Merge branch 'm7/cockroach-poker' into codex/owner/grp_discord_joke-app

This commit is contained in:
EJClaw
2026-10-05 14:56:12 +09:00
8 changed files with 2422 additions and 0 deletions

View File

@@ -528,6 +528,55 @@ async function lasVegasStep(
}
}
/**
* 바퀴벌레 포커: 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.
@@ -1383,6 +1432,61 @@ const PLANS: Record<string, Plan> = {
console.log(` las-vegas: ${st.rolls} rolls, ${st.places} placements, tie cancellation seen`);
},
},
'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);