M4: 다빈치 코드 규칙·화면·테스트
- 규칙 모듈(packages/games/src/davinci-code): 2~4명, 시작 색 고르기(동시 선택·15초), 정렬 규칙(같은 숫자 검정/흰색 먼저 옵션), 조커 위치 직접 고르기, 맞힘 뒤 계속/멈춤, 틀리면 가져온 타일 공개, 더미가 빈 뒤 자기 타일 공개 벌칙(옵션), 탈락 순서로 순위. 숨은 타일은 view에 색·위치·틀린 추리만 보내고 타일 ID(숫자 포함)는 아는 사람에게만 보냄. 시간 초과는 시스템 전용 timeout 행동으로 처리하고, 자동 추리는 그 사람의 view만으로 후보를 계산해 결정적으로 고름. 3턴 연속 시간 초과면 마감 5초. 나가면 탈락·타일 공개. - 테스트: 문서 10절 시나리오 전부, 시간 초과·연결 끊김·기권, 300판 이벤트/뷰 유출 검사, 시드 1,000판 무작위 대국(타일 보존, 줄 정렬, 결정성, 내장 유출 검사, onTimeout 합법). - 화면(apps/web/src/games/davinci-code): 상대 줄·더미·내 줄, 숫자 패드(0~11, 조커), 한 번 더/멈추기, 조커 자리 고르기, 자기 타일 공개, 시작 색 고르기, 추리 기록, 규칙 그림. - 등록: GAMES, 카탈로그(available), 웹 레지스트리, 아이콘, '곧 나와요'에서 제외. - E2E: games.e2e.ts에 davinci-code 계획(PC 방장 + 폰 친구, 5턴 실제 진행). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,42 @@ const PLANS: Record<string, Plan> = {
|
||||
await finishHand(pages);
|
||||
},
|
||||
},
|
||||
'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) => {
|
||||
for (const p of pages) {
|
||||
await p.getByRole('button', { name: '검정 2개 · 흰색 2개' }).click();
|
||||
await p.getByRole('button', { name: '이렇게 받기' }).click();
|
||||
}
|
||||
const myTurn = (p: Page) => p.evaluate(() => !!document.querySelector('.turn-status')?.textContent?.includes('내 차례예요'));
|
||||
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();
|
||||
},
|
||||
},
|
||||
othello: {
|
||||
// cursor starts at d4 (3,3): black d3 = up; then white c3 = up, left
|
||||
moves: [['ArrowUp', 'Enter'], ['ArrowUp', 'ArrowLeft', 'Enter']],
|
||||
|
||||
Reference in New Issue
Block a user