Python小游戏——扫雷

需求分析+设计

用Python做一个传统的扫雷游戏。

  • 游戏界面和操作
  1. 游戏界面:一系列游戏图标 + 时间和剩余旗子数 + (背景音乐)。
  2. 操作:开始游戏 / 重新开始游戏 + 退出游戏 + 打开格子 + 标记格子 + 显示格子信息。

游戏状态转化逻辑、游戏赢输规则和游戏界面和事件检测在主程序(main.py)中实现。游戏界面和事件检测主要用pygame设计。

设计格子(Grid)类和棋盘(ChessBoard)类用以封装格子和棋盘的数据和操作。

编码实现

文件目录结构(python环境与项目文件同级)

Mine-clearing game
|–data \
 |–font \---------------------------------存放游戏界面显示的字体
 |–pictures \----------------------------存放游戏界面显示的图片
|–include \
 |–Mine_clear_class.py-------------存放自定义的Python类
|–main.py---------------------------------程序的入口文件

Mine_clear_class.py

# _*_ coding : utf-8 _*_
from enum import Enum
import random

# 坐标说明,本文件内坐标均是“矩阵坐标”即在矩阵中(x,y)表示x行y列。

class GridState(Enum):  # 格子状态
    normal = 1 	 	# 初始化的状态,未被打开,未被标记
    opened = 2    	# 已打开的格子
    flag = 3    	# 标记为旗子
    ask = 4     	# 标记为问号
class GameStae(Enum):  # 游戏状态
    ready = 1,		# 不稳定,会立即被start状态取代
    start = 2,
    lose = 3,
    win = 4

class Grid:  # 一个小格子
    def __init__(self, x=0, y=0, is_mine=False):
        self.x = x
        self.y = y
        self.is_mine = is_mine
        self.around_mine_count = 0  # around_mine_count
        self.state = GridState.normal

    def __repr__(self):
        re_str = "该格子的矩阵坐标: (" + str(self.y)+','+str(self.x)+')'+'\n'
        re_str += "该格子的状态: " + str(self.state)+'\n'
        re_str += "该格子是否是地雷:" + str(self.is_mine)+'\n'
        re_str += "该格子周围地雷数:" + str(self.around_mine_count)
        return re_str

    def get_x(self): return self.x
    def set_x(self, x): self.x = x

    def get_y(self): return self.y
    def set_y(self, y): self.y = y

    def get_is_mine(self): return self.is_mine
    def set_is_mine(self, is_mine): self.is_mine = is_mine

    def get_around_mine_count(self): return self.around_mine_count
    def set_around_mine_count(self, around_mine_count): self.around_mine_count = around_mine_count

    def get_state(self): return self.state
    def set_state(self, state): self.state = state


class ChessBoard:  # 游戏棋盘和一些影响棋盘的操作
    def __init__(self, WIDTH=10, HEIGHT=10, MINE_COUNT=10):
        # 生成一个棋盘
        self.WIDTH = WIDTH
        self.HEIGHT = HEIGHT
        self.MINE_COUNT = MINE_COUNT
        self.grids = [[Grid(i, j) for i in range(WIDTH)]for j in range(HEIGHT)]
        # 放置地雷
        # random.sample 返回一个MINE_COUNT长的列表,内存放k个随机的唯一的元素。
        # “//”代表取整
        for i in random.sample(range(WIDTH * HEIGHT), MINE_COUNT):
            x = i // WIDTH
            y = i % WIDTH
            self.grids[x][y].set_is_mine(True)
            # 计算周围地雷数,即地雷周围每一个格子的around mine count数加一
            for m in range(max(0, x-1), min(WIDTH, x+2)):
                for n in range(max(0, y-1), min(HEIGHT, y+2)):
                    self.grids[m][n].set_around_mine_count(self.grids[m][n].get_around_mine_count()+1)

    def get_all_grids(self): return self.grids
    def get_one_grid(self, x, y): return self.grids[x][y]

    # 后续需要依据游戏模式(简单、困难之类)才能更改这些变量。
    def get_WIDTH(self): return self.WIDTH
    def get_HEIGHT(self): return self.HEIGHT
    def get_MINE_COUNT(self): return self.MINE_COUNT

    def opened_grid(self, x, y):  # 打开格子
        self.grids[x][y].set_state(GridState.opened)
        if self.grids[x][y].get_is_mine() == True:  # 踩到雷了
            return False

        # 没踩到,打开周围3*3格子
        if self.grids[x][y].get_around_mine_count() == 0:
            for i in range(max(0, x-1),  min(self.WIDTH, x+2)):
                for j in range(max(0, y-1), min(self.HEIGHT, y+2)):
                    if self.grids[i][j].get_state() == GridState.normal:
                        self.opened_grid(i, j)
        
        return True

    def mark_grid(self, x, y):  # 标记格子
        if (self.grids[x][y].get_state() == GridState.normal):
            self.grids[x][y].set_state(GridState.flag)
        elif (self.grids[x][y].get_state() == GridState.flag):
            self.grids[x][y].set_state(GridState.ask)
        elif (self.grids[x][y].get_state() == GridState.ask):
            self.grids[x][y].set_state(GridState.normal)

    # 赢的方式1.打开所有非雷的格子
    # 赢的方式2.标记所有雷的格子
    # (逻辑判断均在主程序中实现)
    def get_opened_grid_count(self):  # 统计所有打开的格子数
        res_count = 0
        for i in range(self.WIDTH):
            for j in range(self.HEIGHT):
                if self.grids[i][j].get_state() == GridState.opened:
                    res_count += 1
        return res_count
    
    def get_flag_mine_count(self):  # 统计所有标记对的格子数
        # \ 是下一行接到这一行的意思,可以用括号代替。
        res_count = 0
        for i in range(self.WIDTH):
            for j in range(self.HEIGHT):
                if self.grids[i][j].get_state() == GridState.flag \
                    and self.grids[i][j].get_is_mine() == True:
                    res_count += 1
        return res_count

