免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

用Python手写命令行推理游戏,带你通关猜凶手赌局

用Python手写命令行推理游戏,带你通关猜凶手赌局 开局先聊聊这个项目的由来。如果你是一个推理游戏爱好者可能也有过类似的经历和朋友打赌“我一定能猜出凶手”结果一开口就被对方的狡黠提示带偏最后赌输了还要被笑话很久。这次要写的项目就是把这种“打赌推理”的场景搬进命令行做一个用 Python 实现的交互式推理小游戏——“小可与斯比的猜凶手赌局”。玩家扮演小可NPC 斯比和你打赌在有限的行动次数内你能否通过调查线索、盘问嫌疑人最终正确指认凶手如果你猜错了斯比就会赢下这场赌局。这个小项目非常适合用来练习 Python 基础语法、数据建模、状态管理和输入校验代码不依赖任何第三方库只要装了 Python 3 就能直接运行。接下来我会从玩法设计、数据模型、核心逻辑到完整代码逐步拆解最后补充常见报错排查和工程化扩展建议不管你是刚学 Python 的初学者还是想拿小项目练手巩固基础的开发者这篇文章都值得收藏慢慢看。1. 项目背景与玩法设计1.1 为什么写这样一个推理小游戏很多人学 Python 时都会写猜数字、计算器、通讯录这类入门项目但写到后面总觉得缺点意思逻辑太直白缺少“状态”和“分支”的练习。推理游戏恰好能弥补这个缺口。它需要你设计一套数据模型来描述人物和线索需要一套规则来管理玩家的行动次数需要用条件分支来判定输赢还要保证随机事件不会让游戏逻辑崩塌。这个项目的灵感来自一个很简单的场景两个人对一件事的真相产生分歧其中一个说“我赌你猜不到”另一个接招“猜就猜猜错了再说”。这里的“小可”和“斯比”就是玩家和 NPC 的关系。小可要在有限行动次数内从若干嫌疑人里找出真凶斯比则负责提供“看似有帮助实际可能误导”的提示让整个赌局充满变数。1.2 游戏规则设计在写代码之前先把规则定清楚。整个游戏围绕“行动次数”展开具体规则如下系统会从 6 名嫌疑人中随机指定一名作为真凶。玩家初始拥有 5 点行动次数。查看案件简报不消耗行动次数。查看嫌疑人资料不消耗行动次数。调查一名嫌疑人消耗 1 点行动次数返回一条线索。询问斯比消耗 2 点行动次数斯比会给出一条提示。玩家可以随时指认凶手指认后立即判定胜负。如果行动次数用完还没有指认也算玩家输。这样的设计让玩家必须规划行动次数调查得越多信息越多但次数消耗越快询问斯比虽然能获得额外信息但代价更高。如何在信息获取和行动次数之间找到平衡就是游戏的核心策略。1.3 功能清单按照上面的设计这个项目需要实现以下功能展示案件背景故事。展示嫌疑人基础档案。支持玩家调查嫌疑人并生成随机线索。支持玩家询问斯比并获得提示。支持玩家指认凶手并判定胜负。输赢结果对应不同的结局文案。这些功能正好覆盖了 Python 编程中的几个关键点数据结构设计、随机数处理、字典与列表操作、循环菜单、异常捕获、函数拆分。接下来我们逐步实现。2. 环境准备与项目结构2.1 运行环境这个项目只需要 Python 标准库不依赖任何第三方包。推荐使用 Python 3.8 及以上版本Windows、macOS、Linux 都可以运行。如果你用的是 Windows 系统运行 Python 脚本时出现中文乱码可以在命令行先执行下面这条命令把控制台编码切换到 UTF-8chcp 65001使用 VS Code、PyCharm 或者其他 IDE 的同学确保文件保存格式是 UTF-8 即可。2.2 项目目录结构为了方便维护和后续扩展我建议把项目拆成三个文件每个文件职责单一guess-murderer/ ├── cases.py # 案件数据与嫌疑人数据 ├── game.py # 游戏状态与核心逻辑 └── main.py # 程序入口与命令行交互cases.py负责存放案件描述、嫌疑人档案等静态数据。game.py负责游戏状态管理比如真凶是谁、剩余行动次数、当前怀疑度等。main.py负责接收用户输入、展示菜单、调用游戏逻辑。这种拆分方式符合“数据与逻辑分离”的工程习惯。以后如果想把数据改成 JSON 或数据库存储只需要修改cases.py的数据来源不需要改动游戏逻辑。2.3 为什么采用这种结构小项目也要有模块化的意识。如果所有代码都堆在一个文件里功能简单时问题不大但一旦想扩展多案件、多结局、存档读档就会变得很痛苦。拆分成独立模块后每个模块都可以单独测试也方便多人协作。在后面的项目中你会感受到这种设计带来的好处。3. 核心数据结构与推理逻辑3.1 嫌疑人数据模型每个嫌疑人需要包含以下字段id嫌疑人编号用于玩家输入和内部判断。name姓名用于展示和胜负判定。role职业或身份让角色更立体。description基础档案描述展示给玩家看。real_clue真实线索只有调查该嫌疑人时有一定概率获得。fake_clue干扰线索会让玩家误判。用一个字典列表来存储 6 名嫌疑人结构清晰也方便以后改成从 JSON 文件读取。每条真实线索和干扰线索都要写得足够有代入感让玩家在阅读时能真正进入推理状态。3.2 线索系统与怀疑度为了让推理过程更有策略性我引入了“怀疑度”机制。游戏内部维护一个分数表记录每个嫌疑人被玩家怀疑的程度调查真凶并命中真实线索对应嫌疑人怀疑度 3。调查无辜者并命中真实线索怀疑度不变。调查任何人但命中干扰线索随机给某一名无辜者怀疑度 2。为什么这么设计因为现实中调查线索并不是次次都有价值。可能你查了一个人发现他行为可疑结果他只是在偷偷相亲再查一个人表面毫无破绽反而最可疑。这种随机扰动让游戏具备了重玩价值。玩家虽然看不到具体分数但可以根据嫌疑人资料、调查结果和斯比的提示自己判断。分数机制只是程序内部的决策依据也可以完全不做展示让推理更接近真实。3.3 打赌胜负判定规则胜负判定是游戏的最终出口规则必须简单明确玩家指认的嫌疑人 id 等于真凶 id玩家获胜。玩家指认错误斯比获胜。行动次数清零仍未指认斯比获胜。由于真凶是随机指定的每次游玩都会有不同的答案。如果希望调试时复现某次结果可以在game.py初始化时给random.seed()传入固定数字方便测试。4. 完整代码实现4.1 编写 cases.py文件路径guess-murderer/cases.py。这个文件存放案件开场白和嫌疑人数据。为了让故事更有夜晚茶楼失窃的氛围我设计了以下 6 个角色当铺老板、茶馆常客、更夫、杂耍班艺人、账房先生、茶楼伙计。每个人都有动机和不在场证明真假线索需要玩家自行判断。# 文件路径guess-murderer/cases.py CASE_INTRO 深夜的“清风茶楼”发生了一起离奇失窃案掌柜最珍爱的紫金烟斗不翼而飞。 当晚茶楼里共有 6 位客人。斯比声称他已经知道真相但他不愿直接告诉你 只愿意和你打赌如果你能在有限的调查次数内猜出凶手他就认输 如果你猜错了那这场赌局就是他赢了。 你小可接下了这场赌局。 凶手就在这 6 个人中间开始调查吧。 SUSPECTS [ { id: 1, name: 王富贵, role: 当铺老板, description: 体型微胖穿着棕色棉袍右手拇指戴着一枚翠玉扳指。, real_clue: 你发现他的鞋底带有后院泥土但他声称整晚没离开过座位。, fake_clue: 你看见他一直在和账房先生讨论当票利息似乎并不关心失窃案。 }, { id: 2, name: 李翠花, role: 茶馆常客, description: 衣着整洁手腕上戴着一串成色很新的玉镯说话声音很轻。, real_clue: 她手上那串玉镯看起来是最近才买的但她却说不出是在哪家店买的。, fake_clue: 她整个晚上都坐在角落里喝茶几乎没有离开过桌子。 }, { id: 3, name: 张铁柱, role: 更夫, description: 身板结实手里始终握着打更用的梆子神情有些疲惫。, real_clue: 他声称案发时段在后院打盹可后院地面却没有他平时睡觉时铺的草席。, fake_clue: 他一直在打哈欠看起来确实困得不行应该没有精力去偷东西。 }, { id: 4, name: 赵小美, role: 杂耍班艺人, description: 腰间挂着一个旧布袋动作敏捷说话带着外地口音。, real_clue: 她的布袋里装着一块与紫金烟斗同色的绸布问她来源时她支支吾吾。, fake_clue: 她当晚在茶楼中央表演了一段杂耍很多人都能证明她一直在前厅。 }, { id: 5, name: 孙文才, role: 账房先生, description: 戴着一副圆框眼镜手里总拿着毛笔袖口沾着墨渍。, real_clue: 他的账本里夹着一张纸条上面写着‘丑时三刻后院见’字迹很新。, fake_clue: 他一直在写写画画有人路过时看到确实是在记流水账。 }, { id: 6, name: 周大福, role: 茶楼伙计, description: 穿着茶楼短褂神色紧张双手一直搓着衣角。, real_clue: 有人看见案发后他偷偷把一包东西塞进了柜台下面的暗格里。, fake_clue: 他整晚都在忙着端茶倒水其他客人对他的服务态度表示满意。 } ]这里有一点需要注意不要把“看起来可疑”和“一定是凶手”画等号。比如赵小美布袋里的绸布可能只是巧合孙文才的纸条也可能另有隐情。玩家需要结合多条线索综合判断。4.2 编写 game.py文件路径guess-murderer/game.py。这一部分是整个游戏的核心。我定义了一个GuessMurdererGame类负责初始化游戏状态、生成真凶、处置调查事件、生成斯比提示、判定胜负。# 文件路径guess-murderer/game.py import random from cases import SUSPECTS, CASE_INTRO class GuessMurdererGame: def __init__(self, max_actions5, seedNone): if seed is not None: random.seed(seed) self.case_intro CASE_INTRO self.suspects SUSPECTS self.murderer random.choice(SUSPECTS) self.max_actions max_actions self.remaining_actions max_actions self.suspect_score {suspect[id]: 0 for suspect in SUSPECTS} self.finished False self.winner None def show_intro(self): return self.case_intro def show_suspect_list(self): lines [] for suspect in self.suspects: lines.append( f{suspect[id]}. {suspect[name]}{suspect[role]} ) return \n.join(lines) def show_suspect_detail(self, suspect_id): for suspect in self.suspects: if suspect[id] suspect_id: return f{suspect[name]}{suspect[role]}{suspect[description]} return 没有这个嫌疑人。 def investigate(self, suspect_id): if self.finished: return 游戏已经结束无法继续调查。 if self.remaining_actions 0: return 行动次数已经用完请直接指认凶手。 self.remaining_actions - 1 for suspect in self.suspects: if suspect[id] suspect_id: # 80% 概率获得真实线索20% 概率获得干扰线索 if random.random() 0.8: if suspect[id] self.murderer[id]: self.suspect_score[suspect_id] 3 return f你调查了{suspect[name]}{suspect[real_clue]} else: # 干扰线索可能把一个无辜者推向怀疑中心 innocent_ids [ s[id] for s in self.suspects if s[id] ! self.murderer[id] ] if innocent_ids: target random.choice(innocent_ids) self.suspect_score[target] 2 return f你调查了{suspect[name]}{suspect[fake_clue]} return 没有这个嫌疑人。 def ask_siby(self): if self.finished: return 游戏已经结束无法再询问斯比。 if self.remaining_actions 2: return 行动次数不足询问斯比需要消耗 2 点行动次数。 self.remaining_actions - 2 murderer_hints [ 那个人在案发后问过一句奇怪的话。, 那个人今晚换过一件外套但没人注意到他原来的衣服是什么颜色。, 那个人对后院的地形特别熟悉步行的声音比其他人轻很多。, 那个人在斯比问话时眼睛老是往柜台方向看。 ] other_hints [ 那个人前半夜其实一直待在包厢里根本没机会靠近柜台。, 那个人根本不认识紫金烟斗甚至不知道这个烟斗长什么样。, 那个人到茶楼是为了等人始终没有离开过前厅。, 那个人听到失窃消息的时候表情很惊讶但不像是在演戏。 ] if random.random() 0.5: return 斯比压低声音说“提示你一句 random.choice(murderer_hints) ” else: return 斯比耸耸肩说“我给你个提示 random.choice(other_hints) ” def accuse(self, suspect_id): if self.finished: return 游戏已经结束。 self.finished True for suspect in self.suspects: if suspect[id] suspect_id: if suspect[id] self.murderer[id]: self.winner small_ke return f你指着{suspect[name]}说凶手就是你\n斯比叹了口气说好吧小可你赢了。 else: self.winner siby return ( f你指着{suspect[name]}说凶手就是你\n f斯比笑了错了错了真正的凶手是{self.murderer[name]}\n f小可这场赌局是我赢了。 ) return 没有这个嫌疑人。 def run_out(self): if self.remaining_actions 0 and not self.finished: self.finished True self.winner siby return ( f行动次数已经用完但你还没能指认凶手。\n f斯比得意地说看来你连猜都不敢猜这场赌局当然是我赢了。\n f真正的凶手是{self.murderer[name]}{self.murderer[role]}。 ) return None这段代码的关键逻辑investigate方法中random.random() 0.8表示有 80% 概率获得真实线索20% 概率被干扰线索带偏。suspect_score字段用于记录怀疑度虽然目前没有直接展示但可以在扩展版本中用来影响结局。ask_siby方法里斯比的提示有一半概率指向真凶一半概率是无关信息。这符合他爱打赌、爱逗人的性格。accuse方法是最终判定一旦调用就会结束游戏后续不能再调查或询问。4.3 编写 main.py文件路径guess-murderer/main.py。作为程序入口main.py负责循环展示菜单、获取用户输入、调用游戏逻辑并对用户输入做基本的异常处理。# 文件路径guess-murderer/main.py from game import GuessMurdererGame def print_menu(): print(\n * 40) print(1. 查看案件简报) print(2. 查看嫌疑人名单) print(3. 查看嫌疑人资料) print(4. 调查嫌疑人) print(5. 询问斯比) print(6. 指认凶手) print(7. 退出游戏) print( * 40) def read_choice(): try: return int(input(请输入操作编号: ).strip()) except ValueError: return -1 def read_suspect_id(): try: return int(input(请输入嫌疑人编号: ).strip()) except ValueError: return -1 def main(): game GuessMurdererGame() print(game.show_intro()) while not game.finished: print(f\n剩余行动次数{game.remaining_actions}) print_menu() choice read_choice() if choice 1: print(game.show_intro()) elif choice 2: print(game.show_suspect_list()) elif choice 3: suspect_id read_suspect_id() print(game.show_suspect_detail(suspect_id)) elif choice 4: if game.remaining_actions 0: print(行动次数已经用完请直接指认凶手。) continue suspect_id read_suspect_id() print(game.investigate(suspect_id)) elif choice 5: print(game.ask_siby()) elif choice 6: suspect_id read_suspect_id() print(game.accuse(suspect_id)) elif choice 7: print(你选择退出游戏斯比笑着说不猜到底可不算赢哦。) break else: print(无效输入请输入 1 - 7 之间的数字。) if not game.finished and game.remaining_actions 0: message game.run_out() if message: print(message) break if game.finished: print(\n游戏结束。) if game.winner small_ke: print(主角小可获胜) else: print(本局胜者斯比) if __name__ __main__: main()在main.py中每次进入循环前先打印剩余行动次数方便玩家决策。如果行动次数为 0 且游戏尚未结束就调用run_out方法触发超时判定。4.4 运行与验证在命令行进入项目目录执行python main.py预期会先看到案件背景介绍然后进入循环菜单。下面是一次模拟运行的关键输出深夜的“清风茶楼”发生了一起离奇失窃案掌柜最珍爱的紫金烟斗不翼而飞。 ... 剩余行动次数5 1. 查看案件简报 2. 查看嫌疑人名单 3. 查看嫌疑人资料 4. 调查嫌疑人 5. 询问斯比 6. 指认凶手 7. 退出游戏 请输入操作编号:输入4调查嫌疑人输入编号3输出类似你调查了张铁柱他声称案发时段在后院打盹可后院地面却没有他平时睡觉时铺的草席。再输入5询问斯比输出类似斯比压低声音说“提示你一句那个人在案发后问过一句奇怪的话。”当玩家指认凶手后程序会立即输出胜负结局。4.5 结果说明如果玩家指认正确你指着周大福说凶手就是你 斯比叹了口气说好吧小可你赢了。如果玩家指认错误你指着李翠花说凶手就是你 斯比笑了错了错了真正的凶手是周大福 小可这场赌局是我赢了。如果行动次数耗尽仍未指认行动次数已经用完但你还没能指认凶手。 斯比得意地说看来你连猜都不敢猜这场赌局当然是我赢了。 真正的凶手是周大福茶楼伙计。到这里一个完整的命令行推理小游戏就可以运行了。5. 常见问题与排查思路在实际运行过程中新手最容易遇到下面几个问题。我整理成了一张表格方便对照排查。问题现象常见原因解决思路输入数字后程序直接报错输入内容无法转换为整数int()抛出 ValueError用try-except捕获异常并提示用户重新输入游戏调查完一个人后无法继续行动次数已经耗尽但玩家没有意识到在循环开头显示剩余行动次数行动次数为 0 时自动触发超时判定Windows 控制台中文乱码控制台默认编码不是 UTF-8执行chcp 65001或在 IDE 中设置文件编码为 UTF-8每次运行凶手都一样手动设置了随机种子去掉seed参数或者用不同的固定种子调试误输入了负数或超大数字输入校验不够严格在read_suspect_id中判断编号是否在合法范围内提示“没有这个嫌疑人”玩家输入的编号不在 1 到 6 范围内在菜单提示中标注编号范围并在代码中做边界判断排查这类问题有一个通用思路先确认输入数据是否合法再确认程序状态是否符合预期最后检查逻辑分支是否遗漏。把这三个环节过一遍大部分小 bug 都能定位。6. 项目扩展与工程实践建议6.1 可扩展方向这个 300 行左右的小项目只是一个起点它的架构留下了足够多的扩展空间。多案件扩展在cases.py中增加多个案件在Game初始化时传入案件编号玩家可以自由选择故事线。数据文件化把嫌疑人数据迁移到 JSON 或 YAML 文件用json.load读取这样新增角色时不需要改代码。完备的怀疑度展示游戏结束后展示每个嫌疑人的怀疑度让玩家复盘自己的推理过程。GUI 版本用 Tkinter 或 Web 界面替代命令行输入把菜单按钮化体验会更接近真正的推理游戏。存档与读档把当前游戏状态序列化到文件下次运行时恢复方便玩家连续体验。6.2 工程实践建议虽然这是一个学习项目但也可以带着工程思维去写。以下几条是我在实际开发中觉得非常重要的习惯数据与逻辑分离。候选人、线索、案件描述都属于数据调查、评分、判定都属于逻辑。数据变了逻辑不应该跟着大改。输入校验要统一。所有用户输入入口都走同一个校验函数避免散落各处导致漏判。特别是在命令行程序里用户输入是不可信的所有来自input()的内容都要假设可能是脏数据。状态管理要集中。游戏是否结束、行动次数剩多少、真凶是谁这些状态统一由Game对象管理避免在多个函数里传来传去。这样代码可读性更强也更方便测试。异常处理要合理。不能只写try-except而不处理异常至少要给用户一个友好的提示而不是让堆栈信息直接刷屏。对于这个项目捕获ValueError并提示重新输入就足够了。为调试留后门。比如给Game构造函数加一个可选seed参数测试时就能稳定复现同一个凶手这个技巧在开发复杂随机系统时非常实用。7. 总结这个“小可与斯比的猜凶手赌局”项目虽然小但涉及了 Python 入门阶段最重要的几个知识点数据结构选型、随机数应用、状态管理、菜单交互、输入校验、函数拆分和模块化设计。通过完成这个项目你不仅写出了一款可以拿来娱乐的推理小游戏还锻炼了把一个抽象场景转换成具体代码逻辑的能力。建议你先照着文章完整敲一遍代码跑通流程之后再根据自己的想法去改规则。比如把行动次数改成 7 次把干扰线索概率改成 0.5或者新增一个“查看证据袋”的功能。每一次改动都会迫使你去理解原有逻辑这就是最好的学习方式。如果你在运行过程中遇到了其他报错或者有更好的玩法设计欢迎在评论区留言讨论也可以把文章收藏起来方便下次复盘时快速查阅。祝你在这场和斯比的赌局中每次都能赢
返回列表