M5: 고스톱·맞고 규칙·화면·테스트
- packages/games/src/gostop: 화투 48장 + 보너스패(cards), 족보·국진·최종 공식·박·독박 정산(scoring), 패 돌리기·바닥 정리·광팔기·총통·먹기 판정(쪽/뻑/따닥/쓸/폭탄/흔들기/보너스패)·피 뺏기·고/스톱·나가리· 첫뻑/연뻑/삼뻑(round), validate·view(남의 손패·더미 숨김, 매칭/점수 미리 계산)·시간 초과·나가기(index). 2인 맞고, 3인 고스톱, 4~5인 광팔기, 여러 판 세트(방 점수 제로섬). - 테스트: 문서 §10 체크리스트 전부, 1,000 시드 무작위 진행(카드·점수 보존, onTimeout 합법), engine randomPlayout(누출·JSON·결정성), 정보 누출 속성 테스트. - 화면 apps/web/src/games/gostop: 월별 바닥, 매칭 하이라이트, 두 장 중 고르기, 흔들기·폭탄·고/스톱·총통· 국진·광팔기 선택, 먹은 패 종류별 묶음과 족보 진행, 이벤트 알림, 판 결과 계산 과정, PC·휴대폰·다크 모드. - HwatuCard: 보너스패(2피·3피) 그림 추가. 게임 목록·레지스트리 등록, 홈 "준비 중"에서 고스톱 제거. - E2E: 맞고 PC 방장 + 휴대폰 친구가 실제 화면 조작으로 고·스톱까지 진행. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
105
e2e/games.e2e.ts
105
e2e/games.e2e.ts
@@ -150,7 +150,112 @@ async function yutStep(p: Page): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 고스톱: one UI step for `p` if it has something to do — answer an open decision sheet (고/스톱: 고 once, then 스톱;
|
||||
* choose a floor card; 흔들기/폭탄; 총통 → 그냥 치기; 국진) or play a hand card (green-bordered match first; phones
|
||||
* use tap-to-lift, so a lifted card is tapped again). Returns what it did, or null.
|
||||
*/
|
||||
async function gostopStep(p: Page, state: { go: number; stop: number; picks: number; plays: number; sheets: number }): Promise<string | null> {
|
||||
const dialog = p.getByRole('dialog');
|
||||
if (await dialog.count()) {
|
||||
const name = (await dialog.first().getAttribute('aria-label')) ?? '';
|
||||
if (name === '게임 결과') return null;
|
||||
if (name === '고 또는 스톱') {
|
||||
const goFirst = state.go === 0;
|
||||
if (goFirst && state.stop === 0) await p.screenshot({ path: join(SHOTS, 'gostop-gostop-dialog.png') });
|
||||
await dialog.getByRole('button', { name: goFirst ? /^고/ : /^스톱/ }).click({ timeout: 1500 });
|
||||
if (goFirst) state.go++;
|
||||
else state.stop++;
|
||||
return goFirst ? 'go' : 'stop';
|
||||
}
|
||||
if (name === '총통') {
|
||||
await dialog.getByRole('button', { name: /^그냥 치기/ }).click({ timeout: 1500 });
|
||||
return 'chongtong';
|
||||
}
|
||||
const pick = dialog.locator('.gs-pick button, .gs-big .btn').first();
|
||||
if (await pick.count()) {
|
||||
if (state.sheets === 0) await p.screenshot({ path: join(SHOTS, 'gostop-choice-dialog.png') });
|
||||
await pick.click({ timeout: 1500 });
|
||||
state.sheets++;
|
||||
if (name.includes('고르기')) state.picks++;
|
||||
return name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const status = (await p.locator('.turn-status').innerText().catch(() => '')).trim();
|
||||
if (!status.includes('내 차례예요')) return null;
|
||||
const card = p.locator('.gs-hcard[data-match="true"]:not([disabled])').first();
|
||||
const any = p.locator('.gs-hcard:not([disabled])').first();
|
||||
const target = (await card.count()) ? card : any;
|
||||
if (!(await target.count())) {
|
||||
const dummy = p.getByRole('button', { name: /^빈패 내기/ });
|
||||
if (await dummy.isEnabled().catch(() => false)) {
|
||||
await dummy.click({ timeout: 1500 });
|
||||
state.plays++;
|
||||
return 'dummy';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const label = await target.getAttribute('aria-label');
|
||||
await target.click({ timeout: 1500 });
|
||||
const lifted = p.locator(`.gs-hcard[data-lifted="true"][aria-label="${label}"]`);
|
||||
if (await lifted.count()) await lifted.click({ timeout: 1500 });
|
||||
state.plays++;
|
||||
return 'play';
|
||||
}
|
||||
|
||||
const PLANS: Record<string, Plan> = {
|
||||
gostop: {
|
||||
// 맞고: PC host + phone guest, 1판. Real turns through the UI — hand cards (tap twice on the phone), floor-card choices,
|
||||
// 흔들기/폭탄 sheets, one 고 and then 스톱 — until the hand ends (스톱 or 나가리) and the game result shows.
|
||||
lobby: async (host) => {
|
||||
await host.getByRole('group', { name: '판 수' }).getByRole('button', { name: '1판', exact: true }).click();
|
||||
await host.getByRole('button', { name: '설정 저장' }).click();
|
||||
await host.getByText(/^1판 · /).first().waitFor();
|
||||
},
|
||||
drive: async (pages) => {
|
||||
const [host, guest] = pages as [Page, Page];
|
||||
const state = { go: 0, stop: 0, picks: 0, plays: 0, sheets: 0 };
|
||||
let shot = false;
|
||||
const until = Date.now() + 240_000;
|
||||
const over = async () => (await host.getByRole('dialog', { name: '게임 결과' }).count()) > 0;
|
||||
while (Date.now() < until && !(await over())) {
|
||||
let acted = false;
|
||||
for (const p of pages) {
|
||||
const did = await gostopStep(p, state).catch(() => null);
|
||||
if (did) {
|
||||
acted = true;
|
||||
await p.waitForTimeout(250);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (state.plays >= 8 && !shot) {
|
||||
shot = true;
|
||||
await host.waitForTimeout(1500);
|
||||
await host.screenshot({ path: join(SHOTS, 'gostop-mid-pc.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'gostop-mid-phone.png'), fullPage: true });
|
||||
await guest.emulateMedia({ colorScheme: 'dark' });
|
||||
await host.emulateMedia({ colorScheme: 'dark' });
|
||||
await guest.screenshot({ path: join(SHOTS, 'gostop-mid-phone-dark.png'), fullPage: true });
|
||||
await host.screenshot({ path: join(SHOTS, 'gostop-mid-pc-dark.png') });
|
||||
await guest.emulateMedia({ colorScheme: 'light' });
|
||||
await host.emulateMedia({ colorScheme: 'light' });
|
||||
}
|
||||
if (!acted) await host.waitForTimeout(150);
|
||||
}
|
||||
if (!(await over())) throw new Error(`hand did not end (${JSON.stringify(state)})`);
|
||||
await guest.getByRole('dialog', { name: '게임 결과' }).waitFor();
|
||||
await host.screenshot({ path: join(SHOTS, 'gostop-result-pc.png') });
|
||||
await guest.screenshot({ path: join(SHOTS, 'gostop-result-phone.png') });
|
||||
// Close the result sheet to see the round breakdown underneath.
|
||||
await host.keyboard.press('Escape');
|
||||
await host.getByText(/1판 결과/).first().waitFor();
|
||||
await host.screenshot({ path: join(SHOTS, 'gostop-breakdown-pc.png') });
|
||||
if (state.plays < 10) throw new Error(`only ${state.plays} cards were played`);
|
||||
console.log(` gostop: ${state.plays} plays, ${state.sheets} choice sheets (${state.picks} card picks), 고 ${state.go}, 스톱 ${state.stop}`);
|
||||
},
|
||||
},
|
||||
rummikub: {
|
||||
// PC host + phone guest, first meld "없음" so hint plays happen early; 10 real turns through the UI.
|
||||
lobby: async (host) => {
|
||||
|
||||
Reference in New Issue
Block a user