Merge branch 'm5/bang' into codex/owner/grp_discord_joke-app
# Conflicts: # apps/web/src/games/registry.ts # apps/web/src/lib/catalog-ui.ts # e2e/games.e2e.ts # packages/games/src/index.ts # packages/shared/src/catalog.ts
This commit is contained in:
106
e2e/games.e2e.ts
106
e2e/games.e2e.ts
@@ -424,6 +424,112 @@ const PLANS: Record<string, Plan> = {
|
||||
// The next hand has started: the table and my hand are back on every screen.
|
||||
for (const q of pages) await q.locator('.mj-table').waitFor({ timeout: 10_000 });
|
||||
console.log(` mahjong: ${discards} discards, ${passes} passes, ${wins} wins, ${results} hand finished → next hand started`);
|
||||
bang: {
|
||||
// PC host + 3 phone guests, reaction time 25s: real turns through the UI — draw, play usable cards
|
||||
// (뱅! at a highlighted target → confirm, other untargeted cards by double tap), answer reaction
|
||||
// sheets (빗나감!/맥주/결투/인디언!/잡화점), discard down to the hand limit, end the turn.
|
||||
players: 4,
|
||||
lobby: async (host) => {
|
||||
await host.getByRole('group', { name: '반응 제한 시간' }).getByRole('button', { name: '25초' }).click();
|
||||
await host.getByRole('button', { name: '설정 저장' }).click();
|
||||
await host.getByText(/반응 25초/).first().waitFor();
|
||||
},
|
||||
drive: async (pages) => {
|
||||
const stats = { turns: 0, bangs: 0, plays: 0, reactions: 0, discards: 0 };
|
||||
let finished = false;
|
||||
let shot = false;
|
||||
let midShot = false;
|
||||
const tryClick = (l: ReturnType<Page['locator']>) => l.click({ timeout: 1500 }).then(() => true, () => false);
|
||||
/** One UI step on this page; true when something was pressed. */
|
||||
const step = async (p: Page): Promise<boolean> => {
|
||||
const modal = p.locator('.bg-modal');
|
||||
if (await modal.count()) {
|
||||
const store = modal.locator('button.bg-card');
|
||||
const title = (await modal.locator('h2').innerText().catch(() => '')).trim();
|
||||
if (title.startsWith('3장 중')) {
|
||||
await store.nth(0).click();
|
||||
await store.nth(1).click();
|
||||
await modal.getByRole('button', { name: '이 2장 받기' }).click();
|
||||
} else if (title.startsWith('잡화점')) await store.first().click();
|
||||
else {
|
||||
const use = modal.getByRole('button', { name: /(내기|마시기)$/ });
|
||||
if (!(await use.count()) || !(await tryClick(use.first()))) {
|
||||
if (!(await tryClick(modal.getByRole('button', { name: /^(포기하고 맞기|맞기|쓰러지기)/ }).first()))) return false;
|
||||
}
|
||||
}
|
||||
stats.reactions++;
|
||||
return true;
|
||||
}
|
||||
const draw = p.getByRole('button', { name: '카드 더미에서 2장 받기' });
|
||||
if (await draw.count()) return tryClick(draw);
|
||||
const discardBtn = p.getByRole('button', { name: /장 버리기$/ });
|
||||
if (await discardBtn.count()) {
|
||||
const need = Number((await p.locator('.bg-actions').innerText()).match(/버릴 카드 (\d+)장/)?.[1] ?? 0);
|
||||
const cards = p.locator('.bg-me .bg-hand button.bg-card');
|
||||
for (let i = 0; i < need; i++) await cards.nth(i).click();
|
||||
await discardBtn.click();
|
||||
stats.discards++;
|
||||
return true;
|
||||
}
|
||||
const end = p.getByRole('button', { name: '턴 마치기' });
|
||||
if (!(await end.count()) || !(await end.isEnabled())) return false;
|
||||
// Shoot first (once per turn is enough), then other untargeted cards, then end the turn.
|
||||
const bang = p.locator('.bg-me button.bg-card[data-kind="bang"][data-ok="true"]').first();
|
||||
if (await bang.count()) {
|
||||
await bang.click();
|
||||
const target = p.locator('button.bg-seat[data-target="ok"]').first();
|
||||
await target.waitFor({ timeout: 2000 });
|
||||
await target.click();
|
||||
const fire = p.getByRole('button', { name: /에게 뱅!$/ });
|
||||
await fire.click();
|
||||
stats.bangs++;
|
||||
if (!shot) {
|
||||
shot = true;
|
||||
await p.waitForTimeout(400);
|
||||
for (const [i, q] of pages.entries()) await q.screenshot({ path: join(SHOTS, `bang-3-shot-${i === 0 ? 'pc' : `phone${i}`}.png`) });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const simple = ['beer', 'stagecoach', 'wellsFargo', 'saloon', 'barrel', 'mustang', 'scope', 'schofield', 'remington', 'carabine', 'winchester', 'volcanic', 'indians', 'gatling', 'generalStore'];
|
||||
for (const k of simple) {
|
||||
const c = p.locator(`.bg-me button.bg-card[data-kind="${k}"][data-ok="true"]`).first();
|
||||
if (!(await c.count())) continue;
|
||||
await c.click();
|
||||
await c.click();
|
||||
stats.plays++;
|
||||
return true;
|
||||
}
|
||||
await end.click();
|
||||
stats.turns++;
|
||||
return true;
|
||||
};
|
||||
const until = Date.now() + 240_000;
|
||||
while (Date.now() < until && stats.turns < 10) {
|
||||
if (await pages[0]!.getByRole('dialog', { name: '게임 결과' }).count()) {
|
||||
finished = true;
|
||||
break;
|
||||
}
|
||||
let acted = false;
|
||||
for (const p of pages) {
|
||||
if (await step(p)) {
|
||||
acted = true;
|
||||
await p.waitForTimeout(250);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stats.turns >= 4 && !midShot) {
|
||||
midShot = true;
|
||||
await pages[0]!.screenshot({ path: join(SHOTS, 'bang-4-mid-pc.png') });
|
||||
await pages[1]!.screenshot({ path: join(SHOTS, 'bang-4-mid-phone.png'), fullPage: true });
|
||||
await pages[1]!.emulateMedia({ colorScheme: 'dark' });
|
||||
await pages[1]!.screenshot({ path: join(SHOTS, 'bang-4-mid-phone-dark.png') });
|
||||
await pages[1]!.emulateMedia({ colorScheme: 'light' });
|
||||
}
|
||||
if (!acted) await pages[0]!.waitForTimeout(150);
|
||||
}
|
||||
console.log(` bang: ${stats.turns} turns, ${stats.bangs} 뱅!, ${stats.plays} other cards, ${stats.reactions} reactions, ${stats.discards} discards${finished ? ', game finished' : ''}`);
|
||||
if (!finished && stats.turns < 8) throw new Error(`only ${stats.turns} turns were played`);
|
||||
if (stats.bangs < 1) throw new Error('no 뱅! was played');
|
||||
},
|
||||
},
|
||||
'davinci-code': {
|
||||
|
||||
Reference in New Issue
Block a user