- 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>
117 lines
4.8 KiB
TypeScript
117 lines
4.8 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|