# 问题 4:all-in 与比牌归属权(side-pot 分配)问题 ## 现象(用户反馈) “德州扑克有 all in 问题和牌判断大小问题,归属权不行。” 拆成两件事: 1. **all-in 相关的归属权**:多个玩家不同筹码 all-in 时,边池(side pot)赢家归属不对 / 钱分错。 2. **比牌判断大小**:比牌(evaluate / compare)结果不对,或平局分池(奇数筹码)归属不对。 ## 现状分析(基于 `holdem.ts` + `holdem-eval.ts` 精读) ### 4.1 牌型评估器(`holdem-eval.ts`)—— 重点排查 - `evaluate5`:逐档判断(同花顺→四条→葫芦→同花→顺子→三条→两对→一对→高牌), `ranks` 是降序用于同档比较,`compareHands` 先比 `category` 再逐位比 `ranks`。 **未发现明显 bug**:wheel(A-2-3-4-5) 在 `straightHigh` 里正确返回 5;同花只数 suit;葫芦/四条的 kicker 取降序正确。 - 需补的**确定性单测**(证明或证伪): - 皇家同花顺 vs 同花顺、四条 vs 葫芦、葫芦 vs 同花、两对 kicker 次序、A 高 vs K 高; - **平局**:完全相同的 7 张 → `compareHands === 0`(已在 `holdem.test.ts` 间接覆盖,需独立断言)。 - **A 既当高又当低**:A-2-3-4-5 与 A-K-Q-J-T 应都识别为顺子且后者更大。 ### 4.2 边池构建(`buildPots`,`holdem.ts:293`)—— 归属权核心 - 按 `totalBetThisHand` 的贡献层级切池;`eligible` = 该层 `inHand && !folded` 的贡献者。 - 逻辑基本正确(单测 `3 人 all-in 100/300/300`、`未跟注返还` 已通过)。 - 潜在问题点: - **空层 / 全弃层**:`winElig.length === 0` 时把整层还给 `contrib[0]`(唯一贡献者)。这在“某层所有人弃牌、但有人更早 all-in 死钱”的极端情况下可能把钱退错人。 标准规则:若某一贡献层级上所有人都弃牌,该层金额应并入**仍能摊牌的最大边池**或退给未弃牌者。 现有实现退给 `contrib[0]`(该层第一贡献者)不够严谨。 - **边池的赢家归属**:`resolveHand` 对每个 `pot` 在 `pot.eligible` 里比牌定赢家(正确)。 - **奇数筹码归属**:`orderFromButton` 按“距按钮顺时针距离”排序,`resolveHand` 里 `extra` 从左往右 +1。 标准:奇数筹码给**按钮左侧第一个赢家**(顺时针)。当前 `seatDistance(from=btn, pid)` 定义是顺时针距离, `orderFromButton` 升序即“离按钮最近的顺时针赢家先拿”,与标准一致(已在 `holdem.test.ts:493` 验证)。 ### 4.3 all-in 街道推进(`roundComplete` / `advance`)—— **已定位真实 bug** `roundComplete` 三个分支(`holdem.ts:198`): - `list.length === 0` → true(正确) - `list.length === 1` → `return G.players[list[0]].betThisRound >= G.currentBet;` - `list.length >= 2` → 全部 `acted && betThisRound === currentBet` **问题在 `list.length === 1` 分支:它只看 `betThisRound >= currentBet`,完全忽略了 `acted`!** 若唯一可行动者已经跟平了当前注、但 `acted === false`(例如他 limp 后别人 all-in、街道尚未因级联结束), 该分支会误判 `roundComplete = true`,**提前结束本街**,导致该玩家被跳过、轮不到他过牌/确认。 **已用确定性单测复现**(见 `holdem-probe.test.ts`): ``` stacks=[0,0,500], bets=[100,100,100], totals=[100,100,100], acted=[true,true,false], currentBet=100, dealerButton='0' → roundComplete 返回 true(错误),应为 false ``` 而 `acted=[true,true,true]` 时返回 true(正确)。说明 bug 仅在“单可行动者 + 已 match 但未 acted”时触发。 **这正是“归属权不行 / all-in 出问题”的根因之一**:本应继续行动的人被跳过,边池在错误时机结算。 **修复**(`holdem.ts`): ```ts if (list.length === 1) { const p = G.players[list[0]!]!; const stillNeedsToAct = p.betThisRound < G.currentBet || G.acted[list[0]!] === false; return !stillNeedsToAct; } ``` 即:单可行动者仍“未完成(需跟注或尚未 acted)”时,不算 roundComplete。 ## 拟采用方案 1. **修 `roundComplete` 的 `list.length === 1` 分支**: 改为“该玩家是否还能行动(call 不为 0 且未 all-in)”,即: ```ts if (list.length === 1) { const p = G.players[list[0]]; const canStillCall = p.betThisRound < G.currentBet; // 还有注要跟 return !canStillCall; } ``` 这样只剩 1 人且面对未 match 的 all-in 注时,仍要等他 call/fold 才结束。 2. **加固 `buildPots` 的空层处理**: - 若某层 `winElig.length === 0` 且存在其他仍有 `inHand && !folded` 的玩家, 该层金额应并入“下一个有赢家的边池”或按总贡献退还给未弃牌者。 - 简化稳妥版:空层时,把金额加进**主池的赢家集合**(即所有未弃牌者摊牌),避免凭空退给某一个人。 - 同时补单测覆盖“某层全弃牌”场景。 3. **扩充 `holdem-eval` 单测 + 加 `compareHands` 平局/大小断言**,证明比牌正确; 若发现 bug 当场修。重点加:A-2-3-4-5 顺、同花顺>四条、两对踢脚次序、完全相同的手牌 `compare===0`。 4. **`resolveHand` 平局/边池综合单测**:构造 3 人不同 all-in + 平局,断言每个池的赢家与金额。 ## 验证 - 新增单测覆盖:`roundComplete` 单可行动者面对大 all-in 注 → 返回 false; `buildPots` 空层;`evaluate7` 各类牌型大小;`resolveHand` 多池 + 平局资金守恒(总发出 = 总池)。 - 资金守恒断言:`Σ stack + pot` 在每手前后恒定(已有 `holdem.test.ts:558`,扩展)。 - 联机手测:3 人不同筹码 all-in → 摊牌后各池赢家与金额符合预期。