M6: 할리갈리 규칙·화면·테스트

- packages/games/src/halli-galli: 56장 과일 카드, 차례 뒤집기(flipGuard 예약), 언제나 칠 수 있는 종
  (tableVersion으로 늦은 종 "늦었어요" 무벌칙, 서버 도착 순서 우선), 오답 벌칙·탈락, official/시간 제한 종료,
  하우스 룰(1대1 오답 패배, 3진 아웃, 벌칙 증가, 벌칙 종 밑), latencyComp light 50ms 창,
  view는 덱 내용을 본인에게도 숨김(장수만), secretsOf/legalActions 제공
- 테스트: §10 체크리스트 전부, 1,000판 시드 랜덤 플레이아웃(카드 보존·누출·onTimeout 합법·결정성) +
  엔진 randomPlayout, 서버 Room 테스트(동시 종 도착 순서, 늦은 종 거절, 자리 비움 자동 뒤집기)
- 화면: 원형 좌석 배치, 큰 종 버튼(스페이스 바), 뒤집기(Enter), SVG 과일 카드, 땡/삐 효과음·진동,
  카드 이동 애니메이션, 어린이 모드 합계 막대, 다크 모드·동작 줄이기 대응
- 공용 변경(하위 호환): GameDefinition.timeoutNotBefore 선택 훅 + Room 타이머 반영(자리 비움 시
  바쁜 반복 방지), feedback.ts에 ding/buzz 소리 추가
- 등록: GAMES, CATALOG, 웹 레지스트리·아이콘, 출시 예정 목록에서 제거, E2E 계획 추가

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-05 13:34:43 +09:00
parent 6321ea8c0d
commit 595d3cdf4f
13 changed files with 2720 additions and 1 deletions

View File

@@ -766,6 +766,7 @@ export class Room {
if (!g || g.finished || this.status !== 'playing') return null;
const def = this.def;
const base = def.deadline?.(g.state) ?? null;
const notBefore = def.timeoutNotBefore?.(g.state) ?? null;
const timed = def.timeoutPlayers ? def.timeoutPlayers(g.state) : def.activePlayers(g.state);
let best: number | null = null;
for (const u of timed) {
@@ -777,6 +778,7 @@ export class Room {
}
if (d === null) continue;
if (g.deadlineFloor !== null) d = Math.max(d, g.deadlineFloor);
if (notBefore !== null) d = Math.max(d, notBefore);
best = best === null ? d : Math.min(best, d);
}
return best;
@@ -803,6 +805,7 @@ export class Room {
const def = this.def;
const now = this.now();
const base = def.deadline?.(g.state) ?? null;
const notBefore = def.timeoutNotBefore?.(g.state) ?? null;
const timed = def.timeoutPlayers ? def.timeoutPlayers(g.state) : def.activePlayers(g.state);
for (const u of timed) {
let d = base;
@@ -812,6 +815,7 @@ export class Room {
d = d === null ? grace : Math.min(d, grace);
}
if (g.deadlineFloor !== null && d !== null) d = Math.max(d, g.deadlineFloor);
if (notBefore !== null && d !== null) d = Math.max(d, notBefore);
if (d === null || d > now) continue;
const action = def.onTimeout(g.state, u);
const err = this.runAction(u, action, undefined, true);