Mine_clear_class.py文件总结:此文件较为独立,主要涉及到Grid类和ChessBoard类。在该文件内没有定义常量或者其他参数,所有变量(如WIDTH、HEIGHT等)均在实例化类时作为参数传进去,因此无需考虑多个文件间数据一致性问题。当设计“游戏难度”功能时,可以删除(del)类后,重新实例化。
编码后先进行简单的验证,避免运行主程序时出现大量语法错误。
如下实例化一个例子。

grid = Grid(0,1,True)
print(grid)
grid.set_around_mine_count(3)
print(grid)
'''
输出
该格子的矩阵坐标: (1,0)
该格子的状态: GridState.normal
该格子是否是地雷:True
该格子周围地雷数:0
该格子的矩阵坐标: (1,0)
该格子的状态: GridState.normal
该格子是否是地雷:True
该格子周围地雷数:3
'''

基础数据(data文件夹)

下一个较为好实现任务是建立data文件夹
|–data \
 |–font \---------------------------------存放游戏界面显示的字体
 |–pictures \----------------------------存放游戏界面显示的图片
这里先没有实现背景音乐功能,故先不放音效。
具体文件放在giteegithub了。

main.py(程序入口)

先捋一下这里要实现的功能:

  1. 呈现游戏界面(pygame模块) 【具体实现起来需要注意pygame呈现坐标系的特点】
  2. 事件检测(pygame.event模块) 如:鼠标左/右键等
  3. 游戏逻辑处理(if else) 如:鼠标左键对应的打开动作、点击笑脸对应的重新开始动作,和游戏输赢的判定。
  4. 游戏数据导入(图片和字体) 其实这个不想放在这里,想要单独拎出来一个文件存放导入的数据,但是可能比较复杂,故先放在一起了。

需要注意的一点编码与测试同行

首先我们需要一个能跑起来进行测试的框架(像下面这样,切记不要贪多,一点一点慢慢加)。

import pygame
FIRST_SCREEN_WIDTH = 720
FIRST_SCREEN_HEIGHT = 720
if __name__ == "__main__":
    pygame.init()
    menu_screen = pygame.display.set_mode((FIRST_SCREEN_WIDTH, FIRST_SCREEN_HEIGHT))
    menu_screen.fill((255,255,255))  # 白色画布1(开始界面用的)
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                exit("ok")
        pygame.display.update()

运行后弹出如下图片,点击右上键x,就会进入到我们设计的退出逻辑中。
这个图片
之后向源代码中加入细节。

import pygame
from pygame.locals import *

# 基础屏幕大小、格子块大小等全局信息。
FIRST_SCREEN_WIDTH = 720
FIRST_SCREEN_HEIGHT = 720
SIZE = 20

def Load_image():
    global first_bac
    first_bac= pygame.image.load('Mine-clearing game/data/pictures/first_bac.png').convert() # 加载并渲染
    first_bac= pygame.transform.smoothscale(first_bac, (FIRST_SCREEN_WIDTH, FIRST_SCREEN_HEIGHT))  # 平滑缩放(这里可以用学过的最近邻?或双线性)

def Load_font():
    global first_font, first_font_size
    first_font_size = FIRST_SCREEN_WIDTH//10
    # 'C:/Windows/Fonts/xxx.ttf'  # (系统自带字体文件夹,xxx是其名字)
    first_font_path = 'Mine-clearing game/data/font/云峰静龙行书.ttf'
    # pygame.font.Font参数(字体位置, 字体大小)
    first_font = pygame.font.Font(first_font_path, first_font_size)

def Set_clock():
    global clock, FPS
    FPS = 60
    # 获取一个Clock对象,限制游戏的运行速度(clock.tick(FPS)这样用)
    clock = pygame.time.Clock() # 类似于sleep
def Set_colors():
    global pink_color
    pink_color = (245, 140, 190)  # (定义RGB三元组)这个还可以在调淡色一点

def My_init():
    Load_image()
    Load_font()
    Set_clock()
    Set_colors()

def Print_text(screen, font, x, y, text, fcolor=(0, 0, 0)):
    # font.render参数 (文本,抗锯齿否,字体颜色,背景颜色)
    imgText = font.render(text, True, fcolor)
    screen.blit(imgText, (x, y))

def Set_easy_mode():
    print("so easy")
def Set_medium_mode():
    print("so normal")
def Set_hard_mode():
    print("so hard")

