ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用Pygame实现大富翁:状态机设计、部署与自动化验证

用Pygame实现大富翁:状态机设计、部署与自动化验证 简介《基于 Pygame 的大富翁游戏》是一份面向毕业设计、课程设计或个人练手的完整游戏源码包尤其适合计算机相关专业在校学生作为项目初期的可运行原型进行二次开发。项目代码经过运行测试包含可执行的主程序、游戏贴图、音效、字体等素材以及 README 与游戏说明文档目录按 pic、font、sound、resource 等模块划分部署门槛低解压后按说明配置即可启动。压缩包共 37 个文件主要类型涵盖 png 图片素材、wav/ogg 音效、ttf 字体、py 主程序以及 md/txt 说明文件整体大小约 16.3MB结构紧凑便于按需替换素材或修改逻辑。当前已有 47 人学习下载可直接用于答辩演示、课程汇报或功能扩展的基础版本适合在现有架构上增加新角色、地图事件与交互玩法也可作为 Pygame 游戏开发的入门参考。1. 用 Pygame 做大富翁一个 zip 里能装多少部署细节一类走心或不走心的 Pygame 大富翁项目最终交付形态往往就是一个 zip 压缩包里面有 main.py、资源目录、教程文档运气好还有 requirements.txt。标题里“简单部署即可运行”这句话其实一点不比“功能完善”好写。Pygame 只负责窗口、绘制和事件分发大富翁该有的棋盘索引、回合状态机、地产结算、破产判定全在业务代码里。这篇顺着“基于 Pygame 的大富翁游戏”这个标题把从解压 zip 到跑起第一局的通用做法拆开讲先搭棋盘和状态再解决 Pygame 的安装与启动然后落到骰子、移动、买卖三段代码最后补两个能让答辩演示更稳的验证手段。适合正在做毕设的学生也适合接手二手 Python 游戏源码的工程师。2. 大富翁棋盘与回合状态机先定数据结构再写 Pygame 循环2.1 棋盘映射成数据结构格子索引与格子类型把大富翁棋盘硬编码在绘制函数里是最常见的返工源头。地图一改逻辑和渲染要同步改两份。我一般会把棋盘定义成一份纯数据列表用同一个列表驱动玩家移动、事件结算和 UI 绘制。每个格子是一个字典字段由格子类型决定不需要的字段直接不写。from enum import IntEnum class GridType(IntEnum): GO 0 # 起点过格发钱 PROPERTY 1 # 可购买地产 CHANCE 2 # 机会卡 TAX 3 # 税收直接扣钱 JAIL 4 # 停留一回合 BOARD [ {type: GridType.GO, name: 起点, pos: (0, 0)}, {type: GridType.PROPERTY, name: 长安街, price: 600, rent_levels: [60, 180, 540], pos: (1, 0)}, {type: GridType.CHANCE, name: 机会, pos: (2, 0)}, {type: GridType.TAX, name: 所得税, amount: 200, pos: (3, 0)}, {type: GridType.JAIL, name: 停留, pos: (4, 0)}, # 继续按顺时针排列到 28~36 格 ]type字段决定事件分支pos是格子在棋盘上的像素坐标绘制时直接取用不用再做坐标换算。rent_levels是三档租金比单档租金更接近原版规则答辩时也更有话讲。price只对PROPERTY生效其他格子读不到也不会报错这依赖 Python 字典的灵活性。2.1.1 为什么用 IntEnum 而不是普通整数GridType.PROPERTY和直接写1相比日志和报错信息里能直接看到名字。assert tile[type] GridType.PROPERTY失败时输出的是PROPERTY而不是难查的1。更重要的是枚举值可以安全地序列化进存档 JSON不会因为列表插入一个类型就整体错位。2.2 回合状态机把游戏循环切成五个状态PyGame 主循环是while True pygame.event.get()但大富翁的回合逻辑不能全堆在事件循环里。玩家状态、动画状态、结算状态互相交叉用状态机收敛是最稳的结构。我按回合流转切五个状态等待掷骰、骰子滚动、玩家移动、事件结算、换人。import pygame import random class GameState(IntEnum): WAIT_ROLL 0 ROLLING 1 MOVING 2 RESOLVING 3 NEXT_TURN 4 class RoundMachine: def __init__(self, players): self.players players self.current 0 self.state GameState.WAIT_ROLL self.steps 0 self.dice_anim_left 0 def handle_event(self, event): if self.state GameState.WAIT_ROLL: if event.type pygame.KEYDOWN and event.key pygame.K_SPACE: self.steps random.randint(1, 6) self.dice_anim_left 10 self.state GameState.ROLLINGWAIT_ROLL只有按下空格才推进连按不会一次掷两次骰因为状态已经切到ROLLING。ROLLING状态在每一帧的update()里递减dice_anim_left动画播完才进入MOVING。状态转移条件是集中判断的而不是散落在事件回调里后面排查“卡在某一格不动”会快很多。当前状态触发条件下一状态WAIT_ROLL按下空格ROLLINGROLLING动画帧数归零MOVINGMOVING剩余步数为 0RESOLVINGRESOLVING事件处理完毕NEXT_TURNNEXT_TURN切换玩家索引WAIT_ROLL2.3 玩家数据模型资金、地产与破产判定大富翁玩家对象不需要太多字段。pid区分身份position指向棋盘下标properties存地产下标列表资金就用一个整数。这样的一份模型足够撑起整局游戏扩展 AI 或联网时再往上加字段也不伤结构。class Player: def __init__(self, pid: int, name: str): self.pid pid self.name name self.money 10_000 self.position 0 self.properties [] def liquidate_value(self) - int: # 变卖估值按购入价 5 折 return self.money int(sum(0.5 * BOARD[p][price] for p in self.properties)) def is_bankrupt(self) - bool: return self.liquidate_value() 0破产判定用money 0是常见误判玩家可能现金为负但手上还有能变卖的地产。先用liquidate_value()算可变现总资产低于 0 才算真破产。变卖比例0.5是一个平衡点放得越高游戏越难结束设计文档里可以把这行单独拎出来解释成“经济参数”。3. 简单部署不代表没布置解压 zip 到 Pygame 跑起来的完整链路3.1 环境准备确认 Python 版本与安装 Pygame 的两种姿势Pygame 项目对 Python 版本要求通常很宽3.8 到 3.12 都能跑。真正容易踩坑的是机器上装了多个 Pythonpip命令对应的解释器和python不是同一个。我一般用python -m pip显式绑定解释器避免单写pip把包装错地方。python --version python -m pip --version python -m pip install pygamepython -m pip install这个写法先把python锁定再调用属于它的pip在 Windows 和 macOS 上基本不会装串。安装完成后可以执行python -c import pygame; print(pygame.version.ver)验证版本。3.1.1 Windows 与 Linux 上安装 Pygame 的差异pygame 2.x 对 Windows 和 macOS 有官方 wheel 包安装是秒级完成。卡住的情况基本出现在 LinuxPython 版本太新或 Pygame 版本太旧时pip拿不到对应 wheel会尝试从源码编译然后报failed to build pygame when getting requirements to build wheel。这个报错本质是缺少 SDL 开发库。# Debian/Ubuntu 下先补编译依赖 sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev python -m pip install pygame如果补了依赖仍编译失败另一个思路是降到 Python 3.10 或 3.11通常能直接拿到现成的 wheel。zip 里的教程如果只写了pip install pygame大概率是按 wheel 路线准备的首发跑不起来时优先怀疑版本匹配。3.2 解压 zip 后检查目录结构与资源路径解压后先别急着运行。看一眼入口是不是main.py资源是不是在assets或resources目录教程里的命令和实际目录结构对不对得上。大富翁项目一定有棋盘图、字体或音效文件路径写错会直接抛FileNotFoundError。文件/目录作用启动前检查main.py入口脚本是否存在入口函数在哪requirements.txt依赖清单pygame 是否锁版本assets/图片、音效、字体资源路径是否写死绝对路径README.md运行说明命令与实际目录是否一致如果脚本里写的是os.path.join(assets, board.png)那当前工作目录必须是项目根目录。我会在入口脚本顶部加一段基于__file__的兜底让资源路径不受启动位置影响。import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) os.chdir(BASE_DIR)加了这段之后无论从哪个目录执行python main.py资源都能被找到。对答辩现场这种经常换机器拷贝的项目这一步能挡掉相当一部分“在我电脑上明明是好的”。3.3 验证部署成功窗口、日志与帧率窗口出现不代表逻辑正常黑屏或卡在开始界面都是可能的。我会在项目里放一个调试开关把关键状态打到终端确认状态机在正循环。if __debug__: print(f[frame{frame}] state{machine.state.name} fplayer{machine.current} pos{player.position} money{player.money})正常状态是每帧一行日志回合切换时能看到状态名按表流转。如果日志停在ROLLING不动优先查动画帧数有没有归零如果停在WAIT_ROLL没反应查按键事件是不是被窗口焦点吞了。窗口标题栏如果显示 FPS那就用标题栏帧率做初步健康检查。现象可能原因排查方向ImportError: No module named pygame包装到了别的解释器改用python -m pip install pygameFileNotFoundError: assets/...工作目录不对在入口加os.chdir(BASE_DIR)窗口黑屏渲染循环没跑或资源没加载检查主循环里是否有pygame.display.flip()4. 核心机制代码化骰子、像素级移动与地产买卖的实现细节4.1 骰子random.randint 与关键帧动画骰子数值层就是一句random.randint(1, 6)但直接放在事件回调里会让画面“瞬发”玩家根本看不清点数。常见做法是掷骰后播一段短动画十来帧里随机闪点数最后一帧落下真实值。class Dice: def __init__(self): self.value 1 self.final_value 0 self.anim_frames 10 self.remaining 0 def roll(self): self.final_value random.randint(1, 6) self.remaining self.anim_frames def update(self) - bool: if self.remaining 0: self.remaining - 1 self.value random.randint(1, 6) if self.remaining 0: self.value self.final_value return self.remaining 0update()返回布尔值表示动画是否播完。主循环里只有返回True才允许状态机从ROLLING切到MOVING。因为是remaining在倒扣动画期间再按空格调roll()也无济于事避免重复掷骰。4.2 移动动画按像素插值而不是按格跳玩家的position如果直接跳到目标格效果太生硬。我一般会为玩家精灵保存像素坐标以目标格的pos为终点做插值。每帧移动固定像素数距离越近走得越慢视觉上接近原版走格子的手感。PIXELS_PER_TILE 80 class PlayerSprite(pygame.sprite.Sprite): def __init__(self, player, board): super().__init__() self.player player self.board board self.x, self.y board[player.position][pos] self.target_x, self.target_y self.x, self.y self.speed 6 def start_move(self, steps): old self.player.position target_idx (old steps) % len(self.board) self.player.position target_idx self.target_x, self.target_y self.board[target_idx][pos] def update(self): dx self.target_x - self.x dy self.target_y - self.y dist abs(dx) abs(dy) if dist 0: step min(self.speed, dist) self.x (dx / dist) * step self.y (dy / dist) * step注意start_move里已经改掉了player.position动画期间逻辑上玩家已在新格子只是渲染还在路上。事件结算读到的是最终格子不受动画影响。过起点发工资的判断要基于旧下标如果old steps len(self.board)说明绕了一圈该发钱。4.3 地产买卖资金校验、归属判断与确认弹窗落到地产格时完整链路是判断是否PROPERTY再判断是否有主。无主且现金够进入购买确认 UI有主则按租档收租。确认弹窗是一个独立 UI 状态不该在结算里用阻塞式循环等输入否则事件队列会被堵死。class GridState(IntEnum): DONE 0 ASK_BUY 1 def resolve_landing(self, player, tile): if tile[type] ! GridType.PROPERTY: return GridState.DONE if tile.get(owner) is not None: return self._pay_rent(player, tile) if player.money tile[price]: return GridState.ASK_BUY return GridState.DONE def _pay_rent(self, player, tile): owner self.players[tile[owner]] if player.money tile[rent_levels][0]: player.money - tile[rent_levels][0] owner.money tile[rent_levels][0] else: player.money - tile[rent_levels][0] return GridState.DONErent_levels[0]只取第一档租金后续升级逻辑可以在RentLevel上做文章。资金不足时仍然扣除租金允许现金为负由is_bankrupt()统一判定是否清算。确认弹窗的按键响应放在事件循环里处理K_y确认购买K_n放弃。if self.grid_state GridState.ASK_BUY: if event.type pygame.KEYDOWN and event.key pygame.K_y: player.money - tile[price] tile[owner] player.pid player.properties.append(player.position) self.grid_state GridState.DONE elif event.key pygame.K_n: self.grid_state GridState.DONE5. 毕设答辩前的落地检查用种子复现与自动化对局验证 Pygame 项目5.1 用固定随机种子让答辩演示可复现答辩最怕的是演示时摇出冷场点数或者抽到一张改变局势的机会卡打乱讲稿。在入口脚本里加两个参数一个固定随机种子一个开调试日志演示时用同一套种子复跑同一局讲稿和画面完全对得上。import argparse import random parser argparse.ArgumentParser() parser.add_argument(--seed, typeint, defaultNone) parser.add_argument(--debug, actionstore_true) args parser.parse_args() if args.seed is not None: random.seed(args.seed)固定种子后轮到谁、掷几点、抽什么卡整个序列在每次启动时都一样。配合调试日志现场可以照着回放跟评审讲“这一步走到机会卡抽到前进三步”把演示从碰运气变成念台词。5.2 一夜跑 500 局的自动化验证大富翁逻辑层可以完全脱离 Pygame 窗口做验证。把棋盘、玩家、掷骰、结算这些不依赖渲染的代码抽成独立模块然后写一个模拟器脚本连续跑几百局断言资金一致性和裁判状态不崩。这一步能在答辩前拦截掉绝大多数隐蔽逻辑错误。for game_idx in range(500): random.seed(game_idx) board create_board() players [Player(0, A), Player(1, B), Player(2, C)] machine RoundMachine(players) for _ in range(200): # 单局最多 200 回合 steps random.randint(1, 6) machine.steps steps machine.state GameState.MOVING # 简化直接按步数移动并结算 p machine.players[machine.current] p.position (p.position steps) % len(board) assert p.liquidate_value() 0 or p.is_bankrupt()这份脚本的价值在于把“手动点到凌晨测试”变成“跑一轮脚本睡觉”。500 局没有断言失败代码的核心一致性就有了保障。答辩时如果说“逻辑层经过 500 局模拟验证”比口头保证可靠得多。5.3 帧率与事件积压Pygame 项目常见的两个性能坑大富翁是回合制对实时性要求不高但有两个坑很典型。第一个是主循环没有用Clock限帧导致动画速度在 144Hz 显示器上比 60Hz 快一倍第二个是高 DPI 缩放下鼠标坐标偏移点击区域错位。clock pygame.time.Clock() while running: dt clock.tick(60)clock.tick(60)把主循环锁在 60 FPS。之后骰子动画帧数要改成时间单位比如 0.6 秒动画在 60 FPS 下是 36 帧而不是写死的 10 帧。鼠标点击用pygame.Vector2(event.pos)与 UI 矩形做碰撞判断减少整数坐标直接比较带来的边界误差。答辩前把窗口标题加上版本号和种子值比如大富翁 v1.0 seed42评审一眼就能看出这个项目考虑过可复现性和版本管理。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进