Skip to content

Latest commit

 

History

History
454 lines (368 loc) · 19.7 KB

File metadata and controls

454 lines (368 loc) · 19.7 KB

Hand Replayer — 产品需求文档(SPEC V2.1)

版本: V2.1(2026-08-05,基于 V2.0 审计修订) 审计依据: 对 V2.0 的独立技术审计(修复全部 P0 问题) 目标: 给 AI 编码 agent 执行的完整 SPEC 技术: 纯前端单文件 HTML(HTML + CSS + Vanilla JS),零依赖,浏览器直接打开 交付物: 1 个自包含 HTML 文件(解析器内嵌,无外部依赖)


〇、V2.1 修订说明(相对 V2.0)

# 问题(V2.0) V2.1 处理
1 "单文件" 与 "index.html + parser.js 双文件" 矛盾 明确:发布态 = 单文件 index.html,解析器代码内嵌 <script>;parser.js 仅开发态拆分,构建/发布时必须合并进 index.html
2 按钮位/位置标签矛盾(Seat1 button 却给 Seat1=UTG) 新增标准位置推导规则(见 §3.6),示例数据全部按规则重写
3 示例算术错误(底池 905≠922、Hero 剩 $0、raises $275 to $275、2.5bb、66% pot) 示例数据重写为自洽版本(见 §2.3、§4.2)
4 无真实 GG/PS 样例 新增 GG、PokerStars 各 1 份真实风格样例(含盲注发布、ante)
5 无行类型清单 新增完整行类型清单与处理规则(见 §2.4)
6 数据模型 amount 语义未定义 明确 amount = 增量、to_amount = 总额(见 §4.1)
7 验收全人工目视、无客观断言 新增黄金输入/期望输出断言(见 §8)

一、产品定位

牌局回放器(Hand Replayer)是一个双用途工具:

用途 场景
用户直接使用 用户复制粘贴 GG/PokerStars 手牌历史,浏览器打开看动态回放
Agent 调用 AI Agent 拿到牌局文本后,生成 HTML 回放并发送给用户

核心价值:把枯燥的纯文本手牌历史,变成一目了然的动态牌桌画面。


二、输入格式

2.1 支持的输入

格式 说明
PokerStars 手牌历史 PokerStars Hand #... 头部 + 标准 action 行 + *** SHOWDOWN *** + SUMMARY
GG (GGPoker) 手牌历史 Poker Hand #RC... 头部 + 标准 action 行 + *** SHOW DOWN *** + SUMMARY
JSON 中间格式 直接粘贴 §4 定义的 JSON,跳过解析直接渲染

明确不支持(超出范围,无需实现):PLO/Omaha、Limit、Zoom/Rush、straddle、非 USD/人民币以外的复杂多币种换算。遇到这些输入给出"不支持该格式"提示,不崩溃。

2.2 真实样例(黄金输入)

样例 A:PokerStars 风格(6-max,含盲注 + 摊牌)

PokerStars Hand #239732915412:  Hold'em No Limit ($2/$5 USD) - 2026/07/14 21:33:12 ET
Table 'Amethyst' 6-max Seat #3 is the button
Seat 1: Alice ($500)
Seat 2: Bob ($600)
Seat 3: Carol ($450)
Seat 4: Hero ($800)
Seat 5: Dave ($320)
Seat 6: Eve ($700)
Dave: posts small blind $2
Eve: posts big blind $5
*** HOLE CARDS ***
Dealt to Hero [Ah Kh]
Alice: folds
Bob: folds
Carol: raises $10 to $15
Hero: calls $15
Dave: folds
Eve: calls $10
*** FLOP *** [Ks 7d 2h]
Eve: checks
Carol: checks
Hero: bets $20
Eve: folds
Carol: calls $20
*** TURN *** [Ks 7d 2h] [Qc]
Carol: checks
Hero: bets $45
Carol: calls $45
*** RIVER *** [Ks 7d 2h Qc] [3s]
Carol: checks
Hero: bets $80
Carol: calls $80
*** SHOWDOWN ***
Carol: shows [Kd Qd] (two pair, Kings and Queens)
Hero: shows [Ah Kh] (a pair of Kings)
Carol collected $335 from pot
*** SUMMARY ***
Total pot $337 | Rake $2
Board [Ks 7d 2h Qc 3s]
Seat 3: Carol showed [Kd Qd] and won ($335) with two pair, Kings and Queens
Seat 4: Hero showed [Ah Kh] and lost with a pair of Kings

账目自洽验证:Dave SB 2 + Eve (BB 5 + call 10 = 15) + Carol (15+20+45+80 = 160) + Hero (15+20+45+80 = 160) = 337。Total pot $337 − Rake $2 = Carol 实收 $335 ✅

