# 포커 (`poker`) > 마일스톤: M3 · 인원: 최소 2 ~ 최대 10명 (모드별 상한은 2.1 표) · 예상 시간: 약 20~40분 (20핸드 기준) · 난이도: 보통(인디언 포커, 7포커) ~ 어려움(오마하, 하이로우) ## 1. 개요 - 52장 트럼프 카드로 하는 베팅 게임 묶음이다. 한 문서·한 엔진에 7개 모드를 `mode` 옵션으로 담는다: 텍사스 홀덤, 7포커(한국식 초이스 세븐 스터드), 7포커 하이로우, 바둑이(한국식 로우 바둑이), 인디언 포커, 5카드 드로우, 오마하. - 한국에서는 한게임·넷마블·피망 등 웹보드 게임으로 7포커·하이로우·바둑이가 오래 친숙했고, 최근에는 홀덤 펍과 방송 영향으로 텍사스 홀덤 인지도가 높다. 인디언 포커는 예능 "더 지니어스" 이후 파티 게임으로 알려졌다. - 돈은 전혀 쓰지 않는다. 칩은 방 안에서만 존재하는 가상 칩이며 방이 끝나면 사라진다. 저장·이월·충전 결제·환전·양도가 없다(12장). - 인원 근거(원작·전통 규칙 기준, 2.1 표에 모드별로 정리): - 홀덤: 통상 2~10명(위키백과 "usually 2–10"). - 오마하: 2~10명(원작 2~11명이지만 4장씩 받으므로 화면 배치상 10명으로 제한). - 7포커/하이로우: 2~6명. 한국식 초이스 룰은 1인당 8장(처음 4장 + 4·5·6·7구 4장)을 소모하므로 52장으로 6명(48장)이 상한이다. 7명이면 카드가 모자란다. - 바둑이: 2~6명(위키백과 "usually played six-handed", 교환 시 버린 카드를 다시 섞어 사용). - 5카드 드로우: 2~6명(교환이 있는 드로우 포커의 일반적 상한). - 인디언 포커: 2~6명(1~10 두 벌 20장 덱 기준. 52장 덱 옵션 사용 시 1장 모드는 최대 10명). ## 2. 모드와 옵션 ### 2.1 모드별 요약 | 모드(`mode`) | 이름 | 인원 | 받는 카드 | 베팅 라운드 | 기본 베팅 방식 | 강제 베팅 | 동점 처리 | |---|---|---|---|---|---|---|---| | `holdem` | 텍사스 홀덤 | 2~10 | 개인 2 + 공용 5 | 4 (프리플랍/플랍/턴/리버) | 노리밋 | 스몰·빅 블라인드 | 키커 비교 후 분할 | | `omaha` | 오마하 | 2~10 | 개인 4 + 공용 5 (개인 정확히 2장 + 공용 정확히 3장) | 4 | 팟리밋 | 스몰·빅 블라인드 | 키커 비교 후 분할 | | `sevenPoker` | 7포커 | 2~6 | 4장 받아 1장 버리고 1장 오픈, 이후 4·5·6구 오픈, 7구 히든 | 4 (4구/5구/6구/7구) | 한국식 판돈 | 앤티(기본 판돈) | 무늬 서열(♠>◆>♥>♣) | | `sevenHiLo` | 7포커 하이로우 | 2~6 | 7포커와 동일 | 4 + 선언 | 한국식 판돈 | 앤티 | 무늬 서열, 하이/로우 반분 | | `badugi` | 바둑이 | 2~6 | 4장, 교환 3회(아침/점심/저녁) | 4 | 한국식 판돈 | 앤티 | 분할(무늬 비교 없음) | | `fiveDraw` | 5카드 드로우 | 2~6 | 5장, 교환 1회 | 2 | 한국식 판돈 | 앤티 | 키커 비교 후 분할 | | `indian` | 인디언 포커 | 2~6 (52장 덱 1장 모드는 2~10) | 1장(이마) 또는 2장(이마 1 + 손 1) | 1장 모드 1회, 2장 모드 2회 | 노리밋(최소 단위 1) | 앤티 | 판돈 이월(옵션: 분할) | `betting` 옵션으로 어떤 모드든 베팅 방식을 바꿀 수 있다(예: 7포커를 노리밋으로). 단 블라인드는 공용 카드형(`holdem`, `omaha`)에서만 쓰고, 나머지 모드에서 `noLimit`/`potLimit`을 고르면 강제 베팅은 앤티로 한다. ### 2.2 옵션 표 | 옵션 키 | 설명 | 선택지 | 기본값 | |---|---|---|---| | `mode` | 게임 모드 | 2.1 표의 7개 | `sevenPoker` | | `betting` | 베팅 방식 | `auto`(모드 기본) / `korean` / `noLimit` / `potLimit` | `auto` | | `startingChips` | 시작 칩 | 500 / 1000 / 2000 / 5000 | 1000 | | `baseUnit` | 기본 단위. 한국식=앤티(삥) 금액, 블라인드형=빅 블라인드(스몰은 절반, 내림) | 2 / 10 / 20 / 50 | 10 | | `koreanMaxRaise` | 한국식 베팅에서 허용하는 최대 레이즈 | `half`(하프까지) / `full`(풀까지) | `half` | | `endCondition` | 게임 종료 방식 | `hands`(정해진 핸드 수) / `lastStanding`(최후 1인) | `hands` | | `handLimit` | `hands`일 때 총 핸드 수 | 10 / 20 / 30 / 50 | 20 | | `blindUpEvery` | N핸드마다 기본 단위 2배(0=안 올림) | 0 / 5 / 10 / 15 | 0 (`lastStanding` 선택 시 UI가 10을 추천) | | `rebuy` | 리바이(재충전) | `off` / `limited` / `unlimited` | `unlimited` (`lastStanding` 선택 시 UI가 `off`를 추천) | | `rebuyLimit` | `limited`일 때 1인당 횟수 | 1 / 2 / 3 | 1 | | `turnSeconds` | 행동 제한 시간(초) | 15 / 20 / 30 / 60 | 20 | | `dealerRule` | 다음 핸드 딜러(선) 결정 | `rotate`(시계 방향 회전) / `winner`(직전 승자) | `rotate` | | `hints` | 현재 족보 표시·추천 행동 하이라이트 | 켬 / 끔 | 켬 | | `hiLoLow` | 하이로우 로우 판정 방식 | `korean`(스트레이트·플러시 불인정, 최고 로우 A-2-3-4-6) / `aceToFive`(스트레이트·플러시 무시, 최고 로우 A-2-3-4-5) | `korean` | | `hiLoQualifier` | 하이로우 로우 자격 | `none`(K탑까지 인정) / `eight`(8탑 이하만) | `none` | | `drawLimit` | 5카드 드로우 교환 상한 | `any`(0~5장) / `three`(3장, A 보유 시 4장) | `any` | | `indianCards` | 인디언 포커 카드 수 | 1 / 2 | 1 | | `indianDeck` | 인디언 포커 덱 | `d20`(1~10 각 2장) / `d52` | `d20` | | `indianFoldPenalty` | 10(덱 최고 숫자)을 이마에 단 채 다이하면 벌칙 | 켬 / 끔 | 켬 | | `indianTie` | 인디언 포커 동점 시 | `carry`(판돈 다음 핸드로 이월) / `split`(분할) | `carry` | | `indianReshuffle` | 인디언 포커 덱 섞기 시점 | `whenEmpty`(덱 소진 시, 카드 기억 요소) / `everyHand` | `whenEmpty` | 옵션 검증(zod `superRefine`): - 참가 인원이 선택한 모드의 상한을 넘으면 시작 불가("이 모드는 최대 6명까지 할 수 있어요"). - `indian` + `indianCards=1` + `indianDeck=d52`만 최대 10명, 그 외 인디언은 최대 6명. - `hiLo*` 옵션은 `sevenHiLo`에서만, `drawLimit`은 `fiveDraw`에서만, `indian*`은 `indian`에서만 의미가 있다(다른 모드에서는 무시). - `baseUnit * 20 > startingChips`이면 거부(시작 칩이 기본 단위의 20배 이상이어야 함). `auto` 해석: `holdem`→`noLimit`, `omaha`→`potLimit`, `indian`→`noLimit`, 나머지→`korean`. ## 3. 구성물 - 표준 52장 덱(조커 없음). 무늬 4종 ♠스페이드 ◆다이아몬드 ♥하트 ♣클로버, 숫자 2~10, J, Q, K, A. - 인디언 포커 `d20` 덱: A(=1), 2~10을 ♠와 ♥ 두 벌, 총 20장. 이 덱에서는 A가 가장 낮다. - 가상 칩(방 전용). 화면에는 금액 숫자와 칩 더미 그림으로 표시. - 딜러 버튼(D), 홀덤·오마하는 SB/BB 표시, 한국식 모드는 "선" 표시와 "보스" 왕관 표시. - 판돈(메인 팟)과 사이드 팟 표시 영역, 공용 카드 영역(홀덤·오마하), 버린 카드 더미(뒷면). ## 4. 준비(셋업) 1. 방 참가자를 좌석 0..N-1에 앉힌다. 좌석 순서는 `rng`로 무작위 섞는다. 2. 모든 플레이어 칩 = `startingChips`, 리바이 횟수 0. 3. 첫 딜러(선) 좌석은 `rng`로 고른다. 4. 인디언 `d20`/`d52`이면 인디언 덱을 만들어 `rng`로 섞어 둔다(`whenEmpty`에서는 핸드 사이에 유지). 5. 첫 핸드를 시작한다(5.1). ## 5. 진행 규칙 ### 5.1 공통 핸드 흐름 1. 핸드 시작: 칩이 1 이상인 플레이어만 참가한다(`busted`/`eliminated` 제외). 2. 딜러 결정: 첫 핸드는 셋업 값. 이후 `rotate`는 직전 딜러 다음 좌석 중 참가자, `winner`는 직전 핸드에서 가장 큰 팟을 가져간 플레이어(분할이면 그중 딜러 다음 좌석에 가장 가까운 사람). 3. 덱을 새로 만들어 `rng`로 섞는다(인디언 `whenEmpty` 제외). 4. 강제 베팅: - 한국식·앤티형: 참가자 전원이 `baseUnit`을 판돈에 낸다(앤티, 7포커 용어로 "기본 판돈"). 앤티는 라운드 베팅 금액(`committedRound`)에 넣지 않고 바로 판돈으로 간다. - 블라인드형(홀덤·오마하): 딜러 다음 좌석 SB(`floor(baseUnit/2)`, 최소 1), 그다음 BB(`baseUnit`). 2인(헤즈업)일 때는 딜러가 SB, 상대가 BB. - 칩이 강제 베팅보다 적으면 가진 칩을 전부 내고 올인 상태가 된다. 5. 모드별 카드 분배와 베팅 라운드(5.4) 진행. 6. 베팅 도중 한 명만 남으면 그 사람이 모든 팟을 가져가고 카드는 공개하지 않는다. 7. 마지막 라운드 후 2명 이상 남으면 쇼다운(6장) → 팟 분배. 8. 핸드 종료 단계(`handEnd`): 결과를 4초간 보여 준다. 참가자는 "다음 판" 버튼으로 준비 완료할 수 있고, 전원 준비 또는 시간 초과 시 다음 단계로. 9. 칩 0이 된 플레이어 처리(5.6 리바이·탈락), 종료 조건 확인(6.7), 아니면 다음 핸드. 카드 분배 순서: 딜러 다음 좌석부터 시계 방향으로 한 장씩 돈다. 덱은 핸드 시작 시 한 번 섞고 위에서부터 꺼낸다(교환으로 덱이 모자랄 때만 5.4.5 규칙으로 다시 섞음). ### 5.2 베팅 방식 A: 한국식 판돈 베팅 (`korean`) 용어: - `pot` = 현재까지 모든 판돈 합계(앤티 + 이전 라운드 + 이번 라운드에 이미 낸 금액, 사이드 팟 포함). - `currentBet` = 이번 라운드에서 한 사람이 낸 최대 금액. - `toCall` = `currentBet - 내 committedRound`. - `opened` = 이번 라운드에 누군가 칩을 걸었는가(삥 또는 레이즈). 행동과 금액(낼 금액 = 이번 행동으로 스택에서 나가는 칩): | 행동 | 조건 | 낼 금액 | |---|---|---| | 체크 | `opened=false` (아직 아무도 걸지 않음) | 0 | | 삥 | `opened=false` | `baseUnit` | | 콜 | `toCall > 0` | `toCall` | | 따당 | `toCall > 0`, 레이즈 가능 | `2 × toCall` (앞사람이 건 금액의 2배) | | 쿼터 | 레이즈 가능 | `toCall + floor((pot + toCall) × 1/4)` | | 하프 | 레이즈 가능 | `toCall + floor((pot + toCall) × 1/2)` | | 풀 | 레이즈 가능, `koreanMaxRaise=full` | `toCall + (pot + toCall)` | | 다이 | 언제나(자기 차례) | 0, 이번 핸드 포기 | - 쿼터/하프/풀은 "콜을 먼저 맞춘 뒤 그때의 판돈 × 비율만큼 더 건다"로 정의한다(한게임 정의 "전체 판돈의 25%/50%/100%"를 콜 금액 포함 판돈 기준으로 구체화). `opened=false`에서 쓰면 `toCall=0`이므로 `floor(pot × 비율)`이다. `toCall`을 뺀 추가분이 `baseUnit`보다 작으면 추가분을 `baseUnit`으로 올린다. - "레이즈 가능" 조건(체크레이즈 금지, 콜 후 레이즈 금지): 1. 이번 라운드에서 아직 체크·콜·삥·레이즈 중 어느 것도 하지 않았다. 즉 한 라운드에 레이즈(삥 포함)는 1인 1회이며, 체크나 콜을 한 뒤에는 같은 라운드에서 다시 올릴 수 없다. 2. 내는 금액이 `toCall`보다 크다. - 위 규칙 때문에 레이즈 당한 사람은 같은 라운드에서 콜 또는 다이만 할 수 있다. 라운드는 반드시 유한하게 끝난다. - 보스(첫 행동자)가 체크하면 다음 사람도 `opened=false` 상태이므로 체크/삥/쿼터/하프(/풀)/다이를 고를 수 있다. 전원 체크하면 다음 카드로 넘어간다. 체크한 사람은 뒤에 누가 걸면 콜/다이만 가능. - 올인: 낼 금액이 스택 이상이면 스택 전부를 내고 올인한다. 올인 금액이 `toCall`보다 작으면 "부분 콜"이며 사이드 팟이 생긴다(5.5). 올인한 사람은 이후 행동하지 않고 카드는 계속 받는다. 올인 금액이 `toCall`보다 크면 레이즈로 취급한다(다른 사람의 레이즈 가능 여부는 위 규칙 그대로). - 라운드 종료: 다이·올인하지 않은 모든 사람이 이번 라운드에 한 번 이상 행동했고, 모두 `committedRound == currentBet`이면 종료. 행동 가능한 사람이 1명 이하이고 그 사람이 `currentBet`을 맞췄으면 남은 라운드는 베팅 없이 카드만 돌린다. 계산 예시(4명, 앤티 10, 기본 단위 10, `koreanMaxRaise=half`): - 앤티 후 `pot=40`. 보스 A 삥 10 → `pot=50`, `currentBet=10`. - B 하프: `toCall=10`, 콜 후 판돈 60, 그 절반 30 → 낼 금액 40. `pot=90`, `currentBet=40`. - C 따당: `toCall=40` → 80. `pot=170`, `currentBet=80`. - D 다이. - A 차례: 이미 삥(레이즈)을 했으므로 콜/다이만 가능. 콜 70 → `pot=240`. - B 차례: 이미 레이즈했으므로 콜 40 → `pot=280`. A, B, C 모두 80을 맞췄으므로 라운드 종료. 검산: 앤티 40 + A 80 + B 80 + C 80 = 280. ### 5.3 베팅 방식 B: 노리밋(`noLimit`) / 팟리밋(`potLimit`) - 행동: 폴드, 체크(`toCall=0`), 콜, 벳(`currentBet=0`일 때 처음 거는 것), 레이즈, 올인. - 최소 벳 = `baseUnit`(빅 블라인드). 최소 레이즈 = 현재 최고액 + 직전 레이즈 크기(`lastRaiseSize`, 라운드 시작 시 `baseUnit`). 예: BB 10에서 30으로 올렸으면(레이즈 크기 20) 다음 레이즈는 최소 50. - 노리밋 최대 = 자기 스택 전부. - 팟리밋 최대 레이즈 총액(`raiseTo` 최대) = `currentBet + (pot + toCall)`. 여기서 `pot`은 이번 라운드에 이미 걸린 금액까지 포함한 전체 판돈. 예: 판돈 100, 앞사람이 20 벳, 내 `toCall=20` → 콜 후 판돈 140 → 최대 `raiseTo = 20 + 140 = 160`. - 불완전 올인 레이즈: 올인 금액이 최소 레이즈에 못 미치면 "불완전 레이즈"다. 이미 행동한 뒤 그 레이즈를 맞닥뜨린 사람은 콜/폴드만 가능하고 다시 올릴 수 없다(베팅 재개 안 됨). 아직 행동하지 않은 사람은 정상 레이즈 가능. `lastRaiseSize`는 불완전 레이즈로 바뀌지 않는다. - 프리플랍 BB 옵션: 아무도 레이즈하지 않고 BB까지 차례가 오면 BB는 체크 또는 레이즈할 수 있다. - 액션 순서: 프리플랍은 BB 다음 좌석부터(헤즈업은 딜러=SB가 먼저), 플랍 이후는 딜러 다음 좌석의 남은 사람부터(헤즈업은 BB가 먼저). - 라운드 종료 조건은 5.2와 같다. ### 5.4 모드별 진행 #### 5.4.1 텍사스 홀덤 (`holdem`) 1. 블라인드 → 각자 개인 카드 2장(비공개). 2. 프리플랍 베팅. 3. 번 카드 1장(뒷면으로 버림) 후 플랍 3장 공개 → 베팅. 4. 번 1장 후 턴 1장 → 베팅. 5. 번 1장 후 리버 1장 → 베팅. 6. 쇼다운: 개인 2장 + 공용 5장 중 아무 5장으로 최고 족보(6.1). 개인 카드를 하나도 안 써도 된다("보드 플레이"). #### 5.4.2 오마하 (`omaha`) - 홀덤과 같지만 개인 카드 4장. 쇼다운에서 반드시 개인 카드 정확히 2장 + 공용 카드 정확히 3장으로 5장을 만든다(가능한 6 × 10 = 60개 조합 중 최고). - 예: 개인 A♠A♥K♠Q♦, 공용 ♠ 4장이 깔려도 개인 ♠가 A♠, K♠ 두 장뿐이면 플러시 가능. 개인에 ♠가 1장뿐이면 공용 ♠ 4장으로는 플러시를 못 만든다. #### 5.4.3 7포커 (`sevenPoker`) 1. 앤티. 2. 각자 4장(비공개)을 받는다. 3. 초이스 단계(동시 진행): 각자 4장 중 1장을 버리고(아무도 못 봄, 끝까지 비공개), 남은 3장 중 1장을 오픈 카드로 고른다. 결과: 히든 2장 + 오픈 1장. 전원이 고르면 오픈 카드가 동시에 공개된다. 4. 4구: 각자 1장씩 오픈으로 받는다(오픈 2장). 첫 베팅 라운드. 3구 베팅은 없다(한국식 4구 룰). 5. 5구: 오픈 1장 → 베팅. 6. 6구: 오픈 1장 → 베팅. 7. 7구(히든): 비공개 1장 → 마지막 베팅. 8. 쇼다운: 7장 중 최고 5장(6.2 한국식 족보, 무늬 서열 사용). - 보스(각 라운드 첫 행동자): 오픈 카드만으로 가장 높은 패를 보인 사람. 오픈 카드 비교는 원페어/투페어/트리플/포카드/탑만 본다(스트레이트·플러시는 오픈 4장 이하라 따지지 않음). 비교는 6.2와 같은 방식(같은 숫자면 무늬). 무늬가 있으므로 동점이 없다. 행동은 보스부터 시계 방향. - 다이한 사람의 카드는 공개하지 않는다(오픈 카드는 이미 공개된 것 그대로 둠). #### 5.4.4 7포커 하이로우 (`sevenHiLo`) - 1~7은 7포커와 동일. - 마지막 베팅이 끝나고 2명 이상 남으면 선언 단계(동시 진행): 남은 사람 전원이 비공개로 하이/로우/스윙 중 하나를 고르고, 전원 선택 후 동시에 공개한다. - 하이: 7장 중 최고 하이 족보(6.2)로 경쟁. - 로우: 7장 중 최고 로우(6.3)로 경쟁. - 스윙: 하이와 로우 양쪽에서 모두 이겨야 한다(6.6). - 남은 사람이 1명이면 선언 없이 그 사람이 가져간다. - 올인한 사람도 선언한다. #### 5.4.5 바둑이 (`badugi`) 1. 앤티 → 각자 4장(비공개). 2. 1차 베팅(교환 전). 첫 행동자는 딜러 다음 좌석, 이하 모든 라운드 동일. 3. 아침(1차 교환, 동시 진행): 각자 0~4장을 골라 버린다. 0장=패스, 1~3장=커트, 4장=박스. 전원 제출 후 딜러 다음 좌석부터 순서대로 버린 수만큼 새 카드를 받는다. 교환 장수는 모두에게 공개된다. 4. 2차 베팅 → 점심(2차 교환) → 3차 베팅 → 저녁(3차 교환) → 4차(마지막) 베팅. 5. 쇼다운: 6.4 바둑이 족보. - 덱 부족: 교환할 카드가 덱에 모자라면, 덱에 남은 카드를 먼저 주고, 그동안 버려진 카드(이번 교환에서 버린 카드는 제외)를 `rng`로 섞어 새 덱으로 삼아 나머지를 준다. 그래도 모자라면 이번 교환에서 버린 카드까지 섞는다(이론상 6인에서만 가능한 극단 상황). - 올인한 사람도 교환은 계속 한다. #### 5.4.6 5카드 드로우 (`fiveDraw`) 1. 앤티 → 각자 5장(비공개). 2. 1차 베팅. 3. 교환 1회(동시 진행): `drawLimit=any`면 0~5장, `three`면 최대 3장(손에 A가 있고 그 A를 남기면 최대 4장). 덱 부족 처리는 바둑이와 같다. 4. 2차 베팅 → 쇼다운(6.1 국제 족보). #### 5.4.7 인디언 포커 (`indian`) - 1장 모드(`indianCards=1`): 1. 앤티(`baseUnit`). 2. 각자 1장을 이마에 붙인다: 자기 카드는 못 보고 남의 카드는 본다. 3. 베팅 1회(노리밋, 최소 단위 `baseUnit`, 딜러 다음 좌석부터). 4. 쇼다운: 숫자가 높은 사람이 이긴다(6.5). - 2장 모드(`indianCards=2`): 1. 앤티 → 이마 카드 1장 → 1차 베팅. 2. 손 카드 1장(본인만 봄) → 2차 베팅. 3. 쇼다운: 이마 + 손 2장으로 6.5 족보. - 핸드가 끝나면 다이한 사람까지 모든 이마/손 카드를 전원에게 공개한다(사용된 카드 기록, `whenEmpty`에서 카드 기억 요소). - 10 벌칙(`indianFoldPenalty`): 덱 최고 숫자(`d20`=10, `d52`=A)를 이마에 단 채 다이하면, 그 사람은 `10 × baseUnit`(스택이 모자라면 남은 전부)을 벌칙으로 낸다. 벌칙 칩은 이번 핸드 팟과 별도로 그 핸드 승자에게 간다(승자가 여럿이면 균등 분할, 판돈이 이월되면 벌칙도 이월 판돈에 더함). - 덱(`whenEmpty`): 핸드마다 필요한 장수(참가자 수 × 카드 수)가 덱에 없으면 사용된 카드 전부를 모아 새로 섞는다. `everyHand`는 매 핸드 새로 섞는다. ### 5.5 올인과 사이드 팟 - 라운드가 끝날 때(또는 쇼다운 직전) 각 플레이어의 이번 핸드 총 기여액(`committedTotal`)으로 팟을 다시 계산한다: 1. 다이하지 않은 올인 플레이어들의 `committedTotal` 값을 오름차순으로 정렬한 레벨 L1 < L2 < ... 2. 각 레벨 구간마다 모든 플레이어(다이한 사람 포함)의 기여액 중 그 구간에 해당하는 부분을 모아 팟을 만들고, 그 구간까지 기여한 다이하지 않은 플레이어를 자격자로 둔다. 3. 마지막 레벨 위로 남은 금액은 최종 팟(자격자 = 다이·올인하지 않고 끝까지 낸 사람). - 예: A 100 올인, B 300 올인, C 500(마지막 베팅 300을 콜해 300만 기여). 기여 A100/B300/C300 → 메인 팟 300(A·B·C 자격), 사이드 팟 400(B·C 자격). C가 처음 500을 걸었다면 아무도 받지 못한 200은 C에게 돌려준다(반환은 라운드 종료 시). - 자격자가 1명뿐인 팟은 쇼다운 없이 그 사람에게 돌려준다. - 앤티와 다이한 사람의 칩은 해당 레벨 팟에 남는다. ### 5.6 칩이 떨어졌을 때: 리바이와 탈락 - 핸드 종료 후 칩이 0인 플레이어는 `busted`. - `rebuy=off`: 즉시 탈락(`eliminated`), 관전 화면으로 전환되지만 결과 순위에는 남는다. - `rebuy=limited/unlimited`(남은 횟수 있음): 리바이 단계에서 "칩 다시 받기 / 그만하기"를 고른다(제한 시간 15초, 시간 초과는 그만하기). 받으면 `startingChips`로 채우고 `rebuys += 1`. `endCondition=hands`에서 마지막 핸드가 끝난 직후에는 리바이를 묻지 않는다. - 칩이 남아 있어도 앤티/블라인드보다 적으면 그대로 참가하고 강제 베팅에서 올인된다(리바이는 0일 때만). - 리바이는 실제 재화와 무관한 방 내부 칩 보충이다. ### 5.7 블라인드/앤티 상승 - `blindUpEvery = N > 0`이면 N핸드가 끝날 때마다 `baseUnit`을 2배로 올린다(레벨 표시). 상한은 `startingChips / 5`. ### 5.8 자리 비움 - 같은 플레이어가 연속 2번 시간 초과되면 `away`로 표시하고, 이후 그 사람의 차례는 제한 시간 5초로 줄인다(자동 행동은 8.5와 같음). 다시 직접 행동하면 해제된다. - 방을 완전히 나간 플레이어(`left`)는 남은 칩을 몰수하지 않고 그대로 두되, 매 핸드 강제 베팅 후 자동 다이한다. 칩이 0이 되면 탈락. 결과 순위에는 남는다. ## 6. 승패와 점수 계산 ### 6.1 국제 족보 (홀덤·오마하·5카드 드로우, 무늬 비교 없음) 높은 순서: 1. 스트레이트 플러시(로열 = A-K-Q-J-10 스트레이트 플러시 포함): 가장 높은 카드로 비교. A-2-3-4-5는 가장 낮은 스트레이트 플러시(5가 탑). 2. 포카드: 포카드 숫자 → 키커 1장. 3. 풀하우스: 트리플 숫자 → 페어 숫자. 4. 플러시: 5장을 높은 순으로 차례대로 비교. 5. 스트레이트: 가장 높은 카드. A-2-3-4-5(휠)가 가장 낮고 10-J-Q-K-A가 가장 높다. K-A-2-3-4 같은 순환은 불인정. 6. 트리플: 트리플 숫자 → 키커 2장. 7. 투페어: 높은 페어 → 낮은 페어 → 키커. 8. 원페어: 페어 → 키커 3장. 9. 하이카드: 5장 차례대로. 5장이 완전히 같은 서열이면 동점 → 해당 팟을 균등 분할. 나머지 칩(홀수 칩)은 딜러 다음 좌석부터 시계 방향으로 가장 가까운 승자에게 1칩씩. 예(홀덤): 보드 K♠ K♦ 7♣ 4♥ 2♠, A: A♣ 3♦, B: Q♥ J♥ → 둘 다 K 원페어, 키커 A > Q → A 승. 보드 9-9-8-8-A면 개인 카드가 모두 A 미만인 두 사람은 보드 플레이로 분할. ### 6.2 한국식 족보 (7포커·하이로우 하이, 무늬 서열 사용) 무늬 서열: ♠ > ◆ > ♥ > ♣. 숫자 서열: A > K > Q > J > 10 > ... > 2. 높은 순서: 1. 로열 스트레이트 플러시(로티플): 같은 무늬 A-K-Q-J-10. 2. 백 스트레이트 플러시: 같은 무늬 A-2-3-4-5. 3. 스트레이트 플러시: 같은 무늬 연속 5장(K탑 ~ 6탑). 4. 포카드. 5. 풀하우스. 6. 플러시. 7. 마운틴: A-K-Q-J-10 스트레이트. 8. 백 스트레이트: A-2-3-4-5 스트레이트. 9. 스트레이트: 그 밖의 연속 5장(K탑 > Q탑 > ... > 6탑). 10. 트리플. 11. 투페어. 12. 원페어. 13. 탑(노페어). 같은 족보끼리의 비교("족보 결정 카드"의 숫자 → 무늬): | 족보 | 1차 비교 | 동점 시 | |---|---|---| | 로티플/백스티플/스티플 | 서열(로티플 > 백스티플 > K탑 > ... > 6탑) | 무늬 | | 포카드 | 포카드 숫자(같을 수 없음) | - | | 풀하우스 | 트리플 숫자(같을 수 없음) | - | | 플러시 | 5장을 높은 숫자부터 차례로 | 플러시 무늬 | | 마운틴/백/스트레이트 | 서열(마운틴 > 백 > K탑 > ... > 6탑) | 가장 높은 카드의 무늬(백 스트레이트는 A의 무늬, 마운틴도 A의 무늬) | | 트리플 | 트리플 숫자(같을 수 없음) | - | | 투페어 | 높은 페어 숫자 → 낮은 페어 숫자 | 높은 페어 중 좋은 무늬 | | 원페어 | 페어 숫자 | 페어 두 장 중 좋은 무늬 | | 탑 | 5장을 높은 숫자부터 차례로 | 가장 높은 카드의 무늬 | - 투페어·원페어는 키커를 보지 않는다(무늬로 바로 결정). 한 덱이고 공용 카드가 없으므로 위 규칙으로 항상 승자가 하나로 정해진다. - 7장 중 5장 선택: 21개 조합을 모두 평가해 위 비교기로 최대인 것을 쓴다(스리페어면 위 두 페어가 자동 선택). - 예1: A 7♠7♣ (원페어 7), B 7◆7♥ (원페어 7) → A의 ♠가 B의 최고 무늬 ◆보다 높으므로 A 승. - 예2: A 마운틴(A♣ 탑), B 백 스트레이트(A♠) → 마운틴이 서열상 높으므로 A 승(무늬는 같은 서열일 때만 봄). - 예3: A 투페어 K-5, B 투페어 K-9 → 낮은 페어 9 > 5로 B 승. ### 6.3 하이로우 로우 판정 `hiLoLow=korean`(기본, 한게임 방식 "최고 로우 A-2-3-4-6"): - A는 1로 본다. 7장 중 서로 다른 숫자 5장을 고르되, 그 5장이 스트레이트(A-2-3-4-5 포함)나 플러시가 되면 안 된다. - 가능한 조합 중 가장 높은 카드부터 차례로 비교해 낮은 쪽이 좋다. 최고 로우는 A-2-3-4-6("6탑"). - 모든 숫자가 같으면 가장 높은 카드의 무늬로 결정(♠ 우선). - 조건을 만족하는 5장이 없으면(서로 다른 숫자가 5개 미만 등) 로우 패가 없다. `hiLoLow=aceToFive`(옵션): 스트레이트·플러시를 무시하고 서로 다른 숫자 5장만 보면 된다. 최고 로우 A-2-3-4-5. `hiLoQualifier=eight`: 로우의 가장 높은 카드가 8 이하여야 로우 자격이 있다. `none`이면 K탑까지 인정. 예: 7장 A♠ 2♦ 3♥ 4♣ 5♠ 9♦ K♥ (`korean`) → A-2-3-4-5는 백 스트레이트라 불가, 가능한 최선은 A-2-3-4-9("9탑", 같은 무늬 5장 아님). `aceToFive`면 A-2-3-4-5. ### 6.4 바둑이 족보 (로우 바둑이) - A는 1(가장 낮음), K가 가장 높다. 무늬는 순위가 없다. - 4장 중 "숫자도 무늬도 서로 겹치지 않는" 가장 큰 부분집합의 크기 k로 등급을 정한다. - k=4: 메이드(예: A♠ 4♣ 5♦ 6♥ = "6 메이드"). - k=3: 베이스(예: A♠ 4♣ 5♦ 5♥ = "5 베이스"). - k=2: 투베이스. - k=1: 노베이스(네 장이 모두 같은 무늬 또는 같은 숫자). - k가 큰 쪽이 무조건 이긴다(메이드 > 베이스 > 투베이스 > 노베이스). - 같은 k끼리는 그 k장을 높은 숫자부터 차례로 비교해 낮은 쪽이 이긴다. 같은 k의 후보 부분집합이 여럿이면 가장 낮은 것을 쓴다. - 이름 붙은 메이드: 골프 A-2-3-4(최강), 세컨드 A-2-3-5, 써드 A-2-4-5. - 모든 숫자가 같으면 무승부 → 분할(홀수 칩은 6.1과 같은 방식). - 예: A 2♠ 3♦ 7♥ 8♣ (8 메이드) vs B A♠ 2♦ 3♥ 3♣ (3 베이스) → A 승(메이드 > 베이스). C 2♥ 4♣ 7♦ 8♠ (8 메이드) vs A → 8=8, 7=7, 다음 4 > 3 → A 승. ### 6.5 인디언 포커 판정 - 1장 모드: 숫자가 높은 사람이 이긴다. `d20`은 10 > 9 > ... > 2 > A(1). `d52`는 A > K > ... > 2. 무늬는 보지 않는다. - 2장 모드: 페어(같은 숫자 2장) > 노페어. 페어끼리는 숫자, 노페어끼리는 높은 카드 → 낮은 카드 순으로 비교. - 최고 패가 여럿이면: - `indianTie=carry`(기본, 지니어스 방식): 해당 팟을 다음 핸드 판돈으로 이월(`carryOver`). 다음 핸드 승자가 함께 가져간다. 게임의 마지막 핸드에서 이월되면 동점자끼리 분할. - `split`: 동점자끼리 분할. ### 6.6 팟 분배 순서와 하이로우 분배 - 팟은 사이드 팟부터(자격자가 적은 팟부터) 메인 팟 순으로 각각 독립적으로 판정한다. - 하이로우 각 팟(자격자 E): 1. 스윙 선언자 각각에 대해: E 안에서 하이 경쟁자(하이·스윙 선언자) 전원보다 하이가 엄격히 높고, 로우 경쟁자(로우·스윙 선언자) 전원보다 로우가 엄격히 좋으면 그 팟 전부를 가져간다. 로우 패가 없거나(6.3) 한쪽이라도 지면 그 스윙 선언자는 이 팟에서 완전히 제외된다. 2. 남은 자격자로: 하이 쪽 승자 = 하이 선언자 중 최고 하이, 로우 쪽 승자 = 로우 패 자격이 있는 로우 선언자 중 최고 로우. 3. 양쪽 다 승자가 있으면 팟을 반으로 나눠 하이 쪽에 `ceil(팟/2)`, 로우 쪽에 `floor(팟/2)`. 4. 한쪽만 승자가 있으면 그쪽이 전부 가져간다. 5. 아무도 없으면(예: 스윙 2명 모두 실패) 그 팟의 자격자 전원이 하이 족보로 다시 겨룬다. - 무늬 서열 덕분에 하이·로우 모두 동점이 생기지 않는다. - 예: A 하이(풀하우스), B 로우(7탑), C 스윙(플러시 + 8탑) → C는 하이에서 A에게 지므로 제외. A가 반, B가 반. ### 6.7 게임 종료와 점수 - `endCondition=hands`: `handLimit` 핸드가 끝나면 종료. 점수 = `최종 칩 - startingChips × rebuys`(리바이로 받은 칩을 뺀 순이익). 점수 높은 순으로 순위. 점수가 같으면 공동 순위. - `endCondition=lastStanding`: 칩을 가진 참가자가 1명만 남으면 종료(리바이 가능한 `busted` 플레이어가 결정을 기다리는 중이면 아직 종료 아님). 1위 = 마지막 생존자, 이후는 늦게 탈락한 순. 같은 핸드에서 여럿이 탈락하면 그 핸드 시작 시 칩이 많았던 사람이 높은 순위, 그것도 같으면 공동 순위. 안전장치: 300핸드에 도달하면 `hands` 방식 점수로 강제 종료. - 공통: 참가자 중 접속 중이고 칩이 있는 사람이 1명 이하가 되면(나머지 모두 `left`) 즉시 종료. ## 7. 지역 룰 / 하우스 룰 (옵션으로 켜고 끄기) | 하우스 룰 | 옵션 | 기본 | |---|---|---| | 풀 베팅 허용(하프 게임/풀 게임) | `koreanMaxRaise` | 하프까지 | | 하이로우 로우에 스트레이트·플러시 인정 여부(A-2-3-4-5 최고 로우 방식) | `hiLoLow` | 불인정(한게임식) | | 하이로우 8탑 자격 | `hiLoQualifier` | 없음 | | 5카드 드로우 교환 3장(A 있으면 4장) 제한 | `drawLimit` | 제한 없음 | | 승자가 다음 선 | `dealerRule` | 회전 | | 인디언 10 다이 벌칙 | `indianFoldPenalty` | 켬 | | 인디언 동점 이월 | `indianTie` | 이월 | | 인디언 덱 카드 기억 | `indianReshuffle` | 덱 소진 시 섞기 | | 리바이 | `rebuy`, `rebuyLimit` | 무제한 | | 블라인드 상승 | `blindUpEvery` | 없음 | | "491/591 룰"(4구/5구 첫 베팅은 하프·다이만 허용) | 미지원(향후) | - | | 맥스 베팅 상한, 하이 바둑이, 오마하 하이로우, 잭스 오어 베터(오프닝 조건) | 미지원(향후) | - | ## 8. 엔진 설계 ### 8.1 상태(State) ```ts // 서버만 보는 전체 상태 type PlayerId = string; type Suit = 'S' | 'D' | 'H' | 'C'; // ♠ ◆ ♥ ♣ (한국식 서열 S > D > H > C) type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14; // 11=J 12=Q 13=K 14=A, 1은 인디언 d20의 A interface Card { id: number; rank: Rank; suit: Suit } // id는 덱 안에서 고유(0..51 또는 0..19) type PokerMode = 'holdem' | 'omaha' | 'sevenPoker' | 'sevenHiLo' | 'badugi' | 'fiveDraw' | 'indian'; type Betting = 'korean' | 'noLimit' | 'potLimit'; interface PokerOptions { mode: PokerMode; betting: Betting | 'auto'; startingChips: 500 | 1000 | 2000 | 5000; baseUnit: 2 | 10 | 20 | 50; koreanMaxRaise: 'half' | 'full'; endCondition: 'hands' | 'lastStanding'; handLimit: 10 | 20 | 30 | 50; blindUpEvery: 0 | 5 | 10 | 15; rebuy: 'off' | 'limited' | 'unlimited'; rebuyLimit: 1 | 2 | 3; turnSeconds: 15 | 20 | 30 | 60; dealerRule: 'rotate' | 'winner'; hints: boolean; hiLoLow: 'korean' | 'aceToFive'; hiLoQualifier: 'none' | 'eight'; drawLimit: 'any' | 'three'; indianCards: 1 | 2; indianDeck: 'd20' | 'd52'; indianFoldPenalty: boolean; indianTie: 'carry' | 'split'; indianReshuffle: 'whenEmpty' | 'everyHand'; } interface SeatState { id: PlayerId; seat: number; stack: number; rebuys: number; status: 'playing' | 'busted' | 'eliminated' | 'left'; away: boolean; timeoutsInRow: number; eliminatedAtHand: number | null; stackAtHandStart: number; // 같은 핸드 동시 탈락 순위용 } interface HandPlayer { id: PlayerId; folded: boolean; allIn: boolean; hole: Card[]; // 비공개: 홀덤/오마하 개인 카드, 7포커 히든, 바둑이·드로우 손패, 인디언 2장 모드 손 카드 up: Card[]; // 공개: 7포커 오픈 카드(나온 순서) forehead: Card | null; // 인디언 이마 카드(본인 제외 공개) choice: Card[] | null; // 7포커 초이스 단계의 4장(초이스 끝나면 null) choiceDone: boolean; committedTotal: number; // 이번 핸드 총 기여(앤티·블라인드 포함) committedRound: number; // 이번 라운드 기여(앤티 제외, 블라인드 포함) actedThisRound: boolean; raisedThisRound: boolean; // 한국식: 삥/레이즈 1회 사용 lockedFromRaise: boolean; // 한국식: 체크·콜 후 / NL: 불완전 레이즈로 재개 불가 drawPending: number[] | null; // 제출한 교환 카드 id(동시 단계) drawCounts: number[]; // 공개: 교환 장수 기록(아침/점심/저녁) declare: 'high' | 'low' | 'swing' | null; ready: boolean; // handEnd "다음 판" } interface Pot { amount: number; eligible: PlayerId[] } interface BettingRound { street: number; // 모드별 라운드 번호(0부터) order: PlayerId[]; // 이번 라운드 행동 순서(보스/첫 행동자부터) toAct: PlayerId | null; currentBet: number; lastRaiseSize: number; // NL/PL 최소 레이즈 opened: boolean; // 한국식: 삥/레이즈 존재 여부 } interface ShowdownEntry { id: PlayerId; cards: Card[]; // 공개되는 전체 카드 best: Card[]; // 사용한 5장(바둑이는 k장) handName: string; // "풀하우스", "7 메이드" 등 한국어 lowName?: string; } interface Award { potIndex: number; to: PlayerId; amount: number; side?: 'high' | 'low' | 'scoop' } interface HandState { handNo: number; phase: 'choice' | 'betting' | 'draw' | 'declare' | 'showdown' | 'handEnd'; street: number; dealerSeat: number; deck: Card[]; // 남은 덱(절대 클라이언트로 안 나감) muck: Card[]; // 버린 카드(초이스 버림, 교환 버림, 번 카드, 다이한 패) board: Card[]; // 공용 카드 players: Record; seatOrder: PlayerId[]; // 이번 핸드 참가자, 딜러 다음 좌석부터 betting: BettingRound | null; pots: Pot[]; // 확정된 팟(라운드 종료마다 재계산) penalties: { from: PlayerId; amount: number }[]; // 인디언 10 벌칙 showdown: ShowdownEntry[] | null; awards: Award[] | null; deadlineAt: number | null; } interface PokerState { options: PokerOptions; betting: Betting; // auto 해석 결과 rng: RngState; // 시드 PRNG 상태 seats: SeatState[]; baseUnit: number; // 블라인드 상승 반영 현재값 hand: HandState | null; handsPlayed: number; carryOver: number; // 인디언 이월 판돈 indianDeck: Card[] | null; // 인디언 whenEmpty 덱(남은 카드) indianUsed: Card[]; // 공개된 사용 카드 phase: 'playing' | 'rebuy' | 'finished'; rebuyPending: PlayerId[]; rebuyDeadlineAt: number | null; lastWinner: PlayerId | null; eliminationSeq: PlayerId[][]; // 탈락 순서(핸드 단위 묶음) publicLog: PublicLogEntry[]; // 최근 100개, 비밀 정보 없음 } ``` 엔진 인터페이스 메모: `minPlayers=2`, `maxPlayers=10`(정적 값). 모드별 상한은 `optionsSchema`의 `superRefine`과 별도 헬퍼 `playerRange(options)`로 로비가 확인한다(인터페이스에 선택 메서드로 추가 제안). `setup`에서도 인원 초과면 예외를 던진다. ### 8.2 액션 | type | payload | 누가 / 언제 | 검증 조건 | |---|---|---|---| | `choose` | `{ discardId, openId }` | 7포커/하이로우, `phase=choice`, 아직 선택 안 한 참가자(동시) | 두 id가 내 `choice` 4장에 있고 서로 다름. 이미 선택했으면 "이미 골랐어요" | | `k.check` | - | 한국식, 내 차례 | `opened=false`, 다이/올인 아님 | | `k.ping` | - | 한국식, 내 차례 | `opened=false`, `raisedThisRound=false`, 스택 > 0 | | `k.call` | - | 한국식, 내 차례 | `toCall > 0` | | `k.ddadang` | - | 한국식, 내 차례 | `toCall > 0`, 레이즈 가능(5.2) | | `k.quarter` / `k.half` | - | 한국식, 내 차례 | 레이즈 가능, 낼 금액 > `toCall` | | `k.full` | - | 한국식, 내 차례 | 위 + `koreanMaxRaise=full` | | `k.die` | - | 한국식, 내 차례 | 항상 | | `nl.fold` | - | NL/PL, 내 차례 | 항상(단 `toCall=0`이면 UI가 확인 창 표시) | | `nl.check` | - | NL/PL, 내 차례 | `toCall=0` | | `nl.call` | - | NL/PL, 내 차례 | `toCall > 0` (스택 부족이면 올인 콜) | | `nl.raise` | `{ to: number }` | NL/PL, 내 차례 | 정수, `lockedFromRaise=false`, `to ≥ 최소 레이즈`(스택 전부면 예외 허용), `to ≤ committedRound + stack`, PL이면 `to ≤ 팟리밋 최대`. `currentBet=0`이면 "벳"으로 표시 | | `nl.allIn` | - | NL/PL, 내 차례 | 스택 > 0. PL에서 스택이 팟리밋보다 크면 거부(최대 레이즈로 안내) | | `draw` | `{ cardIds: number[] }` | 바둑이/드로우, `phase=draw`, 미제출 참가자(동시, 올인 포함) | 중복 없음, 모두 내 손패, 개수: 바둑이 0~4, 드로우 `any` 0~5 / `three` 0~3(A를 남기면 0~4) | | `declare` | `{ side: 'high'\|'low'\|'swing' }` | 하이로우, `phase=declare`, 남은 참가자(동시) | 미선언 | | `ready` | - | `phase=handEnd`, 참가자 | 아직 준비 안 함 | | `rebuy` | `{ accept: boolean }` | `phase=rebuy`, `rebuyPending`에 있는 사람 | 남은 리바이 횟수 > 0 | | `leave` | - | 아무 때나(서버가 방 퇴장 시 주입) | 이미 `left` 아님 | 공통 거부 사유 예: "지금은 내 차례가 아니에요", "이번 판에서 이미 다이했어요", "콜 금액이 있어서 체크할 수 없어요", "이번 라운드에서는 더 올릴 수 없어요(콜 또는 다이만 가능)", "최소 레이즈는 50이에요". `apply` 순서: 칩 이동 → 행동 기록 → 라운드 종료 판정 → (종료 시) 팟 재계산, 다음 카드 분배 또는 다음 단계 → 다음 행동자와 `deadlineAt = now + turnSeconds*1000`. ### 8.3 공개/비공개 정보 (view) 공개(모든 시청자 동일): 좌석·닉네임·스택·상태, 딜러/선/보스 표시, 공용 카드, 7포커 오픈 카드, 각자의 비공개 카드 장수, 교환 장수, 이번 라운드 베팅액과 마지막 행동, 팟 목록, 현재 행동자와 남은 시간, 하이로우 선언(공개 시점 이후), 인디언 사용 카드 목록, 이월 판돈, 핸드 결과. | 정보 | 본인 | 다른 플레이어 | 관전자(`null`) | |---|---|---|---| | 내 `hole`(히든·개인 카드) | 보임 | 장수만 | 장수만 | | 7포커 초이스 4장 | 보임 | 장수만(4) | 장수만 | | 초이스로 버린 카드 | 내 것만 "버린 카드"로 표시 | 안 보임 | 안 보임 | | 하이로우 선언 | 내 선택만 | 전원 선택 완료 후 공개 | 같음 | | 인디언 이마 카드 | 안 보임("?") | 보임 | 핸드 종료 전에는 안 보임 | | 인디언 2장 모드 손 카드 | 보임 | 안 보임 | 안 보임 | | 덱, 번 카드, 교환으로 버린 카드 | 안 보임 | 안 보임 | 안 보임 | | 쇼다운 카드 | 쇼다운에 간 사람의 전체 카드 공개 | 같음 | 같음 | | 다이한 사람 카드 | 내 것만 | 안 보임(인디언은 핸드 종료 후 공개) | 같음 | - 관전자에게 인디언 이마 카드를 숨기는 이유: 플레이어가 다른 기기로 관전에 들어와 자기 이마 카드를 보는 부정행위를 막기 위해서다. 같은 이유로 관전자는 어떤 모드에서도 비공개 카드를 볼 수 없다("고스트" 방지). - `view`는 `legalActions`(본인 차례일 때 가능한 행동 목록과 각 행동의 정확한 낼 금액, NL 레이즈 최소/최대)도 계산해 내려 준다. UI는 이 값만 사용한다. - 이벤트(`GameEvent`)에도 비밀을 넣지 않는다. 카드 분배 이벤트는 `{ type: 'dealt', to, count, faceUp?: Card }`처럼 공개 카드만 값이 있고, 비공개 카드는 이후 `view`로만 전달된다. - 카드 `id`는 숫자·무늬와 고정 대응하므로 비공개 카드 자리에는 `id`도 보내지 않고 `{ hidden: true }`만 보낸다. 클라이언트 애니메이션용 키는 view마다 새로 만든 임시 슬롯 번호를 쓴다. ### 8.4 랜덤 요소 (시드 RNG 사용 지점) 1. `setup`: 좌석 섞기, 첫 딜러 선택. 2. 핸드 시작: 52장(또는 인디언 덱) Fisher-Yates 섞기. 이후 분배는 덱 위에서 순서대로 꺼내므로 추가 난수 없음. 3. 바둑이/드로우 덱 부족 시 버린 카드 재섞기. 4. 인디언 `whenEmpty` 덱 소진 시 재섞기. - 자동 행동, 팟 분배, 홀수 칩, 보스 결정은 난수를 쓰지 않는다(결정적). - 테스트는 고정 시드로 덱 순서를 재현한다. 상태에 `rng`가 있으므로 같은 시드 + 같은 액션 열이면 같은 결과. ### 8.5 타이머·시간 초과·연결 끊김 시 자동 행동 `deadline(state)`: - 베팅: `turnSeconds`(자리 비움이면 5초). - 7포커 초이스·교환·선언: `turnSeconds + 10`초(동시 단계, 모두에게 같은 마감). - `handEnd`: 4초. 리바이 결정: 15초. `onTimeout(state, player)`: | 단계 | 자동 행동 | |---|---| | 베팅(한국식) | 체크 가능하면 `k.check`, 아니면 `k.die` | | 베팅(NL/PL) | `toCall=0`이면 `nl.check`, 아니면 `nl.fold` | | 7포커 초이스 | 4장 중 숫자가 가장 낮은 카드(같으면 무늬가 낮은 것)를 버리고, 남은 3장 중 숫자가 가장 낮은 카드를 오픈(페어를 오픈하지 않는 단순 규칙) | | 바둑이 교환 | 패를 바꾸지 않음(패스) | | 드로우 교환 | 패스 | | 하이로우 선언 | 하이 | | `handEnd` | `ready` | | 리바이 | `{ accept: false }` | 연결 끊김: 서버는 끊긴 플레이어의 차례가 오면 일반 마감까지 기다린 뒤 `onTimeout`을 적용한다(재접속하면 바로 직접 행동 가능). 5.8의 자리 비움 규칙으로 진행 속도를 유지한다. ### 8.6 종료 조건과 결과(GameResult) 종료 판정은 6.7. `result(state)`는 `phase=finished`일 때: ```ts interface PokerResult /* extends GameResult */ { ranking: { playerId: PlayerId; rank: number; // 1부터, 공동 순위 허용 score: number; // hands: 최종 칩 - startingChips × rebuys / lastStanding: 생존 순서 기반(1위=N) finalChips: number; rebuys: number; eliminatedAtHand: number | null; }[]; summary: string; // 예: "20판 종료. 1위 민수(+1,240칩)" stats: { handsPlayed: number; biggestPot: { handNo: number; amount: number; winners: PlayerId[] }; bestHand: { playerId: PlayerId; handName: string; handNo: number } | null; }; } ``` ## 9. UI/UX 화면 배치(모바일 세로): - 위쪽 2/3: 테이블. 상대 플레이어는 위·양옆 호 모양으로 배치(닉네임, 스택, 이번 라운드 베팅액, 오픈 카드 축소판, 상태 뱃지 "다이"/"올인"/"자리 비움"). 10명이면 2줄 축소 배치. - 가운데: 판돈 큰 숫자, 사이드 팟은 작은 칩 더미와 금액, 공용 카드 5칸(홀덤·오마하). - 아래 1/3: 내 카드 크게(카드 높이 최소 96px), 그 위에 현재 족보 이름(`hints`일 때 "지금: 투페어"), 맨 아래 행동 버튼 줄. - PC: 테이블 원형, 오른쪽에 핸드 기록·족보표 패널. 행동 버튼(높이 56px 이상, 글자 18px 이상): - 한국식: `다이` `체크` `삥` `콜 (+70)` `따당 (+160)` `쿼터 (+60)` `하프 (+120)` `풀 (+240)`. 불가능한 버튼은 회색으로 숨기지 않고 비활성 표시, 가능한 버튼만 색 강조. 금액은 `legalActions` 값을 그대로 표시. - NL/PL: `폴드` `체크/콜 (+20)` `레이즈` → 레이즈 누르면 슬라이더 + 빠른 버튼(최소, 1/2팟, 팟, 올인) + `-`/`+` 버튼(기본 단위씩). 확인 버튼 한 번 더. - 다이/폴드는 `toCall=0`일 때 "체크할 수 있는데 포기할까요?" 확인. 모드별 조작: - 7포커 초이스: 4장 펼침 → 1단계 "버릴 카드를 누르세요"(빨간 X 표시) → 2단계 "보여 줄 카드를 누르세요"(위로 올라감) → `확인`. 되돌리기 가능. - 교환(바둑이/드로우): 카드를 눌러 위로 올리면 교환 대상. 버튼 문구가 "패스" / "2장 바꾸기"로 바뀜. 바둑이 힌트: 겹치는 무늬·숫자 카드에 노란 테두리. - 하이로우 선언: 큰 버튼 3개 `하이` `로우` `스윙`, 각 버튼 아래 내 하이/로우 패 이름. - 인디언: 내 자리에 "?" 카드가 이마 위치에, 다른 사람 카드는 크게. 사용된 카드 패널(1~10 각 2칸에 체크). - 쇼다운: 승자 카드 5장 위로 올라오며 족보 이름 크게, 칩이 승자에게 날아가는 애니메이션(0.6초). 사이드 팟은 하나씩 순서대로. 초보자 도움: - "규칙 보기": 모드별 1쪽 요약 + 족보표(그림 예시). 한국식 베팅 용어 설명(삥/따당/하프). - 첫 핸드에서 말풍선 튜토리얼(끌 수 있음). 내 차례에 진동/소리(설정에서 끄기), 남은 시간 원형 게이지, 5초 남으면 빨간색. - 접근성: 무늬는 색 + 모양 + 글자(♠ 검정, ◆ 파랑, ♥ 빨강, ♣ 초록 4색 덱 옵션)로 구분, 큰 글씨 모드. ## 10. 테스트 체크리스트 - [ ] 한국식 5.2 예시: 앤티 40 → 삥 10 → 하프 40 → 따당 80 → 다이 → 콜 70 → 콜 40, 최종 팟 280이고 라운드 종료. - [ ] 한국식: 콜 또는 체크한 플레이어가 같은 라운드에서 `k.half`를 보내면 거부("이번 라운드에서는 더 올릴 수 없어요"). - [ ] 한국식: 보스 체크 후 다음 사람 삥 가능, 이후 보스는 콜/다이만 가능. - [ ] 한국식: 전원 체크 시 베팅 없이 다음 카드로 진행. - [ ] 한국식 `koreanMaxRaise=half`에서 `k.full` 거부. - [ ] NL: BB 10, A 30 레이즈 후 B의 `nl.raise {to:45}` 거부(최소 50), `{to:50}` 허용. - [ ] NL 불완전 올인: A 벳 100, B 레이즈 300, C 올인 350(불완전), A는 콜/폴드만 가능하고 B도 다시 레이즈 불가. - [ ] PL: 판돈 100, 상대 벳 20일 때 최대 `to=160`, 161 거부. - [ ] 헤즈업 홀덤: 딜러가 SB이고 프리플랍 먼저 행동, 플랍부터는 BB가 먼저. - [ ] 사이드 팟: A 100 올인 / B 300 올인 / C 500 → 메인 300(ABC), 사이드 400(BC), C에게 200 반환. A 최강이면 A 300, 나머지 400은 B·C 중 승자. - [ ] 홀덤 분할: 보드 플레이 동점 시 균등 분할, 홀수 칩은 딜러 다음 좌석 쪽 승자. - [ ] 오마하: 개인 ♠ 1장 + 공용 ♠ 4장은 플러시가 아님을 판정. - [ ] 7포커 족보: 마운틴 > 백 스트레이트 > K탑 스트레이트, 백 스티플 > K탑 스티플 > 포카드. - [ ] 7포커 무늬 동점: 7♠7♣ vs 7◆7♥ → 앞쪽 승. 투페어 K-5 vs K-9 → 뒤쪽 승(키커 무시). - [ ] 7포커 보스: 오픈 카드 원페어가 탑보다 먼저 행동, 같은 숫자면 무늬로. - [ ] 7포커 초이스 동시 진행: 한 명만 골랐을 때 다른 사람 화면에 그 사람의 오픈 카드가 아직 안 보임. 전원 완료 후 동시 공개. - [ ] 하이로우 `korean`: A-2-3-4-5-9-K → 로우는 A-2-3-4-9. `aceToFive`면 A-2-3-4-5. - [ ] 하이로우 스윙 실패: 스윙이 하이에서 지면 제외되고 하이 승자·로우 승자가 반분. 로우 선언자가 없으면 하이 승자가 전부. - [ ] 하이로우 홀수 칩: 팟 101 → 하이 51, 로우 50. - [ ] 바둑이: A♠2♦3♥3♣(3 베이스) < 2♠3♦7♥8♣(8 메이드). 2♥4♣7♦8♠ vs 2♠3♦7♥8♣ → 뒤쪽(3) 승. 같은 숫자면 분할. - [ ] 바둑이 덱 부족: 6인 박스 반복 시 버린 카드 재섞기 후 정확한 장수 지급, 카드 중복 없음(52장 보존 불변식). - [ ] 드로우 `three`: A 없이 4장 교환 거부, A를 남기고 4장 교환 허용. - [ ] 인디언 1장: 본인 `view`의 자기 이마 카드가 `null`, 다른 플레이어 `view`에는 값이 있음, 관전자 `view`에는 핸드 종료 전 `null`. - [ ] 인디언: 10을 단 채 다이 → 벌칙 10×기본 단위가 승자에게 이동. 동점 `carry` → 다음 핸드 승자가 이월 판돈 획득. - [ ] 인디언 `whenEmpty`: 2인 20장 덱에서 10핸드 후 재섞기, 사용 카드 목록 초기화. - [ ] 시간 초과: 한국식 베팅에서 `toCall>0`이면 자동 다이, `opened=false`면 자동 체크. - [ ] 연결 끊김 2연속 시간 초과 → `away`, 이후 마감 5초. 재접속 후 직접 행동 시 해제. - [ ] 리바이: `limited`/1회 사용 후 다시 0이 되면 탈락. `hands` 점수가 `칩 - 1000×리바이`로 계산됨. - [ ] `lastStanding`: 같은 핸드에서 2명 탈락 시 핸드 시작 칩이 많던 사람이 더 높은 순위. - [ ] 비밀 누출: 모든 단계에서 각 시청자의 `view`와 이벤트 JSON을 직렬화해 상대 비공개 카드의 숫자/무늬/id, 덱 내용, 초이스 버림 카드가 문자열에 없는지 검사(속성 기반 테스트, 무작위 시드 1,000판). - [ ] 재현성: 같은 시드 + 같은 액션 열 → `apply` 결과 상태 해시 동일. - [ ] 칩 보존 불변식: 매 `apply` 후 `Σ스택 + Σ팟 + Σ이번 라운드 기여 + 이월 판돈 = 초기 칩 합 + 리바이 합`. ## 11. 참고 자료 - 나무위키 "세븐 포커": https://namu.wiki/w/세븐%20포커 (한국식 초이스 룰, 4구 룰, 체크레이즈 금지, 무늬로 동점 처리) - 나무위키 "섯다"(한국식 베팅 용어 정의 공통): https://namu.wiki/w/섯다 - 나무위키 "바둑이(플레잉 카드)": https://namu.wiki/w/바둑이(플레잉%20카드) (메이드/베이스/투베이스, 골프·세컨드·써드, 아침/점심/저녁, 무늬 비교 없음) - 한게임 포커 7포커 베팅 방법: https://poker.hangame.com/gameguide/poker7/game_7poker2_1.html (삥·체크는 보스, 따당·하프·쿼터·풀 정의) - 한게임 모바일 포커 규칙: https://m-poker.hangame.com/guide/rule (초이스 룰, 4구~7구 베팅, 하이로우 최고 로우 A-2-3-4-6, 패스/커트/박스) - 한게임 하이로우 기본룰: https://poker.hangame.com/gameguide/highlow2/game_highlow1_1.html (선언 시점, 스윙은 양쪽 모두 이겨야 함) - 넷마블 하이로우 가이드: http://c2.img.netmarble.kr/web/2007/html/gopo/po_guide/7cardhighlow/rule.html (로우에서 스트레이트·플러시 무시하는 변형 → `aceToFive` 옵션 근거) - 홀덤마스터 "세븐포커 족보로 홀덤 치면 지는 이유": https://www.holdemmaster.com/blog/holdem-vs-7poker-hand-rankings (무늬 서열 ♠>◆>♥>♣, 키커 대신 무늬) - 블루스택 한게임 포커 가이드: https://www.bluestacks.com/ko/blog/game-guides/hangame-poker/hp-rules-guide-ko.html - Wikipedia "Texas hold 'em": https://en.wikipedia.org/wiki/Texas_hold_%27em (인원, 헤즈업 블라인드, 불완전 레이즈, 홀수 칩) - Wikipedia "Omaha hold 'em": https://en.wikipedia.org/wiki/Omaha_hold_%27em - Wikipedia "Five-card draw": https://en.wikipedia.org/wiki/Five-card_draw (3장, A 있으면 4장 교환 하우스 룰) - Wikipedia "Badugi": https://en.wikipedia.org/wiki/Badugi (버린 카드 재섞기, 4회 베팅) - 보드라이프 "더 지니어스 인디언 포커" 후기: https://boardlife.co.kr/bbs_detail.php?bbs_num=1503&tb=board_community (1~10 두 벌 20장, 10 다이 벌칙, 동점 이월) 출처 간 차이와 이 문서의 선택: - 백 스트레이트 위치: 한게임 등 대부분 "마운틴 다음"으로 보지만 일부 글은 가장 낮은 스트레이트로 설명한다. 기본은 마운틴 다음(7포커 한국식). - 하이로우 로우: 한게임은 A-2-3-4-6이 최고(스트레이트 불인정), 넷마블 가이드는 A-2-3-4-5(무시). 기본은 한게임식, 옵션으로 제공. - 쿼터/하프/풀 금액 기준: 출처마다 "전체 판돈의 비율"이라고만 되어 있어, 콜 금액을 먼저 더한 판돈 기준으로 정의했다. ## 12. 메모 (상표·법적 주의 등) - 가상 칩만 사용: 방 전용, 실제 돈·유료 재화·광고 보상과 연결하지 않는다. 칩 선물·양도·방 간 이월·누적 랭킹 금지. 결과 화면에서도 "칩"이라는 단어만 쓰고 원(₩) 표시나 환산 표현을 쓰지 않는다. - 국내 법 검토 필요(확정 전 사용자 확인): - 형법 제246조(도박): 재물이나 재산상 이익을 걸지 않으므로 해당하지 않도록 설계했지만, 사용자 간 외부 정산을 유도하는 기능(칩 수 공유 랭킹, 칩 거래)은 만들지 않는다. - 게임산업진흥법: 게임물을 공개 배포하면 원칙적으로 등급분류 대상이다. 비영리 게임은 예외가 있으나 "청소년이용불가 등급 기준에 해당하는 내용"을 포함하면 예외에서 빠진다. 포커·고스톱류 웹보드 게임은 통상 청소년이용불가로 분류되므로, 포커를 공개 서비스에 넣기 전에 등급분류 필요 여부를 확인해야 한다. - 대상 사용자에 어린이가 포함되므로, 포커·섯다 등 베팅형 게임은 기본 목록에서 분리(예: "어른용 카드 게임" 탭, 방장 확인 문구)하는 방안을 검토한다. - 상표: "텍사스 홀덤", "7포커", "바둑이", "오마하", "인디언 포커"는 일반 게임 명칭으로 쓰인다. 다만 "한게임 포커", "피망 포커", "WSOP" 등 회사·대회 이름과 그 로고, 특정 사이트의 카드 디자인은 쓰지 않는다. 카드 그림(무늬·인물 카드)은 직접 그린 SVG를 사용한다. - 표시 이름 후보: 메뉴 이름 "포커"(모드: 홀덤 / 7포커 / 하이로우 / 바둑이 / 인디언 포커 / 5카드 드로우 / 오마하). "인디언 포커"는 영어권에서 문화적으로 민감하게 볼 수 있으므로 대체 표시 이름 "이마 포커"를 옵션으로 준비하고 사용자가 고르게 한다. - 문서 간 공유: 한국식 판돈 베팅(5.2), 사이드 팟(5.5), 리바이·탈락(5.6)은 `seotda`(섯다)와 같은 공용 모듈로 구현한다.