- 개요, 조사(BGA·한국 인기 보드게임·UI/UX), 아키텍처, 실시간 프로토콜, 안정성·보안, 계정, 로비·방, UI/UX, 데이터 모델, 게임 엔진, 테스트, 배포·운영, 로드맵(M0~M9), 사용자 결정 항목 - 게임별 규칙·엔진 설계·UI·테스트 체크리스트 26종 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
204 lines
15 KiB
Markdown
204 lines
15 KiB
Markdown
# 할리갈리 (`halli-galli`)
|
||
|
||
> 마일스톤: M6 · 인원: 최소 2 ~ 최대 6명 · 예상 시간: 약 5~15분 · 난이도: 쉬움
|
||
|
||
## 1. 개요
|
||
- 차례대로 과일 카드를 한 장씩 뒤집다가, 펼쳐진 카드에서 **한 종류 과일이 정확히 5개**가 되면 가운데 종을 가장 먼저 친 사람이 펼쳐진 카드를 모두 가져가는 순발력 게임.
|
||
- 한국에서 가장 유명한 보드게임 중 하나로(코리아보드게임즈 유통), 어린이부터 어르신까지 규칙을 1분이면 이해한다.
|
||
- 인원 근거: 원작(Amigo, 1990, 하임 샤피르) 박스 표기 2~6인.
|
||
|
||
## 2. 모드와 옵션
|
||
| 옵션 키 | 설명 | 선택지 | 기본값 |
|
||
|---|---|---|---|
|
||
| `endMode` | 종료 방식 | `lastStanding`(한 명이 모든 카드를 가질 때까지, 국내 통상 규칙) / `official`(2명 남으면 다음 종 판정으로 종료, 원작 규칙) | `lastStanding` |
|
||
| `timeLimitMin` | 전체 제한 시간(분). 만료 시 카드 많은 사람 승 | 5 / 10 / 15 / 없음 | 10 |
|
||
| `flipSeconds` | 자기 차례에 뒤집기 제한 시간(초과 시 자동 뒤집기) | 2 / 3 / 5 / 8 | 3 |
|
||
| `autoFlip` | 모든 뒤집기를 서버가 일정 간격으로 자동 진행 | on / off | off |
|
||
| `flipGuardMs` | 뒤집기 직후 다음 뒤집기를 받지 않는 시간(지연 공정성) | 400 / 600 / 1000 | 600 |
|
||
| `wrongRingLockoutMs` | 틀린 종 후 그 사람의 종 잠금 시간 | 0 / 1000 / 2000 | 1000 |
|
||
| `leftoverCards` | 나누고 남은 카드 처리 | `pot`(종 밑, 다음 정답자가 가져감) / `remove`(게임에서 제외) | `pot` |
|
||
| `latencyComp` | 지연 보정 | `off` / `light` | `off` |
|
||
| `showRtt` | 각자 핑 표시 | on / off | on |
|
||
| `kidMode` | 어린이 모드: `flipSeconds` 8, 과일 개수 합계 보조 표시 | on / off | off |
|
||
|
||
## 3. 구성물
|
||
- 카드 56장: 과일 4종(딸기, 바나나, 라임, 자두) × 14장.
|
||
- 과일 1종당 구성:
|
||
|
||
| 그려진 과일 수 | 장수 |
|
||
|---|---|
|
||
| 1개 | 5장 |
|
||
| 2개 | 3장 |
|
||
| 3개 | 3장 |
|
||
| 4개 | 2장 |
|
||
| 5개 | 1장 |
|
||
|
||
과일 1종 합계 14장, 과일 개수 총합 5×1 + 3×2 + 3×3 + 2×4 + 1×5 = 33개.
|
||
- 종 1개(가상, 화면 가운데).
|
||
|
||
## 4. 준비(셋업)
|
||
1. 56장을 RNG로 섞는다. 좌석과 첫 플레이어도 RNG.
|
||
2. 같은 장수로 나눈다: `floor(56 / N)`장씩. 남은 카드는 `leftoverCards`에 따라 종 밑(공개되지 않은 더미, 장수만 공개) 또는 제외.
|
||
|
||
| 인원 | 1인당 | 남는 카드 |
|
||
|---|---|---|
|
||
| 2 | 28 | 0 |
|
||
| 3 | 18 | 2 |
|
||
| 4 | 14 | 0 |
|
||
| 5 | 11 | 1 |
|
||
| 6 | 9 | 2 |
|
||
|
||
3. 각자 받은 카드는 뒷면 더미(덱). 그 앞에 공개 더미(펼친 카드) 자리가 빈 채로 시작.
|
||
4. 3초 카운트다운 후 첫 플레이어 차례.
|
||
|
||
## 5. 진행 규칙
|
||
### 5.1 뒤집기
|
||
- 차례인 플레이어는 자기 덱 맨 위 카드를 뒤집어 자기 공개 더미 위에 놓는다. 그 다음 시계 방향으로 다음 "참가 중" 플레이어 차례.
|
||
- 각 플레이어의 공개 더미 **맨 위 카드만** 셈에 들어간다.
|
||
- `flipSeconds` 안에 뒤집지 않으면 서버가 자동으로 뒤집는다.
|
||
- 뒤집은 직후 `flipGuardMs` 동안은 다음 사람의 뒤집기를 받지 않는다(예약해 두었다가 시간이 지나면 처리). 지연이 큰 사람도 같은 화면을 보고 종을 칠 시간을 보장하기 위함.
|
||
|
||
### 5.2 종 치기
|
||
- 누구든(차례와 무관하게, 참가 중이면) 언제든 종을 칠 수 있다.
|
||
- 판정 기준: 종을 친 순간의 테이블 상태. 테이블의 모든 공개 더미 맨 위 카드(탈락자의 공개 더미 포함)에서 과일별 개수를 합산해 **어떤 과일이든 정확히 5**면 정답. 6 이상, 4 이하는 정답이 아니다.
|
||
- 예: 딸기 2 + 딸기 3 + 바나나 4 → 딸기 5 → 정답. 딸기 2 + 딸기 3 + 딸기 1 → 딸기 6 → 오답.
|
||
- **정답**: 가장 먼저 도착한 사람이 모든 공개 더미(+ 종 밑 카드)를 가져와 자기 덱 맨 아래에 넣는다(순서: 좌석 순으로 각 공개 더미를 아래부터, 마지막에 종 밑 카드). 그 사람이 다음 뒤집기 차례.
|
||
- **오답**: 종을 친 사람은 자기 덱에서 다른 참가 중인 모든 플레이어에게 1장씩 준다(받는 사람 덱 맨 아래, 지급 순서는 시계 방향). 덱이 모자라 다 주지 못하면 시계 방향 순으로 줄 수 있는 만큼만 주고 즉시 탈락. 정확히 다 줘서 0장이 된 경우는 5.3과 같이 자기 차례가 올 때까지 참가 상태. 진행 차례는 바뀌지 않는다.
|
||
- **늦은 종**: 종에는 클라이언트가 보고 있던 테이블 버전(`tableVersion`, 뒤집기·수거마다 +1)이 담긴다. 서버 버전과 다르면 "늦었어요"로 무시(벌칙 없음). 같은 버전에서 이미 누가 정답을 가져갔다면 버전이 바뀌었으므로 자연히 무시된다.
|
||
- 2명이 거의 동시에 친 경우: 서버 도착 순서가 우선(5.4).
|
||
|
||
### 5.3 탈락
|
||
- 자기 차례가 왔는데 덱이 0장이면 탈락(공개 더미는 테이블에 남아 계속 셈에 들어가며, 다음 정답자가 가져간다).
|
||
- 덱이 0장이어도 자기 차례가 오기 전에는 종을 칠 수 있다(정답이면 카드를 얻어 복귀). 단 오답이면 줄 카드가 없으므로 즉시 탈락.
|
||
- 탈락자는 관전 화면으로 전환.
|
||
|
||
### 5.4 실시간 판정과 공정성
|
||
- 방 단위 단일 큐에서 서버 수신 순서대로 `serverTs`를 붙여 처리한다. 같은 ms라도 큐 순서가 판정 순서이고 로그로 재생 가능.
|
||
- 클라이언트 시각은 신뢰하지 않는다.
|
||
- RTT: 2초마다 ping, 지수이동평균(α=0.2). `showRtt`면 이름 옆에 표시(초록 < 80ms, 노랑 < 200ms, 빨강 ≥ 200ms). 200ms 이상인 사람에게 "연결이 느려요" 안내.
|
||
- `flipGuardMs`: 테이블이 바뀐 뒤 최소 이 시간 동안은 다시 바뀌지 않으므로, 지연 RTT/2가 이 값보다 작은 사람은 정답 상태를 놓치지 않는다.
|
||
- `latencyComp=light`(선택): 첫 정답 종이 도착하면 50ms 창을 열고, 창 안의 같은 버전 정답 종들을 `serverTs - min(RTT/2, 80)` 기준으로 비교해 가장 이른 사람 승리. 창 마감 시 서버 내부 `resolve` 액션으로 확정(재생 가능). 오답 종은 창과 무관하게 즉시 벌칙.
|
||
- `wrongRingLockoutMs`: 오답 후 잠금(연타 방지). 잠금 중 종은 거부(벌칙 없음).
|
||
|
||
### 5.5 예시(3인 A·B·C)
|
||
1. A 뒤집기: 바나나 3. B 뒤집기: 딸기 2. C 뒤집기: 바나나 2 → 바나나 5.
|
||
2. B가 먼저 종(버전 3) → 정답. B가 세 공개 더미 + 종 밑 2장을 덱 아래로. B 차례로 다음 뒤집기.
|
||
3. 0.1초 뒤 A의 종(버전 3) 도착 → 버전이 4로 바뀌었으므로 "늦었어요", 벌칙 없음.
|
||
4. 이후 A가 바나나 4 상태에서 착각해 종 → 오답 → A가 B와 C에게 1장씩.
|
||
|
||
## 6. 승패와 점수 계산
|
||
- `lastStanding`: 마지막까지 남은 1명(모든 카드 보유) 승리. 순위는 탈락 역순.
|
||
- `official`: 참가자가 2명 남으면, 그다음 정답 종이 나올 때까지 진행하고 종료. 그 2명 상태에서 오답 종이 나오면 상대가 펼쳐진 카드 전부를 가져가고 즉시 종료. 종료 시 보유 카드가 많은 순. 처음부터 2인 게임이면 `official`도 `lastStanding`으로 진행한다(원작 문구대로면 첫 종에서 끝나 버리므로).
|
||
- `timeLimitMin` 만료: 즉시 종료, 보유 카드 수 많은 순.
|
||
- 보유 카드 수 = 덱 장수 + 자기 공개 더미 장수(아직 아무도 가져가지 않은 자기 카드). 종 밑 카드는 누구 것도 아님.
|
||
- 동점: 정답 종 횟수 많은 순 → 그래도 같으면 공동 순위.
|
||
- 방 기록용 점수: 1등 3점, 2등 2점, 3등 1점.
|
||
|
||
## 7. 지역 룰 / 하우스 룰 (옵션으로 켜고 끄기)
|
||
| 옵션 | 내용 | 기본값 |
|
||
|---|---|---|
|
||
| `duelWrongLoses` | 1대1 상황에서 오답 종을 친 사람이 즉시 패배(국내 하우스 룰) | off |
|
||
| `strikeOut` | 오답 3회 누적 시 몰수패 | off |
|
||
| `penaltyEscalation` | 오답마다 줄 장수가 1, 2, 3장...으로 증가 | off |
|
||
| `wrongToPot` | 오답 벌칙 카드를 다른 사람에게 주지 않고 종 밑으로 | off |
|
||
|
||
## 8. 엔진 설계
|
||
### 8.1 상태(State)
|
||
```ts
|
||
type Fruit = 'strawberry' | 'banana' | 'lime' | 'plum';
|
||
interface Card { id: number; fruit: Fruit; count: 1 | 2 | 3 | 4 | 5 } // id 0..55
|
||
|
||
interface HalliGalliState {
|
||
options: HalliGalliOptions;
|
||
rng: RngState;
|
||
order: PlayerId[];
|
||
decks: Record<PlayerId, number[]>; // 뒷면 덱, 맨 위 = 끝
|
||
faceUp: Record<PlayerId, number[]>; // 공개 더미, 맨 위 = 끝
|
||
pot: number[]; // 종 밑
|
||
active: PlayerId[]; // 참가 중(탈락 안 함)
|
||
eliminated: PlayerId[]; // 탈락 순서
|
||
turn: PlayerId;
|
||
turnDeadline: number;
|
||
flipAllowedAt: number; // flipGuard
|
||
pendingFlip: PlayerId | null; // 가드 중 들어온 뒤집기 예약
|
||
tableVersion: number;
|
||
lockedUntil: Record<PlayerId, number>;
|
||
wrongCount: Record<PlayerId, number>;
|
||
correctCount: Record<PlayerId, number>;
|
||
ringWindow: null | { version: number; openedAt: number; rings: { player: PlayerId; adjTs: number }[] };
|
||
phase: 'countdown' | 'playing' | 'ended';
|
||
startedAt: number;
|
||
endsAt: number | null; // timeLimit
|
||
lastBell?: { player: PlayerId; correct: boolean; fruit?: Fruit; at: number };
|
||
}
|
||
```
|
||
|
||
### 8.2 액션
|
||
액션에는 서버가 `serverTs`를 붙인다.
|
||
|
||
| type | payload | 누가 / 언제 | 검증 조건 |
|
||
|---|---|---|---|
|
||
| `flip` | `{}` | 차례인 플레이어, playing | `turn == actor`, 덱 1장 이상. `serverTs < flipAllowedAt`이면 거부하지 않고 `pendingFlip`에 예약 |
|
||
| `ring` | `{ version: number }` | 참가 중 플레이어(덱 0장 포함), playing | `serverTs >= lockedUntil[actor]`, `version == tableVersion`(다르면 "늦었어요"). 정답/오답 판정은 apply에서 |
|
||
| `tick` | `{}` | 서버 타이머 | 자동 뒤집기, 예약 뒤집기 실행, 시간 제한, 카운트다운 |
|
||
| `resolve` | `{}` | 서버 내부, `latencyComp=light` | 창 열림 |
|
||
|
||
### 8.3 공개/비공개 정보 (view)
|
||
- 모두(관전자 포함): 각 공개 더미 맨 위 카드(과일·개수), 공개 더미 장수, 각자 덱 장수, 종 밑 장수, 차례, 테이블 버전, 잠금 상태, RTT, 남은 시간, 탈락자.
|
||
- 비공개: 모든 덱의 카드 내용과 순서(본인 덱 포함 — 실제 게임에서도 자기 덱을 보지 않음), 종 밑 카드 내용, 공개 더미의 맨 위 아래 카드(가려져 있으므로 보낼 필요 없음).
|
||
- 자기 덱 내용을 본인에게도 보내지 않는 이유: 다음 카드를 미리 알면 뒤집기 타이밍을 조작할 수 있다.
|
||
- "지금 5개다"라는 판정 결과는 view에 넣지 않는다(`kidMode`의 과일 합계 보조 표시는 클라이언트가 공개 정보로 계산).
|
||
|
||
### 8.4 랜덤 요소 (시드 RNG 사용 지점)
|
||
덱 셔플, 좌석, 첫 플레이어. 이후 카드 이동은 모두 결정적(수거 순서 고정).
|
||
|
||
### 8.5 타이머·시간 초과·연결 끊김 시 자동 행동
|
||
- `activePlayers`: 참가 중 전원(종은 누구나), 뒤집기는 `turn`.
|
||
- `deadline`: `min(turnDeadline, flipAllowedAt(예약 있을 때), ringWindow 마감, endsAt)`.
|
||
- `onTimeout(player)`: 차례 플레이어의 `flip`. 종은 자동으로 치지 않는다.
|
||
- 연결 끊김: 그 사람 차례는 `flipSeconds` 후 자동 뒤집기로 계속 진행(카드를 모두 잃을 때까지 남아 있음). 60초 이상 끊기면 `flipSeconds`를 1초로 줄여 진행 속도 유지.
|
||
- `autoFlip` on이면 모든 뒤집기를 서버가 `flipSeconds` 간격으로 실행(`flip` 액션 거부).
|
||
|
||
### 8.6 종료 조건과 결과(GameResult)
|
||
```ts
|
||
interface HalliGalliResult {
|
||
endReason: 'lastStanding' | 'official' | 'timeLimit';
|
||
ranking: { rank: number; id: PlayerId; cards: number; correct: number; wrong: number }[];
|
||
summary: string; // 예: "도윤님이 56장을 모두 모아 우승!"
|
||
}
|
||
```
|
||
|
||
## 9. UI/UX
|
||
- 모바일 세로: 화면 가운데 큰 종 버튼(지름 화면 폭 40% 이상, 최소 120px) — 화면 어디를 쳐도 되게 하지 않고 종만 반응(오조작 방지). 종 주위에 플레이어별 공개 카드(최대 6장)를 원형 배치. 하단 내 덱(장수 표시)과 "뒤집기" 버튼(내 차례에만 활성, 큰 버튼). 상단 남은 시간, 순위.
|
||
- 차례 표시: 차례인 사람 카드 자리 테두리 깜빡임 + 남은 뒤집기 시간 원형 게이지.
|
||
- 종 피드백: 정답 시 "땡!" 효과음, 카드가 정답자에게 날아가는 애니메이션. 오답 시 "삐!" + 벌칙 카드 이동 애니메이션 + 잠금 원형 타이머. 늦음 시 작은 "늦었어요".
|
||
- 키보드(PC): 스페이스 = 종, 엔터 = 뒤집기.
|
||
- 어린이 모드: 느린 뒤집기, 과일별 합계 막대(공개 정보로 계산), "5개가 되면 종을 쳐요" 상시 안내.
|
||
- 효과음은 끄기 가능, 진동(지원 기기) 옵션.
|
||
|
||
## 10. 테스트 체크리스트
|
||
- [ ] 덱: 56장, 과일별 14장, 개수별 5/3/3/2/1장.
|
||
- [ ] 분배: 인원별 장수와 남는 카드가 4장 표와 일치, `pot`/`remove` 처리.
|
||
- [ ] 판정: 딸기 2+3 → 정답, 딸기 2+3+1 → 오답, 다른 과일 섞여도 한 과일이 정확히 5면 정답, 탈락자 공개 더미 맨 위도 합산.
|
||
- [ ] 정답 수거 순서 결정적(좌석 순, 종 밑 마지막), 정답자가 다음 차례.
|
||
- [ ] 동시 종: 같은 버전 정답 종 2개 → 먼저 도착한 사람만 수거, 두 번째는 버전 불일치로 "늦었어요" 무벌칙.
|
||
- [ ] 오답 벌칙: 4인에서 덱 2장인 사람 오답 → 시계 방향 2명에게만 1장씩 주고 0장 → 즉시 탈락.
|
||
- [ ] 덱 0장인 사람이 차례 전에 정답 종 → 카드 획득 후 계속 참가. 차례가 왔을 때 0장 → 탈락.
|
||
- [ ] `flipGuardMs`: 가드 중 들어온 flip은 예약되었다가 가드 종료 시 실행, 테이블 버전 1회만 증가.
|
||
- [ ] 뒤집기 시간 초과 → 자동 뒤집기. 연결 끊긴 사람도 계속 자동 진행.
|
||
- [ ] `official` 종료: 3인 → 2인 남은 뒤 첫 정답 종에서 종료, 2인 상태 오답 → 상대 수거 후 종료. 처음부터 2인이면 lastStanding처럼 진행.
|
||
- [ ] 시간 제한 만료: 보유 카드(덱 + 자기 공개 더미) 기준 순위, 동점은 정답 횟수.
|
||
- [ ] 비공개 누출 검사: 모든 view/이벤트에 덱 카드 내용(본인 포함), 종 밑 카드 내용, 다음 카드가 없는지 자동 검사.
|
||
- [ ] 같은 시드 + 액션 로그(serverTs 포함) 재생 시 동일 결과.
|
||
|
||
## 11. 참고 자료
|
||
- UltraBoardGames "How to play Halli Galli"(원작 규칙: 오답 벌칙, 탈락, 2인 종료 규칙) — https://www.ultraboardgames.com/halli-galli/game-rules.php
|
||
- 나무위키 "할리갈리"(인원별 분배와 남는 카드, 국내 하우스 룰) — https://namu.wiki/w/할리갈리
|
||
- BoardGameGeek "Halli Galli" — https://boardgamegeek.com/boardgame/2944/halli-galli
|
||
- Board Game Capital "How To Play Halli Galli"(과일별 카드 구성) — https://www.boardgamecapital.com/halli-galli-rules.htm
|
||
|
||
## 12. 메모 (상표·법적 주의 등)
|
||
- "Halli Galli / 할리갈리"는 Amigo Spiele의 상표이며 국내 유통은 코리아보드게임즈. 카드 그림도 저작물이므로 자체 제작. 표시 이름 후보: "과일 종치기", "다섯 개 땡!", "후르츠 벨".
|
||
- 도박 요소 없음.
|
||
- 종 버튼 연타는 터치스크린 기기 성능 차이(입력 지연)가 있으므로 경쟁 기록(랭킹)을 만들 경우 공정성 한계를 안내한다.
|