# 简单介绍一下模块内各种函数的作用,具体的可以用时现查
# 把各种加载操作放到别出去,看代码的时候可以直接缩小他们。
if __name__ == "__main__":  # main
    pygame.init()  # 检查硬件调用接口、基础功能是否有问题(模糊一点就说初始化)
    # pygame.display.set_mode参数(分辨率, 显示模式=0, 颜色位数=0, 不知道=0, 不知道=0)
    menu_screen = pygame.display.set_mode((FIRST_SCREEN_WIDTH, FIRST_SCREEN_HEIGHT))
    My_init() # 自定义的初始化函数
    pygame.display.set_caption("扫雷") # 设置当前窗口标题
    # screen.blit参数(图像/字,位置,不知道,不知道)
    # 第三个参数好像与动态图有关,可能其他程序会用得到。
    menu_screen.blit(first_bac, (0, 0)) # 载入游戏开始界面
    # 之后要用这些参数定位鼠标动作,所以单起一个“稍短”的名字
    (text_width, text_height) = first_font.size("游戏难度")
    text_x = int((FIRST_SCREEN_WIDTH-text_width)/2)
    base_text_y = int(FIRST_SCREEN_HEIGHT/6)
    Print_text(menu_screen, first_font, text_x, base_text_y, "游戏难度", pink_color)
    Print_text(menu_screen, first_font, text_x, base_text_y*2, "简单模式", pink_color)
    Print_text(menu_screen, first_font, text_x, base_text_y*3, "中单模式", pink_color)
    Print_text(menu_screen, first_font, text_x, base_text_y*4, "困单模式", pink_color)
    while True:
        # 获取事件,包括鼠标和键盘
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                exit("ok")
            # 暂时还想不到窗口最大化的好办法
            elif event.type == pygame.MOUSEBUTTONDOWN: # 鼠标按下(任何一键)
                screen_x, screen_y = event.pos # 获取鼠标点击位置的pygame坐标
                mouse_l, mouse_m, mouse_r = pygame.mouse.get_pressed()# 检测具体哪个键(bool型)(左,滚轮,右)
                if mouse_l == True:
                    # \ 表示承接上一行,因为一行写不开了。
                    if text_x <= screen_x <= text_x + text_width \
                        and base_text_y*2 <= screen_y <= base_text_y*2 + text_height:
                        Set_easy_mode()
                    elif text_x <= screen_x <= text_x + text_width \
                        and base_text_y*3 <= screen_y <= base_text_y*3 + text_height:
                        Set_medium_mode()
                    elif text_x <= screen_x <= text_x + text_width \
                        and base_text_y*4 <= screen_y <= base_text_y*4 + text_height:
                        Set_hard_mode()

        pygame.display.update()
        clock.tick(FPS)

写了一些代码之后我们立刻进行测试,避免到最后才发现问题。
下面是测试结果,背景图片可自己定义。
在这里插入图片描述

下面是终端输出结果。
请添加图片描述
测试之后我们不难发现进一步扩充代码需要解决的问题。
1.只定义了一个屏幕。所以接下来可以尝试定义第二个屏幕(也可以将第一个屏幕覆盖),选完游戏难度后跳出第一个循环(最好是跳出第一个界面的循环,否则需要新增变量来记录目前是哪个界面)。这样设计之后,若想不退出游戏就能重新选择游戏难度,就需要将第二个屏幕整理到一个函数中,或令设一个大循环包括住两个屏幕。【总之要逻辑自洽。】
第一次用pygame,可能会陷入想要分离模块,但模块之间藕断丝连的情况。

所以接下来主要解决上一次编码留下的问题,并尽量做出二层的简单界面,方便调试。其中Load_image()、Load_font()、Set_clock()、Set_colors()、My_init()、Print_text()与解决问题没关系,所以都不用改。

新增WIDTH, HEIGHT, MINE_COUNT作为全局变量,它们是游戏的参数,随不同游戏难度而变化。
修改Set_easy_mode()、Set_medium_mode()、Set_hard_mode()函数,其更改全局游戏参数,且返回新的屏幕。
将第二个游戏屏幕设计成函数Secend_game_interface()。【暂时啥功能也没有,只会展示一个可视化界面+实现了退出功能】
新在主函数中加入reset_mode变量,表示是否由二层游戏界面退回到菜单界面,在该分支中设置屏幕切换。

import pygame
from pygame.locals import *

# 基础屏幕大小、格子块大小等全局信息。
FIRST_SCREEN_WIDTH = 720
FIRST_SCREEN_HEIGHT = 720
SIZE = 20 # 一个正方形格子的大小

WIDTH = 10 # 长宽多少个格子
HEIGHT = 10
MINE_COUNT = 10     # 地雷数

def Load_image():
    global first_bac
    first_bac= pygame.image.load('Mine-clearing game/data/pictures/first_bac.png').convert() # 加载并渲染
    first_bac= pygame.transform.smoothscale(first_bac, (FIRST_SCREEN_WIDTH, FIRST_SCREEN_HEIGHT))  # 平滑缩放(这里可以用学过的最近邻?或双线性)

def Load_font():
    global first_font, first_font_size
    first_font_size = FIRST_SCREEN_WIDTH//10
    # 'C:/Windows/Fonts/xxx.ttf'  # (系统自带字体文件夹,xxx是其名字)
    first_font_path = 'Mine-clearing game/data/font/云峰静龙行书.ttf'
    # pygame.font.Font参数(字体位置, 字体大小)
    first_font = pygame.font.Font(first_font_path, first_font_size)

def Set_clock():
    global clock, FPS
    FPS = 60
    # 获取一个Clock对象,限制游戏的运行速度(clock.tick(FPS)这样用)
    clock = pygame.time.Clock() # 类似于sleep
