Files
joke-app/docs/games/davinci-code.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

18 KiB
Raw Blame History

다빈치 코드 (davinci-code)

마일스톤: M4 · 인원: 최소 2 ~ 최대 4명 · 예상 시간: 약 1015분 · 난이도: 쉬움보통

1. 개요

  • 검은색과 흰색 숫자 타일(0~11)을 작은 수부터 큰 수 순서로 세워 두고, 상대 타일의 숫자를 추리해 맞히는 게임이다. 상대의 숨은 타일을 모두 드러내면 그 상대는 탈락하고, 마지막까지 숨은 타일이 남은 사람이 이긴다.
  • 한국에서는 코리아보드게임즈 판으로 널리 알려졌고, 학교 수업과 방과후 교실의 추리 게임으로 많이 쓰인다. 예능 "더 지니어스"의 "흑과 백"으로도 유명하다.
  • 인원 근거: 원작 박스와 나무위키 모두 2~4명이다.

2. 모드와 옵션

옵션 키 설명 선택지 기본값
jokers 조커(줄 표시 타일, 검정·흰색 각 1개) 사용 on / off off
tieOrder 같은 숫자일 때 색 순서 black-left(검정이 왼쪽) / white-left black-left
startTiles 시작 타일 수 auto(2·3인 4개, 4인 3개) / 3 / 4 / 5 auto
chooseColors 타일을 가져올 때 색을 고르게 하기(실물 규칙, 뒷면 색이 보임) on / off(무작위) on
emptyPoolPenalty 더미가 비었을 때 틀리면 reveal-own(자기 숨은 타일 1개 공개) / none(벌칙 없음) reveal-own
turnSeconds 추리 제한 시간 20 / 30 / 45 / 60 30
deductionHelper 공개 정보로 가능한 숫자 후보 표시(초보 도움) on / off off

3. 구성물

  • 검은 타일 12개(011), 흰 타일 12개(011).
  • 조커 2개(검정 1, 흰색 1). 숫자 대신 "-"가 그려져 있다. jokers = on일 때만 쓴다.
  • 합계 24개(조커 사용 시 26개).
  • 타일 뒷면은 그 타일의 색이다. 그래서 숨은 타일도 색은 모두에게 보이고 숫자만 숨겨진다.

4. 준비(셋업)

  1. 모든 타일을 뒤집어 가운데에 섞어 둔다(더미).
  2. 선 플레이어를 RNG로 정하고, 진행은 시계 방향이다.
  3. 시작 타일 가져오기
    • 인원별 개수: 2·3인 4개, 4인 3개(startTiles).
    • chooseColors = on: 각자 "검은색 몇 개, 흰색 몇 개"를 동시에 고른다(예: 검2 흰2). 고른 색의 타일 중에서 서버가 RNG로 뽑는다. 한 색이 모자라면 남은 수까지만 고를 수 있다. 선택 마감은 15초이고, 넘기면 가능한 한 반반으로 자동 선택한다.
    • chooseColors = off: 서버가 무작위로 나눠 준다.
    • 시작 타일에는 조커가 들어가지 않는다. 시작 분배는 조커를 뺀 타일에서만 하고, 조커는 분배가 끝난 뒤 더미에 섞는다.
  4. 각자 받은 타일을 정렬 규칙(5.1)에 따라 자기 앞에 세운다. 서버가 자동으로 정렬한다.

5. 진행 규칙

5.1 정렬 규칙

  • 자기 줄은 왼쪽에서 오른쪽으로 숫자가 커진다.
  • 같은 숫자의 검정과 흰색이 함께 있으면 tieOrder에 따라 놓는다(기본: 검정이 왼쪽).
  • 조커는 숫자가 없으므로 가져온 사람이 원하는 위치(아무 틈)에 끼운다. 한 번 놓은 뒤에는 옮길 수 없다.
  • 줄의 타일 위치는 바뀌지 않고, 새 타일만 정렬 위치에 끼워진다. 상대 입장에서 위치가 추리의 중요한 단서다.

