-4

I've tried implementing an AI minesweeper for the past few days. My code runs for a 2-3 turns and then crashes.I can't find the error but i'm guessing it must be on the add_knowledge function I'm relatively new in Python Class Objects, but in general, i think the logic i used is valid. Here is the code:

import itertools
import random
import copy

class Minesweeper():
    """
    Minesweeper game representation
    """

    def __init__(self, height=4, width=4, mines=2):

        # 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 --to be implemented
        """
        if len(self.cells) == self.count and self.count != 0:
            return self.cells
        else:
            mines = set()
        return mines
   

    def known_safes(self):
        """
        Returns the set of all cells in self.cells known to be safe -- to be implemented
        """
        if self.count == 0:
            return self.cells
        else:
            safes = set()
        return safes

    def mark_mine(self, cell):
        """
        Updates internal knowledge representation given the fact that
        a cell is known to be a mine. -- to be implemented
        """
        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. -- to be implemented
        """
        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
               -- to be implemented
        """
        self.moves_made.add(cell)
        self.mark_safe(cell)

        neighbors = set()
   
        for k in (cell[0]-1, cell[0]+2):
            for m in (cell[1]-1, cell[1]+2):
                if 0<=k < self.height and 0<=m < self.width:
                    if (k,m) not in self.safes:
                        if (k,m) not in self.mines:
                            if (k,m) == cell:
                                continue
                            neighbors.add((k,m))

        neighbors_count = count - len(neighbors.intersection(self.mines))
        if neighbors_count == 0:
            for cell in neighbors:
                self.mark_safe(cell)
        if neighbors_count == len(neighbors):
                for cell in neighbors:
                    self.mark_mine(cell)
        s = Sentence(neighbors, neighbors_count)
        self.knowledge.append(s)

        for sentence in self.knowledge:
            if sentence.known_mines():
                for cell in sentence.known_mines().copy():
                    self.mark_mine(cell)
            if sentence.known_safes():
                for cell in sentence.known_safes().copy():
                    self.mark_safe(cell)
            if sentence == s:
                continue
            a = list((sentence.cells).difference(s.cells))
            b = list((s.cells).intersection(sentence.cells))
            c = (sentence.count) - (s.count)
            if (s.cells).issubset(sentence.cells): 
                if c>0:
                    if s.count or sentence.count==0 and b is not None:
                        for cell in b:
                            self.mark_safe(cell)
                    else:
                        self.knowledge.append(Sentence(a,c))


                   

    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 = []
        for cell in self.safes:
            if cell not in self.moves_made:
                safe_moves.append(cell)
                return random.choice(safe_moves)
        if safe_moves is None:
            print("HHH")
            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
        """
        possible_moves = []
        for f in range(0, self.height-1):
            for g in range(0, self.width-1):
                cell = (f,g)
                if cell not in (self.moves_made and self.mines):
                    possible_moves.append(cell)
        if len(possible_moves) != 0:
            return random.choice(possible_moves)
        else:
            return None

This is the runner.py that launches the program:

import pygame
import sys
import time

from minesweeper import Minesweeper, MinesweeperAI

HEIGHT = 4
WIDTH = 4
MINES = 2

# Colors
BLACK = (0, 0, 0)
GRAY = (180, 180, 180)
WHITE = (255, 255, 255)

# Create game
pygame.init()
size = width, height = 600, 400
screen = pygame.display.set_mode(size)

# Fonts
OPEN_SANS = "assets/fonts/OpenSans-Regular.ttf"
smallFont = pygame.font.Font(OPEN_SANS, 20)
mediumFont = pygame.font.Font(OPEN_SANS, 28)
largeFont = pygame.font.Font(OPEN_SANS, 40)

# Compute board size
BOARD_PADDING = 20
board_width = ((2 / 3) * width) - (BOARD_PADDING * 2)
board_height = height - (BOARD_PADDING * 2)
cell_size = int(min(board_width / WIDTH, board_height / HEIGHT))
board_origin = (BOARD_PADDING, BOARD_PADDING)

# Add images
flag = pygame.image.load("assets/images/flag.png")
flag = pygame.transform.scale(flag, (cell_size, cell_size))
mine = pygame.image.load("assets/images/mine.png")
mine = pygame.transform.scale(mine, (cell_size, cell_size))

# Create game and AI agent
game = Minesweeper(height=HEIGHT, width=WIDTH, mines=MINES)
ai = MinesweeperAI(height=HEIGHT, width=WIDTH)

