- 개요, 조사(BGA·한국 인기 보드게임·UI/UX), 아키텍처, 실시간 프로토콜, 안정성·보안, 계정, 로비·방, UI/UX, 데이터 모델, 게임 엔진, 테스트, 배포·운영, 로드맵(M0~M9), 사용자 결정 항목 - 게임별 규칙·엔진 설계·UI·테스트 체크리스트 26종 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
516 lines
35 KiB
Markdown
516 lines
35 KiB
Markdown
# 섯다 (`seotda`)
|
||
|
||
> 마일스톤: M3 · 인원: 최소 2 ~ 최대 10명 (3장 섯다는 최대 6명) · 예상 시간: 약 15~25분 (20판 기준) · 난이도: 쉬움~보통
|
||
|
||
## 1. 개요
|
||
|
||
- 화투 20장(1~10월 각 2장)으로 각자 2장의 조합(족보)을 겨루는 한국 전통 베팅 카드 게임이다. 명절·모임에서 고스톱과 함께 가장 널리 알려진 화투 놀이이고, "광땡", "장땡", "땡잡이", "구사" 같은 용어는 일상어처럼 쓰인다.
|
||
- 칩은 방 안에서만 쓰는 가상 칩이다. 돈·유료 재화·환전·양도·방 간 이월이 없다(12장).
|
||
- 인원 근거: 전통 규칙에 고정된 인원 상한은 없고, 덱 20장이 물리적 상한을 정한다. 2장 섯다는 1인 2장이므로 최대 10명, 3장 섯다는 1인 3장이므로 최대 6명(18장). 최소 2명. 실제로는 4~6명이 가장 흔하다.
|
||
- 베팅 방식(삥/콜/따당/하프/체크/다이)과 사이드 팟·리바이는 포커 문서(`poker`)의 한국식 판돈 베팅 공용 모듈을 그대로 쓴다. 이 문서에도 필요한 정의를 모두 다시 적는다.
|
||
|
||
## 2. 모드와 옵션
|
||
|
||
| 옵션 키 | 설명 | 선택지 | 기본값 |
|
||
|---|---|---|---|
|
||
| `cardMode` | 2장 섯다 / 3장 섯다(3장 받아 2장 선택) | `two` / `three` | `two` |
|
||
| `dealMode` | 2장 섯다 분배 방식 | `twoRounds`(1장 받고 베팅, 1장 더 받고 베팅) / `oneRound`(2장 한 번에 받고 베팅 1회) | `twoRounds` |
|
||
| `startingChips` | 시작 칩 | 500 / 1000 / 2000 / 5000 | 1000 |
|
||
| `baseUnit` | 기본 단위 = 학교(참가비, 앤티) = 삥 금액 | 2 / 10 / 20 / 50 | 10 |
|
||
| `maxRaise` | 허용하는 최대 레이즈 | `half`(하프까지) / `full`(풀까지) | `half` |
|
||
| `endCondition` | 게임 종료 방식 | `hands`(정해진 판 수) / `lastStanding`(최후 1인) | `hands` |
|
||
| `handLimit` | `hands`일 때 총 판 수(재경기는 판 수에 포함하지 않음) | 10 / 20 / 30 / 50 | 20 |
|
||
| `schoolUpEvery` | N판마다 학교 금액 2배(0=안 올림) | 0 / 5 / 10 / 15 | 0 |
|
||
| `rebuy` | 리바이(칩 다시 받기) | `off` / `limited` / `unlimited` | `unlimited` |
|
||
| `rebuyLimit` | `limited`일 때 1인당 횟수 | 1 / 2 / 3 | 1 |
|
||
| `turnSeconds` | 행동 제한 시간(초) | 15 / 20 / 30 / 60 | 20 |
|
||
| `dealerRule` | 다음 판의 선 | `winner`(직전 판 승자) / `rotate`(시계 방향) | `winner` |
|
||
| `gwangTtaeng` | 광땡 종류 | `all`(38·18·13광땡) / `only38`(38광땡만) | `all` |
|
||
| `ttaengjabi` | 땡잡이(3·7) | `off` / `strict`(3월 광 + 7월 열끗) / `loose`(3월 아무 패 + 7월 아무 패) | `strict` |
|
||
| `amhaeng` | 암행어사(4월 열끗 + 7월 열끗) | 켬 / 끔 | 켬 |
|
||
| `gusa` | 구사 재경기(4·9, "49파토") | 켬 / 끔 | 켬 |
|
||
| `mungGusa` | 멍텅구리구사(4월 열끗 + 9월 열끗) 재경기 범위 | `off`(일반 구사로 취급) / `upTo8`(8땡 이하 재경기) / `upTo9`(9땡 이하) / `upToJang`(장땡 이하) | `upTo8` |
|
||
| `tieRule` | 같은 족보 동점 처리 | `rematch`(동점자끼리 재경기) / `split`(나눠 가짐) | `rematch` |
|
||
| `rejoinCost` | 구사 재경기 때 다이한 사람의 재참가 | `none`(불가) / `half`(현재 판돈의 절반을 내고 참가) / `full`(현재 판돈만큼 내고 참가) | `none` |
|
||
| `hints` | 내 족보 이름 표시, 족보표 강조 | 켬 / 끔 | 켬 |
|
||
|
||
옵션 검증: `cardMode=three`이면 참가자 6명 이하, `dealMode`는 무시(3장 섯다 고유 흐름). `baseUnit × 20 ≤ startingChips`. `gusa=off`이면 `mungGusa`도 무시(4·9는 모두 3끗).
|
||
|
||
## 3. 구성물
|
||
|
||
화투 20장. 각 월의 "특수 패" 1장 + "일반 패" 1장:
|
||
|
||
| 월 | 꽃(그림 소재) | 특수 패 | 일반 패 |
|
||
|---|---|---|---|
|
||
| 1 | 송학(소나무·학) | 광 | 띠 |
|
||
| 2 | 매조(매화·꾀꼬리) | 열끗 | 띠 |
|
||
| 3 | 벚꽃 | 광 | 띠 |
|
||
| 4 | 흑싸리(등나무·두견새) | 열끗 | 띠 |
|
||
| 5 | 난초(창포·다리) | 열끗 | 띠 |
|
||
| 6 | 모란(나비) | 열끗 | 띠 |
|
||
| 7 | 홍싸리(싸리·멧돼지) | 열끗 | 띠 |
|
||
| 8 | 공산(억새·보름달) | 광 | 일반(기러기 그림) |
|
||
| 9 | 국진(국화·술잔) | 열끗 | 띠 |
|
||
| 10 | 단풍(사슴) | 열끗 | 띠 |
|
||
|
||
- 규칙상 의미가 있는 특수 패는 1·3·8월 광, 4·7·9월 열끗뿐이다. 나머지 열끗·띠·일반 패는 숫자(월)로만 쓰인다.
|
||
- 가상 칩, 선 표시, 판돈 영역.
|
||
- 카드 그림은 직접 그린 단순 SVG(9장, 12장 참고).
|
||
|
||
## 4. 준비(셋업)
|
||
|
||
1. 참가자를 좌석에 앉히고 좌석 순서를 `rng`로 섞는다.
|
||
2. 모든 플레이어 칩 = `startingChips`.
|
||
3. 첫 판의 선을 `rng`로 고른다.
|
||
4. 첫 판을 시작한다.
|
||
|
||
## 5. 진행 규칙
|
||
|
||
### 5.1 한 판의 흐름 (2장 섯다, `dealMode=twoRounds`)
|
||
|
||
1. 학교: 칩이 있는 참가자 전원이 `baseUnit`을 판돈에 낸다(칩이 모자라면 전부 내고 올인).
|
||
2. 기리(패 떼기): 실제 섞기는 서버 `rng`가 한다. 화면에서는 선의 오른쪽 사람이 패를 떼는 짧은 연출만 보여 준다(규칙 영향 없음).
|
||
3. 선부터 시계 방향으로 1장씩(비공개) 나눠 준다.
|
||
4. 1차 베팅(선부터 시계 방향, 5.4).
|
||
5. 남은 사람에게 1장씩 더 나눠 준다(비공개).
|
||
6. 2차 베팅(선이 다이했으면 선 다음 좌석의 남은 사람부터).
|
||
7. 2명 이상 남으면 쇼다운: 남은 사람의 패를 동시에 공개하고 6장 규칙으로 판정.
|
||
8. 도중에 1명만 남으면 그 사람이 판돈을 가져가고 패는 공개하지 않는다.
|
||
9. 판 종료 결과를 4초간 보여 준 뒤 다음 판(리바이·탈락·종료 확인 포함).
|
||
|
||
`dealMode=oneRound`: 3~6 대신 2장을 한 번에 나눠 주고 베팅 1회 후 쇼다운.
|
||
|
||
### 5.2 3장 섯다 (`cardMode=three`)
|
||
|
||
1. 학교 → 2장씩 나눠 줌(비공개) → 1차 베팅.
|
||
2. 남은 사람에게 1장 더(비공개, 손에 3장).
|
||
3. 선택 단계(동시 진행): 각자 3장 중 2장을 고른다. 고르지 않은 1장은 뒷면으로 버리며 끝까지 아무에게도 공개하지 않는다.
|
||
4. 2차 베팅 → 쇼다운(고른 2장으로 판정).
|
||
- 선택은 2차 베팅 전에 확정되므로 마지막 베팅은 확정된 패로 한다.
|
||
|
||
### 5.3 선(先)
|
||
|
||
- 선 = 그 판의 카드를 받는 첫 사람이자 각 베팅 라운드의 첫 행동자.
|
||
- 다음 판의 선: `winner`면 직전 판에서 판돈(메인 팟)을 가져간 사람. 나눠 가졌으면 그중 직전 선에서 시계 방향으로 가장 가까운 사람. `rotate`면 직전 선 다음 좌석의 참가자.
|
||
- 재경기의 선: 직전 선이 재경기 참가자면 그대로, 아니면 직전 선 다음 좌석부터 시계 방향으로 첫 재경기 참가자.
|
||
|
||
### 5.4 베팅 (한국식 판돈 베팅, 포커 문서 5.2와 동일한 공용 모듈)
|
||
|
||
용어: `pot` = 현재 판돈 전체(학교 포함), `currentBet` = 이번 라운드 최고 베팅액, `toCall` = `currentBet - 내 이번 라운드 베팅액`, `opened` = 이번 라운드에 누군가 칩을 걸었는가.
|
||
|
||
| 행동 | 조건 | 낼 금액 |
|
||
|---|---|---|
|
||
| 체크 | `opened=false` | 0 (돈을 걸지 않고 넘김) |
|
||
| 삥 | `opened=false` | `baseUnit` (기본 단위만큼) |
|
||
| 콜 | `toCall > 0` | `toCall` (앞사람만큼 맞춤) |
|
||
| 따당 | `toCall > 0`, 레이즈 가능 | `2 × toCall` (앞사람이 건 금액의 2배) |
|
||
| 쿼터 | 레이즈 가능 | `toCall + floor((pot + toCall) / 4)` |
|
||
| 하프 | 레이즈 가능 | `toCall + floor((pot + toCall) / 2)` |
|
||
| 풀 | 레이즈 가능, `maxRaise=full` | `toCall + (pot + toCall)` |
|
||
| 다이 | 자기 차례 언제나 | 0, 이번 판 포기(낸 돈은 판돈에 남음) |
|
||
|
||
- 레이즈 가능 조건: (1) 이번 라운드에서 아직 체크·콜·삥·레이즈를 하지 않았다(한 라운드에 1인 1회, 체크·콜 후 레이즈 금지), (2) 낼 금액이 `toCall`보다 크다. `toCall`을 뺀 추가분이 `baseUnit`보다 작으면 `baseUnit`으로 올린다.
|
||
- 첫 행동자가 체크하면 다음 사람도 체크/삥/쿼터/하프(/풀)/다이 중 고를 수 있다. 전원 체크하면 다음 단계로 넘어간다.
|
||
- 라운드 종료: 다이·올인하지 않은 모든 사람이 이번 라운드에 한 번 이상 행동했고 모두 `currentBet`을 맞췄을 때.
|
||
- 올인: 낼 금액이 스택 이상이면 전부 내고 올인. `toCall`보다 적게 올인하면 사이드 팟이 생긴다. 올인한 사람은 더 행동하지 않고 남은 카드는 받는다.
|
||
- 사이드 팟 계산은 포커 문서 5.5와 같다: 올인 금액 레벨별로 팟을 나누고, 각 팟의 자격자는 그 레벨까지 낸 다이하지 않은 사람. 아무도 맞추지 못한 초과분은 돌려준다.
|
||
|
||
예(3명, 학교 10, 기본 단위 10): 학교 후 판돈 30. 선 A 삥 10(판돈 40) → B 하프: 콜 10 + (50의 절반 25) = 35(판돈 75) → C 따당: `toCall=35` → 70(판돈 145) → A는 이미 삥을 했으므로 콜 60 또는 다이 → A 콜(판돈 205) → B 콜 35(판돈 240). 세 사람 모두 70을 맞춰 종료. 검산: 30 + 70 × 3 = 240.
|
||
|
||
### 5.5 재경기(구사·동점)
|
||
|
||
재경기가 결정되면(6.3) 그 판의 판돈은 그대로 두고 새 판을 진행한다.
|
||
1. 참가자:
|
||
- 구사 재경기: 쇼다운에 남아 있던 사람 전원(구사를 든 사람 포함). `rejoinCost`가 `none`이 아니면 그 판에서 다이한 사람(탈락자 제외)에게 재참가 여부를 묻는다(동시 진행, 15초, 시간 초과는 불참). 재참가하면 `half`=현재 판돈의 절반(내림), `full`=현재 판돈만큼을 판돈에 낸다. 칩이 모자라면 재참가할 수 없다.
|
||
- 동점 재경기(`tieRule=rematch`): 동점으로 최고패를 가진 사람만. 진 사람은 빠진다.
|
||
2. 덱 20장을 새로 섞고, 학교는 다시 내지 않는다. 카드 분배·베팅·쇼다운은 5.1/5.2와 같다(새 베팅은 기존 판돈 위에 더해진다).
|
||
3. 이전 판에서 올인한 사람은 계속 올인 상태로 카드만 받는다.
|
||
4. 사이드 팟이 있었던 경우: 각 팟을 그 팟의 자격자끼리 따로 판정한다. 재경기가 필요 없는 팟(자격자끼리 승자가 하나로 정해진 팟)은 재경기 전에 바로 지급하고, 재경기가 필요한 팟만 남긴다. 재경기 참가자는 남은 팟들의 재경기 참가자 합집합이며, 재경기 후 각 남은 팟을 다시 그 팟 자격자(재참가자 포함)끼리 판정한다.
|
||
5. 같은 판에서 재경기가 연속 3번 일어나면 4번째 재경기 대신 그 팟을 재경기 참가자끼리 균등 분할한다(무한 반복 방지).
|
||
6. 재경기는 `handLimit`의 판 수에 포함하지 않는다(원래 판의 연장).
|
||
|
||
### 5.6 칩이 떨어졌을 때
|
||
|
||
- 판(재경기 포함)이 완전히 끝난 뒤 칩 0인 사람은 `busted`.
|
||
- `rebuy=off`: 탈락(관전 화면, 결과 순위에는 남음).
|
||
- `rebuy=limited/unlimited`: "칩 다시 받기 / 그만하기"(15초, 시간 초과는 그만하기). 받으면 `startingChips`로 채움. `hands` 방식에서 마지막 판 직후에는 묻지 않는다.
|
||
- 칩이 학교 금액보다 적으면 가진 만큼 내고 올인 상태로 참가한다.
|
||
|
||
### 5.7 학교 상승과 자리 비움
|
||
|
||
- `schoolUpEvery = N > 0`이면 N판마다 `baseUnit` 2배(상한 `startingChips / 5`).
|
||
- 연속 2번 시간 초과하면 `away` 표시, 이후 그 사람의 제한 시간 5초. 직접 행동하면 해제.
|
||
- 방을 나간 사람(`left`)은 매 판 학교를 낸 뒤 자동 다이. 칩이 0이 되면 탈락.
|
||
|
||
## 6. 승패와 점수 계산
|
||
|
||
### 6.1 족보 (높은 순)
|
||
|
||
끗 계산: 두 장의 월을 더한 값의 일의 자리. 예: 7+8=15 → 5끗, 9+10=19 → 9끗(갑오), 2+8=10 → 0끗(망통).
|
||
|
||
| 순위 | 이름 | 구성 | 비교값(구현용) |
|
||
|---|---|---|---|
|
||
| 1 | 38광땡 | 3월 광 + 8월 광 | 1000 |
|
||
| 2 | 18광땡 | 1월 광 + 8월 광 (`gwangTtaeng=all`) | 990 |
|
||
| 3 | 13광땡 | 1월 광 + 3월 광 (`gwangTtaeng=all`) | 980 |
|
||
| 4 | 장땡(10땡) | 10월 + 10월 | 910 |
|
||
| 5 | 9땡 ~ 2땡 | 같은 월 두 장 | 909 ~ 902 |
|
||
| 6 | 삥땡(1땡) | 1월 + 1월 | 901 |
|
||
| 7 | 알리 | 1월 + 2월 | 806 |
|
||
| 8 | 독사 | 1월 + 4월 | 805 |
|
||
| 9 | 구삥 | 1월 + 9월 | 804 |
|
||
| 10 | 장삥 | 1월 + 10월 | 803 |
|
||
| 11 | 장사 | 4월 + 10월 | 802 |
|
||
| 12 | 세륙 | 4월 + 6월 | 801 |
|
||
| 13 | 갑오(9끗) | 끗 9 | 9 |
|
||
| 14 | 8끗 ~ 1끗 | 끗 8 ~ 1 | 8 ~ 1 |
|
||
| 15 | 망통(0끗) | 끗 0 | 0 |
|
||
|
||
- 위 표에서 먼저 맞는 줄을 쓴다(예: 1월 광 + 3월 광은 `all`이면 13광땡, `only38`이면 일반 조합으로 1+3=4끗).
|
||
- 광 조합이 아니면 광땡이 아니다: 1월 띠 + 8월 광 = 9끗(갑오), 3월 띠 + 8월 광 = 1끗.
|
||
- 같은 값 = 동점(무늬 같은 추가 비교 없음). 땡과 광땡은 같은 패가 두 사람에게 갈 수 없으므로 동점이 생기지 않고, 알리~세륙과 끗은 동점이 생길 수 있다(예: 1월 광+2월 띠 vs 1월 띠+2월 열끗 → 둘 다 알리).
|
||
|
||
### 6.2 특수패 (옵션)
|
||
|
||
특수패는 위 표의 "일반값"을 가지면서, 특정 상대를 만났을 때만 특별한 효과를 낸다.
|
||
|
||
| 특수패 | 구성 | 효과 | 일반값(효과가 없을 때) |
|
||
|---|---|---|---|
|
||
| 땡잡이 | `strict`: 3월 광 + 7월 열끗 / `loose`: 3월 + 7월 아무 패 | 쇼다운 최고 일반패가 1땡~9땡이면 승리(장땡·광땡에는 무효) | 망통(0) |
|
||
| 암행어사 | 4월 열끗 + 7월 열끗 | 쇼다운 최고 일반패가 13광땡 또는 18광땡이면 승리(38광땡에는 무효) | 1끗 |
|
||
| 구사 | 4월 + 9월(멍텅구리구사 조합 제외) | 다른 사람의 최고 일반패가 알리 이하(값 ≤ 806)이고 구사 쪽이 이기지 못하면 재경기 | 3끗 |
|
||
| 멍텅구리구사 | 4월 열끗 + 9월 열끗 | 다른 사람의 최고 일반패가 `upTo8`=8땡 이하(≤ 908) / `upTo9`=9땡 이하(≤ 909) / `upToJang`=장땡 이하(≤ 910)이고 멍구사 쪽이 이기지 못하면 재경기. 광땡은 언제나 이김 | 3끗 |
|
||
|
||
- 땡잡이(`strict`)와 암행어사는 7월 열끗을 함께 쓰므로 한 판에 동시에 나올 수 없다. `loose` 땡잡이는 한 판에 2명까지 나올 수 있다.
|
||
- 옵션이 꺼진 특수패는 일반값만 갖는다.
|
||
|
||
### 6.3 쇼다운 판정 알고리즘
|
||
|
||
입력: 쇼다운 참가자 집합 S(2명 이상), 각자의 2장.
|
||
1. 각자의 일반값 v(p)를 6.1로 구한다(특수패도 일반값).
|
||
2. `Vmax = max v(p)`.
|
||
3. 잠정 승자 W:
|
||
- 땡잡이가 켜져 있고 S에 땡잡이가 있고 `901 ≤ Vmax ≤ 909`이면 W = 땡잡이를 든 사람(들).
|
||
- 아니면, 암행어사가 켜져 있고 S에 암행어사가 있고 `Vmax ∈ {980, 990}`이면 W = 암행어사를 든 사람.
|
||
- 아니면 W = `v(p) = Vmax`인 사람들.
|
||
4. 구사 재경기 검사(`gusa` 켬): 구사 또는 멍텅구리구사를 든 사람 h 중 W에 속하지 않은 사람이 있고, `max{ v(q) : q ∈ S, q ≠ h }`가 h의 기준값 이하이면 → 구사 재경기(5.5). 기준값: 구사 806, 멍텅구리구사는 `mungGusa`에 따라 908/909/910(`off`면 일반 구사로 보아 806).
|
||
5. 재경기가 아니고 W가 1명이면 그 사람이 승리.
|
||
6. W가 2명 이상이면 `tieRule`: `rematch`면 W끼리 재경기, `split`이면 균등 분할(나머지 칩은 선에서 시계 방향으로 가장 가까운 승자부터 1칩씩).
|
||
|
||
이 판정은 팟마다 그 팟의 자격자(S ∩ 자격자)로 따로 수행한다(5.5의 4).
|
||
|
||
설계 결정(사용자 확인 필요): 원 규칙은 "구사는 상대 최고패가 알리 이하면 재경기"이지만, 구사(3끗)가 그대로 이기는 경우(상대가 모두 2끗 이하)까지 재경기하면 구사를 든 쪽이 손해다. 그래서 구사 쪽이 이미 이기는 경우는 재경기 없이 승리로 처리한다(4단계의 "W에 속하지 않은").
|
||
|
||
### 6.4 판정 예시
|
||
|
||
1. A 38광땡, B 암행어사 → A 승(암행어사는 38광땡에 무효, B는 1끗).
|
||
2. A 13광땡, B 암행어사, C 장땡 → 일반값 A=980, B=1, C=910, 최고 일반값 980(13광땡) → 암행어사 발동 → B 승.
|
||
3. A 9땡, B 땡잡이(3광+7열끗), C 알리 → 최고 일반값 909(9땡) → 땡잡이 발동 → B 승.
|
||
4. A 장땡, B 땡잡이 → 최고 일반값 910 → 무효 → A 승.
|
||
5. A 땡잡이, B 알리 → 최고 일반값 806 → 무효, A는 망통 → B 승.
|
||
6. A 구사(4월 띠 + 9월 열끗), B 알리 → W={B}, A는 W 밖, 상대 최고 806 ≤ 806 → 구사 재경기.
|
||
7. A 구사, B 1땡 → 901 > 806 → B 승.
|
||
8. A 구사, B 2끗, C 망통 → W={A}(3끗 최고) → 재경기 없이 A 승.
|
||
9. A 멍텅구리구사(4열끗+9열끗), B 8땡 → `upTo8`: 908 ≤ 908 → 재경기. B가 9땡이면 B 승. `upToJang`이면 장땡까지 재경기.
|
||
10. A 5끗(2월+3월), B 5끗(6월+9월) → 동점 → `rematch`면 A·B 재경기, `split`이면 반씩.
|
||
11. A 구사, B 5땡, C 땡잡이 → 일반값 A=3, B=905, C=0, 최고 905 → W={C}(땡잡이). 구사 검사: A 기준 806, 상대 최고 905 > 806 → 재경기 아님 → C 승. A가 멍텅구리구사(`upTo8`)였다면 905 ≤ 908 → 재경기.
|
||
12. 3장 섯다: 손패 1월 광, 8월 광, 4월 열끗 → 조합 (1광+8광)=18광땡, (1광+4열끗)=독사, (8광+4열끗)=2끗 → 18광땡 선택이 최선.
|
||
|
||
### 6.5 판돈 지급
|
||
|
||
- 승자(들)가 해당 팟을 가져간다. 다이한 사람·패배자의 낸 칩은 돌려주지 않는다.
|
||
- 사이드 팟은 자격자가 적은 팟부터 판정해 지급하고 마지막에 메인 팟.
|
||
|
||
### 6.6 게임 종료와 점수
|
||
|
||
- `endCondition=hands`: `handLimit`판(재경기 제외)이 끝나면 종료. 점수 = `최종 칩 - startingChips × 리바이 횟수`. 점수 높은 순 순위, 같으면 공동 순위.
|
||
- `endCondition=lastStanding`: 칩을 가진 사람이 1명 남으면 종료(리바이 결정 대기 중이면 대기). 순위 = 늦게 탈락한 순, 같은 판 동시 탈락은 그 판 시작 칩이 많은 쪽이 위. 300판 도달 시 `hands` 방식 점수로 강제 종료.
|
||
- 접속 중이고 칩이 있는 사람이 1명 이하가 되면 즉시 종료.
|
||
|
||
## 7. 지역 룰 / 하우스 룰 (옵션으로 켜고 끄기)
|
||
|
||
| 하우스 룰 | 옵션 | 기본 |
|
||
|---|---|---|
|
||
| 1장씩 2번 받기 / 2장 한 번에 | `dealMode` | 1장씩 2번 |
|
||
| 3장 받아 2장 고르기 | `cardMode=three` | 2장 |
|
||
| 광땡 종류(38만 / 38·18·13) | `gwangTtaeng` | 모두 |
|
||
| 땡잡이 엄격(3광+7열끗) / 느슨(3·7 아무 패) / 없음 | `ttaengjabi` | 엄격 |
|
||
| 암행어사 | `amhaeng` | 켬 |
|
||
| 구사 재경기("49파토") | `gusa` | 켬 |
|
||
| 멍텅구리구사 범위(8땡/9땡/장땡까지) | `mungGusa` | 8땡까지 |
|
||
| 동점 재경기 / 나눔 | `tieRule` | 재경기 |
|
||
| 구사 재경기 때 다이한 사람 재참가 비용 | `rejoinCost` | 재참가 불가 |
|
||
| 선 = 이긴 사람 / 회전 | `dealerRule` | 이긴 사람 |
|
||
| 풀 베팅 허용 | `maxRaise` | 하프까지 |
|
||
| 땡값(땡·광땡으로 이기면 다른 사람에게 추가로 받기), 구사 선택권(구사 쥔 사람이 재경기 여부 결정), 사구 파토 시 판돈 반환 | 미지원(향후) | - |
|
||
|
||
## 8. 엔진 설계
|
||
|
||
### 8.1 상태(State)
|
||
|
||
```ts
|
||
// 서버만 보는 전체 상태
|
||
type PlayerId = string;
|
||
type Month = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
|
||
interface HwatuCard {
|
||
id: number; // 0..19 = (month-1)*2 + (special ? 0 : 1)
|
||
month: Month;
|
||
kind: 'gwang' | 'yeol' | 'normal'; // 1·3·8월 특수=gwang, 그 외 월 특수=yeol, 나머지 normal
|
||
}
|
||
|
||
interface SeotdaOptions {
|
||
cardMode: 'two' | 'three';
|
||
dealMode: 'twoRounds' | 'oneRound';
|
||
startingChips: 500 | 1000 | 2000 | 5000;
|
||
baseUnit: 2 | 10 | 20 | 50;
|
||
maxRaise: 'half' | 'full';
|
||
endCondition: 'hands' | 'lastStanding';
|
||
handLimit: 10 | 20 | 30 | 50;
|
||
schoolUpEvery: 0 | 5 | 10 | 15;
|
||
rebuy: 'off' | 'limited' | 'unlimited';
|
||
rebuyLimit: 1 | 2 | 3;
|
||
turnSeconds: 15 | 20 | 30 | 60;
|
||
dealerRule: 'winner' | 'rotate';
|
||
gwangTtaeng: 'all' | 'only38';
|
||
ttaengjabi: 'off' | 'strict' | 'loose';
|
||
amhaeng: boolean;
|
||
gusa: boolean;
|
||
mungGusa: 'off' | 'upTo8' | 'upTo9' | 'upToJang';
|
||
tieRule: 'rematch' | 'split';
|
||
rejoinCost: 'none' | 'half' | 'full';
|
||
hints: boolean;
|
||
}
|
||
|
||
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 RoundPlayer {
|
||
id: PlayerId;
|
||
hand: HwatuCard[]; // 받은 카드(2 또는 3장)
|
||
chosen: [number, number] | null; // 3장 섯다 선택한 두 카드 id
|
||
discarded: HwatuCard | null; // 3장 섯다 버린 카드(영구 비공개)
|
||
folded: boolean;
|
||
allIn: boolean;
|
||
committedTotal: number; // 이번 판(재경기 포함) 총 기여
|
||
committedRound: number;
|
||
actedThisRound: boolean;
|
||
raisedThisRound: boolean;
|
||
lockedFromRaise: boolean; // 체크·콜 후 레이즈 금지
|
||
ready: boolean;
|
||
}
|
||
|
||
interface Pot { amount: number; eligible: PlayerId[] }
|
||
|
||
interface BettingRound {
|
||
street: number; // 0 = 1차, 1 = 2차
|
||
order: PlayerId[]; // 선(또는 선 다음 남은 사람)부터
|
||
toAct: PlayerId | null;
|
||
currentBet: number;
|
||
opened: boolean;
|
||
}
|
||
|
||
interface ShowdownInfo {
|
||
hands: { id: PlayerId; cards: HwatuCard[]; name: string; value: number; special: SpecialKind | null }[];
|
||
outcome:
|
||
| { type: 'win'; potIndex: number; winners: PlayerId[]; reason: 'normal' | 'ttaengjabi' | 'amhaeng' }
|
||
| { type: 'split'; potIndex: number; winners: PlayerId[] }
|
||
| { type: 'rematch'; potIndex: number; reason: 'gusa' | 'mungGusa' | 'tie'; participants: PlayerId[] };
|
||
}
|
||
type SpecialKind = 'ttaengjabi' | 'amhaeng' | 'gusa' | 'mungGusa';
|
||
|
||
interface SeotdaHand {
|
||
handNo: number; // 재경기는 같은 handNo
|
||
rematchNo: number; // 0 = 본 판, 1..3 = 재경기 차수
|
||
phase: 'dealing' | 'betting' | 'choose' | 'rejoin' | 'showdown' | 'handEnd';
|
||
dealerSeat: number; // 선
|
||
deck: HwatuCard[]; // 남은 덱(비공개)
|
||
players: Record<PlayerId, RoundPlayer>; // 이번 (재)경기 참가자
|
||
participants: PlayerId[]; // 이번 (재)경기 참가 순서(선부터)
|
||
betting: BettingRound | null;
|
||
carriedPots: Pot[]; // 재경기로 넘어온 팟(자격자 고정)
|
||
pots: Pot[]; // 이번 (재)경기 베팅으로 생긴 팟
|
||
rejoinPending: PlayerId[];
|
||
showdown: ShowdownInfo[] | null;
|
||
deadlineAt: number | null;
|
||
}
|
||
|
||
interface SeotdaState {
|
||
options: SeotdaOptions;
|
||
rng: RngState;
|
||
seats: SeatState[];
|
||
baseUnit: number; // 학교 상승 반영
|
||
hand: SeotdaHand | null;
|
||
handsPlayed: number;
|
||
lastWinner: PlayerId | null;
|
||
phase: 'playing' | 'rebuy' | 'finished';
|
||
rebuyPending: PlayerId[];
|
||
rebuyDeadlineAt: number | null;
|
||
eliminationSeq: PlayerId[][];
|
||
publicLog: PublicLogEntry[]; // 비밀 정보 없음
|
||
}
|
||
```
|
||
|
||
엔진 메모: `minPlayers=2`, `maxPlayers=10`(정적). `cardMode=three`의 6명 상한은 `optionsSchema.superRefine`과 `playerRange(options)` 헬퍼로 확인한다. 족보 계산 `evalSeotda(a, b, options) → { value, name, special }`과 판정 `resolve(S, options)`는 순수 함수로 분리해 표 기반 단위 테스트(20장에서 나올 수 있는 190개 조합 전부)를 한다.
|
||
|
||
### 8.2 액션
|
||
|
||
| type | payload | 누가 / 언제 | 검증 조건 |
|
||
|---|---|---|---|
|
||
| `check` | - | `phase=betting`, 내 차례 | `opened=false` |
|
||
| `ping` | - | 내 차례 | `opened=false`, `raisedThisRound=false`, 스택 > 0 |
|
||
| `call` | - | 내 차례 | `toCall > 0` |
|
||
| `ddadang` | - | 내 차례 | `toCall > 0`, 레이즈 가능 |
|
||
| `quarter` / `half` | - | 내 차례 | 레이즈 가능, 낼 금액 > `toCall` |
|
||
| `full` | - | 내 차례 | 위 + `maxRaise=full` |
|
||
| `die` | - | 내 차례 | 항상 |
|
||
| `choose` | `{ cardIds: [number, number] }` | `phase=choose`(3장 섯다), 남은 참가자(동시, 올인 포함) | 두 id가 내 손패에 있고 서로 다름, 아직 미선택 |
|
||
| `rejoin` | `{ accept: boolean }` | `phase=rejoin`, `rejoinPending`에 있는 사람 | `accept=true`면 스택 ≥ 재참가 비용 |
|
||
| `ready` | - | `phase=handEnd` | 미준비 |
|
||
| `rebuy` | `{ accept: boolean }` | `phase=rebuy`, 대상자 | 남은 리바이 횟수 > 0 |
|
||
| `leave` | - | 서버가 퇴장 시 주입 | - |
|
||
|
||
거부 사유 예: "지금은 내 차례가 아니에요", "누가 이미 걸어서 체크할 수 없어요", "이번 라운드에는 더 올릴 수 없어요(콜 또는 다이)", "칩이 모자라 재참가할 수 없어요", "카드 두 장을 골라 주세요".
|
||
|
||
### 8.3 공개/비공개 정보 (view)
|
||
|
||
| 정보 | 본인 | 다른 플레이어 | 관전자(`null`) |
|
||
|---|---|---|---|
|
||
| 내 손패 | 보임 | 장수만 | 장수만 |
|
||
| 3장 섯다 선택 결과 | 보임 | 선택 완료 여부만 | 같음 |
|
||
| 3장 섯다 버린 카드 | 내 것만 | 영구 비공개 | 영구 비공개 |
|
||
| 덱 | 안 보임 | 안 보임 | 안 보임 |
|
||
| 쇼다운 패 | 쇼다운 참가자의 2장(선택한 2장) 공개 | 같음 | 같음 |
|
||
| 다이한 사람 패 | 내 것만 | 영구 비공개 | 영구 비공개 |
|
||
| 다른 사람의 족보 이름 | - | 쇼다운 후에만 | 쇼다운 후에만 |
|
||
|
||
- 공개: 좌석·스택·상태, 선 표시, 판돈과 사이드 팟, 이번 라운드 베팅액과 마지막 행동, 현재 행동자와 마감 시각, 재경기 여부와 사유, 재경기 차수.
|
||
- 비공개 카드 자리는 `{ hidden: true }`만 보낸다. 카드 `id`는 월·종류와 고정 대응하므로 비공개 카드의 `id`도 보내지 않는다.
|
||
- `view`는 본인 차례일 때 `legalActions`(가능 행동과 정확한 낼 금액)와, `hints` 켬일 때 본인 족보 이름(예: "알리", "5끗", "구사 - 상대가 알리 이하면 재경기")을 내려 준다.
|
||
- 이벤트에도 비공개 카드 값을 넣지 않는다(`{ type: 'dealt', to, count }`). 쇼다운 공개 이벤트에만 카드 값을 넣는다.
|
||
|
||
### 8.4 랜덤 요소 (시드 RNG 사용 지점)
|
||
|
||
1. `setup`: 좌석 섞기, 첫 선.
|
||
2. 매 판·매 재경기 시작: 20장 Fisher-Yates 섞기. 분배는 덱 위에서 순서대로.
|
||
- 판정, 분할 나머지 칩, 자동 선택은 결정적(난수 없음). 같은 시드 + 같은 액션 열 → 같은 결과.
|
||
|
||
### 8.5 타이머·시간 초과·연결 끊김 시 자동 행동
|
||
|
||
`deadline`: 베팅 `turnSeconds`(자리 비움 5초), 3장 선택 `turnSeconds + 10`초(동시), 재참가 15초, `handEnd` 4초, 리바이 15초.
|
||
|
||
| 단계 | `onTimeout` 자동 행동 |
|
||
|---|---|
|
||
| 베팅 | `opened=false`면 `check`, 아니면 `die` |
|
||
| 3장 선택 | 일반값이 가장 높은 2장 조합. 같으면 특수패(땡잡이·암행어사·구사) 조합, 그것도 같으면 카드 id 합이 작은 조합 |
|
||
| 재참가 | `{ accept: false }` |
|
||
| `handEnd` | `ready` |
|
||
| 리바이 | `{ accept: false }` |
|
||
|
||
연결이 끊긴 사람의 차례는 일반 마감까지 기다린 뒤 자동 행동한다. 재접속하면 즉시 직접 행동할 수 있다.
|
||
|
||
### 8.6 종료 조건과 결과(GameResult)
|
||
|
||
종료 판정은 6.6.
|
||
|
||
```ts
|
||
interface SeotdaResult /* extends GameResult */ {
|
||
ranking: {
|
||
playerId: PlayerId;
|
||
rank: number; // 공동 순위 허용
|
||
score: number; // hands: 최종 칩 - startingChips × rebuys / lastStanding: 생존 순서(1위=N)
|
||
finalChips: number;
|
||
rebuys: number;
|
||
eliminatedAtHand: number | null;
|
||
}[];
|
||
summary: string; // 예: "20판 종료. 1위 영희(+860칩), 최고 족보 38광땡"
|
||
stats: {
|
||
handsPlayed: number;
|
||
rematches: number;
|
||
bestHand: { playerId: PlayerId; name: string; handNo: number } | null;
|
||
biggestPot: { handNo: number; amount: number; winners: PlayerId[] };
|
||
};
|
||
}
|
||
```
|
||
|
||
## 9. UI/UX
|
||
|
||
화면 배치(모바일 세로):
|
||
- 위: 상대 플레이어 원형 배치(닉네임, 스택, 이번 라운드 베팅, 카드 뒷면 장수, "다이"/"올인"/"자리 비움" 뱃지, 선 표시).
|
||
- 가운데: 판돈 큰 숫자, 재경기 중이면 "재경기 2회차 - 구사" 띠 배너.
|
||
- 아래: 내 카드 2~3장을 크게(카드 높이 최소 120px), 그 위에 내 족보 이름 큰 글씨(`hints`), 맨 아래 행동 버튼.
|
||
- PC: 오른쪽에 족보표 상시 표시, 판 기록.
|
||
|
||
조작:
|
||
- 버튼(높이 56px 이상): `다이` `체크` `삥` `콜 (+60)` `따당 (+140)` `쿼터` `하프 (+75)` `풀`. 가능한 버튼만 색 강조, 불가능한 버튼은 흐리게(위치 고정으로 오조작 방지). 금액은 서버 `legalActions` 값.
|
||
- 3장 섯다 선택: 카드 3장을 누르면 2장이 위로 올라오고, 고른 조합의 족보 이름을 미리 보여 준다. "추천 조합" 버튼(`hints`).
|
||
- 쇼다운: 남은 사람 패가 동시에 뒤집히고 족보 이름 표시, 특수패 발동 시 "땡잡이!" "암행어사 출두!" 큰 글자 연출(1초), 재경기 시 "구사 재경기" 연출.
|
||
- 족보표 버튼: 6.1 표를 그림 카드와 함께 표시, 내 현재 족보 줄 강조, 특수패는 켜진 옵션만 표시.
|
||
|
||
카드 그림(자체 제작 SVG):
|
||
- 화투 고유 이미지(시판 화투 인쇄 디자인, 특정 게임사 화투 그림)를 복제하지 않는다. 직접 그린 단순 SVG를 쓴다.
|
||
- 구성: 세로 2:3 카드, 왼쪽 위에 큰 월 숫자(1~10, 고대비), 월별 배경색과 단순화한 식물 아이콘(소나무, 매화, 벚꽃, 등나무, 창포, 모란, 싸리, 억새와 달, 국화, 단풍), 특수 패 표시:
|
||
- 광: 빨간 원 안에 "광" 글자.
|
||
- 열끗: 동물 실루엣(꾀꼬리, 두견새, 다리, 나비, 멧돼지, 술잔, 사슴) + 작은 "열" 뱃지. 4·7·9월 열끗은 특수패 재료이므로 뱃지를 더 눈에 띄게.
|
||
- 일반 패: 띠(가로 리본) 또는 잎 무늬.
|
||
- 색만으로 구분하지 않고 글자(월 숫자, "광", "열")를 반드시 함께 넣는다. 큰 글씨 모드에서는 월 숫자만 크게 보이는 단순 카드로 전환.
|
||
|
||
초보자 도움:
|
||
- "규칙 보기": 진행 순서, 끗 계산법, 족보표, 특수패 설명, 베팅 용어(학교·삥·따당·하프·다이).
|
||
- 첫 판 말풍선 튜토리얼. 내 차례 진동/소리, 남은 시간 게이지.
|
||
- 힌트: "지금 패는 5끗이에요. 중간 정도 패예요." 같은 쉬운 문장(강도 3단계: 높음/중간/낮음 색).
|
||
|
||
## 10. 테스트 체크리스트
|
||
|
||
- [ ] 족보 전수: 20장에서 나오는 190개 2장 조합의 이름·값이 6.1/6.2 표와 일치(옵션 조합 `gwangTtaeng`, `ttaengjabi`, `amhaeng`, `gusa`, `mungGusa`별 스냅샷).
|
||
- [ ] 1월 광 + 3월 광: `all`이면 13광땡, `only38`이면 4끗. 1월 띠 + 8월 광은 갑오.
|
||
- [ ] 6.4 예시 1~12가 모두 기대 결과와 일치.
|
||
- [ ] 땡잡이 `strict`: 3월 띠 + 7월 열끗은 일반 망통이며 9땡을 잡지 못함. `loose`면 잡음.
|
||
- [ ] `loose` 땡잡이 2명 + 5땡 → 땡잡이 두 사람 동점 처리(`tieRule`).
|
||
- [ ] 구사 재경기: 판돈 유지, 학교 재징수 없음, 참가자는 쇼다운 생존자만(`rejoinCost=none`).
|
||
- [ ] 구사 재경기 `rejoinCost=half`: 다이한 사람이 판돈 절반(내림)을 내고 참가, 칩이 모자라면 거부, 시간 초과 시 불참.
|
||
- [ ] 동점 재경기 3연속 후 4번째에는 균등 분할, 나머지 칩은 선에서 가까운 승자.
|
||
- [ ] 사이드 팟 + 재경기: A 올인(메인만), 메인에서 A·B 동점, 사이드(B·C)에서 B 단독 승 → 사이드는 즉시 B에게, 메인만 A·B 재경기.
|
||
- [ ] 베팅: 5.4 예시(학교 30 → 삥 → 하프 35 → 따당 70 → 콜 → 콜) 최종 판돈 240.
|
||
- [ ] 체크·콜 후 같은 라운드 레이즈 거부, `maxRaise=half`에서 `full` 거부.
|
||
- [ ] 2차 베팅 첫 행동자: 선이 1차에서 다이했으면 선 다음 좌석의 남은 사람.
|
||
- [ ] `dealMode=oneRound`: 베팅 1회 후 바로 쇼다운.
|
||
- [ ] 3장 섯다: 선택 전 다른 사람 `view`에 선택 결과 없음, 버린 카드는 쇼다운 후에도 어떤 `view`·이벤트에도 없음.
|
||
- [ ] 3장 섯다 시간 초과 자동 선택: 1광·8광·4열끗 → 18광땡 조합 선택.
|
||
- [ ] 시간 초과: `opened=false`면 체크, 아니면 다이. 2연속 → `away`, 마감 5초.
|
||
- [ ] 연결 끊김 상태로 선 차례 → 마감 후 자동 체크/다이, 재접속 후 정상 행동.
|
||
- [ ] 비밀 누출: 매 단계 모든 시청자(플레이어·관전자) `view`와 이벤트 JSON에 남의 비공개 카드의 월·종류·id가 없음(무작위 시드 1,000판 속성 테스트).
|
||
- [ ] 다이로 끝난 판: 승자 패도 공개되지 않음.
|
||
- [ ] 리바이·탈락·점수: `hands` 점수 = 칩 - 시작 칩 × 리바이, `lastStanding` 동시 탈락 순위.
|
||
- [ ] 칩 보존 불변식: `Σ스택 + Σ팟 + Σ이번 라운드 기여 = 초기 칩 합 + 리바이 합` (재경기 중에도).
|
||
- [ ] 재현성: 같은 시드 + 같은 액션 열 → 같은 상태 해시.
|
||
|
||
## 11. 참고 자료
|
||
|
||
- 나무위키 "섯다": https://namu.wiki/w/섯다 (20장 구성, 족보 순서, 땡잡이 = 3월 광 + 7월 열끗, 암행어사 = 4월 열끗 + 7월 열끗로 13·18광땡만 잡음, 구사는 상대 최고패가 알리 이하일 때 판돈을 둔 채 재경기, 멍텅구리구사는 8땡까지 재경기·광땡은 불가·지역에 따라 장땡 차이, 다이한 사람이 구사 재경기에 참가하려면 판돈만큼 지불, 같은 족보는 판돈을 둔 채 재대결, 3장 섯다, 베팅 용어 정의)
|
||
- 한게임 포커 7포커 베팅 방법(공용 한국식 베팅 용어 정의): https://poker.hangame.com/gameguide/poker7/game_7poker2_1.html
|
||
- 포커 문서 `docs/games/poker.md` 5.2(한국식 베팅), 5.5(사이드 팟), 5.6(리바이).
|
||
|
||
출처 간 차이와 이 문서의 선택:
|
||
- 멍텅구리구사 범위: "8땡까지"가 기본 서술이고 지역에 따라 장땡까지 무승부로 보기도 한다 → `mungGusa` 옵션, 기본 8땡.
|
||
- 재경기 재참가 비용: 나무위키는 "판돈만큼"이라 하지만 지역마다 다르다 → 기본은 재참가 불가, 옵션으로 절반/전액.
|
||
- 구사가 이미 이기는 경우의 재경기 여부: 6.3 설계 결정 참고.
|
||
|
||
## 12. 메모 (상표·법적 주의 등)
|
||
|
||
- 가상 칩만 사용한다. 방 전용이며 돈·유료 재화·광고 보상과 연결하지 않는다. 칩 거래·선물·누적 랭킹을 만들지 않는다. 결과 화면에 원(₩) 같은 화폐 표현을 쓰지 않는다.
|
||
- 법적 검토 필요(사용자 확인):
|
||
- 형법 제246조(도박): 재물을 걸지 않는 구조라 해당하지 않도록 설계했지만, 외부 정산을 부추기는 기능은 넣지 않는다.
|
||
- 게임산업진흥법 등급분류: 공개 배포하는 게임물은 원칙적으로 등급분류 대상이고, 비영리 예외는 청소년이용불가 기준에 해당하는 내용을 포함하면 적용되지 않는다. 섯다·고스톱·포커 같은 웹보드 게임은 통상 청소년이용불가로 분류되므로 서비스 전 확인이 필요하다.
|
||
- 어린이 사용자가 있으므로 섯다는 기본 목록과 분리하거나(예: "어른용" 탭) 방장 확인 문구를 두는 방안을 검토한다.
|
||
- 화투 그림: 화투(하나후다) 전통 도안 자체는 오래된 디자인이지만, 시판 화투나 다른 게임의 그림은 각 제작사의 저작물일 수 있다. 이미지를 가져오지 말고 9장의 지침대로 직접 단순화한 SVG를 그린다.
|
||
- 명칭: "섯다"는 일반 놀이 이름이라 상표 문제가 적다. 특정 서비스 이름(예: "한게임 섯다", "피망 섯다")은 쓰지 않는다. 표시 이름 후보: "섯다"(기본), 어린이 노출을 줄이고 싶다면 "화투 족보 대결".
|