Files
joke-app/apps/web/src/games/ninety-nine/help.ts
EJClaw 7da06d1b27 구십구: 코리아보드게임즈 공식 규칙으로 맞춤
- 공식 상품 페이지 규칙 설명 이미지 기준: 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>
2026-10-06 22:16:32 +09:00

62 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 구십구 [?] "지금 할 수 있는 것" (docs/07 §5). Pure: only reads the client view. */
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 {
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 {
const goal = '카드를 1장씩 내며 합계를 더해요. 합계가 99를 넘게 될 카드밖에 없으면 져요.';
if (v.phase === 'finished') return `게임이 끝났어요. ${v.result?.summary ?? ''}`.trim() + '\n결과 창에서 [한 판 더]를 누르면 다시 할 수 있어요.';
const seat = me ? v.seats.find((s) => s.id === me) : undefined;
const my = seat ? v.my : null;
const cur = v.current;
const curNick = nm(players, cur);
const turnLine = cur ? `${curNick} 차례예요(지금 합계 ${v.total}).` : `지금 합계는 ${v.total}까지 왔어요.`;
if (!seat || !my) return `${turnLine}\n${goal}`;
if (!seat.alive) return `${seat.left ? '게임에서 나갔어요.' : '탈락했어요. 남은 사람의 게임을 지켜봐요.'}\n${turnLine}`;
const lifeLine =
v.livesStart > 0
? seat.onBoard
? '목숨을 다 잃어서 마지막 기회예요. 한 번 더 지면 탈락해요.'
: `내 목숨은 ${seat.lives}개예요.`
: '';
if (!my.myTurn) {
const lines = [`${turnLine} 기다려 주세요.`];
lines.push(`내 차례에 낼 수 있는 카드가 없으면 ${loseText(v)}. 카드를 누르면 낼 때 합계를 미리 볼 수 있어요.`);
if (lifeLine) lines.push(lifeLine);
return lines.join('\n');
}
const legal = my.hand.filter((c) => c.legal);
const lines = [`내 차례예요(지금 합계 ${v.total}). 합계가 99를 넘지 않게 카드 1장을 내요.`];
if (legal.length < my.hand.length) lines.push(`초록 테두리로 밝게 보이는 카드만 낼 수 있어요(${legal.length}장). 어두운 카드는 99를 넘거나 0보다 작아져요.`);
lines.push('카드를 누르면 낸 뒤 합계가 보여요. 한 번 더 누르면 내요. 패스는 없어요.');
if (legal.some((c) => c.kind === 'plusMinus')) lines.push('±카드는 누른 뒤 더하기(+)와 빼기(−) 중 하나를 골라요. 합계가 클 때는 빼기가 좋아요.');
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로 만들어요.`);
if (face('minus')) lines.push(`${face('minus')} 카드는 합계에서 10을 빼요.`);
if (lifeLine) lines.push(lifeLine);
return lines.join('\n');
}