Files
joke-app/docs/games/mafia.md
EJClaw f4d63e1544 docs: 온라인 보드게임 사이트 계획서 (공통 14편 + 게임 26종)
- 개요, 조사(BGA·한국 인기 보드게임·UI/UX), 아키텍처, 실시간 프로토콜,
  안정성·보안, 계정, 로비·방, UI/UX, 데이터 모델, 게임 엔진, 테스트,
  배포·운영, 로드맵(M0~M9), 사용자 결정 항목
- 게임별 규칙·엔진 설계·UI·테스트 체크리스트 26종

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 04:18:29 +09:00

19 KiB
Raw Blame History

마피아 (mafia)

마일스톤: M8 · 인원: 최소 4 ~ 최대 12명 · 예상 시간: 약 10~25분 · 난이도: 보통

1. 개요

  • 비밀 역할을 받은 시민 팀과 마피아 팀이 낮(토론·투표)과 밤(비밀 행동)을 반복하며 상대 팀을 제거하는 대화형 추리 게임. 이 게임에서는 채팅이 곧 게임판이다.
  • 한국에서는 수련회·MT의 오프라인 놀이와 "마피아42" 같은 온라인 게임으로 매우 유명하다. 이 문서는 한국 온라인 마피아 진행 방식(토론 → 투표 → 최후의 반론 → 찬반 투표 → 밤)을 기준으로 한다.
  • 인원 근거: 원작(디미트리 다비도프, 1986)은 인원 제한이 고정되어 있지 않은 진행자 게임이다. 사용자 요청과 국내 온라인 관행(한 방 최대 812명)에 따라 412명으로 정한다. 4명 미만은 낮 투표가 성립하지 않는다(1대2에서 바로 결판).
  • 진행자는 서버가 맡는다(사람 사회자 불필요).

2. 모드와 옵션

방장이 대기실에서 조정. 모든 시간은 초 단위.

옵션 키 설명 선택지 기본값
mafiaCount 마피아 수 auto / 1~4 auto(4장 표)
extraRoles 추가 직업(시민 자리를 대체) soldier, politician, medium, lovers 다중 선택 없음
firstPhase 시작 페이즈 day(투표 없는 인사 낮) / night day
discussionPerAlive 토론 시간 = 생존자 수 × 이 값 10 / 15 / 20 15
discussionMin / discussionMax 토론 시간 하한/상한 30~300 60 / 180
timeAdjust 낮마다 생존자 1인 1회 시간 연장/단축(±15초) on / off on
voteSeconds 지목 투표 시간 10~60 20
defenseSeconds 최후의 반론 시간 10~60 20
confirmSeconds 찬반 투표 시간 5~30 10
nightSeconds 밤 시간 15~90 30
deathReveal 사망 시 공개 정보 none / team(마피아 여부) / role(직업) team
openVote 지목 투표에서 누가 누구를 찍었는지 공개 on / off on
doctorSelfHeal 의사 자기 치료 허용 on / off on
doctorRepeat 의사가 같은 사람을 연속 두 밤 치료 허용 on / off on
mafiaMustKill 마피아가 대상 미선택 시 처리 noKill / random noKill
deadSeeAll 죽은 사람이 모든 직업·마피아 채팅을 열람 on / off off
kidSafeChat 비속어 필터 + 링크 차단 on / off on

3. 구성물

  • 직업(역할) 카드 — 가상. 화면에서 "내 직업" 카드로 표시.
  • 기본 직업: 마피아, 경찰, 의사, 시민.
  • 추가 직업(옵션): 군인, 정치인, 영매, 연인(2명 1쌍).
직업 팀 능력
마피아 마피아 서로를 안다. 밤에 마피아 전용 채팅. 밤마다 1명을 처치 대상으로 지정(팀 공용 1표, 마지막 선택이 유효).
경찰 시민 밤마다 살아 있는 다른 1명을 조사해 "마피아입니다 / 마피아가 아닙니다"를 즉시 받는다.
의사 시민 밤마다 살아 있는 1명을 치료. 그 밤 마피아의 공격 대상이면 살린다.
시민 시민 능력 없음. 토론과 투표로 마피아를 찾는다.
군인 시민 마피아 공격을 1회 버틴다(그때 군인임이 전원에게 공개). 두 번째 공격에는 사망.
정치인 시민 지목 투표와 찬반 투표에서 표가 2표로 계산. 투표로 처형되지 않는다(찬반 통과 시 "정치인이라 처형되지 않았다"로 공개 후 생존).
영매 시민 밤에 죽은 사람 채팅방을 읽고 쓸 수 있다. 밤마다 죽은 사람 1명을 골라 그 직업을 알 수 있다.
연인 시민 2명이 서로를 안다. 밤에 둘만의 채팅.

