Project Infinity 静态分析:模块设计与工作原理
对象:electronistu/Project_Infinity · 方法:纯静态分析
1. 总体架构
┌──────────────────────────────────────────────────────────────┐
│ 终端 TUI(Rich + prompt_toolkit) │
│ play.py / play_with_{deepseek,gpt,gemini,claude,nano}.py │
│ → create_*_chat_fn() 各后端适配器(统一 chat_fn 接口) │
└───────────────┬──────────────────────────────────────────────┘
│ game_engine.run_game() 编排循环(game_engine.py)
┌───────────────▼──────────────────────────────────────────────┐
│ game_engine.py —— 会话编排层 │
│ 选世界→注入系统提示词+世界文件→MCP 客户端→工具调用循环→ │
│ 斜杠命令→每 5 回合 timeline 摘要→/save 落盘 │
└───────┬──────────────────────────────────────────────────────┘
│ MCP stdio(game_engine.py:130 启动子进程 dice_server.py)
┌───────▼──────────────────────────────────────────────────────┐
│ dice_server.py —— 规则引擎(FastMCP,3001 行) │
│ @mcp.tool 暴露:modify_player_numeric / update_player_list / │
│ dump_player_db / rest / roll_dice / perform_check / │
│ register_combatants / resolve_attack / resolve_magic │
│ ┌────────────┐ ┌────────────┐ ┌──────────────────────────┐ │
│ │内存 SQLite │ │战斗注册表 │ │config/spells.yml(5619行) │ │
│ │(玩家状态) │ │_COMBAT_REGISTRY│ │SRD 5.1 法术库 │ │
│ └────────────┘ └────────────┘ └──────────────────────────┘ │
└───────┬──────────────────────────────────────────────────────┘
│ level_up.py(等级/法术位表 单一事实源)
┌───────▼──────────────────────────────────────────────────────┐
│ 持久化:output/*.wwf(世界)+ *.player(角色 JSON)+ *.timeline.md │
└──────────────────────────────────────────────────────────────┘
关键解耦:dice_server.py 是独立进程(MCP stdio server),game_engine.py 通过 mcp.ClientSession 作为客户端连入,二者只靠 JSON 工具调用通信。所有随机数在 server 侧用 random.randint 产生,LLM 只发工具调用、读返回的 narrative_format,无法自造数值。
2. 核心工作流:玩家命令 → 规则判定 → 骰子结算 → LLM 叙事 → 状态持久化
这是项目最重要的一条链路,贯穿 game_engine.py 与 dice_server.py:
- 启动装配(game_engine.py:124-166):读入
GameMaster_MCP.md作为 system prompt、.wwf世界文件、.timeline.md历史;stdio_client启动dice_server.py子进程,session.list_tools()把 server 的@mcp.tool转成 LLM 的 function-calling schema(game_engine.py:137-147)。 - 玩家输入(game_engine.py:434):
prompt_async收取文本,非/开头即作为 action 交给chat_with_tools。 - LLM 产出工具调用(game_engine.py:170-276):
chat_fn返回message.tool_calls,逐个session.call_tool(name, arguments)执行,结果以role:"tool"追加回 messages;若 LLM 输出{{_NEED_AN_OTHER_PROMPT}}同步令牌则暂停(game_engine.py:271-274)。 - 规则判定与骰子结算(dice_server.py):如
resolve_attack(1504-1787)一次完成「d20 攻击掷骰→命中判定(优势/强击/暴击)→伤害掷骰(暴击翻倍主骰、附加骰不翻倍)→HP 结算→击杀判定→XP 奖励」;resolve_magic(1990-2992)完成「法术位校验与消耗→攻击/豁免→伤害/治疗→AoE 多目标→临时HP/状态→击杀与 XP」。 - 两阶段协议(GameMaster_MCP.md:32-67):机械阶段 LLM 只发工具调用、不发叙事,审计清单跑满后发同步令牌;叙事阶段才输出 prose,并把每个工具返回的
narrative_format逐条嵌入「Mechanics:」块(GameMaster_MCP.md:59-67)。 - 持久化:数值改动落在内存 SQLite(
modify_player_numeric),/save(game_engine.py:353-392)把dump_player_db结果回写.player;每 5 回合触发 timeline 摘要落盘(game_engine.py:485-506)。
第二条链路:战斗结算(register_combatants → resolve_attack/resolve_magic)
- GM 开战先调
register_combatants(dice_server.py:1345):玩家从 DB 自动注册(名/HP/AC/DEX 先攻修正,1389-1424),NPC 逐个d20+init掷先攻并按总分排序返回(1426-1493)。 - 随后每次
resolve_attack/resolve_magic若省略target_current_hp则从_COMBAT_REGISTRY查表(1305-1342,1740-1745),命中后_registry_update_hp写回减伤后的 HP——同轮多人集火自动串联扣血。 - 击杀判定用「剩余 HP ≤ 0」而非原始值,击杀后
_registry_kill标记并按CR_XP_TABLE奖励 XP(1767-1776);法术的 AoE 豁免分支同样逐目标更新注册表(2722-2825)。 - 治疗法术(Cure Wounds 等)经同一
resolve_magic走sp_healing分支,写入注册表并min(..., max_hp)封顶(2544-2593),实现「玩家↔NPC↔NPC 三向治疗」。
3. 模块逐一分析
3.1 dice_server.py —— 5e 规则引擎(MCP server,3001 行)
- 职责:唯一掷骰与状态权威;所有工具返回
narrative_format供叙事逐字引用。 - 初始化
init_player_db(180-208):把.playerJSON 逐 key 装入:memory:SQLite 单表player(key,value),并补active_effects/[]、_active_buff_data/{}、temporary_hit_points/0。 - 底层骰子
_parse_and_roll_dice(255-274):解析XdY,random.randint(1,die_size)逐颗掷;roll_dice(1200-1250)、perform_check(1253-1302,自然 20/1 判定暴击/大失败)是其公开封装。 - 法术库
_load_spells(49-68):惰性加载config/spells.yml,按名小写索引;_compute_spell_damage(86-135)实现戏法 5/11/17 级缩放与高阶施法(higher_levels字段)。 - 数值状态
modify_player_numeric(538-725):点分路径;current_hit_points走_apply_hp_change(277-357,THP 吸收、钳位、归零即 Unconscious+死亡豁免);spellcasting.slots.N先_validate_spell_slot(513-535)再扣;xp越阈值自动调apply_level_up(692-722)。 - 列表状态
update_player_list(728-892):增删清单;删active_effects时按_active_buff_data反推-delta回滚数值(837-858)。 - 休息
rest(924-1197):短休逐个消耗生命骰1d<hd>+CON(991-1017)、邪术师契约法术位全回、法师奥术恢复ceil(level/2)贪心补低环(1027-1058);长休回满 HP、回max(level//2,1)生命骰、清空效果并回滚(1081-1114)、可换准备法术并校验(1116-1169)。 - 战斗注册表
register_combatants(1345-1501)+ 帮助函数(1305-1342):玩家从 DB 自动注册并掷先攻(1389-1424),NPC 逐个掷d20+init,排序返回先攻顺序;add_to_existing=True不抹表、不掷新先攻。 - 武器攻击
resolve_attack(1504-1787):见 §2 第 4 点;优势双掷取高(1577-1584)、force_crit强制暴击(1598-1602)、附加骰不随暴击翻倍(1679 注释「Bug 2 fix」)。 - 法术
resolve_magic(1990-2992):最复杂路径。法术位校验/消耗在掷骰前(2233-2316);重复 buff 拒绝在耗位前(2216-2231);卷轴施法超环做d20+施法调整 vs DC 10+环阶检定(2324-2359);HP 池法术按 HP 升序排目标、依次耗尽池(2595-2652);AoE 多目标豁免单次掷伤害、逐目标掷豁免(2722-2825)。
3.2 game_engine.py —— 会话编排(515 行)
- 职责:MCP 客户端、游戏主循环、斜杠命令、timeline、图像生成。
run_game(87-515)核心;chat_with_tools(170-276)是工具调用/同步令牌状态机。- 斜杠命令
handle_slash_command(321-398):/help/stats/save/sync/quit;/save手动回滚 buff 后写.player(361-390)。 - timeline 机制:
TIMELINE_INTERVAL=5(16)、TIMELINE_PROMPT固定格式(18-31);load_timeline(34-39)/append_timeline_file(42-54);每 5 轮round_counter % 5 == 0时用同一chat_with_tools让 GM 生成摘要,仅当含「Key Events」才落盘(485-506);下次启动把历史注入 system(157-164),实现「缓存友好、旧条目复用」。 - 图像
_auto_generate_image(278-319):从dump_player_db拼角色锚点,交给image_gen_fn,存output/current_scene.png。
3.3 level_up.py —— 升级与法术位表(305 行)
- 单一事实源:全/半/契约施法者法术位表(8-76)、
CASTER_TYPE_MAP(78-88)、熟练加值表(101-107)、职业生命骰(109-122)。 apply_level_up(194-305):升级回写 level/熟练/生命骰,掷生命骰加 CON 增 HP(245-266,山丘矮人 +1/级),重算法术位与 DC/攻击加值(277-303)。注意:只做数值,职业特性/ASIs/子职需 GM 手动补(dice_server.py:554、720-721)。
3.4 多后端 LLM 抽象
统一契约即 run_game 的 docstring(game_engine.py:92-105):chat_fn(messages, tools, model, context_window) -> {'prompt_eval_count', 'message': {'content', 'tool_calls'}}。各 play_with_*.py 只实现该闭包:
play.py(Ollama,199 行):create_ollama_chat_fn(63-157),ollama.AsyncClient().chat,带 500/502/503 指数退避(145-155)。play_with_deepseek.py(309 行):OpenAI 兼容AsyncOpenAI(base_url="https://api.deepseek.com"),增量把内部 messages 转 OpenAI 格式(74-129),空响应重试(211-230)。play_with_gpt/gemini/claude/nano.py:同接口不同协议适配(未逐行读全,结构由 README 63-95 与深 seek 版可推)。play_with_nano.py额外实现create_image_gen_fn与 Gemini 工具 schema 转换(28-431)。
3.5 forge/ —— World Forge(世界生成器)
models.py(219 行):Pydantic 强类型Stats/Item/NPC/PlayerCharacter/Guild/Kingdom/WorldState等。config_loader.py(72 行):load_config(50-72)读 5 个 YAML 校验为Config。character_creator.py(898 行):create_character(442+)交互建卡,point-buy 27 点(479-513)、calculate_ac(343)、roll_starting_gold(384)。population_generator.py(264 行):populate_world(224-264)硬编码 4 王国与关系;_generate_npc_details(36-222)随机生成 NPC 并算 HP/AC/CR/施法 DC。formatter.py(135 行):format_world_to_wwf(96-133)写.wwf紧凑格式;get_player_json(38-94)写.player。main.py(57 行):串起「建卡→生成王国→公会→随机一名 walker→写文件」。
3.6 display.py —— TUI 渲染(206 行)
render_gm_text(18-27):Markdown**bold**/*italic*/#转 Rich 标记。format_stats(54-206):把dump_player_db的 dict 拆成角色/战斗/属性/施法/技能/背包/声望/持续效果多个 Panel。render_image(30-41):调用kitty +kitten icat显示图片。
3.7 GameMaster_MCP.md —— 系统提示词(168 行)
- 角色/协议元数据(15-23);
states.ACTIVE.turn_cycle定义「机械阶段→审计循环→同步令牌→叙事阶段」完整两阶段协议(31-67)。 directives.combat:突袭战/开战都要求「先register_combatants再任何攻击/法术」(88-104),增援用add_to_existing=True;round_completion规定全员行动才算回合结束(109-110)。content_restrictions.srd_compliance:明确禁用名单(Strahd/夺心魔/非 SRD 法术等)与安全名单(113-118)。failure_modes:列举 10 种协议违反模式及正确行为(126-146),是「防 AI 偷懒/漏结算」的提示词工程核心。systems.state_management.sync_handshake:{{_SYNC_DATABASE}}触发dump_player_db自查补账(152-163),对应/sync命令。
4. 数据模型与持久化
.wwf世界文件(formatter.py:96-133):带schemas:头的紧凑文本;NPC 压成[lvl, race, class, ac, hp, stats[6], walker]数组(get_npc_array:33-36)。示例output/electronistu_weave.wwf(108 行)含 4 王国、每国 ruler + 若干 guild leader/right_hand。.player角色 JSON(formatter.py:38-94,示例 344 行):name/level/xp/gold/stats/spellcasting/consumables/reputation/...。reputation为{kingdom: {faction: [{"name","description"}...]}}三层结构,跨会话持久。- 运行时状态:
:memory:SQLite 单表(dice_server.py:186-204),key 为顶层字段,dict/list 存 JSON 字符串。注意:故事本身不持久,只靠/save写角色卡 + timeline 摘要衔接会话(README 187-195)。 .timeline.md:每 5 回合追加## Rounds X-Y | 地点 | 时间段落(game_engine.py:18-54)。
5. 与 Z.R.I.C 的功能对照表
| Z.R.I.C 能力 | Project Infinity 对应实现 | 位置 | 结论 |
|---|---|---|---|
| 剧本解析 | 无剧本文件;世界由 World Forge 程序化生成(4 王国/公会/NPC) | forge/population_generator.py:224 | 部分(生成而非解析) |
| 推演分支 | 无分支系统;LLM 自由叙事,靠系统提示词约束 | GameMaster_MCP.md | 未实现(无结构化分支) |
| 自动判定 | 全部机械判定走 MCP 工具(豁免/命中/HP/击杀) | dice_server.py resolve_attack/magic | 强 |
| 世界状态 | .wwf 只读注入 + 玩家状态 SQLite |
game_engine.py:124-127, dice_server.py | 强(偏玩家侧) |
| NPC 情绪记忆 | 无情绪状态机;NPC 仅静态数据 + reputation(玩家侧) | forge/models.py NPC | 未实现 |
| 地图 | 无空间地图/移动系统 | — | 未实现 |
| 触发器 | 无触发器表;靠 GM 系统提示词手动遵循流程 | GameMaster_MCP.md | 未实现 |
| 时间线 | .timeline.md 每 5 回合自动摘要 + 下次注入 |
game_engine.py:16,485-506 | 有(轻量版) |
| 记忆系统 | 无 RAG/向量记忆;仅 timeline 文本回灌 + SQLite 状态 | game_engine.py:157-164 | 部分 |
| 本地模型 | Ollama 后端(play.py) | play.py:63 | 有 |
| 许可 | 源码 MIT + SRD 内容 CC-BY-4.0 双许可 | README:363-380, LICENSE | 有 |
6. 设计评价
优点
- 「AI 不生成骰子」落实彻底:随机数只在独立 MCP server 进程内
random.randint产生(dice_server.py:271,1232,1277,1578,2414),LLM 只能读工具返回的narrative_format;且系统提示词强制「每次调用必须对应叙事行」(GameMaster_MCP.md:67),从进程边界到提示词双重兜底。 - 单次工具调用完成全链路结算:
resolve_attack/resolve_magic把命中→伤害→HP→击杀→XP 一次返回,避免多次往返的数值漂移,是防幻觉的关键设计(dice_server.py:1504,1990)。 - 战斗注册表 + 顺序命中共享 HP:
_COMBAT_REGISTRY让同轮多次命中自动携带上次扣血后的 HP(dice_server.py:1305-1342,1740-1751),README 255-258 所述「注册一次、逐击扣减」有对应实现。 - 两阶段协议防「边算边编」:机械阶段与叙事阶段用
{{_NEED_AN_OTHER_PROMPT}}/{{_CONTINUE_EXECUTION}}令牌硬隔离,还枚举 10 种失败模式(GameMaster_MCP.md:32-67,126-146)。 - 可逆副作用:buff 用
_active_buff_data记录增量,移除/长休时按-delta回滚(dice_server.py:837-858,1104-1114),不产生脏状态。
缺陷
- 世界/NPC 状态是只读的:
.wwf只注入一次(game_engine.py:126-127),NPC 死亡/关系变化不写回,dump_player_db也不含世界状态——「持久世界」只停留在玩家侧。 - NPC-vs-NPC 与部分路径不落库:
is_npc_vs_npc分支只更新内存注册表、明确「no player HP modified, no XP auto-awarded」(dice_server.py:1536,1707-1737),且注册表是进程内 dict(dice_server.py:20),重启即失。 - 死亡豁免只是标记:
_apply_hp_change归零仅返回"death_saves": True提示(dice_server.py:337-348),未实现 3 成功/3 失败的真实死亡豁免计数——README 275 的「triggers death saves」被高估。 - 法术库依赖外部 YAML 的字段约定:
resolve_magic大量硬读spell.get(...)字段名(dice_server.py:2156-2179),字段缺失即静默走默认分支,与config/spells.yml(5619 行)强耦合且无 schema 校验。 - HP 池/治疗对
targets依赖参数传 HP:Sleep 按t.get("current_hp")排序而非必查注册表(dice_server.py:2622-2629),GM 若传错 HP 会得到错误受影响列表。 - 经验表/阈值是硬编码常量:
XP_THRESHOLDS与CR_XP_TABLE手写于模块顶(dice_server.py:22-43),与 level_up.py 的表分属两处,改规则需两处同步。
7. 结论
Project Infinity 是「MCP 规则引擎 + LLM 叙事」范式里完成度较高、把「公平骰子」落实得最彻底的一个:独立骰子进程 + 单调用全链路结算 + 两阶段协议三管齐下,使战斗/法术/豁免/休息/升级的数值几乎不可能被 LLM 捏造。其边界也很清晰——它解决的是机械正确性,而非叙事结构化:分支、地图、触发器、NPC 情绪记忆等 Z.R.I.C 级别的叙事引擎能力基本缺失,世界状态只读、NPC 关系与死亡豁免等也停在「提示 GM 手动处理」的层面。因此它更适合作为「有真骰子的单人 AI 跑团 GM」,而非完整的世界推演引擎;若需对标 Z.R.I.C,最值得借鉴的是它的「骰子权威进程 + 两阶段结算」设计,而它缺失的部分恰是 Z.R.I.C 的主场。