# Keep track of revealed cells, flagged cells, and if a mine was hit
revealed = set()
flags = set()
lost = False

# Show instructions initially
instructions = True

while True:

    # Check if game quit
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

    screen.fill(BLACK)

    # Show game instructions
    if instructions:

        # Title
        title = largeFont.render("Play Minesweeper", True, WHITE)
        titleRect = title.get_rect()
        titleRect.center = ((width / 2), 50)
        screen.blit(title, titleRect)

        # Rules
        rules = [
            "Click a cell to reveal it.",
            "Right-click a cell to mark it as a mine.",
            "Mark all mines successfully to win!"
        ]
        for i, rule in enumerate(rules):
            line = smallFont.render(rule, True, WHITE)
            lineRect = line.get_rect()
            lineRect.center = ((width / 2), 150 + 30 * i)
            screen.blit(line, lineRect)

        # Play game button
        buttonRect = pygame.Rect((width / 4), (3 / 4) * height, width / 2, 50)
        buttonText = mediumFont.render("Play Game", True, BLACK)
        buttonTextRect = buttonText.get_rect()
        buttonTextRect.center = buttonRect.center
        pygame.draw.rect(screen, WHITE, buttonRect)
        screen.blit(buttonText, buttonTextRect)

        # Check if play button clicked
        click, _, _ = pygame.mouse.get_pressed()
        if click == 1:
            mouse = pygame.mouse.get_pos()
            if buttonRect.collidepoint(mouse):
                instructions = False
                time.sleep(0.3)

        pygame.display.flip()
        continue

    # Draw board
    cells = []
    for i in range(HEIGHT):
        row = []
        for j in range(WIDTH):

            # Draw rectangle for cell
            rect = pygame.Rect(
                board_origin[0] + j * cell_size,
                board_origin[1] + i * cell_size,
                cell_size, cell_size
            )
            pygame.draw.rect(screen, GRAY, rect)
            pygame.draw.rect(screen, WHITE, rect, 3)

            # Add a mine, flag, or number if needed
            if game.is_mine((i, j)) and lost:
                screen.blit(mine, rect)
            elif (i, j) in flags:
                screen.blit(flag, rect)
            elif (i, j) in revealed:
                neighbors = smallFont.render(
                    str(game.nearby_mines((i, j))),
                    True, BLACK
                )
                neighborsTextRect = neighbors.get_rect()
                neighborsTextRect.center = rect.center
                screen.blit(neighbors, neighborsTextRect)

            row.append(rect)
        cells.append(row)

    # AI Move button
    aiButton = pygame.Rect(
        (2 / 3) * width + BOARD_PADDING, (1 / 3) * height - 50,
        (width / 3) - BOARD_PADDING * 2, 50
    )
    buttonText = mediumFont.render("AI Move", True, BLACK)
    buttonRect = buttonText.get_rect()
    buttonRect.center = aiButton.center
    pygame.draw.rect(screen, WHITE, aiButton)
    screen.blit(buttonText, buttonRect)

    # Reset button
    resetButton = pygame.Rect(
        (2 / 3) * width + BOARD_PADDING, (1 / 3) * height + 20,
        (width / 3) - BOARD_PADDING * 2, 50
    )
    buttonText = mediumFont.render("Reset", True, BLACK)
    buttonRect = buttonText.get_rect()
    buttonRect.center = resetButton.center
    pygame.draw.rect(screen, WHITE, resetButton)
    screen.blit(buttonText, buttonRect)

    # Display text
    text = "Lost" if lost else "Won" if game.mines == flags else ""
    text = mediumFont.render(text, True, WHITE)
    textRect = text.get_rect()
    textRect.center = ((5 / 6) * width, (2 / 3) * height)
    screen.blit(text, textRect)

    move = None

    left, _, right = pygame.mouse.get_pressed()

    # Check for a right-click to toggle flagging
    if right == 1 and not lost:
        mouse = pygame.mouse.get_pos()
        for i in range(HEIGHT):
            for j in range(WIDTH):
                if cells[i][j].collidepoint(mouse) and (i, j) not in revealed:
                    if (i, j) in flags:
                        flags.remove((i, j))
                    else:
                        flags.add((i, j))
                    time.sleep(0.2)

    elif left == 1:
        mouse = pygame.mouse.get_pos()

        # If AI button clicked, make an AI move
        if aiButton.collidepoint(mouse) and not lost:
            move = ai.make_safe_move()
            if move is None:
                move = ai.make_random_move()
                if move is None:
                    flags = ai.mines.copy()
                    print("No moves left to make.")
                else:
                    print("No known safe moves, AI making random move.")
            else:
                print("AI making safe move.")
            time.sleep(0.2)

        # Reset game state
        elif resetButton.collidepoint(mouse):
            game = Minesweeper(height=HEIGHT, width=WIDTH, mines=MINES)
            ai = MinesweeperAI(height=HEIGHT, width=WIDTH)
            revealed = set()
            flags = set()
            lost = False
            continue

        # User-made move
        elif not lost:
            for i in range(HEIGHT):
                for j in range(WIDTH):
                    if (cells[i][j].collidepoint(mouse)
                            and (i, j) not in flags
                            and (i, j) not in revealed):
                        move = (i, j)

    # Make move and update AI knowledge
    if move:
        if game.is_mine(move):
            lost = True
        else:
            nearby = game.nearby_mines(move)
            revealed.add(move)
            ai.add_knowledge(move, nearby)

    pygame.display.flip()