4. 준비(셋업)

  1. mafiaCount=auto일 때 기본 배분:
인원 마피아 경찰 의사 시민(추가 직업 포함)
4 1 1 1 1
5 1 1 1 2
6 2 1 1 2
7 2 1 1 3
8 2 1 1 4
9 3 1 1 4
10 3 1 1 5
11 3 1 1 6
12 3 1 1 7
  1. 추가 직업은 시민 자리를 대체. 시민 자리가 부족하면 대기실에서 선택 불가(사유 표시: "인원이 부족해 연인을 넣을 수 없습니다(시민 2자리 필요)"). 수동 mafiaCount는 마피아 < 전체/2여야 한다.
  2. RNG로 직업 셔플 후 배정. 마피아는 서로의 이름을 받는다. 연인은 서로를 안다.
  3. 역할 확인 화면 8초 후 firstPhase로 시작. 일차(day number)는 1부터.

5. 진행 규칙

5.1 페이즈 순서

[낮 토론] → [지목 투표] → (최다 득표자 1명일 때) [최후의 반론] → [찬반 투표] → [처형 결과] → [밤] → [아침 발표] → 다음 날 [낮 토론] ...
  • firstPhase=day이면 첫날은 "인사 토론"만 하고(60초 고정, 투표 없음) 바로 밤으로.
  • 각 단계 끝나고 승리 조건 확인(6장).

5.2 낮 토론

  • 토론 시간 = clamp(생존자 수 × discussionPerAlive, discussionMin, discussionMax).
  • timeAdjust on: 생존자는 하루 1회 "+15초" 또는 "-15초"를 누를 수 있다(누가 눌렀는지 공개). 남은 시간은 최소 5초 아래로 내려가지 않는다.
  • 살아 있는 사람 모두 낮 채팅 가능.

5.3 지목 투표

  • 생존자는 다른 생존자 1명 또는 "기권"을 선택. 자기 자신은 지목 불가. 시간 내 변경 가능, 마감 시점 선택이 유효. 미투표 = 기권.
  • 정치인 표는 2표.
  • 최다 득표자가 정확히 1명이고 득표가 1표 이상이면 그 사람이 "피고". 동률 또는 전원 기권이면 처형 없이 밤으로.
  • openVote on이면 마감 후 누가 누구를 찍었는지 공개, off면 득표수만.

5.4 최후의 반론과 찬반 투표

  • 최후의 반론: 피고만 낮 채팅에 쓸 수 있다(다른 생존자 채팅 잠금).
  • 찬반 투표: 피고를 제외한 생존자가 "찬성/반대" 선택. 미투표는 집계 제외. 정치인 2표.
  • 찬성 > 반대면 처형, 같거나 적으면 생존. 찬반 결과는 수만 공개(익명).
  • 처형 시 deathReveal에 따라 공개. 피고가 정치인이면 처형 무효, 정치인임이 공개되고 생존.

5.5 밤

  • 시간 nightSeconds. 밤에는 낮 채팅 잠금.
  • 동시 행동(모두 마감 전까지 변경 가능, 마감 시점 값으로 처리):
    • 마피아: 살아 있는 비마피아 1명을 공용 대상으로 지정(마피아끼리 서로 보임, 마지막 변경이 유효). 마피아 채팅 가능.
    • 경찰: 살아 있는 다른 1명 조사. 결과는 선택 즉시가 아니라 밤 마감 시 경찰에게만 전달(대상 변경으로 여러 명 조사하는 것을 막기 위해).
    • 의사: 살아 있는 1명(옵션에 따라 자신 포함, 연속 불가) 치료.
    • 영매: 죽은 사람 1명의 직업 확인(밤 마감 시 전달), 죽은 사람 채팅 참여.
    • 연인: 연인 채팅.
  • 처리 순서(밤 마감 시):
    1. 마피아 대상 확정(없으면 mafiaMustKill에 따라 미처치 또는 RNG 무작위).
    2. 대상 == 의사 치료 대상 → 생존, 아침에 "의사의 치료로 누군가 살아났습니다" 발표(이름은 공개하지 않음).
    3. 대상이 군인이고 첫 공격 → 생존, "군인 ○○이 공격을 버텼습니다" 공개.
    4. 아니면 사망, deathReveal에 따라 공개.
    5. 경찰·영매 결과 개인 전달.
  • 경찰/의사가 그 밤에 죽어도 그 밤의 행동은 유효(동시 처리).

