Advertisement
jules0707

Minesweeper.py

Oct 10th, 2023 (edited)
6
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.10 KB | None | 0 0
  1. from itertools import combinations
  2. import random
  3. import copy
  4.  
  5.  
  6. class Minesweeper():
  7.     """
  8.    Minesweeper game representation
  9.    """
  10.  
  11.     def __init__(self, height=8, width=8, mines=8):
  12.  
  13.         # Set initial width, height, and number of mines
  14.         self.height = height
  15.         self.width = width
  16.         self.mines = set()
  17.  
  18.         # Initialize an empty field with no mines
  19.         self.board = []
  20.         for i in range(self.height):
  21.             row = []
  22.             for j in range(self.width):
  23.                 row.append(False)
  24.             self.board.append(row)
  25.  
  26.         # Add mines randomly
  27.         while len(self.mines) != mines:
  28.             i = random.randrange(height)
  29.             j = random.randrange(width)
  30.             if not self.board[i][j]:
  31.                 self.mines.add((i, j))
  32.                 self.board[i][j] = True
  33.  
  34.         # At first, player has found no mines
  35.         self.mines_found = set()
  36.  
  37.     def print(self):
  38.         """
  39.        Prints a text-based representation
  40.        of where mines are located.
  41.        """
  42.         for i in range(self.height):
  43.             print("--" * self.width + "-")
  44.             for j in range(self.width):
  45.                 if self.board[i][j]:
  46.                     print("|X", end="")
  47.                 else:
  48.                     print("| ", end="")
  49.             print("|")
  50.         print("--" * self.width + "-")
  51.  
  52.     def is_mine(self, cell):
  53.         i, j = cell
  54.         return self.board[i][j]
  55.  
  56.     def nearby_mines(self, cell):
  57.         """
  58.        Returns the number of mines that are
  59.        within one row and column of a given cell,
  60.        not including the cell itself.
  61.        """
  62.  
  63.         # Keep count of nearby mines
  64.         count = 0
  65.  
  66.         # Loop over all cells within one row and column
  67.         for i in range(cell[0] - 1, cell[0] + 2):
  68.             for j in range(cell[1] - 1, cell[1] + 2):
  69.  
  70.                 # Ignore the cell itself
  71.                 if (i, j) == cell:
  72.                     continue
  73.  
  74.                 # Update count if cell in bounds and is mine
  75.                 if 0 <= i < self.height and 0 <= j < self.width:
  76.                     if self.board[i][j]:
  77.                         count += 1
  78.  
  79.         return count
  80.  
  81.     def won(self):
  82.         """
  83.        Checks if all mines have been flagged.
  84.        """
  85.         return self.mines_found == self.mines
  86.  
  87.  
  88. class Sentence():
  89.     """
  90.    Logical statement about a Minesweeper game
  91.    A sentence consists of a set of board cells,
  92.    and a count of the number of those cells which are mines.
  93.    """
  94.  
  95.     def __init__(self, cells, count):
  96.         self.cells = set(cells)
  97.         self.count = count
  98.  
  99.     def __eq__(self, other):
  100.         return self.cells == other.cells and self.count == other.count
  101.  
  102.     def __str__(self):
  103.         return f"{self.cells} = {self.count}"
  104.  
  105.     def known_mines(self):
  106.         """
  107.        Returns the set of all cells in self.cells known to be mines.
  108.        From the spex: Any time the number of cells is equal to the count,
  109.        we know that all of that sentence’s cells must be mines.
  110.        """
  111.         return self.cells if len(self.cells) == self.count else None
  112.  
  113.     def known_safes(self):
  114.         """
  115.        Returns the set of all cells in self.cells known to be safe.
  116.        From the spex: any time we have a sentence whose count is 0,
  117.        we know that all of that sentence’s cells must be safe.
  118.        """
  119.         return self.cells if self.count == 0 else None
  120.  
  121.     def mark_mine(self, cell):
  122.         """
  123.        Updates internal knowledge representation given the fact that
  124.        a cell is known to be a mine.
  125.        """
  126.         if cell in self.cells:
  127.             trans = copy.deepcopy(self.cells)
  128.             trans.discard(cell)
  129.             self.cells = trans
  130.             self.count -= 1
  131.  
  132.     def mark_safe(self, cell):
  133.         """
  134.        Updates internal knowledge representation given the fact that
  135.        a cell is known to be safe.
  136.        """
  137.         if cell in self.cells:
  138.             trans = copy.deepcopy(self.cells)
  139.             trans.discard(cell)
  140.             self.cells = trans
  141.  
  142.  
  143. class MinesweeperAI():
  144.     """
  145.    Minesweeper game player
  146.    """
  147.  
  148.     def __init__(self, height=8, width=8):
  149.  
  150.         # Set initial height and width
  151.         self.height = height
  152.         self.width = width
  153.  
  154.         # Keep track of which cells have been clicked on
  155.         self.moves_made = set()
  156.  
  157.         # Keep track of cells known to be safe or mines
  158.         self.mines = set()
  159.         self.safes = set()
  160.  
  161.         # List of sentences about the game known to be true
  162.         self.knowledge = []
  163.  
  164.         # A change marker to knowledge
  165.         self.KB_has_changed = False
  166.  
  167.         # the empty sentence
  168.         self.empty = Sentence(set(), 0)
  169.  
  170.         # the list of all possible moves on the grid
  171.         self.all_possible_moves = self.all_possible_moves()
  172.  
  173.     def mark_mine(self, cell):
  174.         """
  175.        Marks a cell as a mine, and updates all knowledge
  176.        to mark that cell as a mine as well.
  177.        """
  178.         if cell not in self.mines:
  179.             self.mines.add(cell)
  180.             self.KB_has_changed = True
  181.             for s in self.knowledge:
  182.                 Sentence.mark_mine(s, cell)
  183.         else:
  184.              self.KB_has_changed = False
  185.  
  186.     def mark_safe(self, cell):
  187.         """
  188.        Marks a cell as safe, and updates all knowledge
  189.        to mark that cell as safe as well.
  190.        """
  191.         if cell not in self.mines:
  192.             self.safes.add(cell)
  193.             self.KB_has_changed = True
  194.             for s in self.knowledge:
  195.                 Sentence.mark_safe(s, cell)
  196.         else:
  197.              self.KB_has_changed = False
  198.  
  199.     def add_knowledge(self, cell, count):
  200.         """
  201.        Called when the Minesweeper board tells us, for a given
  202.        safe cell, how many neighboring cells have mines in them.
  203.        """
  204.         """
  205.        1) mark the cell as a move that has been made
  206.        """
  207.         self.moves_made.add(cell)
  208.  
  209.         """
  210.        2) mark the cell as safe
  211.        """
  212.         self.mark_safe(cell)
  213.  
  214.         """
  215.        3) add a new sentence to the AI's knowledge base
  216.           based on the value of `cell` and `count`
  217.           From spex: only include neigbouring'cells whose state
  218.           is still undetermined ie. neither known mines nor known safes
  219.        """
  220.         self.add_new_sentence(cell, count)
  221.  
  222.         """
  223.        4) mark any additional cells as safe or as mines
  224.           if it can be concluded based on the AI's knowledge base
  225.        """
  226.         self.mark_additional_cells()
  227.  
  228.         """
  229.        5) add any new sentences to the AI's knowledge base
  230.        if they can be inferred from existing knowledge
  231.        """
  232.         self.add_inferred_sentence()
  233.  
  234.         """
  235.        6) Loops marking additional cells and infering new sentences
  236.        """
  237.         # Has the knowledge changed since our new sentence inclusion?
  238.         while self.KB_has_changed:
  239.             self.KB_has_changed = False
  240.             self.mark_additional_cells()
  241.             self.add_inferred_sentence()
  242.  
  243.     def make_safe_move(self):
  244.         """
  245.        Returns a safe cell to choose on the Minesweeper board.
  246.        The move must be known to be safe, and not already a move
  247.        that has been made.
  248.  
  249.        This function may use the knowledge in self.mines, self.safes
  250.        and self.moves_made, but should not modify any of those values.
  251.        """
  252.         safe_moves = self.safes - self.moves_made
  253.         if len(safe_moves) > 0:
  254.             return safe_moves.pop()
  255.         else:
  256.             return None
  257.  
  258.     def make_random_move(self):
  259.         """
  260.        Returns a move to make on the Minesweeper board.
  261.        Should choose randomly among cells that:
  262.            1) have not already been chosen, and
  263.            2) are not known to be mines
  264.        """
  265.         random_move = self.all_possible_moves - self.moves_made - self.mines
  266.  
  267.         if len(random_move) > 0:
  268.             return random_move.pop()
  269.         else:
  270.             return None
  271.  
  272.     """
  273.    ========================================================================
  274.    ------------------------------  UTILITY --------------------------------
  275.    ========================================================================
  276.    """
  277.  
  278.     """
  279.    -------------------------------------------------------------------------------
  280.    3a) Takes a cell and a count, find all neighbours, only keep undetermined ones,
  281.    adjust count, creates a new sentence and adds it to knowledge
  282.    -------------------------------------------------------------------------------
  283.    """
  284.  
  285.     def add_new_sentence(self, cell, count):
  286.  
  287.         # get all neighbors omitting cell itself
  288.         neighbors = self.neighbors(cell)
  289.  
  290.         # creates a new sentence by removing known safes and mines
  291.         # marks potential new found mines and safes, and updates knowledge base
  292.         sentence = self.clean_up(neighbors, count)
  293.  
  294.         # add sentence to knowledge only if sentence is not none and not already known
  295.         if sentence:
  296.             self.add_if_new(sentence)
  297.  
  298.     """
  299.    3b) Adds new sentence to knowledge if not empty or already there
  300.    """
  301.  
  302.     def add_if_new(self, sentence):
  303.         if len(self.knowledge) == 0:
  304.             self.KB_has_changed = True
  305.             self.knowledge.append(sentence)
  306.         elif not self.in_knowledge(sentence):
  307.             self.KB_has_changed = True
  308.             self.knowledge.append(sentence)
  309.  
  310.     """
  311.    3c) Append sentence to knowledge if not already present
  312.    """
  313.  
  314.     def in_knowledge(self, sentence):
  315.         return any(list(filter(lambda s: sentence.__eq__(s), self.knowledge)))
  316.  
  317.     """
  318.    ---------------------------------------------
  319.    4a) Check knowledge for any new mines or safes
  320.    ---------------------------------------------
  321.    """
  322.  
  323.     def mark_additional_cells(self):
  324.         for s in self.knowledge:
  325.             new_mines = Sentence.known_mines(s)
  326.             new_safes = Sentence.known_safes(s)
  327.             if new_mines:
  328.                 self.KB_has_changed = True
  329.                 self.mark_mines(new_mines)
  330.             elif new_safes:  # and new safes
  331.                 self.KB_has_changed = True
  332.                 self.mark_safes(new_safes)
  333.  
  334.     """
  335.    ----------------------------
  336.    4b) Marks all cells as safes
  337.    ----------------------------
  338.    """
  339.  
  340.     def mark_safes(self, new_safes):
  341.         for a in new_safes:
  342.             if a not in self.safes:
  343.                 self.mark_safe(a)
  344.  
  345.     """
  346.    4c) Marks all cells as mines
  347.    """
  348.  
  349.     def mark_mines(self, new_mines):
  350.         for m in new_mines:
  351.             if m not in self.mines:  # we found new unlisted mines
  352.                 self.mark_mine(m)
  353.  
  354.     """
  355.    -----------------------------------------------------------------------
  356.    5a) Infers new knowledge based on set of cells inclusion into other set
  357.    -----------------------------------------------------------------------
  358.    """
  359.  
  360.     def add_inferred_sentence(self):
  361.         pairs = self.unique_pairs()
  362.         if pairs:
  363.             for (s1, s2) in pairs:
  364.                 # adds knowledfge if s1 is a proper subset of s2
  365.                 if s1.cells < s2.cells:
  366.                     cells = s2.cells - s1.cells
  367.                     count = s2.count - s1.count
  368.                     # sent1 = Sentence(cells, count)
  369.                     sent1 = self.clean_up(cells, count)
  370.                     # self.KB_has_changed = True
  371.                     self.add_if_new(sent1)
  372.                 # or vice versa
  373.                 elif s2.cells < s1.cells:
  374.                     cells = s1.cells - s2.cells
  375.                     count = s1.count - s2.count
  376.                     # sent2 = Sentence(cells, count)
  377.                     sent2 = self.clean_up(cells, count)
  378.                     # self.KB_has_changed = True
  379.                     self.add_if_new(sent2)
  380.  
  381.     """
  382.    ----------------------------------------------
  383.    5b) finds unique pairs of sentences amongst KB
  384.    ----------------------------------------------
  385.    """
  386.  
  387.     def unique_pairs(self):
  388.         return [(s1, s2) for s1, s2
  389.                 in combinations(filter(lambda s: self.is_not_empty(s), self.knowledge), 2)
  390.                 if not self.are_identicals(s1, s2)]
  391.  
  392.     """
  393.    ==============================
  394.    GENERAL MULTIPURPOSE UTILITIES
  395.    ==============================
  396.    """
  397.    
  398.     def are_identicals(self, s1, s2):
  399.         return s1.__eq__(s2)
  400.  
  401.     def is_not_empty(self, s):
  402.         return not s.__eq__(self.empty)
  403.  
  404.     def neighbors(self, cell):
  405.         neighbors = set()
  406.         # Loop over all cells within one row and column
  407.         for i in range(cell[0] - 1, cell[0] + 2):
  408.             for j in range(cell[1] - 1, cell[1] + 2):
  409.  
  410.                 # Ignore the cell itself
  411.                 if (i, j) == cell:
  412.                     continue
  413.  
  414.                 # Update count if cell in bounds and is mine
  415.                 if 0 <= i < self.height and 0 <= j < self.width:
  416.                     neighbors.add((i, j))
  417.         return neighbors
  418.  
  419.  
  420.     """
  421.    return sentence only including cells whose state is still undetermined
  422.    """
  423.  
  424.     def clean_up(self, cells, count):
  425.         # Remove known safes and mines
  426.         trans_cells = copy.deepcopy(cells)
  427.         for c in cells:
  428.             if c in self.safes:
  429.                 trans_cells.discard(c)
  430.             elif c in self.mines:
  431.                 trans_cells.discard(c)
  432.                 count -= 1
  433.         # search for new safes and mines
  434.         # update sets and knowledge
  435.         if trans_cells:
  436.             if count == 0:
  437.                 self.mark_safes(trans_cells)
  438.                 return None
  439.             elif len(trans_cells) == count:
  440.                 self.mark_mines(trans_cells)
  441.                 return None
  442.             else:
  443.                 return Sentence(trans_cells, count)
  444.         return None
  445.  
  446.    
  447.     def all_possible_moves(self):
  448.         all_moves = set()
  449.         for i in range(self.height):
  450.             for j in range(self.width):
  451.                 all_moves.add((i, j))
  452.         return all_moves
  453.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement