- 개요, 조사(BGA·한국 인기 보드게임·UI/UX), 아키텍처, 실시간 프로토콜, 안정성·보안, 계정, 로비·방, UI/UX, 데이터 모델, 게임 엔진, 테스트, 배포·운영, 로드맵(M0~M9), 사용자 결정 항목 - 게임별 규칙·엔진 설계·UI·테스트 체크리스트 26종 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
17 KiB
17 KiB
도블 (dobble)
마일스톤: M8 · 인원: 최소 2 ~ 최대 8명 · 예상 시간: 약 3~10분 · 난이도: 쉬움
1. 개요
- 그림 8개가 그려진 원형 카드 두 장에는 항상 정확히 하나의 같은 그림이 있다. 그 그림을 누구보다 빨리 찾아 누르는 실시간 순발력 게임. 여러 개의 짧은 미니게임으로 즐긴다.
- 한국에서는 "도블"(영문판 Spot it!)이라는 이름으로 가족·어린이 보드게임으로 매우 유명하다. 글을 몰라도 할 수 있어 어린이·어르신에게 적합.
- 인원 근거: 원작(Play Factory / Asmodee, 2009) 박스 표기 2~8인.
2. 모드와 옵션
| 옵션 키 | 설명 | 선택지 | 기본값 |
|---|---|---|---|
miniGame |
미니게임 | tower(탑 쌓기) / well(우물) / hotPotato(뜨거운 감자) / poisonedGift(독이 든 선물) |
tower |
deckSize |
덱 장수 | 57(수학적 완전 덱) / 55(원작과 같은 장수) | 57 |
hotPotatoRounds |
뜨거운 감자 라운드 수 | 3 / 5 / 7 | 5 |
wrongLockoutMs |
틀린 그림을 눌렀을 때 잠금 시간 | 0 / 1000 / 2000 | 1000 |
latencyComp |
지연 보정 | off / light |
off |
symbolStyle |
그림 크기·회전 | easy(크기 균일, 회전 없음) / classic(크기·회전 무작위) |
classic |
showRtt |
각자 핑(ms) 표시 | on / off | on |
wellEndAtFirst |
우물: 첫 완주자가 나오면 종료 | on / off | off |
3. 구성물
3.1 덱 생성: 7차 사영평면
- 그림(기호) 57종, 카드 57장, 카드당 그림 8개. 어떤 두 카드도 정확히 1개의 그림을 공유하고, 각 그림은 정확히 8장에 나온다.
- n = 7(소수)일 때 다음 알고리즘으로 생성(서버 코드에서 실제 실행해 57장·57종·모든 쌍 공유 1개·그림당 8회 등장을 확인함):
function buildDeck(n = 7): number[][] {
const cards: number[][] = [];
// 1) 무한원 직선: 그림 0..n
cards.push(Array.from({ length: n + 1 }, (_, i) => i));
// 2) 그림 0을 지나는 n장
for (let j = 0; j < n; j++) {
const c = [0];
for (let k = 0; k < n; k++) c.push(n + 1 + n * j + k);
cards.push(c);
}
// 3) 나머지 n*n장: 기울기 i, 절편 j
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
const c = [i + 1];
for (let k = 0; k < n; k++) c.push(n + 1 + n * k + ((i * k + j) % n));
cards.push(c);
}
}
return cards; // 1 + n + n*n = 57장, 그림 번호 0..56
}
deckSize=55이면 생성 후 RNG로 2장을 제외한다(원작도 55장). 두 카드 공유 1개 성질은 유지된다.- 이 알고리즘은 n이 소수일 때만 성립(n=7 고정).
- 그림 번호는 고정이 아니라, 게임마다 RNG로 57개 그림 이미지에 섞어 배정한다(같은 카드 조합이 매번 같은 모양으로 반복되지 않게).
3.2 그림 57종 (자체 제작 아이콘, 한글 이름)
사과, 해, 달, 별, 하트, 물방울, 불꽃, 눈송이, 번개, 구름, 나무, 꽃, 나뭇잎, 선인장, 버섯, 당근, 레몬, 체리, 고양이, 강아지, 물고기, 새, 거북이, 무당벌레, 거미, 공룡, 토끼, 펭귄, 열쇠, 자물쇠, 시계, 전구, 연필, 가위, 안경, 우산, 모자, 왕관, 종, 닻, 자전거, 자동차, 로켓, 배, 공, 주사위, 음표, 카메라, 집, 성, 선물상자, 촛불, 망치, 느낌표, 물음표, 눈사람, 무지개 (57개).
- 색약 배려: 모양만으로 구분 가능해야 하며 색은 보조. 비슷한 실루엣(해/별, 달/바나나 등) 피하기.
3.3 카드 배치(레이아웃)
- 카드는 원형. 8개 그림의 위치는 미리 만든 원 패킹 템플릿 6종 중 하나를 RNG로 고르고,
classic이면 그림마다 크기(0.61.2배)와 회전(0359도)을 RNG로 정한다.easy는 크기 1.0, 회전 0. - 레이아웃은 셋업 때 카드별로 확정해 상태에 저장(모든 클라이언트가 같은 모양을 봄).
4. 준비(셋업)
공통: 덱 생성 → RNG 셔플 → 레이아웃 확정. 좌석은 RNG.
- 탑 쌓기(tower): 각자 1장(자기 탑의 첫 장, 공개). 나머지는 가운데 뽑기 더미, 맨 위 1장 공개.
- 우물(well): 가운데에 1장 공개. 남은 카드를 같은 장수로 나눔
floor((덱-1)/N)장씩, 나머지는 사용하지 않음(비공개 보관). 각자 자기 더미의 맨 위가 공개. - 뜨거운 감자(hotPotato): 라운드마다 각자 1장. 이미 쓴 카드는 다시 쓰지 않음(8인 × 7라운드 = 56장 ≤ 57).
- 독이 든 선물(poisonedGift): 각자 1장(공개), 나머지는 가운데 뽑기 더미, 맨 위 공개.
- 모든 미니게임은 3초 카운트다운 후 동시에 카드가 공개되며 시작.
5. 진행 규칙
5.1 공통: "찾기(claim)"
- 플레이어는 두 카드(아래 미니게임별 조합)의 공통 그림을 눌러 "찾기"를 보낸다. 서버가 도착 순서로 판정.
- 찾기에는 기준 카드 버전(가운데 카드 id 또는 대상 플레이어의 맨 위 카드 id)이 들어간다. 그 사이 카드가 바뀌었으면 "늦었어요"로 거부하고 벌칙 없음.
- 공통 그림이 아니면 "틀렸어요" +
wrongLockoutMs동안 그 플레이어의 찾기 잠금(서버가 판정). - 원작의 "동시에 외치면 카드를 먼저 놓은 사람"은 온라인에서 서버 도착 순서로 대체한다.
5.2 미니게임별 규칙
| 미니게임 | 비교하는 두 카드 | 성공 시 | 종료 | 승리 |
|---|---|---|---|---|
| 탑 쌓기 | 내 탑 맨 위 ↔ 가운데 맨 위 | 가운데 카드를 내 탑 위로 가져옴(내 새 맨 위) | 뽑기 더미 소진 | 카드 가장 많은 사람 |
| 우물 | 내 더미 맨 위 ↔ 가운데 맨 위 | 내 카드를 가운데에 올림(새 가운데) | 1명 빼고 모두 비움(wellEndAtFirst면 첫 완주자) |
먼저 비운 순서대로 순위, 마지막 사람 꼴찌 |
| 뜨거운 감자 | 내 카드(맨 위) ↔ 다른 사람 카드 맨 위 | 내 카드 묶음 전체를 그 사람 위에 올림. 나는 이번 라운드 탈출 | 1명이 모든 카드를 가짐 → 그 사람이 그 카드를 벌점으로 보관 | 모든 라운드 후 벌점 카드 가장 적은 사람 |
| 독이 든 선물 | 다른 사람 카드 맨 위 ↔ 가운데 맨 위 | 가운데 카드를 그 사람 위에 올림 | 뽑기 더미 소진 | 카드 가장 적은 사람 |
- 뜨거운 감자의 "묶음 전체를 올림": 원작은 받은 사람이 새 맨 위 카드로 계속 경기한다. 탈출한 사람(카드 0장)은 그 라운드 동안 대상이 될 수 없다.
- 독이 든 선물에서 자기 카드에는 올릴 수 없다(대상은 다른 사람만).
- 탑 쌓기 예(3인): 가운데 카드 {사과, 해, 고양이, ...}, 내 맨 위 {고양이, 열쇠, ...} → "고양이" 누름 → 서버 도착 1등이면 가운데 카드가 내 탑으로, 다음 가운데 카드 공개. 다른 사람이 같은 가운데 카드 기준으로 0.05초 늦게 보낸 찾기는 "늦었어요".
5.3 실시간 판정과 공정성
- 서버(방 단위 단일 스레드 큐)가 메시지 수신 순서대로
serverTs(서버 수신 시각, ms)를 찍어 액션에 붙이고 그 순서대로 validate/apply한다. 같은 ms라도 큐 순서가 곧 판정 순서이며 로그에 남아 재생 가능. - 클라이언트가 보내는 시각은 신뢰하지 않는다.
- RTT 측정: 2초마다 ping/pong, 지수이동평균(α=0.2)으로 플레이어별 RTT 저장.
showRtt면 이름 옆에 표시(초록 < 80ms, 노랑 < 200ms, 빨강 ≥ 200ms). - 지연 보정
light(선택):- 첫 유효 찾기가 도착하면 즉시 확정하지 않고 60ms 창을 연다.
- 창 안에 도착한 같은 기준 카드의 유효 찾기들의 보정 시각 =
serverTs - min(RTT/2, 80). 가장 작은 사람이 승리. - 확정은 창이 닫힐 때 서버 내부
resolve액션으로(재생 가능). 화면 반응이 60ms 늦어지는 대신 지연이 큰 사람의 불리함을 최대 80ms까지 줄인다. RTT를 일부러 부풀리는 악용을 막기 위해 보정 상한 80ms 고정.
- 잠금: 틀린 찾기 후
wrongLockoutMs동안 거부(무작위 연타 방지). 잠금 중 거부 사유 "잠깐! 1초 뒤에 다시 눌러요". - 봇 의심: 공개된 카드에서 공통 그림은 프로그램으로 즉시 계산 가능하므로 막을 수 없다. 카드 공개 후 200ms 미만 정답이 연속 5회면 방장에게 "의심" 표시만 한다(자동 처벌 없음).
6. 승패와 점수 계산
- 미니게임별 순위는 5.2 표. 동점은 공동 순위.
- 우물: 비운 순서가 순위. 우물에서 남은 2명 중 1명이 비우면 나머지가 꼴찌.
- 뜨거운 감자: 라운드마다 패자가 받은 카드 수를 벌점 누적. 최종 벌점 적은 순.
- 방 기록용 점수(여러 판 연속 플레이 시 누적, 원작 토너먼트 방식 단순화): 1등 3점, 2등 2점, 3등 1점, 그 외 0점. 공동 순위는 같은 점수.
7. 지역 룰 / 하우스 룰 (옵션으로 켜고 끄기)
| 옵션 | 내용 | 기본값 |
|---|---|---|
wrongLockoutMs=0 |
틀려도 벌칙 없음(원작은 벌칙 규정 없음) | 1000 |
wrongGivesCard |
탑 쌓기에서 틀리면 내 탑 맨 위 1장을 가운데 더미 맨 아래로 | off |
symbolStyle=easy |
어린이 모드: 크기 균일·회전 없음 | classic |
triplet |
트리플렛(9장 중 같은 그림을 가진 3장 찾기) 미니게임 | 향후 추가 |
8. 엔진 설계
8.1 상태(State)
interface CardLayout { template: number; items: { symbol: number; x: number; y: number; scale: number; rot: number }[] }
interface DobbleState {
options: DobbleOptions;
rng: RngState;
cards: number[][]; // 카드 id → 그림 번호 8개(셋업에서 생성)
layouts: CardLayout[]; // 카드 id → 레이아웃
symbolArt: number[]; // 그림 번호 → 아이콘 id(셔플)
order: PlayerId[];
piles: Record<PlayerId, number[]>; // 플레이어 더미, 맨 위 = 끝. well은 뒷면 더미 포함
center: number[]; // 가운데 공개 더미(맨 위 = 끝)
drawPile: number[]; // 비공개 뽑기 더미
unused: number[]; // 우물 나머지·55장 제외분
lockedUntil: Record<PlayerId, number>;
pendingWindow: null | { base: number; openedAt: number; claims: { player: PlayerId; target: PlayerId | 'center'; symbol: number; adjTs: number }[] };
round: number; // 뜨거운 감자
penalty: Record<PlayerId, number>; // 뜨거운 감자 벌점
outThisRound: PlayerId[]; // 뜨거운 감자 탈출자
finishedOrder: PlayerId[]; // 우물 완주 순서
phase: 'countdown' | 'playing' | 'roundEnd' | 'ended';
phaseEndsAt: number;
stats: Record<PlayerId, { correct: number; wrong: number; fastHits: number }>;
}
8.2 액션
액션에는 서버가 serverTs를 붙인다(클라이언트 값 무시).
| type | payload | 누가 / 언제 | 검증 조건 |
|---|---|---|---|
claim |
{ base: number; target?: PlayerId; symbol: number } |
참가자, playing | 잠금 해제됨(serverTs >= lockedUntil), base가 현재 기준 카드 id와 일치(아니면 "늦었어요"), 미니게임별 대상 규칙(우물·탑은 target 없음, 감자·선물은 다른 생존 대상), 그림이 두 카드에 모두 있음(아니면 잠금 부여) |
resolve |
{} |
서버 내부, latencyComp=light 창 마감 |
창이 열려 있음 |
tick |
{} |
서버 타이머 | 카운트다운·라운드 전환 |
"틀렸어요"는 validate 실패가 아니라 apply가 잠금만 기록하는 정상 액션으로 처리한다(잠금 상태를 남겨야 하므로). 늦은 찾기는 validate 실패.
8.3 공개/비공개 정보 (view)
- 모두(관전자 포함): 모든 공개 카드(가운데 맨 위, 각자 더미 맨 위, 뜨거운 감자의 각자 맨 위)의 그림과 레이아웃, 더미 장수, 순위, 잠금 상태, RTT.
- 비공개: 뽑기 더미 순서, 우물의 각자 뒷면 카드, 사용하지 않는 카드, 다음에 나올 카드, RNG 상태.
- 레이아웃과 그림 번호는 공개 카드에 대해서만 보낸다(나중에 나올 카드의 레이아웃 선전송 금지).
- 정답(공통 그림)은 보내지 않는다(클라이언트가 계산 가능하다는 점은 5.3 참고).
8.4 랜덤 요소 (시드 RNG 사용 지점)
그림-아이콘 배정, 덱 셔플, 55장 제외 카드, 레이아웃 템플릿·크기·회전, 좌석.
8.5 타이머·시간 초과·연결 끊김 시 자동 행동
- 차례가 없는 동시 게임이므로
activePlayers= 남아 있는 참가자 전원, 턴 타이머 없음.deadline= 카운트다운/라운드 전환/pendingWindow마감 시각. onTimeout: 개인 시간 초과 개념이 없으므로 아무 행동도 반환하지 않는noop.- 정체 방지: 45초 동안 아무도 맞히지 못하면(거의 없음) 서버가 공통 그림을 2초간 깜빡여 보여 주고 계속(점수 없음).
- 연결 끊김: 탑 쌓기·독이 든 선물은 그대로 진행(끊긴 사람은 못 맞힐 뿐). 우물·뜨거운 감자는 30초 이상 끊기면 "기권" 처리 — 우물은 꼴찌 그룹, 뜨거운 감자는 대상에서 제외되고 현재 카드는 가운데로 반환.
8.6 종료 조건과 결과(GameResult)
interface DobbleResult {
miniGame: 'tower' | 'well' | 'hotPotato' | 'poisonedGift';
ranking: { rank: number; id: PlayerId; cards?: number; penalty?: number; points: number }[];
stats: Record<PlayerId, { correct: number; wrong: number }>;
summary: string; // 예: "탑 쌓기 1등 하린님 21장!"
}
9. UI/UX
- 모바일 세로: 위쪽에 가운데 카드(화면 폭 80% 원), 아래쪽에 내 카드(같은 크기). 다른 사람은 상단 작은 아바타 + 장수(뜨거운 감자·독이 든 선물에서는 대상 카드를 탭해 크게 보기).
- 조작: 내 카드 또는 가운데 카드의 그림을 탭하면 찾기. 그림 터치 영역은 최소 48px(카드 지름 300px 기준으로 템플릿 설계). 뜨거운 감자/독이 든 선물은 먼저 대상 카드를 선택하고 그림 탭(두 번 탭), 대상이 2명이면 자동 선택.
- 피드백: 정답 시 그림 확대+초록 테두리, 늦음 시 회색 "늦었어요", 틀림 시 화면 흔들림 + 잠금 원형 타이머.
- 접근성: 그림 이름 음성 읽기(선택),
easy모드, 큰 카드 모드. 색만으로 정보 전달 금지. - 연습 모드: 혼자서 공통 그림 찾기(엔진 재사용, 기록 없음).
- PC: 카드 좌우 배치, 마우스 클릭.
10. 테스트 체크리스트
- 덱 생성: 57장, 그림 57종, 카드당 서로 다른 그림 8개, 모든 카드 쌍(1596쌍) 공유 그림 정확히 1개, 각 그림 8회 등장. 55장 모드에서도 쌍 성질 유지.
- 탑 쌓기: 두 사람이 같은 가운데 카드에 정답 → 먼저 도착한 사람만 획득, 나중 사람은 "늦었어요"(잠금 없음).
- 틀린 그림 →
wrongLockoutMs동안 정답도 거부, 잠금 해제 후 허용.serverTs기준. - 우물: 장수 균등(
floor((덱-1)/N)), 완주 순서 순위, 마지막 1명 꼴찌로 종료.wellEndAtFirston이면 첫 완주 즉시 종료. - 뜨거운 감자: 탈출자는 대상 불가, 한 명이 모두 가지면 라운드 종료·벌점 누적, 설정 라운드 후 벌점 최소 승리.
- 독이 든 선물: 자기 자신 대상 거부, 더미 소진 시 카드 최소 승리, 동점 공동 순위.
- 지연 보정 light: 상대 RTT 20ms(보정 10ms), 내 RTT 200ms(보정 80ms 상한)일 때 내가 40ms 늦게 도착해도 보정 시각이 30ms 빨라 승리, 100ms 늦게 도착하면 60ms 창 밖이라 패배.
- 연결 끊김: 우물에서 30초 끊김 → 기권 처리 후 남은 사람으로 계속.
- 비공개 누출 검사: 모든 view/이벤트에 뽑기 더미·우물 뒷면·미사용 카드의 그림·레이아웃·id가 없는지 자동 검사.
- 같은 시드 + 액션 로그(serverTs 포함) 재생 시 동일 결과.
- 봇 의심 표시: 200ms 미만 정답 5연속 시 방장 view에만 표시.
11. 참고 자료
- Wikipedia "Dobble" (55장, 카드당 8개, 7차 사영평면) — https://en.wikipedia.org/wiki/Dobble
- UltraBoardGames "Spot it! 규칙"(동시 외침 처리, 미니게임 점수) — https://www.ultraboardgames.com/spot-it/game-rules.php
- BoardGameGeek "Spot it!" — https://boardgamegeek.com/boardgame/63268/spot-it
- Wikipedia "Projective plane"(유한 사영평면 구성) — https://en.wikipedia.org/wiki/Projective_plane
- 나무위키 "도블" — https://namu.wiki/w/도블
12. 메모 (상표·법적 주의 등)
- "Dobble"과 "Spot it!"은 Asmodee 그룹의 상표이고 원작 그림 일러스트는 저작물이다. 사영평면 구조(수학)는 자유롭게 쓸 수 있지만 이름·그림은 자체 제작해야 한다. 표시 이름 후보: "같은 그림 찾기", "짝꿍 그림", "번쩍 찾기".
- 도박 요소 없음.