5.6 채팅 채널(서버가 강제)

채널 쓰기 읽기 열리는 시간
day 생존자(최후의 반론 중엔 피고만) 전원(관전자·죽은 사람 포함) 낮 토론·투표·반론·찬반
mafia 살아 있는 마피아 마피아(죽은 마피아 포함), deadSeeAll이면 죽은 사람 밤(쓰기), 기록은 마피아가 언제든 열람
dead 죽은 사람, 밤의 영매 죽은 사람, 영매(밤에만 표시) 항상(영매는 밤)
lovers 살아 있는 연인 연인 밤
system 서버 전원 항상
  • 메시지 제한: 200자, 0.7초당 1개(서버 레이트 리밋), kidSafeChat 필터. 위반 시 메시지 거부 사유 표시.
  • 죽은 사람의 메시지가 살아 있는 사람에게 보이는 경로는 절대 없어야 한다(영매 제외).
  • 외부 음성(디스코드)으로 정보를 흘리는 것은 막을 수 없으므로 규칙 보기에 "죽은 사람은 말하지 않기" 에티켓을 안내한다.

6. 승패와 점수 계산

  • 시민 팀 승리: 살아 있는 마피아 0명.
  • 마피아 팀 승리: 살아 있는 마피아 수 >= 살아 있는 비마피아 수.
  • 판정 시점: 처형 결과 직후, 아침 발표 직후, 연결 끊김 강제 탈락 직후.
  • 예: 생존 마피아 2, 시민 3 상태에서 낮에 시민 1명이 처형 → 2 대 2 → 즉시 마피아 승리.
  • 예: 4인(마피아1)에서 첫 밤 시민 사망 → 1 대 2 → 계속. 다음 낮 오처형 → 1 대 1 → 마피아 승리.
  • 무승부 없음. 동시에 두 조건이 될 수 없다(마피아 0이면 시민 승).
  • 점수(방 내 기록): 승리 팀 전원 1점(죽은 팀원 포함). MVP 표시(선택): 마피아를 지목해 처형시킨 표를 가장 많이 던진 시민.

7. 지역 룰 / 하우스 룰 (옵션으로 켜고 끄기)

옵션 내용 기본값
deathReveal=none 블라인드 룰(사망자 정보 비공개). 마피아에게 유리, 영매가 의미 있어짐 team
doctorSelfHeal=off 의사 자힐 금지(국내 오프라인에서 흔함) on
doctorRepeat=off 같은 대상 연속 치료 금지 on
noVoteDayOne 인사 낮을 생략하고 첫날부터 투표 firstPhase로 표현
policeRevealRole 경찰이 "마피아 여부" 대신 정확한 직업을 앎 off
mafiaMustKill=random 마피아가 대상 미선택 시 무작위 처치 noKill

8. 엔진 설계

8.1 상태(State)

type Role = 'mafia' | 'police' | 'doctor' | 'citizen' | 'soldier' | 'politician' | 'medium' | 'lover';
type Phase =
  | { kind: 'roleReveal'; endsAt: number }
  | { kind: 'greeting'; endsAt: number }
  | { kind: 'discussion'; endsAt: number; adjustedBy: PlayerId[] }
  | { kind: 'vote'; endsAt: number; votes: Record<PlayerId, PlayerId | 'abstain'> }
  | { kind: 'defense'; endsAt: number; accused: PlayerId }
  | { kind: 'confirm'; endsAt: number; accused: PlayerId; ballots: Record<PlayerId, 'yes' | 'no'> }
  | { kind: 'night'; endsAt: number; mafiaTarget: PlayerId | null; mafiaPickedBy: PlayerId | null;
      investigate: PlayerId | null; heal: PlayerId | null; mediumPeek: PlayerId | null }
  | { kind: 'ended' };

interface ChatMessage { id: number; channel: 'day' | 'mafia' | 'dead' | 'lovers' | 'system'; from: PlayerId | null; text: string; at: number; day: number }

