- 개요, 조사(BGA·한국 인기 보드게임·UI/UX), 아키텍처, 실시간 프로토콜, 안정성·보안, 계정, 로비·방, UI/UX, 데이터 모델, 게임 엔진, 테스트, 배포·운영, 로드맵(M0~M9), 사용자 결정 항목 - 게임별 규칙·엔진 설계·UI·테스트 체크리스트 26종 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
49 KiB
49 KiB
마작 (mahjong)
마일스톤: M5 · 인원: 최소 3 ~ 최대 4명 (기본 4인 리치 마작, 3인은 산마 모드) · 예상 시간: 동풍전 약 25분 / 반장전 약 50분 · 난이도: 어려움
1. 개요
- 136장의 마작패로 14장 화료형(4면자 1머리 등)을 먼저 완성하는 게임. 기본 규칙은 일본식 리치 마작이다. 한국 온라인에서는 작혼(Mahjong Soul), 텐호 등을 통해 리치 마작이 가장 널리 쓰이므로 기본 모드로 삼는다. 화료하려면 역(役)이 최소 1개 있어야 하고(도라는 역이 아님), 점수는 판(翻)과 부(符)로 계산한다.
- 기본 모드: 4인 리치 마작(반장전/동풍전 선택). 보조 모드: 3인 마작(산마, 三麻).
- 인원 근거: 리치 마작 표준 규칙은 4인 전용이다. 3인용 산마는 일본에서 정착된 공식 변형(텐호·작혼 랭크전 지원)이라 최소 인원을 3명으로 둔다. 2인 변형은 전통 규칙에 없으므로 지원하지 않는다. 모드는 방 인원으로 정해진다: 4명이면 4인 마작, 3명이면 산마. 빈 자리를 AI로 채우는 기능은 이 문서 범위 밖이다.
- 점수는 방 안에서만 쓰는 가상 점수다. 저장되는 재화, 결제, 환전은 전혀 없다.
2. 모드와 옵션
| 옵션 키 | 설명 | 선택지 | 기본값 |
|---|---|---|---|
length |
대국 길이 | tonpuu(동풍전, 동1hanchan(반장전, 동1 |
hanchan |
startPoints |
시작 점수 | 정수(100 단위) | 4인 25000 / 3인 35000 |
returnPoints |
반환점(오카·서입 기준) | 정수 | 4인 30000 / 3인 40000 |
uma |
순위 우마(천 점 단위) | none / 5-10 / 10-20 / 10-30 (3인은 0 / 15) |
4인 10-20(+20,+10,-10,-20) / 3인 15(+15,0,-15) |
akaDora |
적도라(빨간 5) 개수 | 0 / 3(5m·5p·5s 각 1) — 3인은 0 / 2(5p·5s 각 1) | 4인 3 / 3인 2 |
kuitan |
울어도 탕야오 인정 | true / false | true |
doubleRon |
동시 론 처리 | allow(더블론 허용) / headBump(머리튀기: 방총자 다음 순서 1명만) |
allow |
tripleRon |
3명 동시 론 | abort(삼가화 유국) / allow |
abort |
tobi |
0점 미만이 되면 즉시 종료(토비) | true / false | true |
agariYame |
올라스트 친이 화료·텐파이로 1위면 자동 종료 | true / false | true |
westEntry |
마지막 국 종료 시 반환점 이상이 없으면 연장전(서입/남입) | true / false | true |
renchanOn |
친 연장 조건 | tenpai(친 화료 또는 유국 시 친 텐파이) / agari(친 화료 시만) |
tenpai |
kiriageMangan |
4판30부·3판60부를 만관으로 올림 | true / false | false |
kazoeYakuman |
13판 이상 헤아림 역만 | true / false (false면 삼배만 상한) | true |
doubleYakuman |
더블 역만(국사 13면·사암각 단기·순정구련·대사희) | true / false | true |
abortiveDraws |
도중유국(구종구패·사풍연타·사깡유국·사가리치) | true / false | true |
nagashiMangan |
유국만관 | true / false | true |
pao |
책임지불(대삼원·대사희 확정 울기) | true / false | true |
ankanChankanKokushi |
국사무쌍만 안깡을 창깡 가능 | true / false | true |
renpuuPairFu |
연풍패(장풍=자풍) 머리 부수 | 2 / 4 | 4 |
sanmaTsumo |
산마 쯔모 지불 방식 | tsumoLoss(쯔모손: 없는 북가 몫 소멸) / northBisection(북가 몫을 둘이 반씩) |
tsumoLoss |
sanmaHonba |
산마 본장 1개당 점수 합계 | 200 / 300 / 1000 | 200 |
sanmaNotenPool |
산마 유국 텐파이료 총액 | 2000 / 3000 | 2000 |
turnSeconds |
타패 기본 제한 시간(초) | 5~60 | 15 |
claimSeconds |
울기·론 응답 창 시간(초) | 3~20 | 8 |
timeBankSeconds |
개인 예비 시간(대국 전체, 매 국 +5초 충전, 최대값까지) | 0~300 | 60 |
beginnerMode |
초보 모드 허용(개인별로 켜고 끔) | true / false | true |
mode(4인/3인)는 옵션이 아니라 setup 시점의 인원 수로 정한다. 3인일 때 4인 전용 옵션 값은 무시하고 3인 기본값을 쓴다.
3. 구성물
- 수패 3종 x 1~9 x 각 4장 = 108장
- 만수(m, 萬子) 1m
9m, 통수(p, 筒子) 1p9p, 삭수(s, 索子) 1s~9s
- 만수(m, 萬子) 1m
- 자패 7종 x 4장 = 28장
- 풍패: 동(東)·남(南)·서(西)·북(北)
- 삼원패: 백(白)·발(發)·중(中)
- 합계 136장. 적도라 옵션이면 5m·5p·5s 중 각 1장이 빨간 5(아카)로 바뀐다(장수는 그대로).
- 3인(산마): 2m~8m 28장을 빼고 108장을 쓴다(1m·9m만 남음). 적도라는 5p·5s 각 1장.
- 용어: 노두패 = 1·9 수패, 요구패 = 노두패 + 자패, 중장패 = 2~8 수패.
- 점봉·주사위는 화면 표시용 가상 물건이다. 리치봉 1개 = 1000점, 본장봉 1개 = 연장 카운터.
4. 준비(셋업)
- 자리 정하기: RNG로 좌석 순서를 섞는다. 좌석 0이 첫 친(기가, 起家)으로 동1국의 동가가 된다. 진행 방향은 반시계(동 → 남 → 서 → 북 → 동). 화면에서는 내 오른쪽 사람이 다음 차례(하가)이고 왼쪽 사람이 앞 차례(상가)다.
- 각 국 시작:
- 136장(산마 108장)을 RNG로 셔플(Fisher–Yates)해 패산 배열을 만든다. 실제 패산 쌓기와 주사위 굴리기는 연출이다. 셔플 결과가 곧 패산이다.
- 배열 끝 14장은 왕패: 영상패(보충패) 4장, 도라 표시패 5장, 우라도라 표시패 5장. 첫 도라 표시패 1장을 공개한다.
- 친부터 13장씩 나눠 준다(연출은 4장씩 3회 + 1장). 친은 바로 첫 쯔모를 해서 14장으로 시작한다.
- 남은 산패(뽑을 수 있는 패)는 4인 70장, 산마 55장이다.
- 점수 초기화: 전원
startPoints. 본장 0, 공탁(리치봉) 0. - 자풍: 친 = 동, 그다음 = 남, 서, 북(산마는 동·남·서만). 장풍: 동장이면 동, 남장이면 남, 서입하면 서.
5. 진행 규칙
5.1 한 차례(턴)의 흐름
- 쯔모: 산패 맨 앞에서 1장 가져온다(울기 직후에는 쯔모하지 않는다).
- 쯔모 뒤 손패 14장(울기한 면자 포함 기준) 상태에서 할 수 있는 행동:
- 쯔모 화료(역이 있어야 함)
- 안깡, 가깡(산패가 1장 이상 남았고 총 깡 4회 미만일 때)
- 북 뽑기(산마만)
- 구종구패 선언(첫 순 첫 쯔모일 때만)
- 리치 선언 + 타패
- 그냥 타패 1장
- 타패 뒤 **응답 창(클레임 창)**을 연다(5.3).
- 아무도 가져가지 않으면 하가가 다음 쯔모를 한다.
5.2 울기(치·퐁·깡)
| 울기 | 대상 | 조건 | 처리 |
|---|---|---|---|
| 치 | **상가(직전 차례)**의 타패만 | 손패 2장 + 그 패로 같은 종류 연속 3장(순쯔). 자패 불가. 산마에서는 치 금지 | 노출 면자 생성 → 쯔모 없이 바로 타패 |
| 퐁 | 누구의 타패든 | 같은 패 2장 보유 | 노출 각쯔 → 바로 타패 |
| 대명깡 | 누구의 타패든 | 같은 패 3장 보유, 산패 1장 이상, 총 깡 4회 미만 | 노출 깡쯔 → 영상패 쯔모 → 타패 |
| 안깡 | 자기 차례 | 같은 패 4장 보유 | 비공개 깡쯔(2장만 뒤집어 보임) → 영상패 쯔모 |
| 가깡 | 자기 차례 | 이미 퐁한 패의 4번째 보유 | 퐁 → 깡으로 승격 → 다른 사람의 창깡 론 창을 연 뒤 영상패 쯔모 |
- 하저패(마지막 타패)는 론만 가능하다. 치·퐁·깡은 할 수 없다. 해저패(마지막 쯔모)로 깡할 수 없다.
- 구이가에(喰い替え) 금지: 치·퐁 직후 같은 차례에 (a) 방금 울은 패와 같은 종류, (b) 치의 경우 반대쪽 끝 스지 패(예: 34m에 2m을 치고 5m 버리기, 34m에 5m을 치고 2m 버리기)를 버릴 수 없다. 울고 나서 버릴 수 있는 패가 이 규칙 때문에 하나도 없으면 그 울기 자체가 불법이다.
- 치를 할 때 쓸 손패 조합이 여러 가지면(예: 4m 치기에 23m / 35m / 56m, 적5 포함 여부) 클라이언트가
tiles로 고른다. - 깡 후 새 도라 표시패: 안깡은 즉시 공개한다. 대명깡·가깡은 그 사람의 다음 타패 시점(응답 창이 열리기 전)에 공개한다. 영상개화로 바로 화료하면 대명깡·가깡의 새 도라는 공개하지 않는다.
- 깡으로 왕패에서 1장을 쓰면 산패 끝 1장이 왕패로 넘어가 왕패 14장을 유지한다. 즉 깡 1회마다 산패가 1장 줄어든다.
- 깡은 판 전체에서 4회까지다. 한 사람이 4깡을 하면 유국 없이 계속하되 그 국에서 더는 아무도 깡할 수 없다.
5.3 응답 창(클레임 창) — 실시간 설계
타패(또는 가깡 패, 북 뽑기 패, 국사 대상 안깡 패)가 나오면 서버는 다음을 한다.
- 타패자를 뺀 각 플레이어의 합법 응답 목록을 계산한다:
ron,pon,kan,chi(상가 관계·4인일 때만). 론은 역 있음 + 후리텐 아님 + 화료형일 때만 넣는다. - 합법 응답이 있는 플레이어가 아무도 없으면 창을 열지 않고 바로 다음 차례로 넘어간다. 단 정보 누출 완화를 위해 모든 타패 뒤에 같은 짧은 최소 지연(기본 400ms)을 둔다. 이 지연은 응답 가능자가 있든 없든 똑같다.
- 있으면 phase=
claim.activePlayers는 아직 응답하지 않은, 응답 가능한 플레이어 전원이다. 각자claim또는pass액션을 보낸다. 마감 시각 = 지금 +claimSeconds. 예비 시간은 쓰지 않는다. - 확정 규칙(조기 종료): 응답이 들어올 때마다 다음을 확인한다.
- 현재까지 선언된 최고 우선순위 응답이 있고, 아직 응답하지 않은 사람들이 그보다 높거나 같은 우선순위 응답을 할 수 없으면 바로 확정한다.
- 우선순위: 론 > 퐁 = 깡 > 치. 같은 패로 퐁과 깡이 동시에 나올 수는 없다(4장 한계). 퐁끼리도 겹칠 수 없다.
- 론이 하나라도 선언되면 다른 론 가능자의 응답(더블론 여부)을 기다린다. 론 가능자가 모두 응답하거나 마감되면 확정한다.
- 모든 응답 가능자가 응답했거나 마감 시각이 지나면 확정한다. 미응답자는
pass로 본다.
- 확정 결과:
- 론 1명: 화료. 론 2명:
doubleRon=allow면 둘 다 화료,headBump면 방총자 다음 차례 순으로 가장 가까운 1명. 론 3명:tripleRon=abort면 삼가화 유국,allow면 3명 모두 화료. - 퐁/깡: 그 사람에게 차례가 넘어간다(사이 사람들은 건너뜀).
- 치: 하가에게 차례.
- 전원 패스: 하가 쯔모.
- 론 1명: 화료. 론 2명:
- 론할 수 있었는데 패스(또는 시간 초과)한 사람은 동순 후리텐이 된다. 리치 중이면 영구 후리텐이 된다.
- 클라이언트에는 내 응답 버튼만 보여 준다. 다른 사람이 응답할 수 있는지, 누가 고민 중인지는 절대 알리지 않는다("다른 플레이어 확인 중"만 표시).
- 리치 선언 타패가 론 당하면 리치는 성립하지 않는다(1000점을 내지 않음). 론이 없으면 응답 창이 끝날 때 1000점을 공탁에 낸다.
5.4 리치
- 조건: 멘젠(안깡만 허용), 텐파이, 점수 1000점 이상, 남은 산패 4장 이상(산마 3장 이상).
- 선언 타패는 옆으로 눕혀 표시한다. 그 타패가 울려서 가져가지면 다음 타패를 눕힌다(표시용).
- 리치 뒤에는 손패를 바꿀 수 없다. 쯔모한 패를 바로 버리는 것(쯔모기리)만 된다. 단 다음은 허용한다.
- 쯔모 화료, 론
- 안깡: 쯔모한 패로 하는 안깡이고, 대기패가 바뀌지 않으며, 손패 해석(면자 구성)이 바뀌지 않을 때만
- 북 뽑기(산마): 대기가 바뀌지 않을 때만
- 일발: 리치 선언 후 자기 다음 타패 전까지(다음 쯔모 포함) 화료하면 +1판. 그 사이 누구든 울기(안깡 포함)를 하면 사라진다. 북 뽑기는 울기가 아니므로 일발을 지우지 않는다.
- 더블리치: 첫 순(자기 첫 타패, 그 전에 누구의 울기도 없음)에 리치하면 2판(리치 1판 대신).
5.5 후리텐(振聴)
- 텐파이 상태에서 대기패의 화료형 기준(역 유무와 상관없이)으로 판단한다.
- 버림패 후리텐: 내 대기패 중 하나라도 내 버림패(남이 울어 간 것 포함)에 있으면 론할 수 없다.
- 동순 후리텐: 다른 사람의 타패나 깡 패가 내 화료패였는데 론하지 않았으면(역이 없어 못한 경우 포함) 내 다음 타패 전까지 론할 수 없다.
- 리치 후 후리텐: 리치 뒤 화료패를 놓치면 그 국 끝까지 론할 수 없다.
- 후리텐이어도 쯔모 화료는 할 수 있다.
- 대기패 4장을 내가 모두 가진 형태(예: 1111m 단기)는 그 패로는 화료할 수 없으므로 대기로 치지 않는다.
5.6 화료 조건
- 화료형: (a) 4면자 + 1머리(면자 = 순쯔 또는 각쯔/깡쯔), (b) 칠대자(서로 다른 7쌍, 같은 패 4장을 2쌍으로 볼 수 없음), (c) 국사무쌍.
- 역이 1개 이상 있어야 한다. 도라·적도라·북은 역이 아니다.
- 대기에 따라 역이 생기는 패와 안 생기는 패가 섞여 있으면(가타아가리) 역이 생기는 패로만 화료할 수 있다. 후 붙이기(後付け)는 허용한다.
5.7 산마(3인) 차이
- 2m~8m 없음. 도라 표시패가 1m이면 도라는 9m, 9m이면 1m.
- 치 금지. 퐁·깡·론은 같다.
- 북 뽑기(拔北): 자기 차례(쯔모 후, 울기 직후 제외)에 손의 북을 옆에 빼 두고 영상패 1장을 쯔모한다. 뺀 북 1장 = 도라 1. 북이 도라 표시로 도라가 되면 뺀 북도 도라로 센다. 산패가 0장이면 할 수 없다. 북 뽑기에도 응답 창을 연다. 북을 기다리는 사람은 론할 수 있다(창깡 역은 붙지 않음). 북 뽑기 뒤 영상패로 쯔모 화료하면 영상개화를 인정한다.
- 북은 손에 남겨 머리·각쯔로 써도 된다. 다만 산마의 북은 장풍·자풍이 아닌 손님 바람이라 북 각쯔에 역은 없다. 국사무쌍·자일색·소사희 등의 구성에는 쓸 수 있다.
- 쯔모 지불:
tsumoLoss면 4인 기준으로 계산한 뒤 없는 북가 몫을 받지 않는다(예: 자 만관 쯔모 = 친 4000 + 자 2000 = 6000).northBisection이면 북가 몫을 남은 두 사람이 반씩 더 낸다(각자 100 단위 올림). - 본장: 1개당
sanmaHonba(기본 200). 론이면 방총자가 전부 내고, 쯔모면 둘이 반씩 낸다. - 유국 텐파이료: 총
sanmaNotenPool(기본 2000). 텐파이 1명이면 노텐 둘이 1000씩, 텐파이 2명이면 노텐 1명이 1000씩 두 명에게 낸다. - 사풍연타: 3명 모두 같은 바람을 처음에 버리면 성립. 사가리치는 없고 대신 3명 모두 리치해도 계속 진행한다.
- 삼색동순은 만수에 2~8이 없어 성립하지 않는다. 일기통관도 만수로는 불가.
5.8 국 종료
- 화료: 점수 정산(6장) → 다음 국.
- 황패유국(산패 소진): 마지막 쯔모자가 타패하고 론이 없으면 유국.
- 텐파이/노텐 자동 판정(형식 텐파이 인정, 역 없어도 텐파이). 텐파이자의 손패는 공개하고 노텐 손패는 공개하지 않는다.
- 4인 텐파이료 3000: 텐파이 1명 → 노텐 3명이 1000씩. 2명 → 노텐 2명이 1500씩 각각 텐파이 1명에게. 3명 → 노텐 1명이 1000씩 3명에게. 0명/4명 → 없음.
- 유국만관(옵션): 버림패가 전부 요구패이고 그중 하나도 남에게 울리지 않은 사람은 만관 쯔모만큼 받는다(친이면 4000 올, 자이면 2000/4000). 본장은 붙지 않고 텐파이료는 정산하지 않는다. 여러 명이면 각각 받는다. 친 연장 여부는 친 텐파이로 정한다.
- 도중유국(옵션, 공통: 친 연장, 본장 +1, 공탁 유지, 점수 이동 없음):
- 구종구패: 첫 순, 아무 울기 전, 자기 첫 쯔모 시점에 요구패 종류가 9종 이상이면 선언할 수 있다(선택).
- 사풍연타: 4인이 첫 타패를 모두 같은 풍패로 했고 그 사이 울기가 없음.
- 사깡유국: 2명 이상이 합계 4깡을 했을 때, 4번째 깡 뒤 타패가 론되지 않으면 유국.
- 사가리치: 4명 모두 리치(4번째 리치 타패가 론되지 않았을 때).
- 삼가화:
tripleRon=abort일 때.
5.9 친·본장·공탁
- 친 화료 → 친 연장(같은 국 번호 반복), 본장 +1.
- 자 화료 → 친이 하가로 넘어가고 국 번호 +1, 본장 0.
- 황패유국 → 본장 +1. 친이 텐파이면 연장, 아니면 친이 넘어간다(
renchanOn=agari면 무조건 넘어감). - 공탁(리치봉): 화료자가 모두 가져간다. 더블론이면 방총자에서 가장 가까운 화료자가 가져간다. 대국이 끝날 때 남은 공탁은 1위가 가져간다.
- 본장 보너스: 4인은 본장 1개당 300점(론: 방총자 300 x 본장, 쯔모: 각자 100 x 본장). 더블론이면 머리튀기 위치의 1명만 받는다.
5.10 대국 종료 조건
- 마지막 국(동풍전 동4, 반장전 남4) 종료 시 친이 넘어가면 종료.
- 올라스트 친 연장 중
agariYame이고 친이 단독 1위가 되면 종료. tobi이고 누군가 0점 미만이 되면 즉시 종료(정확히 0점은 계속).westEntry: 마지막 국이 끝났는데 아무도returnPoints이상이 아니면 다음 장(반장전은 서장, 동풍전은 남장)으로 들어간다. 연장 장에서는 국이 끝난 시점에 누군가 반환점 이상이면 바로 종료(서든데스). 연장 장의 4국까지 끝나면 무조건 종료.- 산마: 북가가 없으므로 국 번호는 1
3(동1동3, 남1~남3).
6. 승패와 점수 계산
6.1 역 목록 (멘젠 / 울었을 때)
"멘젠 한정"은 울면 성립하지 않는다. "울면 -1"은 울면 판수가 1 내려간다(쿠이사가리).
1판
| 역 | 조건 | 멘젠 | 울기 |
|---|---|---|---|
| 리치 | 5.4 | 1 | 불가 |
| 일발 | 리치 후 1순 안 화료 | 1 | 불가 |
| 멘젠쯔모 | 멘젠으로 쯔모 화료 | 1 | 불가 |
| 핑후 | 멘젠, 4면자 모두 순쯔, 머리가 역패가 아님, 양면 대기 | 1 | 불가 |
| 탕야오 | 요구패 하나도 없음 | 1 | 1 (kuitan false면 불가) |
| 이페코 | 같은 순쯔 2개 | 1 | 불가 |
| 역패: 백 / 발 / 중 | 해당 각쯔·깡쯔 | 각 1 | 각 1 |
| 역패: 장풍 | 장풍 각쯔 | 1 | 1 |
| 역패: 자풍 | 자풍 각쯔(장풍과 같으면 합 2판) | 1 | 1 |
| 해저로월 | 마지막 산패로 쯔모 화료 | 1 | 1 |
| 하저로어 | 마지막 타패로 론 | 1 | 1 |
| 영상개화 | 깡·북 보충패로 쯔모 화료 | 1 | 1 |
| 창깡 | 남의 가깡 패로 론(국사는 안깡도 옵션) | 1 | 1 |
2판
| 역 | 조건 | 멘젠 | 울기 |
|---|---|---|---|
| 더블리치 | 첫 순 리치(리치 대신) | 2 | 불가 |
| 칠대자 | 서로 다른 7쌍, 부수 25 고정 | 2 | 불가 |
| 삼색동순 | 만·통·삭 같은 숫자 순쯔 | 2 | 1 |
| 일기통관 | 한 종류 123·456·789 | 2 | 1 |
| 찬타(혼전대요구) | 모든 면자·머리에 요구패, 순쯔 1개 이상, 자패 포함 | 2 | 1 |
| 또이또이(대대화) | 4면자 모두 각쯔·깡쯔 | 2 | 2 |
| 산안커(삼암각) | 암각 3개(론으로 완성한 각쯔는 명각) | 2 | 2 |
| 삼색동각 | 만·통·삭 같은 숫자 각쯔 | 2 | 2 |
| 산깡쯔 | 깡 3개 | 2 | 2 |
| 소삼원 | 삼원패 2각쯔 + 1머리(역패 2판 별도 합산) | 2 | 2 |
| 혼노두 | 요구패만(또이또이 또는 칠대자와 복합) | 2 | 2 |
3판 / 6판
| 역 | 조건 | 멘젠 | 울기 |
|---|---|---|---|
| 혼일색 | 한 종류 수패 + 자패 | 3 | 2 |
| 준찬타(순전대요구) | 모든 면자·머리에 노두패, 자패 없음, 순쯔 1개 이상 | 3 | 2 |
| 량페코 | 이페코 2개(칠대자와 중복 불가, 높은 쪽 채택) | 3 | 불가 |
| 청일색 | 한 종류 수패만 | 6 | 5 |
역만(기본점 8000, 일반 역과 합산하지 않음. 여러 역만은 합산)
| 역만 | 조건 | 비고 |
|---|---|---|
| 국사무쌍 | 요구패 13종 각 1장 + 그중 1장 더 | 13면 대기(13종 모두 가진 상태에서 화료)는 더블 |
| 사암각 | 암각 4개(쯔모, 또는 단기 대기 론) | 단기 대기는 더블. 샤보 대기 론은 산안커+또이또이 |
| 대삼원 | 삼원패 3각쯔 | 책임지불 대상 |
| 소사희 | 풍패 3각쯔 + 풍패 머리 | |
| 대사희 | 풍패 4각쯔 | 더블. 책임지불 대상 |
| 자일색 | 자패만 | 칠대자형 인정 |
| 청노두 | 노두패만 | |
| 녹일색 | 23468s·발만(발 없어도 인정) | |
| 구련보등 | 멘젠 청일색 1112345678999 + 같은 종류 1장 | 9면 대기(순정)는 더블 |
| 사깡쯔 | 깡 4개 | |
| 천화 | 친이 배패 14장으로 화료 | 멘젠 한정 |
| 지화 | 자가 첫 쯔모로 화료, 그 전에 울기 없음 | 멘젠 한정 |
| 헤아림 역만 | 일반 역+도라 합 13판 이상 | kazoeYakuman |
doubleYakuman=false면 더블 역만도 1배 역만으로 계산한다.
6.2 역 판정 세부
- 멘젠: 치·퐁·대명깡·가깡이 없음(안깡은 멘젠 유지).
- 한 손패를 여러 면자 구성으로 해석할 수 있으면 점수가 가장 높은 해석을 고른다(판 → 부 → 최종 점수 순). 대기 형태도 같다. 예: 23455m에 5m 화료는 단기로 볼 수도 있고 양면으로 볼 수도 있다.
- 배타 관계: 량페코는 이페코를 포함하므로 이페코 별도 없음. 준찬타 ⊃ 찬타. 청일색 ⊃ 혼일색. 혼노두와 찬타는 동시 불가(찬타는 순쯔 필수). 칠대자와 핑후·이페코는 불가.
- 핑후 머리: 삼원패, 장풍, 자풍이면 불가. 그 밖의 풍패(손님 바람) 머리는 된다.
- 역패 머리는 판이 아니라 부(머리 2부)만 준다.
- 도라: 표시패의 다음 패. 수패 1→2→…→9→1, 풍패 동→남→서→북→동, 삼원패 백→발→중→백. 표시패 수만큼 따로 센다(같은 도라가 두 번 표시되면 장당 2).
- 우라도라: 리치 화료자만. 화료 시 공개된 도라 표시패마다 그 아래 패를 뒤집는다.
- 적도라: 장당 +1. 북 뽑기(산마): 장당 +1.
- 도라류는 역이 있어야만 더한다. 헤아림 역만 판정에는 포함한다.
6.3 부(符) 계산
- 칠대자: 25부 고정(아래 계산 없음).
- 핑후 쯔모: 20부 고정. 핑후 론: 30부(20 + 멘젠론 10).
- 그 밖의 경우:
- 부저 20
- 멘젠 론 +10
- 쯔모 +2(핑후 쯔모 제외, 영상개화도 +2)
- 면자 부(론으로 완성한 각쯔는 명각으로 계산)
| 면자 | 중장패 | 요구패 |
|---|---|---|
| 순쯔 | 0 | 0 |
| 명각 | 2 | 4 |
| 암각 | 4 | 8 |
| 명깡 | 8 | 16 |
| 암깡 | 16 | 32 |
- 머리: 삼원패 2, 장풍 2, 자풍 2, 연풍패(장풍=자풍)
renpuuPairFu(기본 4), 그 밖 0 - 대기: 간짱(4_6), 변짱(12_ / _89), 단기 +2 / 양면, 샤보 0
- 합계를 10 단위로 올림.
- 울었고 합계가 20부(부가 하나도 없는 울기 핑후형)이면 30부로 한다.
- 예: 멘젠 론, 중 암각(8) + 9s 암각(8) + 간짱(2) → 20+10+8+8+2 = 48 → 50부.
6.4 점수 공식
- 기본점 = 부 x 2^(판+2). 단 2000을 넘으면 만관(2000)으로 자른다.
- 판에 따른 상한: 5판 만관 2000 / 6
7판 하네만 3000 / 810판 배만 4000 / 11~12판 삼배만 6000 / 13판 이상 헤아림 역만 8000(옵션 off면 삼배만) / 역만 8000 x 배수. - 3판 70부 이상, 4판 40부 이상은 계산값이 만관(2000)을 넘으므로 만관.
kiriageMangan이면 4판30부·3판60부도 만관. - 지불(각 지불액을 100 단위 올림):
- 자 론: 방총자가 기본점 x 4
- 친 론: 방총자가 기본점 x 6
- 자 쯔모: 친이 기본점 x 2, 다른 자가 각 기본점 x 1
- 친 쯔모: 자 전원이 각 기본점 x 2
- 그다음 본장 보너스와 공탁을 더한다.
만관 이상 표
| 이름 | 판 | 기본점 | 자 론 | 자 쯔모(자/친) | 친 론 | 친 쯔모(각자) |
|---|---|---|---|---|---|---|
| 만관 | 5 (또는 3판70+ / 4판40+) | 2000 | 8000 | 2000 / 4000 | 12000 | 4000 |
| 하네만 | 6~7 | 3000 | 12000 | 3000 / 6000 | 18000 | 6000 |
| 배만 | 8~10 | 4000 | 16000 | 4000 / 8000 | 24000 | 8000 |
| 삼배만 | 11~12 | 6000 | 24000 | 6000 / 12000 | 36000 | 12000 |
| 역만 | 13+ / 역만 | 8000 | 32000 | 8000 / 16000 | 48000 | 16000 |
| 더블 역만 | 16000 | 64000 | 16000 / 32000 | 96000 | 32000 |
1~4판 점수표("-"는 불가능한 조합. 쯔모 칸의 "a/b"는 자 a, 친 b. "올"은 각자)
자(비친) 화료 (론 / 쯔모)
| 부 | 1판 | 2판 | 3판 | 4판 |
|---|---|---|---|---|
| 20 | - / - | - / 400/700 | - / 700/1300 | - / 1300/2600 |
| 25 | - / - | 1600 / - | 3200 / 800/1600 | 6400 / 1600/3200 |
| 30 | 1000 / 300/500 | 2000 / 500/1000 | 3900 / 1000/2000 | 7700 / 2000/3900 |
| 40 | 1300 / 400/700 | 2600 / 700/1300 | 5200 / 1300/2600 | 8000(만관) / 2000/4000 |
| 50 | 1600 / 400/800 | 3200 / 800/1600 | 6400 / 1600/3200 | 8000(만관) / 2000/4000 |
| 60 | 2000 / 500/1000 | 3900 / 1000/2000 | 7700 / 2000/3900 | 8000(만관) / 2000/4000 |
| 70 | 2300 / 600/1200 | 4500 / 1200/2300 | 8000(만관) / 2000/4000 | 8000(만관) / 2000/4000 |
| 80 | 2600 / 700/1300 | 5200 / 1300/2600 | 8000(만관) / 2000/4000 | 8000(만관) / 2000/4000 |
| 90 | 2900 / 800/1500 | 5800 / 1500/2900 | 8000(만관) / 2000/4000 | 8000(만관) / 2000/4000 |
| 100 | 3200 / 800/1600 | 6400 / 1600/3200 | 8000(만관) / 2000/4000 | 8000(만관) / 2000/4000 |
| 110 | 3600 / 900/1800 | 7100 / 1800/3600 | 8000(만관) / 2000/4000 | 8000(만관) / 2000/4000 |
친 화료 (론 / 쯔모)
| 부 | 1판 | 2판 | 3판 | 4판 |
|---|---|---|---|---|
| 20 | - / - | - / 700 올 | - / 1300 올 | - / 2600 올 |
| 25 | - / - | 2400 / - | 4800 / 1600 올 | 9600 / 3200 올 |
| 30 | 1500 / 500 올 | 2900 / 1000 올 | 5800 / 2000 올 | 11600 / 3900 올 |
| 40 | 2000 / 700 올 | 3900 / 1300 올 | 7700 / 2600 올 | 12000(만관) / 4000 올 |
| 50 | 2400 / 800 올 | 4800 / 1600 올 | 9600 / 3200 올 | 12000(만관) / 4000 올 |
| 60 | 2900 / 1000 올 | 5800 / 2000 올 | 11600 / 3900 올 | 12000(만관) / 4000 올 |
| 70 | 3400 / 1200 올 | 6800 / 2300 올 | 12000(만관) / 4000 올 | 12000(만관) / 4000 올 |
| 80 | 3900 / 1300 올 | 7700 / 2600 올 | 12000(만관) / 4000 올 | 12000(만관) / 4000 올 |
| 90 | 4400 / 1500 올 | 8700 / 2900 올 | 12000(만관) / 4000 올 | 12000(만관) / 4000 올 |
| 100 | 4800 / 1600 올 | 9600 / 3200 올 | 12000(만관) / 4000 올 | 12000(만관) / 4000 올 |
| 110 | 5300 / 1800 올 | 10600 / 3600 올 | 12000(만관) / 4000 올 | 12000(만관) / 4000 올 |
엔진은 표가 아니라 공식으로 계산하고, 이 표는 단위 테스트의 기대값으로 쓴다.
6.5 책임지불(파오, pao)
- 대삼원의 3번째 삼원 각쯔, 대사희의 4번째 풍패 각쯔를 퐁·대명깡으로 확정시킨 타패자가 책임자다.
- 그 역만을 쯔모하면 책임자가 전액(역만 부분) 낸다. 다른 사람이 방총하면 방총자와 책임자가 반씩 낸다. 본장은 쯔모면 책임자, 론이면 방총자가 낸다.
6.6 계산 예시
- 자, 멘젠 론. 손: 234m 567p 789s 22s 45p + 화료 3p(양면). 역: 핑후 1판. 도라 없음. 부: 30(핑후 론). → 1000점. 본장 1개면 1300점.
- 친, 쯔모. 리치·멘젠쯔모·탕야오·도라1 = 4판. 부: 20 + 쯔모 2 + 암각(중장) 4 + 간짱 2 = 28 → 30부. 기본점 30 x 2^6 = 1920 → 친 쯔모 각 1920 x 2 = 3840 → 3900 올(합 11700).
- 자, 론. 혼일색(울기 2) + 백 1 + 도라 2 = 5판 → 만관 8000.
- 칠대자 리치 론, 도라 2 = 5판 → 만관. 칠대자 리치 론 도라 0 = 3판 25부 → 기본점 25 x 32 = 800 → 자 3200.
- 더블론: 자 A(3900)와 친 B(12000)가 동시에 같은 사람의 타패로 론. 방총자가 둘 다 지불(15900). 공탁과 본장은 방총자 다음 차례에서 가까운 쪽이 받는다.
- 산마, 쯔모손: 자 만관 쯔모 → 친 4000, 자 2000, 합 6000.
6.7 최종 순위와 결과 점수
- 순위: 최종 점수 내림차순. 동점이면 기가(좌석 0)에 가까운 좌석이 위.
- 남은 공탁은 1위에게.
- 결과 점수(방 표시용, 천 점 단위 소수 1자리):
(점수 - returnPoints) / 1000 + 우마. 1위에게 오카(returnPoints - startPoints) x 인원 / 1000을 더한다(4인 기본 +20, 3인 기본 +15). - 예(4인, 25000/30000, 우마 10-20): 최종 42000 / 31000 / 18000 / 9000 → 1위 (12)+20+20 = +52.0, 2위 1+10 = +11.0, 3위 -12-10 = -22.0, 4위 -21-20 = -41.0. 합 0.
7. 지역 룰 / 하우스 룰 (옵션으로 켜고 끄기)
- 울어도 탕야오(쿠이탕):
kuitan(기본 on) - 적도라 수:
akaDora - 더블론/머리튀기, 삼가화:
doubleRon,tripleRon - 토비 종료, 아가리야메, 서입:
tobi,agariYame,westEntry - 텐파이 연장/화료 연장:
renchanOn - 절상만관:
kiriageMangan - 헤아림 역만, 더블 역만:
kazoeYakuman,doubleYakuman - 도중유국, 유국만관, 책임지불:
abortiveDraws,nagashiMangan,pao - 국사무쌍 안깡 창깡:
ankanChankanKokushi - 연풍패 머리 2부/4부:
renpuuPairFu - 산마 쯔모손/북가절반, 본장 금액, 텐파이료 총액
- 지원하지 않는 로컬 역(향후 확장 후보): 인화, 대차륜, 8연장, 오픈 리치, 십삼불탑, 석상삼년 등. 기본 규칙과 섞이면 혼란이 커서 1차 범위에서 뺀다.
7.1 초보 모드(개인별, beginnerMode 허용 시)
- 손패 자동 정렬(만→통→삭→자, 숫자순, 쯔모패는 오른쪽에 띄움). 자동 정렬은 모든 사람에게 기본이고 끌 수 없다.
- 텐파이 유지 타패 하이라이트: 버리면 텐파이가 되는 패 위에 표시, 탭하면 그 경우의 대기패와 남은 장수(내가 볼 수 있는 정보로만 계산)를 보여 준다.
- 역 힌트: 지금 손에서 노릴 만한 역 후보(탕야오, 역패, 혼일색 등)와 "역이 없음" 경고를 띄운다.
- 화료 가능하면 [쯔모]/[론] 버튼을 크게 강조하고 진동을 준다. 후리텐이면 "후리텐: 론 불가, 쯔모만 가능" 배지를 띄운다.
- 울기 버튼 옆에 "울면 리치를 못 해요" 같은 짧은 설명을 붙인다.
- 자동 화료(쯔모·론 자동), 울기 안 함(자동 패스) 토글.
- 초보 모드 계산은 모두 본인
view에 이미 있는 정보만으로 클라이언트에서 해도 된다. 다른 사람 손패나 패산 정보는 쓰지 않는다.
8. 엔진 설계
8.1 상태(State)
// 패 표현: 0..135 고유 ID. kind = id >> 2 (0..33)
// kind 0-8: 1m-9m, 9-17: 1p-9p, 18-26: 1s-9s, 27-30: 동남서북, 31-33: 백발중
// 적도라: ID 16(5m), 52(5p), 88(5s) — 옵션 수만큼만 적도라로 취급
type TileId = number;
type Kind = number;
type Seat = 0 | 1 | 2 | 3;
interface MahjongOptions {
length: 'tonpuu' | 'hanchan';
startPoints: number; returnPoints: number;
uma: 'none' | '5-10' | '10-20' | '10-30' | '0' | '15';
akaDora: 0 | 2 | 3; kuitan: boolean;
doubleRon: 'allow' | 'headBump'; tripleRon: 'abort' | 'allow';
tobi: boolean; agariYame: boolean; westEntry: boolean;
renchanOn: 'tenpai' | 'agari';
kiriageMangan: boolean; kazoeYakuman: boolean; doubleYakuman: boolean;
abortiveDraws: boolean; nagashiMangan: boolean; pao: boolean;
ankanChankanKokushi: boolean; renpuuPairFu: 2 | 4;
sanmaTsumo: 'tsumoLoss' | 'northBisection'; sanmaHonba: 200 | 300 | 1000; sanmaNotenPool: 2000 | 3000;
turnSeconds: number; claimSeconds: number; timeBankSeconds: number;
beginnerMode: boolean;
}
type Meld =
| { type: 'chi'; tiles: [TileId, TileId, TileId]; called: TileId; from: Seat }
| { type: 'pon'; tiles: [TileId, TileId, TileId]; called: TileId; from: Seat }
| { type: 'daiminkan'; tiles: TileId[]; called: TileId; from: Seat }
| { type: 'kakan'; tiles: TileId[]; called: TileId; from: Seat; added: TileId }
| { type: 'ankan'; tiles: TileId[] };
interface DiscardEntry { tile: TileId; tsumogiri: boolean; riichiDeclare: boolean; calledBy: Seat | null; }
interface ClaimWindow {
source: 'discard' | 'kakan' | 'ankan' | 'kita';
tile: TileId; from: Seat;
eligible: Partial<Record<Seat, Array<'ron' | 'pon' | 'kan' | 'chi'>>>; // 서버 내부 전용
responses: Partial<Record<Seat, ClaimResponse>>;
deadline: number;
riichiPending: boolean; // 이 타패가 리치 선언 타패인지
}
type ClaimResponse = { type: 'pass' } | { type: 'ron' } | { type: 'pon' | 'kan' | 'chi'; tiles: TileId[] };
interface HandState {
liveWall: TileId[]; // 앞에서 쯔모, 깡/북 시 끝에서 1장을 왕패로 이동
deadWall: TileId[]; // 항상 14장 유지 (영상패, 도라/우라 표시패)
rinshanUsed: number;
doraIndicators: number; // 공개된 표시패 수 (1~5)
pendingKanDora: number; // 대명깡/가깡 후 다음 타패 때 공개할 수
hands: TileId[][]; // seat별 손패 (정렬 상태 무관)
drawn: (TileId | null)[]; // 이번 차례 쯔모패 (쯔모기리·리치 판정용)
melds: Meld[][];
kita: TileId[][]; // 산마 북 뽑기
discards: DiscardEntry[][];
riichi: ({ discardIndex: number; double: boolean; ippatsuLive: boolean } | null)[];
furiten: { tempUntilNextDiscard: boolean; riichiPermanent: boolean }[];
turn: Seat;
stage: 'draw' | 'afterDraw' | 'afterCall' | 'claim' | 'ended';
firstRoundClean: boolean; // 첫 순이 울기 없이 진행 중 (더블리치/천화/지화/구종구패/사풍연타)
firstDiscardWinds: (Kind | null)[];
kanCount: number; kanBySeat: number[];
lastDrawWasRinshan: boolean; // 영상개화 판정
paoBy: Partial<Record<Seat, { yakuman: 'daisangen' | 'daisuushii'; liable: Seat }>>;
claim: ClaimWindow | null;
deadline: number | null;
turnStartedAt: number;
}
interface HandResultRecord {
kind: 'agari' | 'exhaustive' | 'abortive';
winners?: Array<{ seat: Seat; from: Seat | null; han: number; fu: number; yaku: Array<{ name: string; han: number }>;
yakumanMultiplier: number; points: number; hand: TileId[]; melds: Meld[]; winTile: TileId;
uraIndicators: TileId[] }>;
tenpai?: Seat[]; abortiveReason?: 'kyuushu' | 'suufon' | 'suukan' | 'suucha' | 'sanchaho';
deltas: number[]; // seat별 점수 변화
}
interface MahjongState {
options: MahjongOptions;
mode: 'yonma' | 'sanma';
players: PlayerId[]; // index = seat
rng: RngState;
points: number[];
roundWind: 0 | 1 | 2 | 3; // 동/남/서/북
handNumber: number; // 0..3 (산마 0..2)
dealer: Seat;
honba: number;
riichiPool: number; // 공탁 점수 (1000 단위)
hand: HandState | null;
phase: 'playing' | 'handResult' | 'finished';
lastHandResult: HandResultRecord | null;
readyForNext: Seat[]; // 결과 화면 확인
timeBank: number[]; // seat별 남은 예비 시간(ms)
autoPilot: boolean[]; // 연속 시간 초과/연결 끊김
prefs: { autoWin: boolean; noCalls: boolean }[];
history: HandResultRecord[];
stats: { wins: number; dealIns: number; riichi: number; calls: number }[];
}
8.2 액션
| type | payload | 누가 / 언제 | 검증 조건 |
|---|---|---|---|
discard |
{ tile: TileId; riichi?: boolean } |
차례인 사람, stage afterDraw/afterCall |
tile이 손패에 있음. 리치 중이면 tile == drawn. afterCall이면 구이가에 금지 패 아님. riichi면 5.4 조건 모두 충족 + 버린 뒤 텐파이 |
tsumo |
{} |
차례인 사람, afterDraw |
화료형 + 역 1개 이상 |
ankan |
{ kind: Kind } |
차례인 사람, afterDraw |
같은 kind 4장 보유, 산패 ≥1, kanCount<4, 리치 중이면 drawn 패로 하는 깡이고 대기·면자 해석이 바뀌지 않음 |
kakan |
{ tile: TileId } |
차례인 사람, afterDraw |
같은 kind 퐁 보유, 산패 ≥1, kanCount<4, 리치 아님 |
kita |
{} |
산마, 차례인 사람, afterDraw |
북 보유, 산패 ≥1, 리치 중이면 대기 불변 |
kyuushu |
{} |
차례인 사람, 첫 쯔모 직후 | abortiveDraws, firstRoundClean, 자기 첫 쯔모, 요구패 종류 ≥9 |
claim |
{ type: 'ron' } | { type: 'pon' | 'kan' | 'chi'; tiles: TileId[] } |
응답 창의 eligible 대상자 중 미응답자 |
해당 타입이 eligible에 있음. tiles가 손패에 있고 대상 패와 맞는 조합. 치/퐁은 울고 난 뒤 구이가에 아닌 버릴 패가 1장 이상 존재 |
pass |
{} |
응답 창 미응답자 | eligible 대상자 |
ready |
{} |
phase handResult의 모든 플레이어 |
아직 ready 안 함 |
setPref |
{ autoWin?: boolean; noCalls?: boolean } |
언제든 본인 | 불리언 |
validate실패 사유 예(한국어): "지금은 당신 차례가 아닙니다", "리치 후에는 쯔모한 패만 버릴 수 있습니다", "역이 없어 화료할 수 없습니다", "후리텐이라 론할 수 없습니다", "방금 울은 패와 같은 패는 버릴 수 없습니다", "남은 패가 부족해 리치할 수 없습니다", "점수가 1000점 미만이라 리치할 수 없습니다".apply이벤트 예:draw(본인에게만 패 정보, 다른 사람에게는 "쯔모함"),discard,riichi,call,kan,doraRevealed,kita,claimWindowOpened(본인 옵션만),agari,ryuukyoku,pointsChanged,handStarted,gameEnded.activePlayers: stage가draw/afterDraw/afterCall이면[turn].claim이면 eligible이면서 미응답인 사람들.handResult면 ready 안 한 사람들.finished면[].- 쯔모(
draw)는 액션이 아니다. 서버가 이전 상태를 확정할 때apply안에서 자동으로 한다(stagedraw→afterDraw).
8.3 공개/비공개 정보 (view)
- 모두(관전자 포함)에게 공개: 좌석·닉네임·점수, 장/국/본장/공탁, 친, 공개된 도라 표시패, 남은 산패 수, 모든 버림패(쯔모기리 여부·리치 표시·울린 패 표시), 모든 노출 면자, 안깡(양 끝 2장만 뒷면 표시하되 kind는 공개), 북 뽑기, 리치 여부, 각자 손패 장수, 현재 차례, 마감 시각.
- 본인에게만: 내 손패 전체, 내 쯔모패, 내 합법 액션 목록(타패 가능 패, 리치 가능 패, 깡/북/쯔모 가능 여부), 응답 창이 열렸을 때 내 응답 선택지, 내 후리텐 상태, 내 예비 시간.
- 절대 보내지 않음: 패산·왕패 순서, 공개 안 된 도라/우라 표시패, 남의 손패, 남의
eligible(응답 가능 여부), 남의 응답 내용(확정 전), RNG 상태. - 국 종료 시 추가 공개: 화료자 손패와 화료패, 우라도라 표시패(리치 화료가 있을 때만), 유국 시 텐파이자 손패(노텐자 손패는 비공개).
- 관전자 view는
viewer=null로 공개 정보만 준다. 관전자 지연 송출은 플랫폼 공통 옵션으로 둔다(마작은 기본 지연 없음이지만 손패를 아예 주지 않으므로 누출 위험이 없다).
8.4 랜덤 요소 (시드 RNG 사용 지점)
setup: 좌석 순서 셔플(기가 결정).- 각 국 시작: 패산 셔플(Fisher–Yates, 136/108장).
- 주사위 2개 값(연출용, 패산 자르는 위치 표시). 실제 패 순서에는 영향 없음.
- 그 밖에는 RNG를 쓰지 않는다. 시간 초과 자동 행동도 결정적이다.
8.5 타이머·시간 초과·연결 끊김 시 자동 행동
- 차례 타이머:
turnSeconds소진 후 개인timeBank를 깎는다. 둘 다 0이면onTimeout. - 응답 창:
claimSeconds고정, 예비 시간을 쓰지 않는다. deadline(state): 현재 단계의 마감 시각(차례 = 시작 + 기본 + 남은 예비, 응답 창 = 창 마감, 결과 화면 = 표시 후 15초).onTimeout(state, player):afterDraw:prefs.autoWin이고 쯔모 화료가 되면tsumo. 아니면 쯔모기리(discard drawn). 리치·깡·북·구종구패는 하지 않는다.afterCall: 손패 맨 오른쪽(정렬 기준 마지막)부터 구이가에가 아닌 첫 패를 버린다.claim:prefs.autoWin이고 론 가능하면ron, 아니면pass(동순/리치 후리텐 규칙 적용).handResult:ready.
- 연속 2회 시간 초과나 연결 끊김이면
autoPilot=true가 된다. 이때 차례 마감을 2초, 응답 창 대상이면 즉시pass처리해 다른 사람을 기다리게 하지 않는다. 재접속해 액션을 1회 보내면 해제된다. - 응답 창은 자동 행동이 와도 확정 규칙(5.3)대로 처리한다. 서버는 항상 도착 순서대로 처리하며, 마감 시각 이후 도착한 응답은 버린다.
8.6 종료 조건과 결과(GameResult)
- 5.10의 조건이 되면 phase=
finished. result(state):
interface MahjongResult {
ranking: Array<{ player: PlayerId; seat: Seat; rank: 1 | 2 | 3 | 4; points: number; score: number /* 우마·오카 반영, 소수 1자리 */ }>;
summary: {
rounds: number; // 진행한 국 수
bestHand: { player: PlayerId; han: number; fu: number; yaku: string[]; points: number } | null;
perPlayer: Array<{ player: PlayerId; wins: number; dealIns: number; riichi: number; calls: number }>;
endReason: 'normal' | 'tobi' | 'agariYame' | 'westEntryEnd';
};
}
9. UI/UX
- 화면 배치(모바일 세로)
- 아래: 내 손패 14장 한 줄. 세로 360px 폭이면 패 1장이 약 24px로 48px 터치 기준보다 좁다. 그래서 (1) 패 영역 높이를 64px 이상으로 잡고 (2) 탭하면 그 패를 확대해 띄우고(선택), 다시 탭하거나 위로 밀면 버린다(2단계 확정). 큰 [버리기] 버튼도 함께 둔다. 오탭 방지를 위해 기본은 2단계 확정이며 설정에서 1탭 버리기로 바꿀 수 있다.
- 가운데: 4방향 버림패(6장 x 3줄), 중앙 정보판(장/국, 본장, 공탁, 남은 패 수, 도라 표시패, 각자 점수와 바람).
- 위·좌·우: 상대 이름, 점수, 리치봉, 노출 면자.
- 응답 창: 화면 아래쪽에 [론] [퐁] [깡] [치] [패스] 큰 버튼(높이 56px 이상), 남은 시간 원형 게이지. 치 조합이 여럿이면 조합 그림 버튼을 따로 띄운다.
- 내 차례 행동 버튼: [쯔모] [리치] [깡] 북 [구종구패].
- PC/가로: 정사각 테이블 배치. 가로 화면을 권장 안내로 띄운다.
- 합법 수 하이라이트: 리치 선언 모드에서 리치 가능한 타패만 밝게, 구이가에 금지 패는 회색 + 자물쇠.
- 애니메이션: 쯔모/타패 150ms, 울기 시 "퐁!" 같은 큰 글자(텍스트, 음성 옵션), 화료 화면에서 역 목록이 한 줄씩 나오고 판/부/점수 표시. 모든 애니메이션은 "애니메이션 줄이기" 설정으로 끌 수 있다.
- 패 디자인: 큰 글씨 모드(한자 대신 한글 보조 표기 "1만", "동", "백" 병기)를 기본으로 켠다. 색약 대응으로 종류별 테두리 무늬를 넣는다.
- 규칙 보기: 역 목록(그림 예시), 점수표, 후리텐 설명, 용어집. 대국 중 하단 [?] 버튼으로 연다.
- 결과 화면: 국별 점수 변화 그래프, 최종 순위, 다시 하기.
10. 테스트 체크리스트
- 셋업: 같은 시드면 같은 좌석·패산·배패가 나온다. 4인 산패 70장, 산마 55장, 왕패 14장.
- 산마 패 구성: 2m~8m 없음, 108장, 1m 표시 → 9m 도라.
- 화료 판정: 칠대자에서 같은 패 4장은 2쌍으로 인정하지 않는다. 국사무쌍 13면 대기에서 더블 역만(옵션 on/off).
- 고점법: 223344m 556677p 88s(칠대자로도 량페코로도 해석 가능)에서 점수가 높은 쪽(량페코)을 고른다. 23455m에 5m 화료 시 단기(+2부) 대 양면(핑후) 중 점수가 높은 쪽.
- 부 계산: 6.6 예시 2(28→30부, 친 쯔모 3900 올), 울기 20부 → 30부, 핑후 쯔모 20부, 칠대자 25부.
- 점수표: 6.4의 1
4판 x 20110부 표 전 칸과 공식 결과가 일치한다(론 불가 칸은 상태 생성 단계에서 막힘). - 응답 창 우선순위: A가 치, B가 퐁을 동시에 선언 → B가 이김. B가 퐁 선언 후 C(론 가능)가 미응답 → 확정하지 않고 C를 기다린다.
- 응답 창 조기 확정: 퐁 선언 후 남은 미응답자가 치만 가능 → 즉시 확정.
- 더블론: 두 사람 론 → 둘 다 정산, 공탁·본장은 방총자 다음 순으로 가까운 사람.
headBump면 그 1명만. 3명 론 +abort→ 삼가화 유국. - 후리텐: 내가 버린 패가 대기 → 론 버튼 없음, 쯔모는 가능. 론 패스 후 같은 순 다른 사람 타패로 론 불가, 내 타패 후 해제. 리치 후 놓침 → 국 끝까지 론 불가.
- 역 없는 화료 시도: 울기 후 역 없는 손에서
tsumo→ "역이 없어 화료할 수 없습니다". 가타아가리 대기에서 역 없는 패로 론 불가. - 리치 검증: 999점에서 리치 불가. 남은 패 3장(4인)에서 리치 불가. 리치 선언 타패가 론 당하면 공탁 1000이 들어가지 않는다.
- 리치 후 안깡: 대기가 바뀌는 안깡은 거부된다(예: 3334m 리치 중 3m 안깡).
- 구이가에: 34m 상태로 2m 치 후 5m·2m 타패 거부. 버릴 수 있는 패가 없게 되는 치는 응답 선택지에 아예 나오지 않는다.
- 깡: 대명깡 후 영상개화 → 새 도라 미공개. 대명깡 후 타패 → 그 시점에 도라 공개되고 그 타패로 론하면 새 도라 포함. 가깡 → 창깡 론 가능, 창깡 1판. 2명 합계 4깡 후 타패 통과 → 사깡유국.
- 하저패: 마지막 타패에 퐁/치 선택지가 없고 론만 있다. 해저 쯔모 후 깡 불가.
- 유국: 텐파이 1/2/3명 각각 텐파이료(4인 3000, 산마 2000). 대기패 4장 자가 보유 형태는 노텐. 친 노텐이면 친 이동, 본장 +1.
- 유국만관: 요구패만 버리고 울리지 않음 → 만관 지불, 텐파이료 없음.
- 도중유국: 구종구패 선언 → 친 연장, 본장 +1, 공탁 유지. 사풍연타, 사가리치.
- 책임지불: 백·발 퐁 후 중 퐁을 준 사람이 있을 때 대삼원 쯔모 → 책임자 전액. 다른 사람 방총 → 반반.
- 종료: 남4 자 화료로 종료. 남4 친 1위 화료 + agariYame → 종료. 토비(-100점)로 즉시 종료, 0점은 계속. 남4 후 전원 30000 미만 → 서입, 서1에서 30000 넘으면 종료.
- 순위 동점: 점수가 같으면 기가에 가까운 좌석이 위. 남은 공탁 1위 지급, 우마·오카 합 0.
- 산마: 북 뽑기 → 영상패 쯔모, 북 도라 +1. 북 뽑기 패로 론 가능. 쯔모손 계산 6000. 치 선택지 없음.
- 시간 초과: 차례 시간 초과 → 쯔모기리, 예비 시간 감소. 응답 창 시간 초과 → 패스 + 동순 후리텐. 2회 연속 → autoPilot, 다음 차례 2초 처리. 재접속 후 액션 → 해제.
- 연결 끊김 중 응답 창: 끊긴 사람이 론 가능해도 다른 사람의 퐁이 무한정 기다리지 않는다(즉시 패스 처리).
- 정보 누출: 다른 사람 view와 관전자 view에 내 손패, 쯔모패, 패산, 미공개 도라/우라,
eligible, 응답 내용이 없다(JSON 전체 문자열 검색으로 TileId 누출 확인). 응답 창 지연이 응답 가능 여부와 상관없이 최소 400ms로 같다. - 재현성: 같은 시드 + 같은 액션 로그 → 같은 최종 상태와 결과.
11. 참고 자료
- 일본 마작 위키백과(역 목록·판수): https://en.wikipedia.org/wiki/Japanese_mahjong_yaku
- 일본 마작 점수 규칙(부 계산, 기본점 공식, 만관 이상, 지불, 본장, 텐파이료): https://en.wikipedia.org/wiki/Japanese_mahjong_scoring_rules
- Riichi Wiki — Mahjong Soul 규칙, 산마(쯔모손, 북 뽑기, 본장): https://riichi.wiki/Mahjong_Soul , https://riichi.wiki/Sanma
- 산마 규칙 체크리스트(Memojong): https://memojong.com/en/guides/sanma-rule-list/
- 나무위키 "리치 마작" 문서(한국어 용어 대조용)
- World Riichi Championship 규칙(대회 표준 규칙, 연풍패·깡 도라 시점 등 비교용)
12. 메모 (상표·법적 주의 등)
- "작혼", "Mahjong Soul", "天鳳/텐호", "M리그" 등은 상표이거나 서비스 이름이다. 화면·마케팅에 쓰지 않는다. 게임 이름은 일반 명칭인 "마작" 또는 "리치 마작"을 쓴다(사용자 선택 후보: "마작", "리치 마작", "4인 마작").
- 패 그림은 직접 그리거나 라이선스가 확인된 공개 자산(예: CC0/CC-BY 마작패 SVG)을 쓴다. 상용 게임 패 그림을 가져오지 않는다.
- 법적 주의: 마작은 점수 내기로 흐르기 쉬운 게임이다. 이 사이트의 점수는 방 안에서만 쓰는 가상 점수이며, 대국이 끝나면 기록용 결과만 남고 재화·포인트로 쌓이거나 바뀌지 않는다. 유료 아이템·재화·환전·선물 기능을 붙이면 「게임산업진흥에 관한 법률」의 사행성·환전 금지 규정(제28조, 제32조)과 형법 도박죄(제246조) 문제가 생기므로 넣지 않는다.
- 게임물 등급분류: 국내에서 온라인 게임을 서비스하려면 원칙적으로 게임물관리위원회 등급분류가 필요하다(게임산업법 제21조). 비영리·무료라도 예외 해당 여부를 출시 전에 확인해야 한다. 마작은 화투·포커류 웹보드 게임과 달리 사행성 운영이 없으면 청소년이용불가로 일괄 분류되지는 않는다. 다만 대상 연령(아이 포함)을 고려해 사전 검토를 권장한다.
- 용어 표기는 한국 커뮤니티 관용(작혼 한국어판·나무위키)에 맞추고, 화면에는 일본어 원어를 괄호로 보조 표기한다.