Search code examples
pythonartificial-intelligencecs50minesweeper

Why am I getting a "IndexError: list index out of range" error in my CS50AI Minesweeper code?


I'm stuck on my CS50AI Minesweeper project, everything runs fine, but somehow after 10 or 15 movements made by the AI, the program crashes and give me this error:

Traceback (most recent call last):
  File "C:\2-Knowledge\minesweeper\runner.py", line 215, in <module>
    if game.is_mine(move):
       ^^^^^^^^^^^^^^^^^^
  File "C:\2-Knowledge\minesweeper\minesweeper.py", line 53, in is_mine
    return self.board[i][j]
           ~~~~~~~~~~^^^
IndexError: list index out of range

This is my minesweeper.py code (line 53 is highlighted):

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] # THIS IS LINE 53**

    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 i,j if they are 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 cell to the undetermined list if cell is not in safes    
                elif (i,j) not in self.safes:    
                    undetermined.add((i,j))
                print(i,j)
        
        # Add new sentence to KB
        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
        modified = True
        while modified:
            modified = False
            safes = set()
            mines = set()

            for sentence in self.knowledge:
                safes = safes.union(sentence.known_safes())
                mines = mines.union(sentence.known_mines())

            # Mark if cell is safe or mine:
            if safes:
                knowledge_changed = True
                for safe in safes:
                    self.mark_safe(safe)
            if mines:
                knowledge_changed = True
                for mine in mines:
                    self.mark_mine(mine)

            # Update KB removing empty sentences:
            empty = Sentence(set(), 0)

            new_knowledge = []
            for x in self.knowledge:
                if x != empty:
                    new_knowledge.append(x)
            self.knowledge = new_knowledge
            # self.knowledge[:] = [x for x in self.knowledge if x != empty]

#5) add any new sentences to the AI's knowledge base if they can 
# be inferred from existing knowledge
            for sentence_1 in self.knowledge:
                for sentence_2 in self.knowledge:

                    # Ignore when sentences are identical
                    if sentence_1.cells == sentence_2.cells:
                        continue

                    if sentence_1.cells == set() and sentence_1.count > 0:
                        print('Error - sentence with no cells and count created')
                        raise ValueError

                    # Create a new sentence if 1 is subset of 2, and not in KB:
                    if sentence_1.cells.issubset(sentence_2.cells):
                        new_sentence_cells = sentence_2.cells - sentence_1.cells
                        new_sentence_count = sentence_2.count - sentence_1.count

                        new_sentence = Sentence(new_sentence_cells, new_sentence_count)

                        # Add to knowledge if not already in KB:
                        if new_sentence not in self.knowledge:
                            knowledge_changed = True
                            print('New Inferred Knowledge: ', new_sentence, 'from', sentence_1, ' and ', sentence_2)
                            self.knowledge.append(new_sentence)

        # Print out AI current knowledge to terminal:
        print('Current AI KB length: ', len(self.knowledge))
        print('Known Mines: ', self.mines)
        print('Safe Moves Remaining: ', self.safes - self.moves_made)
        print('====================================================')


    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.
        """
        safe_moves = self.safes - self.moves_made
        if safe_moves:
            print('Making a Safe Move! Safe moves available: ', len(safe_moves))
            return random.choice(list(safe_moves))

        # Otherwise no guaranteed safe moves can be made
        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

If someone could help me to know what part of the code is incorrect, I'd really appreciate it.


Solution

  • This is an example where the last line number in the traceback is NOT where the error occurs (e.g. the error is NOT really in the .is_mine() method from the Minesweeper class). That method simply returns the board value at the index (either True or False, where True means there is a mine in the cell). The error tells you that you provided invalid (i,j) values for the board (either negative or i>height or j>width).

    That method is called from runner.py with the move value returned by an earlier call to a MinesweeperAI method (either make_safe_move() on line 186 or make_random_move() on line 188). So, 1 or both of those functions is returning invalid values. Inspect the returned value of move (either with the debugger or by adding print statements). Then work backwards to find the source of the error.