구십구: 코리아보드게임즈 공식 규칙으로 맞춤

- 공식 상품 페이지 규칙 설명 이미지 기준: 5장씩, 시작 카드(2명 40·3명 20·4명 이상 0),
  0 카드는 방향 유지/반대 선택, 칩(목숨)은 공식 규칙이 아니라 하우스 룰
- handSize 'auto'(전용 카드 5, 트럼프 3), startTotal 옵션, 카드 종류 'zero'와 dir 액션
- stateVersion 2, 진행 중인 v1 게임은 옛 규칙 그대로 이어서 하도록 migrate
- 특수 카드 장수(±9·±10·0)는 공개 자료가 없어 각 4장 유지

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-06 22:16:32 +09:00
parent d8b8cdd8f3
commit 7da06d1b27
10 changed files with 325 additions and 119 deletions

View File

@@ -1,12 +1,19 @@
/** 구십구 [?] "지금 할 수 있는 것" (docs/07 §5). Pure: only reads the client view. */
import type { NinetyNineView } from '@bg/games/ninety-nine';
import type { NNOptions, NinetyNineView } from '@bg/games/ninety-nine';
import type { PublicUser } from '@bg/shared/lite';
/** Same as the engine's: `auto` deals 5 with the dedicated cards, 3 with trump cards. */
export const resolvedHandSize = (o: Pick<NNOptions, 'mode' | 'handSize'>): number => (o.handSize === 'auto' ? (o.mode === 'classic' ? 3 : 5) : o.handSize);
/** Same as the engine's: the start card shows 40 for 2 players, 20 for 3, 0 for 4 or more. */
export const resolvedStartTotal = (o: Pick<NNOptions, 'mode' | 'startTotal'>, players: number): number =>
o.mode === 'kr-product' && o.startTotal === 'auto' ? (players <= 2 ? 40 : players === 3 ? 20 : 0) : 0;
const nm = (players: Record<string, PublicUser>, id: string | null | undefined) => `${(id && players[id]?.nickname) || '다른 사람'}님`;
/** What happens to someone who has no card to play (option dependent). */
function loseText(v: NinetyNineView): string {
return v.livesStart > 0 ? '목숨 1개를 잃고 합계 0부터 새 판을 해요' : '바로 탈락해요';
if (v.livesStart === 0) return '바로 탈락해요';
return `목숨 1개를 잃고 합계 ${resolvedStartTotal(v.options, v.seats.filter((s) => s.alive).length)}부터 새 판을 해요`;
}
export function helpNow(v: NinetyNineView, me: string | null, players: Record<string, PublicUser>): string {
@@ -44,6 +51,7 @@ export function helpNow(v: NinetyNineView, me: string | null, players: Record<st
if (legal.some((c) => c.kind === 'ace')) lines.push('A는 누른 뒤 더할 값을 골라요.');
if (legal.some((c) => c.kind === 'joker')) lines.push(`조커는 합계를 ${v.jokerRange.min}~${v.jokerRange.max} 중 원하는 수로 만들어요. 수를 고르고 [조커 내기]를 눌러요.`);
const face = (k: string) => legal.find((c) => c.kind === k)?.face.label;
if (face('zero')) lines.push(`${face('zero')} 카드는 합계를 그대로 둬요. 누른 뒤 [방향 그대로]와 [방향 바꾸기] 중 하나를 골라요.`);
if (face('reverse')) lines.push(`${face('reverse')} 카드는 합계를 그대로 두고 진행 방향만 바꿔요.`);
if (face('hold')) lines.push(`${face('hold')} 카드는 합계를 그대로 두고 다음 사람에게 넘겨요.`);
if (face('to99')) lines.push(`${face('to99')} 카드는 합계를 바로 99로 만들어요.`);