问题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>
5.7 KiB
问题 4:all-in 与比牌归属权(side-pot 分配)问题
现象(用户反馈)
“德州扑克有 all in 问题和牌判断大小问题,归属权不行。”
拆成两件事:
- all-in 相关的归属权:多个玩家不同筹码 all-in 时,边池(side pot)赢家归属不对 / 钱分错。
- 比牌判断大小:比牌(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):
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。
拟采用方案
-
修
roundComplete的list.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 才结束。
-
加固
buildPots的空层处理:- 若某层
winElig.length === 0且存在其他仍有inHand && !folded的玩家, 该层金额应并入“下一个有赢家的边池”或按总贡献退还给未弃牌者。 - 简化稳妥版:空层时,把金额加进主池的赢家集合(即所有未弃牌者摊牌),避免凭空退给某一个人。
- 同时补单测覆盖“某层全弃牌”场景。
- 若某层
-
扩充
holdem-eval单测 + 加compareHands平局/大小断言,证明比牌正确; 若发现 bug 当场修。重点加:A-2-3-4-5 顺、同花顺>四条、两对踢脚次序、完全相同的手牌compare===0。 -
resolveHand平局/边池综合单测:构造 3 人不同 all-in + 平局,断言每个池的赢家与金额。
验证
- 新增单测覆盖:
roundComplete单可行动者面对大 all-in 注 → 返回 false;buildPots空层;evaluate7各类牌型大小;resolveHand多池 + 平局资金守恒(总发出 = 总池)。 - 资金守恒断言:
Σ stack + pot在每手前后恒定(已有holdem.test.ts:558,扩展)。 - 联机手测:3 人不同筹码 all-in → 摊牌后各池赢家与金额符合预期。