interface MafiaState {
  options: MafiaOptions;
  rng: RngState;
  players: { id: PlayerId; role: Role; alive: boolean; soldierUsed: boolean; diedDay: number | null; diedBy: 'vote' | 'mafia' | 'disconnect' | null }[];
  day: number;
  phase: Phase;
  lastHealTarget: PlayerId | null;
  privateResults: { to: PlayerId; day: number; text: string }[];   // 경찰/영매 결과
  chat: ChatMessage[];                                                // 서버 상태에 전부 저장, view에서 필터
  lastChatAt: Record<PlayerId, number>;                               // 레이트 리밋
  history: PublicEvent[];                                             // 공개 발표 기록
  winner: null | 'town' | 'mafia';
}

8.2 액션

type payload 누가 / 언제 검증 조건
chat { channel, text } 채널 쓰기 권한자(5.6) 권한·페이즈 일치, 1~200자, 레이트 리밋, 필터 통과
adjustTime { delta: 15 | -15 } 생존자, discussion timeAdjust on, 오늘 미사용
vote { target: PlayerId | 'abstain' } 생존자, vote 대상 생존·본인 아님
confirm { choice: 'yes' | 'no' } 피고 제외 생존자, confirm —
mafiaTarget { target: PlayerId | null } 살아 있는 마피아, night 대상 생존·비마피아
investigate { target } 살아 있는 경찰, night 대상 생존·본인 아님
heal { target } 살아 있는 의사, night 대상 생존, 자기면 doctorSelfHeal, 연속이면 doctorRepeat
mediumPeek { target } 살아 있는 영매, night 대상 사망자
phaseTimeout {} 서버 내부(타이머) now >= phase.endsAt
readyEarly {} 생존자, discussion 생존자 전원이 누르면 토론 즉시 종료(선택 기능)

페이즈 전환은 서버 타이머가 phaseTimeout을 apply하는 방식으로 일관되게 처리한다(재생 가능).

8.3 공개/비공개 정보 (view)

  • 모두: 생존 여부, 페이즈와 남은 시간, 공개 발표(사망자, deathReveal에 따른 소속/직업), 낮 채팅, 투표 집계(openVote에 따라), 찬반 수, 공개된 군인/정치인.
  • 본인: 자기 직업. 마피아는 동료 목록 + 현재 공용 대상 + mafia 채팅. 연인은 상대 + lovers 채팅. 경찰/영매는 자기 결과 목록. 의사는 자기 치료 대상.
  • 죽은 사람: 위 + dead 채팅. deadSeeAll on이면 전원 직업 + mafia 채팅 열람.
  • 관전자: day, system과 공개 발표만. 진행 중에는 직업을 절대 받지 않는다.
  • 밤 행동 대상(경찰 조사, 의사 치료, 마피아 대상)은 해당 권한자 외에는 보내지 않는다. 밤에 "누가 행동을 마쳤는지"도 보내지 않는다(직업 유추 방지).
  • 채팅 필터링은 반드시 view에서 채널 권한으로 한다. 클라이언트에 전체 chat을 보내고 숨기는 방식 금지.
  • 게임 종료 후 전체 직업과 모든 채널 공개(선택: 마피아 채팅 공개 여부 방장 설정).

8.4 랜덤 요소 (시드 RNG 사용 지점)

직업 배정, mafiaMustKill=random일 때 대상 선택. 그 외에는 없음.

8.5 타이머·시간 초과·연결 끊김 시 자동 행동

  • deadline(state) = phase.endsAt(모든 페이즈가 시간제). activePlayers: 낮 = 생존자 전원, 밤 = 행동 직업 보유 생존자, 찬반 = 피고 제외 생존자.
  • onTimeout(player): vote → abstain, confirm → 미투표(집계 제외), night → 행동 없음. 페이즈 자체는 phaseTimeout으로 넘어간다.
  • 연결 끊김: 진행은 멈추지 않는다. 같은 플레이어가 연속 2번의 낮 동안 접속하지 않으면 "자리 비움 탈락" 처리(사망과 동일, deathReveal 적용). 이후 승리 판정.
  • 재접속 시 현재 view 전체(허용된 채팅 기록 포함) 재전송.

8.6 종료 조건과 결과(GameResult)

interface MafiaResult {
  winner: 'town' | 'mafia';
  days: number;
  players: { id: PlayerId; role: Role; team: 'town' | 'mafia'; alive: boolean; diedDay: number | null; diedBy: string | null; won: boolean }[];
  ranking: PlayerId[];  // 승리 팀(생존자 먼저) → 패배 팀
  summary: string;      // 예: "시민 승리! 3일차 낮 투표로 마지막 마피아를 찾아냈습니다."
}