def Set_colors():
    global pink_color
    pink_color = (245, 140, 190)  # (定义RGB三元组)这个还可以在调淡色一点

def My_init():
    Load_image()
    Load_font()
    Set_clock()
    Set_colors()

def Print_text(screen, font, x, y, text, fcolor=(0, 0, 0)):
    # font.render参数 (文本,抗锯齿否,字体颜色,背景颜色)
    imgText = font.render(text, True, fcolor)
    screen.blit(imgText, (x, y))

def Set_easy_mode():
    # easy无需改参数,使用默认的
    global WIDTH, HEIGHT, MINE_COUNT
    game_screen = pygame.display.set_mode((WIDTH * SIZE, (HEIGHT + 2) * SIZE)) # 因为要放笑脸,所以长一点
    return game_screen
def Set_medium_mode():
    global WIDTH, HEIGHT, MINE_COUNT
    WIDTH = WIDTH*2
    HEIGHT = HEIGHT*2
    MINE_COUNT = MINE_COUNT*2
    game_screen = pygame.display.set_mode((WIDTH * SIZE, (HEIGHT + 2) * SIZE)) # 因为要放笑脸,所以长一点
    return game_screen
def Set_hard_mode():
    global WIDTH, HEIGHT, MINE_COUNT
    WIDTH = WIDTH*3
    HEIGHT = HEIGHT*3
    MINE_COUNT = MINE_COUNT*3
    game_screen = pygame.display.set_mode((WIDTH * SIZE, (HEIGHT + 2) * SIZE)) # 因为要放笑脸,所以长一点
    return game_screen

def Secend_game_interface(screen):
    global WIDTH, HEIGHT, MINE_COUNT
    screen.fill((0,0,0))
    # screen.blit(first_bac, (0, 0)) # 载入游戏开始界面
    while True:
        # 获取事件,包括鼠标和键盘
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                # 需要回调参数
                WIDTH = 10 # 长宽多少个格子
                HEIGHT = 10
                MINE_COUNT = 10     # 地雷数
                print("成功退出")
                return
        pygame.display.update()
        clock.tick(FPS)
    
# 简单介绍一下模块内各种函数的作用,具体的可以用时现查
# 把各种加载操作放到别出去,看代码的时候可以直接缩小他们。
if __name__ == "__main__":  # main
    pygame.init()  # 检查硬件调用接口、基础功能是否有问题(模糊一点就说初始化)
    # pygame.display.set_mode参数(分辨率, 显示模式=0, 颜色位数=0, 不知道=0, 不知道=0)
    menu_screen = pygame.display.set_mode((FIRST_SCREEN_WIDTH, FIRST_SCREEN_HEIGHT))
    My_init() # 自定义的初始化函数
    pygame.display.set_caption("扫雷") # 设置当前窗口标题
    # screen.blit参数(图像/字,位置,不知道,不知道)
    # 第三个参数好像与动态图有关,可能其他程序会用得到。
    menu_screen.blit(first_bac, (0, 0)) # 载入游戏开始界面
    # 之后要用这些参数定位鼠标动作,所以单起一个“稍短”的名字
    (text_width, text_height) = first_font.size("游戏难度")
    text_x = int((FIRST_SCREEN_WIDTH-text_width)/2)
    base_text_y = int(FIRST_SCREEN_HEIGHT/6)
    Print_text(menu_screen, first_font, text_x, base_text_y, "游戏难度", pink_color)
    Print_text(menu_screen, first_font, text_x, base_text_y*2, "简单模式", pink_color)
    Print_text(menu_screen, first_font, text_x, base_text_y*3, "中单模式", pink_color)
    Print_text(menu_screen, first_font, text_x, base_text_y*4, "困单模式", pink_color)
    reset_mode = False # 不会挑来跳去,便用一个bool变量代替。
    while True:
        # 获取事件,包括鼠标和键盘
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                exit("ok")
            # 暂时还想不到窗口最大化的好办法
            elif event.type == pygame.MOUSEBUTTONDOWN: # 鼠标按下(任何一键)
                screen_x, screen_y = event.pos # 获取鼠标点击位置的pygame坐标
                mouse_l, mouse_m, mouse_r = pygame.mouse.get_pressed()# 检测具体哪个键(bool型)(左,滚轮,右)
                if mouse_l == True:
                    # \ 表示承接上一行,因为一行写不开了。
                    if text_x <= screen_x <= text_x + text_width \
                        and base_text_y*2 <= screen_y <= base_text_y*2 + text_height:
                        game_screen = Set_easy_mode()
                        Secend_game_interface(game_screen)
                        # 退出来之后需要重新刷新屏幕。
                        reset_mode = True
                        # 这里要是有goto跳来跳去岂不美哉。但怕用多了自己也掌控不了它跳到哪里去了>_<

                    elif text_x <= screen_x <= text_x + text_width \
                        and base_text_y*3 <= screen_y <= base_text_y*3 + text_height:
                        game_screen = Set_medium_mode()
                        Secend_game_interface(game_screen)
                        reset_mode = True

                    elif text_x <= screen_x <= text_x + text_width \
                        and base_text_y*4 <= screen_y <= base_text_y*4 + text_height:
                        game_screen = Set_hard_mode()
                        Secend_game_interface(game_screen)
                        reset_mode = True

            if reset_mode == True:
                menu_screen = pygame.display.set_mode((FIRST_SCREEN_WIDTH, FIRST_SCREEN_HEIGHT))
                menu_screen.blit(first_bac, (0, 0)) # 载入游戏开始界面
                Print_text(menu_screen, first_font, text_x, base_text_y, "游戏难度", pink_color)
                Print_text(menu_screen, first_font, text_x, base_text_y*2, "简单模式", pink_color)
                Print_text(menu_screen, first_font, text_x, base_text_y*3, "中单模式", pink_color)
                Print_text(menu_screen, first_font, text_x, base_text_y*4, "困单模式", pink_color)
                reset_mode = False

        pygame.display.update()
        clock.tick(FPS)

