Files
huajishe-tts/docs/texas/04-allin-hand-comparison.md
e2hang 04d0af515c feat: 登录系统落地 + 踢人修正 + 单房间约束 + 权限收口
问题8 登录系统:
- node:sqlite 账号存储(scrypt 哈希 + HMAC 签名 token),preset admin 改为 e2hang/evan1115
- /api/auth/register|login|me,me 返回 room 供重联
- 网关拦截 create/join 需登录(401),列表保持开放

踢人逻辑修正(问题2 延伸):
- kickMove 真正释放座位(folded=false, name=null)而非锁死
- war/mill 去掉永久封禁语义

管理员越权:管理员 token 可删任意房间、踢任意房间内的人

单房间约束(用户新需求):
- 同一用户同时只能在一个房间,create/join 他房返回 409
- 踢人后若房间仅剩踢人者则自动解散并释放其记录(修复 2 人局踢光对手后被困 409)
- 陈旧房间记录兜底清理

UI 清理:移除"改名"按钮与"你的名字"输入框;删除房间按钮仅房主/管理员可见

docs: 新增 12-auth-kick-single-room.md 汇总本会话全部需求/根因/修复/边界测试

Co-Authored-By: Claude (CodeBuddy) <noreply@codebuddy.ai>
2026-08-25 14:13:59 +08:00

5.7 KiB
Raw Permalink Blame History

问题 4all-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未发现明显 bugwheel(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 边池构建(buildPotsholdem.ts:293)—— 归属权核心

  • totalBetThisHand 的贡献层级切池;eligible = 该层 inHand && !folded 的贡献者。
  • 逻辑基本正确(单测 3 人 all-in 100/300/300未跟注返还 已通过)。
  • 潜在问题点:
    • 空层 / 全弃层winElig.length === 0 时把整层还给 contrib[0](唯一贡献者)。这在“某层所有人弃牌、但有人更早 all-in 死钱”的极端情况下可能把钱退错人。 标准规则:若某一贡献层级上所有人都弃牌,该层金额应并入仍能摊牌的最大边池或退给未弃牌者。 现有实现退给 contrib[0](该层第一贡献者)不够严谨。
    • 边池的赢家归属resolveHand 对每个 potpot.eligible 里比牌定赢家(正确)。
    • 奇数筹码归属orderFromButton 按“距按钮顺时针距离”排序,resolveHandextra 从左往右 +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 === 1return 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

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. roundCompletelist.length === 1 分支 改为“该玩家是否还能行动call 不为 0 且未 all-in

    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 → 摊牌后各池赢家与金额符合预期。