问题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>
91 lines
5.7 KiB
Markdown
91 lines
5.7 KiB
Markdown
# 问题 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 → 摊牌后各池赢家与金额符合预期。
|