如此如此便解决了上一步的问题。
接下来需要实打实设计游戏逻辑了。

import pygame
import time
from pygame.locals import *
from include.Mine_clear_class import *

# 基础屏幕大小、格子块大小等全局信息。
FIRST_SCREEN_WIDTH = 720
FIRST_SCREEN_HEIGHT = 720
SIZE = 20 # 一个正方形格子的大小

WIDTH = 10 # 长宽多少个格子
HEIGHT = 10
MINE_COUNT = 10     # 地雷数

def Load_image():
    # 把图片和笑脸大小声明为全局变量
    global face_size
    global first_bac,img_normal,img_flag,img_ask,img_mine,img_explode,img_wrong_mine,img_face_lose,img_face_normal,img_face_win
    global img_mine_count
    # first background
    first_bac = pygame.image.load('Mine-clearing game/data/pictures/first_bac.png').convert() # 加载并渲染
    first_bac = pygame.transform.smoothscale(first_bac, (FIRST_SCREEN_WIDTH, FIRST_SCREEN_HEIGHT))  # 平滑缩放(这里可以用学过的最近邻?或双线性)
    # open 状态 around_mines_count
    img_amc0 = pygame.image.load('Mine-clearing game/data/pictures/0.bmp').convert()
    img_amc0 = pygame.transform.smoothscale(img_amc0, (SIZE, SIZE))
    img_amc1 = pygame.image.load('Mine-clearing game/data/pictures/1.bmp').convert()
    img_amc1 = pygame.transform.smoothscale(img_amc1, (SIZE, SIZE))
    img_amc2 = pygame.image.load('Mine-clearing game/data/pictures/2.bmp').convert()
    img_amc2 = pygame.transform.smoothscale(img_amc2, (SIZE, SIZE))
    img_amc3 = pygame.image.load('Mine-clearing game/data/pictures/3.bmp').convert()
    img_amc3 = pygame.transform.smoothscale(img_amc3, (SIZE, SIZE))
    img_amc4 = pygame.image.load('Mine-clearing game/data/pictures/4.bmp').convert()
    img_amc4 = pygame.transform.smoothscale(img_amc4, (SIZE, SIZE))
    img_amc5 = pygame.image.load('Mine-clearing game/data/pictures/5.bmp').convert()
    img_amc5 = pygame.transform.smoothscale(img_amc5, (SIZE, SIZE))
    img_amc6 = pygame.image.load('Mine-clearing game/data/pictures/6.bmp').convert()
    img_amc6 = pygame.transform.smoothscale(img_amc6, (SIZE, SIZE))
    img_amc7 = pygame.image.load('Mine-clearing game/data/pictures/7.bmp').convert()
    img_amc7 = pygame.transform.smoothscale(img_amc7, (SIZE, SIZE))
    img_amc8 = pygame.image.load('Mine-clearing game/data/pictures/8.bmp').convert()
    img_amc8 = pygame.transform.smoothscale(img_amc8, (SIZE, SIZE))
    img_mine_count = {
    
     # 字典数据结构
        0: img_amc0,
        1: img_amc1,
        2: img_amc2,
        3: img_amc3,
        4: img_amc4,
        5: img_amc5,
        6: img_amc6,
        7: img_amc7,
        8: img_amc8
    }
    # 格子状态图:normal, flag, ask
    img_normal = pygame.image.load('Mine-clearing game/data/pictures/normal.bmp').convert()
    img_normal = pygame.transform.smoothscale(img_normal, (SIZE, SIZE))
    img_flag = pygame.image.load('Mine-clearing game/data/pictures/flag.bmp').convert()
    img_flag = pygame.transform.smoothscale(img_flag, (SIZE, SIZE))
    img_ask = pygame.image.load('Mine-clearing game/data/pictures/ask.bmp').convert()
    img_ask = pygame.transform.smoothscale(img_ask, (SIZE, SIZE))
    # 雷的状态
    img_mine = pygame.image.load('Mine-clearing game/data/pictures/mine.bmp').convert() # normal->mine , ask也默认这个
    img_mine = pygame.transform.smoothscale(img_mine, (SIZE, SIZE))
    img_explode = pygame.image.load('Mine-clearing game/data/pictures/explode.bmp').convert() # open->mine
    img_explode = pygame.transform.smoothscale(img_explode, (SIZE, SIZE))
    img_wrong_mine = pygame.image.load('Mine-clearing game/data/pictures/wrong_mine.bmp').convert() # flag->mine
    img_wrong_mine = pygame.transform.smoothscale(img_wrong_mine, (SIZE, SIZE))
    # 经典笑脸 ready和start , lose , win
    face_size = int(SIZE * 1.5) # 笑脸大小
    img_face_lose = pygame.image.load('Mine-clearing game/data/pictures/face_lose.bmp').convert()
    img_face_lose = pygame.transform.smoothscale(img_face_lose, (face_size, face_size))
    img_face_normal = pygame.image.load('Mine-clearing game/data/pictures/face_normal.bmp').convert()
    img_face_normal = pygame.transform.smoothscale(img_face_normal, (face_size, face_size))
    img_face_win = pygame.image.load('Mine-clearing game/data/pictures/face_win.bmp').convert()
    img_face_win = pygame.transform.smoothscale(img_face_win, (face_size, face_size))

