Skill: caoyachao/worldline-choice (version 4.4.1)

--- name: worldline-choice description: | AI驱动的互动叙事游戏引擎 v4.4.1 - 强制自动保存版。作为主持人(agent)，你与玩家共同创造一个开放式的故事世界。 核心职责：初始化游戏→生成场景和ABCD+E选项→接收玩家选择→调用引擎执行d20检定→根据骰子结果讲述剧情→更新状态。 本skill强制透明暴露d20结果，禁止LLM脑补骰子结果；引擎层强制执行每回合自动保存，禁止LLM幻觉保存行为。 --- # Worldline Choice — Agent 主持手册 (v4.4.1) ## 1. 这是什么？ `worldline-choice` 是一个 **AI + d20 检定** 驱动的互动叙事游戏引擎。 - **你不是在写固定剧本**，而是根据玩家的选择和骰子结果，实时推演故事。 - **骰子结果是绝对的**： engine 会客观生成 `d20 + 属性修正 vs DC` 的结果，你的叙事必须服从骰子决定的成功/失败程度，不能随意改写。 - **每回合都是一场小电影**：你要描述场景、制造冲突、给出选择、然后讲述后果。 - **v4.4.1 强制自动保存**：每回合结束由引擎层强制执行自动保存，返回 `auto_save` 字段。** - **v4.3.0 战术增强**：在连续回合中，玩家可以通过**前置准备**、**NPC协作**、**环境互动**来建立“加成链”，提升后续关键行动的成功率。 ### 什么时候触发？ 当用户说"开始游戏"、"继续玩"、"我要选 A"、或做出任何与当前剧情相关的行动时，调用本 skill。 ## 2. 【强制】d20检定执行规则（v4.4.0） ### 绝对禁止的行为 ❌ **禁止自行脑补骰子结果**：你不能根据"剧情需要"或"玩家体验"自行决定成功或失败。 ❌ **禁止在检定前生成结果性叙事**：在调用 `execute_check` 或 `process_turn` 之前，你只能描述场景和选项，**不能**说"你成功了"或"你失败了"。 ❌ **禁止覆盖骰子结果**：即使骰子显示"大失败"，你也不能写成"虽然失败了但意外有好事发生"。 --- ## 3. 【强制】自动保存机制（v4.4.0+） ### 保存行为已被引擎接管 **每回合 `process_turn` 返回时，自动保存已经由引擎强制执行完成**。 你**不需要**、也**不应该**自己决定"是否要保存"。保存是引擎层的强制行为，不是你的决策。 ### `process_turn` 返回的保存信息 ```json { "turn": 3, "action": "我说服守卫放我过去", "narrative": "...", "auto_save": { "save_id": "auto_turn_3_1712567823", "success": true, "timestamp": "2024-04-08T12:30:23" } } ``` ### 绝对禁止的保存相关幻觉 ❌ **禁止说"我来保存一下"**：保存已经由引擎完成，不是你"来"做的。 ❌ **禁止说"正在保存..."**：保存是瞬间完成的，不存在"正在"的过程。 ❌ **禁止说"记得保存"或"别忘了保存"**：暗示保存还没发生，这是错误的。 ❌ **禁止编造保存失败**：`auto_save.success` 为 false 时才可能保存失败，且你应该报告错误而不是编造。 ### 正确的保存信息展示 当 `auto_save.success` 为 true 时，你可以简要告知玩家： > "✓ 第 3 回合已自动保存" 或完全省略（保存是后台行为，玩家不需要每次都关心）。 当 `auto_save.success` 为 false 时，报告错误： > "⚠ 自动保存失败：{auto_save.error}" 但不要惊慌，游戏可以继续，只是建议玩家稍后手动保存一次。 ### 正确的执行顺序（强制） 每回合你必须严格按照以下顺序执行： ``` 1. 展示场景和选项（A/B/C/D/E）给玩家 ↓ 2. 等待玩家输入 ↓ 3. 【必须】调用 analyze_action 分析意图（可选，但推荐） ↓ 4. 【必须】调用 execute_check 执行d20检定 ↓ 5. 【必须】将 check_result 传给 narrative 生成 ↓ 6. 向玩家展示骰子结果 + 叙事 ``` **或者使用简化的流程：** ``` 1. 展示场景和选项 ↓ 2. 等待玩家输入 ↓ 3. 【必须】调用 process_turn（内部自动执行 analyze + execute_check + narrative） ↓ 4. 向玩家展示完整结果 ``` ### 使用 execute_check 的示例 当需要单独控制检定流程时： ```json // 第1步：分析行动（可选，用于预览DC） // analyze_action {"action": "我说服守卫放我过去"} // 返回: {"primary_attribute": "INFLUENCE", "base_dc": 14} // 第2步：执行d20检定【强制】 // execute_check { "attribute": "INFLUENCE", "dc": 14, "advantage": false } // 返回: {"roll": 8, "modifier": 2, "total": 10, "dc": 14, "degree": "失败", "success": false} // 第3步：基于骰子结果生成叙事【强制】 // generate_narrative { "action": "我说服守卫放我过去", "intention": "通过说服让守卫放行", "check_result": {"roll": 8, "modifier": 2, "total": 10, "dc": 14, "degree": "失败", "success": false}, "world_setting": "赛博朋克" } // 返回的 narrative 必须描述失败后果，不能改写为成功 ``` ### 使用 process_turn 的简化示例 当不需要单独控制时，使用 `process_turn` 一步完成： ```json // process_turn {"player_input": "我说服守卫放我过去"} // 返回包含完整的检定结果和叙事： { "turn": 3, "action": "我说服守卫放我过去", "intention": "通过说服让守卫放行", "check": { "roll": 8, "modifier": 2, "total": 10, "dc": 14, "degree": "失败", "success": false }, "narrative": "守卫冷笑着摇头：'这种借口我一天听二十遍。'他按响了警报...", "consequences": {...} } ``` ### 叙事铁律：骰子至高无上 | 骰子结果 | 你的叙事必须... | 禁止行为 | |:---|:---|:---| | 大成功 | 超额完成，有意外之喜 | 写成普通成功 | | 成功 | 顺利完成 | 添加不必要的代价 | | 勉强成功 | 完成但有瑕疵/隐患 | 写成完美成功 | | 勉强失败 | 失败但可补救 | 写成成功但"不完美" | | 失败 | 明确失败，承担后果 | 用"但是"转折成成功 | | 大失败 | 灾难性后果 | 弱化后果或添加意外救场 | --- ## 4. 主持人（Agent）的核心工作流程 整个游戏循环遵循 **5 步法则**： ``` [开始] ↓ 1. start_game — 初始化世界观、角色、属性 ↓ 2. 人工生成开场场景 + 第一轮 ABCD+E 选项（或调用 generate_turn_options） ↓ 3. 等待玩家选择（A/B/C/D/E 或自由输入） ↓ 4. process_turn — 让引擎执行 d20 检定并返回叙事/后果 ↓ 5. 你将引擎返回的叙事润色后展示给玩家 ↓ [回到第 2 步，直到结局触发] ``` ### 关于 options（选项） - **A/B/C/D**：你可以自己根据剧情设计，也可以调用 `generate_turn_options()` 让引擎提供灵感。 - **E（自由选项）**：玩家总是可以说"我想做别的"。 - 每个预设选项末尾可以提示 `DC` 和关联属性，例如：`(DC 12 [MIND])`，增加策略感。 - **战术选项**：在 v4.3.0 中，你可以设计一些选项专门用于“建立优势”（如观察环境、布置陷阱、请求同伴掩护），这些选项本身可能不直接推进主线，但成功后会为后续回合提供 `active_benefit`。 --- ## 5. 快速启动：一局游戏怎么开？ ### Step 1: 初始化 调用 `start_game`： ```json { "world_setting": "水浒传", "player_role": "深藏不露的酒楼老板", "player_name": "少龙" } ``` 引擎返回初始属性和世界观。默认生成 6 项通用属性（FORCE, MIND, INFLUENCE, REFLEX, RESILIENCE, LUCK），值在 8-16 之间。 ### Step 2: 你的第一次输出 向玩家介绍： - 世界观氛围（2-3 句话） - 角色名、身份、初始属性 - **第一个场景**：将玩家置入一个有冲突的情境中 - **选项 A/B/C/D + E** --- ## 6. 处理玩家回合 ### 当玩家做出选择后 用 `process_turn` 传递玩家的行动描述： ```json { "player_input": "我笑着给张捕头倒酒，暗中塞给他银子拖延时间" } ``` 引擎内部会： 1. **分析意图** → 确定主属性（比如 INFLUENCE）和 DC 2. **掷骰子** → `d20 + 修正 >= DC` 3. **返回叙事 + 后果**（JSON） 4. **自动更新 GameState** ### 你应该怎么展示结果？ 推荐格式： ``` ━━━━━━━━━━━━━━ 【d20 检定】魅力 +2 = 14 + 2 = 16 (DC 12) → 成功！ ━━━━━━━━━━━━━━ 你笑眯眯地迎上去，三碗烈酒下肚，张捕头的眼神开始发飘…… （此处展开 150-300 字的剧情描述） 【状态更新】 - 财富: 100 → 70 (-30) - 获得标签: 长袖善舞 - 张捕头 态度变为: 收了银子仍猜忌 ``` > **注意**：如果 `show_dice=True`（默认），引擎会直接返回完整的骰子结果，你可以原样展示。 --- ## 7. d20 检定制：这是不可违抗的物理法则 ### 公式 `d20 + (属性 - 10) / 2（向下取整） >= DC` ### 结果程度（叙事必须严格遵守） | 程度 | 条件 | 叙事要求 | |:---|:---|:---| | **大成功** | 骰出 20，或总计超 DC 10+ | 超额完成，有意外之喜 | | **成功** | 超 DC 5+ | 顺利完成 | | **勉强成功** | 刚好 >= DC | 完成，但有代价、瑕疵或隐患 | | **勉强失败** | 差 1~4 | 失败，但留有补救余地 | | **失败** | 差 5~9 | 明确失败，承担负面后果 | | **大失败** | 骰出 1，或差 10+ | 灾难性后果，可能触发连锁危机 | ### 六项通用属性 | 属性 | 英文 | 典型场景 | |:---|:---|:---| | 力量 | FORCE | 战斗、破门、搬运重物 | | 智力 | MIND | 解谜、识破谎言、战术布局 | | 魅力 | INFLUENCE | 说服、欺骗、领导、社交 | | 敏捷 | REFLEX | 潜行、闪避、偷窃、翻墙 | | 体质 | RESILIENCE | 扛伤、耐毒、意志力检定 | | 运气 | LUCK | 意外发现、绝境逢生、赌局 | --- ## 8. 战术增强系统（v4.3.0） v4.3.0 在单回合 d20 框架内引入了 **加成链** 机制。玩家可以通过前置行动积累 `active_benefits`，在关键时刻一举释放，提升策略深度。 ### 6.1 三大战术来源 | 来源 | 调用方法 | 效果 | |:---|:---|:---| | **前置准备** | `add_active_benefit`（或由你在 narrative 成功后手动调用） | 玩家花一整回合做准备（勘察地形、磨刀、散布谣言等），成功后获得 DC 降低或优势 | | **NPC 协作** | `execute_npc_check` | 让 NPC 执行一个子任务；若 NPC 检定成功，自动为玩家添加一个 `active_benefit` | | **环境互动** | `set_scene_objects` + `interact_with_scene_object` | 场景中存在可互动的物体（油灯、绳索、暗门等），激活后获得对应加成 | ### 6.2 active_benefit 的结构 每个加成包含： - `name`: 加成名称（如“居高临下”） - `description`: 描述 - `dc_modifier`: DC 修正（通常为负数，如 -3 表示降低 DC） - `advantage`: 是否提供优势（优势 = 掷两颗 d20 取高） - `applies_to`: 适用的属性列表（空数组表示适用所有属性） - `remaining_uses`: 剩余使用次数（默认 1） ### 6.3 计算有效 DC 与优势 在玩家决定执行最终行动前，调用 `calculate_effective_dc`： ```json { "base_dc": 15, "attribute": "FORCE" } ``` 引擎会自动叠加所有符合条件的 `active_benefits`，返回： - `effective_dc`: 最终 DC - `advantage`: 是否有优势 - `disadvantage`: 是否有劣势 - `applied_benefits`: 本次生效的加成列表 然后，将计算结果传入 `process_turn`： ```json { "player_input": "我从房梁一跃而下，挥刀劈向敌首", "dc_modifier": -3, "advantage": true } ``` > 注意：`dc_modifier` 是**所有加成与外部条件的总和**，正数为增加难度，负数为降低难度。 ### 6.4 设计战术选项的原则 - **准备回合必须有真实内容**：不能是“我随便找找”然后白拿 -5 DC；要描述具体的准备行为，并为此执行一次独立的 `process_turn`（可能判定其成功与否）。 - **加成会过期或消耗**：`remaining_uses` 会在 `process_turn` 成功应用后自动减 1，归零后移除（自动由引擎处理）。 - **NPC 不是万能打手**：`execute_npc_check` 中 NPC 的属性会根据其与玩家的关系浮动；关系越差，协助能力越弱甚至反噬。 - **环境道具有限**：`scene_objects` 是**场景级别**的，换场景后通常需要重新 `set_scene_objects`。 --- ## 9. 设计选项的核心原则（每回合必看） 当你为玩家生成 A/B/C/D 选项时，必须遵循以下原则： 1. **A/B/C 没有完美解** - 每个常规选项都必须有真实代价或风险。 - 错误示例："A. 毫发无伤地逃走" - 正确示例："A. 翻墙逃走，但会扭伤脚踝 (DC 14 [REFLEX])" 2. **D 选项是特殊路线（双刃剑）** - D 只在玩家拥有特定标签、物品或关系时出现。 - D 不是完美解决方案，而是**高效但有代价**。 - 示例： - "D. 你暗示自己认识知府大人（需【官场人脉】标签）→ 张捕头会知难而退，但三日内官场会派人来查你的底细" - "D. 你拔出柜台下的短剑——这是你最不想动用的底牌 → 可一剑定局，但你的隐秘身份将暴露于市井" 3. **战术选项（v4.3.0）** - 可以设置 1~2 个选项专门用于“建立优势”。 - 示例： - "B. 先爬上屋顶观察全局，为后续行动找制高点 (DC 12 [REFLEX]) → 成功则后续战斗获得 -2 DC" - "C. 暗示旁边那位书生与你配合，让他去引开守卫 (DC 14 [INFLUENCE]) → 成功则获得优势" 4. **E 永远是自由输入** - 示例文本："E. 做其他你想做的事..." - 玩家可能说出完全不在你预期内的行动，接受它，交给 engine 检定。 --- ## 10. 叙事铁律（Agent 必须遵守） ### 骰子至高无上 **绝对禁止**：骰子结果是"失败"，你却写成"虽然没成功，但意外有好结果"。 ### 不要替玩家编造资源 如果玩家说"我的同伴突然出现帮我"，但他并没有这个同伴（不在 items/npcs 中），engine 会返回条件不满足错误或检定劣势。你应该如实描述"你四顾无人，呼喊只有空谷回音"。 ### 状态变更必须对应叙事 - 叙事说"受伤了" → 后果里要有 health/stamina 下降。 - 叙事说"获得了一枚令牌" → 后果里要有 items_gained。 - 叙事说"好不容易在屋顶找到一处好位置" → 调用 `add_active_benefit` 记录“居高临下”。 ### 保持世界观一致性 如果你的世界是"水浒传"，不要突然让角色掏出一把激光枪。 --- ## 11. 存档管理 ### 保存 ```json {"save_id": "shaolong_001"} ``` ### 加载 ```json {"save_id": "shaolong_001"} ``` > v4.2.0+ 引擎内置 `_migrate_legacy_save()`，v3.x 旧存档会自动迁移，无需额外处理。 --- ## 12. 关键 API 速查 | 工具/方法 | 作用 | 何时调用 | |:---|:---|:---| | `start_game` | 初始化新游戏 | 开局时 | | `generate_turn_options` | 生成本回合 A/B/C/D/E | 每回合开始前（可选） | | `process_turn` | 处理玩家行动，执行 d20，推进剧情 | 玩家做出选择后 | | `save_game` | 保存当前状态 | 玩家要求保存，或重要节点 | | `load_game` | 加载旧存档 | 玩家要求继续 | | `get_game_state` | 查看当前状态 | 需要核对属性/NPC/物品时 | | `set_scene_objects` | 设置场景中的互动道具 | 进入新场景时 | | `interact_with_scene_object` | 玩家与场景道具互动 | 玩家选择使用该道具时 | | `execute_npc_check` | 让 NPC 执行协助检定 | NPC 参与战术协同时 | | `add_active_benefit` | 手动添加战术加成 | 前置准备成功后 | | `get_active_benefits` | 查看当前所有可用加成 | 计算战术收益时 | | `calculate_effective_dc` | 计算加成后的最终 DC | 发动关键行动前 | --- ## 13. 完整示例：从开局到第一回合 ### 初始化 ```json // start_game { "world_setting": "1960年代香港黑帮", "player_role": "卧底警察", "player_name": "阿超" } ``` ### Agent 生成开场 ``` 游戏开始：1960年代香港黑帮 角色：阿超（卧底警察） 属性：FORCE 12 | MIND 14 | INFLUENCE 10 | REFLEX 13 | RESILIENCE 11 | LUCK 9 场景：九龙城寨的一家地下赌档里，你刚收到线报——今晚有人要"做掉"你的上线老鬼。 A. 不动声色，继续赌钱，同时找机会通风报信 (DC 14 [REFLEX]) B. 故意闹事，制造混乱让老鬼有机会脱身 (DC 12 [FORCE]) C. 直接亮出警徽制止火并 (DC 16 [INFLUENCE]) D. 你注意到角落里坐着社团红棍丧彪——他曾欠你一条命 (DC 10 [INFLUENCE]，需关系≥20) E. 做其他你想做的事... ``` ### 玩家选择 > 玩家：A ### Agent 调用 process_turn ```json // process_turn {"player_input": "不动声色，继续赌钱，同时找机会通风报信"} ``` ### 引擎返回（示例） ```json { "check": { "roll": 15, "modifier": 1, "total": 16, "dc": 14, "degree": "成功", "success": true }, "narrative": "你低下头继续摸牌，右手却在桌底悄悄将一张纸条塞给旁边的擦鞋童...", "consequences": { "tags_gained": ["沉着冷静"], "flags_set": {"老鬼已获救": true} } } ``` ### Agent 润色后输出 （按第 4 节推荐的格式展示给玩家） --- ## 14. 战术示例：加成链的实际运用 ### 场景：酒楼对峙 **第 1 回合 - 环境互动** > 玩家选择：先悄悄把油灯挪到对方身后，让对方的影子暴露在我的视野中。 Agent 调用： 1. `set_scene_objects` 预先设置 `[{"id":"oil_lamp","name":"油灯","description":"一盏黄豆大的油灯","interaction_hint":"可以移动位置改变光影","benefit":{"name":"光影掌控","dc_modifier":-2,"applies_to":["REFLEX","FORCE"]}}]` 2. `interact_with_scene_object` 处理玩家的移动油灯行动。 3. 成功后，引擎返回该 `benefit` 已加入 `active_benefits`。 **第 2 回合 - NPC 协作** > 玩家选择：让我那跑堂的兄弟在门口放哨，如果有人说上来就咳嗽示警。 Agent 调用 `execute_npc_check` (NPC: 跑堂小哥, action: 放哨, attribute: REFLEX, dc: 12)。 - 若 NPC 检定成功，自动获得一个 `active_benefit`（如“提前预警”，提供 advantage）。 **第 3 回合 - 总攻** > 玩家选择：趁他影子晃动的瞬间，从柜台后扑出去制服他。 Agent 先调用 `calculate_effective_dc` (base_dc: 14, attribute: FORCE) → 返回 `effective_dc: 12`（因油灯 -2），`advantage: true`（因放哨成功）。 再调用 `process_turn`： ```json { "player_input": "趁他影子晃动的瞬间，从柜台后扑出去制服他", "dc_modifier": -2, "advantage": true } ``` Agent 向玩家展示完整的骰子结果与叙事。 --- ## 15. 版本历史（简要） - **v4.4.1** (2026-04-09): 强制自动保存版。每回合结束由引擎层强制执行自动保存，返回结果中包含 `auto_save` 字段。新增「保存机制」章节，禁止LLM的保存相关幻觉行为。 - **v4.4.0** (2026-04-07): d20强制检定版。强化execute_check强制执行，禁止LLM脑补骰子结果，新增强制检定顺序检查清单。 - **v4.3.0** (2026-04-04): 战术增强版。整合前置准备、NPC协作、环境互动三大系统，引入 `active_benefits` 加成链机制。 - **v4.2.0** (2026-04-04): `worldline_engine.py` 重写为兼容薄层，默认 `show_dice=True`，支持 v3.x 存档自动迁移。 - **v4.1.1** (2026-04-03): 移除世界观硬编码，LLM 自动适配任意世界观。 - **v4.0.0** (2026-04-01): 架构革命，升级为 LLM + d20 混合架构，引入 ABCD+E 选项系统。 - **v3.x**: 纯代码驱动的多步骤战术检定系统（已归档但兼容）。 --- ## 许可证 MIT License

