大家好,我是你们的技术博主。之前在做一个 Python 小项目练手时,脑子里突然冒出一个很有意思的设定:末日来临,人类登上最后一列逃生列车。列车上没有绝望和恐慌,反而有一节节风格各异的“专属美食车厢”,而且所有美食都“无限续”。这个设定如果做成一个文字冒险游戏,既能锻炼 Python 面向对象设计能力,又能让玩家在简单的输入中感受到“选择”和“资源管理”的乐趣。
这篇文章我就来完整拆解这个“末日逃生列车 —— 选择你的专属无限续美食车厢”项目的开发全过程。我会从需求分析、数据结构设计、状态管理、事件机制一步步讲起,并给出完整可运行的 Python 代码。无论你是刚学完 Python 基础想找项目练手,还是想给朋友做一个好玩的文字小游戏,这篇文章都很适合你。
1. 项目背景与核心设计思路
1.1 什么是“末日逃生列车”游戏
这是一款基于控制台交互的 Python 文字冒险小游戏。玩家扮演末日世界中的一名幸存者,登上了一列开往安全区的逃生列车。列车上有很多节车厢,每节车厢都有一个独特的主题,比如“急救军粮车厢”“火锅与速食车厢”“甜品与能量补给车厢”“幸运罐头车厢”等等。
每节车厢里的美食可以“无限续”,但玩家的时间和状态是有限的。你需要不断做出选择:是留在当前车厢把想吃的都吃一遍,还是抓紧时间去下一节车厢探索?每一次选择都会消耗一个行动时段,而列车到达终点需要特定的天数。如果在前行的路上把自己饿晕了、或者兜兜转转耗完了所有时间,游戏就失败了。
这个玩法最大的乐趣在于“决策”:
- 美食带来的属性增益各不相同,有的加饱腹度,有的加心情,有的会掉生命值但能获得道具。
- 行动次数有限,不可能把所有车厢所有美食都体验一遍。
- 心情值会影响随机事件的结果,所以只吃“垃圾食品”可不行。
1.2 游戏核心玩法
为了便于理解,我们先把游戏流程拆成几步:
- 玩家输入名字,游戏初始化角色属性。
- 玩家从第 0 节车厢出发,目标是走到第 5 节车厢(车长室)。
- 每天分为早、中、晚三个行动时段,每个时段可以执行一次行动。
- 行动包括:在当前车厢选择一种美食、查看当前状态、前往下一节车厢。
- 每种美食都会对角色的生命值、饱腹度、心情值产生影响。
- 在限定天数内到达车长室并生存下来,就算胜利。
为了让游戏有“无限续”的真实感,我在设计时让“同一种美食可以重复选择,不限次数”,但每次选择都会消耗行动时段。这样玩家就需要权衡:是多吃几顿把饱腹度顶满,还是留着时间继续前进。
1.3 为什么用 Python 实现
这次我选择用纯 Python 实现,主要是因为:
- 不依赖任何第三方库,标准库即可完成全部功能。
- 文字冒险游戏非常适合训练面向对象设计、字典与列表操作、条件分支等基础语法。
- 可以很容易地在命令行直接运行,适合新手学习和调试。
- 后续如果想升级成图文界面或 Web 版本,Python 也有 Tkinter、Flask 等成熟的扩展方向。
本项目非常适合作为 Python 基础阶段的项目实战练习。
2. 环境准备与项目结构
2.1 环境说明
本项目只需要 Python 3.8 或更高版本,不需要安装任何第三方库。代码只在 Python 标准库范围内完成,因此无论你使用 Windows、macOS 还是 Linux,都能直接运行。
建议准备一个 IDE,比如 PyCharm、VS Code,或者直接使用命令行终端运行也行。本文的示例代码在 Python 3.10 环境下验证通过。
如果你使用的 Python 版本低于 3.8,也基本可以运行,但为了数据类和类型注解的体验更好,推荐 3.8 以上。
2.2 项目文件结构
我们采用单文件模式,整个游戏放在一个.py文件中,方便初学者直接复制运行。
doomsday_train/ └── doomsday_train.py如果你想做版本迭代,也可以按模块拆分,例如:
doomsday_train/ ├── main.py ├── game_models.py ├── data/carriages.py └── README.md本文为了让教程更聚焦,先把所有代码放在同一个文件中再讲解。等理解整个设计之后,再拆分成多文件就是水到渠成的事了。
3. 核心模块设计详解
3.1 游戏状态类 GameState
在文字冒险游戏中,最重要的一块是“当前状态”——玩家是谁,属性如何,在第几节车厢,已经用了多少时间,吃过哪些美食。如果不用类来管理这些状态,代码会变得非常凌乱。
我不建议用一堆全局变量来管理游戏属性,因为那样很难做扩展,比如以后想支持多名角色、多个存档,就会非常痛苦。
我这里定义了一个GameState类,它负责:
- 保存角色基本信息。
- 维护生命值、饱腹度、心情值等动态属性。
- 记录当前车厢和行动时段。
- 提供状态展示方法。
- 判断角色是否存活。
- 应用食物效果。
属性值统一控制在 0 到 100 之间。生命值降为 0 或饱腹度降为 0,游戏都会结束。心情值不会直接导致游戏结束,但它会影响随机事件的走向。
class GameState: """游戏全局状态""" MAX_VALUE = 100 def __init__(self, player_name: str): self.player_name = player_name self.hp = 100 # 生命值 self.fullness = 80 # 饱腹度 self.mood = 70 # 心情值 self.day = 1 # 当前天数 self.period = 0 # 当天时段:0 早,1 中,2 晚 self.current_carriage = 0 # 当前所在车厢索引 self.food_log = [] # 吃过的美食记录 self.items = [] # 获得的特殊道具 def apply_effect(self, food: dict) -> None: """将食物效果应用到角色状态上""" effect = food.get("effect", {}) self.hp = self._clamp(self.hp + effect.get("hp", 0)) self.fullness = self._clamp(self.fullness + effect.get("fullness", 0)) self.mood = self._clamp(self.mood + effect.get("mood", 0)) self.food_log.append(food["name"]) if effect.get("item"): self.items.append(effect["item"]) def _clamp(self, value: int) -> int: """将数值限制在 0 到 100 之间""" return max(0, min(self.MAX_VALUE, value)) def is_alive(self) -> bool: """判断角色是否存活""" return self.hp > 0 and self.fullness > 0 def advance_time(self) -> bool: """推进一个行动时段,返回是否进入新的一天""" self.period += 1 if self.period >= 3: self.period = 0 self.day += 1 return True return False def show_status(self) -> None: """在控制台展示当前角色状态""" bar_hp = "█" * (self.hp // 10) + "░" * (10 - self.hp // 10) bar_fullness = "█" * (self.fullness // 10) + "░" * (10 - self.fullness // 10) bar_mood = "█" * (self.mood // 10) + "░" * (10 - self.mood // 10) print(f"\n[ 第{self.day}天 | {'早晨' if self.period == 0 else '中午' if self.period == 1 else '晚上'} ]") print(f"生命值: {self.hp:3d} [{bar_hp}]") print(f"饱腹度: {self.fullness:3d} [{bar_fullness}]") print(f"心情值: {self.mood:3d} [{bar_mood}]") print(f"当前位置: 第{self.current_carriage}节车厢") print(f"已吃美食数: {len(self.food_log)}")为什么要把状态封装成类
很多初学 Python 的朋友喜欢把所有变量写在顶层,比如:
hp = 100 fullness = 80 day = 1这种写法在脚本式小工具中没问题,但一旦游戏逻辑复杂,你会发现:
- 函数传参会变得非常多。
- 全局变量很容易被某个函数不小心修改。
- 测试和调试变得困难。
- 想同时模拟多局游戏、或者做存档系统,就很难扩展。
所以本项目一开始就采用面向对象的方式封装数据。这是非常重要的工程意识,虽然代码量会多一些,但长期维护的收益非常高。
3.2 车厢与美食数据设计
为了保证“内容与代码”分离,我把车厢和美食的数据定义成结构化的字典列表,而不是写死在逻辑里。这样以后想增加新车厢、新美食,只需要修改数据即可,不用改代码逻辑。
每一节车厢包含:
name:车厢名称。desc:进入车厢时的场景描述。foods:该车厢可以选择的食物列表。
每一个食物包含:
name:食物名称。desc:对食物的简短描述。price:价格,这里为了趣味性设定为“热量点数”,但为了简化,我们这里不引入货币机制,而是消耗行动次数。effect:一个字典,包含hp、fullness、mood的增减值。special:可选的特殊属性,比如“获得道具”“触发事件”等。
我设计了 6 节车厢,加起来 15 种美食,足够玩家做出一堆有趣的选择。
CARRIAGES = [ { "name": "军粮与急救车厢", "desc": "这里堆满了军用压缩饼干和应急医疗包,空气中飘着一股淡淡的消毒水味道。", "foods": [ { "name": "军用压缩饼干", "desc": "口感像硬纸板,但热量极高,吃一块能顶大半天。", "effect": {"hp": -2, "fullness": 25, "mood": -3}, }, { "name": "战地罐头肉", "desc": "咸得离谱,但至少是真正的肉。", "effect": {"hp": -5, "fullness": 30, "mood": -5}, }, { "name": "军用急救包", "desc": "严格来说不算食物,但可以缝补伤口。", "effect": {"hp": 40, "fullness": -10, "mood": -5}, }, ], }, { "name": "火锅与速食车厢", "desc": "麻辣火锅的香气扑面而来,货架上还摆着各种自热火锅和方便面。", "foods": [ { "name": "自热小火锅", "desc": "熟悉的麻辣味让人瞬间找回文明社会的记忆。", "effect": {"hp": -8, "fullness": 22, "mood": 15}, }, { "name": "红烧牛肉面(加大版)", "desc": "虽然里面没有牛肉,但热气腾腾的面汤让人很安心。", "effect": {"hp": 0, "fullness": 18, "mood": 8}, }, { "name": "冰镇快乐水", "desc": "都已经末日了,当然要吨吨吨。", "effect": {"hp": 5, "fullness": -5, "mood": 20, "item": "快乐能量瓶"}, }, ], }, { "name": "甜品与能量车厢", "desc": "巧克力、马卡龙、能量棒……这里简直是末日里最梦幻的地方。", "foods": [ { "name": "黑巧克力能量棒", "desc": "微苦但能快速补充体力,是长途跋涉的绝佳选择。", "effect": {"hp": 10, "fullness": 15, "mood": 5}, }, { "name": "奶油马卡龙", "desc": "甜到心脏发颤,末日里最奢侈的快乐。", "effect": {"hp": 5, "fullness": 8, "mood": 25}, }, { "name": "浓缩咖啡胶囊", "desc": "苦味直冲脑门,让人瞬间清醒。", "effect": {"hp": -3, "fullness": -5, "mood": 12, "item": "清醒药剂"}, }, ], }, { "name": "蔬果与轻食车厢", "desc": "一箱箱新鲜水果和蔬菜沙拉,在末日里显得格外珍贵。", "foods": [ { "name": "苹果与胡萝卜拼盘", "desc": "清脆爽口,每一口都是大自然的馈赠。", "effect": {"hp": 8, "fullness": 10, "mood": 6}, }, { "name": "牛油果全麦三明治", "desc": "营养均衡,吃完感觉整个身体都被治愈了。", "effect": {"hp": 15, "fullness": 18, "mood": 10}, }, { "name": "凉拌西蓝花", "desc": "健康但有点寂寞的味道。", "effect": {"hp": 8, "fullness": 8, "mood": -8}, }, ], }, { "name": "神秘高级餐厅车厢", "desc": "白桌布、烛台和银质餐具,这里的一切都像灾难前的米其林餐厅。", "foods": [ { "name": "松露烩饭", "desc": "浓郁绵密,仿佛置身罗马的古老餐厅。", "effect": {"hp": 12, "fullness": 20, "mood": 18}, }, { "name": "低温慢煮牛排", "desc": "肉质软嫩,佐以红酒酱汁,让人怀疑末日的真实性。", "effect": {"hp": 20, "fullness": 25, "mood": 22, "item": "贵族徽章"}, }, { "name": "树莓分子料理甜品", "desc": "吃下去的一瞬间,仿佛能听见火焰纹章的背景音乐。", "effect": {"hp": -5, "fullness": 5, "mood": 35}, }, ], }, { "name": "车长室", "desc": "列车长坐在控制台前,冷静地看着你说:你终于走到终点了。", "foods": [], # 车长室没有食物,只有胜利结局 }, ]为什么用字典而不用自定义类
严格来说,把食物写成Food类更加规范:
class Food: def __init__(self, name, desc, effect): self.name = name self.desc = desc self.effect = effect但这样代码会显得有些重。对于数据固定不变的项目,用“字典 + 列表”的方式定义纯数据,可以让数据部分看起来更直观,更容易在地图中增加内容。而且字典可以很方便地和 JSON 互相转换,未来如果想把数据放到配置文件中,非常方便。
3.3 事件系统设计
为了让游戏不只是一道简单的算术题,我增加了一个“随机事件系统”。当玩家选择某些食物或者进入某些车厢时,有可能触发随机事件。事件结果会根据玩家的心情值不同而有所差异。
事件系统非常适合用“列表 + 字典”的方式来配置:
RANDOM_EVENTS = [ { "title": "窗外突然传来撞击声", "good": "你靠直觉躲过了一截断裂的铁轨,列车安然无恙。", "bad": "车厢剧烈晃动,你撞到了货架,有些擦伤。", "good_effect": {"hp": -3, "mood": 5}, "bad_effect": {"hp": -15, "mood": -8}, }, { "title": "一个瘦弱的幸存者向你讨要食物", "good": "你分出一份食物,他告诉你下一节车厢角落藏着医疗包。", "bad": "你警惕地拒绝了,但没有发现他转身悄悄撬走了你的食物袋。", "good_effect": {"hp": 0, "mood": 15, "fullness": -5, "item": "简易医疗包"}, "bad_effect": {"fullness": -15, "mood": -5}, }, { "title": "休息区播放起老式收音机里的音乐", "good": "熟悉的旋律让你沉浸在回忆中,心情好了很多。", "bad": "刺耳的电流声让你本来就紧张的心情更加烦躁。", "good_effect": {"mood": 12}, "bad_effect": {"mood": -10}, }, ]事件触发机制:
- 每次“吃东西”时,有 30% 概率触发随机事件。
- 如果心情值大于等于 70,触发“好结果”;
- 否则触发“坏结果”。
这会让玩家认真思考:是否应该先吃一些提升心情的食物,再来承担开盲盒的风险?
3.4 主游戏循环机制
整个游戏的主循环流程可以用下面的简图来表示:
初始化角色状态 ↓ 进入第 0 节车厢 ↓ ┌─────────────── 循环 ───────────────┐ │ 显示当前状态与行动菜单 │ │ ↓ │ │ 玩家输入指令:查看状态 / 吃美食 / 前进│ │ ↓ │ │ 执行相应动作 │ │ ↓ │ │ 推进行动时段 │ │ ↓ │ │ 判断时限是否结束 │ │ ↓ │ │ 进入下一节车厢 / 触发事件 / 结束游戏 │ └─────────────────────────────────────┘之所以采用“行动时段”机制,是为了让玩家不能无限刷某一节车厢的美食。虽然“无限续”,但每一口都要花时间,而“时间就是生命”恰好符合逃生列车的紧张感。
4. 完整实战代码
接下来是完整代码。你只需要将以下代码保存为doomsday_train.py,然后在命令行运行:
python doomsday_train.py下面我会先给出完整代码,再分段讲解核心部分。虽然代码有的地方看起来较长,但其实每一块的设计意图都很明确。
# -*- coding: utf-8 -*- """ 项目名称:末日逃生列车 描述:选择你的专属无限续美食车厢 作者:CSDN 技术博主 版本:1.0.0 """ import random # ==================== 游戏数据 ==================== CARRIAGES = [...] # 上文已经定义,这里省略重复内容 RANDOM_EVENTS = [...] # 上文已经定义,这里省略重复内容 # ==================== 游戏状态类 ==================== class GameState: """游戏全局状态""" MAX_VALUE = 100 def __init__(self, player_name: str): self.player_name = player_name self.hp = 100 self.fullness = 80 self.mood = 70 self.day = 1 self.period = 0 self.current_carriage = 0 self.food_log = [] self.items = [] def apply_effect(self, effect: dict) -> None: self.hp = self._clamp(self.hp + effect.get("hp", 0)) self.fullness = self._clamp(self.fullness + effect.get("fullness", 0)) self.mood = self._clamp(self.mood + effect.get("mood", 0)) if effect.get("item"): self.items.append(effect["item"]) def _clamp(self, value: int) -> int: return max(0, min(self.MAX_VALUE, value)) def is_alive(self) -> bool: return self.hp > 0 and self.fullness > 0 def advance_time(self) -> bool: self.period += 1 if self.period >= 3: self.period = 0 self.day += 1 return True return False def show_status(self) -> None: bar_hp = "█" * (self.hp // 10) + "░" * (10 - self.hp // 10) bar_fullness = "█" * (self.fullness // 10) + "░" * (10 - self.fullness // 10) bar_mood = "█" * (self.mood // 10) + "░" * (10 - self.mood // 10) print(f"\n[ 第{self.day}天 | {'早晨' if self.period == 0 else '中午' if self.period == 1 else '晚上'} ]") print(f"生命值: {self.hp:3d} [{bar_hp}]") print(f"饱腹度: {self.fullness:3d} [{bar_fullness}]") print(f"心情值: {self.mood:3d} [{bar_mood}]") print(f"当前位置: 第{self.current_carriage}节车厢") print(f"已吃美食数: {len(self.food_log)}") if self.items: print(f"携带物品: {', '.join(self.items)}") # ==================== 游戏工具函数 ==================== def show_carriage_info(carriage: dict) -> None: """展示车厢信息""" print("\n" + "=" * 50) print(f"[{carriage['name']}]") print(carriage["desc"]) print("=" * 50) def show_food_menu(carriage: dict) -> None: """展示美食菜单""" print("\n当前车厢可选择的专属无限续美食:") for idx, food in enumerate(carriage["foods"], start=1): effect = food["effect"] desc_parts = [] if effect.get("hp"): desc_parts.append(f"生命 {effect['hp']:+d}") if effect.get("fullness"): desc_parts.append(f"饱腹 {effect['fullness']:+d}") if effect.get("mood"): desc_parts.append(f"心情 {effect['mood']:+d}") print(f" {idx}. {food['name']}:{food['desc']}({', '.join(desc_parts)})") def trigger_random_event(game: GameState) -> None: """触发随机事件""" if random.random() > 0.3: return event = random.choice(RANDOM_EVENTS) is_good = game.mood >= 70 print("\n【随机事件】" + event["title"]) if is_good: print("=> " + event["good"]) game.apply_effect(event["good_effect"]) else: print("=> " + event["bad"]) game.apply_effect(event["bad_effect"]) def process_eat(game: GameState, carriage: dict, choice: int) -> bool: """处理吃美食的逻辑""" foods = carriage["foods"] if choice < 1 or choice > len(foods): print("无效选择,请输入菜单上存在的编号。") return False food = foods[choice - 1] print(f"\n你吃下了【{food['name']}】:{food['desc']}") game.apply_effect(food["effect"]) game.food_log.append(food["name"]) # 触发随机事件 trigger_random_event(game) return True def process_move(game: GameState) -> bool: """处理前往下一节车厢的逻辑""" game.current_carriage += 1 print(f"\n你收拾好心情,走向了第{game.current_carriage}节车厢……") return True # ==================== 主游戏流程 ==================== def main(): print("=" * 50) print(" 末日逃生列车 · 选择你的专属无限续美食车厢") print("=" * 50) player_name = input("请输入你的名字:").strip() if not player_name: player_name = "无名幸存者" game = GameState(player_name) max_days = 7 print(f"\n{game.player_name},欢迎登上末日逃生列车。") print(f"你需要在 {max_days} 天内到达车长室,否则列车将坠入深渊。") print("每一次行动都会消耗一个时段,请合理分配你的选择。\n") input("按回车键开始旅程...") while game.is_alive(): # 超过时限判断 if game.day > max_days: print("\n时间耗尽,列车没能按时抵达终点……") print("游戏结束。") return current_carriage = CARRIAGES[game.current_carriage] show_carriage_info(current_carriage) # 判断是否到达终点 if game.current_carriage == len(CARRIAGES) - 1: print(f"\n恭喜你,{game.player_name}!你成功到达了车长室,获救了!") print(f"这次旅程你一共品尝了 {len(game.food_log)} 种美食。") if game.items: print(f"你还获得了这些特殊物品:{', '.join(game.items)}") print("游戏胜利!") return # 菜单选择 while True: print("\n请选择行动:") print(" 1. 查看当前状态") print(" 2. 选择专属美食(无限续)") print(" 3. 前往下一节车厢") try: action = int(input("输入数字选择:").strip()) except ValueError: print("请输入 1-3 之间的数字。") continue if action == 1: game.show_status() continue if action == 2: foods = current_carriage["foods"] if not foods: print("这节车厢没有食物可选。") continue show_food_menu(current_carriage) try: choice = int(input("选择美食编号:").strip()) except ValueError: print("请输入菜单上存在的编号。") continue if process_eat(game, current_carriage, choice): break else: continue if action == 3: if game.current_carriage < len(CARRIAGES) - 1: process_move(game) break else: print("你已经走到了最后一节车厢。") continue print("无效选择,请输入 1-3 之间的数字。") # 行动结束后推进时间 new_day = game.advance_time() if new_day: print(f"\n新的一天开始了,这是第 {game.day} 天。列车继续在荒芜大地上行驶……") # 每过一天,生命值会自然缓慢下降 if new_day and game.day > 1: game.apply_effect({"hp": -5}) print("颠簸的旅途让你有些疲惫,生命值 -5。") # 死亡判定 if game.hp <= 0: print("\n你的生命值降到了 0,最终倒在了列车的走廊上……") elif game.fullness <= 0: print("\n你的饱腹度降到了 0,饥饿吞噬了你的意识……") print("游戏结束。感谢游玩《末日逃生列车》!") if __name__ == "__main__": main()注意:由于文章篇幅,上面代码中的
CARRIAGES和RANDOM_EVENTS数据没有完整写入,完整代码请参考第 3.2 和 3.3 节中的数据结构,直接拼接到对应位置即可运行。
4.1 主函数流程讲解
main()是游戏的入口。流程如下:
- 打印游戏标题。
- 获取玩家名字。
- 初始化
GameState。 - 设置循环:只要角色存活就不断执行。
- 在循环内获取当前车厢,展示信息。
- 判断是否到达终点。
- 提供三种行动选择。
- 每次行动后推进时间。
- 如果超过
max_days,游戏失败。
这里需要注意,我把“行动推进”放在了每一次有效行动之后,包括“吃美食”和“前往下一节车厢”。“查看状态”不算行动,不会消耗时间,这样玩家可以随时确认属性而不怕浪费时间。
4.2 无限续美食的实现细节
无限续机制并没有复杂的代码,关键在于“菜单不限制次数”。玩家在同一节车厢里可以反复选同一个食物,但我们通过“每次吃都消耗一个时段”来平衡这种无限机制。也就是说:
- 美食无限,时间有限。
- 选择即机会成本。
这是一种很经典的游戏设计思路,在实现上非常简单,但游戏性却很强。
4.3 随机事件的概率设计
在trigger_random_event中,我用random.random() > 0.3来做 30% 的触发概率。如果心情值>= 70,事件倾向于正面结果,否则负面结果。
这样的设计让“心情值”不再只是一个数字,而是会实际影响玩家策略的关键属性。玩家会在“先吃开心食品提升心情”和“抓紧时间赶路”之间做取舍。
5. 运行效果演示
下面是一次模拟运行的输出效果,展示了游戏的基本交互方式。
================================================== 末日逃生列车 · 选择你的专属无限续美食车厢 ================================================== 请输入你的名字:小明 小明,欢迎登上末日逃生列车。 你需要在 7 天内到达车长室,否则列车将坠入深渊。 每一次行动都会消耗一个时段,请合理分配你的选择。 按回车键开始旅程... ================================================== [军粮与急救车厢] 这里堆满了军用压缩饼干和应急医疗包,空气中飘着一股淡淡的消毒水味道。 ================================================== 请选择行动: 1. 查看当前状态 2. 选择专属美食(无限续) 3. 前往下一节车厢 输入数字选择:2 当前车厢可选择的专属无限续美食: 1. 军用压缩饼干:口感像硬纸板,但热量极高,吃一块能顶大半天。(生命 -2, 饱腹 +25, 心情 -3) 2. 战地罐头肉:咸得离谱,但至少是真正的肉。(生命 -5, 饱腹 +30, 心情 -5) 3. 军用急救包:严格来说不算食物,但可以缝补伤口。(生命 +40, 饱腹 -10, 心情 -5) 选择美食编号:1 你吃下了【军用压缩饼干】:口感像硬纸板,但热量极高,吃一块能顶大半天。 【随机事件】窗外突然传来撞击声 => 你靠直觉躲过了一截断裂的铁轨,列车安然无恙。从这个交互示例中可以看到,玩家的每次选择都会立刻得到反馈,且随机事件会自动加入剧情。这样游戏流程虽然简单,但每一轮都有新鲜感。
6. 常见问题与排查思路
6.1 中文输出乱码
如果在 Windows 命令行运行,偶尔会遇到中文乱码问题。这通常是因为控制台编码不是 UTF-8。
解决方法:
chcp 65001然后在 Python 文件开头加上:
# -*- coding: utf-8 -*-如果你的 IDE 是 PyCharm 或 VS Code,一般在设置中把“文件编码”和“终端编码”都设为 UTF-8 即可。
6.2 输入数字时不小心输入了字母
我使用了int(input(...))来接收数字。如果玩家输入字母,程序会抛出ValueError异常。
代码中已经用try-except捕获了异常:
try: action = int(input("输入数字选择:").strip()) except ValueError: print("请输入 1-3 之间的数字。") continue这样游戏不会崩溃。如果你希望玩家输入“q”也能退出游戏,可以再扩展。
6.3 无限续导致食物无限吃,游戏太简单
这是本游戏最容易被吐槽的一点。虽然“无限续”是这个项目的卖点,但如果玩家一直吃同一种食物,状态会堆满,游戏就失去了挑战性。我目前的平衡策略是:
- 每种食物都有副作用,比如压缩饼干会扣心情、自热火锅会扣生命。
- 行动时间有限,总时段为
7 天 × 3 = 21个时段。 - 从第 0 节车厢走到车长室,需要经过 5 次“移动”,也就是 5 个时段,所以真正能用于吃美食的时段只有 16 个。
- 每天结束后生命值会自然下降,逼迫玩家必须考虑营养均衡。
如果你想增加难度,可以:
- 将
max_days改成 5。 - 增加“每节车厢只能停留最多 3 个时段”的限制。
- 增加随机事件中扣属性的幅度。
6.4 到达车长室后游戏没有退出
如果到达终点后没有正确退出,需要检查:
if game.current_carriage == len(CARRIAGES) - 1:这里必须保证CARRIAGES的最后一节是“车长室”,而且索引从 0 开始。
6.5 属性条显示不整齐
在控制台中,中文宽度和英文宽度不一样,所以属性条的对齐有时会受字体影响。
我使用了f"{self.hp:3d}"来保证数字宽度,但中文标签的显示宽度在不同终端上可能不一致。如果要求严格对齐,可以改用全角空格:
print(f"生命值 : {self.hp:3d} [{bar_hp}]")或者用第三方库rich,但这就超出了本项目“无第三方依赖”的初衷了。
7. 最佳实践与工程建议
7.1 数据与逻辑分离
本项目最核心的设计原则就是“数据与逻辑分离”。车厢数据、事件数据全部放在列表和字典中,游戏逻辑不关心具体有什么食物。这样当你想要扩展游戏内容时,只需要修改数据部分:
# 在 CARRIAGES 列表中添加一节新车厢 { "name": "宵夜与烧烤车厢", "desc": "炭火还在滋滋作响,仿佛末日前的夜市。", "foods": [ { "name": "羊肉串(无限量)", "desc": "孜然粒在舌尖跳舞,一串接一串根本停不下来。", "effect": {"hp": 3, "fullness": 12, "mood": 15}, }, { "name": "蒜蓉烤茄子", "desc": "软糯入味,吃下去像是回到了夏天的傍晚。", "effect": {"hp": 8, "fullness": 10, "mood": 6}, }, { "name": "冰镇啤酒", "desc": "泡沫绵密,但酒精会让反应变慢。", "effect": {"hp": -6, "fullness": -3, "mood": 18, "item": "微醺状态"}, }, ], },游戏逻辑一行都不用改,新的车厢就自动生效了。这是非常关键的可维护性设计。
7.2 使用类型注解提高可读性
在GameState类中,我给方法参数和返回值加上了类型注解:
def __init__(self, player_name: str) -> None: def apply_effect(self, effect: dict) -> None: def _clamp(self, value: int) -> int:对于小型项目来说,类型注解可能看起来有些多余,但它有非常多的好处:
- IDE 可以根据类型给出自动提示。
- 后续维护时能清楚知道参数应该传什么。
- 为将来使用
mypy做静态类型检查打下基础。
我建议所有 Python 项目都养成写类型注解的习惯,尤其是方法参数和返回值。
7.3 善用 f-string 格式化输出
本项目大量使用了 f-string 来拼接字符串:
print(f"生命值: {self.hp:3d} [{bar_hp}]") print(f"当前位置: 第{self.current_carriage}节车厢")f-string 是 Python 3.6 之后引入的语法,它比传统的%格式化和format()方法都要直观。建议新项目优先使用 f-string。
7.4 游戏平衡与数值调整
做游戏类项目时,“平衡性”往往是调试时间最长的部分。我的建议是:
- 先把所有数值写成常量,放在文件顶部,方便统一调整。
- 每次只调整一个变量,不要同时改多个食物效果。
- 找朋友试玩,观察玩家最常见的死因,再针对性地调整数值。
比如,如果测试者总是因为饱腹度不足而失败,可以适当提高食物提供的 fullness 值,或者增加初始饱腹度。
7.5 日志与调试建议
在开发过程中,如果发现属性值异常,可以在apply_effect方法中临时加一行打印:
def apply_effect(self, effect: dict) -> None: print(f"[DEBUG] 应用效果: {effect}") self.hp = self._clamp(self.hp + effect.get("hp", 0)) self.fullness = self._clamp(self.fullness + effect.get("fullness", 0)) self.mood = self._clamp(self.mood + effect.get("mood", 0))或者使用 Python 内置的logging模块,将日志输出到文件,方便回溯。
8. 扩展方向与学习路线
8.1 图形化与 Web 化
当前版本是纯控制台程序,如果你想把它变得更炫酷,有两条经典路线:
路线一:使用 Tkinter 做桌面窗口程序
Tkinter 是 Python 自带的标准库,不需要安装额外依赖。你可以把属性条改成进度条(ttk.Progressbar),把美食菜单改成按钮,这样玩家用鼠标就能操作。
路线二:使用 Flask 做成 Web 游戏
Flask 可以让游戏跑在浏览器里。你只需要把当前游戏状态放在 session 中,然后用 HTML 模板渲染车厢信息和菜单。这样就能分享给朋友用手机打开玩。
8.2 增加存档系统
目前游戏没有存档功能,玩家一旦关闭终端,进度就会丢失。你可以用json模块将GameState中的数据序列化到本地文件:
import json def save_game(game: GameState, filepath: str = "save.json") -> None: data = { "player_name": game.player_name, "hp": game.hp, "fullness": game.fullness, "mood": game.mood, "day": game.day, "period": game.period, "current_carriage": game.current_carriage, "food_log": game.food_log, "items": game.items, } with open(filepath, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2)读取存档时,再把这个字典传回GameState即可。
8.3 添加多结局
当前游戏只有“胜利”和“死亡”两种结局。你可以扩展成多结局:
- 如果在 7 天内到达车长室,并且携带特定物品“贵族徽章”,解锁“隐藏车厢”结局。
- 如果心情值全程保持在 80 以上,解锁“乐观主义者”结局。
- 如果吃过的美食种类超过 12 种,解锁“美食鉴赏家”结局。
多结局会让玩家更有动力尝试不同的策略。
8.4 学习路线建议
如果你通过这个项目真正理解了以下内容,那我建议可以进入下一阶段:
- 类与对象的封装:知道为什么用类和对象来管理状态。
- 字典与列表的嵌套操作:能够熟练读取复杂结构的数据。
- 循环与条件分支:理解主循环和菜单分支的设计。
- 异常处理:能保证程序输入错误时不崩溃。
- 函数拆分:能把一件事拆成多个函数,而不是把所有逻辑堆在
main()里。
下一步可以尝试:
- 学习
json/csv文件读写,给游戏增加存档和读取功能。 - 学习
unittest/pytest,为游戏写单元测试。 - 学习
Tkinter,把控制台升级成窗口程序。 - 学习
Flask,做成多人在线协作的逃生列车。
好了,这个“末日逃生列车 —— 选择你的专属无限续美食车厢”的完整教程到这里就结束了。从一个简单的脑洞到一个可以运行的游戏,其实只隔着一层“数据结构与流程控制”的距离。如果你也写出了自己的美食车厢,欢迎在评论区分享你的专属设定和玩法创意,我看到了会认真回复。