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:
EJClaw
2026-10-05 13:35:21 +09:00
parent 6321ea8c0d
commit f5e4a7ce59
13 changed files with 4236 additions and 3 deletions

View File

@@ -145,7 +145,13 @@ export function HwatuCard({
<Tag style={s} onClick={onClick} aria-label={card.name} aria-pressed={onClick ? !!selected : undefined} title={card.name}>
<svg viewBox="0 0 100 160" width={w} height={h} aria-hidden>
<rect x="1" y="1" width="98" height="158" rx="8" fill="#fffaf0" stroke={selected ? '#2563eb' : '#1f2937'} strokeWidth={selected ? 6 : 3} />
<Motif month={card.month} color={color} />
{card.kind === 'bonus' ? (
<text x="50" y="102" fontSize="34" fontWeight="900" fill="#b45309" textAnchor="middle" fontFamily="Pretendard Variable, sans-serif">
{card.piValue ?? 2}피
</text>
) : (
<Motif month={card.month} color={color} />
)}
{card.kind === 'tti' && (
<g>
<rect x="58" y="30" width="18" height="90" rx="3" fill={TTI_COLOR[card.tti ?? 'plain']} />
@@ -170,7 +176,7 @@ export function HwatuCard({
{card.month || '+'}
</text>
<text x="50" y="154" fontSize="13" fontWeight="700" fill="#374151" textAnchor="middle" fontFamily="Pretendard Variable, sans-serif">
{MONTH_NAME[card.month]} · {card.kind === 'tti' ? (TTI_TEXT[card.tti ?? 'plain'] || '띠') : KIND_BADGE[card.kind] === '光' ? '광' : KIND_BADGE[card.kind] === '열' ? '열끗' : KIND_BADGE[card.kind]}
{MONTH_NAME[card.month]} · {card.kind === 'bonus' ? `${card.piValue ?? 2}피` : card.kind === 'tti' ? (TTI_TEXT[card.tti ?? 'plain'] || '띠') : KIND_BADGE[card.kind] === '光' ? '광' : KIND_BADGE[card.kind] === '열' ? '열끗' : KIND_BADGE[card.kind]}
</text>
</svg>
</Tag>

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ export const GAME_UI: Record<string, () => Promise<{ default: GameUI }>> = {
'one-card': () => import('./one-card'),
rummikub: () => import('./rummikub'),
'davinci-code': () => import('./davinci-code'),
gostop: () => import('./gostop'),
};
const cache = new Map<string, GameUI>();

View File

@@ -12,7 +12,8 @@ export const GAME_ICON: Record<string, string> = {
'one-card': '♣',
rummikub: '🔢',
'davinci-code': '🔐',
gostop: '🌸',
};
/** Shown on the home screen so users know what's coming (docs/12-roadmap.md). */
export const UPCOMING = ['마작', '고스톱', '뱅', '할리갈리'];
export const UPCOMING = ['마작', '뱅', '할리갈리'];