5.2 턴 순서

  1. 가져오기: 더미에서 타일 1개를 가져온다(chooseColors = on이면 색을 골라서). 이 타일을 "이번 턴 타일"이라 하고, 아직 줄에 끼우지 않고 따로 둔다. 숫자는 본인만 본다.
    • 더미가 비었으면 이 단계는 건너뛴다.
  2. 추리: 아직 탈락하지 않은 상대 한 명의 숨은 타일 하나를 고르고 숫자를 말한다.
    • 말할 수 있는 값: 0~11, 그리고 jokers = on이면 "조커".
    • 그 타일의 색은 이미 보이므로 숫자만 말한다.
  3. 결과
    • 맞힘: 그 타일이 앞면으로 공개된다(자리는 그대로). 그 상대의 숨은 타일이 0개가 되면 그 상대는 즉시 탈락한다. 이어서 둘 중 하나를 고른다.
      • 계속: 2단계로 돌아가 다시 추리한다(대상과 타일은 자유).
      • 멈춤: 이번 턴 타일을 숨긴 채 정렬 위치에 끼우고 턴을 끝낸다.
    • 틀림: 이번 턴 타일을 앞면으로 공개한 채 정렬 위치에 끼우고 턴을 끝낸다.
      • 더미가 비어 이번 턴 타일이 없으면 emptyPoolPenalty = reveal-own일 때 추리한 사람이 자기 숨은 타일 중 하나를 골라 공개한다. 그래서 자기 숨은 타일이 0개가 되면 본인이 탈락한다.
  4. 마지막 상대를 탈락시키면 그 즉시 게임이 끝난다(멈춤/계속 선택 없음).
  • 조커가 이번 턴 타일이면 끼울 때(멈춤 또는 틀림) 위치를 직접 고른다.
  • 이미 공개된 타일은 추리 대상이 아니다. 자기 타일도 대상이 아니다.
  • 이미 공개된 같은 색·같은 숫자 조합을 말하는 것처럼 논리적으로 불가능한 추리도 규칙상 허용한다(실물과 같음). UI에서만 경고한다.

5.3 예시

  • A의 줄(왼→오): [검?][흰?][검?][흰?]. 공개된 것은 없다.
  • B가 흰색 9를 가져온다. A의 두 번째 타일(흰)을 "3"이라고 말한다. 맞힘 → A의 흰3 공개.
  • B가 계속을 고른다. A의 네 번째(흰)를 "10"이라고 말한다. 틀림 → B의 흰9가 앞면으로 B의 줄 정렬 위치에 들어간다. 턴 종료.

6. 승패와 점수 계산

  • 숨은 타일이 1개라도 남은 플레이어가 1명만 남으면 그 사람이 승자다.
  • 순위: 승자 1위, 그다음은 늦게 탈락한 순이다.
  • 한 턴에 여러 명이 탈락할 수 있는데, 탈락 순서는 액션이 적용된 순서이므로 동시 탈락은 없다.
  • 점수는 없다. 결과 화면에 각자 "맞힌 횟수 / 추리 횟수"를 통계로 보여 준다.

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

  • 조커 사용(jokers). 원작 상급 규칙이고, 기본은 끔이다.
  • 같은 숫자 색 순서(tieOrder). 나무위키에 따르면 보통 검정이 왼쪽이지만 흰색 왼쪽이나 자유로 하는 경우도 있다. "자유"는 상대가 순서를 알 수 없어 추리가 달라지므로 지원하지 않는다.
  • 시작 타일 수 조정(startTiles). 어린이용 3개, 고수용 5개.
  • 더미가 빈 뒤 틀렸을 때 벌칙(emptyPoolPenalty). 원작 영문 규칙은 "타일 없이 계속 진행"만 적고 있다. 국내에서는 자기 타일 1개 공개가 널리 쓰여 기본값으로 한다(오너 확인 필요).
  • 2인용 변형(단서 타일을 공개로 하는 등)은 지원하지 않는다.

8. 엔진 설계

8.1 상태(State)

