- 개요, 조사(BGA·한국 인기 보드게임·UI/UX), 아키텍처, 실시간 프로토콜, 안정성·보안, 계정, 로비·방, UI/UX, 데이터 모델, 게임 엔진, 테스트, 배포·운영, 로드맵(M0~M9), 사용자 결정 항목 - 게임별 규칙·엔진 설계·UI·테스트 체크리스트 26종 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
19 KiB
19 KiB
구십구 (ninety-nine)
마일스톤: M7 · 인원: 최소 2 ~ 최대 10명 · 예상 시간: 약 5~15분 · 난이도: 쉬움
1. 개요
- 차례대로 카드를 1장씩 내며 숫자를 더해 간다. 합계를 99보다 크게 만들 수밖에 없는 사람이 탈락(또는 목숨 1개를 잃음)하는 서바이벌 카드 게임이다.
- 이 문서는 두 가지 규칙을 담는다.
kr-product모드(기본): 코리아보드게임즈의 카드게임 "구십구"(210명, 만 8세 이상, 약 5분) 방식. 전용 카드(18, 10, ±9, ±10, 0, 조커)를 쓴다.classic모드: 트럼프 52장으로 하는 전통 게임 "99(Ninety-Nine)". 영미권 놀이 게임이고, 나무위키 "99(플레잉 카드)"에도 같은 계열 규칙이 있다.
- 인원 근거
- 코리아보드게임즈 공식 상품 페이지에 2
10명으로 나온다(일부 쇼핑몰은 26명으로 표기). - 나무위키 "99(플레잉 카드)"도 2~10명이다.
- 그래서 2~10명으로 한다.
- 코리아보드게임즈 공식 상품 페이지에 2
확인된 것과 확인되지 않은 것 (오너 확인 필요)
- 확인됨(코리아보드게임즈 매거진 기사, 공식 상품 페이지)
- 1~8 카드 각 7장, 10 카드 21장.
- 특수 카드 ±9, ±10, 0, 조커(정확히 2장).
- 자기 차례에 1장을 내고 이전 수에 더한 새 수를 말한다.
- 99를 넘게 만드는 카드는 낼 수 없고, 낼 카드가 1장도 없으면 탈락한다. 혼자 남으면 승리한다.
- ±9·±10은 더하거나 뺄 수 있다. 0은 수 변화 없이 진행 방향을 바꾼다. 조커는 앞 수와 상관없이 60~99 중 원하는 수를 부른다.
- 확인되지 않음(공식 규칙서 원문을 구하지 못함. 아래는 이 문서의 기본값이고 모두 옵션)
- ±9, ±10, 0 카드의 장수. 기본값은 각 4장(추정)이다.
- 시작 손패 장수. 기본값은 3장(전통 99와 같음)이다.
- 카드를 낸 뒤 1장 보충 여부. 기본값은 보충한다(전통 99와 같음).
- 빼기로 0 미만이 될 때 처리. 기본값은 0 미만이 되는 빼기는 금지다.
- 탈락자가 생긴 뒤 합계를 이어 갈지 초기화할지. 기본값은 이어 간다.
- 칩(목숨) 변형이 공식 규칙서에 있는지. 일부 소개 글에 "칩 3개, 99를 넘기면 1개 잃음" 변형이 언급되지만 원문으로 확인하지 못했다. 전통 99의 토큰 3개 규칙과 같은 구조라
lives옵션으로 둔다.
2. 모드와 옵션
| 옵션 키 | 설명 | 선택지 | 기본값 |
|---|---|---|---|
mode |
규칙 | kr-product / classic |
kr-product |
handSize |
손패 장수 | 3 / 4 / 5 | 3 |
refill |
카드를 낸 뒤 1장 보충 | on / off | on |
lives |
목숨(토큰) 수. 0이면 목숨 없이 바로 탈락 | 0 / 1 / 2 / 3 / 5 | kr-product 0, classic 3 |
onTheBoard |
목숨을 다 잃은 뒤에도 한 번 더 질 때까지 남음(전통 변형) | on / off | off |
afterElimination |
(lives=0) 탈락 뒤 합계 처리 |
continue(그대로 이어 감) / reset(0으로, 다시 나눔) |
continue |
floorAtZero |
빼기 결과가 0 미만일 때 | forbid(그런 빼기는 못 냄) / clamp(0으로 맞춤) |
forbid |
specialCounts |
(kr-product) ±9, ±10, 0 각 장수 |
2 / 3 / 4 / 5 / 6 | 4 |
jokerRange |
(kr-product) 조커로 부를 수 있는 범위 |
60-99(원작) / 0-99 |
60-99 |
aceHigh |
(classic) A의 큰 값 |
10(나무위키) / 11(Bicycle) | 11 |
deckCount |
(classic) 덱 수 |
auto(7인 이상 2벌) / 1 / 2 |
auto |
turnSeconds |
턴 제한 시간 | 10 / 15 / 20 / 30 | 15 |
3. 구성물
kr-product (기본값 기준 91장)
| 카드 | 효과 | 장수 |
|---|---|---|
| 1~8 | 그 숫자만큼 더함 | 각 7장(56장) |
| 10 | 10 더함 | 21장 |
| ±9 | 9를 더하거나 뺌(낸 사람이 선택) | specialCounts(기본 4, 추정) |
| ±10 | 10을 더하거나 뺌 | specialCounts(기본 4, 추정) |
| 0 | 수 변화 없음, 진행 방향 반대 | specialCounts(기본 4, 추정) |
| 조커 | 앞 수와 상관없이 60~99 중 원하는 수로 만듦 | 2장 |
- 9 숫자 카드는 없다(±9만 있음).
- 목숨을 쓰면(
lives > 0) 플레이어마다 목숨 토큰을 그만큼 준다. 방 안에서만 쓰는 표시용이다.
classic (트럼프 52장, Bicycle 규칙 기준)
| 카드 | 효과 |
|---|---|
| A | 1 또는 aceHigh(11) 더함(낸 사람이 선택) |
| 2, 3, 5, 6, 7, 8 | 그 숫자만큼 더함 |
| 4 | 0(수 변화 없음), 진행 방향 반대 |
| 9 | 0(수 변화 없음), 그냥 넘김 |
| 10 | 10을 뺌 |
| J, Q | 10 더함 |
| K | 합계를 바로 99로 만듦(이미 99면 그대로 99) |
- 조커는 쓰지 않는다.
deckCount = 2이면 104장이다.
4. 준비(셋업)
- 모드에 맞는 덱을 만들고 시드 RNG로 섞는다.
- 선 플레이어를 RNG로 정한다. 진행은 시계 방향이다.
- 각자
handSize장을 받는다. 남은 카드는 뽑을 더미가 된다. - 합계는 0에서 시작한다.
- 목숨을 쓰면 각자
lives개를 받는다.
5. 진행 규칙
5.1 턴
- 탈락 판정(턴 시작 때 서버 자동): 현재 플레이어가 낼 수 있는 카드가 하나도 없으면 바로 "패배 처리"(5.3)를 한다. 패배 처리 결과 탈락하지 않은 경우(목숨이 남음)에는 새 라운드가 시작된다.
- 카드 내기: 낼 수 있는 카드 1장을 낸다. ±카드는 더하기/빼기를, A는 1/11을, 조커는 숫자를 함께 고른다. 새 합계가 공개된다.
- 보충(
refill = on): 뽑을 더미에서 1장을 가져온다. 더미가 비었으면 버린 카드를 모두 섞어 새 더미를 만든다(합계는 그대로). 그래도 없으면 보충하지 않는다. - 다음 차례로 넘긴다. 0(또는
classic의 4)이면 방향을 반대로 바꾼 뒤 넘긴다.
- 패스는 없다. 낼 수 있는 카드가 있으면 반드시 1장을 내야 한다.
- 2인일 때 방향 반대 카드는 방향만 바뀌고 다음 차례는 여전히 상대다.
5.2 낼 수 있는 카드(합법 조건)
합계를 S, 카드를 낸 뒤 합계를 S'라 할 때 S' ≤ 99여야 한다.
kr-product
- 1~8, 10: S' = S + 값.
- ±9, ±10
- 더하기면 S' = S + 값.
- 빼기면 S' = S - 값.
floorAtZero = forbid이면 S' ≥ 0이어야 하고,clamp이면 S' = max(0, S - 값). - 두 방향 중 하나라도 합법이면 낼 수 있다.
- 0: S' = S. 항상 낼 수 있다.
- 조커: S'를
jokerRange(60~99) 안의 아무 정수로 고른다. 항상 낼 수 있다. 현재 합계와 같은 수를 골라도 된다.
classic
- A: 1 또는 11 중 S' ≤ 99인 값.
- 2, 3, 5~8, J, Q: S' = S + 값.
- 4, 9: S' = S. 항상 낼 수 있다.
- 10: S' = S - 10.
forbid이면 S ≥ 10일 때만,clamp이면 항상 가능. - K: S' = 99. 항상 낼 수 있다.
5.3 패배 처리
lives = 0(kr-product기본)- 그 플레이어는 탈락한다. 손패는 버린 더미로 간다.
afterElimination = continue면 같은 합계로 다음 사람 차례가 된다(다음 사람도 낼 카드가 없으면 연달아 탈락할 수 있다).reset이면 합계를 0으로 하고 남은 사람 손패를 모두 거둬 다시 섞어 나눈다. 탈락자의 다음 사람이 시작한다.
lives > 0- 그 플레이어가 목숨 1개를 잃는다. 이번 라운드는 끝난다.
- 목숨이 0이 되면 탈락한다.
onTheBoard = on이면 목숨 0인 상태로 계속 하고, 그 상태에서 한 번 더 지면 탈락한다. - 새 라운드: 모든 카드를 거둬 섞고, 남은 사람에게 다시 나누고, 합계 0에서 시작한다. 진 사람이 탈락하지 않았으면 진 사람이 먼저 하고, 탈락했으면 그 다음 사람이 먼저 한다.
- 남은 사람이 1명이면 게임이 끝난다.
5.4 예시 (kr-product, 3인)
- 합계 85에서 A가 10을 내 95가 된다. A가 보충한다.
- B의 손패는 [8, 7, ±9]이다. 8과 7은 99를 넘는다. ±9를 빼기로 내서 86이 된다.
- C의 손패는 [10, 10, 0]이다. 0을 내서 86 그대로이고 방향이 반대가 되어 B 차례가 된다.
- B의 손패는 [8, 7, 6](보충한 6)이다. 8 → 94, 7 → 93, 6 → 92 모두 가능하다. B가 7을 내서 93.
- 방향이 바뀌었으므로 다음은 A다. A의 손패가 [8, 10, 7]이면 101, 103, 100이라 낼 카드가 없다. A는 턴 시작 때 탈락하고 C 차례가 된다.
6. 승패와 점수 계산
- 마지막까지 남은 1명이 승자다.
- 순위: 늦게 탈락한 사람이 앞선다. 같은 시점 동시 탈락은 없다(턴 순서대로 판정).
- 목숨을 쓰는 경우에도 탈락 순서로 순위를 정한다. 결과 화면에 각자 잃은 목숨 수를 보여 준다.
- 점수, 칩, 돈은 없다. 목숨 토큰은 이 판에서만 쓰는 표시이고 방 밖으로 이어지지 않는다.
7. 지역 룰 / 하우스 룰 (옵션으로 켜고 끄기)
- 목숨(칩) 변형(
lives,onTheBoard): 전통 99의 토큰 3개 규칙이다. 구십구 소개 글 일부에서도 "칩 3개" 변형이 언급된다(원문 미확인). - 손패 장수(
handSize), 보충 여부(refill). - 탈락 뒤 합계 처리(
afterElimination). - 0 미만 빼기 처리(
floorAtZero). - 조커 범위 확장(
jokerRange, 어린이용 0~99). classic의 A 큰 값(aceHigh10/11).- 지원하지 않는 변형(문서화만)
- Wikipedia "표준" 변형: 3이 다음 사람 건너뛰기, 9가 99로 만들기, 10이 ±10, K가 0.
- 정확히 99를 만들면 보너스, 같은 숫자 연속 내기 금지 같은 지역 규칙.
- 필요하면
classic카드 효과표를 옵션 프리셋으로 늘린다.
8. 엔진 설계
8.1 상태(State)
type NNCard =
| { id: string; kind: 'add'; v: number } // kr: 1..8, 10 / classic: 2,3,5,6,7,8, J/Q(10)
| { id: string; kind: 'plusMinus'; v: 9 | 10 } // kr ±9, ±10
| { id: string; kind: 'reverse' } // kr 0 / classic 4
| { id: string; kind: 'hold' } // classic 9
| { id: string; kind: 'minus'; v: 10 } // classic 10
| { id: string; kind: 'ace' } // classic A (1 또는 aceHigh)
| { id: string; kind: 'to99' } // classic K
| { id: string; kind: 'joker' }; // kr 조커 (jokerRange)
// 화면 표시용으로 원래 표기(rank/suit 또는 kr 라벨)를 별도 필드 face로 둔다.
interface NinetyNineState {
mode: 'kr-product' | 'classic';
options: NNOptions;
rng: RngState;
seats: PlayerId[];
hands: Record<PlayerId, NNCard[]>; // 비공개
drawPile: NNCard[]; // 비공개
discard: NNCard[]; // 공개(최근 몇 장만 view)
total: number; // 공개 합계
direction: 1 | -1;
current: PlayerId;
alive: Record<PlayerId, boolean>;
lives: Record<PlayerId, number>; // lives=0 모드에서는 사용 안 함
onBoard: Record<PlayerId, boolean>; // onTheBoard 상태
eliminationOrder: PlayerId[];
round: number;
phase: 'playing' | 'finished';
turnStartedAt: number;
seq: number;
}
8.2 액션
| type | payload | 누가 / 언제 | 검증 조건 |
|---|---|---|---|
play |
{ cardId: string; sign?: '+' | '-'; aceValue?: 1 | 10 | 11; jokerValue?: number } |
current, playing |
카드가 손에 있음. ±카드는 sign 필수, A는 aceValue 필수(1 또는 aceHigh), 조커는 jokerValue 필수이고 jokerRange 안의 정수. 5.2 조건으로 계산한 S' ≤ 99(그리고 forbid면 S' ≥ 0) |
- 플레이어가 보내는 액션은
play하나뿐이다. 탈락, 목숨 감소, 라운드 재시작, 보충은apply안에서 자동으로 처리한다.play를 적용한 직후 다음 플레이어를 정한다. 그 플레이어에게 합법 수가 없으면 같은apply안에서 패배 처리를 하고, 다시 다음 사람을 검사한다(반복). 이 과정을 이벤트로 순서대로 남긴다.- 이렇게 하면 "낼 카드가 없는 사람이 타이머를 기다리는" 상황이 없다.
- 이벤트 예:
cardPlayed{player, face, total},directionChanged,cardDrawn{player},reshuffled,lifeLost{player, left},eliminated{player},roundStarted{round, starter}. reason예: "99를 넘어서 낼 수 없어요", "0보다 작아져서 뺄 수 없어요", "더하기와 빼기 중 하나를 골라 주세요", "조커는 60부터 99 사이 숫자만 고를 수 있어요".
8.3 공개/비공개 정보 (view)
- 모두(관전자 포함): 현재 합계, 방향, 현재 차례, 각자 손패 장수, 목숨, 생존 여부, 탈락 순서, 뽑을 더미 장수, 최근에 낸 카드 몇 장과 그때의 합계 기록.
- 본인만
- 자기 손패.
- 각 카드의 합법 여부와 결과 미리보기(예: "±9 → 더하면 104 불가 / 빼면 86").
- 탈락 때 손패는 공개하지 않고 장수만 버린 더미로 옮긴다(실물에서는 보일 수 있지만 다른 사람 추리에 영향이 없도록 비공개로 둔다).
- 절대 보내지 않는 것: 남의 손패, 더미 순서, RNG 상태. 보충 이벤트는 본인에게만 카드 내용을 준다.
8.4 랜덤 요소 (시드 RNG 사용 지점)
setup: 덱 섞기, 선 플레이어.- 더미 소진 때 버린 카드 다시 섞기.
- 라운드 재시작(목숨 모드,
reset) 때 다시 섞기.
8.5 타이머·시간 초과·연결 끊김 시 자동 행동
deadline = turnStartedAt + turnSeconds*1000.onTimeout(state, p): 합법인 (카드, 선택값) 조합 중 결과 합계 S'가 가장 작은 것을 고른다.- 같으면 우선순위: 0/4/9(변화 없음) → 일반 숫자 카드 → ±카드 → K/조커. 그래도 같으면 카드 ID 순.
- 조커는
jokerRange최소값(60)으로 낸다. - 이 규칙은 본인 손패와 공개 합계만 쓰므로 정보 유출이 없고 결정적이다.
- 합법 수가 없는 경우는 타이머까지 가지 않고
apply에서 이미 처리된다(8.2). - 연결이 끊겨도 위 자동 행동으로 계속 진행한다. 연속 3턴 타임아웃이면 서버가 마감을 3초로 줄인다.
8.6 종료 조건과 결과(GameResult)
- 생존자가 1명이 되면
finished.
interface GameResult {
ranking: { player: PlayerId; rank: number; livesLost: number; eliminatedAtRound: number | null }[];
winner: PlayerId[];
summary: string; // "끝까지 살아남은 사람은 지호 님! 마지막 합계 97"
}
9. UI/UX
- 모바일 세로 화면
- 가운데: 현재 합계를 아주 크게(예: 72pt) 보여 준다. 99에 가까울수록 색이 초록 → 노랑 → 빨강으로 바뀌는 게이지 링을 두른다.
- 위: 상대 아바타(손패 장수, 목숨 하트, 차례 표시), 진행 방향 화살표.
- 아래: 내 손패 3장을 큰 카드로 보여 준다(최소 72×100px).
- 조작
- 카드를 탭하면 "내면 → 81"처럼 결과 합계 미리보기가 카드 위에 뜬다. 한 번 더 탭하면 낸다.
- ±카드를 탭하면 [+9 → 95] [−9 → 77] 두 버튼이 뜬다. 불가능한 쪽은 비활성이다.
- 조커를 탭하면 60~99 숫자 다이얼(+/− 버튼과 빠른 선택 60/90/99)이 뜬다.
- 낼 수 없는 카드는 흐리게 하고 "99 초과" 배지를 붙인다.
- 애니메이션: 합계 숫자가 올라가는 카운트 애니메이션, 방향 전환 화살표 회전, 탈락 때 아바타 흑백 처리, 목숨 하트가 깨지는 연출.
- 초보 도움말: "99를 넘기면 안 돼요!" 한 줄 규칙과 특수 카드 표. 어린이 방 추천 설정(조커 0~99, 손패 4장).
- 실시간 감각: 턴이 빨라서 타이머 링을 크게 보여 주고, 내 차례가 되면 진동(모바일 지원 시)으로 알린다.
10. 테스트 체크리스트
- 덱 구성:
kr-product기본값 91장(1~8 각 7, 10이 21, ±9/±10/0 각 4, 조커 2).specialCounts=2면 85장.classic52장, 7인 이상auto면 104장. - 같은 시드면 같은 분배, 같은 선이 나온다.
- 합계 95에서 손패 [8, 5, 4] (
kr-product)이면 4만 합법이다. 4를 내서 99가 된다. 다음 사람이 [1, 2, 3]만 있으면 턴 시작 때 바로 탈락한다. - ±10: 합계 5에서 빼기는
forbid면 거부된다.clamp면 0이 된다. 더하기는 15로 허용된다. - 조커: 합계 20에서 조커로 99를 고르면 99가 된다. 59는 거부되고(
60-99), 100도 거부된다. - 0 카드: 3인에서 A → B 차례에 B가 0을 내면 방향이 반대가 되어 다음이 A다. 2인에서는 상대 차례 그대로다.
classic: 합계 90에서 K → 99, 그다음 사람은 4/9/10/K만 낼 수 있다. A는 1이어도 100이라 거부된다. 합계 88에서 A를 11로 내면 99로 허용된다.lives=3: 진 사람이 목숨 1개를 잃고, 합계 0으로 새 라운드가 시작되며 진 사람이 먼저 한다. 목숨 0이면 탈락하고 다음 사람이 시작한다.onTheBoard면 0개에서 한 번 더 져야 탈락한다.- 연쇄 탈락(
lives=0,continue): 합계 98에서 다음 두 사람 모두 낼 카드가 없으면 한 번의apply안에서 두 명이 순서대로 탈락하고, 세 번째 사람 차례가 된다. 이벤트 순서가 맞다. - 더미 소진: 보충할 때 더미가 비면 버린 카드가 섞여 보충되고 합계는 그대로다.
- 시간 초과: 손패 [10, 3, 0], 합계 50이면 자동 행동은 0(결과 50, 가장 작음)을 낸다. 손패 [±9, 3, 0], 합계 50이면 −9(결과 41)를 낸다.
- 연결 끊김: 끊긴 플레이어 차례가 자동으로 진행되고 게임이 멈추지 않는다.
- 정보 유출: 관전자와 상대 view에 남의 손패, 보충 카드 내용, 더미 순서가 없다. 탈락자 손패도 공개되지 않는다.
- 차례가 아닌
play,sign없는 ±카드, 범위 밖jokerValue, 중복 패킷이 거부되고 상태가 바뀌지 않는다.
11. 참고 자료
- 코리아보드게임즈 매거진 "숫자를 더해나가며 한계에 도전하라!": https://www.koreaboardgames.com/magazine/menuDetail?boardCd=contents&postNo=947 (1
8 각 7장, 10이 21장, ±9·±10·0·조커 효과, 조커 2장 6099, 99 초과 금지, 낼 카드 없으면 탈락) - 코리아보드게임즈 상품 페이지 "구십구": https://www.koreaboardgames.com/product/detail?prdCd=PD2024002211B1JR (2~10명, 만 8세+, 5분)
- 나무위키 "99(플레잉 카드)": https://namu.wiki/w/99(플레잉%20카드) (트럼프판 2~10명, 손패 3장, 내고 1장 보충, A 1/10, 4 반대, 9는 0, 10은 −10, J·Q 10, K는 99)
- Bicycle Cards "99 (Ninety Nine)": https://bicyclecards.com/how-to-play/99-ninety-nine (토큰 3개, 손패 3장, A 1/11, 4 반대, 9 패스, 10 −10, K 99, 넘기면 토큰 1개 잃고 라운드 종료)
- Wikipedia "Ninety-nine (addition card game)": https://en.wikipedia.org/wiki/Ninety-nine_(addition_card_game) (토큰 3개, 지역 변형 목록)
12. 메모 (상표·법적 주의 등)
- "구십구"는 코리아보드게임즈가 파는 상품명이다. 숫자 이름이라 상표 효력이 약할 수 있지만 안전하게 표시 이름을 바꾸는 것을 권한다. 후보: "99 넘기지 마!", "아흔아홉", "숫자 더하기 99"(사용자 선택).
- 전통 게임 "99(Ninety-Nine)" 규칙 자체는 공공 영역이다.
kr-product모드의 전용 카드 구성(±9, ±10, 0, 조커 60~99)은 상품의 고유 설계를 따른 것이다. 규칙 자체는 저작권 보호 대상이 아니지만, 카드 그림과 규칙서 문장은 쓰지 않고 직접 만든다. - 목숨(칩) 토큰은 방 안에서만 쓰는 게임 내 표시다. 돈, 포인트, 교환과 연결하지 않는다. 화면 용어는 도박 느낌을 피하려고 "칩" 대신 "목숨"(하트)을 쓴다.