Python game development, pygame module, python implementation of minesweeping games

Posted by adeelzia on Sat, 20 Nov 2021 18:14:14 +0100

preface

Today I'll share with you a minesweeping game. There's no more nonsense. Let's start happily~

Effect display

development tool

Python version: 3.6.4

Related modules:

pygame module;

And some python built-in modules.

Environment construction

Install Python and add it to the environment variable. pip can install the relevant modules required.

Principle introduction

Here I briefly introduce the implementation idea of the game.

I believe you are no stranger to minesweeping, a classic little game of windows. Its rules are very simple:

The number in the upper left corner of the game interface represents the number of Mines buried in all squares, and the upper right corner is a timer. All you have to do is find all the thunder in the square according to the prompt.

So what's the hint? At the beginning of the game, you need to click a square, like this:

The number above represents the number of Mines buried in the Jiugong grid centered on this number. So if you have a bad character and point to ray at the beginning, the game will end directly.

ok, after understanding the rules of the game, we can start writing code. First, initialize the game:

# Game initialization
pygame.init()
screen = pygame.display.set_mode(cfg.SCREENSIZE)
pygame.display.set_caption('mine sweeper - ilove-python')
Copy code

Then import the required fonts, pictures and music:

# Import all pictures
images = {}
for key, value in cfg.IMAGE_PATHS.items():
    if key in ['face_fail', 'face_normal', 'face_success']:
        image = pygame.image.load(value)
        images[key] = pygame.transform.smoothscale(image, (int(cfg.GRIDSIZE*1.25), int(cfg.GRIDSIZE*1.25)))
    else:
        image = pygame.image.load(value).convert()
        images[key] = pygame.transform.smoothscale(image, (cfg.GRIDSIZE, cfg.GRIDSIZE))
# Load font
font = pygame.font.Font(cfg.FONT_PATH, cfg.FONT_SIZE)
# Import and play background music
pygame.mixer.music.load(cfg.BGM_PATH)
pygame.mixer.music.play(-1)
Copy code

Next, let's define a text board to display the number of Mines buried in the upper left corner and the game elapsed time in the upper right corner:

'''Text board'''
class TextBoard(pygame.sprite.Sprite):
    def __init__(self, text, font, position, color, **kwargs):
        pygame.sprite.Sprite.__init__(self)
        self.text = text
        self.font = font
        self.position = position
        self.color = color
    def draw(self, screen):
        text_render = self.font.render(self.text, True, self.color)
        screen.blit(text_render, self.position)
    def update(self, text):
        self.text = text
 Copy code

In fact, it's very simple. Just bind the text object rendered with font to the screen, and then set an update function to update the text content in real time.

Then, we define an emoticon button class:
'''Emoticon button'''
class EmojiButton(pygame.sprite.Sprite):
    def __init__(self, images, position, status_code=0, **kwargs):
        pygame.sprite.Sprite.__init__(self)
        # import picture
        self.images = images
        self.image = self.images['face_normal']
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = position
        # The current state of the emoticon button
        self.status_code = status_code
    '''Draw to screen'''
    def draw(self, screen):
        # The status code is 0, which represents a normal expression
        if self.status_code == 0:
            self.image = self.images['face_normal']
        # The status code is 1, which represents the expression of failure
        elif self.status_code == 1:
            self.image = self.images['face_fail']
        # The status code is 2, which represents the expression of success
        elif self.status_code == 2:
            self.image = self.images['face_success']
        # Bind picture to screen
        screen.blit(self.image, self.rect)
    '''Sets the status of the current button'''
    def setstatus(self, status_code):
        self.status_code = status_code
 Copy code

When the mouse clicks this button, the new game will be restarted (regardless of the current game state, the new game will be restarted):

Next, we need to define the following grid class:

