# 그림 맞히기 (`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; correctCounts: Record; 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의 상표다. 표시 이름 후보: "그림 맞히기", "그려서 맞혀요", "쓱싹 퀴즈". - 사용자가 그린 그림과 채팅은 사용자 생성 콘텐츠다. 부적절한 그림 신고 → 방장 강퇴 기능, 서버에는 턴 종료 후 그림 기록을 보관하지 않음(재생용 액션 로그 보관 기간은 짧게). - 단어 목록은 직접 작성한 것을 사용(타 서비스 단어 목록 복제 금지). - 도박 요소 없음.