- 개요, 조사(BGA·한국 인기 보드게임·UI/UX), 아키텍처, 실시간 프로토콜, 안정성·보안, 계정, 로비·방, UI/UX, 데이터 모델, 게임 엔진, 테스트, 배포·운영, 로드맵(M0~M9), 사용자 결정 항목 - 게임별 규칙·엔진 설계·UI·테스트 체크리스트 26종 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
35 KiB
섯다 (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. 준비(셋업)
- 참가자를 좌석에 앉히고 좌석 순서를
rng로 섞는다. - 모든 플레이어 칩 =
startingChips. - 첫 판의 선을
rng로 고른다. - 첫 판을 시작한다.
5. 진행 규칙
5.1 한 판의 흐름 (2장 섯다, dealMode=twoRounds)
- 학교: 칩이 있는 참가자 전원이
baseUnit을 판돈에 낸다(칩이 모자라면 전부 내고 올인). - 기리(패 떼기): 실제 섞기는 서버
rng가 한다. 화면에서는 선의 오른쪽 사람이 패를 떼는 짧은 연출만 보여 준다(규칙 영향 없음). - 선부터 시계 방향으로 1장씩(비공개) 나눠 준다.
- 1차 베팅(선부터 시계 방향, 5.4).
- 남은 사람에게 1장씩 더 나눠 준다(비공개).
- 2차 베팅(선이 다이했으면 선 다음 좌석의 남은 사람부터).
- 2명 이상 남으면 쇼다운: 남은 사람의 패를 동시에 공개하고 6장 규칙으로 판정.
- 도중에 1명만 남으면 그 사람이 판돈을 가져가고 패는 공개하지 않는다.
- 판 종료 결과를 4초간 보여 준 뒤 다음 판(리바이·탈락·종료 확인 포함).
dealMode=oneRound: 3~6 대신 2장을 한 번에 나눠 주고 베팅 1회 후 쇼다운.
5.2 3장 섯다 (cardMode=three)
- 학교 → 2장씩 나눠 줌(비공개) → 1차 베팅.
- 남은 사람에게 1장 더(비공개, 손에 3장).
- 선택 단계(동시 진행): 각자 3장 중 2장을 고른다. 고르지 않은 1장은 뒷면으로 버리며 끝까지 아무에게도 공개하지 않는다.
- 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) 그 판의 판돈은 그대로 두고 새 판을 진행한다.
- 참가자:
- 구사 재경기: 쇼다운에 남아 있던 사람 전원(구사를 든 사람 포함).
rejoinCost가none이 아니면 그 판에서 다이한 사람(탈락자 제외)에게 재참가 여부를 묻는다(동시 진행, 15초, 시간 초과는 불참). 재참가하면half=현재 판돈의 절반(내림),full=현재 판돈만큼을 판돈에 낸다. 칩이 모자라면 재참가할 수 없다. - 동점 재경기(
tieRule=rematch): 동점으로 최고패를 가진 사람만. 진 사람은 빠진다.
- 구사 재경기: 쇼다운에 남아 있던 사람 전원(구사를 든 사람 포함).
- 덱 20장을 새로 섞고, 학교는 다시 내지 않는다. 카드 분배·베팅·쇼다운은 5.1/5.2와 같다(새 베팅은 기존 판돈 위에 더해진다).
- 이전 판에서 올인한 사람은 계속 올인 상태로 카드만 받는다.
- 사이드 팟이 있었던 경우: 각 팟을 그 팟의 자격자끼리 따로 판정한다. 재경기가 필요 없는 팟(자격자끼리 승자가 하나로 정해진 팟)은 재경기 전에 바로 지급하고, 재경기가 필요한 팟만 남긴다. 재경기 참가자는 남은 팟들의 재경기 참가자 합집합이며, 재경기 후 각 남은 팟을 다시 그 팟 자격자(재참가자 포함)끼리 판정한다.
- 같은 판에서 재경기가 연속 3번 일어나면 4번째 재경기 대신 그 팟을 재경기 참가자끼리 균등 분할한다(무한 반복 방지).
- 재경기는
handLimit의 판 수에 포함하지 않는다(원래 판의 연장).
5.6 칩이 떨어졌을 때
- 판(재경기 포함)이 완전히 끝난 뒤 칩 0인 사람은
busted. rebuy=off: 탈락(관전 화면, 결과 순위에는 남음).rebuy=limited/unlimited: "칩 다시 받기 / 그만하기"(15초, 시간 초과는 그만하기). 받으면startingChips로 채움.hands방식에서 마지막 판 직후에는 묻지 않는다.- 칩이 학교 금액보다 적으면 가진 만큼 내고 올인 상태로 참가한다.
5.7 학교 상승과 자리 비움
schoolUpEvery = N > 0이면 N판마다baseUnit2배(상한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장.
- 각자의 일반값 v(p)를 6.1로 구한다(특수패도 일반값).
Vmax = max v(p).- 잠정 승자 W:
- 땡잡이가 켜져 있고 S에 땡잡이가 있고
901 ≤ Vmax ≤ 909이면 W = 땡잡이를 든 사람(들). - 아니면, 암행어사가 켜져 있고 S에 암행어사가 있고
Vmax ∈ {980, 990}이면 W = 암행어사를 든 사람. - 아니면 W =
v(p) = Vmax인 사람들.
- 땡잡이가 켜져 있고 S에 땡잡이가 있고
- 구사 재경기 검사(
gusa켬): 구사 또는 멍텅구리구사를 든 사람 h 중 W에 속하지 않은 사람이 있고,max{ v(q) : q ∈ S, q ≠ h }가 h의 기준값 이하이면 → 구사 재경기(5.5). 기준값: 구사 806, 멍텅구리구사는mungGusa에 따라 908/909/910(off면 일반 구사로 보아 806). - 재경기가 아니고 W가 1명이면 그 사람이 승리.
- W가 2명 이상이면
tieRule:rematch면 W끼리 재경기,split이면 균등 분할(나머지 칩은 선에서 시계 방향으로 가장 가까운 승자부터 1칩씩).
이 판정은 팟마다 그 팟의 자격자(S ∩ 자격자)로 따로 수행한다(5.5의 4).
설계 결정(사용자 확인 필요): 원 규칙은 "구사는 상대 최고패가 알리 이하면 재경기"이지만, 구사(3끗)가 그대로 이기는 경우(상대가 모두 2끗 이하)까지 재경기하면 구사를 든 쪽이 손해다. 그래서 구사 쪽이 이미 이기는 경우는 재경기 없이 승리로 처리한다(4단계의 "W에 속하지 않은").
6.4 판정 예시
- A 38광땡, B 암행어사 → A 승(암행어사는 38광땡에 무효, B는 1끗).
- A 13광땡, B 암행어사, C 장땡 → 일반값 A=980, B=1, C=910, 최고 일반값 980(13광땡) → 암행어사 발동 → B 승.
- A 9땡, B 땡잡이(3광+7열끗), C 알리 → 최고 일반값 909(9땡) → 땡잡이 발동 → B 승.
- A 장땡, B 땡잡이 → 최고 일반값 910 → 무효 → A 승.
- A 땡잡이, B 알리 → 최고 일반값 806 → 무효, A는 망통 → B 승.
- A 구사(4월 띠 + 9월 열끗), B 알리 → W={B}, A는 W 밖, 상대 최고 806 ≤ 806 → 구사 재경기.
- A 구사, B 1땡 → 901 > 806 → B 승.
- A 구사, B 2끗, C 망통 → W={A}(3끗 최고) → 재경기 없이 A 승.
- A 멍텅구리구사(4열끗+9열끗), B 8땡 →
upTo8: 908 ≤ 908 → 재경기. B가 9땡이면 B 승.upToJang이면 장땡까지 재경기. - A 5끗(2월+3월), B 5끗(6월+9월) → 동점 →
rematch면 A·B 재경기,split이면 반씩. - A 구사, B 5땡, C 땡잡이 → 일반값 A=3, B=905, C=0, 최고 905 → W={C}(땡잡이). 구사 검사: A 기준 806, 상대 최고 905 > 806 → 재경기 아님 → C 승. A가 멍텅구리구사(
upTo8)였다면 905 ≤ 908 → 재경기. - 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)
// 서버만 보는 전체 상태
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 사용 지점)
setup: 좌석 섞기, 첫 선.- 매 판·매 재경기 시작: 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.
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.md5.2(한국식 베팅), 5.5(사이드 팟), 5.6(리바이).
출처 간 차이와 이 문서의 선택:
- 멍텅구리구사 범위: "8땡까지"가 기본 서술이고 지역에 따라 장땡까지 무승부로 보기도 한다 →
mungGusa옵션, 기본 8땡. - 재경기 재참가 비용: 나무위키는 "판돈만큼"이라 하지만 지역마다 다르다 → 기본은 재참가 불가, 옵션으로 절반/전액.
- 구사가 이미 이기는 경우의 재경기 여부: 6.3 설계 결정 참고.
12. 메모 (상표·법적 주의 등)
- 가상 칩만 사용한다. 방 전용이며 돈·유료 재화·광고 보상과 연결하지 않는다. 칩 거래·선물·누적 랭킹을 만들지 않는다. 결과 화면에 원(₩) 같은 화폐 표현을 쓰지 않는다.
- 법적 검토 필요(사용자 확인):
- 형법 제246조(도박): 재물을 걸지 않는 구조라 해당하지 않도록 설계했지만, 외부 정산을 부추기는 기능은 넣지 않는다.
- 게임산업진흥법 등급분류: 공개 배포하는 게임물은 원칙적으로 등급분류 대상이고, 비영리 예외는 청소년이용불가 기준에 해당하는 내용을 포함하면 적용되지 않는다. 섯다·고스톱·포커 같은 웹보드 게임은 통상 청소년이용불가로 분류되므로 서비스 전 확인이 필요하다.
- 어린이 사용자가 있으므로 섯다는 기본 목록과 분리하거나(예: "어른용" 탭) 방장 확인 문구를 두는 방안을 검토한다.
- 화투 그림: 화투(하나후다) 전통 도안 자체는 오래된 디자인이지만, 시판 화투나 다른 게임의 그림은 각 제작사의 저작물일 수 있다. 이미지를 가져오지 말고 9장의 지침대로 직접 단순화한 SVG를 그린다.
- 명칭: "섯다"는 일반 놀이 이름이라 상표 문제가 적다. 특정 서비스 이름(예: "한게임 섯다", "피망 섯다")은 쓰지 않는다. 표시 이름 후보: "섯다"(기본), 어린이 노출을 줄이고 싶다면 "화투 족보 대결".