My CS50AI Minesweeper code causes my PC to run out of resources

Question:

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):
        """
        Returns the set of all cells in self.cells known to be mines.
        """
        if len(self.cells) == self.count and self.count != 0:
            return self.cells
        
        else:
            return set()
    
    def known_safes(self):
        """
        Returns the set of all cells in self.cells known to be safe.
        """
        if self.count == 0:
            return self.cells
        
        else:
            return set()
    
    def mark_mine(self, cell):
        """
        Updates internal knowledge representation given the fact that
        a cell is known to be a mine.
        """
        if cell in self.cells:
            self.cells.remove(cell)
            self.count -= 1
    
    def mark_safe(self, cell):
        """
        Updates internal knowledge representation given the fact that
        a cell is known to be safe.
        """
        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

        self.moves_made.add(cell)

# 2) mark the cell as safe

        self.mark_safe(cell)

# 3) add a new sentence to the AI's knowledge base based
# on the value of `cell` and `count`

        up = cell[1] - 1
        down = cell[1] + 1
        left = cell[0] - 1
        right = cell[0] + 1
        
        undetermined = set()
        mine_counter = 0
        horizontal_boundary = self.width
        vertical_boundary = self.height
        
        for i in range(left, right + 1):
            for j in range(up, down + 1):
                # Ignore cell if it is out of boundaries or if is the cell itself
                if j < 0 or j > vertical_boundary or i < 0 or i > horizontal_boundary or (i,j) == cell:
                    continue
                
                # Augment mine counter if cell is in mines list
                if (i,j) in self.mines:
                    mine_counter += 1
                
                # Add the mine to the undetermined list if cell is not in safes    
                if (i,j) not in self.safes:    
                    undetermined.add((i,j))
        
        new_counter = count - mine_counter
        new_sentence = Sentence(undetermined, new_counter)
        print(f'Move on cell: {cell} has added sentence to knowledge {undetermined} = {new_counter}' )
        self.knowledge.append(new_sentence)

# 4) mark any additional cells as safe or as mines if it
# can be concluded based on the AI's knowledge base

        for sen in self.knowledge:
            if sen.known_mines():
                for cell in sen.known_mines().copy():
                    self.mark_mine(cell)
        
            if sen.known_safes():
                for cell in sen.known_safes().copy():
                    self.mark_safe(cell)

#5) add any new sentences to the AI's knowledge base if they can
# be inferred from existing knowledge

        knowledge_copy = self.knowledge.copy()
        for sentence1 in knowledge_copy:
            for sentence2 in knowledge_copy:
                sen1 = sentence1.cells
                sen2 = sentence2.cells
                count1 = sentence1.count
                count2 = sentence2.count
                if sen2.issubset(sen1) and knowledge_copy.index(sentence2) != knowledge_copy.index(sentence1):
                    for rem in sen2:
                        if rem in sen1:
                            sen1.remove(rem)
                    new_count = count1 - count2
                    another_sentence = Sentence(sen1, new_count)
                    self.knowledge.append(another_sentence)
    
    
    def make_safe_move(self):
        """
        Returns a safe cell to choose on the Minesweeper board.
        The move must be known to be safe, and not already a move
        that has been made.
    
        This function may use the knowledge in self.mines, self.safes
        and self.moves_made, but should not modify any of those values.
        """
        for move in self.safes:
            if move not in self.moves_made and move not in self.mines:
                return move
            else:
                return None
    
    
    def make_random_move(self):
        """
        Returns a move to make on the Minesweeper board.
        Should choose randomly among cells that:
            1) have not already been chosen, and
            2) are not known to be mines
        """
        for i in range(self.height):
            for j in range(self.width):
                move = (i, j)
                if move not in self.moves_made and move not in self.mines:
                    return move
        return None

When I try to run the AI to play this minesweeper code, it runs ok in some moves, but after 6 or more moves, it just crashes and the program exits. I think it is a problem with my PC resources. How can I fix it?

Asked By: Gustavo Velandia

||

Answers:

I assume "out of resources" means your system is out of memory/RAM. If so, that indicates a problem with your AI knowledge base. There are only 64 cells (and 8 mines), so at the very most you would have 56 sentences in the KB. In fact, once your AI is working correctly, the KB will only have sentences with count>0 (so, never more than 4 or 5 sentences).

The Minesweeper project is more complicated that it first appears. You have to get the each step correct before writing the next one. Otherwise, an error early in the procedure propagates throughout the process, and they become very hard to diagnose (speaking from experience on this!).

I modified the posted code to only test the first 4 requirements and played for 3 moves. My observations:

  1. mark moves and mark safe: OK
  2. add new sentences: OK, but see comments
  3. mark any additional cells as safe or mines based on the AI: needs work
  4. make_safe_move() function: not OK
  5. make_random_move() function: not really random

Here is the printed output (modified to get more details).

No known safe moves, AI making random move: (0, 0)
After move on cell: (0, 0) new sentence added to knowledge:
    {(0, 1), (1, 0), (1, 1)} = 0
    Size of KB: 1
    # of safe cells: 4
AI making safe move: (0, 1)
After move on cell: (0, 1) new sentence added to knowledge:
    {(0, 2), (1, 2)} = 0
    Size of KB: 2
    # of safe cells: 6
No known safe moves, AI making random move: (0, 2)
After move on cell: (0, 2) new sentence added to knowledge:
    {(0, 3), (1, 3)} = 1
    Size of KB: 3
    # of safe cells: 6

The 3rd move shows the error in make_safe_move() function. It returns None when there are safe moves available (so you make a random move). Inspect the logic closely and you will see why that happens. That’s 1 problem. It won’t cause a memory error, but your AI will lose games when it shouldn’t.

There is another problem when checking the KB for safe cells or mines. After the first move you add Sentence {(0, 1), (1, 0), (1, 1)} = 0 and correctly determine there the safe cells. However, when you exit the add_knowledge() function, that sentence is still in the KB. If you inspect it, it looks like this: set() = 0. Over time (as you find more safe cells), you will have more and more sentences like that. I suspect this leads to the resource error.

In addition, after you modify the appropriate sentences in the KB (for new safe cells or mines), there is "new knowledge" that may reveal more safe cells or mines. So you have to keep iterating until there are no changes.

Regarding, your make_random_move() function — it isn’t really "random". It starts at cell (0,0), and continues until it finds one that isn’t in moves or mines. So you always get a similar order of "random" cells (the only change is due to mines).

Fix those items before you implemment requirement 5 ("add new sentences to the KB that can be inferred from existing knowledge").

Finally, as @luk2302 noted, SO isn’t the forum for this type of question. There is an ED Forum with filters for each CS50 AI project. It has TONS of Q&A on the minesweeper project. (maybe too many!). 🙂

Answered By: kcw78
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.