|
|
|
@@ -27,8 +27,6 @@ interface Plan {
|
|
|
|
moves?: string[][];
|
|
|
|
moves?: string[][];
|
|
|
|
/** Card games: custom driver over all pages (host first). */
|
|
|
|
/** Card games: custom driver over all pages (host first). */
|
|
|
|
drive?: (pages: Page[]) => Promise<void>;
|
|
|
|
drive?: (pages: Page[]) => Promise<void>;
|
|
|
|
/** Host changes game options in the lobby before starting. */
|
|
|
|
|
|
|
|
lobby?: (host: Page) => Promise<void>;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Card games: whoever has the turn presses the first available safe action (check/call/…) until `rounds` actions were taken. */
|
|
|
|
/** Card games: whoever has the turn presses the first available safe action (check/call/…) until `rounds` actions were taken. */
|
|
|
|
@@ -57,71 +55,6 @@ async function finishHand(pages: Page[]) {
|
|
|
|
for (const p of pages) await p.getByText('이번 판 결과').first().waitFor({ timeout: 10_000 });
|
|
|
|
for (const p of pages) await p.getByText('이번 판 결과').first().waitFor({ timeout: 10_000 });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 윷놀이: one UI step for whoever can act on `p` — press the throw button, or pick a result chip →
|
|
|
|
|
|
|
|
* a glowing piece (or the waiting-piece tray) → "이동하기" (the two-step move confirmation).
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
async function yutStep(p: Page): Promise<boolean> {
|
|
|
|
|
|
|
|
const throwBtn = p.getByRole('button', { name: /윷 던지기|한 번 더 던지기/ });
|
|
|
|
|
|
|
|
if ((await throwBtn.count()) && (await throwBtn.isEnabled().catch(() => false))) {
|
|
|
|
|
|
|
|
return throwBtn.click({ timeout: 1500 }).then(() => true, () => false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const chip = p.locator('.yut-chip:not([disabled])').first();
|
|
|
|
|
|
|
|
if (!(await chip.count())) return false;
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await chip.click({ timeout: 1500 });
|
|
|
|
|
|
|
|
const src = p.locator('g.yut-hit[tabindex="0"], .yut-tray-btn.can').first();
|
|
|
|
|
|
|
|
await src.waitFor({ timeout: 2000 });
|
|
|
|
|
|
|
|
// Keyboard: Enter on a piece shows where it lands, Enter again on the same piece moves it.
|
|
|
|
|
|
|
|
const h = await src.elementHandle({ timeout: 1000 });
|
|
|
|
|
|
|
|
await h!.focus();
|
|
|
|
|
|
|
|
await p.keyboard.press('Enter');
|
|
|
|
|
|
|
|
await p.getByRole('button', { name: /^이동하기/ }).waitFor({ timeout: 1500 });
|
|
|
|
|
|
|
|
await h!.focus();
|
|
|
|
|
|
|
|
await p.keyboard.press('Enter');
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const PLANS: Record<string, Plan> = {
|
|
|
|
|
|
|
|
yut: {
|
|
|
|
|
|
|
|
// 말 2개로 줄여서 PC 방장 + 휴대폰 친구가 실제 화면 조작으로 끝까지 둔다(중간 화면도 저장).
|
|
|
|
|
|
|
|
lobby: async (host) => {
|
|
|
|
|
|
|
|
await host.getByRole('button', { name: '2개', exact: true }).click();
|
|
|
|
|
|
|
|
await host.getByRole('button', { name: '설정 저장' }).click();
|
|
|
|
|
|
|
|
await host.getByText(/말 2개/).first().waitFor();
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
drive: async (pages) => {
|
|
|
|
|
|
|
|
const [host, guest] = pages as [Page, Page];
|
|
|
|
|
|
|
|
let actions = 0;
|
|
|
|
|
|
|
|
let shot = false;
|
|
|
|
|
|
|
|
const until = Date.now() + 300_000;
|
|
|
|
|
|
|
|
const over = () => host.getByText(/이겼어요|아쉽게 졌어요/).count();
|
|
|
|
|
|
|
|
while (Date.now() < until && !(await over())) {
|
|
|
|
|
|
|
|
let acted = false;
|
|
|
|
|
|
|
|
for (const p of pages) {
|
|
|
|
|
|
|
|
if (await yutStep(p)) {
|
|
|
|
|
|
|
|
acted = true;
|
|
|
|
|
|
|
|
actions++;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (actions >= 16 && !shot) {
|
|
|
|
|
|
|
|
shot = true;
|
|
|
|
|
|
|
|
await host.waitForTimeout(400);
|
|
|
|
|
|
|
|
await host.screenshot({ path: join(SHOTS, 'yut-mid-pc.png') });
|
|
|
|
|
|
|
|
await guest.screenshot({ path: join(SHOTS, 'yut-mid-phone.png'), fullPage: true });
|
|
|
|
|
|
|
|
await guest.emulateMedia({ colorScheme: 'dark' });
|
|
|
|
|
|
|
|
await guest.screenshot({ path: join(SHOTS, 'yut-mid-phone-dark.png') });
|
|
|
|
|
|
|
|
await guest.emulateMedia({ colorScheme: 'light' });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!acted) await host.waitForTimeout(120);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!(await over())) throw new Error(`game did not finish (${actions} UI actions)`);
|
|
|
|
|
|
|
|
await guest.getByText(/이겼어요|아쉽게 졌어요/).waitFor();
|
|
|
|
|
|
|
|
console.log(` yut: finished after ${actions} UI actions`);
|
|
|
|
|
|
|
|
/** 숫자 타일: whoever has the turn tries "도와줘" → "새 묶음" → "확정"; otherwise takes one tile. */
|
|
|
|
/** 숫자 타일: whoever has the turn tries "도와줘" → "새 묶음" → "확정"; otherwise takes one tile. */
|
|
|
|
async function rummikubTurns(pages: Page[], turns: number) {
|
|
|
|
async function rummikubTurns(pages: Page[], turns: number) {
|
|
|
|
const stats = { plays: 0, draws: 0, undo: false };
|
|
|
|
const stats = { plays: 0, draws: 0, undo: false };
|
|
|
|
@@ -189,6 +122,34 @@ async function rummikubTurns(pages: Page[], turns: number) {
|
|
|
|
return stats;
|
|
|
|
return stats;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* 윷놀이: one UI step for whoever can act on `p` — press the throw button, or pick a result chip →
|
|
|
|
|
|
|
|
* a glowing piece (or the waiting-piece tray) → "이동하기" (the two-step move confirmation).
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
async function yutStep(p: Page): Promise<boolean> {
|
|
|
|
|
|
|
|
const throwBtn = p.getByRole('button', { name: /윷 던지기|한 번 더 던지기/ });
|
|
|
|
|
|
|
|
if ((await throwBtn.count()) && (await throwBtn.isEnabled().catch(() => false))) {
|
|
|
|
|
|
|
|
return throwBtn.click({ timeout: 1500 }).then(() => true, () => false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const chip = p.locator('.yut-chip:not([disabled])').first();
|
|
|
|
|
|
|
|
if (!(await chip.count())) return false;
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await chip.click({ timeout: 1500 });
|
|
|
|
|
|
|
|
const src = p.locator('g.yut-hit[tabindex="0"], .yut-tray-btn.can').first();
|
|
|
|
|
|
|
|
await src.waitFor({ timeout: 2000 });
|
|
|
|
|
|
|
|
// Keyboard: Enter on a piece shows where it lands, Enter again on the same piece moves it.
|
|
|
|
|
|
|
|
const h = await src.elementHandle({ timeout: 1000 });
|
|
|
|
|
|
|
|
await h!.focus();
|
|
|
|
|
|
|
|
await p.keyboard.press('Enter');
|
|
|
|
|
|
|
|
await p.getByRole('button', { name: /^이동하기/ }).waitFor({ timeout: 1500 });
|
|
|
|
|
|
|
|
await h!.focus();
|
|
|
|
|
|
|
|
await p.keyboard.press('Enter');
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const PLANS: Record<string, Plan> = {
|
|
|
|
const PLANS: Record<string, Plan> = {
|
|
|
|
rummikub: {
|
|
|
|
rummikub: {
|
|
|
|
// PC host + phone guest, first meld "없음" so hint plays happen early; 10 real turns through the UI.
|
|
|
|
// PC host + phone guest, first meld "없음" so hint plays happen early; 10 real turns through the UI.
|
|
|
|
@@ -202,27 +163,126 @@ const PLANS: Record<string, Plan> = {
|
|
|
|
if (stats.plays + stats.draws < 10) throw new Error('not enough turns');
|
|
|
|
if (stats.plays + stats.draws < 10) throw new Error('not enough turns');
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
seotda: {
|
|
|
|
yut: {
|
|
|
|
// 3 players, default 2장 섯다 (1장씩 두 번): everyone checks both betting rounds → showdown.
|
|
|
|
// 말 2개로 줄여서 PC 방장 + 휴대폰 친구가 실제 화면 조작으로 끝까지 둔다(중간 화면도 저장).
|
|
|
|
|
|
|
|
lobby: async (host) => {
|
|
|
|
|
|
|
|
await host.getByRole('button', { name: '2개', exact: true }).click();
|
|
|
|
|
|
|
|
await host.getByRole('button', { name: '설정 저장' }).click();
|
|
|
|
|
|
|
|
await host.getByText(/말 2개/).first().waitFor();
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
drive: async (pages) => {
|
|
|
|
|
|
|
|
const [host, guest] = pages as [Page, Page];
|
|
|
|
|
|
|
|
let actions = 0;
|
|
|
|
|
|
|
|
let shot = false;
|
|
|
|
|
|
|
|
const until = Date.now() + 300_000;
|
|
|
|
|
|
|
|
const over = () => host.getByText(/이겼어요|아쉽게 졌어요/).count();
|
|
|
|
|
|
|
|
while (Date.now() < until && !(await over())) {
|
|
|
|
|
|
|
|
let acted = false;
|
|
|
|
|
|
|
|
for (const p of pages) {
|
|
|
|
|
|
|
|
if (await yutStep(p)) {
|
|
|
|
|
|
|
|
acted = true;
|
|
|
|
|
|
|
|
actions++;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (actions >= 16 && !shot) {
|
|
|
|
|
|
|
|
shot = true;
|
|
|
|
|
|
|
|
await host.waitForTimeout(400);
|
|
|
|
|
|
|
|
await host.screenshot({ path: join(SHOTS, 'yut-mid-pc.png') });
|
|
|
|
|
|
|
|
await guest.screenshot({ path: join(SHOTS, 'yut-mid-phone.png'), fullPage: true });
|
|
|
|
|
|
|
|
await guest.emulateMedia({ colorScheme: 'dark' });
|
|
|
|
|
|
|
|
await guest.screenshot({ path: join(SHOTS, 'yut-mid-phone-dark.png') });
|
|
|
|
|
|
|
|
await guest.emulateMedia({ colorScheme: 'light' });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!acted) await host.waitForTimeout(120);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!(await over())) throw new Error(`game did not finish (${actions} UI actions)`);
|
|
|
|
|
|
|
|
await guest.getByText(/이겼어요|아쉽게 졌어요/).waitFor();
|
|
|
|
|
|
|
|
console.log(` yut: finished after ${actions} UI actions`);
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
'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.
|
|
|
|
players: 3,
|
|
|
|
players: 3,
|
|
|
|
drive: async (pages) => {
|
|
|
|
drive: async (pages) => {
|
|
|
|
await pressActions(pages, /^(체크|콜)/, 6);
|
|
|
|
const statusOf = (p: Page) => p.locator('.turn-status').innerText().catch(() => '');
|
|
|
|
await finishHand(pages);
|
|
|
|
let turns = 0;
|
|
|
|
|
|
|
|
let plays = 0;
|
|
|
|
|
|
|
|
let draws = 0;
|
|
|
|
|
|
|
|
let finished = false;
|
|
|
|
|
|
|
|
for (let t = 0; t < 600 && turns < 60; t++) {
|
|
|
|
|
|
|
|
if (await pages[0]!.getByRole('dialog', { name: '게임 결과' }).count()) {
|
|
|
|
|
|
|
|
finished = true;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
let acted = false;
|
|
|
|
|
|
|
|
for (const p of pages) {
|
|
|
|
|
|
|
|
const one = p.locator('.oc-onebtn');
|
|
|
|
|
|
|
|
if ((await one.count()) && (await one.isEnabled().catch(() => false))) await one.click({ timeout: 1000 }).catch(() => {});
|
|
|
|
|
|
|
|
const before = await statusOf(p);
|
|
|
|
|
|
|
|
if (!before.includes('내 차례예요')) continue;
|
|
|
|
|
|
|
|
const accept = p.getByRole('button', { name: /^받아들이기/ });
|
|
|
|
|
|
|
|
const card = p.locator('button.oc-card[data-legal="true"]').first();
|
|
|
|
|
|
|
|
if (await accept.count()) await accept.click();
|
|
|
|
|
|
|
|
else if (await card.count()) {
|
|
|
|
|
|
|
|
await card.click();
|
|
|
|
|
|
|
|
await card.click();
|
|
|
|
|
|
|
|
const pick = p.locator('.oc-pick button').first();
|
|
|
|
|
|
|
|
if (await pick.waitFor({ timeout: 400 }).then(() => true, () => false)) await pick.click();
|
|
|
|
|
|
|
|
plays++;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
const pass = p.getByRole('button', { name: '넘기기' });
|
|
|
|
|
|
|
|
if (await pass.count()) await pass.click();
|
|
|
|
|
|
|
|
else await p.getByRole('button', { name: /장 먹기$/ }).first().click();
|
|
|
|
|
|
|
|
draws++;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
await p.waitForFunction((s) => document.querySelector('.turn-status')?.textContent?.trim() !== s, before.trim(), { timeout: 5000 }).catch(() => {});
|
|
|
|
|
|
|
|
turns++;
|
|
|
|
|
|
|
|
acted = true;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!acted) await pages[0]!.waitForTimeout(150);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!finished && turns < 30) throw new Error(`only ${turns} turns were played`);
|
|
|
|
|
|
|
|
if (finished) await pages[0]!.screenshot({ path: join(SHOTS, 'one-card-3-result-pc.png') });
|
|
|
|
|
|
|
|
console.log(` one-card: ${turns} turns (${plays} plays, ${draws} draws)${finished ? ', game finished' : ''}`);
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
poker: {
|
|
|
|
'davinci-code': {
|
|
|
|
// 3 players, default 7포커: 초이스 (discard 1st card, open 2nd), then everyone checks 4 streets → showdown.
|
|
|
|
// 2 players, default options (choose colours): both pick 2+2 start tiles, then several real turns:
|
|
|
|
players: 3,
|
|
|
|
// draw from a pile, tap an opponent's hidden tile, pick a number, confirm; after a hit press "여기서 멈추기".
|
|
|
|
drive: async (pages) => {
|
|
|
|
drive: async (pages) => {
|
|
|
|
for (const p of pages) {
|
|
|
|
for (const p of pages) {
|
|
|
|
const cards = p.locator('.pk-mine button');
|
|
|
|
await p.getByRole('button', { name: '검정 2개 · 흰색 2개' }).click();
|
|
|
|
await cards.first().waitFor();
|
|
|
|
await p.getByRole('button', { name: '이렇게 받기' }).click();
|
|
|
|
await cards.nth(0).click();
|
|
|
|
|
|
|
|
await cards.nth(1).click();
|
|
|
|
|
|
|
|
await p.getByRole('button', { name: '확인', exact: true }).click();
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await pressActions(pages, /^체크/, 12);
|
|
|
|
const myTurn = (p: Page) => p.evaluate(() => !!document.querySelector('.turn-status')?.textContent?.includes('내 차례예요'));
|
|
|
|
await finishHand(pages);
|
|
|
|
for (let turn = 0; turn < 5; turn++) {
|
|
|
|
|
|
|
|
let mover: Page | null = null;
|
|
|
|
|
|
|
|
for (let t = 0; t < 100 && !mover; t++) {
|
|
|
|
|
|
|
|
for (const p of pages) if (!mover && (await myTurn(p))) mover = p;
|
|
|
|
|
|
|
|
if (!mover) await pages[0]!.waitForTimeout(100);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!mover) throw new Error(`turn ${turn + 1}: nobody has the turn`);
|
|
|
|
|
|
|
|
const pile = mover.getByRole('button', { name: turn % 2 ? /^흰색 타일 가져오기/ : /^검은색 타일 가져오기/ });
|
|
|
|
|
|
|
|
await pile.click();
|
|
|
|
|
|
|
|
await mover.getByText('맞힐 상대 타일을 눌러 주세요.').waitFor();
|
|
|
|
|
|
|
|
await mover.locator('.dv-opps .dv-pick.can').nth(turn % 3).click();
|
|
|
|
|
|
|
|
await mover.getByRole('button', { name: String((turn * 5) % 12), exact: true }).click();
|
|
|
|
|
|
|
|
if (turn < 2) await mover.screenshot({ path: join(SHOTS, `davinci-code-3-pad-${mover === pages[0] ? 'pc' : 'phone'}.png`), fullPage: true });
|
|
|
|
|
|
|
|
await mover.getByRole('button', { name: /이 숫자!$/ }).click();
|
|
|
|
|
|
|
|
// A hit → stop (the drawn tile goes in hidden); a miss ends the turn by itself.
|
|
|
|
|
|
|
|
for (let t = 0; t < 60; t++) {
|
|
|
|
|
|
|
|
const stop = mover.getByRole('button', { name: '여기서 멈추기' });
|
|
|
|
|
|
|
|
if (await stop.isVisible().catch(() => false)) await stop.click().catch(() => {});
|
|
|
|
|
|
|
|
if (!(await myTurn(mover))) break;
|
|
|
|
|
|
|
|
await mover.waitForTimeout(100);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (await myTurn(mover)) throw new Error(`turn ${turn + 1}: turn did not pass`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Both screens show the guess log with every guess.
|
|
|
|
|
|
|
|
for (const p of pages) await p.getByText('추리 기록 (5)').waitFor();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
yacht: {
|
|
|
|
yacht: {
|
|
|
|
@@ -273,86 +333,33 @@ const PLANS: Record<string, Plan> = {
|
|
|
|
for (const p of pages) await p.getByRole('dialog', { name: '게임 결과' }).waitFor({ timeout: 10_000 });
|
|
|
|
for (const p of pages) await p.getByRole('dialog', { name: '게임 결과' }).waitFor({ timeout: 10_000 });
|
|
|
|
const sheet = await pages[0]!.locator('.yt-table tr.yt-total').innerText();
|
|
|
|
const sheet = await pages[0]!.locator('.yt-table tr.yt-total').innerText();
|
|
|
|
if (!/\d/.test(sheet)) throw new Error('no totals on the score sheet');
|
|
|
|
if (!/\d/.test(sheet)) throw new Error('no totals on the score sheet');
|
|
|
|
'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.
|
|
|
|
seotda: {
|
|
|
|
|
|
|
|
// 3 players, default 2장 섯다 (1장씩 두 번): everyone checks both betting rounds → showdown.
|
|
|
|
|
|
|
|
// A tie (or 구사) starts a 재경기 with the same pot, so keep checking until the hand result shows up.
|
|
|
|
players: 3,
|
|
|
|
players: 3,
|
|
|
|
drive: async (pages) => {
|
|
|
|
drive: async (pages) => {
|
|
|
|
const statusOf = (p: Page) => p.locator('.turn-status').innerText().catch(() => '');
|
|
|
|
await pressActions(pages, /^(체크|콜)/, 6);
|
|
|
|
let turns = 0;
|
|
|
|
for (let extra = 0; extra < 30 && !(await pages[0]!.getByText('이번 판 결과').count()); extra++) {
|
|
|
|
let plays = 0;
|
|
|
|
await pressActions(pages, /^(체크|콜)/, 1).catch(() => {});
|
|
|
|
let draws = 0;
|
|
|
|
|
|
|
|
let finished = false;
|
|
|
|
|
|
|
|
for (let t = 0; t < 600 && turns < 60; t++) {
|
|
|
|
|
|
|
|
if (await pages[0]!.getByRole('dialog', { name: '게임 결과' }).count()) {
|
|
|
|
|
|
|
|
finished = true;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let acted = false;
|
|
|
|
await finishHand(pages);
|
|
|
|
for (const p of pages) {
|
|
|
|
},
|
|
|
|
const one = p.locator('.oc-onebtn');
|
|
|
|
},
|
|
|
|
if ((await one.count()) && (await one.isEnabled().catch(() => false))) await one.click({ timeout: 1000 }).catch(() => {});
|
|
|
|
poker: {
|
|
|
|
const before = await statusOf(p);
|
|
|
|
// 3 players, default 7포커: 초이스 (discard 1st card, open 2nd), then everyone checks 4 streets → showdown.
|
|
|
|
if (!before.includes('내 차례예요')) continue;
|
|
|
|
players: 3,
|
|
|
|
const accept = p.getByRole('button', { name: /^받아들이기/ });
|
|
|
|
|
|
|
|
const card = p.locator('button.oc-card[data-legal="true"]').first();
|
|
|
|
|
|
|
|
if (await accept.count()) await accept.click();
|
|
|
|
|
|
|
|
else if (await card.count()) {
|
|
|
|
|
|
|
|
await card.click();
|
|
|
|
|
|
|
|
await card.click();
|
|
|
|
|
|
|
|
const pick = p.locator('.oc-pick button').first();
|
|
|
|
|
|
|
|
if (await pick.waitFor({ timeout: 400 }).then(() => true, () => false)) await pick.click();
|
|
|
|
|
|
|
|
plays++;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
const pass = p.getByRole('button', { name: '넘기기' });
|
|
|
|
|
|
|
|
if (await pass.count()) await pass.click();
|
|
|
|
|
|
|
|
else await p.getByRole('button', { name: /장 먹기$/ }).first().click();
|
|
|
|
|
|
|
|
draws++;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
await p.waitForFunction((s) => document.querySelector('.turn-status')?.textContent?.trim() !== s, before.trim(), { timeout: 5000 }).catch(() => {});
|
|
|
|
|
|
|
|
turns++;
|
|
|
|
|
|
|
|
acted = true;
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!acted) await pages[0]!.waitForTimeout(150);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!finished && turns < 30) throw new Error(`only ${turns} turns were played`);
|
|
|
|
|
|
|
|
if (finished) await pages[0]!.screenshot({ path: join(SHOTS, 'one-card-3-result-pc.png') });
|
|
|
|
|
|
|
|
console.log(` one-card: ${turns} turns (${plays} plays, ${draws} draws)${finished ? ', game finished' : ''}`);
|
|
|
|
|
|
|
|
'davinci-code': {
|
|
|
|
|
|
|
|
// 2 players, default options (choose colours): both pick 2+2 start tiles, then several real turns:
|
|
|
|
|
|
|
|
// draw from a pile, tap an opponent's hidden tile, pick a number, confirm; after a hit press "여기서 멈추기".
|
|
|
|
|
|
|
|
drive: async (pages) => {
|
|
|
|
drive: async (pages) => {
|
|
|
|
for (const p of pages) {
|
|
|
|
for (const p of pages) {
|
|
|
|
await p.getByRole('button', { name: '검정 2개 · 흰색 2개' }).click();
|
|
|
|
const cards = p.locator('.pk-mine button');
|
|
|
|
await p.getByRole('button', { name: '이렇게 받기' }).click();
|
|
|
|
await cards.first().waitFor();
|
|
|
|
|
|
|
|
await cards.nth(0).click();
|
|
|
|
|
|
|
|
await cards.nth(1).click();
|
|
|
|
|
|
|
|
await p.getByRole('button', { name: '확인', exact: true }).click();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const myTurn = (p: Page) => p.evaluate(() => !!document.querySelector('.turn-status')?.textContent?.includes('내 차례예요'));
|
|
|
|
await pressActions(pages, /^체크/, 12);
|
|
|
|
for (let turn = 0; turn < 5; turn++) {
|
|
|
|
await finishHand(pages);
|
|
|
|
let mover: Page | null = null;
|
|
|
|
|
|
|
|
for (let t = 0; t < 100 && !mover; t++) {
|
|
|
|
|
|
|
|
for (const p of pages) if (!mover && (await myTurn(p))) mover = p;
|
|
|
|
|
|
|
|
if (!mover) await pages[0]!.waitForTimeout(100);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!mover) throw new Error(`turn ${turn + 1}: nobody has the turn`);
|
|
|
|
|
|
|
|
const pile = mover.getByRole('button', { name: turn % 2 ? /^흰색 타일 가져오기/ : /^검은색 타일 가져오기/ });
|
|
|
|
|
|
|
|
await pile.click();
|
|
|
|
|
|
|
|
await mover.getByText('맞힐 상대 타일을 눌러 주세요.').waitFor();
|
|
|
|
|
|
|
|
await mover.locator('.dv-opps .dv-pick.can').nth(turn % 3).click();
|
|
|
|
|
|
|
|
await mover.getByRole('button', { name: String((turn * 5) % 12), exact: true }).click();
|
|
|
|
|
|
|
|
if (turn < 2) await mover.screenshot({ path: join(SHOTS, `davinci-code-3-pad-${mover === pages[0] ? 'pc' : 'phone'}.png`), fullPage: true });
|
|
|
|
|
|
|
|
await mover.getByRole('button', { name: /이 숫자!$/ }).click();
|
|
|
|
|
|
|
|
// A hit → stop (the drawn tile goes in hidden); a miss ends the turn by itself.
|
|
|
|
|
|
|
|
for (let t = 0; t < 60; t++) {
|
|
|
|
|
|
|
|
const stop = mover.getByRole('button', { name: '여기서 멈추기' });
|
|
|
|
|
|
|
|
if (await stop.isVisible().catch(() => false)) await stop.click().catch(() => {});
|
|
|
|
|
|
|
|
if (!(await myTurn(mover))) break;
|
|
|
|
|
|
|
|
await mover.waitForTimeout(100);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (await myTurn(mover)) throw new Error(`turn ${turn + 1}: turn did not pass`);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Both screens show the guess log with every guess.
|
|
|
|
|
|
|
|
for (const p of pages) await p.getByText('추리 기록 (5)').waitFor();
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
othello: {
|
|
|
|
othello: {
|
|
|
|
|