Merge branch 'm5/marble' 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:
@@ -203,6 +203,48 @@ async function gostopStep(p: Page, state: { go: number; stop: number; picks: num
|
||||
if (await lifted.count()) await lifted.click({ timeout: 1500 });
|
||||
state.plays++;
|
||||
return 'play';
|
||||
/**
|
||||
* 주사위 땅따먹기: one UI step for whoever can act on `p` — answer a trade, sell, buy/build (or skip),
|
||||
* pick a space-travel square on the board, escape the island or roll. Returns the label pressed.
|
||||
*/
|
||||
const tag = (p: Page) => ((p.viewportSize()?.width ?? 0) < 600 ? 'phone' : 'pc');
|
||||
async function marbleStep(p: Page, stats: Record<string, number>): Promise<string | null> {
|
||||
const prompt = p.locator('.mb-prompt.mine');
|
||||
if (!(await prompt.count())) return null;
|
||||
const btn = (name: string | RegExp) => prompt.getByRole('button', { name, exact: typeof name === 'string' });
|
||||
const press = async (name: string | RegExp) => {
|
||||
const b = btn(name);
|
||||
if (!(await b.count()) || !(await b.first().isEnabled().catch(() => false))) return false;
|
||||
return b.first().click({ timeout: 1500 }).then(() => true, () => false);
|
||||
};
|
||||
// Space travel: tap a glowing square on the board, check the preview, fly there.
|
||||
if (await btn(/^출발로 가기/).count()) {
|
||||
const cell = p.locator('g.mb-cell.pick').nth(5 + (stats.space ?? 0) * 7);
|
||||
await cell.click({ timeout: 1500 }).catch(() => {});
|
||||
const go = p.getByRole('button', { name: '🚀 여기로 가기' });
|
||||
if (await go.waitFor({ timeout: 1500 }).then(() => true, () => false)) {
|
||||
await p.getByText('여기로 가면').waitFor({ timeout: 1000 });
|
||||
if (!stats.space) await p.screenshot({ path: join(SHOTS, `marble-space-${tag(p)}.png`) });
|
||||
await go.click();
|
||||
stats.space = (stats.space ?? 0) + 1;
|
||||
return 'space';
|
||||
}
|
||||
return (await press(/^출발로 가기/)) ? 'space-start' : null;
|
||||
}
|
||||
// First buy prompt on each screen size, for the visual check.
|
||||
const shotKey = `shot-${tag(p)}`;
|
||||
if (!stats[shotKey] && (await btn('사기').count())) {
|
||||
stats[shotKey] = 1;
|
||||
await p.screenshot({ path: join(SHOTS, `marble-buy-${tag(p)}.png`) });
|
||||
}
|
||||
for (const name of ['거절', /^가장 싼 것부터 팔기/, '사기', '짓기', '인수하기', '🎲 탈출 굴리기', '🎲 주사위 굴리기', '🎲 한 번 더 굴리기', '안 사기', '안 짓기', '안 하기'] as const) {
|
||||
if (await press(name)) {
|
||||
const key = typeof name === 'string' ? name : 'sell';
|
||||
stats[key] = (stats[key] ?? 0) + 1;
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const PLANS: Record<string, Plan> = {
|
||||
@@ -394,6 +436,7 @@ const PLANS: Record<string, Plan> = {
|
||||
await guest.screenshot({ path: join(SHOTS, 'yut-mid-phone-dark.png') });
|
||||
await guest.emulateMedia({ colorScheme: 'light' });
|
||||
}
|
||||
if (acted && !stats.toll && (await host.locator('.mb-log li', { hasText: /→.*통행료/ }).count())) stats.toll = 1;
|
||||
if (!acted) await host.waitForTimeout(120);
|
||||
}
|
||||
if (!(await over())) throw new Error(`game did not finish (${actions} UI actions)`);
|
||||
@@ -401,6 +444,59 @@ const PLANS: Record<string, Plan> = {
|
||||
console.log(` yut: finished after ${actions} UI actions`);
|
||||
},
|
||||
},
|
||||
marble: {
|
||||
// PC host + phone guest, 15 rounds (shortest limit) played through the UI to the result: roll, buy or
|
||||
// build (skip when short), pay tolls, sell when short, space travel via a board square. Mid-game shots.
|
||||
lobby: async (host) => {
|
||||
await host.getByRole('group', { name: '라운드 제한' }).getByRole('button', { name: '15라운드' }).click();
|
||||
await host.getByRole('group', { name: '시작 포인트' }).getByRole('button', { name: '200P' }).click();
|
||||
await host.getByRole('button', { name: '설정 저장' }).click();
|
||||
await host.getByText(/^15라운드 · 시작 200P/).first().waitFor();
|
||||
},
|
||||
drive: async (pages) => {
|
||||
const [host, guest] = pages as [Page, Page];
|
||||
const stats: Record<string, number> = {};
|
||||
let actions = 0;
|
||||
let shot = false;
|
||||
const until = Date.now() + 420_000;
|
||||
const over = () => host.getByRole('dialog', { name: '게임 결과' }).count();
|
||||
while (Date.now() < until && !(await over())) {
|
||||
let acted = false;
|
||||
for (const p of pages) {
|
||||
if (await marbleStep(p, stats)) {
|
||||
acted = true;
|
||||
actions++;
|
||||
await p.waitForTimeout(80);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (actions >= 40 && !shot && (stats['사기'] ?? 0) >= 3) {
|
||||
shot = true;
|
||||
await host.waitForTimeout(1300);
|
||||
await host.screenshot({ path: join(SHOTS, 'marble-mid-pc.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'marble-mid-phone.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'marble-mid-phone-full.png'), fullPage: true });
|
||||
await host.emulateMedia({ colorScheme: 'dark' });
|
||||
await guest.emulateMedia({ colorScheme: 'dark' });
|
||||
await host.waitForTimeout(400); // let the button colour transitions settle
|
||||
await host.screenshot({ path: join(SHOTS, 'marble-mid-pc-dark.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'marble-mid-phone-dark.png') });
|
||||
await host.emulateMedia({ colorScheme: 'light' });
|
||||
await guest.emulateMedia({ colorScheme: 'light' });
|
||||
}
|
||||
if (acted && !stats.toll && (await host.locator('.mb-log li', { hasText: /→.*통행료/ }).count())) stats.toll = 1;
|
||||
if (!acted) await host.waitForTimeout(120);
|
||||
}
|
||||
if (!(await over())) throw new Error(`game did not finish (${actions} UI actions: ${JSON.stringify(stats)})`);
|
||||
await guest.getByRole('dialog', { name: '게임 결과' }).waitFor();
|
||||
await host.waitForTimeout(600); // result dialog fade-in
|
||||
await host.screenshot({ path: join(SHOTS, 'marble-result-pc.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'marble-result-phone.png') });
|
||||
const rolls = (stats['🎲 주사위 굴리기'] ?? 0) + (stats['🎲 한 번 더 굴리기'] ?? 0) + (stats['🎲 탈출 굴리기'] ?? 0);
|
||||
if (rolls < 15 || !stats['사기']) throw new Error(`too few real turns: ${JSON.stringify(stats)}`);
|
||||
console.log(` marble: finished after ${actions} UI actions ${JSON.stringify(stats)}`);
|
||||
},
|
||||
},
|
||||
'one-card': {
|
||||
// 3 players, default 원카드: whoever has the turn plays a highlighted card (tap to lift, tap again to play,
|
||||
// pick a suit for 7) or draws; the 원카드 button is pressed whenever it lights up. 60 turns or until the game ends.
|
||||
|
||||
Reference in New Issue
Block a user