基于python的“扫雷”游戏实现

一、引言:

        最近在学习python语言,想着尝试通过python来实现儿时玩过的小游戏,于是从"扫雷"游戏开始,依据自己的理解,编写游戏代码。若有不周到之处,还望大家批评指正。

        环境配置:python3.12, pygame2.6.1, numpy1.26.4

二、效果展示

         

        灰色为未揭开的单元,红色表示地雷,绿色表示“插旗”(即认定此单元格为地雷)

三、程序思路

1.程序框架图

         如图所示,我们需要创建“扫雷”用的棋盘,并通过实时的输入来修改棋盘中的数据,从而检测是否达到游戏结束的条件。

2.具体程序实现

(1)生成棋盘(初始化)

        本项目采用正方形棋盘,基于numpy的array对象来生成棋盘上每一个点对应的属性。每个单元格的数值说明如下:

数值  含义
-1 未触碰的地雷
0~8

当前九宫格内的地雷总数

-2 被触碰过的地雷
10~18 被触碰过的普通单元(无地雷)
19~28 被认定为地雷的单元

        例如 1 表示当前九宫格内的地雷数为1,当鼠标左键点击这个单元后,1自加10变成11,于是这个单元变为被触碰过的普通单元(对应显示为数字);而当鼠标右键单机这个单元时,1自加20变成21,于是这个单元被标记为“可能含有地雷”(对应显示为绿色)