样例 B:GG 风格(单挑 + 全下 + 未跟注返还)

Poker Hand #RC87654321:  Hold'em No Limit ($1/$2) - 2026/07/15 14:02:45
Table 'GG8899' 2-max Seat #2 is the button
Seat 1: Tom ($150)
Seat 2: Hero ($200)
Tom: posts small blind $1
Hero: posts big blind $2
*** HOLE CARDS ***
Dealt to Hero [Qc Qd]
Tom: raises $4 to $6
Hero: raises $10 to $16
Tom: calls $10
*** FLOP *** [Jh 8s 2c]
Hero: bets $20
Tom: calls $20
*** TURN *** [Jh 8s 2c] [3d]
Hero: bets $40
Tom: raises $74 to $114 and is all-in
Hero: calls $74
*** RIVER *** [Jh 8s 2c 3d] [7h]
*** SHOW DOWN ***
Hero: shows [Qc Qd] (a pair of Queens)
Tom: shows [As Jh] (a pair of Jacks)
Hero collected $298 from pot
Uncalled bet ($0) returned to Hero
*** SUMMARY ***
Total pot $300 | Rake $2
Board [Jh 8s 2c 3d 7h]
Seat 1: Tom showed [As Jh] and lost with a pair of Jacks
Seat 2: Hero showed [Qc Qd] and won ($298) with a pair of Queens

账目自洽验证:Tom SB 1 + raise 4 (to 6) + call 10 (to 16) + call 20 (to 36) + raise 74 (all-in, to 150) = 150; Hero BB 2 + raise 10 (to 16) + bet 20 (to 36) + bet 40 (to 76) + call 74 (to 150) = 150。底池 = 150+150 = 300。 Total pot $300 − Rake $2 = Hero 实收 $298 ✅

2.3 重写后的自洽示例(渲染基准,替代 V2.0 错误示例)

V2.0 示例的算术错误已全部修正。新基准示例(6-max,$2/$5,按钮 Seat 3):

  • 翻牌前:Dave (SB) $2、Eve (BB) $5;Carol (BTN) raise $10 to $15;Hero call $15;Dave fold;Eve call $10 → 底池 $47
  • Flop [Ks 7d 2h]:Eve check / Carol check / Hero bet $20;Eve fold / Carol call $20 → 底池 $87
  • Turn [Qc]:Carol check / Hero bet $45;Carol call $45 → 底池 $177
  • River [3s]:Carol check / Hero bet $80;Carol call $80 → 底池 $337
  • 摊牌:Carol [Kd Qd] 两对赢,收 $335(Total pot $337 − Rake $2)

(渲染验收时以本示例数值为基准;§2.2 样例 A 与本示例同源)

2.4 行类型清单与处理规则(解析器必须覆盖)

# 行类型 真实格式示例 处理规则
1 盲注发布 Dave: posts small blind $2 / Eve: posts big blind $5 记为 blind action(SB/BB),渲染时从对应玩家栈扣减;必须出现在 HOLE CARDS 之前
2 前注 Ante Player1: posts the ante $1 / posts big blind 20 and ante 5 记为 ante action;锦标赛头部常见
3 不摊牌收池 Player2 collected $150 from pot(无 SHOWDOWN 段) 视为 showdown: false,胜者由 collected 行推断
4 摊牌变体 Player1: mucks hand / Player1: doesn't show hand 该玩家底牌隐藏,不渲染牌面,标注 "mucked"
5 未跟注返还 Uncalled bet ($65) returned to Hero 该金额从底池退回玩家栈,渲染时记录
6 边池 X collected $500 from main pot / Y collected $400 from side pot-1 多胜者场景,每个 collected 行生成一条 result 记录
7 分池 Player1 collected $100 from pot + Player2 collected $100 from pot(含奇数分、小数 $50.50) result.winners[] 数组,支持多人;金额允许小数
8 all-in 变体 calls $65 and is all-in / bets $100 and is all-in / raises $74 to $114 and is all-in action 带 all_in: true 标记
9 锦标赛头部 PokerStars Hand #...: Tournament #1234, $2.50+$0.25 USD Hold'em No Limit - Level I (10/20) 解析 blinds 取 Level 的 (10/20);标记 tournament: true
10 SUMMARY 段 `Total pot $905 Rake $2、Board [...]、Seat 2: Player2 showed [Ad Ac] and won ($903)`
11 噪声/附属行 said, "nice hand"、has timed out、is disconnected、sits out、joins the table、leaves the table、rebuys and receives… 静默跳过,不渲染
12 多手牌连贴 一个文本含多段 Poker Hand #… 按手牌 ID 分段,生成 hands[] 数组;播放器 ⏮/⏭ 切换
13 非 2/6/9 人桌 3-max / 4-max / 5-max / 8-max 按实际座位数布局(通用布局算法,不硬编码 3 种)
14 币种/小数 ($2/$5 USD)、€、$50.50 金额统一按浮点解析;显示保留原币种符号
15 玩家名特殊字符 PS 把名字里 , 转成 ; 解析玩家名时按 seat 行正则捕获,不做逗号分割
16 位置名推导 见 §3.6 按钮位 + 人数 → 位置名映射
17 底牌揭示时机 Hero 底牌发牌即显示;对手底牌摊牌/亮牌时显示 players[].cards 在 JSON 中给出,但渲染时按回合揭示(见 §4.1 reveal 规则)

