My solutions to Harvard's online course CS50AI, An Introduction to Machine Learning
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

253 lines
8.0 KiB

import itertools
import random
class Minesweeper():
"""
Minesweeper game representation
"""
def __init__(self, height=8, width=8, mines=8):
# Set initial width, height, and number of mines
self.height = height
self.width = width
self.mines = set()
# Initialize an empty field with no mines
self.board = []
for i in range(self.height):
row = []
for j in range(self.width):
row.append(False)
self.board.append(row)
# Add mines randomly
while len(self.mines) != mines:
i = random.randrange(height)
j = random.randrange(width)
if not self.board[i][j]:
self.mines.add((i, j))
self.board[i][j] = True
# At first, player has found no mines
self.mines_found = set()
def print(self):
"""
Prints a text-based representation
of where mines are located.
"""
for i in range(self.height):
print("--" * self.width + "-")
for j in range(self.width):
if self.board[i][j]:
print("|X", end="")
else:
print("| ", end="")
print("|")
print("--" * self.width + "-")
def is_mine(self, cell):
i, j = cell
return self.board[i][j]
def nearby_mines(self, cell):
"""
Returns the number of mines that are
within one row and column of a given cell,
not including the cell itself.
"""
# Keep count of nearby mines
count = 0
# Loop over all cells within one row and column
for i in range(cell[0] - 1, cell[0] + 2):
for j in range(cell[1] - 1, cell[1] + 2):
# Ignore the cell itself
if (i, j) == cell:
continue
# Update count if cell in bounds and is mine
if 0 <= i < self.height and 0 <= j < self.width:
if self.board[i][j]:
count += 1
return count
def won(self):
"""
Checks if all mines have been flagged.
"""
return self.mines_found == self.mines
class Sentence():
"""
Logical statement about a Minesweeper game
A sentence consists of a set of board cells,
and a count of the number of those cells which are mines.
"""
def __init__(self, cells, count):
self.cells = set(cells)
self.count = count
def __eq__(self, other):
return self.cells == other.cells and self.count == other.count
def __str__(self):
return f"{self.cells} = {self.count}"
def known_mines(self):
if self.cells.__len__() == self.count:
return self.cells
return set()
def known_safes(self):
if self.count == 0:
return self.cells
return set()
def mark_mine(self, cell): # Remove cell from set and decrease count by one
if cell in self.cells:
self.cells.remove(cell)
self.count -= 1
def mark_safe(self, cell): # Remove safe cell from cells set
if cell in self.cells:
self.cells.remove(cell)
class MinesweeperAI():
"""
Minesweeper game player
"""
def __init__(self, height=8, width=8):
# Set initial height and width
self.height = height
self.width = width
# Keep track of which cells have been clicked on
self.moves_made = set()
# Keep track of cells known to be safe or mines
self.mines = set()
self.safes = set()
# List of sentences about the game known to be true
self.knowledge = []
def mark_mine(self, cell):
"""
Marks a cell as a mine, and updates all knowledge
to mark that cell as a mine as well.
"""
self.mines.add(cell)
for sentence in self.knowledge:
sentence.mark_mine(cell)
def mark_safe(self, cell):
"""
Marks a cell as safe, and updates all knowledge
to mark that cell as safe as well.
"""
self.safes.add(cell)
for sentence in self.knowledge:
sentence.mark_safe(cell)
def add_knowledge(self, cell, count):
"""
Called when the Minesweeper board tells us, for a given
safe cell, how many neighboring cells have mines in them.
This function should:
1) mark the cell as a move that has been made
2) mark the cell as safe
3) add a new sentence to the AI's knowledge base
based on the value of `cell` and `count`
4) mark any additional cells as safe or as mines
if it can be concluded based on the AI's knowledge base
5) add any new sentences to the AI's knowledge base
if they can be inferred from existing knowledge
"""
self.moves_made.add(cell) #Store information about the cell
self.mark_safe(cell)
surrounding_cells = set()
for i in range(cell[0]-1 if cell[0] - 1 >= 0 else 0, cell[0]+2 if cell[0] + 2 <= self.width else self.width):
for j in range(cell[1]-1 if cell[1] - 1 >= 0 else 0, cell[1]+2 if cell[1] + 2 <= self.height else self.height):
count -= int((i, j) in self.mines)
if (i, j) not in self.safes.union(self.mines):
surrounding_cells.add((i, j))
new_knowledge = Sentence(surrounding_cells, count)
if new_knowledge in self.knowledge:
return
inferred_knowledge = []
knowledge_cpy = self.knowledge.copy()
popped = 0
for i, sentence in enumerate(knowledge_cpy):
if sentence.cells == set():
self.knowledge.pop(i - popped)
popped += 1
continue
if new_knowledge.cells.issubset(sentence.cells):
new_cells = sentence.cells - new_knowledge.cells
new_count = sentence.count - new_knowledge.count
new_sentence = Sentence(new_cells, new_count)
inferred_knowledge.append(Sentence(new_cells, new_count))
elif sentence.cells.issubset(new_knowledge.cells):
new_cells = new_knowledge.cells - sentence.cells
new_count = new_knowledge.count - sentence.count
new_sentence = Sentence(new_cells, new_count)
inferred_knowledge.append(new_sentence)
for i in inferred_knowledge:
if i.known_safes() != set():
for j in i.cells:
self.mark_safe(j)
elif i.known_mines() != set():
for j in i.cells:
self.mark_mine(j)
for i in self.knowledge:
cells = i.cells.copy()
if i.known_safes() != set():
for j in cells:
self.mark_safe(j)
elif i.known_mines() != set():
for j in cells:
self.mark_mine(j)
inferred_knowledge.append(new_knowledge)
for i in inferred_knowledge:
exists = False
for j in self.knowledge:
if i == j:
exists = True
if not exists:
self.knowledge.append(i)
def make_safe_move(self):
available_moves = self.safes - self.moves_made
for s in self.knowledge:
available_moves = available_moves.union(s.known_safes())
if available_moves.__len__() == 0:
return None
return available_moves.pop()
def make_random_move(self):
unavailable_moves = self.moves_made.union(self.mines)
available_moves = set()
for i in range(self.width):
for j in range(self.height):
if (i, j) not in unavailable_moves:
available_moves.add((i, j))
if available_moves.__len__() == 0:
return None
return available_moves.pop()