9. UI/UX

  • 모바일 세로: 상단 페이즈 배너(낮=밝은 색, 밤=어두운 색) + 큰 남은 시간. 가운데 채팅(채널 탭: 전체 / 마피아 / 죽은 사람 / 연인 — 권한 있는 탭만 표시). 하단 입력창 + 상황별 큰 버튼.
  • 플레이어 목록: 아바타 그리드(2~3열), 죽은 사람은 흑백 + 사망 표시. 투표·밤 행동 시 그리드에서 탭으로 선택, 고를 수 있는 사람만 강조.
  • 내 직업은 상단 "내 직업" 버튼을 눌러야 보인다(화면 공유·옆 사람 방지). 밤 행동은 "오늘 밤 할 일: 조사할 사람을 고르세요" 문장으로 안내.
  • 밤 화면은 행동 없는 직업도 같은 레이아웃("밤입니다. 잠시 기다려 주세요")으로 보여 줘서 화면만으로 직업이 드러나지 않게 한다.
  • 투표 결과 애니메이션: 득표 막대. 처형/사망 발표는 큰 카드 팝업.
  • 어린이용: 직업 설명 그림 카드, "규칙 보기"에 낮/밤 흐름 그림.
  • PC: 왼쪽 플레이어 목록, 가운데 채팅, 오른쪽 행동 패널.

10. 테스트 체크리스트

  • 4~12인 자동 배분이 표와 일치, 추가 직업 선택 시 시민 수만큼만 대체, 부족하면 대기실 거부.
  • 지목 투표 동률 → 처형 없음. 전원 기권 → 처형 없음. 정치인 2표로 동률이 깨지는 경우.
  • 찬반 동률 → 생존. 정치인 피고 찬반 통과 → 정치인 공개 후 생존.
  • 의사가 마피아 대상 치료 → 사망 없음, 발표에 이름 미포함. doctorSelfHeal/doctorRepeat off 시 검증 거부.
  • 군인: 첫 공격 생존·공개, 두 번째 사망. 의사 치료와 겹치면 의사 우선(군인 능력 미소모).
  • 마피아 2명이 서로 다른 대상 선택 → 마지막 선택만 유효, 대상 미선택 + noKill → 사망 없음.
  • 경찰이 밤 중 대상 3번 변경 → 마감 시 마지막 1명 결과만 수신.
  • 승리 판정: 처형 직후 마피아 = 비마피아 → 즉시 마피아 승리(밤으로 넘어가지 않음).
  • 채팅 권한: 밤에 시민의 day 채팅 거부, 반론 중 피고 외 거부, 죽은 사람의 day 채팅 거부, 레이트 리밋 초과 거부.
  • 비공개 누출 검사: 각 생존 시민/관전자 view 직렬화 결과에 다른 사람 role, mafia/dead/lovers 메시지, 밤 행동 대상, privateResults가 없는지 자동 검사. 영매는 낮 동안 dead 메시지를 받지 않는지.
  • 밤 화면 view 형태가 시민과 경찰·의사에서 필드 구성상 구별되지 않는지(행동 정보는 별도 private 필드로만).
  • 연결 끊김: 2번의 낮 동안 미접속 → 자리 비움 탈락, 그 결과로 승리 조건 충족 시 종료.
  • timeAdjust: 1인 1일 1회, -15초로 5초 미만 내려가지 않음.
  • 같은 시드와 액션(채팅 포함) 로그 재생 시 동일 결과.

11. 참고 자료

12. 메모 (상표·법적 주의 등)

  • "마피아 게임"은 공유 놀이 이름이라 상표 문제가 거의 없다. "마피아42"는 특정 회사 제품명이므로 언급·모방 표기를 피한다. 표시 이름 후보: "마피아", "마피아 찾기", "밤의 마을".
  • 아동 이용자: "처치/처형" 표현을 "탈락/퇴장"으로 바꾸는 어린이 표현 모드 검토. 채팅이 핵심이므로 신고 버튼, 방장 강퇴, 비속어 필터, 개인정보(전화번호 등) 패턴 차단을 기본 제공.
  • 채팅 로그 보관: 신고 처리용으로 짧게(예: 7일) 보관하고 이후 삭제. 개인정보 처리방침에 명시.
  • 도박 요소 없음.