三、界面规格

3.1 牌桌布局(三种桌型 + 通用)

沿用 V2.0 的 ASCII 布局(单挑 / 6-max / 9-max),补充:

  • 通用布局算法:按 players.length 动态布置座位(角度 = 360°/人数),不再硬编码三种。
  • 按钮位标记 [D],每个座位显示:玩家名、筹码量、行动状态。
  • 摊牌时显示底牌;当前行动玩家高亮。
  • 公牌区渐进显示(翻牌 3 → 转牌 1 → 河牌 1)。
  • 底池金额显示在牌桌中央(实时累计)。

3.2 播放控制栏(沿用 V2.0)

⏮  ◀◀  ▶  ▶▶  ⏭    [P] [F] [T] [R]    速度: [====o====]
按钮 功能 快捷键
⏮ 回到第一手牌 —
◀◀ 上一步(回退一个 action) ←
▶ 播放 / 暂停 Space
▶▶ 下一步 →
⏭ 跳到下一手牌 —
P/F/T/R 跳到 Preflop/Flop/Turn/River P/F/T/R
速度滑块 0.5x ~ 3x(1x = 600ms/action) —

P/F/T/R 跳转语义:跳到该 street 的第一个 action 之前的状态(栈/底池按该 street 开始时重算)。播放到最后一手牌结束时自动暂停并显示结果摘要。

3.3 操作日志(底部)

沿用 V2.0,修正:

  • 金额统一显示增量 + 总额:CO (Carol) raises $10 to $15
  • bb 换算统一:raises 2bb → $10(bb 以当前大盲为基准,保留 1 位小数)
  • pot 百分比统一按"该 action 发生时当前底池"计算,保留整数百分比

3.4 手牌信息栏(顶部)

#123456789  NL Hold'em $2/$5  6-max  |  Hand 1 of 2  |  Pot: $337

3.5 导出/分享(沿用 V2.0,含单文件约束)

  • 导出 HTML:生成完全自包含文件(CSS+JS+数据内嵌),命名 hand_<hand_id>.html
  • 复制链接:JSON → gzip → base64 → URL #hash;对方打开同一个 index.html 自动加载
  • 自包含导出必须包含解析器代码(不依赖外部 parser.js)

3.6 位置推导规则(V2.1 新增,修正 V2.0 矛盾)

按钮位 = 头部 Seat #N is the button。位置名由按钮位顺时针推导:

桌型 位置顺序(从按钮顺时针)
2-max (HU) BTN → SB → BB
6-max BTN → SB → BB → UTG → HJ → CO
9-max BTN → SB → BB → UTG → UTG+1 → MP → MP+1 → HJ → CO

翻牌前第一个行动人 = 按钮左手的 SB(HU 则为 BB)。示例:6-max 按钮 Seat 3 → Seat 4=SB、Seat 5=BB、Seat 6=UTG、Seat 1=HJ、Seat 2=CO,翻牌前第一个行动 Seat 4 (SB)。

⚠️ 若头部明确 (button) 坐在某座,位置标签必须与上述推导一致;解析器输出位置名,渲染日志按此显示。


四、技术方案

架构

输入(文本)→ 解析器(内嵌) → JSON 中间格式 → 渲染引擎 → HTML 牌桌视图 + 播放器

4.1 数据模型(JSON 中间格式,V2.1 修订)

