资讯动态

Pygame小游戏集合:四合一架构与碰撞检测实战

发布时间:2026/9/15 16:12:02 来源:尧图企业网站定制
简介一份基于pygame的Python小游戏合集内含贪吃蛇、五子棋、扫雷、俄罗斯方块四款经典作品面向Python初学者、编程学习者以及游戏开发爱好者适合作为入门练手与课程设计参考。压缩包共29个文件包含6个py源码、18个bmp图像素材、3个png运行截图、1个ttf字体文件及1个md说明文档整体仅182KB结构清晰便于边读源码边对应资源。四款游戏分别呈现贪吃蛇的食物生成与碰撞判定、五子棋的连珠检测与胜负判断、扫雷的雷区展开与标记、俄罗斯方块的生成旋转与消行覆盖键盘监听、事件队列、碰撞检测、图形绘制等pygame核心机制。已有548人学习下载源码可直接运行通过阅读和修改代码可理解经典游戏设计思路并在此基础上增加计分、音效或关卡功能进一步提升算法与调试能力。1. python小游戏集合一个入口切换四个 pygame 游戏的用意把贪吃蛇、五子棋、扫雷、俄罗斯方块放进同一个 python 小游戏集合直接价值不是炫耀代码量而是用一套 pygame 工程同时验证四种差异明显的游戏循环。贪吃蛇考验方向输入和碰撞链五子棋考验回合状态和棋盘扫描扫雷考验随机生成与状态揭示俄罗斯方块考验形状旋转、重力下落和消行统计。这四类逻辑的输入时机和更新节奏完全不同扔进同一个启动器后主循环反而变得更简单。基于 pygame 做这套集合的常见做法是把每局游戏封装成统一接口只对事件、更新、绘制三个入口负责。这样菜单切换、重开一局、暂停恢复都不需要改内部逻辑后续再塞第五个游戏也只是多注册一个类。下面是完整可跑的工程设计。2. 基于 pygame 搭建四合一工程骨架事件、时钟与渲染分离2.1 目录与公共模块划分不搞复杂框架工程结构按模块职责切一刀# 项目根目录 # ├── main.py # 四合一入口 # ├── base.py # GameBase 基类 # └── games/ # 四个小游戏独立模块 # ├── snake.py # ├── gobang.py # ├── minesweeper.py # └── tetris.pygames 目录里每个文件都导出同一个 Game 类构造参数统一接收 screen。main.py 不关心内部棋盘实现只负责把事件列表传给当前游戏。这个划分的核心价值在于回放和测试每个 Game 类可以在无窗口模式下单独构造逻辑层完全不依赖具体窗口对象后面第 5 章会用到这个特性做 headless 自检。2.2 统一 Game 接口与主循环base.py 里定义一个最薄基类# base.py import pygame class GameBase: def __init__(self, screen): self.screen screen self.running True def handle_events(self, events): for e in events: if e.type pygame.QUIT: self.running False def update(self): pass def draw(self): passhandle_events 默认只处理窗口关闭事件保证玩到一半点右上角叉能退出。四个子类各自覆盖 update 和 draw而 main.py 的长循环只需要写一次# main.py import pygame from games.snake import Game as SnakeGame from games.gobang import Game as GobangGame from games.minesweeper import Game as MineGame from games.tetris import Game as TetrisGame GAME_TABLE { 1: (贪吃蛇, SnakeGame), 2: (五子棋, GobangGame), 3: (扫雷, MineGame), 4: (俄罗斯方块, TetrisGame), } def main(): pygame.init() screen pygame.display.set_mode((900, 680)) clock pygame.time.Clock() running True while running: choice input(1 贪吃蛇 2 五子棋 3 扫雷 4 俄罗斯方块 q 退出\n) if choice q: running False break if choice not in GAME_TABLE: continue current GAME_TABLE[choice][1](screen) while current.running: events pygame.event.get() current.handle_events(events) current.update() current.draw() pygame.display.flip() clock.tick(60) pygame.quit() if __name__ __main__: main()参数说明pygame.event.get()每帧只取一次四个游戏共享一个事件序列避免同一帧重复处理某次按键clock.tick(60)把渲染循环锁在 60 FPS。但注意贪吃蛇移动、俄罗斯方块下落这种“逻辑节奏”不能用总帧率硬锁要放到 update 里用时间差控制否则不同游戏切换后手感不一样。2.3 四类游戏的网格与帧率参考值四个游戏对网格密度和更新节奏的需求差很多下面这组参数是我在一套小游戏集合里默认使用的初值游戏画面尺寸网格逻辑更新频率关键注意点贪吃蛇640x48032x2410 次/秒方向要用队列缓存五子棋600x64015x1530 FPS鼠标点击要量化到交叉点扫雷480x40016x1630 FPS深展开用队列不用递归俄罗斯方块300x60010x2060 FPS 渲染下落间隔按关卡加速贪吃蛇的 10 次/秒不是让clock.tick(10)去锁总帧率而是在 update 里累计pygame.time.get_ticks()达到 1000/10 毫秒才让蛇位移一格。这样渲染保持流畅键盘按键不会堆进缓存也不会出现“按一次方向键连续转两次”的经典问题。3. 贪吃蛇与五子棋方向输入、碰撞链与五连判定3.1 贪吃蛇的“撞尾巴误判”死亡判定要排除正在离开的尾节贪吃蛇最容易写错的地方是“刚好吃到食物那一帧的碰撞检测”。很多教程直接写new_head in self.snake这在擦身而过时会误伤蛇头要去的格子正好是当前尾巴的位置而这一帧尾巴本来就要前移结果被判定成撞墙。正确做法是先排除尾部再查碰撞。# games/snake.py 核心逻辑 import random import pygame GRID_W, GRID_H 32, 24 CELL 20 DIRS { pygame.K_UP: (0, -1), pygame.K_DOWN: (0, 1), pygame.K_LEFT: (-1, 0), pygame.K_RIGHT: (1, 0), } class Game: def __init__(self, screen): self.screen screen self.reset() def reset(self): self.snake [(GRID_W // 2, GRID_H // 2)] self.direction (1, 0) # 起始向右 self.running True self.food self._spawn_food() def handle_events(self, events): for e in events: if e.type pygame.KEYDOWN and e.key in DIRS: dx, dy DIRS[e.key] # 反方向直接忽略保证不会“瞬间掉头撞到自己” if (dx, dy) ! (-self.direction[0], -self.direction[1]): self.direction (dx, dy) def update(self): head self.snake[0] new_head (head[0] self.direction[0], head[1] self.direction[1]) # 排除尾节后再查身体否则会误判“正离开的尾巴”为碰撞 body_without_tail self.snake[:-1] if new_head in body_without_tail: self.running False return if not (0 new_head[0] GRID_W and 0 new_head[1] GRID_H): self.running False return self.snake.insert(0, new_head) if new_head self.food: self.food self._spawn_food() else: self.snake.pop()关键点在于body_without_tail self.snake[:-1]这句。蛇没吃到食物时尾巴即将移动所以尾节不算碰撞体蛇吃到食物时尾巴本来就该保留但此时新头不可能落在尾节位置因为食物不会生成在蛇身上。边界判断放在身体判断之后也可以先判边界再判身体顺序不影响正确性关键是别把“碰尾巴”算成必死。食物生成要注意避开蛇身否则下一帧会出现“食物在蛇身体里”的脏状态def _spawn_food(self): while True: pos (random.randrange(GRID_W), random.randrange(GRID_H)) if pos not in self.snake: return pos蛇越长这个 while 循环的期望次数越高但 32x24 棋盘上最坏情况也只是几百次尝试完全可接受。如果以后换成上千格的超大棋盘再用格子集合来采样。3.2 五子棋胜负判定四个方向一次扫描五子棋逻辑层比渲染简单核心就两件事点坐标换算成棋盘行列落子后判断是否有五连。# games/gobang.py 胜负判断 SIZE 15 def check_win(board, row, col): color board[row][col] for dx, dy in ((1, 0), (0, 1), (1, 1), (1, -1)): total 1 for step in (1, -1): nr, nc row dx * step, col dy * step while 0 nr SIZE and 0 nc SIZE and board[nr][nc] color: total 1 nr dx * step nc dy * step if total 5: return True return False四个方向的向量覆盖了全部直线连五可能方向dxdy覆盖的线横向10水平线纵向01垂直线主对角线11左下到右上副对角线1-1左上到右下每次落子只从当前点向两个方向延伸并累计同色数量累计值大于等于 5 就直接判定不需要全盘扫描。这个写法比“每次下完遍历整个棋盘”快得多15x15 棋盘上差距不明显但逻辑更清晰。鼠标点击转棋盘坐标时最容易翻车的是交叉点和格子的偏移。常见做法是画面四周留边距格子中心点等于BOARD_LEFT col * CELL点击后取最近的交叉点def cell_from_mouse(pos): x, y pos col round((x - BOARD_LEFT) / CELL) row round((y - BOARD_TOP) / CELL) if 0 col SIZE and 0 row SIZE: return row, col return None这里round而不是int因为鼠标点到两个交叉点中间时应该就近落子。渲染时棋子圆心放在BOARD_LEFT col * CELL棋盘线则按CELL间隔画两边共用同一套常量避免出现“棋子和网格错半格”的视觉偏差。4. 扫雷与俄罗斯方块随机生成与状态机设计4.1 扫雷的布雷、数字计算与队列展开扫雷的雷区生成和翻开是两个独立阶段。生成阶段用random.sample一次抽完雷位避免循环随机带来的重复判断。# games/minesweeper.py 雷区生成 import random NEIGHBORS [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] def create_minefield(rows, cols, mine_count): cells [[0] * cols for _ in range(rows)] for p in random.sample(range(rows * cols), mine_count): r, c divmod(p, cols) cells[r][c] -1 # -1 表示雷 for r in range(rows): for c in range(cols): if cells[r][c] -1: continue cells[r][c] sum( 1 for dr, dc in NEIGHBORS if 0 r dr rows and 0 c dc cols and cells[r dr][c dc] -1 ) return cellsrandom.sample(range(rows * cols), mine_count)保证了雷位互不重复divmod把一维下标还原成二维行列。数字计算用sum配合生成器只统计 8 邻域内的雷数。翻牌展开这步我建议用队列而不是递归# 翻开 area: 当点到的格子是 0 时向外扩散 from collections import deque def reveal_area(board, visible_dirty, rows, cols, start_r, start_c): # visible_dirty 初始全 FalseTrue 表示已经翻开 q deque([(start_r, start_c)]) while q: r, c q.popleft() if not (0 r rows and 0 c cols): continue if visible_dirty[r][c] or board[r][c] -1: continue visible_dirty[r][c] True if board[r][c] 0: for dr, dc in NEIGHBORS: q.append((r dr, c dc))用栈或队列的核心原因不是递归写法不对而是 16x16 棋盘上空白区域连片时递归深度可能到几十层Python 默认递归限制虽然够用但扫雷后续如果改成更大棋盘递归版容易踩RecursionError。队列把所有待展开节点摊平渲染层也更容易在展开过程中逐格动画。右键插旗只改一个状态数组不要直接改雷区数值。雷区数组cells是从开局就固定的插旗、问号都放在独立布尔数组里否则翻牌逻辑会读到被污染的数字。4.2 俄罗斯方块形状表、旋转矩阵与消行俄罗斯方块建议用坐标元组表示形状而不是二维矩阵。7 种方块落在一个 4x4 单元范围内用元组列表方便碰撞检测。# games/tetris.py 形状表与旋转 SHAPES { I: [(0, 0), (1, 0), (2, 0), (3, 0)], O: [(0, 0), (1, 0), (0, 1), (1, 1)], T: [(0, 0), (1, 0), (2, 0), (1, 1)], S: [(1, 0), (2, 0), (0, 1), (1, 1)], Z: [(0, 0), (1, 0), (1, 1), (2, 1)], J: [(0, 0), (0, 1), (1, 1), (2, 1)], L: [(0, 1), (1, 1), (2, 1), (2, 0)], } def rotate(shape): # 绕原点逆时针旋转后再归一化到非负坐标 rotated [(-y, x) for x, y in shape] min_x min(x for x, _ in rotated) min_y min(y for _, y in rotated) return [(x - min_x, y - min_y) for x, y in rotated]旋转公式核心是坐标映射(x, y) - (-y, x)这个变换每用一次代表转 90 度。O 方块四个格子旋转后不变所以不需要特殊处理。但注意旋转后要对坐标做归一化否则有些形状会转出负坐标导致碰撞检测误判。碰撞检测要区分“正在下落的方块”和“已固定的棋盘格”COLS, ROWS 10, 20 def collides(board, shape, offset_x, offset_y): for px, py in shape: x offset_x px y offset_y py if x 0 or x COLS or y ROWS: return True if y 0 and board[y][x] ! 0: return True return Falsey 0这个判断必须有因为新方块从顶部落下来时部分格子可能还在画布外此时只检查横向越界和固定块冲突不把顶部未进入区域判成碰撞。很多初学者把y 0也直接判死结果方块一出生就报结束。消行代码写在固定落块之后def clear_full_rows(board): new_board [] cleared 0 for row in board: if all(cell ! 0 for cell in row): cleared 1 else: new_board.append(row) for _ in range(cleared): new_board.insert(0, [0] * COLS) return new_board, cleared注意这里board的行顺序和渲染方向要一致。pygame 的 y 轴向下所以board[0]是屏幕最上面一行消行后补的新空行插在顶部符合俄罗斯方块“上方生成新行”的直觉。如果反过来从底部补行整个屏幕会出现下移错误。5. 用 SDL dummy 驱动验证四个小游戏逻辑并处理 pygame 安装报错5.1 无头回放脚本让回归问题提前暴露四个游戏合并维护后改一个游戏常会误伤另一个。比如调整GameBase接口时忘改某个子类或者公共常量被重复定义。此时可以写一个不弹窗的回放脚本直接在服务器或本地终端里跑逻辑层SDL_VIDEODRIVERdummy python scripts/replay_tetris.py或者在 Python 文件顶部强制指定# scripts/replay_all.py import os os.environ.setdefault(SDL_VIDEODRIVER, dummy) import pygame from games.tetris import Game as TetrisGame pygame.init() screen pygame.display.set_mode((1, 1)) game TetrisGame(screen) for _ in range(300): events [] game.handle_events(events) game.update() if not game.running: print(Tetris exited early at frame, _) break else: print(Tetris survived 300 frames without window)SDL_VIDEODRIVERdummy让 pygame 不创建真实窗口set_mode((1, 1))只提供一个最小 Surface。把事件列表传空列表进来本质上是在纪律化地测 update 和 draw 的崩溃风险。对贪吃蛇和俄罗斯方块这种持续运动型游戏很有用能暴露越界和消行逻辑的边界问题。5.2 pygame 安装与初始化常见错误python 环境里装 pygame 报failed to build pygame when getting requirements to build wheel时先看 pip 版本和 Python 版本再确认本机是否缺 SDL2 相关依赖。macOS 和多数 Linux 发行版上装好系统依赖后重装即可。不要在 wheel 报错时强行加--no-build-isolation那会把问题转移到编译环境上排错更难。运行时报pygame.error: No available video device通常发生在远程服务器或 Docker 容器里。这个环境根本没有显示设备不需要真实窗口直接把SDL_VIDEODRIVER环境变量设成dummy就能跑逻辑测试。如果四个小游戏还要对外展示则把 pyinstaller 打包命令里的隐藏导入补上pyinstaller --hidden-importpygame --add-data games:games main.py多文件工程打包时最容易漏掉games目录运行时表现为菜单能进但选择游戏后ModuleNotFoundError。检查打包产物时优先盯这个导入关系。无头回放脚本最好跟着每个游戏模块一起提交而不是某次临时调试完就删掉。像“蛇撞尾巴误判”“消行后顶部补行”“扫雷展开越界”这些四合一集合里最容易反复出现的回归点脚本里都能用固定种子复现。把逻辑层和 pygame 界面层焊死是这类小游戏集合维护到后期最容易翻车的地方而 dummy 驱动配合固定帧数回放能第一时间把问题定位到具体模块。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价