版本: V2.1(2026-08-05,基于 V2.0 审计修订) 审计依据: 对 V2.0 的独立技术审计(修复全部 P0 问题) 目标: 给 AI 编码 agent 执行的完整 SPEC 技术: 纯前端单文件 HTML(HTML + CSS + Vanilla JS),零依赖,浏览器直接打开 交付物: 1 个自包含 HTML 文件(解析器内嵌,无外部依赖)
| # | 问题(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 回放并发送给用户 |
核心价值:把枯燥的纯文本手牌历史,变成一目了然的动态牌桌画面。
| 格式 | 说明 |
|---|---|
| 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/人民币以外的复杂多币种换算。遇到这些输入给出"不支持该格式"提示,不崩溃。
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 ✅
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 ✅
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 与本示例同源)
| # | 行类型 | 真实格式示例 | 处理规则 |
|---|---|---|---|
| 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 规则) |
沿用 V2.0 的 ASCII 布局(单挑 / 6-max / 9-max),补充:
- 通用布局算法:按
players.length动态布置座位(角度 = 360°/人数),不再硬编码三种。 - 按钮位标记
[D],每个座位显示:玩家名、筹码量、行动状态。 - 摊牌时显示底牌;当前行动玩家高亮。
- 公牌区渐进显示(翻牌 3 → 转牌 1 → 河牌 1)。
- 底池金额显示在牌桌中央(实时累计)。
⏮ ◀◀ ▶ ▶▶ ⏭ [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 开始时重算)。播放到最后一手牌结束时自动暂停并显示结果摘要。
沿用 V2.0,修正:
- 金额统一显示增量 + 总额:
CO (Carol) raises $10 to $15 - bb 换算统一:
raises 2bb → $10(bb 以当前大盲为基准,保留 1 位小数) - pot 百分比统一按"该 action 发生时当前底池"计算,保留整数百分比
#123456789 NL Hold'em $2/$5 6-max | Hand 1 of 2 | Pot: $337
- 导出 HTML:生成完全自包含文件(CSS+JS+数据内嵌),命名
hand_<hand_id>.html - 复制链接:JSON → gzip → base64 → URL
#hash;对方打开同一个 index.html 自动加载 - 自包含导出必须包含解析器代码(不依赖外部 parser.js)
按钮位 = 头部 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 牌桌视图 + 播放器
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": [ {...}, {...} ]}。
- 按 §2.4 行类型清单逐行正则匹配
- 玩家名捕获:
Seat (\d+): (.+?) \($([\d.,]+)\) - 金额统一浮点;
$后数字支持小数($50.50) - 输出 §4.1 JSON;解析失败时:标注错误行号 + 原因,UI 显示"解析失败:第 N 行(原文)",不静默丢弃
- 容错边界:无 SUMMARY 段时用动作推底池;无 rake 信息时 rake=0
扑克牌纯 CSS 绘制(♠♣ 黑、♥♦ 红);深绿椭圆桌面;筹码简化数字;行动高亮。
- 状态快照实现后退(每 action 后存栈/底池状态)
- 播放速度 1x = 600ms/action
- 结束自动暂停 + 结果摘要(胜者/手牌/奖金)
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 无外部资源引用。
| Phase | 内容 | 优先级 |
|---|---|---|
| P1 | index.html — 单文件回放器(解析器内嵌 + 渲染 + 播放) |
🔴 必须 |
| P2 | 黄金输入样例(GG/PS 各 1 + JSON) | 🔴 必须 |
| P3 | 解析器覆盖全部行类型(§2.4 17 项) | 🟡 重要 |
| P4 | parse_nl.py — 自然语言 → JSON(LLM) |
🟢 增强 |
| P5 | README + GUIDE — 中英双语 | 🟡 重要 |
任务:实现 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 画牌面
- 暗色主题(深绿牌桌 + 黑色背景)
- 单文件内嵌全部代码(验收会检查)
- 黄金输入解析:粘贴 §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}
- 样例 B(GG/单挑/全下):解析后
result.winners[0].player = "Hero"、pot = 300、存在all_in: trueaction - 坏输入:粘贴
hello world→ 显示"解析失败"提示,不崩溃、不白屏 - 单文件检查:index.html 内无
<script src=、<link rel=stylesheet href=、http(s)://外部引用 - 多手牌:拼接样例 A + B → 解析出
hands长度 2,⏮/⏭ 可切换
- 三种桌型渲染正确(单挑/6-max/9-max,用黄金样例验证)
- ▶ 播放逐行动更新;← → 单步前进/后退;F 跳到 Flop(栈/底池正确)
- 摊牌双方底牌亮出、胜者高亮
- 操作日志同步高亮;金额显示"增量 to 总额"
- 导出 HTML → 新窗口打开可播放(自包含)
- 复制链接 → 新浏览器粘贴自动加载
- iPhone 竖屏(375px)控件可用、牌桌可缩放
- 界面文案:中文为主,英文术语保留(BTN/BB/raise/call)
- 数字显示:金额保留整数(除非小数),bb 保留 1 位小数
- 牌局文本粘贴区:textarea,支持自动检测多手牌
- 性能:1000 条 action 内播放不卡顿(状态快照内存上限 2000 帧,超出压缩)
版本历史: V2.0 (2026-05) → V2.1 (2026-08-05,审计修订)