amount 语义(明确):

  • action.amount = 本次动作的增量(call/raise/bet 各扣多少筹码)
  • action.to_amount = 动作后的总额(仅 raise/bet 有,可选)
  • 例:raises $10 to $15 → {action: "raise", amount: 10, to_amount: 15}
{
  "hand_id": "239732915412",
  "game": "NL Hold'em",
  "game_type": "cash",
  "blinds": [2, 5],
  "ante": 0,
  "table": "Amethyst",
  "max_players": 6,
  "button_seat": 3,
  "tournament": false,
  "players": [
    {"seat": 1, "name": "Alice", "stack": 500, "cards": null, "position": "HJ"},
    {"seat": 2, "name": "Bob", "stack": 600, "cards": null, "position": "CO"},
    {"seat": 3, "name": "Carol", "stack": 450, "cards": ["Kd", "Qd"], "position": "BTN"},
    {"seat": 4, "name": "Hero", "stack": 800, "cards": ["Ah", "Kh"], "position": "SB"},
    {"seat": 5, "name": "Dave", "stack": 320, "cards": null, "position": "BB"},
    {"seat": 6, "name": "Eve", "stack": 700, "cards": null, "position": "UTG"}
  ],
  "streets": [
    {
      "name": "preflop",
      "actions": [
        {"player": "Dave", "action": "blind", "amount": 2, "type": "sb"},
        {"player": "Eve", "action": "blind", "amount": 5, "type": "bb"},
        {"player": "Carol", "action": "raise", "amount": 10, "to_amount": 15},
        {"player": "Hero", "action": "call", "amount": 15, "to_amount": 15},
        {"player": "Dave", "action": "fold"},
        {"player": "Eve", "action": "call", "amount": 10, "to_amount": 15}
      ]
    },
    {
      "name": "flop",
      "cards": ["Ks", "7d", "2h"],
      "actions": [
        {"player": "Eve", "action": "check"},
        {"player": "Carol", "action": "check"},
        {"player": "Hero", "action": "bet", "amount": 20, "to_amount": 20},
        {"player": "Eve", "action": "fold"},
        {"player": "Carol", "action": "call", "amount": 20, "to_amount": 20}
      ]
    },
    {
      "name": "turn",
      "cards": ["Qc"],
      "actions": [
        {"player": "Carol", "action": "check"},
        {"player": "Hero", "action": "bet", "amount": 45, "to_amount": 45},
        {"player": "Carol", "action": "call", "amount": 45, "to_amount": 45}
      ]
    },
    {
      "name": "river",
      "cards": ["3s"],
      "actions": [
        {"player": "Carol", "action": "check"},
        {"player": "Hero", "action": "bet", "amount": 80, "to_amount": 80},
        {"player": "Carol", "action": "call", "amount": 80, "to_amount": 80}
      ]
    }
  ],
  "result": {
    "winners": [
      {"player": "Carol", "amount": 335, "hand": "two pair, Kings and Queens"}
    ],
    "pot": 337,
    "rake": 2,
    "showdown": true
  }
}

reveal 规则:players[].cards 中 Hero(或 JSON 中标记 is_hero: true 的玩家)在发牌(HOLE CARDS)时显示;其他玩家仅在 result.showdown = true 且该玩家未 muck 时显示。

多手牌:输入含多段手牌时,顶层为 {"hands": [ {...}, {...} ]}。

4.2 解析器(Parser,内嵌)

  • 按 §2.4 行类型清单逐行正则匹配
  • 玩家名捕获:Seat (\d+): (.+?) \($([\d.,]+)\)
  • 金额统一浮点;$ 后数字支持小数($50.50)
  • 输出 §4.1 JSON;解析失败时:标注错误行号 + 原因,UI 显示"解析失败:第 N 行(原文)",不静默丢弃
  • 容错边界:无 SUMMARY 段时用动作推底池;无 rake 信息时 rake=0

4.3 渲染引擎(纯 CSS,沿用 V2.0)

扑克牌纯 CSS 绘制(♠♣ 黑、♥♦ 红);深绿椭圆桌面;筹码简化数字;行动高亮。

4.4 播放引擎(JS,沿用 V2.0 + 修正)

  • 状态快照实现后退(每 action 后存栈/底池状态)
  • 播放速度 1x = 600ms/action
  • 结束自动暂停 + 结果摘要(胜者/手牌/奖金)

五、文件结构(V2.1 修订)

projects/HandReplayer/
├── index.html           ← ★ 唯一交付物:自包含回放器(解析器内嵌,浏览器打开即用)
├── src/                  ← 开发态源码(构建时合并进 index.html)
│   └── parser.js         ← 标准格式解析器(GG/PokerStars)
├── spec/
│   └── data_model.json   ← 数据模型示例(§4.1)
├── examples/
│   ├── example_gg.txt    ← GG 格式黄金输入(§2.2 样例 B)
│   ├── example_ps.txt    ← PokerStars 格式黄金输入(§2.2 样例 A)
│   ├── example_nl.txt    ← 自然语言输入示例
│   └── example.json      ← JSON 中间格式示例(§4.1)
├── scripts/
│   └── parse_nl.py       ← 自然语言→JSON(LLM,供 agent 使用)
├── README.md             ← 用户文档(中英双语)
└── GUIDE.md              ← 开发/集成指南(中英双语)