01.生成地雷

        本项目采用numpy.random.randint()生成的随机数来生成地雷,具体生成方式为:遍历棋盘上每一个单元格,若当前单元格数值为0,则尝试在该单元格生成地雷(概率为0.5),直到生成的地雷总数达到预设值。

    def set_bomb(self):
        bomb_total = 0
        enough = False
        while not enough:
            for i in range(1, self.size + 1):
                for j in range(1, self.size + 1):
                    if self.board[i, j] == 0 and not enough:
                        temp = np.random.randint(0, 100)
                        if temp < 50:
                            self.board[i, j] = -1
                            bomb_total += 1
                            if bomb_total == self.num_bomb:
                                enough = True

        这里索引值设置为(1,self.size+1)目的是为了使得棋盘边缘处单元格数值均为0,便于后续的计算。例如下面的棋盘,游戏区域中心的为10*10区域,其外侧均为0,此时计算(1,1)位置单元格(下方标红位置)所处九宫格的地雷总数时,便不会存在溢出的问题。

                        [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
                         [ 0.  1. -1.  3. -1. -1. -1. -1.  3.  3. -1.  0.]
                         [ 0.  1.  1.  3. -1. -1.  7. -1.  4. -1. -1.  0.]
                         [ 0.  2.  3.  3.  4. -1.  4. -1.  4.  5. -1.  0.]
                         [ 0. -1. -1. -1.  4.  2.  3.  4. -1.  5. -1.  0.]
                         [ 0. -1. -1. -1. -1.  3.  2. -1. -1.  6. -1.  0.]
                         [ 0. -1. -1. -1. -1.  3. -1.  4. -1. -1.  2.  0.]
                         [ 0.  2.  3.  3.  2.  2.  1.  2.  2.  2.  1.  0.]
                         [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
                         [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
                        [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
                         [ 0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.]

02.生成普通单元格

        即生成普通单元格为中心的九宫格内的地雷总数,并将该数值写入该单元格。

    def set_number(self):
        for i in range(1, self.size + 1):
            for j in range(1, self.size + 1):
                number = 0
                if self.board[i, j] == 0:
                    for x in range(-1, 2):
                        for y in range(-1, 2):
                            if self.board[i + x, j + y] == -1:
                                number += 1
                    self.board[i, j] = number

(2)图形化棋盘

        被触碰的地雷,渲染对应的单元格为 红色 正方形

        被 “插旗”(被认定为地雷)的单元格,渲染对应的单元格为 绿色 正方形

        被触碰的普通单元格,显示当前单元格所在九宫格的地雷数

        未被触碰的普通单元格,渲染对应的单元格为 灰色 正方形

    def draw(self):
        for i in range(1, self.size + 1):
            for j in range(1, self.size + 1):
                if 19 > self.board[i, j] > 9:
                    font = pygame.font.SysFont('Arial', 10)
                    if self.board[i, j] == 10:
                        pass
                    else:
                        text = font.render(f'{int(self.board[i, j] - 10)}', True, (0, 0, 0))
                        self.screen.blit(text, [i*35 + 10, j*35 + 10])
                else:
                    pygame.draw.rect(self.screen, (100, 100, 100), pygame.Rect([i*35, j*35, 35, 35]))
                if self.board[i, j] == -2:
                    pygame.draw.rect(self.screen, 'red', pygame.Rect([i*35, j*35, 35, 35]))
                if self.board[i, j] > 18:
                    pygame.draw.rect(self.screen, (0, 180, 0), pygame.Rect([i*35, j*35, 35, 35]))
                pygame.draw.rect(self.screen, (0, 0, 0), [i*35, j*35, 35, 35], 1)
        pygame.display.flip()

(3)检测输入并更新棋盘

        检测鼠标点击的位置和点击方式,执行对应的程序。当鼠标左键单击时,将当前单元格标记为 “已触碰” ,当鼠标右键单击时,将当前单元格标记为 “ 认定为地雷”(对应“插旗”),再次鼠标右键单击则取消标记。

    def update_board(self):
        if not self.game_over:
            x, y = self._pos
            x, y = x//35, y//35
            if self.board.board[x, y] == -1:
                self.board.board[x, y] = -2
                self.game_over = True
            self.find_neighbour(x, y)

    def update_board2(self):
        if not self.game_over:
            x, y = self._pos
            x, y = x//35, y//35
            if -2 < self.board.board[x, y] < 10:
                self.board.board[x, y] += 20
            elif 18 < self.board.board[x, y]:
                self.board.board[x, y] -= 20
01.自动触碰“8联通”的单元格

         当鼠标左键单击某一普通单元时,会自动将该单元格的 “8联通” 区域执行触碰操作,以下算法又被称为 “递归回溯法” ,感兴趣的伙伴可以自行了解更多内容。

    def find_neighbour(self, x, y):  # 八连通关系
        if 0 < x < self.board.size + 1 and 0 < y < self.board.size + 1:
            if 10 > self.board.board[x, y] > -1:
                for i in range(-1, 2):
                    for j in range(-1, 2):
                        if 10 > self.board.board[i + x, y + j] > -1:
                            self.board.board[i + x, y + j] += 10
                self.find_neighbour(x + 2, y)
                self.find_neighbour(x - 2, y)
                self.find_neighbour(x, y + 2)
                self.find_neighbour(x, y - 2)

(4)检测棋盘

01.游戏失败

        当触碰到地雷时,判定为游戏失败。

 def update_board(self):
        if not self.game_over:
            x, y = self._pos
            x, y = x//35, y//35
            if self.board.board[x, y] == -1:
                self.board.board[x, y] = -2
                self.game_over = True
            self.find_neighbour(x, y)
02.游戏胜利

        当找到所有地雷,即将所有地雷标记为 “认定为地雷” 时,判定游戏胜利。

    def win(self):
        found_bomb = 0
        for i in range(1, self.size + 1):
            for j in range(1, self.size + 1):
                if self.board[i, j] == 19:
                    found_bomb += 1
        if found_bomb == self.num_bomb:
            self.win_flag = True

(5)游戏结束

        游戏胜利显示“win”,游戏失败显示"GAME OVER"。

    def end(self):
        pos = ((self.board.size // 2 * 35), 5)
        font = pygame.font.SysFont('Arial', 20)
        if self.board.win_flag:
            text = font.render('Win', 1, (0, 0, 0))
        else:
            text = font.render('GAME OVER', True, (0, 0, 0))
        self.screen.blit(text, pos)

 3.主函数

        size表示棋盘的大小(例如size=10,则棋盘为10*10),bomb为设定的地雷总数。可以自行更改数值,来实现不同大小的游戏区域和地雷总数。

def main():
    pygame.init()
    size = 10
    bomb = size ** 2 // 3
    screen = pygame.display.set_mode(((size + 2) * 35, (size + 2) * 35))
    game_section = Board(size, bomb, screen)
    # print(game_section.board)
    click = Click((0, 0), game_section, screen)
    screen.fill((230, 230, 230))  
    game_section.draw()

    running = True
    while running:
        for event in pygame.event.get():
            game_section.win()
            if event.type == pygame.QUIT:
                running = False
            if event.type == pygame.MOUSEBUTTONDOWN:
                screen.fill((230, 230, 230))

                click.pos = pygame.mouse.get_pos()
                if event.button == 1:
                    click.update_board()
                if event.button == 3:
                    click.update_board2()
                if click.game_over or game_section.win_flag:
                    click.end()
                game_section.draw()

四、完整代码

import pygame
import numpy as np


class Board(object):
    def __init__(self, size, num_bomb, screen):
        self.size = size
        self.num_bomb = num_bomb
        self.board = np.zeros((self.size + 2, self.size + 2))
        self.screen = screen
        self.set_bomb()
        self.set_number()
        self.win_flag = False

    def set_bomb(self):
        bomb_total = 0
        enough = False
        while not enough:
            for i in range(1, self.size + 1):
                for j in range(1, self.size + 1):
                    if self.board[i, j] == 0 and not enough:
                        temp = np.random.randint(0, 100)
                        if temp < 50:
                            self.board[i, j] = -1
                            bomb_total += 1
                            if bomb_total == self.num_bomb:
                                enough = True

    def set_number(self):
        for i in range(1, self.size + 1):
            for j in range(1, self.size + 1):
                number = 0
                if self.board[i, j] == 0:
                    for x in range(-1, 2):
                        for y in range(-1, 2):
                            if self.board[i + x, j + y] == -1:
                                number += 1
                    self.board[i, j] = number

    def draw(self):
        for i in range(1, self.size + 1):
            for j in range(1, self.size + 1):
                if 19 > self.board[i, j] > 9:
                    font = pygame.font.SysFont('Arial', 10)
                    if self.board[i, j] == 10:
                        pass
                    else:
                        text = font.render(f'{int(self.board[i, j] - 10)}', True, (0, 0, 0))
                        self.screen.blit(text, [i*35 + 10, j*35 + 10])
                else:
                    pygame.draw.rect(self.screen, (100, 100, 100), pygame.Rect([i*35, j*35, 35, 35]))
                if self.board[i, j] == -2:
                    pygame.draw.rect(self.screen, 'red', pygame.Rect([i*35, j*35, 35, 35]))
                if self.board[i, j] > 18:
                    pygame.draw.rect(self.screen, (0, 180, 0), pygame.Rect([i*35, j*35, 35, 35]))
                pygame.draw.rect(self.screen, (0, 0, 0), [i*35, j*35, 35, 35], 1)
        pygame.display.flip()

    def win(self):
        found_bomb = 0
        for i in range(1, self.size + 1):
            for j in range(1, self.size + 1):
                if self.board[i, j] == 19:
                    found_bomb += 1
        if found_bomb == self.num_bomb:
            self.win_flag = True


class Click(object):
    def __init__(self, pos, board, screen):
        self._pos = pos
        self.board = board
        self.screen = screen
        self.game_over = False

    @property
    def pos(self):
        return self._pos

    @pos.setter
    def pos(self, value):
        self._pos = value

    def update_board(self):
        if not self.game_over:
            x, y = self._pos
            x, y = x//35, y//35
            if self.board.board[x, y] == -1:
                self.board.board[x, y] = -2
                self.game_over = True
            self.find_neighbour(x, y)

    def update_board2(self):
        if not self.game_over:
            x, y = self._pos
            x, y = x//35, y//35
            if -2 < self.board.board[x, y] < 10:
                self.board.board[x, y] += 20
            elif 18 < self.board.board[x, y]:
                self.board.board[x, y] -= 20

    def end(self):
        pos = ((self.board.size // 2 * 35), 5)
        font = pygame.font.SysFont('Arial', 20)
        if self.board.win_flag:
            text = font.render('Win', 1, (0, 0, 0))
        else:
            text = font.render('GAME OVER', True, (0, 0, 0))
        self.screen.blit(text, pos)

    def find_neighbour(self, x, y):  # 八连通关系
        if 0 < x < self.board.size + 1 and 0 < y < self.board.size + 1:
            if 10 > self.board.board[x, y] > -1:
                for i in range(-1, 2):
                    for j in range(-1, 2):
                        if 10 > self.board.board[i + x, y + j] > -1:
                            self.board.board[i + x, y + j] += 10
                self.find_neighbour(x + 2, y)
                self.find_neighbour(x - 2, y)
                self.find_neighbour(x, y + 2)
                self.find_neighbour(x, y - 2)


def main():
    pygame.init()
    size = 10
    bomb = size ** 2 // 3
    screen = pygame.display.set_mode(((size + 2) * 35, (size + 2) * 35))
    game_section = Board(size, bomb, screen)
    # print(game_section.board)
    click = Click((0, 0), game_section, screen)
    screen.fill((230, 230, 230))  
    game_section.draw()

    running = True
    while running:
        for event in pygame.event.get():
            game_section.win()
            if event.type == pygame.QUIT:
                running = False
            if event.type == pygame.MOUSEBUTTONDOWN:
                screen.fill((230, 230, 230))

                click.pos = pygame.mouse.get_pos()
                if event.button == 1:
                    click.update_board()
                if event.button == 3:
                    click.update_board2()
                if click.game_over or game_section.win_flag:
                    click.end()
                game_section.draw()


if __name__ == '__main__':
    main()

五、项目总结

        本项目基于pycharm2024实现,使用了numpy和pygame库,代码总共155行(加上所有的空行),是一个轻量化的项目。本项目实现了“扫雷”游戏的主体内容,即生成棋盘,根据输入更新棋盘。此外,本项目的扩展方向可以为优化显示内容,例如将触碰后的地雷渲染为 “地雷图片” ,而不是简单的以红色来表示。

作者:m0_74802518

物联沃分享整理
物联沃-IOTWORD物联网 » 基于python的“扫雷”游戏实现

发表回复