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()
|