def Load_font():
    # first 一层;second 二层
    global first_font, second_font
    first_font_size = FIRST_SCREEN_WIDTH//10
    # 'C:/Windows/Fonts/xxx.ttf'  # (系统自带字体文件夹,xxx是其名字)
    first_font_path = 'Mine-clearing game/data/font/云峰静龙行书.ttf'
    # pygame.font.Font参数(字体位置, 字体大小)
    first_font = pygame.font.Font(first_font_path, first_font_size)

    second_font_path = 'Mine-clearing game/data/font/kaiti.ttf'
    second_font = pygame.font.Font(second_font_path, SIZE*2)  # 得分的字体

def Set_clock():
    global clock, FPS
    FPS = 60
    # 获取一个Clock对象,限制游戏的运行速度(clock.tick(FPS)这样用)
    clock = pygame.time.Clock() # 类似于sleep
def Set_colors():
    global pink_color, white_color
    pink_color = (245, 140, 190)  # (定义RGB三元组)这个还可以在调淡色一点
    white_color = (255, 255, 255)
def My_init():
    Load_image()
    Load_font()
    Set_clock()
    Set_colors()

def Print_text(screen, font, x, y, text, fcolor=(0, 0, 0)):
    # font.render参数 (文本,抗锯齿否,字体颜色,背景颜色)
    imgText = font.render(text, True, fcolor)
    screen.blit(imgText, (x, y))

def Set_easy_mode():
    # easy无需改参数,使用默认的
    global WIDTH, HEIGHT, MINE_COUNT
    game_screen = pygame.display.set_mode((WIDTH * SIZE, (HEIGHT + 2) * SIZE)) # 因为要放笑脸,所以长一点
    return game_screen
def Set_medium_mode():
    global WIDTH, HEIGHT, MINE_COUNT
    WIDTH = WIDTH*2
    HEIGHT = HEIGHT*2
    MINE_COUNT = MINE_COUNT*2
    game_screen = pygame.display.set_mode((WIDTH * SIZE, (HEIGHT + 2) * SIZE)) # 因为要放笑脸,所以长一点
    return game_screen
def Set_hard_mode():
    global WIDTH, HEIGHT, MINE_COUNT
    WIDTH = WIDTH*3
    HEIGHT = HEIGHT*3
    MINE_COUNT = MINE_COUNT*3
    game_screen = pygame.display.set_mode((WIDTH * SIZE, (HEIGHT + 2) * SIZE)) # 因为要放笑脸,所以长一点
    return game_screen

def matrixTOscreen(matrix_x, matrix_y):
    screen_x = matrix_y * SIZE
    screen_y = (matrix_x+2) * SIZE
    return screen_x, screen_y
def screenTOmatrix(screen_x, screen_y):
    matrix_x = screen_y // SIZE - 2
    matrix_y = screen_x // SIZE
    return matrix_x, matrix_y