path: README.md
content: ---
name: worldline-choice
description: AI驱动互动叙事游戏引擎 - 在无限世界线中，你的每个选择都创造独一无二的故事。支持自由输入、实时剧情生成、多结局收集。
---

# Worldline Choice - 世界线·抉择

[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Version](https://img.shields.io/badge/version-v4.4.1-green.svg)](https://github.com/caoyachao/worldline-choice)

> 对智能体说：
> 
> “安装skill： https://github.com/caoyachao/worldline-choice"
> 
> "开始游戏"

**Worldline Choice** 是一个AI驱动的开放式互动叙事引擎。可以在科幻、奇幻、历史、现代都市等各种世界中扮演关键角色，也可以创造全新的原创世界。所有剧情由AI实时生成，**没有固定剧本**每个选择都在创造一条独一无二的世界线。

---

## 📋 目录

1. [游戏概述](#-游戏概述)
2. [能玩什么](#-能玩什么)
3. [核心体验](#-核心体验)
4. [怎么玩](#-怎么玩)
5. [抉择系统](#-抉择系统)
6. [角色与状态](#-角色与状态)
7. [技术机制](#-技术机制)
8. [快速开始](#-快速开始)
9. [设计原则](#-设计原则)
10. [版本历史](#-版本历史)

---

## 🎮 游戏概述
（以下都是AI写的，大概是这么回事）

### 一句话介绍

> **这不是游戏，是你和AI一起写的一本小说，而你，就是主角。**

在传统文字冒险游戏中，编剧预设了3个结局，你选一个。  
在 Worldline Choice 中，**AI根据你的每一个选择实时生成剧情**，没有预设结局——你的每个决定都在创造全新的故事走向。

### 游戏定位

| 项目 | 内容 |
|------|------|
| **游戏名称** | 世界线·抉择 (Worldline: Choice) |
| **游戏类型** | AI驱动互动叙事 / 开放式文字冒险 |
| **核心体验** | 「你就是主角，重写传奇」 |
| **目标用户** | 喜爱剧情、历史、文学、角色扮演的玩家 |
| **单局时长** | 15-45分钟（短篇）/ 1-2小时（长篇） |

### 两大游戏模式

**🎭 叙事模式：自由探索，重写传奇**
- 穿越赛博朋克都市，成为黑客或企业特工
- 探索克苏鲁神话世界，揭开古老秘密
- 置身太空歌剧，在星际间书写你的传奇
- 潜入冷战谍战，在阴影中改变历史
- 进入黑帮江湖，体验权力与背叛
- **甚至创造你自己的世界观**

⚔️ **战术模式：策略推演，运筹帷幄**
- 千人对战场景，设计分步战术
- 每一步都由AI判定成败
- 像军师一样排兵布阵

---

## 🌍 能玩什么

### 官方世界观

| 世界观 | 背景 | 可扮演角色 |
|--------|------|-----------|
| **赛博侦探** | 2077年新东京，高科技与低生活的碰撞 | 黑客、企业特工、私家侦探、仿生人 |
| **三体·星战篇** | 水滴降临 | 云天明、大史同事、王教授学徒、新兴科技领袖 |
| **黑手党往事** | 1920年代禁酒令时期的芝加哥 | 帮派成员、联邦探员、走私商、政客 |
| **克苏鲁纪元** | 1920年代，神秘事件频发 | 调查员、考古学家、神秘学者 |
| **太空站危机** | 木卫二科考站，氧气即将耗尽 | 工程师、医生、指挥官、AI研究员 |

### 无限可能

不只是官方世界观，你可以和AI一起创造：
- **穿越到任何影视作品**：西部世界、权力的游戏、绝命毒师…
- **扮演任何角色**：太空船长、黑帮老大、吸血鬼猎人、时间旅行者
- **改写任何结局**：让泰坦尼克号避开冰山，让蝙蝠侠拯救小丑…

---

## ✨ 核心体验

### 真正的自由

**不限于预设选项**——每个抉择点都有4个预设选项 + 1个自由输入通道：

```
┌──────────────────────────────────────────────────────────────┐
│  【抉择点】荒坂大厦潜入                                        │
│                                                              │
│  你站在荒坂集团大厦的阴影中，看着巡逻的安保无人机...            │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  A. 【正面】伪装成维修人员从正门进入                       │  │
│  ├────────────────────────────────────────────────────────┤  │
│  │  B. 【迂回】寻找通风管道潜入                               │  │
│  ├────────────────────────────────────────────────────────┤  │
│  │  C. 【技术】远程黑入监控系统制造盲区                         │  │
│  ├────────────────────────────────────────────────────────┤  │
│  │  D. 【特殊】若【声望】≥20，可利用内部关系直接获取门禁卡       │  │
│  ├────────────────────────────────────────────────────────┤  │
│  │  💭 我另有打算...（点击输入自定义行动）                   │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘
```

**自由输入示例**：
> 玩家输入："我想贿赂那个警卫，让他放我进数据库"

> AI回应："警卫看了看你的信用芯片，挑了挑眉：'这点钱只够买一杯合成咖啡，朋友。'"

### 每次都不一样

同样的开局，不同的选择 = 完全不同的故事：

**示例：赛博侦探的三种开局**

| 路线 | 关键选择 | 可能结局 |
|------|---------|---------|
| **企业线** | 接受荒坂集团雇佣 | 升职加薪、成为高管、发现公司黑暗秘密… |
| **黑客线** | 加入地下反抗组织 | 被通缉、揭露真相、成为数字幽灵… |
| **独狼线** | 单干做赏金猎人 | 名利双收、被黑白两道追杀、归隐贫民窟… |

我玩了5遍赛博侦探篇，5次结局都不一样。

### 失败也有趣

被追杀、被背叛、战死沙场、众叛亲离…  
每种失败都有对应的剧情，而且往往比成功更精彩。

**示例：黑入企业数据库失败**
> 你掷出了自然1（大失败）  
> 安全系统立即锁定你的位置，警报声震耳欲聋！  
> 荒坂集团的特工已经在路上了，第一局就这么凉了…  

但那种"差一点就成功"的感觉，让人一直想重来！

### 你的选择被记住

NPC会记得你之前做过什么：
- 背叛过的人下次见你态度会不一样
- 帮助过的人会在关键时刻伸出援手
- 你的性格标签会影响NPC对你的看法

这种"真实感"太绝了。

---

## 🎯 怎么玩

### 安装（2A游戏 - 面向智能体）

Worldline Choice 是一款 **2A 游戏**（Agent-to-Agent，面向智能体）：

**方式一：通过 OpenClaw 安装**
```
在 OpenClaw 中发送：
安装 skill worldline-choice
或
访问 https://github.com/caoyachao/worldline-choice
```

**方式二：通过 CLI 直接对话**
```bash
# 克隆仓库
git clone https://github.com/caoyachao/worldline-choice.git
cd worldline-choice

# 运行游戏
python3 worldline_engine.py --new "赛博侦探" "黑客" "你的名字"
```

**系统要求**：
- Python 3.8+
- Mac / Windows / Linux 均可

### 游戏流程

```
1. 选择世界观
   └─ 赛博朋克 / 太空歌剧 / 克苏鲁 / 谍战 / 黑帮 / 原创...

2. 创建角色
   └─ 选择身份：黑客 / 侦探 / 走私商 / 调查员 / 特工 / 自定义
   └─ 设定名字

3. 开始故事
   └─ AI生成场景描述
   └─ 呈现抉择点（4选项 + 自由输入）

4. 做出选择
   └─ 选预设选项，或输入你的想法
   └─ AI实时生成剧情回应

5. 推动剧情
   └─ 重复步骤3-4，直到结局

6. 收集结局
   └─ 同一世界观，多周目探索不同结局
```

---

## ⚔️ 抉择系统

### 四类预设选项

每个抉择点围绕**当前场景的核心冲突**设计：

**模式一：阵营/立场选择**
```
A. 【支持甲方】理由/方式
B. 【支持乙方】理由/方式
C. 【保持中立】寻找第三条路/暂时观望
D. 【特殊条件】基于状态的独特选择
```

**模式二：策略/方法选择**
```
A. 【直接方案】正面突破，高效但风险高
B. 【迂回方案】侧面渗透，耗时但稳妥
C. 【非常规方案】利用环境/第三方/诡计
D. 【特殊条件】基于能力的专属方案
```

**模式三：价值观/道德选择**
```
A. 【利己】优先保障自身/己方利益
B. 【利他】牺牲自己帮助他人
C. 【平衡】寻求多方共赢（难度更高）
D. 【特殊条件】基于信仰/原则的立场
```

### D选项：条件触发式特殊路线

D选项不一定是"特殊道具"，更可以是**基于之前选择解锁的独特剧情方向**：

| 触发类型 | 示例场景 | D选项示例 |
|----------|----------|----------|
| **前置关系** | 与某NPC好感度达标 | 【求助】关键时刻，你的黑客盟友远程黑入系统（需与黑客好感≥30） |
| **前置选择** | 之前做过特定选择 | 【兑现承诺】用你之前救下的那人的情报破局 |
| **身份专属** | 特定职业/出身 | 【以医生的身份】利用医学知识找到生物识别系统的漏洞 |

---

## 👤 角色与状态

### 属性系统

| 属性 | 说明 | 影响范围 | 初始值范围 |
|------|------|----------|------------|
| **武力** | 战斗能力 | 单挑胜率、战斗选项成功率 | 10-20 |
| **智力** | 谋略能力 | 识破陷阱、特殊选项解锁 | 10-20 |
| **魅力** | 人格魅力 | 说服成功率、NPC好感获取 | 10-20 |
| **声望** | 江湖名气 | 特殊NPC态度、势力选择 | 0-10 |

### 关系系统

```
关系等级：

憎恨 [-100, -50)  → 敌对行动，可能追杀玩家
冷淡 [-50, 0)     → 冷淡对待，交易/对话受限
中立 [0, 20)      → 普通对待，基础互动
友善 [20, 50)     → 友好对待，提供额外帮助
信任 [50, 80)     → 深度交流，解锁特殊剧情
生死之交 [80, 100] → 无条件支持，可能牺牲自己
```

### 标签系统

记录玩家的行为模式和关键选择：

| 标签类型 | 示例 | 作用 |
|----------|------|------|
| **性格标签** | 仁慈、残忍、狡诈、正直、勇敢、谨慎 | 影响NPC对玩家的态度 |
| **阵营标签** | 荒坂集团、流浪者部落、公司狗、街头小子 | 解锁对应势力选项 |
| **事件标签** | 数据大盗、空间站幸存者、义体改造人 | 特定剧情触发条件 |

---

## ⚙️ 技术机制

### d20检定系统

游戏中使用d20（20面骰）检定系统来决定行动成败：

```
检定公式：d20 + 属性修正 ≥ DC（难度等级）

结果等级：
• 大成功（超10+）：超额完成，额外收益
• 成功（超5+）：顺利完成，后续DC-3
• 勉强成功（0~4）：完成但有代价，后续DC-1
• 勉强失败（-1~-4）：失败但可补救
• 失败（-5~-9）：明确失败，承担后果
• 大失败（-10+）：灾难性后果

特殊规则：
• 自然20必成功（视为大成功）
• 自然1必失败（视为大失败）
```

### 战术多步骤判定

在战斗场景中，你可以设计复杂的多步战术：

```
示例：千人对战的连环计

1. 要求对方主将出来单挑
2. 在单挑中诈败引敌追击
3. 反身扔出暗器
4. 伏兵万箭齐发
5. 主力大军冲阵

每一步独立检定，步骤之间有依赖关系：
• 前置步骤成功 → 后续DC降低
• 关键步骤失败 → 可能中断整个计划
```

### 防套利机制（v3.3.2）

为防止玩家通过"低成本步骤蹭加成"套利：

| 机制 | 说明 |
|------|------|
| 勉强成功降级 | 勉强成功仅给后续DC-1（原为DC-3） |
| 指挥链过载 | 步骤超过3个后，每多一步DC+1 |
| 信息泄露 | 大失败会泄露情报，后续步骤DC累加+2 |

---

## 🚀 快速开始
(我没试过不保证对，建议直接对话安装）

### Python API 使用

```python
from worldline_engine import WorldlineEngine

# 创建游戏引擎
game = WorldlineEngine()

# 开始游戏
game.start_game(
    world_setting="赛博侦探",
    player_role="黑客",
    player_name="V"
)

# 获取AI Prompt用于生成场景
system_prompt = game.get_system_prompt()

# 处理玩家行动
result = game.process_action(
    player_input="我想黑入荒坂集团的数据库",
    ai_response={
        "intention": "入侵",
        "feasible": True,
        "narrative": "你启动了神经接口，防火墙比想象中更复杂...",
        "consequences": {
            "attribute_changes": {"智力": 1},
            "relationship_changes": {"荒坂集团": -10}
        }
    }
)

# 保存游戏
game.save_game("save_001")
```

### 命令行使用

```bash
# 开始新游戏
python3 worldline_engine.py --new "赛博侦探" "黑客" "V"

# 列出所有存档
python3 worldline_engine.py --list

# 加载存档
python3 worldline_engine.py --load save_001

# 运行测试
python3 test_engine.py
```

---

## 📝 设计原则

1. **真正的自由** - 不限于预设选项，可以输入任何想法
2. **实时生成** - 没有固定剧本，AI根据选择即时创作
3. **选择有重量** - 每个决定都会影响后续剧情和NPC态度
4. **失败也有趣** - 不是简单的GAME OVER，而是另一条故事线
5. **永不丢失历史** - 所有选择永久记录，支持完整复盘

---

## 📜 版本历史

### v4.4.1 (2026-04-09) - 强制自动保存版
- 引擎层强制执行每回合自动保存，返回结果包含 `auto_save` 字段
- 新增「自动保存机制」章节，禁止LLM保存相关幻觉行为
- 借鉴Harness强制工具调用思想，确保保存操作真实执行

### v4.4.0 (2026-04-07) - d20强制检定版
- 强制execute_check工具调用，禁止LLM脑补骰子结果
- 新增强制检定顺序检查清单
- 强化叙事prompt中的骰子结果绝对性约束

### v4.3.0 (2026-04-04) - 战术增强版
- 整合前置准备、NPC协作、环境互动三大战术系统
- 引入active_benefits加成链机制

### v4.0.0 (2026-04-01) - 架构革命版
- 升级为LLM + d20混合架构
- 引入ABCD+E选项系统

### v3.3.2 (2026-03-31) - 平衡补丁版
- 防套利机制：勉强成功降级、指挥链过载、信息泄露

### v3.3.1 (2026-03-31) - 数字标识版
- 数字标识多步骤判定、复合步骤独立检定

### v3.3 (2026-03-31) - 战术判定版
- 战术多步骤判定系统、关键步骤中断机制

### v3.0 (2026-03-30) - 通用框架版
- d20强制检定系统、6个通用能力维度

---

## 📄 许可证

MIT License

---

<p align="center">
  <i>"在无限的世界线中，你的选择定义了唯一的故事"</i>
</p>

<p align="center">
  <b>Worldline Choice</b> - 让每一次抉择都真实而有分量
</p>


path: openclaw_adapter.py
content: #!/usr/bin/env python3
"""
OpenClaw适配器
连接WorldlineSkill与OpenClaw的LLM调用机制
"""

import json
from typing import Dict, Callable, Optional
from worldline_skill import WorldlineSkill, GameState, NarrativeContext


class OpenClawAdapter:
    """
    OpenClaw适配器
    将WorldlineSkill封装为OpenClaw可调用的形式
    """

    def __init__(self, llm_call: Callable[[str, str], str], auto_save: bool = True, show_dice: bool = False):
        """
        Args:
            llm_call: OpenClaw提供的LLM调用函数
                     签名: fn(prompt: str, format: str) -> str
            auto_save: 是否每回合自动存档（默认True）
            show_dice: 是否显示骰子结果（默认False）
        """
        self.llm_call = llm_call
        self.skill = WorldlineSkill(llm_callback=self._llm_callback, auto_save=auto_save, show_dice=show_dice)

    def _llm_callback(self, prompt: str, response_format: str) -> Dict:
        """
        内部LLM回调，适配OpenClaw接口
        """
        try:
            # 调用OpenClaw提供的LLM
            response = self.llm_call(prompt, response_format)

            # 解析JSON响应
            if response_format == "json":
                # 处理可能的Markdown代码块
                if "```json" in response:
                    response = response.split("```json")[1].split("```")[0]
                elif "```" in response:
                    response = response.split("```")[1].split("```")[0]

                return json.loads(response.strip())
            else:
                return {"content": response}

        except Exception as e:
            # 回退到默认响应
            return {
                "error": str(e),
                "fallback": True,
                "intention": "未知意图",
                "action_type": "通用",
                "primary_attribute": "MIND",
                "base_dc": 10,
                "dc_reasoning": "使用默认配置",
                "risks": ["失败"],
                "required_items": [],
                "required_knowledge": []
            }

    # ============ OpenClaw工具接口 ============

    def start_game(self, world_setting: str, player_role: str,
                   player_name: str, world_description: str = "") -> Dict:
        """开始新游戏"""
        return self.skill.start_game(world_setting, player_role,
                                      player_name, world_description)

    def process_turn(self, player_input: str) -> Dict:
        """处理游戏回合"""
        return self.skill.process_turn(player_input)

    def analyze_action(self, action: str) -> Dict:
        """仅分析行动，不执行"""
        # 临时创建分析
        from worldline_skill import LLMDriver
        llm = LLMDriver(self._llm_callback)
        analysis = llm.analyze_action(action, self.skill.state)
        return analysis.to_dict()

    def execute_check(self, attribute: str, dc: int,
                      advantage: bool = False, disadvantage: bool = False) -> Dict:
        """执行d20检定"""
        from worldline_skill import D20Engine

        attr_value = self.skill.state.player["attributes"].get(attribute, 10)
        result = D20Engine.execute_check(
            attribute_value=attr_value,
            dc=dc,
            advantage=advantage,
            disadvantage=disadvantage
        )
        return result.to_dict()

    def generate_narrative(self, action: str, intention: str,
                          check_result: Dict, world_setting: str) -> Dict:
        """生成叙事"""
        from worldline_skill import NarrativeContext, CheckResult, LLMDriver

        # 重建CheckResult
        cr = CheckResult(**check_result)

        ctx = NarrativeContext(
            action=action,
            intention=intention,
            check_result=cr,
            world_setting=world_setting,
            scene_description=self.skill.state.current_scene,
            player_name=self.skill.state.player["name"]
        )

        llm = LLMDriver(self._llm_callback)
        return llm.generate_narrative(ctx)

    def generate_turn_options(self, previous_result: Optional[Dict] = None) -> Dict:
        """生成本回合的ABCD预定义选项 + E自由选项"""
        options = self.skill.generate_turn_options(previous_result)
        return options.to_dict()

    def process_option(self, choice: str, free_text: Optional[str] = None) -> Dict:
        """处理玩家选择的选项"""
        # 先获取当前选项
        options = self.skill.generate_turn_options()
        return self.skill.process_option(options, choice, free_text)

    def save_game(self, save_id: str) -> Dict:
        """保存游戏"""
        path = self.skill.save_game(save_id)
        return {"saved": True, "path": path}

    def load_game(self, save_id: str) -> Dict:
        """加载游戏"""
        success = self.skill.load_game(save_id)
        return {"loaded": success, "state": self.skill.get_state() if success else None}

    def get_game_state(self) -> Dict:
        """获取游戏状态"""
        return self.skill.get_state()


# ============ OpenClaw运行时入口 ============

def create_skill(llm_call: Callable[[str, str], str], auto_save: bool = True, show_dice: bool = False) -> OpenClawAdapter:
    """
    OpenClaw调用的工厂函数

    Usage:
        skill = create_skill(openclaw_llm_call)
        result = skill.process_turn("我尝试说服守卫")

    Args:
        llm_call: OpenClaw提供的LLM调用函数
        auto_save: 是否每回合自动存档（默认True）
        show_dice: 是否显示骰子结果（默认False，后台静默投骰）
    """
    return OpenClawAdapter(llm_call, auto_save=auto_save, show_dice=show_dice)


# ============ 测试用模拟LLM ============

def mock_llm_call(prompt: str, format: str) -> str:
    """
    模拟LLM调用，用于测试
    """
    # 根据prompt内容返回模拟响应
    if "分析玩家行动" in prompt or "分析这个行动" in prompt:
        return json.dumps({
            "intention": "尝试进行一般行动",
            "action_type": "通用",
            "primary_attribute": "MIND",
            "base_dc": 12,
            "dc_reasoning": "一般难度",
            "risks": ["失败"],
            "required_items": [],
            "required_knowledge": []
        }, ensure_ascii=False)

    elif "生成叙事" in prompt or "基于骰子结果" in prompt:
        # 从prompt中提取骰子结果 - 精确匹配
        degree = "成功"
        if "结果程度: 大成功" in prompt:
            degree = "大成功"
        elif "结果程度: 大失败" in prompt:
            degree = "大失败"
        elif "结果程度: 勉强成功" in prompt:
            degree = "勉强成功"
        elif "结果程度: 勉强失败" in prompt:
            degree = "勉强失败"
        elif "结果程度: 失败" in prompt:
            degree = "失败"

        templates = {
            "大成功": "你完美地完成了行动，效果超出预期！",
            "成功": "你顺利地完成了行动。",
            "勉强成功": "你完成了行动，但过程有些惊险。",
            "勉强失败": "你差一点就成功了。",
            "失败": "你的行动失败了。",
            "大失败": "灾难！你的行动彻底失败。"
        }

        return json.dumps({
            "narrative": templates.get(degree, "行动结束。"),
            "consequences": {
                "attribute_changes": {},
                "items_gained": [],
                "items_lost": [],
                "relationship_changes": {},
                "flags_set": {},
                "tags_gained": [],
                "scene_change": ""
            },
            "ending_triggered": False,
            "ending_type": ""
        }, ensure_ascii=False)

    elif "生成本回合" in prompt or "预定义选项" in prompt or "回合选项" in prompt:
        # 返回剧情化的模拟选项
        return json.dumps({
            "context": "你站在岔路口，前方传来争吵声和金属碰撞的声音。空气中弥漫着紧张的气息...",
            "options": [
                {
                    "letter": "A",
                    "description": "握紧武器，大步走向声音来源",
                    "action": "毫不畏惧地走向声音来源，准备面对任何危险",
                    "dc_hint": 14,
                    "attr_hint": "FORCE"
                },
                {
                    "letter": "B",
                    "description": "躲进阴影，先观察情况",
                    "action": "悄悄躲进附近的阴影中，仔细观察声音来源和周围情况",
                    "dc_hint": 12,
                    "attr_hint": "REFLEX"
                },
                {
                    "letter": "C",
                    "description": "大声喊话，试图阻止冲突",
                    "action": "朝声音方向喊话，试图制止冲突并表明自己的存在",
                    "dc_hint": 16,
                    "attr_hint": "INFLUENCE"
                },
                {
                    "letter": "D",
                    "description": "绕路避开，不想惹麻烦",
                    "action": "悄悄绕行，避开声音来源，寻找其他安全的道路",
                    "dc_hint": 13,
                    "attr_hint": "MIND"
                }
            ],
            "free_text": {
                "description": "自定义行动（描述你想做的其他事情）"
            }
        }, ensure_ascii=False)

    return json.dumps({"content": "模拟响应"})


if __name__ == "__main__":
    # 测试运行
    print("测试OpenClaw适配器...")

    adapter = create_skill(mock_llm_call)

    # 开始游戏
    result = adapter.start_game("武侠", "剑客", "测试者")
    print(f"游戏初始化: {result}")

    # 处理回合
    turn = adapter.process_turn("我尝试与店小二交谈")
    print(f"回合结果:")
    print(f"  意图: {turn.get('intention')}")
    print(f"  检定: {turn.get('check')}")
    print(f"  叙事: {turn.get('narrative', '')[:50]}...")


path: skill.json
content: {
  "name": "worldline-choice",
  "version": "4.4.1",
  "description": "AI驱动互动叙事游戏引擎 v4.4.1 - 强制自动保存版。在v4.4.0基础上增加每回合强制自动保存机制，引擎层强制执行，LLM只需读取保存状态。禁止保存相关幻觉行为。",
  "author": "Worldline Team",
  "entry": "worldline_skill.py",
  "runtime": "python3",
  "tools": [
    {
      "name": "start_game",
      "description": "开始一个新的Worldline Choice游戏会话",
      "parameters": {
        "type": "object",
        "properties": {
          "world_setting": {
            "type": "string",
            "description": "游戏世界观设定，例如：武侠、赛博朋克、克苏鲁、现代都市等"
          },
          "player_role": {
            "type": "string",
            "description": "玩家角色类型，例如：剑客、黑客、侦探、法师等"
          },
          "player_name": {
            "type": "string",
            "description": "玩家角色姓名"
          },
          "world_description": {
            "type": "string",
            "description": "可选的世界观详细描述"
          }
        },
        "required": ["world_setting", "player_role", "player_name"]
      }
    },
    {
      "name": "process_turn",
      "description": "处理玩家的一个游戏回合。分析意图、执行d20检定、生成叙事。支持外部战术加成修正DC和应用优势/劣势",
      "parameters": {
        "type": "object",
        "properties": {
          "player_input": {
            "type": "string",
            "description": "玩家的自然语言行动描述"
          },
          "dc_modifier": {
            "type": "integer",
            "description": "外部DC修正值（例如来自前置准备或环境互动的加成），默认0"
          },
          "advantage": {
            "type": "boolean",
            "description": "是否拥有优势（如NPC协助成功或环境互动激活），默认false"
          },
          "disadvantage": {
            "type": "boolean",
            "description": "是否处于劣势，默认false"
          }
        },
        "required": ["player_input"]
      }
    },
    {
      "name": "analyze_action",
      "description": "分析玩家行动的意图、难度和可能后果（不执行检定）",
      "parameters": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "description": "玩家行动描述"
          }
        },
        "required": ["action"]
      }
    },
    {
      "name": "execute_check",
      "description": "执行d20检定（客观判定，不涉及LLM）",
      "parameters": {
        "type": "object",
        "properties": {
          "attribute": {
            "type": "string",
            "enum": ["FORCE", "MIND", "INFLUENCE", "REFLEX", "RESILIENCE", "LUCK"],
            "description": "检定时使用的属性"
          },
          "dc": {
            "type": "integer",
            "description": "难度等级（5-25）"
          },
          "advantage": {
            "type": "boolean",
            "description": "是否有优势"
          },
          "disadvantage": {
            "type": "boolean",
            "description": "是否有劣势"
          }
        },
        "required": ["attribute", "dc"]
      }
    },
    {
      "name": "generate_narrative",
      "description": "基于骰子结果生成叙事描述",
      "parameters": {
        "type": "object",
        "properties": {
          "action": {
            "type": "string",
            "description": "原始行动描述"
          },
          "intention": {
            "type": "string",
            "description": "行动意图"
          },
          "check_result": {
            "type": "object",
            "description": "d20检定结果"
          },
          "world_setting": {
            "type": "string",
            "description": "世界观"
          }
        },
        "required": ["action", "intention", "check_result", "world_setting"]
      }
    },
    {
      "name": "generate_turn_options",
      "description": "生成本回合的ABCD预定义选项和E自由选项",
      "parameters": {
        "type": "object",
        "properties": {
          "previous_result": {
            "type": "object",
            "description": "上一回合的结果（可选）"
          }
        }
      }
    },
    {
      "name": "process_option",
      "description": "处理玩家选择的选项（A/B/C/D/E）",
      "parameters": {
        "type": "object",
        "properties": {
          "choice": {
            "type": "string",
            "enum": ["A", "B", "C", "D", "E"],
            "description": "玩家选择的选项"
          },
          "free_text": {
            "type": "string",
            "description": "如果选E，玩家的自由输入文本"
          }
        },
        "required": ["choice"]
      }
    },
    {
      "name": "save_game",
      "description": "保存当前游戏状态",
      "parameters": {
        "type": "object",
        "properties": {
          "save_id": {
            "type": "string",
            "description": "存档标识"
          }
        },
        "required": ["save_id"]
      }
    },
    {
      "name": "load_game",
      "description": "加载游戏存档",
      "parameters": {
        "type": "object",
        "properties": {
          "save_id": {
            "type": "string",
            "description": "存档标识"
          }
        },
        "required": ["save_id"]
      }
    },
    {
      "name": "get_game_state",
      "description": "获取当前游戏状态摘要",
      "parameters": {
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "set_scene_objects",
      "description": "设置当前场景中的可互动物体（环境互动系统）",
      "parameters": {
        "type": "object",
        "properties": {
          "objects": {
            "type": "array",
            "description": "场景物体列表，每个元素包含 id, name, description, interaction_hint, benefit"
          }
        },
        "required": ["objects"]
      }
    },
    {
      "name": "interact_with_scene_object",
      "description": "玩家与场景中的指定物体互动。成功后该物体对应的 benefit 会自动加入 active_benefits",
      "parameters": {
        "type": "object",
        "properties": {
          "object_id": {
            "type": "string",
            "description": "物体ID"
          },
          "player_input": {
            "type": "string",
            "description": "玩家的互动行动描述"
          }
        },
        "required": ["object_id", "player_input"]
      }
    },
    {
      "name": "execute_npc_check",
      "description": "执行NPC的协助检定。成功后自动为玩家添加一个 active_benefit",
      "parameters": {
        "type": "object",
        "properties": {
          "npc_name": {
            "type": "string",
            "description": "NPC名称"
          },
          "action": {
            "type": "string",
            "description": "NPC执行的行动描述"
          },
          "attribute": {
            "type": "string",
            "enum": ["FORCE", "MIND", "INFLUENCE", "REFLEX", "RESILIENCE", "LUCK"],
            "description": "检定时使用的属性"
          },
          "dc": {
            "type": "integer",
            "description": "难度等级"
          }
        },
        "required": ["npc_name", "action", "attribute", "dc"]
      }
    },
    {
      "name": "add_active_benefit",
      "description": "手动添加一个战术加成（通常用于记录玩家的前置准备成功）",
      "parameters": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "加成名称"
          },
          "description": {
            "type": "string",
            "description": "加成描述"
          },
          "dc_modifier": {
            "type": "integer",
            "description": "DC修正值，例如-3表示降低3点DC"
          },
          "advantage": {
            "type": "boolean",
            "description": "是否提供优势"
          },
          "applies_to": {
            "type": "array",
            "items": { "type": "string" },
            "description": "适用属性列表，空数组表示适用所有属性"
          },
          "remaining_uses": {
            "type": "integer",
            "description": "可使用次数，默认1"
          }
        },
        "required": ["name", "description"]
      }
    },
    {
      "name": "consume_active_benefit",
      "description": "按名称消耗一个 active_benefit",
      "parameters": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "要消耗的加成名称"
          }
        },
        "required": ["name"]
      }
    },
    {
      "name": "get_active_benefits",
      "description": "获取当前所有可用的战术加成，可按属性过滤",
      "parameters": {
        "type": "object",
        "properties": {
          "filter_attribute": {
            "type": "string",
            "description": "可选，过滤适用某属性的加成"
          }
        }
      }
    },
    {
      "name": "calculate_effective_dc",
      "description": "计算经过所有可用加成后的最终 DC 和 advantage 状态",
      "parameters": {
        "type": "object",
        "properties": {
          "base_dc": {
            "type": "integer",
            "description": "基础DC"
          },
          "attribute": {
            "type": "string",
            "description": "本次行动使用的属性"
          }
        },
        "required": ["base_dc", "attribute"]
      }
    }
  ]
}


path: worldline_engine.py
content: #!/usr/bin/env python3
"""
Worldline Choice - 兼容入口层 (v4.4.1)

本文件作为向后兼容的入口点，统一代理到 worldline_skill.py 的核心实现。
确保 agent / CLI / 旧代码都能使用到内建的 d20 检定系统。

修改要点：
1. WorldlineEngine 继承自 WorldlineSkill，默认 show_dice=True，透明暴露 d20 结果。
2. 兼容旧版 API 签名（如 get_system_prompt / process_action 等映射到新接口）。
3. 旧版存档格式可通过 _migrate_legacy_save() 自动迁移到新版 flat GameState。
"""

import json
import os
import sys
from typing import Dict, List, Optional, Any

# 从新版 skill 导入全部公开接口
from worldline_skill import (
    AttributeDimension,
    ActionAnalysis,
    CheckResult,
    NarrativeContext,
    TurnOption,
    TurnOptions,
    D20Engine,
    LLMDriver,
    GameState,
    WorldlineSkill,
    cli_main,
)


class WorldlineEngine(WorldlineSkill):
    """
    向后兼容的引擎类。

    默认启用骰子显示 (show_dice=True)，让 agent/CLI 都能直接看到 d20 检定过程。
    同时保留旧版 ``WorldlineEngine`` 类的使用习惯。
    """

    def __init__(self, llm_callback: Optional[Any] = None, auto_save: bool = True):
        super().__init__(
            llm_callback=llm_callback,
            auto_save=auto_save,
            show_dice=True,  # 默认开启：让 d20 检定可见
        )
        self.save_dir = os.path.expanduser("~/.claude/skills/worldline_choice/saves")
        os.makedirs(self.save_dir, exist_ok=True)

    # ------------------------------------------------------------------
    # 旧版 API 兼容映射
    # ------------------------------------------------------------------

    def initialize_world(
        self,
        world_setting: str,
        player_role: str = "",
        player_name: str = "",
        world_desc: str = "",
    ):
        """旧版初始化签名兼容。"""
        self.state = GameState()
        self.state.world_setting = world_setting
        self.state.world_description = world_desc
        self.state.current_scene = world_desc or f"{world_setting}的世界"
        self.state.player["name"] = player_name or "主角"
        self.state.player["role"] = player_role or "参与者"

        # 使用新版随机属性生成逻辑
        for attr in self.state.player["attributes"]:
            self.state.player["attributes"][attr] = D20Engine.calculate_modifier(10) + 10
        return self.state

    def get_system_prompt(self) -> str:
        """
        旧版接口：生成 system prompt 用于外部 LLM。
        由于新版已经内置默认回退叙事，这里提供一份可直接扔给 LLM 的 prompt，
        其中明确声明 **必须使用 d20 检定结果**叙事，禁止推翻骰子。
        """
        recent_history = self.get_history_summary(limit=5)
        npcs_text = "\n".join(
            f"- {name}: 关系{info.get('relationship', 0)}, 态度{info.get('attitude', '中立')}"
            for name, info in self.state.npcs.items()
        ) if self.state.npcs else "暂无重要NPC"

        return f"""你是《世界线·抉择》的叙事AI。这是一个基于"{self.state.world_setting}"世界观的互动叙事游戏。

【核心规则 - 必须遵守】
1. 本游戏采用 **d20 客观检定** 决定行动成败。骰子结果由引擎自动生成，叙事必须严格服从骰子结果，禁止编造反结果。
2. 成功程度定义：
   - 大成功(≥10高出DC或骰出20)：超额完成，有额外收益
   - 成功(≥5高出DC)：顺利完成
   - 勉强成功(≥0)：完成但有代价或瑕疵
   - 勉强失败(-1~-4)：失败但有机会补救
   - 失败(-5~-9)：明确失败，承担后果
   - 大失败(≤-10或骰出1)：灾难性后果

【当前游戏状态】
- 回合数: {self.state.turn_count}
- 玩家角色: {self.state.player['name']} ({self.state.player['role']})
- 玩家属性: {json.dumps(self.state.player['attributes'], ensure_ascii=False)}
- 持有物品: {', '.join(self.state.player['items']) or '无'}
- 性格标签: {', '.join(self.state.player['tags']) or '暂无'}
- 知道的秘密: {', '.join(self.state.player['secrets']) or '暂无'}

【重要NPC】
{npcs_text}

【最近历史】
{recent_history}

【你的任务】
1. 根据当前状态生成一个引人入胜的场景（100-200字），包含冲突或抉择点
2. 生成4个剧情方向的选项(A/B/C/D)和1个自由选项(E)
   - A/B/C 必须有真实权衡，没有完美选择
   - D 选项是特殊路线，需要条件时才出现，且必有代价
3. 当收到玩家行动后，基于给定的 d20 检定结果生成叙事和状态变更(JSON)
"""

    def get_action_prompt(self, player_input: str, check_result: Optional[CheckResult] = None) -> str:
        """旧版接口：生成处理玩家行动的 prompt（新版内部已自动完成）。"""
        # 直接返回系统 prompt 的扩展版本，供外部 LLM 使用
        cr_text = ""
        if check_result:
            cr_text = (
                f"骰子结果: d20={check_result.roll}, 修正={check_result.modifier:+d}, "
                f"总计={check_result.total}, DC={check_result.dc}, 程度={check_result.degree}"
            )
        return f"{self.get_system_prompt()}\n\n【玩家行动】\n{player_input}\n{cr_text}\n\n请生成叙事与状态变更(JSON)。"

    def process_action(self, player_input: str, ai_response: Optional[Dict] = None) -> Dict:
        """
        旧版接口：处理玩家行动。
        新版中已内建 intent分析 → d20检定 → 叙事生成 → 应用后果 的完整链路，
        因此这里直接调用 process_turn 即可。
        """
        return self.process_turn(player_input)

    def load_game(self, save_id: str) -> bool:
        """加载存档，自动兼容旧版格式。"""
        filepath = os.path.join(self.save_dir, f"{save_id}.json")
        if not os.path.exists(filepath):
            return False
        with open(filepath, "r", encoding="utf-8") as f:
            data = json.load(f)
        # 兼容旧版富结构存档 -> 新版扁平结构
        data = self._migrate_legacy_save(data)
        self.state = GameState.from_dict(data)
        return True

    @staticmethod
    def _migrate_legacy_save(data: Dict) -> Dict:
        """将旧版 v3.x 富结构存档转换为新版 v4.x 扁平结构。"""
        # 如果已经是新版格式，直接返回
        if data.get("version", "").startswith("4."):
            return data

        # 旧版 normally 在 "player" -> "attributes" 下可能是结构化 dict
        player = data.get("player", {})
        raw_attrs = player.get("attributes", {})
        flat_attrs = {}
        for k, v in raw_attrs.items():
            if isinstance(v, dict) and "value" in v:
                flat_attrs[k] = v["value"]
            else:
                flat_attrs[k] = v

        # 将旧版中文属性名映射到新版通用维度（若存在）
        attr_mapping = {
            "武力": "FORCE",
            "力量": "FORCE",
            "智力": "MIND",
            "智慧": "MIND",
            "魅力": "INFLUENCE",
            "口才": "INFLUENCE",
            "敏捷": "REFLEX",
            "潜行": "REFLEX",
            "体质": "RESILIENCE",
            "意志": "RESILIENCE",
            "运气": "LUCK",
        }
        migrated_attrs = {}
        for k, v in flat_attrs.items():
            mk = attr_mapping.get(k, k)
            migrated_attrs[mk] = v

        # 确保6大维度都存在
        for dim in ["FORCE", "MIND", "INFLUENCE", "REFLEX", "RESILIENCE", "LUCK"]:
            if dim not in migrated_attrs:
                migrated_attrs[dim] = 10

        # 限制在 1-20
        for k in migrated_attrs:
            migrated_attrs[k] = max(1, min(20, int(migrated_attrs[k])))

        player["attributes"] = migrated_attrs

        # 历史记录：旧版可能是 dict，新版要求是 list
        hist = data.get("history", [])
        if isinstance(hist, dict):
            hist = hist.get("raw", [])
        data["history"] = hist

        # NPCs：旧版富结构 npc_database 需要扁平化
        npcs = data.get("npcs", {})
        if not npcs and "npc_database" in data:
            npcs = {}
            for npc_id, info in data["npc_database"].items():
                name = info.get("basic_info", {}).get("name", npc_id)
                matrix = info.get("relationship_matrix", {}).get("towards_player", {})
                trust = matrix.get("trust", {}).get("value", 5)
                respect = matrix.get("respect", {}).get("value", 5)
                fear = matrix.get("fear", {}).get("value", 0)
                relationship = (trust - 5) * 10 + (respect - 5) * 5 - fear * 5
                npcs[name] = {
                    "relationship": relationship,
                    "attitude": info.get("current_state", {}).get("mood", "中立"),
                    "known_secrets": info.get("knowledge_state", {}).get("secrets_known_by_player", []),
                }
            data["npcs"] = npcs

        # flags / story_flags 统一
        flags = data.get("flags", {})
        if "story_flags" in data:
            flags.update(data["story_flags"])
        data["flags"] = flags

        # 版本升级标记
        data["version"] = "4.4.1"
        return data

    # ------------------------------------------------------------------
    # 便捷：直接暴露 d20 给旧代码或 agent 脚本
    # ------------------------------------------------------------------

    def roll_check(self, attribute: str, dc: int, advantage: bool = False, disadvantage: bool = False) -> CheckResult:
        """
        显式执行一次 d20 检定，方便 agent 在外部调用。
        """
        attr_value = self.state.player["attributes"].get(attribute, 10)
        return self.d20.execute_check(
            attribute_value=attr_value,
            dc=dc,
            advantage=advantage,
            disadvantage=disadvantage,
        )

    # ------------------------------------------------------------------
    # 战术增强系统代理（v4.3.0）
    # ------------------------------------------------------------------

    def set_scene_objects(self, objects: List[Dict]) -> Dict:
        """代理到 WorldlineSkill.set_scene_objects"""
        return super().set_scene_objects(objects)

    def interact_with_scene_object(self, object_id: str, player_input: str) -> Dict:
        """代理到 WorldlineSkill.interact_with_scene_object"""
        return super().interact_with_scene_object(object_id, player_input)

    def execute_npc_check(self, npc_name: str, action: str, attribute: str, dc: int) -> Dict:
        """代理到 WorldlineSkill.execute_npc_check"""
        return super().execute_npc_check(npc_name, action, attribute, dc)

    def add_active_benefit(self, name: str, description: str, dc_modifier: int = 0,
                           advantage: bool = False, applies_to: Optional[List[str]] = None,
                           remaining_uses: int = 1) -> Dict:
        """代理到 WorldlineSkill.add_active_benefit"""
        return super().add_active_benefit(name, description, dc_modifier, advantage, applies_to, remaining_uses)

    def consume_active_benefit(self, name: str) -> Optional[Dict]:
        """代理到 WorldlineSkill.consume_active_benefit"""
        return super().consume_active_benefit(name)

    def get_active_benefits(self, filter_attribute: Optional[str] = None) -> List[Dict]:
        """代理到 WorldlineSkill.get_active_benefits"""
        return super().get_active_benefits(filter_attribute)

    def calculate_effective_dc(self, base_dc: int, attribute: str) -> Dict:
        """代理到 WorldlineSkill.calculate_effective_dc"""
        return super().calculate_effective_dc(base_dc, attribute)


# ============ CLI 兼容层 ============

class GameCLI:
    """向后兼容的 CLI 包装，实际代理到新版 cli_main。"""

    def __init__(self):
        self.engine = WorldlineEngine()

    def run(self, args: List[str]):
        """兼容旧版参数解析，然后启动新版 CLI 或命令。"""
        if len(args) < 1:
            self.show_help()
            return

        command = args[0]

        if command in ("--new", "-n"):
            world = args[1] if len(args) > 1 else input("请输入世界观设定: ")
            role = args[2] if len(args) > 2 else input("请输入你的角色身份: ")
            name = args[3] if len(args) > 3 else input("请输入角色名字: ")
            result = self.engine.start_game(world, role, name)
            print(f"\n{'='*50}")
            print(f"游戏开始: {result['world']}")
            print(f"角色: {name} ({role})")
            print(f"属性: {json.dumps(result['player']['attributes'], ensure_ascii=False)}")
            print(f"\n新版引擎已启用 d20 检定系统。")
            print(f"现在可以使用 process_turn() 或 generate_turn_options() 继续游戏。")

        elif command in ("--load", "-l"):
            if len(args) < 2:
                print("错误: 请指定存档ID")
                return
            save_id = args[1]
            if self.engine.load_game(save_id):
                print(f"已加载存档: {save_id}")
                print(f"当前回合: {self.engine.state.turn_count}")
            else:
                print(f"存档不存在: {save_id}")

        elif command in ("--list", "-ls"):
            saves = [
                f.replace(".json", "")
                for f in os.listdir(self.engine.save_dir)
                if f.endswith(".json")
            ]
            if not saves:
                print("暂无存档")
                return
            for s in saves:
                print(f"  {s}")

        elif command in ("--delete", "-d"):
            if len(args) < 2:
                print("错误: 请指定存档ID")
                return
            filepath = os.path.join(self.engine.save_dir, f"{args[1]}.json")
            if os.path.exists(filepath):
                os.remove(filepath)
                print(f"已删除存档: {args[1]}")
            else:
                print(f"存档不存在: {args[1]}")

        elif command in ("--help", "-h"):
            self.show_help()

        else:
            # 默认：直接启动新版交互式 CLI
            print("启动新版交互式 CLI...")
            cli_main()

    def show_help(self):
        print("""
Worldline Choice - d20 检定版游戏引擎

用法:
  python3 worldline_engine.py [命令] [参数]

命令:
  --new, -n [世界观] [角色] [名字]  开始新游戏
  --load, -l <存档ID>              加载存档
  --list, -ls                      列出所有存档
  --delete, -d <存档ID>            删除存档
  (无命令)                         启动交互式 CLI (带ABCDE选项 + d20检定)
        """)


def main():
    """主入口"""
    cli = GameCLI()
    cli.run(sys.argv[1:])


if __name__ == "__main__":
    main()


path: worldline_skill.py
content: #!/usr/bin/env python3
"""
Worldline Choice - LLM驱动 + d20检定混合架构 (v4.4.1)
面向OpenClaw智能体和CLI的Skill实现

核心设计：
1. LLM负责：意图理解、DC评估、叙事生成（基于骰子结果）
2. d20负责：客观判定（成功/失败程度）- 必须通过execute_check工具执行
3. 引擎负责：状态管理、规则执行、强制骰子结果不可被LLM覆盖
"""

import json
import os
import random
import sys
from dataclasses import dataclass, asdict
from datetime import datetime
from typing import Dict, List, Optional, Any, Callable
from enum import Enum


# ============ 核心数据模型 ============

class AttributeDimension(Enum):
    FORCE = "FORCE"           # 力量/战斗
    MIND = "MIND"             # 智力/技术
    INFLUENCE = "INFLUENCE"   # 魅力/社交
    REFLEX = "REFLEX"         # 敏捷/潜行
    RESILIENCE = "RESILIENCE" # 体质/意志
    LUCK = "LUCK"             # 运气


@dataclass
class CheckResult:
    """d20检定结果"""
    roll: int           # 原始骰子 (1-20)
    modifier: int       # 属性修正
    total: int          # 总计
    dc: int             # 难度
    success: bool       # 是否成功
    margin: int         # 差值
    degree: str         # 程度：大成功/成功/勉强成功/勉强失败/失败/大失败

    def to_dict(self) -> Dict:
        return asdict(self)


@dataclass
class ActionAnalysis:
    """LLM对玩家行动的分析结果"""
    intention: str              # 玩家真实意图
    action_type: str            # 行动类型（战斗/社交/潜行/技术等）
    primary_attribute: str      # 主属性
    base_dc: int                # 基础DC
    dc_reasoning: str           # DC评估理由
    risks: List[str]            # 失败风险
    required_items: List[str]   # 需要的物品（如果有）
    required_knowledge: List[str]  # 需要的知识/能力

    def to_dict(self) -> Dict:
        return asdict(self)


@dataclass
class NarrativeContext:
    """叙事生成上下文"""
    action: str                 # 原始行动
    intention: str              # 意图
    check_result: CheckResult   # 检定结果
    world_setting: str          # 世界观
    scene_description: str      # 当前场景
    player_name: str

    def to_dict(self) -> Dict:
        return {
            "action": self.action,
            "intention": self.intention,
            "check_result": self.check_result.to_dict(),
            "world_setting": self.world_setting,
            "scene_description": self.scene_description,
            "player_name": self.player_name
        }


@dataclass
class TurnOption:
    """单个回合选项"""
    letter: str                 # A/B/C/D/E
    description: str            # 选项描述（展示给玩家）
    action: str                 # 实际执行的行动（传给引擎）
    dc_hint: Optional[int] = None  # 可选的难度提示（用于UI显示）
    attr_hint: Optional[str] = None  # 可选的属性提示

    def to_dict(self) -> Dict:
        return asdict(self)


@dataclass
class TurnOptions:
    """回合选项集合 (ABCD预定义 + E自由)"""
    options: List[TurnOption]   # A/B/C/D选项
    free_text: TurnOption       # E选项（自由输入）
    context: str                # 当前情境描述

    def to_dict(self) -> Dict:
        return {
            "options": [opt.to_dict() for opt in self.options],
            "free_text": self.free_text.to_dict(),
            "context": self.context
        }

    def get_option(self, letter: str) -> Optional[TurnOption]:
        """获取指定字母的选项"""
        letter = letter.upper()
        if letter == "E":
            return self.free_text
        for opt in self.options:
            if opt.letter == letter:
                return opt
        return None


# ============ 纯d20检定系统（客观判定） ============

class D20Engine:
    """
    纯代码实现的d20检定系统
    完全客观，不受LLM影响
    """

    @staticmethod
    def calculate_modifier(attribute_value: int) -> int:
        """D&D 5e风格修正：(属性-10)/2"""
        return (attribute_value - 10) // 2

    @staticmethod
    def execute_check(
        attribute_value: int,
        dc: int,
        advantage: bool = False,
        disadvantage: bool = False
    ) -> CheckResult:
        """
        执行d20检定

        Args:
            attribute_value: 属性值
            dc: 难度等级
            advantage: 优势（投2个取高）
            disadvantage: 劣势（投2个取低）
        """
        modifier = D20Engine.calculate_modifier(attribute_value)

        # 投骰
        if advantage and not disadvantage:
            roll = max(random.randint(1, 20), random.randint(1, 20))
        elif disadvantage and not advantage:
            roll = min(random.randint(1, 20), random.randint(1, 20))
        else:
            roll = random.randint(1, 20)

        total = roll + modifier
        success = total >= dc
        margin = total - dc

        # 确定程度
        if roll == 20:
            degree = "大成功"
        elif roll == 1:
            degree = "大失败"
        elif margin >= 10:
            degree = "大成功"
        elif margin >= 5:
            degree = "成功"
        elif margin >= 0:
            degree = "勉强成功"
        elif margin >= -4:
            degree = "勉强失败"
        elif margin >= -9:
            degree = "失败"
        else:
            degree = "大失败"

        return CheckResult(
            roll=roll,
            modifier=modifier,
            total=total,
            dc=dc,
            success=success,
            margin=margin,
            degree=degree
        )


# ============ 游戏状态管理 ============

class GameState:
    """精简版游戏状态，专注核心数据"""

    VERSION = "4.4.1"

    def __init__(self):
        # 基础信息
        self.world_setting: str = ""
        self.world_description: str = ""
        self.current_scene: str = ""

        # 玩家状态
        self.player = {
            "name": "",
            "role": "",
            "attributes": {
                "FORCE": 10,
                "MIND": 10,
                "INFLUENCE": 10,
                "REFLEX": 10,
                "RESILIENCE": 10,
                "LUCK": 10
            },
            "items": [],
            "tags": [],
            "secrets": []
        }

        # NPC关系（扁平结构）
        self.npcs: Dict[str, Dict] = {}

        # 历史与进度
        self.history: List[Dict] = []
        self.turn_count: int = 0
        self.flags: Dict[str, Any] = {}

        # 战术增强系统（v4.3.0）：前置准备 + NPC协助 + 场景互动
        self.active_benefits: List[Dict] = []    # 当前可用的战术加成
        self.scene_objects: List[Dict] = []      # 场景中的可互动物体
        self.npc_assist_log: List[Dict] = []     # NPC协助历史记录

        # 结局
        self.ending_triggered: bool = False
        self.ending_type: str = ""

    def update_attribute(self, attr: str, delta: int):
        """更新属性"""
        if attr in self.player["attributes"]:
            current = self.player["attributes"][attr]
            self.player["attributes"][attr] = max(1, min(20, current + delta))

    def add_item(self, item: str):
        """添加物品"""
        if item not in self.player["items"]:
            self.player["items"].append(item)

    def remove_item(self, item: str):
        """移除物品"""
        if item in self.player["items"]:
            self.player["items"].remove(item)

    def update_npc(self, name: str, **kwargs):
        """更新NPC关系"""
        if name not in self.npcs:
            self.npcs[name] = {
                "relationship": 0,
                "attitude": "中立",
                "known_secrets": []
            }
        self.npcs[name].update(kwargs)

    def add_history(self, action: str, result: Dict):
        """添加历史记录"""
        self.turn_count += 1
        self.history.append({
            "turn": self.turn_count,
            "action": action,
            "result": result,
            "timestamp": datetime.now().isoformat()
        })

    def to_dict(self) -> Dict:
        """序列化"""
        return {
            "version": self.VERSION,
            "world_setting": self.world_setting,
            "world_description": self.world_description,
            "current_scene": self.current_scene,
            "player": self.player,
            "npcs": self.npcs,
            "history": self.history[-50:],  # 只保留最近50条
            "turn_count": self.turn_count,
            "flags": self.flags,
            "ending_triggered": self.ending_triggered,
            "ending_type": self.ending_type,
            "active_benefits": self.active_benefits,
            "scene_objects": self.scene_objects,
            "npc_assist_log": self.npc_assist_log,
        }

    @classmethod
    def from_dict(cls, data: Dict) -> "GameState":
        """反序列化"""
        state = cls()
        state.world_setting = data.get("world_setting", "")
        state.world_description = data.get("world_description", "")
        state.current_scene = data.get("current_scene", "")
        state.player = data.get("player", state.player)
        state.npcs = data.get("npcs", {})
        state.history = data.get("history", [])
        state.turn_count = data.get("turn_count", 0)
        state.flags = data.get("flags", {})
        state.ending_triggered = data.get("ending_triggered", False)
        state.ending_type = data.get("ending_type", "")
        state.active_benefits = data.get("active_benefits", [])
        state.scene_objects = data.get("scene_objects", [])
        state.npc_assist_log = data.get("npc_assist_log", [])
        return state


# ============ LLM驱动层（抽象接口） ============

class LLMDriver:
    """
    LLM驱动层抽象
    实际实现由OpenClaw提供或CLI模拟
    """

    def __init__(self, callback: Optional[Callable] = None):
        """
        Args:
            callback: 调用LLM的回调函数
                     签名: fn(prompt: str, response_format: str) -> Dict
        """
        self.callback = callback

    def analyze_action(
        self,
        player_input: str,
        game_state: GameState
    ) -> ActionAnalysis:
        """
        分析玩家行动
        输出检定配置，不决定结果
        """
        prompt = self._build_analysis_prompt(player_input, game_state)

        if self.callback:
            result = self.callback(prompt, "json")
        else:
            # 默认实现（用于测试）
            result = self._default_analysis(player_input, game_state)

        return ActionAnalysis(**result)

    def generate_options(
        self,
        game_state: GameState,
        previous_result: Optional[Dict] = None
    ) -> TurnOptions:
        """
        生成本回合的ABCD预定义选项 + E自由选项

        Args:
            game_state: 当前游戏状态
            previous_result: 上一回合的结果（如果有）
        """
        prompt = self._build_options_prompt(game_state, previous_result)

        if self.callback:
            result = self.callback(prompt, "json")
        else:
            result = self._default_options(game_state)

        # 解析选项
        options = []
        for opt_data in result.get("options", []):
            options.append(TurnOption(**opt_data))

        free_text = TurnOption(
            letter="E",
            description=result.get("free_text", {}).get("description", "自定义行动..."),
            action="__FREE_TEXT__",  # 特殊标记表示需要玩家输入
            dc_hint=None,
            attr_hint=None
        )

        return TurnOptions(
            options=options,
            free_text=free_text,
            context=result.get("context", "请选择你的行动：")
        )

    def generate_narrative(
        self,
        context: NarrativeContext
    ) -> Dict:
        """
        根据骰子结果生成叙事
        """
        prompt = self._build_narrative_prompt(context)

        if self.callback:
            result = self.callback(prompt, "json")
        else:
            result = self._default_narrative(context)

        return result

    def _build_analysis_prompt(self, action: str, state: GameState) -> str:
        """构建行动分析Prompt"""
        return f"""你是一个公正的TRPG游戏主持人。你的任务是分析玩家的行动意图，设定检定参数，但**不决定成败**。

【当前游戏状态】
世界观: {state.world_setting}
场景: {state.current_scene}
玩家: {state.player['name']} ({state.player['role']})
属性: {json.dumps(state.player['attributes'], ensure_ascii=False)}
持有物品: {', '.join(state.player['items']) or '无'}

【玩家输入】
{action}

【你的任务】
分析这个行动，输出JSON：
{{
  "intention": "玩家真实意图的简要描述",
  "action_type": "行动类型（如：战斗/社交/潜行/技术/魔法/探索）",
  "primary_attribute": "主属性（FORCE/MIND/INFLUENCE/REFLEX/RESILIENCE/LUCK之一）",
  "base_dc": "建议的DC数值（5-25之间的整数）",
  "dc_reasoning": "为什么设置这个DC的理由",
  "risks": ["失败可能发生的风险1", "风险2"],
  "required_items": ["需要的物品（如果有）"],
  "required_knowledge": ["需要的知识或能力（如果有）"]
}}

【重要约束 - 违反将导致机制失效】
1. **只分析可行性，绝对不要预测骰子结果**：
   - 你不能说"你很有可能成功"或"这很难成功"
   - 你的任务只是设定DC和属性，成败由骰子决定
   - 在调用 execute_check 之前，你不知道结果

2. **不要给玩家建议**（"你应该..."）

3. **基于世界观判断什么是合理的**

4. **如果玩家声明了不存在的能力/物品，在分析中指出"但玩家目前不具备"**

【执行顺序提醒】
正确的流程是：analyze_action → execute_check → generate_narrative
你必须调用 execute_check 获取骰子结果，禁止自行脑补结果。
"""

    def _build_options_prompt(self, state: GameState, previous_result: Optional[Dict]) -> str:
        """构建回合选项生成Prompt"""
        prev_narrative = ""
        if previous_result:
            prev_narrative = f"""
【上一回合结果】
{previous_result.get('narrative', '无')}

上一回合检定: {previous_result.get('check', {}).get('degree', '未知')}
"""

        return f"""你是一个游戏主持人，为玩家提供有意义的行动选择。

【当前游戏状态】
世界观: {state.world_setting}
场景: {state.current_scene}
玩家: {state.player['name']} ({state.player['role']})
属性: {json.dumps(state.player['attributes'], ensure_ascii=False)}
持有物品: {', '.join(state.player['items']) or '无'}
当前回合: {state.turn_count + 1}
{prev_narrative}

【你的任务】
基于当前情境，生成4个预定义选项(A/B/C/D)和1个自由选项(E)。

输出JSON格式:
{{
  "context": "当前情境的简短描述（1-2句话）",
  "options": [
    {{
      "letter": "A",
      "description": "选项描述（15-20字，展示给玩家看的）",
      "action": "实际执行的行动（详细描述，传给引擎的）",
      "dc_hint": 12,
      "attr_hint": "MIND"
    }},
    {{
      "letter": "B",
      "description": "...",
      "action": "...",
      "dc_hint": 15,
      "attr_hint": "FORCE"
    }},
    {{
      "letter": "C",
      "description": "...",
      "action": "...",
      "dc_hint": 10,
      "attr_hint": "INFLUENCE"
    }},
    {{
      "letter": "D",
      "description": "...",
      "action": "...",
      "dc_hint": 18,
      "attr_hint": "REFLEX"
    }}
  ],
  "free_text": {{
    "description": "自定义行动（输入你想做的任何事情）"
  }}
}}

【选项设计原则 - 剧情导向】
1. **情境沉浸**: 选项必须紧密结合当前场景的具体元素，不是通用模板
2. **戏剧张力**: 四个选项应呈现真实的两难或多元价值观（如：道德vs效率，安全vs收益）
3. **角色扮演**: 选项应反映不同的人物性格或行事风格（如：鲁莽/谨慎/狡诈/仁慈）
4. **后果分化**: 每个选项应该导向明显不同的剧情分支，不只是换皮
5. **属性自然**: 属性使用应自然融入剧情，不要为了凑属性而硬塞
6. **难度合理**: DC应由情境复杂度决定，不是人为设置

【剧情化选项示例 - 不要照搬】
场景：你发现受伤的敌人倒在路边
A- "救他，可能获得情报但也可能暴露自己" (高风险高回报，INFLUENCE)
B- "搜刮物资后离开，不管他的死活" (安全但可能有道德代价，MIND)
C- "给他个痛快，结束他的痛苦" (残酷但确定，FORCE)
D- "藏起来观察，等他的同伴来" (被动但信息丰富，REFLEX)

【反例 - 避免这种】
❌ A- "用力量攻击" B- "用智力解谜" C- "用魅力说服" D- "用敏捷闪避"
这种选项只是换了个说法的属性选择，完全没有剧情意义。
"""

    def _build_narrative_prompt(self, ctx: NarrativeContext) -> str:
        """构建叙事生成Prompt"""
        return f"""你是一个叙事AI。骰子结果已经确定，你必须基于这个结果生成叙事。

【游戏背景】
世界观: {ctx.world_setting}
场景: {ctx.scene_description}
玩家: {ctx.player_name}

【行动描述】
意图: {ctx.intention}
原始输入: {ctx.action}

【骰子结果 - 这是不可更改的事实】
骰子点数: {ctx.check_result.roll}
属性修正: {ctx.check_result.modifier:+d}
总计: {ctx.check_result.total}
难度DC: {ctx.check_result.dc}
差值: {ctx.check_result.margin:+d}
结果程度: {ctx.check_result.degree}

【你的任务】
基于上述骰子结果，生成JSON：
{{
  "narrative": "剧情描述（200-300字），必须符合骰子结果的程度",
  "consequences": {{
    "attribute_changes": {{"属性名": 变化值}},
    "items_gained": ["获得的物品"],
    "items_lost": ["失去的物品"],
    "relationship_changes": {{"NPC名": 变化值}},
    "flags_set": {{"标志名": true}},
    "tags_gained": ["获得的标签"],
    "scene_change": "场景变化（如果有）"
  }},
  "ending_triggered": false,
  "ending_type": ""
}}

【叙事约束 - 严格遵守，违反将导致游戏机制崩溃】
1. **骰子结果是绝对不可更改的物理法则**：
   - 结果程度: {ctx.check_result.degree}
   - 你必须严格按照这个结果生成叙事，没有任何例外
   - **绝对禁止**：用"但是"、"然而"、"没想到"等转折词弱化失败或改写结果

2. **结果程度与叙事必须严格对应**：
   - 大成功: 超额完成，有意外之喜，效果显著
   - 成功: 顺利完成，达到预期目标
   - 勉强成功: 完成但付出代价、有瑕疵、或埋下隐患
   - 勉强失败: 明确失败，但留有补救机会或线索
   - 失败: 明确失败，承担负面后果，目标未达成
   - 大失败: 灾难性后果，目标彻底失败，可能引发连锁危机

3. **禁止行为（红线）**：
   ❌ 骰子失败却写成"虽然失败但意外获得好处"
   ❌ 骰子大失败却写成"差点失败"或"有惊无险"
   ❌ 用"运气不好"、"意外"来软化失败后果
   ❌ 给失败添加不应有的补偿性收益

4. **状态变更必须与叙事一致**：如果叙事中说"受伤了", consequences中要有相应体现
5. **不要编造**：所有状态变更必须基于叙事中实际发生的事件

【验证检查清单】
生成叙事前确认：
□ 骰子结果是 {ctx.check_result.degree}（{ctx.check_result.total} vs DC {ctx.check_result.dc}）
□ 叙事基调与结果程度一致
□ 没有使用转折词弱化结果
□ 状态变更有叙事支撑
"""

    def _default_analysis(self, action: str, state: GameState) -> Dict:
        """默认分析（用于无LLM时的回退）"""
        # 简单的关键词匹配作为回退
        action_lower = action.lower()

        # 判断行动类型和属性
        if any(w in action_lower for w in ["打", "杀", "战", "攻", "fight", "attack"]):
            return {
                "intention": "进行战斗",
                "action_type": "战斗",
                "primary_attribute": "FORCE",
                "base_dc": 15,
                "dc_reasoning": "标准战斗难度",
                "risks": ["受伤", "敌人警觉"],
                "required_items": [],
                "required_knowledge": []
            }
        elif any(w in action_lower for w in ["说", "劝", "骗", "talk", "persuade"]):
            return {
                "intention": "进行社交互动",
                "action_type": "社交",
                "primary_attribute": "INFLUENCE",
                "base_dc": 12,
                "dc_reasoning": "一般社交难度",
                "risks": ["对方反感", "信息泄露"],
                "required_items": [],
                "required_knowledge": []
            }
        elif any(w in action_lower for w in ["偷", "躲", "潜", "hide", "steal"]):
            return {
                "intention": "进行潜行",
                "action_type": "潜行",
                "primary_attribute": "REFLEX",
                "base_dc": 14,
                "dc_reasoning": "潜行需要不被发现",
                "risks": ["被发现", "陷入包围"],
                "required_items": [],
                "required_knowledge": []
            }
        else:
            return {
                "intention": "进行一般行动",
                "action_type": "通用",
                "primary_attribute": "MIND",
                "base_dc": 10,
                "dc_reasoning": "一般难度",
                "risks": ["失败"],
                "required_items": [],
                "required_knowledge": []
            }

    def _default_options(self, state: GameState) -> Dict:
        """默认选项（用于无LLM时的回退）- 剧情导向设计"""
        # 基于回合数轮换不同的情境模板，展示多样化的剧情选项
        templates = [
            {
                "context": f"你站在{state.current_scene}的十字路口，前方传来奇怪的声音...",
                "options": [
                    {
                        "letter": "A",
                        "description": "握紧武器，大步走向声音来源",
                        "action": "毫不畏惧地走向声音来源，准备面对任何危险",
                        "dc_hint": 14,
                        "attr_hint": "FORCE"
                    },
                    {
                        "letter": "B",
                        "description": "躲进阴影，先观察情况",
                        "action": "悄悄躲进附近的阴影中，仔细观察声音来源",
                        "dc_hint": 12,
                        "attr_hint": "REFLEX"
                    },
                    {
                        "letter": "C",
                        "description": "大声喊话，试图交流",
                        "action": "朝声音方向喊话，表明自己没有敌意，试图沟通",
                        "dc_hint": 15,
                        "attr_hint": "INFLUENCE"
                    },
                    {
                        "letter": "D",
                        "description": "分析环境，寻找其他路径",
                        "action": "冷静分析周围环境，寻找可以绕行的安全路径",
                        "dc_hint": 13,
                        "attr_hint": "MIND"
                    }
                ]
            },
            {
                "context": f"一个陌生人拦住了你，眼神中带着试探...",
                "options": [
                    {
                        "letter": "A",
                        "description": "坦诚相待，说出真实目的",
                        "action": "直视对方的眼睛，坦诚地说出自己的真实目的",
                        "dc_hint": 14,
                        "attr_hint": "INFLUENCE"
                    },
                    {
                        "letter": "B",
                        "description": "编造谎言，试探对方反应",
                        "action": "编造一个半真半假的故事，观察对方的反应",
                        "dc_hint": 16,
                        "attr_hint": "MIND"
                    },
                    {
                        "letter": "C",
                        "description": "保持沉默，让对方先开口",
                        "action": "冷冷地看着对方，一言不发，等待对方表明来意",
                        "dc_hint": 13,
                        "attr_hint": "LUCK"
                    },
                    {
                        "letter": "D",
                        "description": "手按剑柄，警告对方不要挡路",
                        "action": "手按在武器上，用肢体语言表明自己不想惹事但也不怕事",
                        "dc_hint": 15,
                        "attr_hint": "FORCE"
                    }
                ]
            },
            {
                "context": f"你发现了一个可疑的线索，但时间紧迫...",
                "options": [
                    {
                        "letter": "A",
                        "description": "仔细搜查，不放过任何细节",
                        "action": "花时间仔细搜查现场，寻找所有可能的线索",
                        "dc_hint": 12,
                        "attr_hint": "MIND"
                    },
                    {
                        "letter": "B",
                        "description": "拿上明显的东西就走",
                        "action": "只拿取最明显、最有价值的物品，尽快离开",
                        "dc_hint": 14,
                        "attr_hint": "REFLEX"
                    },
                    {
                        "letter": "C",
                        "description": "设置陷阱，以防有人跟踪",
                        "action": "在离开前设置一个简单的警报陷阱，防止被跟踪",
                        "dc_hint": 16,
                        "attr_hint": "MIND"
                    },
                    {
                        "letter": "D",
                        "description": "破坏现场，不留下痕迹",
                        "action": "故意破坏现场，让别人无法追踪你的行踪",
                        "dc_hint": 15,
                        "attr_hint": "FORCE"
                    }
                ]
            }
        ]

        # 根据回合数选择模板
        template_index = (state.turn_count // 3) % len(templates)
        selected = templates[template_index]

        return {
            "context": selected["context"],
            "options": selected["options"],
            "free_text": {
                "description": "自定义行动（描述你想做的其他事情）"
            }
        }

    def _default_narrative(self, ctx: NarrativeContext) -> Dict:
        """默认叙事（回退）"""
        degree = ctx.check_result.degree

        templates = {
            "大成功": f"你完美地执行了计划，效果超出预期！",
            "成功": f"你顺利地完成了行动。",
            "勉强成功": f"你完成了行动，但过程有些惊险。",
            "勉强失败": f"你差一点就成功了，但最终还是失败了。",
            "失败": f"你的行动失败了。",
            "大失败": f"灾难！你的行动彻底失败，还引发了额外的问题。"
        }

        return {
            "narrative": templates.get(degree, "行动结束。"),
            "consequences": {
                "attribute_changes": {},
                "items_gained": [],
                "items_lost": [],
                "relationship_changes": {},
                "flags_set": {},
                "tags_gained": [],
                "scene_change": ""
            },
            "ending_triggered": False,
            "ending_type": ""
        }


# ============ 核心游戏引擎 ============

class WorldlineSkill:
    """
    Worldline Choice Skill
    LLM驱动 + d20检定的混合架构
    """

    def __init__(self, llm_callback: Optional[Callable] = None, auto_save: bool = True, show_dice: bool = False):
        """
        Args:
            llm_callback: LLM调用回调函数
            auto_save: 是否每回合自动存档（默认True）
            show_dice: 是否显示骰子结果（默认False，后台静默投骰）
        """
        self.state = GameState()
        self.d20 = D20Engine()
        self.llm = LLMDriver(llm_callback)
        self.save_dir = "./saves"
        self.auto_save = auto_save
        self.show_dice = show_dice
        os.makedirs(self.save_dir, exist_ok=True)

    def start_game(
        self,
        world_setting: str,
        player_role: str,
        player_name: str,
        world_desc: str = ""
    ) -> Dict:
        """开始新游戏"""
        self.state = GameState()
        self.state.world_setting = world_setting
        self.state.world_description = world_desc
        self.state.current_scene = world_desc or f"{world_setting}的世界"
        self.state.player["name"] = player_name
        self.state.player["role"] = player_role

        # 生成初始属性（简化版，可扩展）
        for attr in self.state.player["attributes"]:
            self.state.player["attributes"][attr] = random.randint(8, 16)

        return {
            "initialized": True,
            "world": world_setting,
            "player": self.state.player,
            "scene": self.state.current_scene
        }

    def process_turn(
        self,
        player_input: str,
        dc_modifier: int = 0,
        advantage: bool = False,
        disadvantage: bool = False
    ) -> Dict:
        """
        处理一个游戏回合

        流程：
        1. LLM分析意图 → 检定配置
        2. d20投骰 → 客观结果（支持外部DC修正和优势/劣势）
        3. LLM生成叙事（基于骰子结果）
        4. 应用状态变更
        """
        if not self.state.world_setting:
            return {"error": "游戏未初始化，请先调用start_game"}

        # Step 1: LLM分析意图
        analysis = self.llm.analyze_action(player_input, self.state)

        # 检查前置条件（物品、知识）
        missing_items = [
            item for item in analysis.required_items
            if item not in self.state.player["items"]
        ]

        if missing_items:
            return {
                "turn": self.state.turn_count + 1,
                "action": player_input,
                "error": f"缺少必要物品: {', '.join(missing_items)}",
                "can_retry": True
            }

        # Step 2: d20检定（客观判定）
        # 应用外部修正（来自战术准备、NPC协助、环境互动）
        effective_dc = max(1, analysis.base_dc + dc_modifier)

        attr_value = self.state.player["attributes"].get(
            analysis.primary_attribute, 10
        )
        check_result = self.d20.execute_check(
            attribute_value=attr_value,
            dc=effective_dc,
            advantage=advantage,
            disadvantage=disadvantage
        )

        # Step 3: LLM生成叙事（基于骰子结果）
        narrative_ctx = NarrativeContext(
            action=player_input,
            intention=analysis.intention,
            check_result=check_result,
            world_setting=self.state.world_setting,
            scene_description=self.state.current_scene,
            player_name=self.state.player["name"]
        )

        narrative_result = self.llm.generate_narrative(narrative_ctx)

        # Step 4: 应用状态变更
        self._apply_consequences(narrative_result.get("consequences", {}))

        # 记录历史
        turn_result = {
            "turn": self.state.turn_count + 1,
            "action": player_input,
            "intention": analysis.intention,
            "narrative": narrative_result.get("narrative", ""),
            "consequences": narrative_result.get("consequences", {}),
            "ending_triggered": narrative_result.get("ending_triggered", False)
        }

        # 根据配置决定是否显示骰子结果
        if self.show_dice:
            turn_result["check"] = check_result.to_dict()
        else:
            # 不显示详细骰子结果，只显示程度
            turn_result["degree"] = check_result.degree
            turn_result["success"] = check_result.success

        self.state.add_history(player_input, turn_result)

        # 检查结局
        if narrative_result.get("ending_triggered"):
            self.state.ending_triggered = True
            self.state.ending_type = narrative_result.get("ending_type", "")

        # 强制自动存档（每回合结束时必然执行）
        auto_save_info = {"save_id": None, "success": False, "timestamp": None, "error": None}
        if self.auto_save:
            try:
                save_id = f"auto_turn_{self.state.turn_count}_{int(datetime.now().timestamp())}"
                filepath = self.save_game(save_id)
                auto_save_info = {
                    "save_id": save_id,
                    "success": True,
                    "timestamp": datetime.now().isoformat(),
                    "filepath": filepath
                }
            except Exception as e:
                auto_save_info = {
                    "save_id": None,
                    "success": False,
                    "timestamp": datetime.now().isoformat(),
                    "error": str(e)
                }

        # 返回结果中包含强制保存状态
        turn_result["auto_save"] = auto_save_info
        return turn_result

    def generate_turn_options(
        self,
        previous_result: Optional[Dict] = None
    ) -> TurnOptions:
        """
        生成本回合的ABCD预定义选项 + E自由选项

        Args:
            previous_result: 上一回合的结果（用于上下文）

        Returns:
            TurnOptions对象，包含A/B/C/D选项和E自由选项
        """
        return self.llm.generate_options(self.state, previous_result)

    def process_option(
        self,
        options: TurnOptions,
        choice: str,
        free_text: Optional[str] = None
    ) -> Dict:
        """
        处理玩家选择的选项

        Args:
            options: 本回合的选项集合
            choice: 玩家选择（A/B/C/D/E）
            free_text: 如果选E，玩家的自由输入

        Returns:
            回合结果（同process_turn）
        """
        choice = choice.upper().strip()

        if choice == "E":
            # 自由选项
            if not free_text:
                return {"error": "选择E时需要提供自由输入"}
            action = free_text
        elif choice in ["A", "B", "C", "D"]:
            # 预定义选项
            option = options.get_option(choice)
            if not option:
                return {"error": f"无效的选项: {choice}"}
            action = option.action
        else:
            return {"error": f"无效的选择: {choice}，请选择A/B/C/D/E"}

        # 执行回合
        return self.process_turn(action)

    def _apply_consequences(self, consequences: Dict):
        """应用状态变更"""
        # 属性变化
        for attr, delta in consequences.get("attribute_changes", {}).items():
            self.state.update_attribute(attr, delta)

        # 物品变化
        for item in consequences.get("items_gained", []):
            self.state.add_item(item)
        for item in consequences.get("items_lost", []):
            self.state.remove_item(item)

        # 关系变化
        for npc, delta in consequences.get("relationship_changes", {}).items():
            current = self.state.npcs.get(npc, {}).get("relationship", 0)
            self.state.update_npc(npc, relationship=current + delta)

        # 标志
        for flag, value in consequences.get("flags_set", {}).items():
            self.state.flags[flag] = value

        # 场景变化
        scene_change = consequences.get("scene_change", "")
        if scene_change:
            self.state.current_scene = scene_change

    # ------------------------------------------------------------------
    # 战术增强系统（v4.3.0）
    # 整合方案1（前置准备）+ 方案3（NPC协作）+ 方案4（环境互动）
    # ------------------------------------------------------------------

    def set_scene_objects(self, objects: List[Dict]) -> Dict:
        """设置当前场景中的可互动物体。"""
        self.state.scene_objects = objects
        return {
            "set_count": len(objects),
            "objects": [{"id": o.get("id"), "name": o.get("name")} for o in objects]
        }

    def interact_with_scene_object(self, object_id: str, player_input: str) -> Dict:
        """
        玩家与场景物体互动。
        成功后会将该物体对应的 benefit 加入 active_benefits。
        """
        obj = None
        for o in self.state.scene_objects:
            if o.get("id") == object_id:
                obj = o
                break

        if not obj:
            return {"error": f"场景中不存在物体: {object_id}"}

        if obj.get("activated"):
            return {"error": f"该物体已被互动过: {obj.get('name')}"}

        # 调用标准回合流程处理互动本身
        result = self.process_turn(player_input)

        if "error" in result:
            return result

        # 若互动成功（成功或勉强成功），激活 benefit
        if result.get("check", {}).get("success") or result.get("success"):
            obj["activated"] = True
            benefit = obj.get("benefit")
            if benefit:
                benefit["source"] = f"scene_object:{object_id}"
                benefit["source_name"] = obj.get("name")
                self.state.active_benefits.append(benefit)
                result["activated_benefit"] = benefit

        result["interacted_object"] = {"id": object_id, "name": obj.get("name")}
        return result

    def execute_npc_check(self, npc_name: str, action: str, attribute: str, dc: int) -> Dict:
        """
        为 NPC 执行一次 d20 检定，模拟 NPC 执行协任务的结果。
        成功后自动为玩家添加一个 active_benefit。
        """
        # NPC 默认属性取 8-14（平凡人），关系好的 NPC 视为 +2
        base_attr = 10
        rel = self.state.npcs.get(npc_name, {}).get("relationship", 0)
        rel_bonus = max(-2, min(4, rel // 20))  # -2 ~ +4
        attr_value = max(1, min(20, base_attr + rel_bonus))

        check_result = self.d20.execute_check(
            attribute_value=attr_value,
            dc=dc
        )

        log_entry = {
            "turn": self.state.turn_count,
            "npc": npc_name,
            "action": action,
            "attribute": attribute,
            "dc": dc,
            "check": check_result.to_dict()
        }
        self.state.npc_assist_log.append(log_entry)

        result = {
            "npc": npc_name,
            "action": action,
            "check": check_result.to_dict(),
            "relationship_bonus": rel_bonus
        }

        # 大成功或成功：添加 benefit
        if check_result.success:
            benefit = {
                "type": "npc_assist",
                "name": f"{npc_name}的协助",
                "description": f"{npc_name}成功执行了'{action}'，你接下来的相关行动获得加成",
                "dc_modifier": -3 if check_result.degree in ["大成功", "成功"] else -1,
                "advantage": check_result.degree == "大成功",
                "applies_to": [attribute] if attribute else [],
                "remaining_uses": 1,
                "source": f"npc:{npc_name}"
            }
            self.state.active_benefits.append(benefit)
            result["granted_benefit"] = benefit
        else:
            # 失败可能带来负面暗示
            if check_result.degree in ["失败", "大失败"]:
                result["warning"] = f"{npc_name}任务失败，敌方可能已警觉"

        return result

    def add_active_benefit(self, name: str, description: str, dc_modifier: int = 0,
                           advantage: bool = False, applies_to: Optional[List[str]] = None,
                           remaining_uses: int = 1) -> Dict:
        """主持人手动添加一个战术加成（通常用于记录玩家的前置准备）。"""
        benefit = {
            "type": "manual",
            "name": name,
            "description": description,
            "dc_modifier": dc_modifier,
            "advantage": advantage,
            "applies_to": applies_to or [],
            "remaining_uses": remaining_uses,
            "source": "host"
        }
        self.state.active_benefits.append(benefit)
        return {"added": True, "benefit": benefit}

    def consume_active_benefit(self, name: str) -> Optional[Dict]:
        """按名称消耗一个 active_benefit。"""
        for i, b in enumerate(self.state.active_benefits):
            if b.get("name") == name:
                b["remaining_uses"] -= 1
                consumed = b.copy()
                if b["remaining_uses"] <= 0:
                    self.state.active_benefits.pop(i)
                return {"consumed": consumed, "remaining": b.get("remaining_uses", 0)}
        return None

    def get_active_benefits(self, filter_attribute: Optional[str] = None) -> List[Dict]:
        """获取当前所有可用的战术加成，可选按属性过滤。"""
        self._clear_expired_benefits()
        if not filter_attribute:
            return [b.copy() for b in self.state.active_benefits]
        return [
            b.copy() for b in self.state.active_benefits
            if not b.get("applies_to") or filter_attribute in b.get("applies_to", [])
        ]

    def calculate_effective_dc(self, base_dc: int, attribute: str) -> Dict:
        """
        计算经过所有可用加成后的最终 DC，以及是否获得 advantage。
        """
        self._clear_expired_benefits()
        applicable = self.get_active_benefits(filter_attribute=attribute)

        total_modifier = 0
        has_advantage = False
        applied_benefits = []

        for b in applicable:
            # 只计算未用完且有 DC 修正或 advantage 的 benefit
            if b.get("remaining_uses", 0) > 0:
                mod = b.get("dc_modifier", 0)
                if mod:
                    total_modifier += mod
                    applied_benefits.append({"name": b["name"], "modifier": mod})
                if b.get("advantage"):
                    has_advantage = True
                    applied_benefits.append({"name": b["name"], "effect": "advantage"})

        effective_dc = max(1, base_dc + total_modifier)
        return {
            "base_dc": base_dc,
            "attribute": attribute,
            "applied_benefits": applied_benefits,
            "total_modifier": total_modifier,
            "effective_dc": effective_dc,
            "advantage": has_advantage
        }

    def _clear_expired_benefits(self):
        """清理已超过使用次数或来源不存在的过期 benefit。"""
        # 简单的 TTL 机制：可以按回合数过期（未来扩展）
        self.state.active_benefits = [
            b for b in self.state.active_benefits
            if b.get("remaining_uses", 0) > 0
        ]

    def save_game(self, save_id: str) -> str:
        """保存游戏"""
        filepath = os.path.join(self.save_dir, f"{save_id}.json")
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(self.state.to_dict(), f, ensure_ascii=False, indent=2)
        return filepath

    def load_game(self, save_id: str) -> bool:
        """加载游戏"""
        filepath = os.path.join(self.save_dir, f"{save_id}.json")
        if not os.path.exists(filepath):
            return False
        with open(filepath, 'r', encoding='utf-8') as f:
            data = json.load(f)
            self.state = GameState.from_dict(data)
        return True

    def get_state(self) -> Dict:
        """获取当前状态"""
        return self.state.to_dict()

    def get_history_summary(self, limit: int = 5) -> str:
        """获取历史摘要（用于LLM上下文）"""
        recent = self.state.history[-limit:]
        lines = []
        for h in recent:
            lines.append(f"回合{h['turn']}: {h['action']}")
            if 'result' in h and 'check' in h['result']:
                check = h['result']['check']
                lines.append(f"  → {check['degree']} (骰子{check['roll']})")
        return "\n".join(lines)


# ============ CLI接口 ============

def cli_main():
    """CLI模式入口 - 带ABCDE选项"""
    print("="*60)
    print("Worldline Choice - LLM驱动 + d20检定")
    print("="*60)

    skill = WorldlineSkill()

    # 初始化
    print("\n开始新游戏...")
    world = input("世界观: ") or "武侠"
    role = input("角色: ") or "剑客"
    name = input("姓名: ") or "无名"

    result = skill.start_game(world, role, name)
    print(f"\n游戏开始: {result['world']}")
    print(f"属性: {result['player']['attributes']}")

    previous_result = None

    # 游戏循环
    while not skill.state.ending_triggered:
        print(f"\n{'='*50}")
        print(f"场景: {skill.state.current_scene}")
        print(f"回合 {skill.state.turn_count + 1}")
        print("-"*50)

        # 生成选项
        print("\n正在生成本回合选项...")
        options = skill.generate_turn_options(previous_result)

        # 显示情境
        print(f"\n【情境】{options.context}")

        # 显示选项
        print("\n【可选行动】")
        for opt in options.options:
            attr_str = f"[{opt.attr_hint}]" if opt.attr_hint else ""
            dc_str = f"(DC{opt.dc_hint})" if opt.dc_hint else ""
            print(f"  {opt.letter}. {opt.description} {attr_str} {dc_str}")
        print(f"  E. {options.free_text.description}")

        # 获取玩家选择
        print("-"*50)
        choice = input("你的选择 (A/B/C/D/E): ").strip().upper()

        if choice == "E":
            free_input = input("描述你的行动: ").strip()
            if not free_input:
                print("[错误] 选择E时需要描述行动")
                continue
        elif choice in ["A", "B", "C", "D"]:
            option = options.get_option(choice)
            if not option:
                print(f"[错误] 无效选项: {choice}")
                continue
            free_input = None
            print(f"你选择了: {option.description}")
        elif choice.lower() in ["save", "保存"]:
            save_id = input("存档ID: ") or "auto"
            path = skill.save_game(save_id)
            print(f"已保存到: {path}")
            continue
        elif choice.lower() in ["quit", "退出"]:
            break
        else:
            print(f"[错误] 无效输入: {choice}，请输入A/B/C/D/E")
            continue

        # 处理回合
        print("\n处理中...")
        result = skill.process_option(options, choice, free_input)

        if "error" in result:
            print(f"[错误] {result['error']}")
            continue

        # 显示结果
        if skill.show_dice:
            # 显示详细骰子结果
            check = result['check']
            print(f"\n【检定】{check['degree']}")
            print(f"骰子: {check['roll']} + 修正{check['modifier']:+d} = {check['total']} vs DC{check['dc']}")
        else:
            # 只显示结果程度，不显示骰子细节
            print(f"\n【结果】{result.get('degree', '未知')}")
        print(f"\n【剧情】")
        print(result['narrative'])

        # 保存结果供下回合使用
        previous_result = result

        if result.get('ending_triggered'):
            print(f"\n{'='*50}")
            print(f"游戏结束: {result.get('ending_type', '结局')}")
            break


if __name__ == "__main__":
    cli_main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)