Files
huajishe-tts/apps/web/e2e/holdem-3p-allin.spec.ts
e2hang bc374350c4 fix(holdem+pig): 筹码不足修复 + 拱猪墩满 3 秒延迟 + 真实姓名同步 + 联机 E2E
- holdem 引擎与 UI:筹码不足大盲的「坐板凳」状态补全
  * eligibleCount 从 seatedWithChips 改用 seatedCanPlay(只看筹码足够大盲的玩家)
  * broke 判定从 stack <= 0 改为 stack < bigBlind(短筹码也能补码)
  * 座位卡新增「筹码不足」徽章(提示玩家补码)
  * 行动者切换时 raiseAmt 重置(避免上一位玩家拖动的值残留)
  * WaitingControls 接收 gameover prop,便于 UI 显示结算

- holdem.test.ts:覆盖新逻辑(broke / eligible / 补码 / 行动者切换 raise 重置)

- pig 引擎与 UI:墩满 3 秒延迟收牌 + 玩家名同步
  * G.trickWinner 字段:满墩但未收牌期间用于 UI 高亮;3 秒后由 collectTrick 收牌推进
  * G.playerNames 字段:setName move 写入,server join 时 best-effort 派发 + 客户端 mount 兜底
  * pig.test.ts:新增 trickWinner / collectTrick / setName 单测

- PigBoard.tsx:座位卡显示真实姓名(优先 G.playerNames → prop → 玩家N 回退)
  * 挂载时调 setName 把本人真实名写入引擎,广播后所有人可见
  * 墩满时显示赢家高亮 + 3 秒倒计时收牌

- rooms.ts:注释说明 join 阶段 setName 派发为 best-effort(boardgame.io 对非 active
  玩家在 stateID=0 拦截 move),真实可靠的姓名来自客户端连上后的 setName

- 新增联机 E2E(playwright):
  * holdem-3p-allin.spec.ts:3 人德州 all-in 流局 + 边池分配
  * holdem-allin-repro.spec.ts:all-in 重现回归
  * pig-names-and-trick-delay.spec.ts:拱猪 4 人真实姓名显示 + 墩满延迟收牌

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-05 22:15:50 +08:00

117 lines
4.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Holdem — 3 人 all-in 摊牌 E2E(联机,真实 UI + server + engine)。
* 多人 all-in 之后:公共牌应发完、亮牌、显示比大小结果。
*/
import { test, expect, type Browser, type Page, type BrowserContext } from '@playwright/test';
interface Seat {
ctx: BrowserContext;
page: Page;
pid: string;
}
async function authUser(suffix: string): Promise<{ token: string; username: string }> {
const uname = `hj${suffix}${Math.floor(Math.random() * 1e6)}`.slice(0, 18);
const pwd = 'pw123456';
const base = process.env.TTS_API_BASE ?? 'http://localhost:8000';
const reg = await fetch(`${base}/api/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: uname, password: pwd }),
});
if (reg.ok) {
const j = (await reg.json()) as { token: string; username: string };
return { token: j.token, username: j.username };
}
const login = await fetch(`${base}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: uname, password: pwd }),
});
const j = (await login.json()) as { token: string; username: string };
return { token: j.token, username: j.username };
}
async function newSeat(browser: Browser, pid: string, auth: { token: string; username: string }): Promise<Seat> {
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.addInitScript(
([token, username]) => {
localStorage.setItem('tts-like.token', token);
localStorage.setItem('tts-like.username', username);
},
[auth.token, auth.username] as [string, string],
);
await page.goto('/');
await page.getByTestId('game-select').selectOption('holdem');
await page.getByTestId('mode-online').click();
await expect(page.getByTestId('lobby-room-list')).toBeVisible({ timeout: 15_000 });
return { ctx, page, pid };
}
test('3人 all-in → 三个客户端都看到完整公共牌 + 亮牌 + 结果', async ({ browser }) => {
const auths = [await authUser('a'), await authUser('b'), await authUser('c')];
const seats = [
await newSeat(browser, '0', auths[0]!),
await newSeat(browser, '1', auths[1]!),
await newSeat(browser, '2', auths[2]!),
];
// P0 创建 3 座房间,起始筹码 100
await seats[0]!.page.getByTestId('lobby-create-seats').selectOption('3');
await seats[0]!.page.getByTestId('lobby-create-startingStack').fill('100');
await seats[0]!.page.getByTestId('lobby-create-room').click();
await expect(seats[0]!.page.getByTestId('holdem-board')).toBeVisible({ timeout: 15_000 });
// 读 P0 "在房间" 显示的 matchID(避免误加入旧房间)
const roomText = await seats[0]!.page.getByTestId('lobby-current-room').innerText();
const mid = roomText.match(/在房间\s+(\S+)\s+作为/)?.[1] ?? '';
// P1 / P2 加入 P0 的房间
for (const seat of seats.slice(1)) {
await expect(async () => {
await seat.page.getByTestId('lobby-refresh').click();
const joinBtn = seat.page.getByTestId(`lobby-join-${seat.pid}-${mid}`);
expect(await joinBtn.count()).toBeGreaterThan(0);
}).toPass({ timeout: 15_000 });
await seat.page.getByTestId(`lobby-join-${seat.pid}-${mid}`).click();
await expect(seat.page.getByTestId('holdem-board')).toBeVisible({ timeout: 15_000 });
}
// 开始
await expect(async () => {
const startBtn = seats[0]!.page.getByTestId('holdem-start');
await expect(startBtn).toBeEnabled({ timeout: 2_000 });
await startBtn.click();
}).toPass({ timeout: 20_000 });
// button=0, SB=1, BB=2, UTG=0。等 P0 的行动条。
await expect(seats[0]!.page.getByTestId('holdem-btn-allin')).toBeVisible({ timeout: 15_000 });
await seats[0]!.page.getByTestId('holdem-btn-allin').click(); // P0 全下 100
// P1 call 全下
await expect(seats[1]!.page.getByTestId('holdem-btn-call')).toBeVisible({ timeout: 15_000 });
await seats[1]!.page.getByTestId('holdem-btn-call').click();
// P2 call 全下
await expect(seats[2]!.page.getByTestId('holdem-btn-call')).toBeVisible({ timeout: 15_000 });
await seats[2]!.page.getByTestId('holdem-btn-call').click();
// 三个客户端都应看到:5 张公共牌 + 6 张亮出的底牌 + 日志含"赢得"
for (const seat of seats) {
await expect(async () => {
const cards = seat.page.locator('[data-testid^="holdem-board-card-"]');
expect(await cards.count()).toBe(5);
}).toPass({ timeout: 15_000 });
await expect(async () => {
const holes = seat.page.locator('[data-testid^="holdem-hole-"]');
expect(await holes.count()).toBe(6);
}).toPass({ timeout: 5_000 });
await expect(async () => {
const log = await seat.page.getByTestId('holdem-log').innerText();
expect(log).toContain('赢得');
}).toPass({ timeout: 5_000 });
}
console.log('matchID:', mid);
});