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

243 lines
18 KiB
Markdown
Raw Permalink 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.
# 그림 맞히기 (`catchmind`)
> 마일스톤: M8 · 인원: 최소 2 ~ 최대 10명 · 예상 시간: 약 10~20분 · 난이도: 쉬움
## 1. 개요
- 한 사람(출제자)이 제시어를 그림으로 그리고, 나머지가 채팅으로 정답을 맞히는 실시간 게임. 차례대로 돌아가며 출제자를 맡는다.
- 한국에서는 넷마블 "캐치마인드"(2000년대)와 방송·모바일 앱으로 전 연령에 익숙한 형식이다. 글자를 몰라도 그림으로 참여할 수 있어 어린이·어르신 모두 즐기기 좋다.
- 인원 근거: 출제자 1명 + 맞히는 사람 1명 이상이면 성립하므로 최소 2명. 최대는 캐치마인드 방 정원 관행과 채팅 가독성(모바일 한 화면)을 고려해 10명.
## 2. 모드와 옵션
| 옵션 키 | 설명 | 선택지 | 기본값 |
|---|---|---|---|
| `guessMode` | 맞히기 방식 | `multi`(여러 명이 맞힐 때까지) / `firstOnly`(첫 정답자가 나오면 즉시 종료, 원조 캐치마인드식) | `multi` |
| `rounds` | 라운드 수(1라운드 = 모두 1번씩 출제) | 1 / 2 / 3 | 2 |
| `drawSeconds` | 그리기 제한 시간 | 60 / 80 / 100 / 120 | 80 |
| `wordChoice` | 출제자가 3개 중 고르기 | on / off(무작위 1개) | on |
| `categories` | 사용할 단어 분야 | 3.2 분야 다중 선택 | 전체 |
| `difficulty` | 단어 난이도 | `easy` / `normal` / `mixed` | `mixed` |
| `customWords` | 방장 추가 단어(줄바꿈 구분, 최대 200개, 필터 통과분만) | 텍스트 | 없음 |
| `customOnly` | 추가 단어만 사용 | on / off | off |
| `hints` | 힌트 단계 | `none` / `basic`(글자 수) / `full`(글자 수 + 초성 + 한 글자) | `full` |
| `closeHint` | "아까워요" 개인 알림 | on / off | on |
| `letterReport` | 맞히는 사람이 "글자를 썼어요" 신고 투표 | on / off | on |
## 3. 구성물
### 3.1 그림판
- 논리 좌표: 1000 × 1000 정사각형(정수). 모든 기기에서 같은 비율로 축소/확대.
- 도구: 펜(굵기 4단계: 4, 8, 16, 32), 지우개(굵기 동일 4단계), 색 12개(검정, 흰색, 회색, 빨강, 주황, 노랑, 초록, 하늘, 파랑, 보라, 분홍, 갈색), 되돌리기(마지막 획 삭제), 전체 지우기.
- 채우기(페인트통)는 기기별 래스터 결과 차이 때문에 1차 범위에서 제외.
### 3.2 단어 목록 (한국어, 어린이 안전)
- 파일: `words.ko.json` — `{ word, aliases[], category, difficulty }`. 목표 600개 이상.
- 선정 기준: 2~5음절, 그림으로 그릴 수 있는 구체 명사 위주, 초등 저학년이 아는 말, 실존 인물·상표·캐릭터·정치·종교·폭력·신체 노출·질병 관련 단어 제외.
- 분야와 예시(쉬움 위주):
| 분야 키 | 이름 | 예시 |
|---|---|---|
| `animal` | 동물 | 고양이, 강아지, 기린, 코끼리, 펭귄, 거북이, 토끼, 사자, 문어, 나비 |
| `food` | 음식 | 김밥, 떡볶이, 피자, 라면, 수박, 아이스크림, 바나나, 케이크, 만두, 붕어빵 |
| `object` | 물건 | 우산, 안경, 가위, 시계, 열쇠, 연필, 의자, 냉장고, 칫솔, 선풍기 |
| `vehicle` | 탈것 | 자전거, 비행기, 기차, 배, 버스, 헬리콥터, 로켓, 소방차 |
| `nature` | 자연 | 무지개, 화산, 눈사람, 해바라기, 구름, 번개, 바다, 산 |
| `place` | 장소 | 학교, 병원, 놀이터, 수영장, 도서관, 동물원 |
| `job` | 직업 | 소방관, 요리사, 의사, 경찰관, 우체부, 화가 |
| `sport` | 운동·놀이 | 축구, 줄넘기, 수영, 연날리기, 그네, 미끄럼틀 |
| `body` | 몸 | 눈, 코, 손, 발, 귀, 이빨 (노출 관련 제외) |
| `season` | 계절·명절 | 송편, 떡국, 크리스마스트리, 눈싸움, 부채, 단풍 |
- `aliases` 예: `휴대폰` ↔ `핸드폰`, `휴대전화`, `스마트폰` / `경찰관` ↔ `경찰`. 별칭은 같은 대상을 가리키는 흔한 말만 넣고 상위·하위 개념(예: `아이스크림` → `아이스크림콘`)은 넣지 않는다.
- `customWords`는 같은 금칙어 필터를 통과해야 하며, 1~10자 한글/영문/숫자만 허용.
## 4. 준비(셋업)
1. 좌석 순서를 RNG로 섞는다. 이 순서가 출제 순서.
2. 옵션에 맞는 단어 풀을 만들고 RNG로 섞는다. 게임 중 같은 단어는 다시 나오지 않는다.
3. 점수 0으로 시작. 첫 출제자의 `choosing` 단계로.
## 5. 진행 규칙
### 5.1 한 차례(턴)의 흐름
1. **고르기(choosing, 10초)**: 출제자에게 단어 3개 제시(`wordChoice` off면 1개 자동). 시간 초과 시 첫 단어.
2. **그리기(drawing, `drawSeconds`)**: 출제자는 그리기만 가능(채팅 금지, 미리 정한 반응 버튼만). 나머지는 채팅으로 추측.
3. **공개(reveal, 5초)**: 정답과 이번 턴 점수 표시, 그림 저장(선택).
4. 다음 출제자. 모든 라운드가 끝나면 게임 종료.
### 5.2 그리기 종료 조건
- 시간 만료.
- `multi`: 출제자를 제외한 접속 중인 전원이 맞힘.
- `firstOnly`: 첫 정답자가 나오는 즉시.
- 출제자 연결 끊김 10초 경과(출제자 0점, 정답 공개).
- `letterReport` 신고가 맞히는 사람 과반(아직 못 맞힌 사람 포함 출제자 제외 전원 기준)에 도달 → 즉시 종료, 출제자 이번 턴 0점(맞힌 사람 점수는 유지).
### 5.3 정답 판정(정규화)
정답 후보 = `word` + `aliases`. 입력과 후보 모두 같은 함수로 정규화한 뒤 완전 일치 비교.
```
normalize(s): // 호출 전: 원문이 호환 자모로만 되어 있으면 정답 판정 생략
1. NFKC 정규화 (전각→반각, NFD로 분리된 한글 자모 → 완성형 결합)
2. 제로폭 문자(U+200B~U+200D, U+FEFF) 제거
3. 공백(모든 유니코드 공백) 제거
4. 문장부호·기호 제거: . , ! ? ~ - _ ' " ( ) [ ] … · 와 이모지
5. 영문 소문자화
6. 끝말 허용: 결과가 정답 + ("요" | "다" | "이다" | "입니다" | "인가" | "인가요") 형태면 끝말 제거 후 비교
```
- 예: `"아이스 크림!"` → `아이스크림` 정답. `"사과요"` → `사과` 정답. macOS 등에서 온 NFD 입력(조합형 자모 U+1100~U+11FF로 분리된 `사과`)도 NFKC로 완성형 `사과`가 됨. 주의: NFKC는 호환 자모(U+3131~U+318E)를 조합형으로 바꿔 `ㅅㅏㄱㅘ` 같은 낱자 입력을 `사과`로 결합해 버린다(실제 확인함). 그래서 정규화 전에 원문 검사를 먼저 한다: 원문(공백 제외)이 호환 자모로만 이루어져 있으면 정답 판정을 하지 않고 아래 초성 규칙으로 처리한다.
- 초성만 입력(`ㅅㄱ`)은 정답 아님. 호환 자모(U+3131~U+318E)만으로 된 메시지가 정답의 초성과 같으면 다른 사람에게 보이지 않게 막는다(힌트 유출 방지).
- **아까워요(`closeHint`)**: 정답이 아니고, 입력과 정답을 자모 단위로 분해(초성·중성·종성, 겹받침·이중모음도 분해)한 뒤 레벤시테인 거리가 1(정답 3음절 이하) 또는 2(4음절 이상) 이하이면 보낸 사람에게만 "아까워요!" 표시. 메시지는 다른 사람에게 그대로 보인다.
- 정답 메시지 자체는 다른 사람에게 보내지 않고 "○○님이 맞혔어요!" 시스템 메시지로 대체.
- 정답을 포함하지만 정답이 아닌 메시지(예: "사과나무 아님")도 다른 사람에게 보내지 않고, 보낸 사람에게 "정답이 들어간 말은 보낼 수 없어요" 안내.
- 이미 맞힌 사람의 채팅은 출제자와 이미 맞힌 사람에게만 보인다(스포일러 방지). 시스템이 "맞힌 사람끼리 대화" 라벨 표시.
### 5.4 힌트 공개 시점(`hints=full`)
- 시작 즉시: 글자 수(`○○○`, 띄어쓰기 위치 포함).
- 50% 경과: 모든 글자의 초성(`ㅇㅇㅅㅋㄹ`).
- 75% 경과: RNG로 고른 한 글자를 완성형으로 공개(`ㅇ이ㅅㅋㄹ`). 단, 2글자 이하 단어는 이 단계 생략(정답이 거의 드러남).
- `basic`은 글자 수만, `none`은 아무것도 없음.
- 힌트는 이미 맞힌 사람·출제자에게도 같은 화면 요소로 표시(정답은 별도 표시).
### 5.5 그림 스트리밍(WebSocket)
- 클라이언트 샘플링: Pointer Events(`getCoalescedEvents` 사용), 논리 좌표로 변환 후 정수 반올림, 직전 점과 거리 2 미만이면 버림.
- 배치: 50ms마다 또는 점 32개가 쌓이면 1개 메시지로 전송(최대 초당 20회). 획 시작/끝은 즉시 전송.
- 메시지(JSON, 짧은 키):
```json
{"t":"act","a":{"type":"draw","op":{"k":"b","s":17,"c":3,"w":2,"e":0}}}
{"t":"act","a":{"type":"draw","op":{"k":"p","s":17,"q":5,"d":[412,330,3,-1,4,0,5,2]}}}
{"t":"act","a":{"type":"draw","op":{"k":"e","s":17}}}
```
- `k`: `b`(획 시작: `s` 획 번호, `c` 색 인덱스, `w` 굵기 인덱스, `e` 지우개 0/1), `p`(점 묶음: `q` 배치 순번, `d` 첫 점은 절대 좌표, 이후는 이전 점과의 차이), `e`(획 끝), `u`(되돌리기), `x`(전체 지우기).
- 서버 검증: 출제자 본인, drawing 단계, 획 번호는 직전 시작 획 번호 + 1, 배치 순번 연속, 점 개수 1~64, 좌표 0~1000, 초당 30 메시지 이하, 턴당 누적 점 30,000 이하(초과 시 거부하고 출제자에게 안내).
- 서버는 검증 통과 즉시 다른 참가자에게 그대로 중계(브로드캐스트)하고 턴의 그림 기록에 추가한다. 재접속/중간 입장 관전자는 기록 전체를 한 번에 받아 다시 그린다.
- 수신 측 렌더링: 받은 점을 이어 중간점 2차 곡선으로 부드럽게 그림. 패킷 지연으로 늦게 와도 순서는 WebSocket(TCP) 순서 보장에 의존.
- 대역폭 추정: 초당 20 메시지 × 약 120바이트 = 약 2.4KB/s/시청자. 10명 방에서 서버 송신 약 22KB/s.
- 프레임워크 요구사항: 그림 액션은 다른 액션처럼 로그에 남겨 재생 가능해야 하지만, 매 액션마다 전체 view를 다시 보내면 안 된다. `apply`가 내보내는 `canvasOp` 이벤트만 증분 전송하고, 전체 그림은 (재)접속 시에만 view로 보낸다. 상태의 그림 기록은 추가 전용 배열로 두고 액션마다 깊은 복사하지 않는다.
## 6. 승패와 점수 계산
- `multi` 모드: k번째 정답자(1부터) 점수 = `max(10 - 2 × (k - 1), 4)` → 10, 8, 6, 4, 4, ...
- 출제자 점수 = 정답자 1명당 2점, 최대 12점. 아무도 못 맞히면 0점.
- `firstOnly` 모드: 정답자 10점, 출제자 5점.
- 신고 투표로 종료된 턴: 출제자 0점.
- 최종 순위: 총점 내림차순 → 동점이면 정답 횟수 많은 순 → 그래도 같으면 공동 순위.
- 예(5인, multi): 출제자 A, C가 1등·E가 2등으로 맞히고 B·D 실패 → C 10, E 8, A 4.
## 7. 지역 룰 / 하우스 룰 (옵션으로 켜고 끄기)
| 옵션 | 내용 | 기본값 |
|---|---|---|
| `firstOnly` | 원조 캐치마인드처럼 첫 정답자만 득점, 즉시 다음 문제 | off(`multi`) |
| `answererDraws` | 정답자가 다음 출제자가 됨(원조 방식, `firstOnly`에서만) | off |
| `noHints` | 힌트 없음(고수 방) | off |
| `speedBonus` | 남은 시간 비율 × 5점 추가 | off |
## 8. 엔진 설계
### 8.1 상태(State)
```ts
type CanvasOp =
| { k: 'b'; s: number; c: number; w: number; e: 0 | 1 }
| { k: 'p'; s: number; q: number; d: number[] }
| { k: 'e'; s: number }
| { k: 'u' }
| { k: 'x' };
interface ChatEntry { id: number; from: PlayerId; text: string; at: number; audience: 'all' | 'solvers' | 'self'; kind: 'chat' | 'correct' | 'close' | 'blocked' }
interface CatchmindState {
options: CatchmindOptions;
rng: RngState;
order: PlayerId[];
scores: Record<PlayerId, number>;
correctCounts: Record<PlayerId, number>;
wordPool: WordEntry[]; // 남은 단어(섞인 순서)
round: number; // 1..rounds
drawerIndex: number;
turn: {
phase: 'choosing' | 'drawing' | 'reveal';
drawer: PlayerId;
choices: WordEntry[]; // choosing 단계 후보
word: WordEntry | null;
startedAt: number; endsAt: number;
hintStage: 0 | 1 | 2; // 0 글자 수, 1 초성, 2 한 글자
revealIndex: number | null; // RNG로 고른 공개 글자 위치
solvers: { id: PlayerId; at: number; points: number }[];
reports: PlayerId[];
canvas: CanvasOp[]; // 추가 전용
totalPoints: number;
lastStroke: number; lastBatch: number;
};
chat: ChatEntry[];
finished: boolean;
}
```
### 8.2 액션
| type | payload | 누가 / 언제 | 검증 조건 |
|---|---|---|---|
| `chooseWord` | `{ index: 0 \| 1 \| 2 }` | 출제자, choosing | 후보 범위 |
| `draw` | `{ op: CanvasOp }` | 출제자, drawing | 5.5의 서버 검증 |
| `guess` | `{ text }` | 출제자 제외, drawing | 1~40자, 초당 3개 이하. 이미 맞힌 사람이면 일반 채팅으로 처리 |
| `chat` | `{ text }` | 출제자 제외, choosing/reveal | 1~100자, 레이트 리밋, 정답 포함 여부 검사 |
| `reaction` | `{ code }` | 출제자, drawing | 미리 정한 반응 목록("거의 다 왔어요", "아니에요", "비슷해요") |
| `report` | `{}` | 출제자 제외, drawing, `letterReport` on | 1인 1회 |
| `tick` | `{}` | 서버 타이머 | 힌트 단계 진행, 시간 만료 처리 |
### 8.3 공개/비공개 정보 (view)
- 모두: 순서, 점수, 라운드, 출제자, 단계, 남은 시간, 힌트(글자 수/초성/공개 글자), 그림 기록, 정답자 목록(이름과 순서), 공개 채팅.
- 출제자: 위 + 고르기 후보 + 정답 단어.
- 이미 맞힌 사람: 위 + 정답 단어 + `solvers` 채팅.
- 아직 못 맞힌 사람·관전자: 정답 단어 없음, `solvers` 채팅 없음, 다른 사람의 `close`/`blocked` 메시지 없음.
- reveal 단계부터는 모두에게 정답 공개.
- 단어 풀(앞으로 나올 단어)은 누구에게도 보내지 않는다.
### 8.4 랜덤 요소 (시드 RNG 사용 지점)
좌석(출제) 순서, 단어 풀 셔플, `wordChoice` off일 때 단어, 75% 힌트 글자 위치.
### 8.5 타이머·시간 초과·연결 끊김 시 자동 행동
- `deadline`: `turn.endsAt`과 다음 힌트 시점 중 이른 시각(서버가 `tick` 적용).
- `onTimeout`: choosing → `chooseWord {index: 0}`. drawing/reveal은 `tick`으로 진행.
- 출제자 연결 끊김: choosing 중이면 즉시 자동 선택 후 10초 대기, drawing 중 10초 이상이면 턴 종료. 재접속하지 않은 플레이어의 다음 출제 차례는 건너뜀(점수 유지).
- 맞히는 사람 연결 끊김: `multi`의 "전원 정답" 판정에서 제외.
### 8.6 종료 조건과 결과(GameResult)
```ts
interface CatchmindResult {
ranking: { rank: number; id: PlayerId; score: number; correct: number }[];
turns: { drawer: PlayerId; word: string; solvers: PlayerId[] }[];
summary: string; // 예: "1등 지민님 54점! 가장 많이 맞힌 사람도 지민님"
}
```
게임 종료: 마지막 라운드의 마지막 출제자 턴의 reveal 종료 시.
## 9. UI/UX
- 모바일 세로: 상단 정답 힌트 줄(`○○○` 큰 글씨) + 남은 시간 막대. 그 아래 정사각형 그림판(화면 폭 100%). 출제자는 그림판 아래 도구 막대(색 12개 원형 48px, 굵기 4단계, 지우개, 되돌리기, 전체 지우기). 맞히는 사람은 그림판 아래 채팅 목록 + 입력창(입력창은 키보드 위에 고정).
- 점수판: 상단 가로 스크롤 아바타 + 점수, 맞힌 사람은 초록 체크.
- 정답 시: 화면 가운데 "정답!" 애니메이션 + 효과음, 입력창에 정답이 그대로 남지 않게 비움.
- 어린이·어르신 배려: "쉬운 단어만" 기본 프리셋, 단어 고르기 버튼에 그림 아이콘, 큰 글자 모드. 음성 입력(브라우저 받아쓰기)도 같은 정규화로 판정됨.
- 그림 저장: 턴 종료 시 그림을 PNG로 내려받기(클라이언트 렌더).
- PC: 그림판 왼쪽, 채팅 오른쪽.
## 10. 테스트 체크리스트
- [ ] 정규화: `"아이스 크림!"`, `"아이스크림요"`, NFD 입력, 전각 영문(`ABC`), 제로폭 문자 포함 입력이 모두 정답 처리.
- [ ] 초성만 입력(`ㅇㅇㅅㅋㄹ`)은 정답 아님 + 다른 사람에게 비공개. 호환 자모 낱자 입력 `ㅅㅏㄱㅘ`도 (NFKC로 `사과`가 되지만) 정답 아님.
- [ ] 끝말 허용이 정답 자체를 깎지 않음: 정답 `바다`에 `바` 입력은 오답, `바다요`는 정답.
- [ ] 정답 포함 비정답 메시지("사과나무") 다른 사람에게 비공개, 보낸 사람에게 안내.
- [ ] 아까워요: `코끼리` 정답에 `코끼라` 입력 → 본인에게만 close 알림, 다른 사람에게는 일반 채팅.
- [ ] multi 점수: 4명 순서대로 정답 시 10/8/6/4, 출제자 8점. 출제자 최대 12점 상한.
- [ ] firstOnly: 첫 정답 즉시 턴 종료, 정답자 10·출제자 5.
- [ ] 힌트: 50%에 초성, 75%에 한 글자. 2글자 단어는 75% 단계 생략.
- [ ] 그림 검증: 출제자 아닌 사람의 draw 거부, 좌표 1001 거부, 점 65개 배치 거부, 획 번호 건너뛰기 거부, 초당 31번째 메시지 거부.
- [ ] 재접속: 그리기 도중 재접속한 맞히는 사람이 같은 그림 기록 전체를 받음(획 수·점 수 일치).
- [ ] 출제자 연결 끊김 10초 → 턴 종료, 정답 공개, 다음 출제자.
- [ ] 신고 과반 → 턴 종료, 출제자 0점.
- [ ] 비공개 누출 검사: 못 맞힌 사람·관전자 view와 이벤트에 정답 단어, 다른 후보 단어, 단어 풀, solvers 채팅이 없는지 자동 검사(그리기 중 모든 시점).
- [ ] 같은 단어가 한 게임에서 두 번 나오지 않음.
- [ ] 같은 시드 + 액션 로그 재생 시 점수·그림 기록 동일.
## 11. 참고 자료
- 나무위키 "캐치마인드" — https://namu.wiki/w/캐치마인드
- Wikipedia "Pictionary" (그림 맞히기 일반 규칙) — https://en.wikipedia.org/wiki/Pictionary
- Unicode Standard Annex #15 정규화 형식(NFKC) — https://unicode.org/reports/tr15/
- 한글 음절 분해(유니코드 한글 음절 공식: (음절 - 0xAC00) = (초성×21 + 중성)×28 + 종성) — https://www.unicode.org/versions/latest/ch03.pdf (3.12절)
- MDN Pointer Events / getCoalescedEvents — https://developer.mozilla.org/docs/Web/API/PointerEvent/getCoalescedEvents
## 12. 메모 (상표·법적 주의 등)
- "캐치마인드"는 넷마블의 상표이고 "Pictionary"는 Mattel의 상표다. 표시 이름 후보: "그림 맞히기", "그려서 맞혀요", "쓱싹 퀴즈".
- 사용자가 그린 그림과 채팅은 사용자 생성 콘텐츠다. 부적절한 그림 신고 → 방장 강퇴 기능, 서버에는 턴 종료 후 그림 기록을 보관하지 않음(재생용 액션 로그 보관 기간은 짧게).
- 단어 목록은 직접 작성한 것을 사용(타 서비스 단어 목록 복제 금지).
- 도박 요소 없음.