Files
game-cards/docs/superpowers/specs/2026-05-28-guandan-card-game-design.md
2026-05-28 23:29:43 +08:00

909 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 掼蛋卡牌游戏 — 设计规格
**日期:** 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 A2、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, # 还贡点数上限(默认 100=不限)
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/CSVAI 参数、关卡等)
│ │ └── 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< 2sL3< 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 X2017 / 骁龙 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 分),免除投票流程