Files
joke-app/apps/web/src/games/dalmuti/help.ts
2026-10-06 13:35:21 +09:00

103 lines
6.8 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 { DalmutiView } from '@bg/games/dalmuti';
import type { PublicUser } from '@bg/shared/lite';
const JESTER = 13;
const nm = (players: Record<string, PublicUser>, id: string | null | undefined) => `${(id && players[id]?.nickname) || '다른 사람'}님`;
const rankText = (r: number) => (r === JESTER ? '광대(13)' : String(r));
const faces = (cards: number[]) => cards.map((n) => (n === JESTER ? '광대' : String(n))).join(', ');
/** What the tax step will do for this title (no revolution). */
function taxPlan(title: string): string {
switch (title) {
case '대달무티':
return '혁명이 없으면 대농노에게서 가장 좋은 카드 2장을 받고, 아무 카드 2장을 골라 돌려줘요.';
case '소달무티':
return '혁명이 없으면 소농노에게서 가장 좋은 카드 1장을 받고, 아무 카드 1장을 골라 돌려줘요.';
case '소농노':
return '혁명이 없으면 내 가장 좋은(가장 낮은 숫자) 카드 1장이 소달무티에게 자동으로 넘어가요.';
case '대농노':
return '혁명이 없으면 내 가장 좋은(가장 낮은 숫자) 카드 2장이 대달무티에게 자동으로 넘어가요.';
default:
return '평민은 세금을 내지도 받지도 않아요.';
}
}
export function helpNow(v: DalmutiView, me: string | null, players: Record<string, PublicUser>): string {
const goal = '숫자가 낮을수록 좋은 카드예요. 카드를 먼저 다 낼수록 다음 라운드에서 높은 신분이 돼요.';
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 roundText = `${v.round}/${v.options.rounds}라운드`;
if (v.phase === 'revolutionCheck') {
const done = v.seats.filter((s) => s.confirmed).length;
if (!my) return `${roundText} 준비 중이에요. 모두 자기 카드를 보고 확인하는 중이에요(${done}/${v.seats.length}).\n${goal}`;
const lines: string[] = [];
if (my.canDeclareRevolution) {
const great = v.seats[v.seats.length - 1]?.id === me;
lines.push(`광대 2장이 다 있어요! [혁명!]을 누르면 광대 2장을 보여 주고, 이번 라운드는 세금이 없어요.${great ? ' 나는 대농노라서 대혁명이 되어 신분이 완전히 뒤집혀요.' : ''}`);
lines.push('혁명하지 않으려면 [손패 확인]을 눌러요.');
return lines.join('\n');
}
if (my.canConfirm) {
lines.push(`${roundText} 준비예요(내 신분: ${seat!.title}). 내 카드를 보고 [손패 확인]을 눌러 주세요.`);
lines.push(taxPlan(seat!.title));
} else {
lines.push(`손패를 확인했어요. 다른 사람이 확인하기를 기다리는 중이에요(${done}/${v.seats.length}).`);
lines.push(taxPlan(seat!.title));
}
return lines.join('\n');
}
if (v.phase === 'taxReturn') {
const pending = v.taxPending.filter((p) => !p.done);
const pendingText = pending.map((p) => nm(players, p.from)).join(', ');
if (!my) return `세금 카드를 주고받는 중이에요. 농노는 가장 좋은 카드를 달무티에게 주고, 달무티는 아무 카드나 같은 장수만큼 돌려줘요.${pendingText ? `\n${pendingText}이 돌려줄 카드를 고르고 있어요.` : ''}`;
if (my.owe) {
const got = v.tax.find((t) => t.kind === 'tax' && t.to === me);
const gotText = got?.cards ? `세금으로 ${nm(players, got.from)}에게서 좋은 카드를 받았어요(받은 카드: ${faces(got.cards)}). ` : '';
return `${gotText}${nm(players, my.owe.to)}에게 돌려줄 카드 ${my.owe.count}장을 눌러 고르고 [돌려주기]를 눌러요.\n처음에는 가장 큰 숫자 카드가 골라져 있어요. 받은 카드를 돌려줘도 돼요.`;
}
const paid = v.tax.find((t) => t.kind === 'tax' && t.from === me);
const lines: string[] = [];
if (paid) lines.push(`세금으로 내 가장 좋은 카드${paid.cards ? `(${faces(paid.cards)})` : ''} ${paid.count}장이 ${nm(players, paid.to)}에게 넘어갔어요. 곧 ${paid.count}장을 돌려받아요.`);
else lines.push('세금 카드를 주고받는 중이에요.');
if (pendingText) lines.push(`${pendingText}이 돌려줄 카드를 고르고 있어요. 기다려 주세요.`);
return lines.join('\n');
}
// playing
const top = v.trick?.top ?? null;
const topText = top ? `${rankText(top.rank)} × ${top.count}장` : '';
const cur = v.current;
if (my && cur === me) {
if (my.leading) {
const lines = ['내 차례예요. 새로 시작해요. 같은 숫자 카드를 원하는 장수만큼 낼 수 있어요.', '숫자 묶음을 누르고 −/+로 장수를 정한 뒤 [내기]를 눌러요. 새로 시작할 때는 패스할 수 없어요.'];
if (my.jesters > 0) lines.push('광대는 [🃏 광대 섞기]로 다른 숫자와 함께 내면 그 숫자로 쳐요. 광대만 내면 13이에요.');
return lines.join('\n');
}
const lines = [`내 차례예요. 지금 바닥은 ${topText}이에요.`];
if (top!.rank === 1) lines.push('1보다 낮은 숫자는 없으니 [패스]를 눌러 주세요.');
else {
lines.push(`같은 ${top!.count}장으로 ${rankText(top!.rank)}보다 낮은 숫자를 내거나 [패스]를 눌러요.`);
if (my.choices.length > 0) {
lines.push('밝게 보이는 숫자 묶음만 낼 수 있어요. 묶음을 누른 뒤 [내기]를 눌러요.');
if (my.choices.some((c) => c.needsJester)) lines.push("'광대 필요'라고 적힌 숫자는 광대를 섞어야 장수가 맞아요.");
} else lines.push('낼 수 있는 카드가 없어요. [패스]를 눌러 주세요.');
}
lines.push(v.options.passLocksOut ? '이 방 규칙에서는 패스하면 이번 트릭이 끝날 때까지 못 내요.' : '패스해도 차례가 다시 오면 낼 수 있어요.');
return lines.join('\n');
}
const curNick = nm(players, cur);
const tableLine = top ? `지금 바닥은 ${topText}(${nm(players, top.by)}이 냄)이에요.` : `${curNick}이 새로 시작해요.`;
if (!my || !seat) return `${curNick} 차례예요. ${tableLine}\n${goal}`;
if (seat.place !== null) return `카드를 ${seat.place}번째로 다 냈어요! 다음 라운드에서 그만큼 높은 신분이 돼요.\n${curNick} 차례예요. 남은 사람을 지켜봐요.`;
const lines = [`${curNick} 차례예요. 기다려 주세요. ${tableLine}`];
if (my.lockedOut) lines.push('이번 트릭은 패스해서 빠졌어요. 다음 트릭을 기다려요.');
lines.push(`내 신분은 ${seat.title}, 남은 카드는 ${my.hand.length}장이에요.`);
return lines.join('\n');
}