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

4 years ago
  1. import itertools
  2. import random
  3. class Minesweeper():
  4. """
  5. Minesweeper game representation
  6. """
  7. def __init__(self, height=8, width=8, mines=8):
  8. # Set initial width, height, and number of mines
  9. self.height = height
  10. self.width = width
  11. self.mines = set()
  12. # Initialize an empty field with no mines
  13. self.board = []
  14. for i in range(self.height):
  15. row = []
  16. for j in range(self.width):
  17. row.append(False)
  18. self.board.append(row)
  19. # Add mines randomly
  20. while len(self.mines) != mines:
  21. i = random.randrange(height)
  22. j = random.randrange(width)
  23. if not self.board[i][j]:
  24. self.mines.add((i, j))
  25. self.board[i][j] = True
  26. # At first, player has found no mines
  27. self.mines_found = set()
  28. def print(self):
  29. """
  30. Prints a text-based representation
  31. of where mines are located.
  32. """
  33. for i in range(self.height):
  34. print("--" * self.width + "-")
  35. for j in range(self.width):
  36. if self.board[i][j]:
  37. print("|X", end="")
  38. else:
  39. print("| ", end="")
  40. print("|")
  41. print("--" * self.width + "-")
  42. def is_mine(self, cell):
  43. i, j = cell
  44. return self.board[i][j]
  45. def nearby_mines(self, cell):
  46. """
  47. Returns the number of mines that are
  48. within one row and column of a given cell,
  49. not including the cell itself.
  50. """
  51. # Keep count of nearby mines
  52. count = 0
  53. # Loop over all cells within one row and column
  54. for i in range(cell[0] - 1, cell[0] + 2):
  55. for j in range(cell[1] - 1, cell[1] + 2):
  56. # Ignore the cell itself
  57. if (i, j) == cell:
  58. continue
  59. # Update count if cell in bounds and is mine
  60. if 0 <= i < self.height and 0 <= j < self.width:
  61. if self.board[i][j]:
  62. count += 1
  63. return count
  64. def won(self):
  65. """
  66. Checks if all mines have been flagged.
  67. """
  68. return self.mines_found == self.mines
  69. class Sentence():
  70. """
  71. Logical statement about a Minesweeper game
  72. A sentence consists of a set of board cells,
  73. and a count of the number of those cells which are mines.
  74. """
  75. def __init__(self, cells, count):
  76. self.cells = set(cells)
  77. self.count = count
  78. def __eq__(self, other):
  79. return self.cells == other.cells and self.count == other.count
  80. def __str__(self):
  81. return f"{self.cells} = {self.count}"
  82. def known_mines(self):
  83. if self.cells.__len__() == self.count:
  84. return self.cells
  85. return set()
  86. def known_safes(self):
  87. if self.count == 0:
  88. return self.cells
  89. return set()
  90. def mark_mine(self, cell): # Remove cell from set and decrease count by one
  91. if cell in self.cells:
  92. self.cells.remove(cell)
  93. self.count -= 1
  94. def mark_safe(self, cell): # Remove safe cell from cells set
  95. if cell in self.cells:
  96. self.cells.remove(cell)
  97. class MinesweeperAI():
  98. """
  99. Minesweeper game player
  100. """
  101. def __init__(self, height=8, width=8):
  102. # Set initial height and width
  103. self.height = height
  104. self.width = width
  105. # Keep track of which cells have been clicked on
  106. self.moves_made = set()
  107. # Keep track of cells known to be safe or mines
  108. self.mines = set()
  109. self.safes = set()
  110. # List of sentences about the game known to be true
  111. self.knowledge = []
  112. def mark_mine(self, cell):
  113. """
  114. Marks a cell as a mine, and updates all knowledge
  115. to mark that cell as a mine as well.
  116. """
  117. self.mines.add(cell)
  118. for sentence in self.knowledge:
  119. sentence.mark_mine(cell)
  120. def mark_safe(self, cell):
  121. """
  122. Marks a cell as safe, and updates all knowledge
  123. to mark that cell as safe as well.
  124. """
  125. self.safes.add(cell)
  126. for sentence in self.knowledge:
  127. sentence.mark_safe(cell)
  128. def add_knowledge(self, cell, count):
  129. """
  130. Called when the Minesweeper board tells us, for a given
  131. safe cell, how many neighboring cells have mines in them.
  132. This function should:
  133. 1) mark the cell as a move that has been made
  134. 2) mark the cell as safe
  135. 3) add a new sentence to the AI's knowledge base
  136. based on the value of `cell` and `count`
  137. 4) mark any additional cells as safe or as mines
  138. if it can be concluded based on the AI's knowledge base
  139. 5) add any new sentences to the AI's knowledge base
  140. if they can be inferred from existing knowledge
  141. """
  142. self.moves_made.add(cell) #Store information about the cell
  143. self.mark_safe(cell)
  144. surrounding_cells = set()
  145. 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):
  146. 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):
  147. count -= int((i, j) in self.mines)
  148. if (i, j) not in self.safes.union(self.mines):
  149. surrounding_cells.add((i, j))
  150. new_knowledge = Sentence(surrounding_cells, count)
  151. if new_knowledge in self.knowledge:
  152. return
  153. inferred_knowledge = []
  154. knowledge_cpy = self.knowledge.copy()
  155. popped = 0
  156. for i, sentence in enumerate(knowledge_cpy):
  157. if sentence.cells == set():
  158. self.knowledge.pop(i - popped)
  159. popped += 1
  160. continue
  161. if new_knowledge.cells.issubset(sentence.cells):
  162. new_cells = sentence.cells - new_knowledge.cells
  163. new_count = sentence.count - new_knowledge.count
  164. new_sentence = Sentence(new_cells, new_count)
  165. inferred_knowledge.append(Sentence(new_cells, new_count))
  166. elif sentence.cells.issubset(new_knowledge.cells):
  167. new_cells = new_knowledge.cells - sentence.cells
  168. new_count = new_knowledge.count - sentence.count
  169. new_sentence = Sentence(new_cells, new_count)
  170. inferred_knowledge.append(new_sentence)
  171. for i in inferred_knowledge:
  172. if i.known_safes() != set():
  173. for j in i.cells:
  174. self.mark_safe(j)
  175. elif i.known_mines() != set():
  176. for j in i.cells:
  177. self.mark_mine(j)
  178. for i in self.knowledge:
  179. cells = i.cells.copy()
  180. if i.known_safes() != set():
  181. for j in cells:
  182. self.mark_safe(j)
  183. elif i.known_mines() != set():
  184. for j in cells:
  185. self.mark_mine(j)
  186. inferred_knowledge.append(new_knowledge)
  187. for i in inferred_knowledge:
  188. exists = False
  189. for j in self.knowledge:
  190. if i == j:
  191. exists = True
  192. if not exists:
  193. self.knowledge.append(i)
  194. def make_safe_move(self):
  195. available_moves = self.safes - self.moves_made
  196. for s in self.knowledge:
  197. available_moves = available_moves.union(s.known_safes())
  198. if available_moves.__len__() == 0:
  199. return None
  200. return available_moves.pop()
  201. def make_random_move(self):
  202. unavailable_moves = self.moves_made.union(self.mines)
  203. available_moves = set()
  204. for i in range(self.width):
  205. for j in range(self.height):
  206. if (i, j) not in unavailable_moves:
  207. available_moves.add((i, j))
  208. if available_moves.__len__() == 0:
  209. return None
  210. return available_moves.pop()