def Secend_game_interface(screen):
    global WIDTH, HEIGHT, MINE_COUNT
    # 定义出游戏实例,定义游戏状态
    chess_board = ChessBoard(WIDTH, HEIGHT, MINE_COUNT)
    game_state = GameStae.ready

    face_pos = ((WIDTH*SIZE - face_size) // 2, (SIZE * 2 - face_size) // 2) # 笑脸位置
    (num_width, num_height) = second_font.size("999")

    start_time = time.time()
    duration_time = 0 # 游戏时长
    # screen.blit(first_bac, (0, 0)) # 载入游戏开始界面
    while True:
        # 大概顺序是操作,判断输赢,图片展示
        # 获取事件,包括鼠标和键盘
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                # 需要回调参数
                WIDTH = 10 # 长宽多少个格子
                HEIGHT = 10
                MINE_COUNT = 10     # 地雷数
                print("成功退出")
                return
            elif event.type == pygame.MOUSEBUTTONDOWN: # 鼠标按下(任何一键)
                screen_x, screen_y = event.pos # 获取鼠标点击位置的pygame坐标
                matrix_x, matrix_y = screenTOmatrix(screen_x, screen_y) # 格子的矩阵坐标
                grid = chess_board.get_one_grid(matrix_x, matrix_y)

                mouse_l, mouse_m, mouse_r = pygame.mouse.get_pressed()# 检测具体哪个键(bool型)(左,滚轮,右)
                # 打破ready状态,进入start,而且放在这里是个按下左键就开始的意思。
                # 但是不跳出本次点击动作,可以顺带执行打开操作
                if game_state == GameStae.ready:
                    game_state = GameStae.start
                    start_time = time.time()
                if mouse_l == True:
                    # 优先级强判定
                    if face_pos[0] <= screen_x <= face_pos[0] + face_size \
                        and face_pos[1] <= screen_y <= face_pos[1] + face_size:
                        # 开始或重新开始游戏
                        del chess_board
                        chess_board = ChessBoard(WIDTH, HEIGHT, MINE_COUNT)
                        game_state = GameStae.ready
                        continue
                    if grid.get_state() == GridState.normal \
                        and (game_state == GameStae.ready or game_state == GameStae.start):
                        lose_flag = chess_board.opened_grid(matrix_x, matrix_y)
                        if(lose_flag == False):
                            game_state = GameStae.lose
                            break # 跳出事件循环
                        else:
                            if(chess_board.get_opened_grid_count() == WIDTH*HEIGHT - MINE_COUNT):
                                # 赢的条件1.打开所有非雷的格子
                                game_state = GameStae.win
                                break # 跳出事件循环

                if mouse_r == True \
                    and (game_state == GameStae.ready or game_state == GameStae.start):
                    chess_board.mark_grid(matrix_x,matrix_y)
                    if(chess_board.get_flag_mine_count() == MINE_COUNT):
                        # 赢的条件2.标记完全部地雷
                        game_state = GameStae.win
                        break # 跳出事件循环

                if mouse_m == True:
                    # 显示调试信息
                    print(grid)
        
        # 下面是各种游戏状态的显示逻辑了

        screen.fill(white_color) # 这句出现了很多次可能多余了
        if game_state == GameStae.ready:
            duration_time = int(0) # 游戏时长
        if game_state == GameStae.start:
            duration_time = int(time.time()- start_time) # 游戏时长
        Print_text(screen, second_font, int((1.5*WIDTH*SIZE-num_width)*0.5), int((2*SIZE-num_height)*0.5), '%03d' % (duration_time), pink_color)

        left_flags_count = chess_board.get_left_flag_count()
        Print_text(screen, second_font, int((0.5*WIDTH*SIZE-num_width)*0.5), int((2*SIZE-num_height)*0.5), '%03d' % (left_flags_count), pink_color)

        if game_state == GameStae.ready:
            screen.blit(img_face_normal, face_pos)
            for i in range(WIDTH):
                for j in range(WIDTH):
                    pos = matrixTOscreen(i, j)
                    screen.blit(img_normal, pos)

        if game_state == GameStae.start:
            screen.blit(img_face_normal, face_pos)
            
            for i in range(WIDTH):
                for j in range(WIDTH):
                    # 开始的时候无需关心地雷此时的状态
                    grid = chess_board.get_one_grid(i, j)
                    pos = matrixTOscreen(i, j)

                    if grid.get_state() == GridState.normal:
                        screen.blit(img_normal, pos)
                    elif grid.get_state() == GridState.opened:
                        screen.blit(img_mine_count[grid.get_around_mine_count()], pos)
                    elif grid.get_state() == GridState.flag:
                        screen.blit(img_flag, pos)
                    elif grid.get_state() == GridState.ask:
                        screen.blit(img_ask, pos)
                    else:
                        print(game_state)
                        print(grid)
                        exit("something error unknown grid state")

        elif game_state == GameStae.lose:
            screen.blit(img_face_lose, face_pos)
            for i in range(WIDTH):
                for j in range(WIDTH):
                    # 这里需要加上对地雷的考虑了
                    grid = chess_board.get_one_grid(i, j)
                    pos = matrixTOscreen(i, j)

                    if grid.get_state() == GridState.normal and grid.get_is_mine() == False:
                        screen.blit(img_normal, pos)
                    elif grid.get_state() == GridState.normal and grid.get_is_mine() == True:
                        screen.blit(img_mine, pos)
                    elif grid.get_state() == GridState.opened and grid.get_is_mine() == False:
                        screen.blit(img_mine_count[grid.get_around_mine_count()], pos)
                    elif grid.get_state() == GridState.opened and grid.get_is_mine() == True:
                        screen.blit(img_explode, pos)
                    elif grid.get_state() == GridState.flag and grid.get_is_mine() == False :#标记错了
                        screen.blit(img_wrong_mine, pos)
                    elif grid.get_state() == GridState.flag and grid.get_is_mine() == True :#标记对了
                        screen.blit(img_flag, pos)
                    elif grid.get_state() == GridState.ask and grid.get_is_mine() == False:
                        screen.blit(img_ask, pos)
                    elif grid.get_state() == GridState.ask and grid.get_is_mine() == True:
                        screen.blit(img_mine, pos)
                    else:
                        print(game_state)
                        print(grid)
                        exit("something error unknown grid state")

        elif game_state == GameStae.win:
            screen.blit(img_face_normal, face_pos)
            for i in range(WIDTH):
                for j in range(WIDTH):
                    # 这里需要加上对地雷的考虑了
                    grid = chess_board.get_one_grid(i, j)
                    pos = matrixTOscreen(i, j)

                    if grid.get_state() == GridState.normal:
                        screen.blit(img_normal, pos)
                    elif grid.get_state() == GridState.opened:
                        screen.blit(img_mine_count[grid.get_around_mine_count()], pos)
                    elif grid.get_state() == GridState.flag:
                        screen.blit(img_flag, pos)
                    elif grid.get_state() == GridState.ask:
                        screen.blit(img_ask, pos)
                    else:
                        print(game_state)
                        print(grid)
                        exit("something error unknown grid state")

        # else:
            # do nothing

        pygame.display.update()
        clock.tick(FPS)
    
# 简单介绍一下模块内各种函数的作用,具体的可以用时现查
# 把各种加载操作放到别出去,看代码的时候可以直接缩小他们。
if __name__ == "__main__":  # main
    pygame.init()  # 检查硬件调用接口、基础功能是否有问题(模糊一点就说初始化)
    # pygame.display.set_mode参数(分辨率, 显示模式=0, 颜色位数=0, 不知道=0, 不知道=0)
    menu_screen = pygame.display.set_mode((FIRST_SCREEN_WIDTH, FIRST_SCREEN_HEIGHT))
    My_init() # 自定义的初始化函数
    pygame.display.set_caption("扫雷") # 设置当前窗口标题
    # screen.blit参数(图像/字,位置,不知道,不知道)
    # 第三个参数好像与动态图有关,可能其他程序会用得到。
    menu_screen.blit(first_bac, (0, 0)) # 载入游戏开始界面
    # 之后要用这些参数定位鼠标动作,所以单起一个“稍短”的名字
    (text_width, text_height) = first_font.size("游戏难度")
    text_x = int((FIRST_SCREEN_WIDTH-text_width)/2)
    base_text_y = int(FIRST_SCREEN_HEIGHT/6)
    Print_text(menu_screen, first_font, text_x, base_text_y, "游戏难度", pink_color)
    Print_text(menu_screen, first_font, text_x, base_text_y*2, "简单模式", pink_color)
    Print_text(menu_screen, first_font, text_x, base_text_y*3, "中单模式", pink_color)
    Print_text(menu_screen, first_font, text_x, base_text_y*4, "困单模式", pink_color)
    reset_mode = False # 不会挑来跳去,便用一个bool变量代替。
    while True:
        # 获取事件,包括鼠标和键盘
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                exit("ok")
            # 暂时还想不到窗口最大化的好办法
            elif event.type == pygame.MOUSEBUTTONDOWN: # 鼠标按下(任何一键)
                screen_x, screen_y = event.pos # 获取鼠标点击位置的pygame坐标
                mouse_l, mouse_m, mouse_r = pygame.mouse.get_pressed()# 检测具体哪个键(bool型)(左,滚轮,右)
                if mouse_l == True:
                    # \ 表示承接上一行,因为一行写不开了。
                    if text_x <= screen_x <= text_x + text_width \
                        and base_text_y*2 <= screen_y <= base_text_y*2 + text_height:
                        game_screen = Set_easy_mode()
                        Secend_game_interface(game_screen)
                        # 退出来之后需要重新刷新屏幕。
                        reset_mode = True
                        # 这里要是有goto跳来跳去岂不美哉。但怕用多了自己也掌控不了它跳到哪里去了>_<

                    elif text_x <= screen_x <= text_x + text_width \
                        and base_text_y*3 <= screen_y <= base_text_y*3 + text_height:
                        game_screen = Set_medium_mode()
                        Secend_game_interface(game_screen)
                        reset_mode = True

                    elif text_x <= screen_x <= text_x + text_width \
                        and base_text_y*4 <= screen_y <= base_text_y*4 + text_height:
                        game_screen = Set_hard_mode()
                        Secend_game_interface(game_screen)
                        reset_mode = True

            if reset_mode == True:
                menu_screen = pygame.display.set_mode((FIRST_SCREEN_WIDTH, FIRST_SCREEN_HEIGHT))
                menu_screen.blit(first_bac, (0, 0)) # 载入游戏开始界面
                Print_text(menu_screen, first_font, text_x, base_text_y, "游戏难度", pink_color)
                Print_text(menu_screen, first_font, text_x, base_text_y*2, "简单模式", pink_color)
                Print_text(menu_screen, first_font, text_x, base_text_y*3, "中单模式", pink_color)
                Print_text(menu_screen, first_font, text_x, base_text_y*4, "困单模式", pink_color)
                reset_mode = False

        pygame.display.update()
        clock.tick(FPS)


# 此时一个正常扫雷该有的功能都具备了
# 接下来可以实现一些打趣的功能
# 1.将随机点开的第一个一直变为雷,直接输掉游戏
# 2.点完第一个,直接赢得游戏
# 这也好改可以直接改变输赢条件的逻辑。

主要添加了:

  1. 为了便于统计所剩旗子数,在ChessBoard类中新增self.LEFT_FLAG_COUNT变量,初始化为MINE_COUNT,并在标记操作mark_grid()中增加对其的修改。在类中添加get_left_flag_count(self):函数,返回self.LEFT_FLAG_COUNT。
  2. matrixTOscreen()矩阵左边转换为屏幕坐标;screenTOmatrix()屏幕坐标转换为矩阵坐标
    这里矩阵坐标是指(i,j)表示i行j列的格子。
    屏幕坐标是指pygame展示屏幕时,以其左上角为坐标原点,水平向右为x正半轴、竖直向下为y轴正半轴。

Secend_game_interface(screen):游戏界面和游戏逻辑:各种动作+游戏状态转变+屏幕显示。
至此,简单的扫雷游戏已经设计完毕了。

最终整个项目的文件均放在giteegithub了。

写在最后

这篇文章主要用来提醒自己时刻注意要尽可能快速地进行验证。不能一直在编码,要时刻注意测试。

猜你喜欢

转载自blog.csdn.net/2201_75691823/article/details/129302618