87 lines
5.0 KiB
TypeScript
87 lines
5.0 KiB
TypeScript
/** 바퀴벌레 포커 [?] "지금 할 수 있는 것" (docs/07 §5). Pure: only reads the client view. */
|
|
import type { CockroachView, Kind } from '@bg/games/cockroach-poker';
|
|
import type { PublicUser } from '@bg/shared/lite';
|
|
|
|
// Kept local (like index.tsx) so the screen chunk does not pull in the rules module.
|
|
const KIND_KO: Record<Kind, string> = {
|
|
cockroach: '바퀴벌레',
|
|
bat: '박쥐',
|
|
fly: '파리',
|
|
toad: '두꺼비',
|
|
rat: '쥐',
|
|
scorpion: '전갈',
|
|
spider: '거미',
|
|
stinkbug: '노린재',
|
|
};
|
|
|
|
const hasBatchim = (word: string) => {
|
|
const c = word.charCodeAt(word.length - 1);
|
|
return c >= 0xac00 && c <= 0xd7a3 && (c - 0xac00) % 28 !== 0;
|
|
};
|
|
/** "이건 쥐야!" / "이건 전갈이야!" — the same words as the screen. */
|
|
const claimLine = (k: Kind) => `이건 ${KIND_KO[k]}${hasBatchim(KIND_KO[k]) ? '이야' : '야'}!`;
|
|
const kindIs = (k: Kind) => `${KIND_KO[k]}${hasBatchim(KIND_KO[k]) ? '이에요' : '예요'}`;
|
|
|
|
export function helpNow(v: CockroachView, me: string | null, players: Record<string, PublicUser>): string {
|
|
const mine = me && v.order.includes(me) ? me : null;
|
|
const name = (id: string) => `${players[id]?.nickname ?? '다른 사람'}님`;
|
|
const subj = (id: string) => (id === mine ? '내가' : `${name(id)}이`);
|
|
const goal = `같은 벌레가 앞마당(내 앞에 펼친 카드)에 ${v.loseCount}장 모이면 져요.`;
|
|
|
|
if (v.phase === 'finished') {
|
|
const f = v.final;
|
|
if (!f) return '게임이 끝났어요.';
|
|
const who = f.loser === mine ? '내가 졌어요.' : mine ? `${name(f.loser)}이 졌고, 나는 이겼어요!` : `${name(f.loser)}이 졌고, 나머지 모두 이겼어요.`;
|
|
const why =
|
|
f.reason === 'four' && f.losingKind
|
|
? `앞마당에 ${KIND_KO[f.losingKind]} ${v.loseCount}장이 모였어요.`
|
|
: f.reason === 'eight'
|
|
? '앞마당에 벌레 8종이 모두 모였어요.'
|
|
: f.reason === 'emptyHand'
|
|
? '카드를 건넬 차례인데 손에 카드가 없었어요.'
|
|
: '한 사람이 게임을 나갔어요.';
|
|
return `게임이 끝났어요. ${why}\n${who}`;
|
|
}
|
|
|
|
const r = v.round;
|
|
const danger = mine ? (Object.keys(v.fronts[mine]?.level ?? {}) as Kind[]).filter((k) => v.fronts[mine]!.level[k] === 'danger') : [];
|
|
const warn = danger.length ? `조심! 내 앞마당의 ${danger.map((k) => `${KIND_KO[k]} ${v.fronts[mine!]!.counts[k]}장`).join(', ')}은 한 장만 더 오면 져요.` : null;
|
|
const lines: string[] = [];
|
|
|
|
if (r.holder === null) {
|
|
// 건네기: the round starter picks a card, a receiver and what to say.
|
|
if (r.starter === mine) {
|
|
lines.push('내 차례예요. 카드 한 장을 뒤집어 다른 사람에게 건네며 무슨 벌레인지 말해요. 진짜를 말해도, 거짓말을 해도 돼요.');
|
|
lines.push('아래에서 ① 건넬 카드 ② 받을 사람 ③ 할 말(벌레 그림)을 차례로 고르고, 맨 아래 [건네기] 버튼을 눌러요.');
|
|
} else {
|
|
lines.push(`${r.no}라운드예요. ${subj(r.starter)} 건넬 카드를 고르는 중이에요.${mine ? ' 나에게 올 수도 있으니 기다려 주세요.' : ''}`);
|
|
lines.push(goal);
|
|
}
|
|
} else {
|
|
const holder = r.holder;
|
|
const passer = r.lastPasser ?? r.starter;
|
|
const said = r.claim ? `"${claimLine(r.claim)}"` : '카드';
|
|
if (holder === mine && !r.peeked) {
|
|
lines.push(`${name(passer)}이 ${said}라며 카드를 줬어요. 진짜 같으면 [⭕ 진짜!], 거짓말 같으면 [❌ 거짓말!]을 눌러요.`);
|
|
lines.push(`맞히면 ${name(passer)}이, 틀리면 내가 이 카드를 앞마당에 놓아요.`);
|
|
if (v.my?.canPeek) lines.push('자신 없으면 [👁 몰래 보고 넘기기]로 카드를 본 뒤, 아직 안 본 사람에게 넘길 수 있어요. 보고 나면 판정은 못 해요.');
|
|
else if (v.my?.peekBlocked) lines.push(v.my.peekBlocked);
|
|
} else if (holder === mine) {
|
|
const known = v.card && v.card.known ? `몰래 본 카드는 ${kindIs(v.card.kind)}. ` : '';
|
|
lines.push(`${known}이제 판정은 못 하고 꼭 다른 사람에게 넘겨야 해요.`);
|
|
lines.push('아직 이 카드를 안 본 사람(눌리는 버튼)과 할 말을 고르고 [넘기기] 버튼을 눌러요. 진짜를 말해도, 거짓말을 해도 돼요.');
|
|
} else {
|
|
if (r.peeked) lines.push(`${subj(holder)} 몰래 본 카드를 넘길 사람을 고르는 중이에요.`);
|
|
else lines.push(`${subj(passer)} ${name(holder)}에게 ${said}라며 카드를 건넸어요. ${subj(holder)} 진짜인지 고민하는 중이에요.`);
|
|
if (mine && r.seen.includes(mine)) {
|
|
const known = v.card && v.card.known ? ` 실제 카드는 ${kindIs(v.card.kind)}.` : '';
|
|
lines.push(`나는 이 카드를 봤기 때문에 다시 받지 않아요.${known}`);
|
|
} else if (mine && r.peeked) lines.push('아직 이 카드를 안 봤으니 나에게 넘어올 수도 있어요.');
|
|
else if (mine) lines.push('기다려 주세요.');
|
|
lines.push(goal);
|
|
}
|
|
}
|
|
if (warn) lines.push(warn);
|
|
return lines.join('\n');
|
|
}
|