type TColor = 'black' | 'white';
interface DTile { id: string; color: TColor; n: number | 'joker' } // n: 0..11

interface RowTile { tileId: string; revealed: boolean }

interface DavinciState {
  options: DavinciOptions;
  rng: RngState;
  seats: PlayerId[];
  tiles: Record<string, DTile>;           // 정의(불변)
  pool: { black: string[]; white: string[] }; // 비공개, 색별로 섞인 더미
  rows: Record<PlayerId, RowTile[]>;      // 왼→오 순서. 숫자는 비공개
  phase:
    | { kind: 'chooseStart'; picks: Record<PlayerId, { black: number; white: number } | null> }
    | { kind: 'draw' }                                      // current가 색 선택
    | { kind: 'guess' }                                     // current가 추리
    | { kind: 'afterCorrect' }                              // 계속 / 멈춤
    | { kind: 'placeJoker'; reveal: boolean }               // 조커 위치 선택
    | { kind: 'revealOwn' }                                 // 더미 빈 뒤 틀림 → 자기 타일 공개
    | { kind: 'finished' };
  current: PlayerId;
  pending: string | null;                  // 이번 턴 타일(본인만 앎)
  alive: Record<PlayerId, boolean>;
  eliminationOrder: PlayerId[];
  stats: Record<PlayerId, { guesses: number; correct: number }>;
  history: { by: PlayerId; target: PlayerId; index: number; said: number | 'joker'; correct: boolean }[]; // 공개 기록
  phaseStartedAt: number;
  seq: number;
}

8.2 액션

type payload 누가 / 언제 검증 조건
chooseStart { black: number; white: number } 모든 플레이어, phase=chooseStart 합 = 시작 타일 수. 각 색의 남은 수 이하(먼저 제출한 사람의 선택을 빼고 계산하므로 서버 도착 순서). 한 번만 제출
draw { color?: TColor } current, phase=draw chooseColors=on이면 color 필수이고 그 색이 남아 있어야 함. 더미가 비었으면 이 phase 자체가 생략됨
guess { target: PlayerId; index: number; value: number | 'joker' } current, phase=guess target ≠ 본인, target 생존, rows[target][index]가 숨김 상태, value는 0~11 정수(또는 jokers=on이면 'joker')
continue {} current, phase=afterCorrect 생존한 상대가 남아 있음
stop {} current, phase=afterCorrect 언제나. 이번 턴 타일이 조커면 placeJoker로 넘어감
placeJoker { index: number } current, phase=placeJoker 0 ≤ index ≤ 줄 길이
revealOwn { index: number } current, phase=revealOwn 자기 줄의 숨은 타일
  • 일반 타일을 끼울 위치는 서버가 계산한다: 숫자 오름차순, 같은 숫자면 tieOrder. 조커는 정렬 계산에서 건너뛰고 자기 자리를 지킨다.
    • 끼울 위치 = 새 타일보다 "앞서야 하는" 비조커 타일 중 가장 오른쪽 타일의 바로 다음 인덱스. 그런 타일이 없으면 인덱스 0.
    • 비조커 타일은 항상 정렬되어 있으므로, 이 위치는 "뒤에 와야 하는" 타일들보다 반드시 앞이다. 조커가 그 사이에 있으면 새 타일은 조커의 왼쪽에 들어간다.
    • 실물에서는 조커 주변 배치를 본인이 고를 수 있지만, 엔진은 위 규칙으로 고정해 결정적으로 만든다.
  • reason 예: "이미 공개된 타일이에요", "자기 타일은 고를 수 없어요", "흰색 타일이 더 이상 없어요", "0부터 11 사이 숫자를 골라 주세요".

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

  • 모두(관전자 포함)
    • 모든 줄의 타일 색과 위치, 공개 여부, 공개된 타일의 숫자.
    • 더미 색별 남은 개수, 추리 기록(history), 생존 여부, 현재 phase, 마감 시각.
    • 이번 턴 타일의 색(실물에서도 보임). 숫자는 보이지 않는다.
  • 본인만: 자기 줄 숨은 타일의 숫자, 자기 이번 턴 타일의 숫자.
  • 시작 선택 단계: 다른 사람의 색 선택은 제출 여부만 보인다. 결과 색 구성은 분배 후 줄에 보이므로 그때 공개된다.
  • 절대 보내지 않는 것: 남의 숨은 숫자, 더미 순서, RNG 상태.
    • 숨은 타일은 view에서 { color, revealed:false }로만 내보낸다. 타일 ID도 보내지 않는다(ID에 숫자가 들어가면 유출). 위치 인덱스로만 가리킨다.
  • deductionHelper = on이면 후보 계산은 클라이언트에서 view 정보(공개 숫자 + 자기 숫자 + 위치·색)만으로 한다. 서버 비공개 정보는 쓰지 않는다.
    • 후보 계산: 각 숨은 타일에 대해 "같은 색의 미공개 숫자 ∖ 내 손의 같은 색 숫자" 중 좌우 공개 타일 사이 범위를 만족하는 값. 조커가 있을 수 있으면 범위 제한을 완화한다.

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

  • setup: 선 플레이어, 색별 더미 섞기. 조커는 시작 분배가 끝난 뒤 해당 색 더미의 무작위 위치에 끼운다.
  • chooseColors = off일 때 무작위 분배.
  • 그 외에는 RNG를 쓰지 않는다(가져오기는 색별 더미의 맨 앞).

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

  • 마감: chooseStart 15초, draw 10초, guess turnSeconds, afterCorrect 10초, placeJoker/revealOwn 10초.
  • onTimeout
    • chooseStart: 가능한 한 반반(홀수면 검정 하나 더).
    • draw: 더 많이 남은 색(같으면 검정).
    • guess: 숨은 타일이 가장 적은 생존 상대의 가장 왼쪽 숨은 타일을 고르고, 그 타일의 가능한 후보(8.3 후보 계산을 서버에서 공개 정보와 본인 정보만으로 수행) 중 가장 작은 값을 말한다. 결정적이라 재현할 수 있다.
    • afterCorrect: stop.
    • placeJoker: 맨 오른쪽.
    • revealOwn: 가장 왼쪽 숨은 타일.
  • 연결이 끊긴 플레이어도 탈락시키지 않고 위 자동 행동으로 진행한다. 연속 3턴 타임아웃이면 서버가 마감을 5초로 줄인다.

