This is the seventh day of my participation in the August More text Challenge. For details, see: August More Text Challenge

AI play with mine

Nice to see you again! 😊

Minesweeper is a single player puzzle game, I believe most people have played in the past computer lessons. The goal of the game is to clear cells containing hidden “mines” or bombs, using clues about the number of adjacent mines in each area, without detonating any of them, and win by clearing all of them. Today we will complete this little program in Python and use AI to learn and implement it.

Look at the end of what we’re going to achieve. 👇

Running mine

1. Make sure Python 3.6+ is installed.

2. Install Pygame.

3. Clone the repository:

GitHub: github.com/wanghao221/…

Set the minesweeper. Py

⚓ Minesweeper game said

class Minesweeper() :

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(a)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)

    # Randomly add mines
    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

    # Initially, the player does not find any mines
    self.mines_found = set(a)Copy the code

Output a text-based representation of the location of the mine

def print(self) :
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) :
Copy the code

Returns the number of mines in a row and a column of a given cell, excluding the cell itself.

def nearby_mines(self, cell) :

        Maintain the number of nearby mines
        count = 0

        # Iterates over all cells in a 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 the count if the cell is within the boundary and is a mine
                if 0 <= i < self.height and 0 <= j < self.width:
                    if self.board[i][j]:
                        count += 1
        return count
Copy the code

Check that all mines are marked.

def won(self) :
        return self.mines_found == self.mines
Copy the code

Logic Statements about Minesweeper A sentence consists of a set of board cells and the number of those cells.

class Sentence() :
    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) :
Copy the code

Returns a collection of all cells in self. Cells that are known to be mines.

def known_mines(self) :	
	if len(self.cells) == self.count:
		return self.cells
Copy the code

Returns a collection of all cells in self.Cells that are known to be safe.

def known_safes(self) :      
	if self.count == 0:
		return self.cells
Copy the code

Update the internal knowledge representation given that the cell is known to be a mine.

def mark_mine(self, cell) :       
	if cell in self.cells:
		self.cells.discard(cell)
		self.count -= 1
Copy the code

Update the internal knowledge representation, given that the cells are known to be safe.

def mark_safe(self, cell) :  
        if cell in self.cells:
            self.cells.discard(cell)
Copy the code

Minesweeper

class MinesweeperAI() :
    def __init__(self, height=8, width=8) :
        Set the initial height and width
        self.height = height
        self.width = width
        # Track which cells were clicked
        self.moves_made = set(a)# Track known safe or landmine cells
        self.mines = set()
        self.safes = set(a)# List of sentences about games known to be true
        self.knowledge = []
Copy the code

Mark a cell as a mine and update all knowledge to mark that cell as a mine as well.

def mark_mine(self, cell) :
       self.mines.add(cell)
       
       for sentence in self.knowledge:
           sentence.mark_mine(cell)
Copy the code

Mark a cell as safe and update all knowledge to mark that cell as safe as well.

def mark_safe(self, cell) :  
    self.safes.add(cell)        
        for sentence in self.knowledge:
            sentence.mark_safe(cell)
Copy the code

Use to get all nearby cells

def nearby_cells(self, cell) :
	cells = set(a)for i in range(cell[0] - 1, cell[0] + 2) :for j in range(cell[1] - 1, cell[1] + 2) :if (i, j) == cell:
	                    continue
	
	                if 0 <= i < self.height and 0 <= j < self.width:
	                    cells.add((i, j))
	
	        return cells
Copy the code

Called when the minesweeper board tells us how many adjacent units have mines for a given safety unit. This feature should: 1) Mark the cell as moved 2) mark the cell as safe 3) add a new sentence to the AI knowledge base based on the values of the cell and count 4) If a conclusion can be drawn from the AI knowledge base, If any new sentence can be inferred from the existing knowledge, it is added to the AI’s knowledge base \

def add_knowledge(self, cell, count) : 
        self.moves_made.add(cell)

        # indicates that the cell is safe

        if cell not in self.safes:    
            self.mark_safe(cell)
                    
        # Get all nearby cells

        nearby = self.nearby_cells(cell)       
        nearby -= self.safes | self.moves_made     
        new_sentence = Sentence(nearby, count)
        self.knowledge.append(new_sentence)

        new_safes = set()
        new_mines = set(a)for sentence in self.knowledge:
            
            if len(sentence.cells) == 0:
                self.knowledge.remove(sentence)           
            else:
                tmp_new_safes = sentence.known_safes()
                tmp_new_mines = sentence.known_mines()                
                if type(tmp_new_safes) is set:
                    new_safes |= tmp_new_safes
                
                if type(tmp_new_mines) is set:
                    new_mines |= tmp_new_mines        
        for safe in new_safes:
            self.mark_safe(safe)        
        for mine in new_mines:
            self.mark_mine(mine)
        prev_sentence = new_sentence
        new_inferences = []
        for sentence in self.knowledge:
            if len(sentence.cells) == 0:
                self.knowledge.remove(sentence)
            elif prev_sentence == sentence:
                break
            elif prev_sentence.cells <= sentence.cells:
                inf_cells = sentence.cells - prev_sentence.cells
                inf_count = sentence.count - prev_sentence.count
                new_inferences.append(Sentence(inf_cells, inf_count))
            prev_sentence = sentence
        self.knowledge += new_inferences
    def make_safe_move(self) :
