From 865eca98c0cd670763e892d0771a477763dc2426 Mon Sep 17 00:00:00 2001 From: xiaji Date: Thu, 28 May 2026 23:29:43 +0800 Subject: [PATCH] Add Guandan card game design spec (v1.0) --- .../2026-05-28-guandan-card-game-design.md | 908 ++++++++++++++++++ 1 file changed, 908 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-28-guandan-card-game-design.md diff --git a/docs/superpowers/specs/2026-05-28-guandan-card-game-design.md b/docs/superpowers/specs/2026-05-28-guandan-card-game-design.md new file mode 100644 index 0000000..3cd1142 --- /dev/null +++ b/docs/superpowers/specs/2026-05-28-guandan-card-game-design.md @@ -0,0 +1,908 @@ +# 掼蛋卡牌游戏 — 设计规格 + +**日期:** 2026-05-28 +**引擎:** Godot 4.x +**语言:** GDScript + +--- + +## 一、分层架构 + +``` +UI 层 (Godot) → 业务逻辑层 (纯 GDScript) → 数据层 (纯 GDScript) + ↓ + 网络层 (Godot ENet,后期) +``` + +| 层 | 职责 | 技术选型 | 说明 | +|---|------|----------|------| +| **UI 层** | 场景渲染、手牌布局、拖拽出牌、按钮交互、动画、音效 | Godot Control 节点 / Tween | 唯一依赖 Godot API 的层,纯"表现" | +| **业务逻辑层** | 牌型识别、比较大小、出牌合法性、回合流转、得分计算、AI 决策 | 纯 GDScript(零 Godot 依赖) | 核心层,不引用任何 Godot 场景/节点类 | +| **数据层** | 牌、牌堆、手牌、回合状态、玩家数据、配置表 | 纯 GDScript 数据结构 | 不可变数据结构优先,便于状态回滚 | +| **网络层** | 联机消息格式、同步协议、断线重连 | Godot ENet | 后期开发,前置预留接口 | + +**交互规则:** +- UI 层通过信号/回调调用逻辑层(如 `logic.play_cards(player, cards)`) +- 逻辑层返回结果后,UI 层负责渲染 +- 逻辑层完全不感知 UI、网络或 AI 的存在(依赖倒置) + +**异常码定义:** + +- 逻辑层所有操作统一返回 `Result` 类型:`{ok: bool, error_code: int, data: Variant}` +- 错误码在 `constants.gd` 中定义枚举:`ERR_INVALID_CARDS`、`ERR_NOT_YOUR_TURN`、`ERR_CANNOT_PASS`、`ERR_CARD_NOT_FOUND`、`ERR_HAND_EMPTY` 等 +- UI 层根据 `error_code` 做对应提示文案,支持多语言 + +**线程安全约束:** + +- `core/` 和数据层禁止写全局变量,所有状态通过参数传入、返回值传出 +- 避免联机/异步场景下的数据竞争,逻辑层保持无状态函数风格 +- 若需缓存计算结果(如 AI 评分),使用局部闭包而非全局状态 + +**逻辑纯函数约束:** + +- 核心判定函数(牌型识别、比较大小、合法性校验)必须为纯函数:相同输入必须返回相同输出,无副作用 +- 禁止在核心函数中操作文件、网络、定时器等外部资源 +- 便于单元测试、并行 AI 计算、状态回滚验证 + +**逻辑层的异步执行方案:** + +- AI 计算(尤其 L2/L3 枚举所有合法出牌组合)是 CPU 密集型操作,同步调用会卡死 UI 线程 +- 利用 Godot 4.x `WorkerThreadPool` 将 AI 决策放入后台线程计算,通过信号或 `await` 将结果回传给 UI 层 +- `move_generator` 的枚举计算同样异步化,`game_controller` 负责协调异步调用链 + +**多线程 AI 的数据竞争防护:** + +- Godot 4 的 `WorkerThreadPool` 执行 GDScript 时,**严禁子线程直接读取或修改主线程的 `GameState`**(GDScript 的 Dictionary/Array 非线程安全,会导致崩溃或脏数据) +- AI 线程只能接收**序列化后的纯数据快照**,在子线程完成计算后,仅将生成的 `Action` 序列传回主线程 +- 快照格式不可使用 JSON 字符串(频繁编解码导致内存抖动和 CPU 耗时反向超标),应使用**原生包装数组(PackedInt32Array / PackedByteArray)**,利用位掩码(Bitmask)传递手牌和已出牌状态 +- 主线程的 `game_controller` 统一调用 `Action.apply()` 修改状态,确保同一时刻只有主线程操作数据 + +**状态回滚策略 — 命令模式 + Action 重放:** + +- GDScript 中深拷贝(`duplicate(true)`)大型 `GameState` 性能极差,不可行 +- 采用 **命令模式(Command Pattern)**:每次操作生成 `Action`,执行 `Action.apply(GameState)` 修改状态 +- 回滚时从初始状态重放 Action 日志到目标点,而非存储完整快照 +- 若需中间快照(如 AI 推演分支),仅对变化的局部数据(Hand、PlayedZone)做浅拷贝,不全局深拷贝 + +**全局事件总线(EventBus):** + +- 在 `autoload/` 中增加 `event_bus.gd`(全局信号中心),用于广播游戏事件 +- 事件类型:`PLAYER_PLAYED_CARDS`、`BOMB_DETONATED`、`TRIBUTE_TRIGGERED`、`ROUND_END`、`GAME_OVER` 等 +- UI 组件之间通过订阅 EventBus 信号解耦,避免网状硬编码依赖 +- `core/` 层不依赖 EventBus,仅 `game/` 胶水层和 `ui/` 层使用 + +**EventBus 内存泄漏防护:** + +- UI 节点(如动态生成的 `card_node.tscn`)若连接全局 EventBus 信号但在销毁时未断开,会导致严重内存泄漏和野指针崩溃 +- 在 UI 基类中重写 `_exit_tree()`,自动遍历并断开所有与 EventBus 的连接 +- 或利用 Godot 4 的 `Callable` 弱引用特性(`bind_weak()`)管理信号订阅,节点销毁时自动失效 + +**配置数据的 Resource 化:** + +- 静态配置(卡牌属性、音效映射、AI 权重参数、牌型分值表)优先定义为继承自 `Resource` 的自定义类 +- 示例:`class_name CardConfig extends Resource`、`class_name AIParams extends Resource` +- 优势:可直接利用 Godot 编辑器 Inspector 面板可视化调参;底层 C++ 序列化的性能和安全性远超手写 JSON 解析 +- `config_loader.gd` 保留用于运行时动态配置(如服务器下发的活动参数),静态配置一律用 `.tres` Resource 文件 + +**状态不可变性统一规范:** + +- GDScript 天生偏可变,需明确不可变性边界: + - **绝对不可变对象:** `Card`、`PlayedCards`、`Action`(创建后其字段不可修改) + - **允许局部修改:** `Round`(回合内状态可变)、`GameState`(通过 Action.apply 修改) + - **禁止共享引用:** 所有 getter 返回副本或只读包装(`duplicate(false)`),禁止返回内部 Array/Dictionary 的原始引用 + - `Array` 和 `Dictionary` 类型的成员变量外部访问时一律浅拷贝 + +**资源生命周期管理:** + +- 场景退出时必须执行清理,防止残留线程、孤儿节点和隐性内存增长: + - **线程取消:** `_exit_tree()` 中取消所有 `WorkerThreadPool` 未完成任务,`await` 超时后强制丢弃 + - **Replay 中止:** 场景切换时中断回放录制/播放,保存当前进度 + - **节点对象池上限:** `ObjectPool` 设置 `MAX_POOL_SIZE`,超出部分直接 `queue_free()` + - **Tween 清理:** `_exit_tree()` 中 `stop_all()` 并 `kill()` 所有活跃 Tween + - **信号断开:** 调用 `disconnect_all()` + 从 EventBus 取消订阅 + +**日志系统分级:** + +- 除 Action 游戏日志外,需另建调试日志系统(`log_manager.gd`,autoload): + - `DEBUG`:详细调试信息,Release 默认关闭 + - `INFO`:关键流程节点(发牌、进贡、出牌、升级) + - `WARN`:异常但可恢复(AI 超时降级、心跳超时、状态不一致) + - `ERROR`:致命错误(Action 失败、状态机非法转移) + - `NETWORK`:联机消息日志 + - `AI_TRACE`:AI 决策明细日志 + - `PERF`:性能基准日志 +- **安全约束:** `INFO` 及以上禁止打印完整手牌内容;联机模式 `DEBUG` 默认关闭;日志支持写入文件 + 按大小轮转 + +**多人异步事件顺序约束:** + +- 常见冲突场景:动画未播完时收到下一 Action、Pass 与清场同时发生、接风与结算同时触发 +- 解决方案: + - **Event Queue(事件队列):** `game_controller` 维护优先级事件队列,按时间戳串行消费 + - **UI Transition Lock:** 动画/过渡期间锁住输入,动画结束回调释放锁 + - **强制事件串行化:** 同一帧内只处理一个状态变更,防止 UI 状态错乱 + - 事件优先级:`FATAL_ERROR > GAME_OVER > PLAYER_ACTION > ANIMATION > IDLE` + +**配置热更新边界:** + +- 配置分为三层,每层有不同的更新约束: + +| 层级 | 说明 | 更新时机 | 示例 | +|------|------|----------|------| +| Immutable Match Config | 对局开始后不可变 | 仅在房间创建时 | 牌数、逢人配规则、炸弹规则 | +| Runtime Tweak Config | 可运行时调整 | 对局中或局间 | AI 难度、音效音量、UI 动画速度 | +| Client Cosmetic Config | 纯本地配置 | 随时 | 卡牌皮肤、背景主题、语言 | + +- `RuleConfig`(不可变)在联机房间中由房主下发,参与状态哈希;对局中修改需重开房间 + +--- + +## 二、核心数据模型 + +### 卡牌 + +掼蛋使用两副牌(108张),含大小王。 + +``` +Card: 整数 ID 0-107(两副牌),位运算高效比较大小 + card_id: 0-107 全局唯一 ID,用于日志追踪、网络同步和 UI 渲染 + card_value: 花色 + 点数编码,用于规则比对(忽略唯一 ID) + Suit: 4 花色 (♠ ♥ ♣ ♦) + Joker + Rank: 2-14 (2=2, ..., 10=10, J=11, Q=12, K=13, A=14) + 小王=15 + 大王=16 + original_id: 0-53,标记该牌来自第几副牌的第几张,区分两副牌中相同点数花色的牌 +Deck: 108 张 Card 列表 → 洗牌、发牌 +Hand: 玩家手牌,按 Rank→Suit 排序 +``` + +**card_id 与 card_value 区分:** +- `card_id`(0-107):全局唯一,日志/同步/渲染时使用 +- `card_value`(Suit + Rank 编码):规则比对时使用,两副牌中相同花色点数的牌共享同一 `card_value` +- 牌型识别、比较大小等核心函数统一按 `card_value` 比较,忽略 `card_id` + +### 出牌 + +``` +PlayedCards: 带类型的联合 + {type: TYPE_SINGLE | TYPE_PAIR | TYPE_TRIPLE | ... | TYPE_ROCKET, + cards: [...], + primary_rank: 主牌点数, + is_pure_bomb: bool} # 炸弹是否为纯炸(无逢人配参与) +``` + +牌型识别是纯函数:输入 `[Card]` → 输出 `PlayedCards | null` + +**多合一牌型歧义与推荐优先级:** + +当手牌同时拥有多张逢人配时,同一组牌可能对应多种牌型。例如手牌 `4、5、6` + 两张逢人配: +- 可构成顺子:`4、5、6、逢(7)、逢(8)` +- 可构成连对:`4、4、5、5、6`(逢人配转写为 4 和 5 的对子) +- 可构成三带二:`4、4、4、5、6`(逢人配转写为三同张) + +`hand_evaluator.gd` 需定义**牌型推荐优先级枚举**,当同一组牌存在多种合法解释时,按优先级返回默认牌型: + +| 优先级 | 牌型 | 说明 | +|--------|------|------| +| 1 | 火箭(四王) | 最高 | +| 2 | 炸弹(含纯炸 > 混炸) | 优先组成更大炸弹 | +| 3 | 同花顺 | | +| 4 | 钢板(三连对) | | +| 5 | 连对 | | +| 6 | 顺子 | | +| 7 | 三带二 | | +| 8 | 三不带 | | +| 9 | 对子 | | +| 10 | 单张 | 最低 | + +UI 层在检测到歧义牌型时,允许玩家通过按钮切换可选分支("以顺子出" / "以连对出")。 + +**同花顺配牌花色转写:** 逢人配(红桃级牌)参与拼凑非红桃同花顺时,其自身花色强制转写为目标花色,结算时仅作为目标花色的点数存在,丧失原本的红桃属性。 + +### 游戏状态 + +``` +Round: 当前回合状态(谁出牌、出的什么、本轮出牌历史) +GameState: 完整一局状态(4个玩家、牌堆、得分、升级、贡牌信息) + round_seq: 局次编号,支持多局连胜/积分统计 + current_rank: 当前打的级牌等级(2-A),决定逢人配(红桃级牌为万能牌) +``` + +**2v2 队伍模型(Team):** + +- 掼蛋为 2v2 对战:1、3 号位为一队(Team A),2、4 号位为一队(Team B) +- 算分、升级、进贡、还贡、AI 配合逻辑全部强依赖队伍关系 +- `GameState` 引入 `Team` 结构:`{team_id, players: [Player], score, current_level}` +- 每局胜利条件:一队中两人都先出完即为"双下"(大胜),一人先出完为"头游"(小胜) + +**局间流转状态机:** + +掼蛋完整循环不仅是出牌,还包含复杂的局间阶段。`GameState` 的状态机: + +``` +INIT → DEAL → TRIBUTE_PHASE → PLAY_PHASE → LEVEL_UP_PHASE → DEAL(下一局)/ GAME_OVER +``` + +| 阶段 | 说明 | 关键校验 | +|------|------|----------| +| `INIT` | 初始化,确定级牌、洗牌 | — | +| `DEAL` | 发牌 27 张/人 | 两副牌 108 张全部分发 | +| `TRIBUTE_PHASE` | 进贡 / 还贡 / 抗贡 | 双下进贡分配逻辑、抗贡条件(对家也是进贡方则互抵) | +| `PLAY_PHASE` | 出牌循环,直到三人出完 | 牌型合法性、回合轮转、过牌规则、三人连续 Pass 触发清场(CLEARED) | +| `LEVEL_UP_PHASE` | 根据名次判定升级 | 头游队升 1 级,双下游队向胜队进贡;双下则胜队升 3 级 | + +**级牌与逢人配动态判定:** + +- `GameState.current_rank` 维护当前打的级牌等级 +- 红桃花色 + 当前级牌点数 = 逢人配(万能牌),在牌型评估时动态赋予百搭属性 +- `rule_engine` / `hand_evaluator` 在识别牌型时,必须结合 `current_rank` 判定是否为逢人配 + +**CLEARED 状态(清场规则):** + +- 掼蛋规则中,若连续三人弃牌(Pass),最后出牌者获得自由出牌权,即该轮"清场" +- `Round` 状态机需增加 `CLEARED` 子状态标识 +- UI 层配合:播放"桌面旧牌收起"动画 + 音效,明确提示玩家当前可出任意合法牌型 +- 清场后当前玩家开始新一轮出牌,本轮历史出牌记录归档至出牌历史侧边栏 + +**接风规则(Catch-Up):** + +- 掼蛋核心规则:当某玩家打出最后一手牌,且全场 Pass(该玩家成为头游),下一轮的自由出牌权(接风权)转移给其 **对家(队友)**,而非下家 +- `round.gd` 在检测到玩家手牌清空(`HAND_EMPTY`)且其余人 Pass 时,强制执行接风拦截:下轮 `active_player` 直接指定为当前出完牌玩家的 `teammate` +- 接风后队友获得自由出牌权(等同于 CLEARED 状态),可出任意牌型 +- 若接风者同时也是最后一名有空牌的玩家,则本局结束 + +**逢人配"纯度"降级规则:** + +- 含逢人配的炸弹(如 3 张真牌 + 1 张逢人配)在比较时,必须 **小于** 同等点数的纯炸(4 张真牌) +- 逢人配 **不能** 参与组成天王炸(四王 / 火箭) +- `PlayedCards.is_pure_bomb` 标记记录炸弹纯度;`rule_engine` 在比较炸弹时,纯度作为高于点数的第一优先级: + 1. 先比类型(火箭 > 普通炸弹) + 2. 再比纯度(纯炸 > 混炸) + 3. 再比点数(Rank 大的 > Rank 小的) + 4. 最后比张数(炸弹张数多的 > 少的) + +**进贡/还贡具体边界校验:** + +- 进贡阶段合法性校验: + - 进贡必须出手牌中 **最大的牌**(受当前级牌/逢人配排除规则约束,依具体规则变体而定) + - 不能进贡逢人配(红桃级牌),不能进贡当前级牌 + - 双下进贡:末游向头游进贡、次末游向头游队友进贡 +- 还贡阶段合法性校验: + - 还贡必须是 **单张** + - 还贡不能是逢人配 + - 还贡点数不能大于 10(依规则变体可配置) +- 抗贡条件:双方均为进贡方(互为进贡对象)时互抵,既不进也不还 + +**大王抗贡的唯一性校验:** + +- 掼蛋规定"双大王抗贡",两副牌共 2 张大王 +- 抗贡必须由 **同一个人** 同时抓到 2 张大王(`Rank.16`),才触发全场抗贡 +- 若队友各执一张大王,不触发抗贡 +- `tribute_phase` 校验逻辑:遍历单人手牌,检查单人 `hand` 中 `Rank.16` 数量 ≥ 2 + +**进贡最大牌的"同点数次级判定":** + +- 当玩家需要进贡最大牌,但手牌中最大点数有两张(如两张不同花色的 K),且都不属于级牌 +- 次级判定规则: + 1. 优先进贡非红桃的牌(避免将逢人配进贡出去) + 2. 若点数花色均相同(两副牌中同一张牌),系统随机选择或提供 UI 让玩家勾选 + 3. 逢人配(红桃级牌)绝对不可进贡,已在主规则中排除 + +### Action 日志 + +``` +Action: {player, action_type, cards, timestamp} +action_type: PLAY | PASS | TRIBUTE_GIVE | TRIBUTE_RETURN | RESHUFFLE | GAME_END +``` + +状态变更生成 `Action` 记录,构成不可变日志。这是联机同步和观战回放的基础。 + +**贡牌/还贡专用 Action:** 使用独立类型 `TRIBUTE_GIVE` 和 `TRIBUTE_RETURN`,避免与普通出牌混同导致逻辑混乱和校验冲突。 + +**逢人配来源标记:** 每张用于配牌的逢人配 Card 附带 `wild_source: GRADE_CARD | SYSTEM_ALLOC` 标记,支持规则变体(是否允许级牌做配牌、系统自动配牌上限等)。 + +### RuleConfig(规则变体配置化) + +掼蛋在不同地区存在规则差异,所有核心规则必须数据驱动,不得硬编码在逻辑中。 + +``` +RuleConfig: { + same_suit_straight_beats_bomb: bool, # 同花顺是否大于普通炸弹 + double_down_levels: int, # 双下升几级(默认 3) + can_tribute_wild: bool, # 是否允许红桃级牌进贡 + tribute_return_max_rank: int, # 还贡点数上限(默认 10,0=不限) + straight_extends_to_ace: bool, # 顺子是否可延伸到 A + bomb_compare_priority: enum, # 炸弹比较优先级:BOMB_BY_RANK 点数优先 / BOMB_BY_COUNT 张数优先 + wild_count: int, # 逢人配数量(默认 2) + team_formation: enum, # 队伍构成:FIXED_1_3 固定 1/3 号位 / RANDOM 随机 +} +``` + +**使用约束:** +- `RuleConfig` 是 Immutable Match Config,对局开始后不可修改 +- 联机时由房主下发统一 `RuleConfig`,参与 `GameState` 状态哈希校验 +- 核心规则函数(`rule_engine`、`hand_evaluator`、`game_state`)的参数中必须包含 `RuleConfig` 引用 +- 单机模式下可通过菜单选择预设规则("标准" / "淮安" / "安徽" 等) + +--- + +## 三、AI 架构 + +| 层级 | 名称 | 适用场景 | 逻辑 | +|------|------|----------|------| +| **L1** | 基础 AI | 训练模式"简单"难度 | 能出就出最少的牌,规则合规即可 | +| **L2** | 规则 AI | 训练模式"中等"、单机普通 | 启发式评分:保留炸弹、拆牌优先短顺、配牌合理使用 | +| **L3** | 策略 AI | 训练模式"困难"、单机困难 | 记忆出牌、推理队友意图、控制炸弹节奏 | + +**技术细节:** + +- AI 与玩家调用同一个 `logic.play_cards()` 接口 +- L2/L3 使用启发式评分:每个合法出牌组合 → 计算策略评分 → 选最优 +- L3 加入对战历史分析,通过复盘 `Action` 日志做状态推演 +- 训练模式的"牌型提示"展示 L2 AI 的最优解 + +**AI 安全与性能约束:** + +- **防死循环机制:** AI 决策若超过 3 秒未返回,自动降级到 L1 基础 AI,确保对局不卡死 +- **可禁用提示开关:** 训练模式的牌型提示可通过配置关闭,避免干扰正式对局或高阶玩家 +- **统一决策耗时上限:** 所有 AI 层级共用 `MAX_AI_DECISION_MS` 配置项,防止卡顿主线程;复杂计算使用分帧/线程池处理 + +**队伍配合意识(Teammate Awareness):** + +- L2/L3 AI 的启发式评分函数必须引入队友和对手状态权重: + - **队友剩余牌数**:队友剩 1-2 张时优先出小单张/对子"送牌"助攻 + - **队友头游判定**:队友已出完时切换到保级策略 + - **对手压制权重**:对手剩 1-2 张时提升炸弹/大牌的出牌优先级,避免对手跑掉 +- 评分公式示例:`score = hand_quality * 0.4 + teammate_context * 0.3 + opponent_threat * 0.3` + +**宏观局势与目标管理:** + +- AI 需根据当前积分差和升级规则,动态切换策略模式: + - **保守策略**:本局只需保一人头游即可升级时,优先保护已有优势,减少激进出牌 + - **激进策略**:落后方必须抓双下才能反超时,主动拆炸弹、保留大牌组合冲击双下 +- 策略模式切换由 `GameState` 的积分差和 `current_rank` 自动计算 + +**记牌与算牌维度(L3 专用):** + +- L3 维护"场外信息池"(Hidden Information Pool),追踪三类信息: + 1. **记大牌**:王、级牌、A 的已出数量,推算剩余威胁 + 2. **记断门**:某花色已出完 13 张时,推断对手不可能持有该花色的同花顺/炸弹 + 3. **记剩余牌数**:结合已出牌总数,推算对手手中最多几张炸弹,判断压制可能性 +- 信息池在每轮出牌后增量更新,不依赖回放全量重算 + +**L3 手牌推断(Hand Inference):** + +- 仅有"记牌池"不足以支撑 L3 高级决策,还需根据对手行为概率化推测其手牌分布 +- 在 `utils/probability.gd` 中增加贝叶斯推断模块: + - 对手进贡了一张红桃A → 推断对手手中缺大牌 → 后续可大胆用中等牌型压制 + - 对手多轮不出某花色 → 推定该花色可能为断门或预留炸弹 + - 还贡牌型(小单张)→ 推断对手在清杂牌准备冲刺 +- 推断结果以概率分布形式存入 Hidden Information Pool,供评分函数加权使用 + +**伪随机数生成器(PRNG)种子注入:** + +- `deck.gd` 的洗牌算法(Fisher-Yates)必须支持外部注入随机种子(Seed) +- AI 决策中涉及随机选择的部分(如多个同等评分出牌)同样使用可复现的 PRNG +- 目的:跑 GUT 自动化测试时,可复现完全相同的发牌序列和 AI 决策路径,实现确定性测试 +- 正式对局使用系统真随机(`randf()`),测试用固定种子 + +**AI 可观测性与调试工具:** + +- AI 决策对玩家和开发者不透明,需提供可观测性支持: + - **AI Decision Trace:** 决策树日志,记录每一轮 AI 考虑的所有合法出牌组合及其评分明细 + - **评分明细展示:** 调试模式下显示"为什么出这手牌"面板:`手牌质量: 0.7 | 队友权重: 0.8 | 对手威胁: 0.5 = 总分 2.0` + - **AI HeatMap:** 手牌上颜色覆盖显示关键牌/危险牌权重(红色=炸弹候选,蓝色=顺子候选,黄色=配牌候选) + - **Replay + AI Debug Overlay:** 回放模式下叠加 AI 决策过程,可逐帧回溯 AI 思路 +- 调试模式通过配置开关控制(`enable_ai_debug`),正式对局关闭 + +**AI 与规则引擎的隔离边界:** + +- AI 推演必须在沙盒环境中完成,防止隐性修改真实状态: + - AI **禁止直接修改**任何 `GameState` 或其子对象的字段 + - AI **不允许缓存**任何可变引用(如 `GameState.players` 数组的引用) + - AI 推演时必须基于 `GameState` 的只读快照(`PackedInt32Array`),在沙盒中操作临时状态 + - AI 唯一输出为 `Action` 对象或 `null`(弃牌),由 `game_controller` 统一 apply + +--- + +## 四、项目文件结构 + +``` +game-cards/ +├── project.godot +├── addons/ # 第三方插件(按需) +├── assets/ +│ ├── cards/ # 52 张卡面图 + 2 王牌 + 牌背 +│ ├── ui/ # UI 贴图、按钮、桌布背景 +│ ├── audio/ # 音效、BGM +│ └── fonts/ # 字体 +│ +├── localization/ # 多语言文本 +│ ├── zh_cn.csv # 简体中文 +│ └── en.csv # 英文 +│ +├── save/ # 存档目录(运行时生成) +│ ├── settings.cfg # 用户设置 +│ └── records/ # 对局记录 +│ +├── tools/ # 工具脚本 +│ ├── card_generator.gd # 批量生成卡牌资源 +│ └── config_converter.gd # 配置文件格式转换 +│ +├── src/ +│ ├── core/ # ★ 业务逻辑层(零 Godot 依赖) +│ │ ├── card.gd # Card 类型、牌组定义、排序比较 +│ │ ├── deck.gd # 洗牌、发牌 +│ │ ├── hand.gd # 手牌管理、排序 +│ │ ├── hand_evaluator.gd # 牌型识别:判断给定牌组属于何种牌型 +│ │ ├── move_generator.gd # 合法出牌生成:枚举手牌所有合法出牌组合(含剪枝优化) +│ │ ├── rule_engine.gd # 出牌合法性校验 + 牌型比较大小 +│ │ ├── round.gd # 回合流转状态机 +│ │ ├── game_state.gd # 全局游戏状态(计分、升级、贡牌、队伍) +│ │ ├── actions.gd # Action 日志定义 + apply/rollback +│ │ ├── config_loader.gd # 配置表解析(JSON/CSV,AI 参数、关卡等) +│ │ └── constants.gd # 枚举、配置常量 +│ │ +│ ├── utils/ # 通用工具库 +│ │ ├── combinatorics.gd # 组合数学(C(n,m)、排列枚举、剪枝辅助) +│ │ ├── shuffle.gd # 洗牌算法(Fisher-Yates) +│ │ └── probability.gd # 概率计算辅助 +│ │ +│ ├── ai/ # AI 模块 +│ │ ├── base_ai.gd # AI 基类接口 +│ │ ├── l1_basic_ai.gd # 简单 AI +│ │ ├── l2_rule_ai.gd # 规则 AI(带启发式评分) +│ │ └── l3_strategy_ai.gd # 策略 AI +│ │ +│ ├── game/ # 游戏控制器(胶水层) +│ │ ├── game_controller.gd # 单机对战的流程控制 +│ │ ├── training_controller.gd # 训练模式控制 +│ │ └── replay_recorder.gd # 回放录制 +│ │ +│ ├── network/ # 联机层(后期开发) +│ │ ├── network_interface.gd # 抽象接口 +│ │ ├── client.gd # 客户端同步 +│ │ └── server.gd # 服务器端 +│ │ +│ ├── ui/ # UI 场景和脚本 +│ │ ├── scenes/ +│ │ │ ├── main_menu.tscn +│ │ │ ├── game_table.tscn # 主牌桌 +│ │ │ ├── training_room.tscn # 训练室 +│ │ │ ├── lobby.tscn # 大厅(联机) +│ │ │ └── spectator.tscn # 观战 +│ │ └── components/ # 可复用 UI 组件 +│ │ ├── card_node.tscn # 单张卡牌控件 +│ │ ├── hand_area.tscn # 手牌区域 +│ │ ├── played_zone.tscn # 出牌展示区 +│ │ ├── card_type_hint.tscn # 牌型提示浮窗(训练模式) +│ │ ├── play_history_sidebar.tscn # 出牌历史侧边栏(可折叠) +│ │ └── scoreboard.tscn # 记分板 +│ │ +│ └── autoload/ # Godot Autoload 单例 +│ ├── config.gd # 全局配置 +│ ├── event_bus.gd # 全局事件总线(信号中心),解耦 UI 组件通信 +│ ├── audio_manager.gd # 音频管理 +│ └── scene_manager.gd # 场景切换 +│ +└── tests/ # 测试(逻辑层) + ├── test_cards.gd + ├── test_deck.gd + ├── test_hand_evaluator.gd + ├── test_move_generator.gd + ├── test_rule_engine.gd + ├── test_game_state.gd + ├── test_tribute.gd # 进贡/还贡/抗贡边界 + ├── test_state_recovery.gd # 断线重连状态恢复 + └── test_ai.gd +``` + +**关键原则:** +- `core/` 目录下任何文件不引用 `res://src/ui/`、`res://src/game/` 或 Godot 节点类 +- `game/` 是桥接层,串联 core + UI(或 core + network) +- test 只测 core 层,不依赖场景 + +**rule_engine 拆分理由:** +- 带有逢人配(百搭)时,合法出牌组合数量呈指数级爆炸,`rule_engine.gd` 职责过重 +- 拆分为三个模块: + - `hand_evaluator.gd`:判断给定牌组属于何种牌型(纯判定,轻量) + - `rule_engine.gd`:出牌合法性校验 + 比较大小(规则比对) + - `move_generator.gd`:生成当前手牌所有合法出牌组合(含剪枝优化,供 AI 专用) + +**move_generator 剪枝策略(防组合爆炸):** + +逢人配 + 双副牌导致合法出牌组合数呈指数级失控,必须硬性剪枝: + +- **等价状态去重:** 相同牌型、相同点数的出牌只保留一种(如两张不同的黑桃A组成的对子视为等价) +- **对称配牌剪枝:** 多张逢人配可互换位置时,只保留一种枚举(如两张逢人配当 `7` 和 `8` 与当 `8` 和 `7` 等价) +- **Beam Search 上限:** L2/L3 评分阶段保留前 `BEAM_WIDTH`(默认 50)个最优候选,丢弃低分分支 +- **最大枚举节点数:** 单次 `move_generator` 枚举上限 `MAX_ENUM_NODES`(默认 10000),超限后自动降级为启发式搜索 +- **降级策略:** 枚举超限时切换为贪心出牌(按牌型优先级选择最大牌组合),保证回合不超时 + +### 新增文件 + +``` +├── src/core/ +│ ├── rule_config.gd # RuleConfig 资源类(定义规则变体) +│ └── ... +├── src/ +│ ├── statistics/ # 数值统计模块 +│ │ └── stats_tracker.gd # 胜率、双下率、炸弹率等统计 +│ │ +│ ├── debug/ # 调试与可观测性 +│ │ ├── ai_trace.gd # AI 决策日志 +│ │ └── debug_overlay.gd # 调试叠加层 +│ │ +│ ├── logging/ # 日志系统 +│ │ └── log_manager.gd # 分级日志管理 +``` + +--- + +## 五、测试策略 + +| 测试类型 | 范围 | 工具 | 内容 | +|----------|------|------|------| +| **单元测试** | `core/` 所有模块 | GUT (Godot Unit Test) | 牌型识别、比较大小、得分计算、洗牌发牌 | +| **集成测试** | `core/` + `ai/` | GUT | AI 对不同局面的决策、完整一局流程 | +| **回归测试** | `core/` 规则引擎 | GUT | 固定测试套件,每次修改规则引擎必须全过 | +| **恶意输入测试** | `core/` 所有入口 | GUT | 空牌、重复牌、越界牌、非法牌型组合 | +| **性能基准** | `core/` + `ai/` | GUT + 计时 | 牌型识别 <1ms、AI决策 <3s、状态序列化 <50ms | +| **UI 测试** | `ui/` 场景 | 手动 + 截图对比 | 手牌渲染、动画、交互 | + +**重点测试用例:** +- 掼蛋所有牌型识别覆盖(单张、对子、三不带、三带二、顺子、钢板、连对、同花顺、炸弹、火箭) +- 配牌(逢人配)边界情况 — 最高/最低配、多配同时可选 +- 炸弹升级场景(四炸 → 五炸 → … → 火箭) +- 逢人配被炸弹压制的优先级 +- CI 每次提交跑 `gut` 命令行,全绿才算合格 + +**回归测试集要求:** 维护一份固定的 `tests/regression/` 目录,包含 50+ 覆盖各种边界的牌型场景,每次修改规则引擎必须全部通过。 + +**恶意输入测试覆盖:** +- 传入空数组、重复卡片 ID、越界 ID(<0 或 >107) +- 不符合任何牌型的非法卡牌组合 +- 同一张牌在牌组中出现超过 4 次(两副牌最多每种 4 张) + +**性能基准阈值:** +- 单次牌型识别 < 1ms +- `move_generator` 最坏情况(手牌 15 张含 2 张逢人配):枚举所有合法组合 < 16ms(保证异步处理不超时) +- AI 决策(L2)< 2s,(L3)< 5s,超时自动降级 +- 完整状态序列化/反序列化 < 50ms + +**进贡/还贡/抗贡边界测试用例:** +- 双下进贡分配逻辑:谁向谁进贡、进几张、大小牌选择 +- 抗贡条件判定:双方均为进贡方时互抵(不进不还)的精确条件 +- 还贡牌型合法性:还贡必须是单张、不能还逢人配、不能还级牌 +- 进贡阶段 Action 日志完整性 + +**断线重连状态恢复测试:** +- 基于 Action 日志重放恢复 `GameState` 的集成测试 +- 确保重连后双方状态绝对一致:已出的牌、剩余牌数、当前分数、谁在回合中 +- 测试中断场景:进贡中掉线、出牌中掉线、结算中掉线 + +**测试数据生成器:** + +- **随机牌局生成器(Fuzz):** `tools/fuzz_dealer.gd`,自动生成 10000+ 局随机发牌,喂入 AI 对打,自动发现崩溃/卡死/非法状态 +- **特殊边界牌局生成器:** `tools/edge_case_generator.gd`,预设边界场景(全炸弹手、全逢人配、空手回溯等) +- **Monte Carlo 对局生成:** 随机发牌 × 多 AI 难度 × 多规则配置的批量对局,输出胜率统计和性能分布 +- **Fuzz 测试集成至 CI:** 每次提交跑 500 局随机对局,发现异常自动标记 + +--- + +## 六、开发顺序 + +1. **训练模式** — 核心逻辑 + L1/L2 AI + 牌型提示 UI +2. **单机对战** — 完整四人局 + L3 AI + 完整 UI +3. **联机** — Action 同步协议 + 匹配 + 房间 +4. **观战** — Action 流回放 + 观战 UI + +**各阶段接口冻结点:** + +| 阶段 | 冻结接口 | 说明 | +|------|----------|------| +| 训练模式完成 | Card、Deck、Hand、PlayedCards、rule_engine | 核心数据结构和牌型判定不再变更 | +| 单机对战完成 | GameState、Round、Action、AI 基类接口 | 状态管理和 AI 接口定型 | +| 联机开始 | NetworkInterface 抽象层 | 联机协议可独立迭代不影响上层 | + +每个阶段冻结后,后续重构只影响当前阶段模块,不回溯修改已冻结接口。 + +**训练模式优先完成:** +- 牌型校验全覆盖(所有牌型 + 边界 + 逢人配),全部回归测试通过 +- 再做 AI 集成与 UI 开发,避免"功能在前,正确性在后"导致返工 + +**单机模式全流程验证:** +- 必须先跑通"发牌 → 进贡 → 还贡 → 出牌循环 → 升级判定 → 结算"完整链路 +- 验证多局连胜、级牌升级、炸弹升级等长期状态正确性 + +--- + +## 七、约束与边界 + +**单局最大时长限制:** 配置项 `MAX_GAME_DURATION_SEC`,超时判定为强制结束/平局,防止一方故意拖延。 + +**防重复出牌校验:** 同一套牌(相同 `original_id` 组合)不能在同一轮连续打出,尤其针对"过牌后再出同一套"的边界。 + +**出牌区最大显示数量:** 中央出牌区每次最多显示 4 组(各玩家最新一轮出牌),旧牌自动收起,避免 UI 溢出。 + +**级牌、主牌、逢人配优先级判定流程:** +1. 判定当前级牌等级(从 `GameState.level` 读取) +2. 主牌 = 级牌 + 两张逢人配(系统配) +3. 逢人配可用于补齐任意牌型(顺子、钢板、连对等),但必须在炸弹压制规则下低于同点数炸弹 +4. 逢人配不参与火箭(大小王)判定 + +**终端状态分支:** +- `GAME_END_NORMAL`:一方升至A并获胜 +- `GAME_END_SURRENDER`:一方投降 +- `GAME_END_TIMEOUT`:超时强制结束 +- `GAME_END_DRAW`:平局(双方同分且均未升级) +- `GAME_END_DISCONNECT`:联机中一方掉线超时 + +**断线托管机制:** 联机模式下,离线玩家自动交由 AI(默认 L2)托管,并在 UI 显示托管标识和倒计时;重连后恢复玩家控制。 + +**异常恢复策略:** + +- 不可恢复的异常(Fatal Error)需分级处理: + - **Action apply 失败:** 日志记录 + 拒绝该 Action + 请求服务端全量状态同步 + - **非法状态进入:** 自动回退到最近合法快照(每 N 轮自动保存快照),从快照重放 + - **Action 日志损坏:** 标记损坏段 + 请求全量同步 + 客户端重建 UI + - **回放中断:** 保存进度 + 允许从中断点续播 + - **网络包缺失:** 基于 `action_seq` 检测空洞 + 发送补发请求 + 超时后请求全量同步 +- **崩溃日志导出:** 发生 Fatal Error 时自动导出崩溃栈 + 最近 100 条 Action 日志到 `save/crash/` + +**多人托管切换边界:** + +- 托管玩家重连时的冲突场景需要明确定义: + - **所有权转移时机:** 托管状态下,AI 已产生但未 apply 的 Action 在玩家重连瞬间丢弃;正在执行的 AI 计算可等结果返回,但标记为"AI 建议"而非强制执行 + - **AI 行为不可撤销:** 已 apply 的 AI Action 不可回退,玩家重连后从当前状态继续 + - **重连保护期:** `RECONNECT_GRACE_PERIOD`(默认 2s),重连后 2 秒内禁用托管自动出牌,给玩家恢复操作时间 + - 托管模式下 UI 显示闪烁边框 + "AI Auto-Play" 标签 + +--- + +## 八、UI 交互规范 + +**手牌布局规则:** +- 自动对齐:手牌按 Rank→Suit 排序后均匀分布,中心对齐 +- 重叠防遮挡:手牌扇形/横向排列,重叠率 50%-65%,被选中牌抬高 20px +- 长牌适配:手牌 >20 张时自动缩小间距和卡牌尺寸,确保全部可见 + +**27 张手牌的"智能理牌/折叠":** + +- 起手 27 张牌(进贡后可达 28 张),在移动端或小屏幕上极其拥挤,拖拽框选极易误触 +- **按牌型分类视图:** 手牌区分为单张区、对子区、三同张区、炸弹区、顺子区,玩家可切换视图 +- **自动折叠:** 连续的顺子、钢板、三同张自动折叠显示为一组,点击展开 +- **手动标记:** 玩家可手动将多张牌标记为一组,组内牌共享高亮颜色 +- 折叠/展开状态持久化,保存到用户设置 + +**出牌历史侧边栏:** + +- 中央出牌区收起旧牌后,玩家算牌极为困难 +- 在 UI 边缘增加可折叠的"出牌历史侧边栏",以紧凑文本或微缩图形式记录最近 3-5 轮各家的出牌明细 +- 显示格式:`[轮次] 玩家1: 过 | 玩家2: 对2 | 玩家3: 对K(压)| 玩家4: 过` +- 辅助玩家手动算牌、也为 L3 AI 的记牌模块提供快速回溯入口 + +**滑动框选防误触(V-Filtering 重叠度阈值):** + +- 移动端 27 张牌极度紧凑,框选时极易连带选中目标牌左右两侧的边缘牌 +- 引入 **滑动轨迹重叠度阈值**:手指滑动的 X/Y 轴轨迹覆盖某张卡牌 **中心区域超过 40%** 时才判定选中 +- 仅擦过卡牌边缘(覆盖率 <10%)的不触发悬起/选中 +- 阈值可配置(`SELECTION_OVERLAP_THRESHOLD`),桌面端可适当降低 + +**逢人配智能吸附:** + +- 玩家手持逢人配且正在构建不完整牌型时(如已选 `3、4、5、6`),点击"提示"或长按空白区 +- UI 自动将手牌中的逢人配牌弹起并闪烁,提示玩家此牌可补齐当前残缺牌型(如当 `7` 完成顺子) +- 发生多歧义时(既可补顺子也可补连对),弹出小型选项菜单供玩家选择 + +**卡牌 UI 节点轻量化设计:** + +- 同屏可能超过 150+ 个卡牌 Control 节点(4 人 × 27 张 + 出牌区 + 历史记录),每个节点挂载独立 Tween/信号监听在低端设备极易掉帧 +- **分级渲染策略:** + - **暗牌(非玩家手牌)**:仅渲染微缩"数量标签(Label)"或单一共享九宫格动态图,不生成独立的卡牌实体节点 + - **明牌(玩家手牌 + 中央出牌区)**:实例化为独立的 `card_node.tscn`,但禁用不必要的关系检测和碰撞 +- 对局中动态销毁已出牌的节点,复用节点对象池(Object Pool),避免频繁 `instantiate` / `queue_free` + +**出牌二次确认:** +- 玩家点击"出牌"按钮后出现确认提示,显示即将打出的牌型和点数 +- 训练模式可选关闭确认,单机/联机默认开启 +- 支持"自动出牌"快捷键:选中牌后双击或按 Enter 直接出 + +**多牌选择交互:** +- 单击选中/取消单张牌 +- 长按拖拽框选连续区域 +- Ctrl+点击连选多张 +- "提示"按钮一键选中 AI 推荐的最优出牌组合 + +**回合计时与状态屏蔽:** +- 每回合显示倒计时进度条(默认 30s),超时自动过牌 +- 等待他人出牌时,手牌区灰显 + 禁用操作 +- 出牌结束到下一回合之间显示过渡动画(0.5s),期间屏蔽输入 + +**输入系统冲突裁定:** + +- 同时存在点击、长按、滑动框选、双击、Ctrl 连选等多种输入方式,需定义冲突优先级: + - **输入状态机:** `InputFSM` 管理空闲(IDLE)→ 轻触(TOUCH_DOWN)→ 拖动(DRAGGING)→ 长按激活(LONG_PRESS)→ 释放(RELEASED) + - **Gesture Priority:** 滑动优先于长按(手指移动 >5px 进入 DRAGGING,取消长按计时);双击 < 300ms 且位移 <5px 判定双击;Ctrl 状态与触摸并存时优先解析为 Ctrl 连选 + - **PC / Mobile 分离输入策略:** 桌面端优先键盘快捷键 + 鼠标操作;移动端优先触摸手势;同一事件不触发双份响应 + +**移动端性能边界:** + +- **最低目标机型:** iPhone X(2017) / 骁龙 665 中端 Android,保持 ≥ 30 FPS +- **FPS 目标:** 桌面 ≥ 60 FPS,移动端 ≥ 30 FPS +- **最大内存占用:** 桌面 < 256MB,移动端 < 128MB +- **动态降级策略:** FPS 持续 < 20 时自动关闭动画(Tween)、降低卡牌分辨率、减少出牌历史保留轮数 +- **低端机配置开关:** `LOW_PERF_MODE`,关闭粒子特效、阴影、抗锯齿,使用预烘焙卡牌贴图 + +**跨平台适配:** + +- **分辨率适配:** 支持 16:9、21:9、4:3 主流比例;采用 `Control.anchor` + `Control.expand` 响应式布局 +- **横竖屏:** 强制横屏(landscape),卡牌游戏竖屏无法容纳 27 张手牌 +- **DPI 缩放:** 使用 Godot `Theme` 系统和 `DisplayServer.screen_get_dpi()` 自动调整字体/控件尺寸 +- **安全区(Safe Area):** 适配刘海屏/挖孔屏,使用 `DisplayServer.get_display_safe_area()` 避让 +- 平板设备使用特殊 UI 布局(更大卡牌、更宽手牌区域) + +--- + +## 九、数据与序列化 + +**状态哈希校验:** 每局结束计算 `GameState` 的 SHA-256 哈希,客户端和服务器(或双方客户端)比对,防止联机数据篡改。 + +**日志裁剪策略:** +- `Action` 日志按局分割,开局清空上局日志 +- 单局日志上限 10000 条(约 500 轮出牌),超出自动截断头部 +- 完整日志仅在开启回放录制时保留,写入 `save/records/` + +**存档格式版本:** +- 所有序列化数据头部包含 `version: int` 字段 +- 当前版本 V1,后续升级需提供 `migrate_vN_to_vN+1()` 迁移函数 +- 读取存档时校验版本号,不兼容版本提示用户更新 + +**状态重放的随机数种子绑定:** + +- Action 日志用于回滚/重放时,若涉及 `RESHUFFLE`(洗牌)或 AI 决策中的 PRNG 动作,必须强制绑定该局开局的 `seed` 值 +- 断线重连重放 Action 时,若随机数种子不一致,AI 会产生与掉线前完全不同的决策分支,导致前后端状态彻底跑飞 +- `Action` 日志起始位置记录 `{seed: int, timestamp: int}`,重放时还原相同种子 +- `deck.gd` 的洗牌和 AI 的随机选择全部使用可注入种子的 PRNG + +**Replay 确定性保证(多平台一致性):** + +仅绑定 seed 不足以保证跨平台回放一致,还须满足严格条件: +- Action 执行顺序绝对固定(按时间戳严格递增,禁止并发 apply) +- Dictionary 遍历不能参与逻辑(GDScript Dictionary 迭代顺序不保证,改用 Array + 排序) +- 所有排序必须使用稳定排序算法(`sort_custom` + 稳定比较器) +- AI 随机源禁止使用系统随机(`randf()` / `randi()`),强制使用种子 PRNG +- 时间戳不能参与规则逻辑(仅用于日志标记和 UI 动画节奏,不用于状态判定) + +**数值统计体系:** + +- 长期统计数据用于匹配、AI 调优和平衡分析: + - `stats_tracker.gd` 记录每个玩家的:总对局数、胜率、双下率、被双下率 + - AI / 实战统计:平均出牌时长、炸弹使用率、逢人配使用次数、进贡胜率 + - Replay 元数据:对局时长、回合数、局次、规则配置、参与者信息 +- 统计数据存储在 `save/stats/`,格式为结构化 JSON,支持导出分析 +- AI 难度统计分离记录("L1 胜率" vs "L2 胜率"),辅助 AI 调优 + +--- + +## 十、网络层(预留) + +**同步模式选型:** + +| 模式 | 原理 | 适用 | +|------|------|------| +| **状态同步(推荐)** | 服务器计算权威状态,客户端接收后渲染 | 回合制卡牌,延迟容忍度高 | +| 帧同步 | 同步操作指令,各端独立计算 | 实时操作类游戏,此处不适用 | + +选择状态同步模型,同步频率为每轮出牌结束时触发一次。 + +**客户端作弊防护(Server-Authoritative):** + +所有 Action 必须在服务端做权威校验,客户端仅允许发送"意图": + +- **非法 Action 注入防范:** 服务端 `validate_action(game_state, action)` 校验每一项 Action,包括:出牌合法性、回合归属、seq_id 连续性 +- **客户端手牌篡改防范:** 客户端不持有完整 GameState,服务端维护权威手牌;客户端只收到自身手牌信息 +- **重放攻击防范:** Action 增加 `seq_id`(严格递增)+ `nonce`(随机盐),服务端拒绝重复 seq_id +- **Action_seq 回退检测:** 服务端记录每个连接的最后 `ack_seq`,拒绝 < ack_seq 的 Action +- **客户端伪造 AI 托管结果防范:** 托管由服务端发起,AI 计算在服务端完成;托管期间客户端发送的 Action 一律拒绝 +- **防作弊时效:** 出牌合法性只在服务端最终裁定,客户端可以做本地预判以优化 UI 响应,但服务端结果始终覆盖客户端 + +**联机同步冲突恢复(Authoritative Rollback):** + +客户端预测状态与服务端权威状态不一致时的处理: + +- 客户端每次提交 Action 后,服务端回传 `{action_seq, state_hash}` +- 客户端比对自身状态的哈希:若一致则确认;若不一致则触发回滚 +- **回滚流程:** 暂停 UI 输入 → 请求从 `action_seq` 开始的全量状态同步 → 重建 UI(手牌重排、出牌区清空重渲染)→ 播放"重同步"过渡动画 → 恢复输入 +- UI 层必须支持**状态全量重建**:接收到新的 `GameStateView` 后完全重建手牌、出牌区、计分板,而非增量 patch + +**网络协议版本兼容:** + +- 存档版本(`data_version`)≠ 联机协议版本(`protocol_version`),两者独立管理 +- `protocol_version` 定义在 `NetworkInterface` 中,起手握手阶段交换 +- **客户端最低兼容版本:** `MIN_COMPATIBLE_PROTOCOL`,低于此版本的客户端拒绝连接,提示更新 +- **热更新后的版本协商:** Lobby 阶段交换支持的 protocol 范围,若房主与客户端版本不兼容则显示提示 +- 协议变更需提供 `protocol_migrate_vN_to_vN+1()` 迁移处理 + +**观战一致性:** + +- **手牌可见性:** 非好友房观战默认隐藏所有手牌,仅显示公共出牌区;好友房可经房主授权后显示队友手牌 +- **延迟观战:** 观战者看到的画面延迟 N 回合(默认 3 回合 / 可配置),防止实时观战泄露信息给对战者 +- **中途加入:** 允许中途加入观战,从加入时刻最近的 `CLEARED` 状态开始重放,不追溯更早回合 +- **透视作弊防护:** 观战者持有独立的 `spectator_view` 视野过滤器,与对局玩家网络通道完全隔离 +- 观战人数上限:每局最多 50 名观战者,超出拒绝新加入 + +**联机状态机正式定义:** + +完整的房间生命周期状态图,确保 network / game / ui 三层状态同步: + +``` + 创建房间 + ↓ + ┌───────────────── IDLE ──────────────────┐ + │ ↓ 有人加入 │ + │ WAITING (等待满员) │ + │ ↓ 4人齐 │ + │ READY_CHECK │ + │ ╱ ╲ │ + │ ALL_READY TIMEOUT │ + │ ↓ ↓ │ + │ IN_GAME 返回 WAITING │ + │ ↓ │ + │ ┌─── PLAYING ───┐ │ + │ │ ↓ 掉线 │ │ + │ │ HOSTING │ (托管中) │ + │ │ ↓ 重连 │ │ + │ │ RECONNECTING │ │ + │ │ ↓ │ │ + │ └→ PLAYING ←┘ │ + │ ↓ 对局结束 │ + │ FINISHED │ + │ ↓ │ + │ VOTE_PHASE (投降投票/再开投票) │ + │ ↓ │ + └── IDLE ─────────────────────────────────┘ + ↓ 房主解散房间 + DESTROYED +``` + +**各状态约束:** +- `PLAYING` → `HOSTING`:心跳超时触发,AI 自动托管,UI 标记托管状态 +- `HOSTING` → `RECONNECTING`:客户端发起重连,服务端追帧 +- `RECONNECTING` → `PLAYING`:状态同步完成,`RECONNECT_GRACE_PERIOD` 内禁止托管出牌 +- `FINISHED` → `VOTE_PHASE`:允许投降投票(联机)/ 下一局投票,对手否决权生效 +- `VOTE_PHASE` → `IDLE`:投票通过或超时,房间解锁 + +**房间与开局条件:** +- `NET_ROOM_CREATED` → `NET_PLAYER_JOINED` × 4 → `NET_ALL_READY` → `NET_GAME_START` +- 4 名玩家全部准备后房主点击开局 +- 等待中可设 AI 补位开关 + +**心跳与异常处理:** +- 客户端每 5s 发送心跳包 +- 服务端 15s 未收到心跳判定断线,触发托管 +- 重连后追帧:客户端发送最后收到的 `action_seq`,服务端补发缺失的 Action 列表 + +**房间锁、准备状态:** +- 开局后房间上锁,禁止新玩家加入 +- 对局中任意时间仅房主可发起投降投票(需队友同意) +- 对局结束后房间自动解锁,返回等待状态 + +**断线重连的视野过滤(Vision Filter / Fog of War):** + +- 服务端序列化状态发送给特定客户端时,**必须经过视野过滤器** +- 只打包该玩家有权知道的信息:自身手牌、公共出牌区、已公开的进贡/还贡牌、历史出牌记录 +- **绝对禁止**将包含所有玩家手牌的完整内存对象直接下发,防止抓包窥牌 +- `NetworkInterface` 中实现 `filter_state_for_player(game_state, player_id): GameStateView` 方法,返回裁剪后的可见状态 + +**进贡阶段的动态视野变化:** + +- 进贡和还贡不是同时发生的:A 进贡给 B 时,B 可见此牌,但 C 和 D 不可见 +- 直到还贡结束或出牌阶段该牌被打出,其他玩家才可见该牌的去向 +- 服务端的 `filter_state_for_player` 必须动态感知 `TRIBUTE_PHASE` 内的"卡牌所有权临时移交状态" +- 防止联机时外挂通过抓包提前得知他人的进贡牌 + +**投降机制的对手否决权:** + +- 掼蛋涉及"升 3 级(双下)"和"升 1 级"的巨大差异,一方提前投降会损害对手抓双下的收益 +- 投降不仅需要队友同意,**还必须发起对手接受投票** +- 流程:发起方 → 队友同意 → 发起对手投票 → 对手两人中至少一人接受 → 投降生效 +- 若对手拒绝投降,游戏必须继续,直至产生实际的名次结算 +- 单机模式中对 AI 可配置投降阈值(如分差 > 100 分),免除投票流程