M9: 게임 26종 30초 요약 카드·[?] 지금 할 수 있는 것 내용
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
131
apps/web/src/games/mafia/help.ts
Normal file
131
apps/web/src/games/mafia/help.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 마피아 [?] "지금 할 수 있는 것" (docs/07 §5). Pure: only reads the client view.
|
||||
* Only what this viewer may know: the own role is named only where the screen already shows
|
||||
* role-specific things (night actions, after death, after the game).
|
||||
*/
|
||||
import type { MafiaView } from '@bg/games/mafia';
|
||||
import type { PublicUser } from '@bg/shared/lite';
|
||||
|
||||
const PHASE_KO: Record<MafiaView['phase']['kind'], string> = {
|
||||
roleReveal: '직업 확인',
|
||||
greeting: '인사 시간',
|
||||
discussion: '낮 토론',
|
||||
vote: '지목 투표',
|
||||
defense: '최후의 반론',
|
||||
confirm: '찬반 투표',
|
||||
night: '밤',
|
||||
ended: '게임 끝',
|
||||
};
|
||||
|
||||
/** "~예요" after a vowel, "~이에요" after a final consonant. */
|
||||
function yeyo(word: string): string {
|
||||
const c = word.charCodeAt(word.length - 1) - 0xac00;
|
||||
return c >= 0 && c < 11172 && c % 28 !== 0 ? `${word}이에요` : `${word}예요`;
|
||||
}
|
||||
|
||||
const WIN = '시민 팀은 마피아를 모두 찾아내면, 마피아 팀은 살아 있는 마피아가 나머지 사람만큼 많아지면 이겨요.';
|
||||
|
||||
export function helpNow(v: MafiaView, me: string | null, players: Record<string, PublicUser>): string {
|
||||
const n = (id: string) => `${players[id]?.nickname ?? '다른 사람'}님`;
|
||||
const ph = v.phase;
|
||||
const head = `${v.day}일차 ${yeyo(PHASE_KO[ph.kind])}.`;
|
||||
if (ph.kind === 'ended') {
|
||||
const chats = v.options.revealChatsAtEnd ? ' 모든 채팅방 기록도 이제 볼 수 있어요.' : '';
|
||||
return `게임이 끝났어요. ${v.final?.summary ?? ''}`.trim() + `\n최종 공개 표에서 모든 사람의 직업을 볼 수 있어요.${chats}`;
|
||||
}
|
||||
const my = v.my;
|
||||
|
||||
// Spectators see the day chat and announcements only.
|
||||
if (!my) {
|
||||
const what: Record<string, string> = {
|
||||
roleReveal: '모두 자기 직업을 확인하고 있어요.',
|
||||
greeting: '살아 있는 사람들이 전체 채팅으로 인사하고 있어요. 오늘은 투표가 없어요.',
|
||||
discussion: '살아 있는 사람들이 누가 마피아인지 이야기하고 있어요.',
|
||||
vote: '살아 있는 사람들이 마피아로 의심되는 사람을 지목하고 있어요.',
|
||||
defense: ph.accused ? `${n(ph.accused)}이 지목돼서 마지막으로 변론하고 있어요.` : '지목된 사람이 변론하고 있어요.',
|
||||
confirm: ph.accused ? `${n(ph.accused)}을 탈락시킬지 찬반 투표를 하고 있어요.` : '찬반 투표를 하고 있어요.',
|
||||
night: '각자 직업에 따라 몰래 행동하는 시간이에요. 아침에 발표가 있어요.',
|
||||
};
|
||||
return `${head} ${what[ph.kind] ?? ''}\n관전 중에는 전체 채팅과 발표만 볼 수 있어요.`;
|
||||
}
|
||||
|
||||
const idle = my.idleWarning ? '\n오늘도 아무것도 하지 않으면 자리 비움으로 탈락해요.' : '';
|
||||
|
||||
if (!my.alive) {
|
||||
const see = v.options.deadSeeAll ? ' 이 방에서는 탈락하면 모든 직업과 마피아 채팅도 볼 수 있어요.' : '';
|
||||
return `${head} 나는 탈락했어요. [죽은 사람] 채팅에서만 이야기할 수 있어요.${see}\n살아 있는 사람에게 정보를 알려 주지 말아 주세요(디스코드 같은 다른 대화방도요).`;
|
||||
}
|
||||
|
||||
switch (ph.kind) {
|
||||
case 'roleReveal':
|
||||
return `직업을 확인하는 시간이에요. 위의 [내 직업] 카드를 꾹 누르고 있는 동안만 직업이 보여요(옆 사람에게 보이지 않게).\n곧 ${v.options.firstPhase === 'day' ? '인사 시간' : '첫 밤'}이 시작돼요. ${WIN}`;
|
||||
case 'greeting':
|
||||
return `${head} [전체] 채팅으로 인사해요. 오늘은 투표가 없고, 끝나면 밤이 와요.\n다 됐으면 [인사 끝내기]를 눌러요. 살아 있는 사람이 모두 누르면 바로 넘어가요(${ph.readyCount}/${ph.readyNeeded}).${idle}`;
|
||||
case 'discussion': {
|
||||
const adj = my.canAdjust ? '\n[+15초]·[−15초]로 토론 시간을 하루 한 번 바꿀 수 있어요.' : '';
|
||||
return `${head} [전체] 채팅으로 누가 마피아 같은지 이야기해요. 직업을 밝힐지는 내 선택이에요.${adj}\n다 이야기했으면 [토론 끝내기]를 눌러요. 모두 누르면 바로 지목 투표로 넘어가요(${ph.readyCount}/${ph.readyNeeded}).${idle}`;
|
||||
}
|
||||
case 'vote': {
|
||||
const now = my.vote === null ? '아직 고르지 않았어요(안 고르면 기권).' : my.vote === 'abstain' ? '지금은 기권으로 되어 있어요.' : `지금 ${n(my.vote)}을 골랐어요.`;
|
||||
return `${head} '사람들' 목록에서 초록 테두리 사람을 눌러 마피아로 의심되는 사람을 골라요. 시간 안에는 바꿀 수 있고, [기권]도 돼요.\n${now} 표가 가장 많은 한 명이 지목돼요. 같은 표가 나오면 아무도 지목되지 않아요.${idle}`;
|
||||
}
|
||||
case 'defense':
|
||||
if (ph.accused === me) return `${head} 내가 지목됐어요! [전체] 채팅으로 내가 왜 마피아가 아닌지 마지막으로 말해요. 지금은 나만 말할 수 있어요.`;
|
||||
return `${head} ${ph.accused ? n(ph.accused) : '지목된 사람'}의 마지막 변론을 들어요. 지금은 지목된 사람만 채팅할 수 있어요. 곧 찬반 투표를 해요.`;
|
||||
case 'confirm': {
|
||||
const who = ph.accused ? n(ph.accused) : '지목된 사람';
|
||||
if (!my.canConfirm) return `${head} 다른 사람들이 나를 탈락시킬지 찬반 투표를 하고 있어요. 결과를 기다려 주세요.`;
|
||||
const now = my.ballot ? `지금 ${my.ballot === 'yes' ? '찬성' : '반대'}했어요. 시간 안에는 바꿀 수 있어요.` : '안 누르면 집계에서 빠져요.';
|
||||
return `${head} ${who}을 탈락시킬지 정해요. [찬성 (탈락)] 또는 [반대 (살리기)]를 눌러요.\n찬성이 반대보다 많아야 탈락해요. ${now}${idle}`;
|
||||
}
|
||||
case 'night':
|
||||
return nightHelp(v, me, n) + idle;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
function nightHelp(v: MafiaView, me: string | null, n: (id: string) => string): string {
|
||||
const my = v.my!;
|
||||
const nv = my.night;
|
||||
const pick = "'사람들' 목록에서 초록 테두리 사람을 눌러 골라요. 밤이 끝날 때 고른 사람으로 정해지고, 그 전엔 바꿀 수 있어요.";
|
||||
const lines: string[] = [];
|
||||
const isAlive = (id: string) => v.players.some((p) => p.id === id && p.alive);
|
||||
const mates = my.mates.filter(isAlive);
|
||||
switch (nv?.action ?? null) {
|
||||
case null: {
|
||||
lines.push('밤이에요. 지금은 할 일이 없어요. 아침 발표를 기다려 주세요.');
|
||||
if (my.role === 'lover' && mates.length) lines.push(`밤에는 [연인] 채팅으로 ${mates.map(n).join(', ')}과 둘이서만 이야기할 수 있어요.`);
|
||||
break;
|
||||
}
|
||||
case 'mafiaTarget': {
|
||||
lines.push(`밤이에요. 나는 마피아예요. 오늘 밤 노릴 사람을 ${pick}`);
|
||||
if (mates.length) lines.push(`마피아끼리 한 명을 함께 정해요. 누가 고르든 마지막에 고른 사람이 대상이에요. 살아 있는 동료 마피아는 ${mates.map(n).join(', ')}이에요. [마피아] 채팅으로 의논해요.`);
|
||||
const none = v.options.mafiaMustKill === 'random' ? '아무도 안 고르면 무작위로 한 명이 탈락해요.' : '아무도 안 고르면 오늘 밤은 아무도 탈락하지 않아요.';
|
||||
lines.push(nv!.choice ? `지금 대상: ${n(nv!.choice)}${nv!.pickedBy && nv!.pickedBy !== me ? ` (${n(nv!.pickedBy)}이 골랐어요)` : ''}. [선택 취소]로 지울 수 있어요.` : none);
|
||||
break;
|
||||
}
|
||||
case 'investigate': {
|
||||
const what = v.options.policeRevealRole ? '그 사람의 직업이' : '마피아인지 아닌지가';
|
||||
lines.push(`밤이에요. 나는 경찰이에요. 조사할 사람을 ${pick}`);
|
||||
lines.push(`밤이 끝나면 ${what} '조사 결과'에 나만 보이게 나와요.${nv!.choice ? ` 지금 고른 사람: ${n(nv!.choice)}.` : ''}`);
|
||||
break;
|
||||
}
|
||||
case 'heal': {
|
||||
const self = v.options.doctorSelfHeal ? '나 자신도 고를 수 있어요.' : '나 자신은 고를 수 없어요.';
|
||||
const rep = v.options.doctorRepeat ? '' : ' 어젯밤 지킨 사람은 오늘 다시 고를 수 없어요.';
|
||||
lines.push(`밤이에요. 나는 의사예요. 지켜 줄 사람을 ${pick}`);
|
||||
lines.push(`마피아가 그 사람을 노리면 살아나요. ${self}${rep}${nv!.choice ? ` 지금 고른 사람: ${n(nv!.choice)}.` : ''}`);
|
||||
break;
|
||||
}
|
||||
case 'mediumPeek': {
|
||||
if (nv!.targets.length === 0) lines.push('밤이에요. 나는 영매예요. 아직 탈락한 사람이 없어서 알아볼 사람이 없어요.');
|
||||
else {
|
||||
lines.push(`밤이에요. 나는 영매예요. 직업을 알아볼 탈락한 사람을 ${pick}`);
|
||||
lines.push(`결과는 밤이 끝나면 '영매 결과'에 나만 보이게 나와요.${nv!.choice ? ` 지금 고른 사람: ${n(nv!.choice)}.` : ''}`);
|
||||
}
|
||||
lines.push('밤에는 [죽은 사람] 채팅을 읽고 쓸 수 있어요.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
Reference in New Issue
Block a user