Copy the code

Returns a secure cell to select on the minesweeper board. You must know that the move is safe, not that it has already been made. This function can use the knowledge in self.mines, self.safes, and self.moves_made, but should not modify any of these values.

def make_safe_move(self) : 
	safe_moves = self.safes.copy()
	safe_moves -= self.moves_made
	if len(safe_moves) == 0:
	return None
	return safe_moves.pop()
	def make_random_move(self) :
Copy the code

Returns to moves made on the minesweeper board. Should be randomly selected from the following cells: 1) not selected and 2) not known to be a mine

def make_random_move(self) :
	if len(self.moves_made) == 56:
	    return None
	
	random_move = random.randrange(self.height), random.randrange(self.height)
	
	not_safe_moves = self.moves_made | self.mines
	
	while random_move in not_safe_moves:
	    random_move = random.randrange(self.height), random.randrange(self.height)
	
	return random_move
Copy the code

Set runner. Py to run the program

color

BLACK = (0.0.0)
GRAY = (180.180.180)
WHITE = (255.255.255)
Copy the code

Create a game

pygame.init()
size = width, height = 600.400
screen = pygame.display.set_mode(size)
Copy the code

The font

Fonts can be on your computerC:\Windows\FontsSelect the one you like and copy it to the assets/fonts directory in the project. I use regular script

OPEN_SANS = "assets/fonts/simkai.ttf"
smallFont = pygame.font.Font(OPEN_SANS, 20)
mediumFont = pygame.font.Font(OPEN_SANS, 28)
largeFont = pygame.font.Font(OPEN_SANS, 40)
Copy the code

Calculate panel 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)
Copy the code

Here we only use two images, one of the mine and one of the flag to mark the mine

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))
Copy the code

Create game and AI agents

game = Minesweeper(height=HEIGHT, width=WIDTH, mines=MINES)
ai = MinesweeperAI(height=HEIGHT, width=WIDTH)
Copy the code

Keep track of the cells displayed, the cells marked, and whether they were hit by a mine

revealed = set()
flags = set()
lost = False
Copy the code

Show the game description initially

instructions = True

while True:

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

    screen.fill(BLACK)

    # Display the game description

    if instructions:

        # titles
        title = largeFont.render("Hold | demining sea".True, WHITE)
        titleRect = title.get_rect()
        titleRect.center = ((width / 2), 50)
        screen.blit(title, titleRect)

        # Rules
        rules = [
            "Click on a cell to display it"."Right-click on 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)

        Start game button
        
        buttonRect = pygame.Rect((width / 4), (3 / 4) * height, width / 2.50)
        buttonText = mediumFont.render("Start the game.".True, BLACK)
        buttonTextRect = buttonText.get_rect()
        buttonTextRect.center = buttonRect.center
        pygame.draw.rect(screen, WHITE, buttonRect)
        screen.blit(buttonText, buttonTextRect)

        Check whether the play button is 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
Copy the code

Drawing board

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

        # Draw a rectangle for the 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)

        If needed, add mines, flags or numbers
        
        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)
Copy the code

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 mobile".True, BLACK)
buttonRect = buttonText.get_rect()
buttonRect.center = aiButton.center
pygame.draw.rect(screen, WHITE, aiButton)
screen.blit(buttonText, buttonRect)
Copy the code

The 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)
Copy the code

According to the text

text = "Failure" if lost else "Win" 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()
Copy the code

Check the right click to toggle tags

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()
Copy the code

If you click the AI button, the AI moves

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)
Copy the code

Reset the 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
Copy the code

User-defined actions

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)
Copy the code

Get moving and update your 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()
Copy the code

That’s all for this article

Here is the full source code of the project: download here is a summary of all my original and work source code: GitHub, if you can add some stars to my GitHub repository is better 😊. Also this is I recently just set up the blog: Haiyong. Site, there is no content, put some HTML games, interested in can try, source code can be their own F12 copy, or directly find me to.

I’ve been writing a technical blog for a long time, mostly through Nuggets, which is my tutorial for a Python single-player AI minesweeper game. I like to share technology and happiness through articles. You can visit my blog homepage:Juejin. Cn/user / 204034…To learn more. Hope you like it!

💌 welcomes comments and suggestions in the comments section! 💌

If you do learn something new from this post, like it, bookmark it and share it with your friends. 🤗 Finally, don’t forget ❤ or 📑 for support.