M7: 바퀴벌레 포커 규칙·화면·테스트
- 규칙 모듈(packages/games/src/cockroach-poker): 64장(8종×8) 셔플·좌석순 분배, 2인 10장 비공개 제거, offer/call/peek/pass 흐름, seen 규칙, 2인 판정 전용(twoPlayerPass 하우스 룰은 1회 되돌려 주기), 같은 종류 loseCount장(자동: 2인 5, 3~6인 4)·손패 0장 시작자·eightKindsRule 패배, 시간 초과 자동 행동(§8.5)과 3연속 시간 초과 시 자리 비움(5초), 나가면 그 사람 패배, 빠른 말(say, 라운드당 3번), view는 남의 손패·미확인자에게 건네는 카드·제거 카드를 숨김 - 테스트: §10 시나리오 전부, randomPlayout 200판 + 빠른 루프 800판(카드 보존·유출·onTimeout 합법·재생 결정성) - 화면(apps/web/src/games/cockroach-poker): 자체 SVG 벌레 8종, 앞마당 종류별 배지(경고/위험), 건네기 3단계(카드→사람→주장), 판정 시트(진짜!/거짓말!/몰래 보고 넘기기), 넘긴 순서·본 사람 표시 - 등록: GAMES, CATALOG, 웹 registry, GAME_ICON / E2E 플랜(PC 방장 + 휴대폰 2명, 끝까지 진행) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
104
e2e/games.e2e.ts
104
e2e/games.e2e.ts
@@ -260,6 +260,55 @@ async function marbleStep(p: Page, stats: Record<string, number>): Promise<strin
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 바퀴벌레 포커: one UI step for whoever must act on `p` — offer (card → person → claim → confirm), pass after
|
||||
* peeking (person → claim → confirm) or answer an offer (진짜!/거짓말!, sometimes 몰래 보고 넘기기).
|
||||
*/
|
||||
async function cockroachStep(p: Page, stats: Record<string, number>): Promise<string | null> {
|
||||
const status = await p.locator('.turn-status').innerText().catch(() => '');
|
||||
if (!status.includes('내 차례예요')) return null;
|
||||
const panel = p.locator('.cp-panel').first();
|
||||
const offering = (await panel.getByText('카드를 건네 주세요', { exact: true }).count()) > 0;
|
||||
if (offering || (await panel.getByText('본 카드를 다른 사람에게 넘겨 주세요', { exact: true }).count())) {
|
||||
const give = panel.getByRole('button', { name: /(건네기|넘기기)$/ });
|
||||
const n = (stats.offer ?? 0) + (stats.pass ?? 0);
|
||||
if (offering) {
|
||||
const cards = panel.locator('.cp-hand button.cp-pick');
|
||||
await cards.nth(n % (await cards.count())).click();
|
||||
}
|
||||
const targets = panel.locator('button.cp-target:not([disabled])');
|
||||
await targets.nth(n % (await targets.count())).click();
|
||||
await panel.locator('button.cp-claim').nth((n * 3) % 8).click();
|
||||
if (!stats[`shot-give-${tag(p)}`]) {
|
||||
stats[`shot-give-${tag(p)}`] = 1;
|
||||
await p.screenshot({ path: join(SHOTS, `cockroach-poker-give-${tag(p)}.png`), fullPage: tag(p) === 'phone' });
|
||||
}
|
||||
await give.click({ timeout: 2000 });
|
||||
stats[offering ? 'offer' : 'pass'] = (stats[offering ? 'offer' : 'pass'] ?? 0) + 1;
|
||||
return offering ? 'offer' : 'pass';
|
||||
}
|
||||
const yes = panel.getByRole('button', { name: /^진짜!/ });
|
||||
if (await yes.count()) {
|
||||
if (!stats[`shot-respond-${tag(p)}`]) {
|
||||
stats[`shot-respond-${tag(p)}`] = 1;
|
||||
await p.screenshot({ path: join(SHOTS, `cockroach-poker-respond-${tag(p)}.png`), fullPage: tag(p) === 'phone' });
|
||||
}
|
||||
const peek = panel.getByRole('button', { name: '👁 몰래 보고 넘기기' });
|
||||
const calls = (stats.true ?? 0) + (stats.false ?? 0);
|
||||
if ((await peek.isEnabled().catch(() => false)) && calls % 3 === 1) {
|
||||
await peek.click();
|
||||
await panel.getByText('본 카드를 다른 사람에게 넘겨 주세요').waitFor({ timeout: 3000 });
|
||||
stats.peek = (stats.peek ?? 0) + 1;
|
||||
return 'peek';
|
||||
}
|
||||
const which = calls % 2 === 0 ? 'true' : 'false';
|
||||
await panel.getByRole('button', { name: which === 'true' ? /^진짜!/ : /^거짓말!/ }).click();
|
||||
stats[which] = (stats[which] ?? 0) + 1;
|
||||
return which;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const PLANS: Record<string, Plan> = {
|
||||
rummikub: {
|
||||
// PC host + phone guest, first meld "없음" so hint plays happen early; 10 real turns through the UI.
|
||||
@@ -879,6 +928,61 @@ const PLANS: Record<string, Plan> = {
|
||||
console.log(` marble: finished after ${actions} UI actions ${JSON.stringify(stats)}`);
|
||||
},
|
||||
},
|
||||
'cockroach-poker': {
|
||||
// PC host + 2 phones, "같은 벌레 3장" for a shorter game. Real turns through the UI until someone loses:
|
||||
// offer a face-down card with a claim, the target calls 진짜!/거짓말! or peeks and passes; the card goes face-up.
|
||||
players: 3,
|
||||
lobby: async (host) => {
|
||||
await host.getByRole('group', { name: '같은 벌레 몇 장이면 패배' }).getByRole('button', { name: '3장(짧게)' }).click();
|
||||
await host.getByRole('button', { name: '설정 저장' }).click();
|
||||
await host.getByText(/같은 벌레 3장이면 패배/).first().waitFor();
|
||||
},
|
||||
drive: async (pages) => {
|
||||
const [host, guest] = pages as [Page, Page, Page];
|
||||
const stats: Record<string, number> = {};
|
||||
let actions = 0;
|
||||
let shot = false;
|
||||
const until = Date.now() + 300_000;
|
||||
const over = () => host.getByRole('dialog', { name: '게임 결과' }).count();
|
||||
while (Date.now() < until && !(await over())) {
|
||||
let acted = false;
|
||||
for (const p of pages) {
|
||||
const did = await cockroachStep(p, stats);
|
||||
if (did) {
|
||||
acted = true;
|
||||
actions++;
|
||||
// Wait for the server to move on before looking again.
|
||||
await p.waitForFunction(() => !document.querySelector('.turn-status')?.textContent?.includes('내 차례예요') || !!document.querySelector('.cp-panel button.cp-target'), null, { timeout: 4000 }).catch(() => {});
|
||||
await p.waitForTimeout(120);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!shot && (stats.true ?? 0) + (stats.false ?? 0) >= 4 && (await host.locator('.cp-chip').count()) >= 3) {
|
||||
shot = true;
|
||||
await host.waitForTimeout(800);
|
||||
await host.screenshot({ path: join(SHOTS, 'cockroach-poker-mid-pc.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'cockroach-poker-mid-phone.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'cockroach-poker-mid-phone-full.png'), fullPage: true });
|
||||
await host.emulateMedia({ colorScheme: 'dark' });
|
||||
await guest.emulateMedia({ colorScheme: 'dark' });
|
||||
await host.waitForTimeout(400);
|
||||
await host.screenshot({ path: join(SHOTS, 'cockroach-poker-mid-pc-dark.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'cockroach-poker-mid-phone-dark.png'), fullPage: true });
|
||||
await host.emulateMedia({ colorScheme: 'light' });
|
||||
await guest.emulateMedia({ colorScheme: 'light' });
|
||||
}
|
||||
if (!acted) await host.waitForTimeout(120);
|
||||
}
|
||||
if (!(await over())) throw new Error(`game did not finish (${actions} UI actions: ${JSON.stringify(stats)})`);
|
||||
for (const p of pages) await p.getByRole('dialog', { name: '게임 결과' }).waitFor();
|
||||
await host.waitForTimeout(600);
|
||||
await host.screenshot({ path: join(SHOTS, 'cockroach-poker-result-pc.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'cockroach-poker-result-phone.png') });
|
||||
const calls = (stats.true ?? 0) + (stats.false ?? 0);
|
||||
if (!stats.offer || calls < 3) throw new Error(`too few real turns: ${JSON.stringify(stats)}`);
|
||||
console.log(` cockroach-poker: finished after ${actions} UI actions ${JSON.stringify(Object.fromEntries(Object.entries(stats).filter(([k]) => !k.startsWith('shot'))))}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const games = process.argv.slice(2).length ? process.argv.slice(2) : Object.keys(PLANS);
|
||||
|
||||
Reference in New Issue
Block a user