8.6 종료 조건과 결과(GameResult)

  • 생존자가 1명이면 finished.
interface GameResult {
  ranking: { player: PlayerId; rank: number; hiddenLeft: number; guesses: number; correct: number }[];
  winner: PlayerId[];
  finalRows: Record<PlayerId, { color: TColor; n: number | 'joker' }[]>; // 종료 후 전원 공개
  summary: string; // "철수 님의 암호가 끝까지 풀리지 않았어요!"
}
  • 게임이 끝난 뒤에는 모든 타일을 공개해도 된다(finalRows).

9. UI/UX

  • 모바일 세로 화면
    • 위: 상대 줄(2~3명). 타일은 검정·흰색 직사각형이고, 공개된 타일에는 숫자를 크게 쓴다. 각 타일 아래에 위치 번호(1, 2, 3…)를 작게 쓴다.
    • 가운데: 더미(검정 n개, 흰 n개 두 무더기). 이번 턴 타일 미리보기.
    • 아래: 내 줄. 내 숨은 타일 숫자는 나만 보이고, 테두리 점선으로 "상대에게 숨김"을 표시한다.
  • 조작
    1. 내 차례에 검정/흰 무더기 중 하나를 탭해서 가져온다.
    2. 상대 타일을 탭하면 그 타일 위에 0~11 숫자 패드(3×4, 버튼 56px)와 "조커" 버튼이 뜬다.
    3. 숫자를 누르고 "이 숫자!" 확인 버튼을 누른다.
    4. 결과는 뒤집기 애니메이션으로 보여 준다(맞힘: 초록 반짝임 / 틀림: 내 타일이 뒤집혀 공개되는 애니메이션).
    5. 맞혔으면 "한 번 더 맞히기"와 "여기서 멈추기" 큰 버튼 2개를 보여 준다. 멈출 때 위험도 안내("멈추면 가져온 타일이 숨겨진 채로 들어가요")를 띄운다.
  • 합법 수 하이라이트: 고를 수 있는 상대 타일(숨김 상태)만 테두리를 강조하고, 공개된 타일은 비활성이다.
  • 초보 도움말: "규칙 보기"에 정렬 규칙 그림(작은 수 왼쪽, 같은 숫자면 검정 왼쪽)을 넣는다. deductionHelper를 켜면 숫자 패드에서 불가능한 숫자를 흐리게 표시한다(누를 수는 있음).
  • 추리 기록 패널: "민수 → 영희 3번째 타일 = 7 (틀림)" 목록을 둔다. 노년층의 기억 부담을 줄인다.