'''thunder'''
class Mine(pygame.sprite.Sprite):
    def __init__(self, images, position, status_code=0, **kwargs):
        pygame.sprite.Sprite.__init__(self)
        # import picture
        self.images = images
        self.image = self.images['blank']
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = position
        # Ray's current state
        self.status_code = status_code
        # True thunder or false thunder (false thunder by default)
        self.is_mine_flag = False
        # Number of surrounding mines
        self.num_mines_around = -1
    '''Set current status code'''
    def setstatus(self, status_code):
        self.status_code = status_code
    '''Buried mine'''
    def burymine(self):
        self.is_mine_flag = True
    '''Set the number of surrounding mines'''
    def setnumminesaround(self, num_mines_around):
        self.num_mines_around = num_mines_around
    '''Draw to screen'''
    def draw(self, screen):
        # The status code is 0, which means the mine is not clicked
        if self.status_code == 0:
            self.image = self.images['blank']
        # The status code is 1, indicating that the mine has been triggered
        elif self.status_code == 1:
            self.image = self.images['mine'] if self.is_mine_flag else self.images[str(self.num_mines_around)]
        # The status code is 2, which means that the mine is marked as mine by the player
        elif self.status_code == 2:
            self.image = self.images['flag']
        # The status code is 3, which means that the mine is marked as a question mark by the player
        elif self.status_code == 3:
            self.image = self.images['ask']
        # The status code is 4, which means that the mine is being double clicked by the left and right mouse buttons
        elif self.status_code == 4:
            assert not self.is_mine_flag
            self.image = self.images[str(self.num_mines_around)]
        # The status code is 5, which means that the mine is around the mine double clicked by the left and right mouse buttons
        elif self.status_code == 5:
            self.image = self.images['0']
        # The status code is 6, which means the mine is stepped on
        elif self.status_code == 6:
            assert self.is_mine_flag
            self.image = self.images['blood']
        # The status code is 7, which means that the mine is mismarked
        elif self.status_code == 7:
            assert not self.is_mine_flag
            self.image = self.images['error']
        # Bind picture to screen
        screen.blit(self.image, self.rect)
    @property
    def opened(self):
        return self.status_code == 1
 Copy code

Its main function is to record the state of a square in the game map (such as whether it is buried with thunder, whether it is clicked, whether it is marked, etc.).

Finally, define a game map class to integrate all squares in the game map, so as to call and update in the main cycle of the game:

