[python] 我做了一个根据对话选择走向的文字游戏 WordFunney
·
WordFunney — 文字生存恐怖游戏的开发记录
今天突然想做一个文字游戏,就是那种玩家选择选项来控制游戏的走向
游戏是文字游戏,想要有多个死亡结局 和 多个胜利结局,这样感觉很有意思
游戏有[ 生命 | 理智 | 道具]
游戏效果


上图为游戏游玩结果,根据剧情提供的选项,选择1/2/…来影响游戏的剧情走向,灰常有意思。。往下看把
1. 项目概述
WordFunney 是一个基于命令行的文字生存恐怖游戏。
采用 数据驱动 模式——所有剧情、选项、状态变化均从 JSON 配置文件读取,引擎代码不包含任何硬编码内容。
核心原则
| 原则 | 说明 |
|---|---|
| 数据驱动 | 引擎代码不含任何剧情硬编码 |
| 单向流程 | 节点到节点的有向图遍历 |
| 纯标准库 | 零外部依赖(仅 json, dataclasses, os) |
| 节点类型化 | 新增节点类型无需修改核心引擎 |
2. 项目结构
wordfunney/
│
├── story.json # 剧情配置(唯一数据入口)
├── main.py # 启动入口
│
├── engine/
│ ├── __init__.py
│ ├── models.py # 数据模型 dataclass
│ ├── loader.py # JSON 加载与校验
│ ├── state.py # 游戏状态管理 & effect 解析
│ └── core.py # 游戏主循环 & 节点派发
│
└── renderer/
├── __init__.py
└── cli.py # CLI 渲染(文本输出 + 输入处理)
模块职责
| 文件 | 职责 |
|---|---|
main.py |
初始化 GameState、加载 story、启动 engine |
engine/models.py |
StoryNode, Option, GameState dataclass |
engine/loader.py |
读 story.json,校验 next_id 引用完整性 → dict[str, StoryNode] |
engine/state.py |
StateManager:管理 health/sanity/flags/inventory,解析 condition & effect |
engine/core.py |
GameEngine:主循环 run(),按 node.type 派发处理器 |
renderer/cli.py |
CliRenderer:show_text(), show_options(), prompt(), show_status() |
3. 数据模型
@dataclass
class GameState:
health: int = 100 # 生命值,归零即死亡
sanity: int = 100 # 理智值,归零即疯狂
inventory: list[str] # 道具列表
flags: dict[str, bool] # 剧情标记
current_node: str # 当前所在节点 ID
visited: set[str] # 已访问节点
@dataclass
class StoryNode:
id: str # 唯一标识
text: str # 展示文本
type: str # normal / win / lose / check
options: list[Option] # 可选分支
effects: dict | None # 进入节点时触发
@dataclass
class Option:
text: str # 选项文本
next_id: str # 指向的下一个节点
condition: dict | None # 显示条件
effects: dict | None # 选择后触发
节点类型
| type | 行为 |
|---|---|
normal |
显示文本 → 展示可用选项 → 玩家选择 |
win |
显示胜利文本 → 游戏结束 |
lose |
显示失败文本 → 游戏结束 |
check |
无玩家交互,根据 condition 自动跳转 true_next / false_next |
4. 剧情配置格式 (story.json)
{
"meta": {
"title": "深渊回响",
"start_node": "start"
},
"nodes": [
{
"id": "start",
"text": "你从一张冰冷的铁床上醒来。\n四周是无尽的黑暗...",
"type": "normal",
"effects": { "sanity": -5 },
"options": [
{
"text": "摸索四周的墙壁",
"next_id": "explore_wall"
},
{
"text": "大声呼救",
"next_id": "shout",
"effects": { "sanity": -10 }
}
]
}
]
}
condition 语法
{ "stat": { "health": { ">": 0 } } }
{ "flag": { "name": "knows_truth", "value": true } }
{ "inventory": { "has": "flashlight" } }
{ "visited": "cellar_dark" }
effects 语法
{ "health": -15, "sanity": 10 }
{ "inventory": { "add": "rusty_key" } }
{ "inventory": { "remove": "flashlight" } }
{ "flags": { "knows_truth": true } }
5. 游戏主循环
┌─ main.py ─────────────────────────────┐
│ state = GameState() │
│ nodes = load_story("story.json") │
│ engine = GameEngine(state, nodes) │
│ engine.run() │
└──────────────────┬────────────────────┘
▼
┌─ engine/core.py ──────────────────────┐
│ while running: │
│ node = nodes[state.current_node] │
│ state_manager.apply(node.effects) │
│ │
│ if game_over := check_game_over(): │
│ handle_ending(game_over) │
│ break │
│ │
│ match node.type: │
│ "normal" → handle_normal(node) │
│ "win" → handle_win(node) │
│ "lose" → handle_lose(node) │
│ "check" → handle_check(node) │
│ │
│ state.visited.add(node.id) │
└──────────────────────────────────────┘
6. 渲染器设计
show_intro(meta) → 显示游戏标题
show_text(text) → 打印文本 + 分隔线
show_status(state) → 显示 [生命: 100 ❤ | 理智: 95 🌙 | 道具: 手电筒]
show_options(options) → 打印编号列表
prompt(options) → 读取 1-n 或 q 退出,返回 Option
show_ending(type) → 显示游戏结束画面
7. 剧情内容总览(当前版本)
7.1 数据统计
| 指标 | 数值 |
|---|---|
| 总节点数 | 54 |
| 普通节点 | 38 |
| 判定节点(check) | 5 |
| 胜利结局 | 4 |
| 死亡结局 | 7 |
| 可用道具 | 5(flashlight / rusty_key / scalpel / battery / keycard) |
| 总文字量 | ~4600 字 |
| 单次通关节点数 | 8~18(取决于路线) |
7.2 故事地图
苏醒 ─── 探索密室 ─── 走廊 ─┬── 左侧通道 ─── 手术室 ─── 手术刀 / 笔记
│ │
│ ├── 右侧走廊 ─┬── 铁门 → 胜利①(铁门逃脱)
│ │ │
│ │ └── 储物间 ─── 医疗包 / 地图
│ │
│ ├── 前方大厅 ─┬── 办公室 ─── 钥匙卡
│ │ │
│ │ ├── 休息室 ─── 恢复状态
│ │ │
│ │ └── 病房区 ─┬── 安全楼梯 → 屋顶 → 胜利③(屋顶救援)
│ │ │
│ │ └── 锅炉房 ─┬── 发电机 → 主出口 → 胜利②(主出口逃脱)
│ │ │
│ │ └── 通风管 ─── 封印区 ─── 深渊 → 死亡
│ │
│ └── 巢穴 ─── 集合体 ─┬── 手术刀战斗 → 胜利④(英雄结局)
│ │
│ ├── 逃跑 → 死亡
│ └── 加入 → 死亡
│
└── 呼救 ─── 暗影 → 死亡
7.3 胜利路线
| # | 路线 | 路径 | 难度 |
|---|---|---|---|
| ① | 铁门逃脱 | 右走廊 → 钥匙 → 开门 | ★☆☆ 最简单 |
| ② | 主出口逃脱 | 办公室拿钥匙卡 → 锅炉房启动发电机 → 主出口 | ★★★ 完整流程 |
| ③ | 屋顶救援 | 病房区 → 安全楼梯 → 直升机 | ★★☆ 中等 |
| ④ | 英雄结局 | 手术室拿手术刀 → 巢穴 → 击败集合体 | ★★★ 高风险高回报 |
7.4 死亡结局
| # | 结局 | 触发方式 |
|---|---|---|
| ① | 暗影扼杀 | 呼救后走向暗影 |
| ② | 精神崩溃 | sanity ≤ 30 时进入左通道 |
| ③ | 深渊坠落 | 跳入封印区的地洞 |
| ④ | 锅炉爆炸 | 启动发电机时 health ≤ 30 |
| ⑤ | 被拖入巢穴 | 在集合体前逃跑 |
| ⑥ | 融为一体 | 主动走向集合体 |
| ⑦ | 战斗疯狂 | 用手术刀战斗时 sanity ≤ 20 |
7.5 道具一览
| 道具 | 位置 | 用途 |
|---|---|---|
| flashlight | 密室地面 | 照明、驱赶暗影 |
| rusty_key | 右走廊 | 打开铁门(胜利①) |
| scalpel | 手术室推车 | 击败集合体(胜利④) |
| keycard | 办公室抽屉 | 打开主出口(胜利②) |
| battery | 储物间急救箱 | 手电筒备用 |
8. 扩展指南
| 需求 | 做法 |
|---|---|
| 添加剧情 | story.json 新增 node |
| 新节点类型 | models.py 加类型 → core.py 加 handler |
| 新条件/效果 | state.py 解析器加分支 |
| 随机事件 | node 加 random_next 字段 |
| 多章节 | 多文件,loader 合并 |
| 存档 | GameState 序列化为 JSON |
最后
我将我的游戏做成可运行的软件分享给你们
如下图
在这里你们可以直接通过打开文本编辑器,或者vscode打开story.json文件,然后修改这个文件内容,创造自己的文字游戏故事
是不是很有意思呢!!!
更多推荐
所有评论(0)