How to fix this?

dgsak
  • 1
  • 1

1 Answers1

0

After you posted the remaining code, I tried out your program and I indeed had issues with the game getting stuck in what appeared to be an endless loop. After placing temporary "print" statements into the program, I zeroed in on the block of code and specific statement that was causing the endless loop condition. The issue resides within your "add_knowledge" function within the "for" loop processing the sentences.

        for sentence in self.knowledge:
            print("sentence:", sentence)
            if sentence.known_mines():
                for cell in sentence.known_mines().copy():
                    self.mark_mine(cell)
            if sentence.known_safes():
                for cell in sentence.known_safes().copy():
                    self.mark_safe(cell)
            if sentence == s:
                continue
            a = list((sentence.cells).difference(s.cells))
            b = list((s.cells).intersection(sentence.cells))
            c = (sentence.count) - (s.count)
            print("self knowledge looping:", len(self.knowledge))
            if (s.cells).issubset(sentence.cells): 
                if c>0:
                    if s.count or sentence.count==0 and b is not None:
                        for cell in b:
                            self.mark_safe(cell)
                    else:
                        self.knowledge.append(Sentence(a,c))    # This is the line of code causing the endless loop

I am not sure of what type of logic is being attempted here, but possibly a test is failing and falling through to this "else" clause. When I watched the terminal output while selecting cells, the appending of new sentences took off rapidly.

sentence: {(2, 3), (2, 0)} = 1
self knowledge looping: 111770
^Csentence: {(2, 3), (2, 0)} = 1
Traceback (most recent call last):
  File "runner.py", line 220, in <module>
    ai.add_knowledge(move, nearby)

As you can see, the number of sentence elements reached into the hundreds of thousands.

As I noted, I was not sure what was being attempted there with the appending of new sentences, but when I temporarily deactivated that line of code and added a "pass" directive, the game program seemed to behave.

            if (s.cells).issubset(sentence.cells): 
                if c>0:
                    if s.count or sentence.count==0 and b is not None:
                        for cell in b:
                            self.mark_safe(cell)
                    else:
                        pass
                        #self.knowledge.append(Sentence(a,c))    # This is the offending line of code

I was able to successfully play the game through to its conclusion.

Minesweeper Sample

You know better what you are trying to accomplish with that block of code, but you might try out the deactivation and see how it affects the game.

NoDakker
  • 3,390
  • 1
  • 10
  • 11
  • The test that i tried to implement within those lines: ``` if c>0: if s.count or sentence.count==0 and b is not None: for cell in b: self.mark_safe(cell) ``` is the equivalent of "check if a difference in count of elements(ie neighboring mines) exists between the two sets, then check if one of the two counts is equal to zero(ie no neighboring mines) and if that's true and the two set of cells being compared, have an intersection(shared cell) , mark that whole intersection as a safe.I might have written it wrong. – dgsak Sep 06 '22 at 18:48
  • Also, i did use the pass solution and it still crashed for some reason.Plus, your screenshot has a winning board even though a cell is not opened, is that correct? – dgsak Sep 06 '22 at 18:50
  • Regarding the win without all of the cells clicked, your code indicated that as a win when I tested out adding the "pass". If all of the cells need to be clicked, you may need to change the "win" test I guess. Best of luck on your game. – NoDakker Sep 06 '22 at 20:14