10. 테스트 체크리스트

  • 셋업: 2·3인은 각 4개, 4인은 각 3개. 시작 타일에 조커가 없다(jokers=on이어도). 같은 시드면 같은 분배가 나온다.
  • chooseStart: 3인이 모두 "흰4"를 고르면 12개로 충분해 성공한다. 4인이 모두 "검3"을 고르는데 검정 12개면 성공하고, 이후 사람은 검정 0개만 남는다. 남은 수를 넘는 선택은 거부된다.
  • 정렬: [흰5, 검5, 검2]를 받으면 [검2, 검5, 흰5]가 된다(black-left). white-left이면 [검2, 흰5, 검5].
  • 조커: 조커를 가져와 틀렸을 때 placeJoker 단계에서 고른 위치에 앞면으로 들어간다. 이후 새 숫자 타일 정렬은 조커 위치를 바꾸지 않는다.
  • 맞힘 → 계속 → 틀림: 처음 맞힌 타일은 공개된 채로 남고, 이번 턴 타일은 앞면으로 내 줄에 들어간다.
  • 맞힘 → 멈춤: 이번 턴 타일이 숨긴 채로 들어간다. 다른 플레이어 view에는 그 타일의 색만 있다.
  • 마지막 숨은 타일을 맞히면 그 상대가 즉시 탈락한다. 2인이면 바로 게임이 끝나고 afterCorrect 단계가 생기지 않는다.
  • 더미가 빈 뒤: draw 단계가 생략된다. 틀리면 revealOwn 단계가 되고, 자기 마지막 숨은 타일을 공개하면 본인이 탈락한다.
  • 이미 공개된 타일이나 탈락자 타일, 자기 타일을 추리하려 하면 거부된다. value 12, -1, 3.5는 거부된다. jokers=off에서 'joker'도 거부된다.
  • 시간 초과: guess 시간 초과 때 자동 추리가 공개 정보만으로 결정적으로 정해진다(같은 상태 → 같은 추리). 상대 숨은 숫자를 참조하지 않는지 코드 리뷰로 확인한다.
  • 연결 끊김: 끊긴 플레이어 차례가 자동으로 진행되고, 게임이 멈추지 않는다.
  • 정보 유출: 관전자와 상대 view JSON에 남의 숨은 숫자, 숨은 타일 ID, 이번 턴 타일 숫자가 없다. 이벤트(tileDrawn)도 본인 외에는 색만 있다.
  • 정보 유출: 종료 후에는 finalRows로 전원 공개되는 것이 정상이다.

11. 참고 자료

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

  • "다빈치 코드"라는 이름은 국내 유통사가 쓰는 상품명이다(원작은 일본 게임 "Algo"/"Coda" 계열). 소설·영화 제목과도 겹친다. 상표 문제를 피하려면 표시 이름을 바꾸는 것을 권한다. 후보: "숫자 암호", "흑백 암호 추리", "비밀 숫자 맞히기"(사용자 선택). 코드 ID davinci-code는 내부용이다.
  • 타일 디자인은 단순한 검정·흰색 직사각형으로 직접 그린다.
  • 돈, 칩을 쓰지 않는다.