# 구십구 (`ninety-nine`) > 마일스톤: M7 · 인원: 최소 2 ~ 최대 10명 · 예상 시간: 약 5~15분 · 난이도: 쉬움 ## 1. 개요 - 차례대로 카드를 1장씩 내며 숫자를 더해 간다. 합계를 99보다 크게 만들 수밖에 없는 사람이 탈락(또는 목숨 1개를 잃음)하는 서바이벌 카드 게임이다. - 이 문서는 두 가지 규칙을 담는다. - `kr-product` 모드(기본): 코리아보드게임즈의 카드게임 "구십구"(2~10명, 만 8세 이상, 약 5분) 방식. 전용 카드(1~8, 10, ±9, ±10, 0, 조커)를 쓴다. - `classic` 모드: 트럼프 52장으로 하는 전통 게임 "99(Ninety-Nine)". 영미권 놀이 게임이고, 나무위키 "99(플레잉 카드)"에도 같은 계열 규칙이 있다. - 인원 근거 - 코리아보드게임즈 공식 상품 페이지에 2~10명으로 나온다(일부 쇼핑몰은 2~6명으로 표기). - 나무위키 "99(플레잉 카드)"도 2~10명이다. - 그래서 2~10명으로 한다. ### 확인된 것과 확인되지 않은 것 (오너 확인 필요) - 확인됨(코리아보드게임즈 매거진 기사, 공식 상품 페이지) - 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. 준비(셋업) 1. 모드에 맞는 덱을 만들고 시드 RNG로 섞는다. 2. 선 플레이어를 RNG로 정한다. 진행은 시계 방향이다. 3. 각자 `handSize`장을 받는다. 남은 카드는 뽑을 더미가 된다. 4. 합계는 0에서 시작한다. 5. 목숨을 쓰면 각자 `lives`개를 받는다. ## 5. 진행 규칙 ### 5.1 턴 1. **탈락 판정(턴 시작 때 서버 자동)**: 현재 플레이어가 낼 수 있는 카드가 하나도 없으면 바로 "패배 처리"(5.3)를 한다. 패배 처리 결과 탈락하지 않은 경우(목숨이 남음)에는 새 라운드가 시작된다. 2. **카드 내기**: 낼 수 있는 카드 1장을 낸다. ±카드는 더하기/빼기를, A는 1/11을, 조커는 숫자를 함께 고른다. 새 합계가 공개된다. 3. **보충**(`refill = on`): 뽑을 더미에서 1장을 가져온다. 더미가 비었으면 버린 카드를 모두 섞어 새 더미를 만든다(합계는 그대로). 그래도 없으면 보충하지 않는다. 4. 다음 차례로 넘긴다. 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 큰 값(`aceHigh` 10/11). - 지원하지 않는 변형(문서화만) - Wikipedia "표준" 변형: 3이 다음 사람 건너뛰기, 9가 99로 만들기, 10이 ±10, K가 0. - 정확히 99를 만들면 보너스, 같은 숫자 연속 내기 금지 같은 지역 규칙. - 필요하면 `classic` 카드 효과표를 옵션 프리셋으로 늘린다. ## 8. 엔진 설계 ### 8.1 상태(State) ```ts 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; // 비공개 drawPile: NNCard[]; // 비공개 discard: NNCard[]; // 공개(최근 몇 장만 view) total: number; // 공개 합계 direction: 1 | -1; current: PlayerId; alive: Record; lives: Record; // lives=0 모드에서는 사용 안 함 onBoard: Record; // 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`. ```ts 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장. `classic` 52장, 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장 60~99, 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)은 상품의 고유 설계를 따른 것이다. 규칙 자체는 저작권 보호 대상이 아니지만, 카드 그림과 규칙서 문장은 쓰지 않고 직접 만든다. - 목숨(칩) 토큰은 방 안에서만 쓰는 게임 내 표시다. 돈, 포인트, 교환과 연결하지 않는다. 화면 용어는 도박 느낌을 피하려고 "칩" 대신 "목숨"(하트)을 쓴다.