从零搭建“末日逃生列车”互动游戏原型:Python状态机与API实践 📅 发布时间:2026/8/30 7:44:50 👁 浏览次数: “末日逃生列车选择你的专属无限续美食车厢”这个主题本质上是一个非常适合做成“互动文字游戏 生存模拟器 美食收集”三合一的轻量级项目。它没有复杂的 3D 场景也不需要高显存渲染核心玩法集中在“列车不断前进、车厢资源有限、玩家必须做出选择、而美食车厢掌握着生存补给”这条链条上。如果把它当做一个可运行的 Web 应用或命令行游戏来开发技术上并不难重点在于生存数值设计、车厢事件循环、存档机制和可扩展的 API 接口。这篇文章就直接围绕这个主题带你从零搭一个“末日逃生列车”互动应用原型。你会看到如何设计车厢选择逻辑、如何实现“无限续美食”的循环机制、如何把游戏状态变成 JSON 接口供外部调用以及如何做批量模拟来验证游戏平衡性。整个项目可以跑在普通电脑上不需要 GPU适合作为独立开发者的练手项目也适合用来快速生成互动剧情体验。如果你关心的是“这个项目能不能快速跑起来、怎么设计数值、怎么接 API、怎么批量测试”这篇文章可以直接收藏备用。1. 核心能力速览能力项说明项目类型末日生存题材互动游戏 / 轻量级 Web 应用原型核心玩法列车行进、车厢选择、生存资源管理、美食解锁、无限续餐循环运行环境Python 3.9普通电脑即可无需 GPU建议内存运行时约 100MB 以内具体以本机为准启动方式命令行启动 / Streamlit Web 界面 / Flask API 服务主要功能随机事件、车厢选择、饥饿值管理、美食图鉴、存档与读档、回合制推进是否支持 API支持可设计 JSON 接口供外部调用是否支持批量任务支持可批量模拟不同初始策略下的生存天数适合场景独立游戏原型、互动剧情创作、生存数值模拟、AI 内容生成测试扩展方向接入本地 LLM 生成事件描述、接入 AI 绘图生成美食插图需要说明的是这不是某个现成开源大项目的安装教程而是一个“从 0 到 1 实现”的工程化示例。你不需要下载模型文件也不需要配置 CUDA只要 Python 环境正常就能把核心循环跑起来。2. 适用场景与使用边界这个项目适合以下几类人想做文字游戏或互动剧情原型的人。它把“选择驱动剧情”和“资源管理”结合天然适合作为叙事游戏的骨架。想练手 Python 状态机、存档系统、API 设计的人。游戏的核心是一个“状态 - 选择 - 新状态”的循环非常适合拆成独立模块。想做 AI 内容创作试验的人。可以在美食车厢事件中加入 LLM 提示词生成随机文本或者用 AI 绘图生成菜品插图让列车旅行更有沉浸感。想验证生存游戏数值平衡的人。通过批量模拟不需要实际玩游戏就能看出一套初始资源下玩家平均能存活几天。使用边界也要说清楚这是一个原型项目不是完整商业游戏。美术、音效、剧情文案、数值平衡都需要继续补充。如果接入 AI 绘图或 LLM 生成内容要注意素材版权问题。AI 生成的图片和文本不一定能直接商用发布前需要确认模型服务条款。如果做成多人联机版本还需要额外设计服务端同步逻辑本文不展开。不要用这个项目名义去冒充别人的商业游戏或未授权 IP“末日逃生列车”这类玩法设定如果是参考某部作品要注意避免直接复制原作文案和美术。3. 环境准备与前置条件先准备一个最小可运行环境。这个项目核心依赖很少不建议一开始就引入重型框架。# 检查 Python 版本推荐 3.9 及以上 python --version # 创建独立虚拟环境避免污染系统 Python python -m venv train_env # 激活虚拟环境 # Windows: train_env\Scripts\activate # macOS / Linux: source train_env/bin/activate建议安装以下依赖# requirements.txt streamlit1.30.0 flask2.3.0 requests2.31.0如果你只需要命令行版本Streamlit 和 Flask 都不需要只保留标准库就可以。下面的教程会分两条路径先用标准库写核心逻辑再给一个 Web 界面启动方式。磁盘空间方面整个项目源码加依赖占用通常在 300MB 以内。不需要单独下载模型文件也不需要配置 CUDA 环境。4. 安装部署与启动方式4.1 项目结构建议把项目分成数据、逻辑、界面三层escape_train/ ├── core/ │ ├── game_state.py # 游戏状态类 │ ├── events.py # 车厢事件与美食逻辑 │ └── train.py # 列车推进主循环 ├── data/ │ └── foods.json # 美食车厢数据 ├── web/ │ └── app.py # Streamlit 或 Flask 启动入口 ├── tests/ │ └── simulate.py # 批量模拟脚本 └── requirements.txt4.2 核心状态设计“末日逃生列车”的本质是一个状态循环。每次列车到站或进入下一节车厢玩家执行一次选择状态随之更新。# core/game_state.py import json import time import random class GameState: def __init__(self, player_name幸存者): self.player_name player_name self.health 100 self.hunger 80 # 0 表示饿到极限 self.day 1 self.current_car 0 # 当前所在车厢编号 self.food_count 0 self.unlocked_foods [] self.is_over False self.history [] def to_dict(self): return { player_name: self.player_name, health: self.health, hunger: self.hunger, day: self.day, current_car: self.current_car, food_count: self.food_count, unlocked_foods: self.unlocked_foods, is_over: self.is_over, } def save(self, pathsavegame.json): with open(path, w, encodingutf-8) as f: json.dump(self.to_dict(), f, ensure_asciiFalse, indent2) def load(self, pathsavegame.json): with open(path, r, encodingutf-8) as f: data json.load(f) self.player_name data[player_name] self.health data[health] self.hunger data[hunger] self.day data[day] self.current_car data[current_car] self.food_count data[food_count] self.unlocked_foods data[unlocked_foods] self.is_over data[is_over]这里的饥饿值是一个递减资源。每天结束或每次推进车厢饥饿值下降玩家必须在美食车厢中“续餐”来维持生存。续餐并不只是加数值还触发随机美食事件形成“选择车厢 - 消耗资源 - 美食补给 - 解锁图鉴”的循环。4.3 美食车厢逻辑“无限续美食”不能做成无脑加满否则游戏没有决策深度。更合理的设计是每次续餐消耗一个“餐券”或“生存点数”同时随机刷新三种美食选项玩家只能选一个。选完后解锁该美食并恢复一定饥饿值然后进入下一节车厢。# core/events.py import random FOOD_POOL [ {id: canned_beef, name: 罐头牛肉, restore: 30, tag: 高蛋白}, {id: instant_noodles, name: 泡面, restore: 18, tag: 快熟}, {id: dried_fruit, name: 果干, restore: 12, tag: 轻量}, {id: chocolate, name: 黑巧克力, restore: 15, tag: 高热量}, {id: compressed_biscuit, name: 压缩饼干, restore: 20, tag: 耐储存}, ] def get_food_options(count3): return random.sample(FOOD_POOL, kmin(count, len(FOOD_POOL))) def eat_food(state, food_id): for food in FOOD_POOL: if food[id] food_id: state.hunger min(100, state.hunger food[restore]) state.food_count 1 if food[id] not in state.unlocked_foods: state.unlocked_foods.append(food[id]) return food return None满足条件后饥饿值恢复同时新菜品进入图鉴这种反馈会让“无限续”有收集感而不是单纯的数值恢复。4.4 列车推进主循环# core/train.py import random from core.game_state import GameState from core.events import get_food_options, eat_food GLOBAL_EVENTS [ {id: storm, name: 电磁风暴, damage: 10}, {id: radar, name: 发现废弃补给站, restore: 10}, {id: stranger, name: 神秘乘客求助, cost: 5}, ] def move_to_next_car(state): state.current_car 1 state.day 1 state.hunger max(0, state.hunger - 12) if state.hunger 0: state.health - 25 state.hunger 0 event random.choice(GLOBAL_EVENTS) if event[id] storm: state.health - event[damage] elif event[id] radar: state.hunger min(100, state.hunger event[restore]) elif event[id] stranger: state.health max(0, state.health - event[cost]) if state.health 0: state.is_over True state.history.append({ day: state.day, car: state.current_car, event: event[name], hunger: state.hunger, health: state.health, })主循环的基本节奏是上车 - 查看当前状态 - 遇到事件 - 进入美食车厢 - 选择食物 - 状态更新 - 继续下一节车厢。这个循环也是后面做 Web 界面和 API 的基础。4.5 命令行启动如果只想快速验证逻辑直接用命令行启动# 在项目根目录执行 python -m core.train也可以在core/train.py末尾加一段可执行的测试逻辑例如创建状态并连续推进 7 天打印每天的饥饿值和健康状况。这样不依赖任何 Web 框架最快看到结果。4.6 Streamlit 界面启动推荐用 Streamlit 做可视化界面。它适合数据展示和快速交互代码量比 Flask 前端少很多。# web/app.py import streamlit as st from core.game_state import GameState from core.events import get_food_options, eat_food st.set_page_config(page_title末日逃生列车, page_icon) st.title(末日逃生列车) if game not in st.session_state: st.session_state.game GameState() st.session_state.food_options get_food_options() game st.session_state.game st.write(f玩家{game.player_name} 第 {game.day} 天 当前车厢 {game.current_car}) st.progress(game.health / 100) st.write(f健康值{game.health} / 100) st.write(f饥饿值{game.hunger} / 100) st.subheader(前方是美食车厢选择一份续餐) for food in st.session_state.food_options: if st.button(f{food[name]}恢复 {food[restore]}): eat_food(game, food[id]) game.move_next() st.session_state.food_options get_food_options() st.rerun() if game.is_over: st.error(列车失去了最后的补给旅程到此结束。) if st.button(重新开始): st.session_state.game GameState() st.session_state.food_options get_food_options() st.rerun() if st.button(保存存档): game.save() st.success(已保存)启动命令streamlit run web/app.py --server.port 8501浏览器访问http://127.0.0.1:8501即可。这里用了标准库json做存档streamlit只负责界面交互核心逻辑可以直接复用。4.7 Flask API 启动如果需要把“末日逃生列车”变成接口服务可以用 Flask 提供 REST API。这样后续可以对接聊天机器人、自动化测试脚本或其他应用。# web/flask_api.py from flask import Flask, jsonify, request from core.game_state import GameState from core.events import get_food_options, eat_food app Flask(__name__) # 简单内存会话管理实际项目建议用数据库 sessions {} app.route(/api/new_game, methods[POST]) def new_game(): data request.get_json(forceTrue) player_name data.get(player_name, 幸存者) session_id ftrain_{len(sessions) 1} game GameState(player_nameplayer_name) sessions[session_id] game return jsonify({session_id: session_id, state: game.to_dict()}) app.route(/api/state/session_id, methods[GET]) def get_state(session_id): game sessions.get(session_id) if not game: return jsonify({error: session not found}), 404 return jsonify(game.to_dict()) app.route(/api/food_options/session_id, methods[GET]) def food_options(session_id): game sessions.get(session_id) if not game: return jsonify({error: session not found}), 404 options get_food_options() return jsonify({food_options: options}) app.route(/api/eat/session_id, methods[POST]) def eat(session_id): game sessions.get(session_id) if not game: return jsonify({error: session not found}), 404 data request.get_json(forceTrue) food_id data.get(food_id) if not food_id: return jsonify({error: food_id is required}), 400 result eat_food(game, food_id) if not result: return jsonify({error: invalid food_id}), 400 return jsonify({food: result, state: game.to_dict()}) app.route(/api/move/session_id, methods[POST]) def move(session_id): game sessions.get(session_id) if not game: return jsonify({error: session not found}), 404 game.move_next() return jsonify(game.to_dict()) if __name__ __main__: app.run(host127.0.0.1, port8000, debugTrue)启动命令python web/flask_api.py启动后访问http://127.0.0.1:8000/api/new_game即可创建新游戏会话。5. 功能测试与效果验证5.1 基础生成能力测试第一个要测试的是“新建游戏是否返回完整状态”。curl -X POST http://127.0.0.1:8000/api/new_game \ -H Content-Type: application/json \ -d {player_name: Alice}预期返回{ session_id: train_1, state: { player_name: Alice, health: 100, hunger: 80, day: 1, current_car: 0, food_count: 0, unlocked_foods: [], is_over: false } }判断成功标准返回的session_id能用于后续请求hunger初始为 80is_over为false。5.2 美食续餐循环测试测试目的是验证“无限续美食”是否真正形成闭环。# 获取当前车厢可选美食 curl -X GET http://127.0.0.1:8000/api/food_options/train_1 # 选择一个食物 ID 进行续餐 curl -X POST http://127.0.0.1:8000/api/eat/train_1 \ -H Content-Type: application/json \ -d {food_id: canned_beef}预期结果hunger从原来的值上升到恢复后的值并不超过 100unlocked_foods列表中新增canned_beef。然后调用移动接口curl -X POST http://127.0.0.1:8000/api/move/train_1此时day和current_car增加hunger下降 12。如果健康值归零is_over变成true。5.3 连跑 7 天的稳定性测试手动测试效率低建议写一个 Python 脚本连续跑 7 天验证状态更新是否正常# tests/test_7days.py import random from core.game_state import GameState from core.events import get_food_options, eat_food from core.train import move_to_next_car game GameState() for i in range(7): if game.is_over: print(f第 {i 1} 天前列车已停止) break options get_food_options() selected random.choice(options) eat_food(game, selected[id]) move_to_next_car(game) print( fDay {game.day}: hunger{game.hunger}, fhealth{game.health}, car{game.current_car} )如果脚本能跑完且状态始终在合法范围说明核心循环稳定。5.4 存档读档测试手动玩几回合后保存再新建一个游戏加载存档确认状态完全恢复。from core.game_state import GameState game GameState() game.hunger 50 game.health 77 game.current_car 3 game.save(test_save.json) loaded GameState() loaded.load(test_save.json) assert loaded.hunger 50 assert loaded.health 77 assert loaded.current_car 3 print(存档读档通过)正常输出存档读档通过说明 JSON 序列化没问题。5.5 失败排查方向如果某个测试不通过先检查这几个点接口请求路径是否写对session_id是否传递正确。美食 ID 是否存在于FOOD_POOL中ID 拼错会返回 400。重复调用move是否导致健康值被连续削减检查游戏结束条件是否触发。存档文件是否因为目录权限或编码问题无法写入Linux 下注意目录可写权限。6. 接口 API 与批量任务6.1 API 设计上面已经给出了 Flask API 的核心实现。这里把接口清单整理成表格接口名称方法作用主要参数/api/new_gamePOST创建新游戏会话player_name/api/state/{session_id}GET获取当前状态无/api/food_options/{session_id}GET获取当前可选美食无/api/eat/{session_id}POST选择并食用美食food_id/api/move/{session_id}POST推进到下一节车厢无6.2 Python 调用示例import requests BASE_URL http://127.0.0.1:8000 # 新建游戏 r requests.post(f{BASE_URL}/api/new_game, json{player_name: Bob}) session_id r.json()[session_id] print(session:, session_id) # 查看状态 state requests.get(f{BASE_URL}/api/state/{session_id}).json() print(state:, state) # 获取美食选项 options requests.get(f{BASE_URL}/api/food_options/{session_id}).json() print(options:, options) # 选择第一种美食 first_food options[food_options][0] r requests.post( f{BASE_URL}/api/eat/{session_id}, json{food_id: first_food[id]} ) print(after eat:, r.json()[state]) # 推进车厢 r requests.post(f{BASE_URL}/api/move/{session_id}) print(after move:, r.json()[state])这样的调用方式很容易接到Nonebot、Slack机器人或自动化测试脚本里。6.3 批量任务设计批量任务在这里有两个含义批量模拟游戏过程用来验证数值平衡。批量处理多个玩家的会话状态例如公会战或排行榜。先看批量模拟脚本# tests/simulate.py import random from core.game_state import GameState from core.events import get_food_options, eat_food from core.train import move_to_next_car def simulate_one(max_days30, seedNone): if seed is not None: random.seed(seed) game GameState() for _ in range(max_days): if game.is_over: break options get_food_options() # 简单策略优先选恢复量最高的食物 best max(options, keylambda x: x[restore]) eat_food(game, best[id]) move_to_next_car(game) return game def simulate_many(n1000, max_days30): alive_days [] for i in range(n): game simulate_one(max_daysmax_days, seedi) alive_days.append(game.day) if i % 200 0: print(f进度: {i}/{n}) avg_day sum(alive_days) / len(alive_days) survival_rate sum(1 for d in alive_days if d max_days) / len(alive_days) print(f平均存活天数: {avg_day:.2f}) print(f存活到第 {max_days} 天概率: {survival_rate:.2%}) if __name__ __main__: simulate_many(n1000, max_days30)跑完批量模拟后你会得到两个关键指标平均存活天数和存活率。这两个指标直接决定难度曲线。如果平均存活天数太低说明饥饿值下降太快或食物恢复量太低如果太高说明游戏缺乏挑战性。6.4 批量任务注意事项批量任务需要加日志和失败重试。如果每个模拟回合依赖外部 API比如调用远程 LLM 生成描述建议加超时重试import time import requests def call_with_retry(url, payload, retries3, timeout10): for attempt in range(retries): try: r requests.post(url, jsonpayload, timeouttimeout) r.raise_for_status() return r.json() except requests.RequestException as e: print(f请求失败: {e}, 第 {attempt 1} 次重试) time.sleep(1) raise RuntimeError(API 调用超过最大重试次数)7. 资源占用与性能观察“末日逃生列车”原型项目本身是轻量级的不像大模型推理那样需要夸张的显存。资源占用主要在三个方面Python 进程本身通常占用几十到一百多 MB 内存。Streamlit 或 Flask 服务占用少量额外内存。如果接入本地 LLM 或 AI 绘图才需要额外考虑内存、显存或磁盘空间。观察资源占用的通用方法# 查看 Python 进程内存占用 ps aux | grep python # 如果接入 GPU 推理可以看显存占用 nvidia-smi也可以在代码里加一个资源记录函数import os import psutil def log_process_usage(tag): process psutil.Process(os.getpid()) mem_mb process.memory_info().rss / 1024 / 1024 print(f[{tag}] 当前内存占用: {mem_mb:.1f} MB)如果想做几百人的并发会话模拟观察点就变成CPU 使用率是否飙升。内存是否持续增长。API 响应时间是否变慢。如果内存持续增长大概率是会话字典sessions没有清理机制。最简单的做法是给每个会话加时间戳定期清理超时会话。8. 常见问题与排查方法问题现象可能原因排查方式解决方案启动streamlit后页面打不开端口被占用或服务未启动检查终端日志和端口换--server.port例如 8502接口返回 404路径或session_id错误确认 API 路径拼写核对接口清单重新创建会话选择美食后状态不更新前端按钮事件未触发或回调写错检查 Streamlit rerun 逻辑在按钮回调后调用st.rerun()存档文件无法写入目录不存在或权限不足查看 Python 报错信息创建目录或修改文件权限批量模拟时程序卡住连续随机数生成耗时过长或死循环在循环中加进度输出检查while循环退出条件中文乱码终端编码不是 UTF-8查看控制台编码Windows 执行chcp 65001API 请求超时本地服务没有启动或防火墙拦截用curl直接测试接口确认服务进程存活检查监听端口健康值掉得飞快数值平衡没调好用批量模拟脚本看平均存活天数降低饥饿值消耗或提高美食恢复量解锁美食图鉴一直不增加美食 ID 重复或unlocked_foods判断逻辑写错打印unlocked_foods内容在添加前检查in判断另一个常见问题是端口冲突。如果你同时跑了 Streamlit 和 Flask建议一个用 8501一个用 8000。启动前先检查端口# Linux / macOS lsof -i :8501 # Windows netstat -ano | findstr :85019. 最佳实践与使用建议第一第一次跑通先别加复杂功能。把“新建游戏 - 选择美食 - 推进车厢 - 结束”这个最小闭环跑通再接 AI 生成、排行榜、账号系统等扩展。第二数值参数建议做成配置文件。当前FOOD_POOL和GLOBAL_EVENTS直接写在代码里扩展时容易到处改。更好的是把食物数据单独放到data/foods.json中代码启动时读取。{ foods: [ {id: canned_beef, name: 罐头牛肉, restore: 30, tag: 高蛋白}, {id: instant_noodles, name: 泡面, restore: 18, tag: 快熟} ], day_hunger_cost: 12, max_hunger: 100, max_health: 100 }第三批量模拟之前先加日志。每次模拟策略、随机种子、最终存活天数都记录下来方便对比不同平衡方案的差异。第四接口服务要限制访问范围。如果只在本机测试host用127.0.0.1即可如果部署到局域网加简单 token 校验或放到内网网关后面。第五如果接入 AI 生成美食描述建议先生成纯文本再考虑图片。文本的调试成本远低于图片而且不会引入版权风险。用 AI 绘图生成菜品插图时确认素材可商用并且不要直接生成真实品牌或真实人物形象。第六涉及玩家个人数据、存档数据时要注意隐私保护。不要为了批量测试随便使用真实用户数据。10. 总结与下一步“末日逃生列车选择你的专属无限续美食车厢”这个主题最有意思的地方是把“生存压力”和“美食反馈”放在同一条时间线上。玩家一边要担心健康和饥饿值一边又被“解锁新美食”的收集感吸引这种张力非常适合做互动内容项目。先建议验证三件事第一核心循环是否能在命令行跑通第二Streamlit 界面能否正常显示和交互第三批量模拟能否跑出一组可读的生存率数据。这三件事做完项目骨架就稳了。最容易踩的坑是数值平衡。饥饿值下降太快玩家体验会变成“一直在找吃的”下降太慢美食车厢就没有存在感。解决方式就是多做批量模拟用平均存活天数来校准参数。往后扩展方向可以考虑给每个美食添加剧情故事变成可收集的“图鉴”把列车车厢改成随机生成的关卡接入本地大模型生成每日事件描述或者做一个小型排行榜记录不同策略下谁存活天数最久。这个项目可以很轻也可以越做越深。从一列末日列车和一个美食车厢开始足够撑起一个完整的互动体验原型。