单文件约束(硬性):index.html 必须内嵌全部 CSS + JS(含解析器),<script src="parser.js"> 只允许在 src/ 开发态出现。发布验收时检查 index.html 无外部资源引用。


六、交付线路图(V2.1 修订)

Phase 内容 优先级
P1 index.html — 单文件回放器(解析器内嵌 + 渲染 + 播放) 🔴 必须
P2 黄金输入样例(GG/PS 各 1 + JSON) 🔴 必须
P3 解析器覆盖全部行类型(§2.4 17 项) 🟡 重要
P4 parse_nl.py — 自然语言 → JSON(LLM) 🟢 增强
P5 README + GUIDE — 中英双语 🟡 重要

七、Claw Code 执行指令(V2.1 修订)

任务:实现 Hand Replayer V2.1

交付物(按顺序):
1. index.html — ★ 单文件牌局回放器(自包含,无外部依赖)
   - 解析器内嵌:GG/PokerStars 手牌历史 → JSON 中间格式(覆盖 §2.4 全部 17 类行)
   - 三种桌型 + 通用布局(按实际人数动态布置)
   - 扑克牌 CSS 绘制(无图片)
   - 完整播放器控件(⏮ ◀◀ ▶ ▶▶ ⏭ P/F/T/R + 速度滑块)+ 快捷键
   - 操作日志同步滚动(金额显示:增量 to 总额)
   - 导出 HTML(自包含)+ 复制链接(gzip+base64 URL hash)
   - 移动端响应式(≤360px 可用)
   - 位置推导(§3.6 规则表)
   - 错误提示(解析失败显示行号+原文)

2. examples/ — 黄金数据
   - example_ps.txt(§2.2 样例 A)、example_gg.txt(§2.2 样例 B)、example.json(§4.1)
   - 数值必须与 SPEC 完全一致

3. README.md + GUIDE.md — 双语

技术约束:
- 零外部依赖(无 npm, 无 CDN, 无图片, 无外部 script/link)
- 浏览器直接打开即可使用
- 纯 CSS 画牌面
- 暗色主题(深绿牌桌 + 黑色背景)
- 单文件内嵌全部代码(验收会检查)

八、验收标准(V2.1 修订:客观断言 + 人工检查)

8.1 客观断言(可自动化验证)

  1. 黄金输入解析:粘贴 §2.2 样例 A,解析后 JSON 满足:
    • hand_id = "239732915412"、blinds = [2,5]、button_seat = 3、max_players = 6
    • 位置:Seat4=SB、Seat5=BB、Seat6=UTG、Seat1=HJ、Seat2=CO、Seat3=BTN
    • result.pot = 337、result.rake = 2、胜者 Carol 收 335
    • action 数量 ≥ 20 条;raise $10 to $15 解析为 {amount: 10, to_amount: 15}
  2. 样例 B(GG/单挑/全下):解析后 result.winners[0].player = "Hero"、pot = 300、存在 all_in: true action
  3. 坏输入:粘贴 hello world → 显示"解析失败"提示,不崩溃、不白屏
  4. 单文件检查:index.html 内无 <script src=、<link rel=stylesheet href=、http(s):// 外部引用
  5. 多手牌:拼接样例 A + B → 解析出 hands 长度 2,⏮/⏭ 可切换

8.2 人工检查(沿用 V2.0,修正数值)

  1. 三种桌型渲染正确(单挑/6-max/9-max,用黄金样例验证)
  2. ▶ 播放逐行动更新;← → 单步前进/后退;F 跳到 Flop(栈/底池正确)
  3. 摊牌双方底牌亮出、胜者高亮
  4. 操作日志同步高亮;金额显示"增量 to 总额"
  5. 导出 HTML → 新窗口打开可播放(自包含)
  6. 复制链接 → 新浏览器粘贴自动加载
  7. iPhone 竖屏(375px)控件可用、牌桌可缩放

九、补充约定

  • 界面文案:中文为主,英文术语保留(BTN/BB/raise/call)
  • 数字显示:金额保留整数(除非小数),bb 保留 1 位小数
  • 牌局文本粘贴区:textarea,支持自动检测多手牌
  • 性能:1000 条 action 内播放不卡顿(状态快照内存上限 2000 帧,超出压缩)

版本历史: V2.0 (2026-05) → V2.1 (2026-08-05,审计修订)