Merge branch 'm7/ninety-nine' into codex/owner/grp_discord_joke-app
This commit is contained in:
119
e2e/games.e2e.ts
119
e2e/games.e2e.ts
@@ -577,6 +577,70 @@ async function cockroachStep(p: Page, stats: Record<string, number>): Promise<st
|
||||
return null;
|
||||
}
|
||||
|
||||
const nnShots = new Set<string>();
|
||||
/** 구십구: one turn for whoever shows "내 차례예요" — joker via the dial, ± via its buttons, 0, else a number card. */
|
||||
async function ninetyNineStep(p: Page, stats: Record<string, number>): Promise<boolean> {
|
||||
const status = (await p.locator('.turn-status').innerText().catch(() => '')).trim();
|
||||
if (!status.includes('내 차례예요')) return false;
|
||||
const total = Number(/합계 (\d+)/.exec(status)?.[1] ?? 0);
|
||||
const phone = tag(p) === 'phone';
|
||||
const press = async (l: ReturnType<Page['locator']>) => (phone ? l.tap() : l.click());
|
||||
const cards = p.locator('button.nn-card[data-legal="true"]');
|
||||
const n = await cards.count();
|
||||
if (!n) return false;
|
||||
const info: { i: number; label: string; prev: number }[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const label = (await cards.nth(i).getAttribute('aria-label')) ?? '';
|
||||
const prev = Number(/→ (\d+)/.exec(await cards.nth(i).locator('.nn-prev').innerText())?.[1] ?? -1);
|
||||
info.push({ i, label, prev });
|
||||
}
|
||||
const joker = info.find((c) => c.label.startsWith('조커'));
|
||||
const pm = info.find((c) => c.label.startsWith('±'));
|
||||
const zero = info.find((c) => c.label.startsWith('0 카드'));
|
||||
const nums = info.filter((c) => c.prev >= 0 && !c.label.startsWith('0 카드')).sort((a, b) => a.prev - b.prev);
|
||||
if (joker && (total >= 40 || !nums.length)) {
|
||||
await press(cards.nth(joker.i));
|
||||
await press(p.getByRole('button', { name: '99', exact: true }));
|
||||
if (!nnShots.has(`joker-${tag(p)}`)) {
|
||||
nnShots.add(`joker-${tag(p)}`);
|
||||
await p.screenshot({ path: join(SHOTS, `ninety-nine-joker-${tag(p)}.png`) });
|
||||
}
|
||||
await press(p.getByRole('button', { name: '1 작게' }));
|
||||
await press(p.getByRole('button', { name: /^조커 내기 → 98$/ }));
|
||||
stats.joker = (stats.joker ?? 0) + 1;
|
||||
} else if (pm) {
|
||||
await press(cards.nth(pm.i));
|
||||
if (!nnShots.has(`pm-${tag(p)}`)) {
|
||||
nnShots.add(`pm-${tag(p)}`);
|
||||
await p.screenshot({ path: join(SHOTS, `ninety-nine-plusminus-${tag(p)}.png`) });
|
||||
}
|
||||
const plus = p.locator('.nn-pick button', { hasText: /^\+/ });
|
||||
const minus = p.locator('.nn-pick button', { hasText: /^−/ });
|
||||
const usePlus = (await plus.isEnabled()) && (total < 70 || !(await minus.isEnabled()));
|
||||
await press(usePlus ? plus : minus);
|
||||
stats[usePlus ? 'plus' : 'minus'] = (stats[usePlus ? 'plus' : 'minus'] ?? 0) + 1;
|
||||
} else if (zero && (total >= 60 || !nums.length)) {
|
||||
await press(cards.nth(zero.i));
|
||||
await press(cards.nth(zero.i));
|
||||
stats.reverse = (stats.reverse ?? 0) + 1;
|
||||
} else {
|
||||
// Small cards while the total is low (longer game, more cards seen), then the biggest one.
|
||||
const pick = total < 55 ? nums[0]! : nums[nums.length - 1]!;
|
||||
const card = cards.nth(pick.i);
|
||||
if (phone) {
|
||||
await card.tap();
|
||||
await card.tap();
|
||||
} else {
|
||||
await card.focus();
|
||||
await p.keyboard.press('Enter');
|
||||
await p.keyboard.press('Enter');
|
||||
}
|
||||
stats.number = (stats.number ?? 0) + 1;
|
||||
}
|
||||
await p.waitForFunction((s) => document.querySelector('.turn-status')?.textContent?.trim() !== s, status, { timeout: 5000 }).catch(() => {});
|
||||
return true;
|
||||
}
|
||||
|
||||
const PLANS: Record<string, Plan> = {
|
||||
rummikub: {
|
||||
// PC host + phone guest, first meld "없음" so hint plays happen early; 10 real turns through the UI.
|
||||
@@ -1487,6 +1551,61 @@ const PLANS: Record<string, Plan> = {
|
||||
console.log(` cockroach-poker: finished after ${actions} UI actions ${JSON.stringify(Object.fromEntries(Object.entries(stats).filter(([k]) => !k.startsWith('shot'))))}`);
|
||||
},
|
||||
},
|
||||
'ninety-nine': {
|
||||
// PC host + 2 phones, 전용 카드 with 6 of each special card and 30-second turns. Real turns through the UI until the
|
||||
// game ends: joker via the number dial (quick 99, then −1), ± cards via their +/− buttons, 0 to turn the direction,
|
||||
// otherwise a number card (PC: keyboard Enter twice, phones: tap twice). Players run out of cards and are eliminated.
|
||||
players: 3,
|
||||
lobby: async (host) => {
|
||||
await host.getByRole('group', { name: '한 턴 제한 시간' }).getByRole('button', { name: '30초' }).click();
|
||||
await host.getByRole('button', { name: '▼ 자세한 설정' }).click();
|
||||
await host.getByRole('group', { name: '특수 카드 장수' }).getByRole('button', { name: '6장' }).click();
|
||||
await host.getByRole('button', { name: '설정 저장' }).click();
|
||||
await host.getByText(/^전용 카드 · 손패 3장 · 목숨 없음 · 30초/).first().waitFor();
|
||||
},
|
||||
drive: async (pages) => {
|
||||
const [host, guest] = pages as [Page, Page, Page];
|
||||
const stats: Record<string, number> = {};
|
||||
let turns = 0;
|
||||
let shot = false;
|
||||
const over = () => host.getByRole('dialog', { name: '게임 결과' }).count();
|
||||
const until = Date.now() + 240_000;
|
||||
while (Date.now() < until && !(await over())) {
|
||||
let acted = false;
|
||||
for (const p of pages) {
|
||||
if (await ninetyNineStep(p, stats)) {
|
||||
acted = true;
|
||||
turns++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!shot && turns >= 5) {
|
||||
shot = true;
|
||||
await host.waitForTimeout(500);
|
||||
await host.screenshot({ path: join(SHOTS, 'ninety-nine-mid-pc.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'ninety-nine-mid-phone.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'ninety-nine-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, 'ninety-nine-mid-pc-dark.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'ninety-nine-mid-phone-dark.png') });
|
||||
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 (${turns} turns: ${JSON.stringify(stats)})`);
|
||||
const outs = await host.locator('.nn-log li', { hasText: '탈락했어요' }).count();
|
||||
await guest.getByRole('dialog', { name: '게임 결과' }).waitFor();
|
||||
await host.waitForTimeout(600);
|
||||
await host.screenshot({ path: join(SHOTS, 'ninety-nine-result-pc.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'ninety-nine-result-phone.png') });
|
||||
const specials = (stats.joker ?? 0) + (stats.plus ?? 0) + (stats.minus ?? 0) + (stats.reverse ?? 0);
|
||||
if (turns < 6 || specials < 1 || outs < 1) throw new Error(`too few real turns / special cards / eliminations: ${turns} ${JSON.stringify(stats)} outs=${outs}`);
|
||||
console.log(` ninety-nine: finished after ${turns} UI turns ${JSON.stringify(stats)}, ${outs} eliminations in the log`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const games = process.argv.slice(2).length ? process.argv.slice(2) : Object.keys(PLANS);
|
||||
|
||||
Reference in New Issue
Block a user