'''Minesweeping map'''
class MinesweeperMap():
    def __init__(self, cfg, images, **kwargs):
        self.cfg = cfg
        # Ray type matrix
        self.mines_matrix = []
        for j in range(cfg.GAME_MATRIX_SIZE[1]):
            mines_line = []
            for i in range(cfg.GAME_MATRIX_SIZE[0]):
                position = i * cfg.GRIDSIZE + cfg.BORDERSIZE, (j + 2) * cfg.GRIDSIZE
                mines_line.append(Mine(images=images, position=position))
            self.mines_matrix.append(mines_line)
        # Random buried mine
        for i in random.sample(range(cfg.GAME_MATRIX_SIZE[0]*cfg.GAME_MATRIX_SIZE[1]), cfg.NUM_MINES):
            self.mines_matrix[i//cfg.GAME_MATRIX_SIZE[0]][i%cfg.GAME_MATRIX_SIZE[0]].burymine()
        count = 0
        for item in self.mines_matrix:
            for i in item:
                count += int(i.is_mine_flag)
        # Current state of the game
        self.status_code = -1
        # Record the position when the mouse is pressed and the key pressed
        self.mouse_pos = None
        self.mouse_pressed = None
    '''Draw the current game state diagram'''
    def draw(self, screen):
        for row in self.mines_matrix:
            for item in row: item.draw(screen)
    '''Set current game status'''
    def setstatus(self, status_code):
        # 0: the game is in progress, 1: the game is over, - 1: the game has not started yet
        self.status_code = status_code
    '''Update the current game status map according to the player's mouse operation'''
    def update(self, mouse_pressed=None, mouse_pos=None, type_='down'):
        assert type_ in ['down', 'up']
        # Record the position when the mouse is pressed and the key pressed
        if type_ == 'down' and mouse_pos is not None and mouse_pressed is not None:
            self.mouse_pos = mouse_pos
            self.mouse_pressed = mouse_pressed
        # The range of mouse click is not in the game map and there is no response
        if self.mouse_pos[0] < self.cfg.BORDERSIZE or self.mouse_pos[0] > self.cfg.SCREENSIZE[0] - self.cfg.BORDERSIZE or \
           self.mouse_pos[1] < self.cfg.GRIDSIZE * 2 or self.mouse_pos[1] > self.cfg.SCREENSIZE[1] - self.cfg.BORDERSIZE:
            return
        # Click the mouse in the game map to start the game (that is, you can start timing)
        if self.status_code == -1:
            self.status_code = 0
        # It's no use pressing the mouse if it's not in the game
        if self.status_code != 0:
            return
        # Mouse position rotation matrix index
        coord_x = (self.mouse_pos[0] - self.cfg.BORDERSIZE) // self.cfg.GRIDSIZE
        coord_y = self.mouse_pos[1] // self.cfg.GRIDSIZE - 2
        mine_clicked = self.mines_matrix[coord_y][coord_x]
        # Mouse down
        if type_ == 'down':
            # --Press the left and right mouse buttons at the same time
            if self.mouse_pressed[0] and self.mouse_pressed[2]:
                if mine_clicked.opened and mine_clicked.num_mines_around > 0:
                    mine_clicked.setstatus(status_code=4)
                    num_flags = 0
                    coords_around = self.getaround(coord_y, coord_x)
                    for (j, i) in coords_around:
                        if self.mines_matrix[j][i].status_code == 2:
                            num_flags += 1
                    if num_flags == mine_clicked.num_mines_around:
                        for (j, i) in coords_around:
                            if self.mines_matrix[j][i].status_code == 0:
                                self.openmine(i, j)
                    else:
                        for (j, i) in coords_around:
                            if self.mines_matrix[j][i].status_code == 0:
                                self.mines_matrix[j][i].setstatus(status_code=5)
        # Mouse release
        else:
            # --Left mouse button
            if self.mouse_pressed[0] and not self.mouse_pressed[2]:
                if not (mine_clicked.status_code == 2 or mine_clicked.status_code == 3):
                    if self.openmine(coord_x, coord_y):
                        self.setstatus(status_code=1)
            # --Right mouse button
            elif self.mouse_pressed[2] and not self.mouse_pressed[0]:
                if mine_clicked.status_code == 0:
                    mine_clicked.setstatus(status_code=2)
                elif mine_clicked.status_code == 2:
                    mine_clicked.setstatus(status_code=3)
                elif mine_clicked.status_code == 3:
                    mine_clicked.setstatus(status_code=0)
            # --Press the left and right mouse buttons at the same time
            elif self.mouse_pressed[0] and self.mouse_pressed[2]:
                mine_clicked.setstatus(status_code=1)
                coords_around = self.getaround(coord_y, coord_x)
                for (j, i) in coords_around:
                    if self.mines_matrix[j][i].status_code == 5:
                        self.mines_matrix[j][i].setstatus(status_code=0)
    '''Open ray'''
    def openmine(self, x, y):
        mine_clicked = self.mines_matrix[y][x]
        if mine_clicked.is_mine_flag:
            for row in self.mines_matrix:
                for item in row:
                    if not item.is_mine_flag and item.status_code == 2:
                        item.setstatus(status_code=7)
                    elif item.is_mine_flag and item.status_code == 0:
                        item.setstatus(status_code=1)
            mine_clicked.setstatus(status_code=6)
            return True
        mine_clicked.setstatus(status_code=1)
        coords_around = self.getaround(y, x)
        num_mines = 0
        for (j, i) in coords_around:
            num_mines += int(self.mines_matrix[j][i].is_mine_flag)
        mine_clicked.setnumminesaround(num_mines)
        if num_mines == 0:
            for (j, i) in coords_around:
                if self.mines_matrix[j][i].num_mines_around == -1:
                    self.openmine(i, j)
        return False
    '''Obtain the surrounding coordinate points of the coordinate points'''
    def getaround(self, row, col):
        coords = []
        for j in range(max(0, row-1), min(row+1, self.cfg.GAME_MATRIX_SIZE[1]-1)+1):
            for i in range(max(0, col-1), min(col+1, self.cfg.GAME_MATRIX_SIZE[0]-1)+1):
                if j == row and i == col:
                    continue
                coords.append((j, i))
        return coords
    '''Are you in the game'''
    @property
    def gaming(self):
        return self.status_code == 0
    '''Number of Mines marked as mines'''
    @property
    def flags(self):
        num_flags = 0
        for row in self.mines_matrix:
            for item in row: num_flags += int(item.status_code == 2)
        return num_flags
    '''Number of mines that have been opened'''
    @property
    def openeds(self):
        num_openeds = 0
        for row in self.mines_matrix:
            for item in row: num_openeds += int(item.opened)
        return num_openeds
 Copy code

Here we will explain only a few things that may be incomprehensible to our friends:

When opening the thunder, we use recursion. The function is that when there is no thunder around the clicked square, the system will automatically open the square around the square, so as to realize the effect that sometimes clicking a square can open a large square. The surroundings here refer to all squares in the Jiugong square centered on the target square;

Press the left and right mouse buttons together on the open grid. If the number of squares around the grid that have been marked as thunder is consistent with the number displayed on the grid, open all squares around the grid that have not been marked as thunder (so if you mark wrong, it will show that your game has been GG when you open it together).

After defining the necessary element classes in the game, instantiate them in the main function of the game:

# Instantiate game map
minesweeper_map = MinesweeperMap(cfg, images)
position = (cfg.SCREENSIZE[0] - int(cfg.GRIDSIZE * 1.25)) // 2, (cfg.GRIDSIZE * 2 - int(cfg.GRIDSIZE * 1.25)) // 2
emoji_button = EmojiButton(images, position=position)
fontsize = font.size(str(cfg.NUM_MINES))
remaining_mine_board = TextBoard(str(cfg.NUM_MINES), font, (30, (cfg.GRIDSIZE*2-fontsize[1])//2-2), cfg.RED)
fontsize = font.size('000')
time_board = TextBoard('000', font, (cfg.SCREENSIZE[0]-30-fontsize[0], (cfg.GRIDSIZE*2-fontsize[1])//2-2), cfg.RED)
time_board.is_start = False
 Copy code

Then write a game main loop to update the current game state according to the user's operation:

# Game main loop
clock = pygame.time.Clock()
while True:
    screen.fill(cfg.BACKGROUND_COLOR)
    # --Key detection
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouse_pos = event.pos
            mouse_pressed = pygame.mouse.get_pressed()
            minesweeper_map.update(mouse_pressed=mouse_pressed, mouse_pos=mouse_pos, type_='down')
        elif event.type == pygame.MOUSEBUTTONUP:
            minesweeper_map.update(type_='up')
            if emoji_button.rect.collidepoint(pygame.mouse.get_pos()):
                minesweeper_map = MinesweeperMap(cfg, images)
                time_board.update('000')
                time_board.is_start = False
                remaining_mine_board.update(str(cfg.NUM_MINES))
                emoji_button.setstatus(status_code=0)
    # --Update time display
    if minesweeper_map.gaming:
        if not time_board.is_start:
            start_time = time.time()
            time_board.is_start = True
        time_board.update(str(int(time.time() - start_time)).zfill(3))
    # --Update the display of the number of remaining mines
    remianing_mines = max(cfg.NUM_MINES - minesweeper_map.flags, 0)
    remaining_mine_board.update(str(remianing_mines).zfill(2))
    # --Update expression
    if minesweeper_map.status_code == 1:
        emoji_button.setstatus(status_code=1)
    if minesweeper_map.openeds + minesweeper_map.flags == cfg.GAME_MATRIX_SIZE[0] * cfg.GAME_MATRIX_SIZE[1]:
        minesweeper_map.status_code = 1
        emoji_button.setstatus(status_code=2)
    # --Displays the current game status map
    minesweeper_map.draw(screen)
    emoji_button.draw(screen)
    remaining_mine_board.draw(screen)
    time_board.draw(screen)
    # --Update screen
    pygame.display.update()
    clock.tick(cfg.FPS)
Copy code

This is the end of the article. Thank you for watching. The next article shares Xiaole games

In order to thank readers, I would like to share some of my recent collection of programming dry goods with you and give back to every reader. I hope I can help you.

Dry goods mainly include: Backstage private letters I need 666

① More than 2000 Python e-books (both mainstream and classic books should be available)

② Python standard library materials (the most complete Chinese version)

③ Project source code (forty or fifty interesting and classic hand training projects and source code)

④ Videos on basic introduction to Python, crawler, web development and big data analysis (suitable for Xiaobai)

⑤ Python learning roadmap (bid farewell to non stream learning)

⑥ Two days of Python crawler training camp live rights

Topics: Python Back-end Programmer