Skip to content

Commit c4cac6a

Browse files
committed
riichi: align round flow rules and reaction priority
1 parent 043b230 commit c4cac6a

5 files changed

Lines changed: 514 additions & 2 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ Admin targeting details:
6565
- a round starts automatically only when all 4 seats are filled and all seated players are ready
6666
- after a hand or a full match ends, players must ready again before the next start
6767

68+
## Rule References
69+
70+
- Riichi round flow rules: [docs/riichi-round-flow.md](./docs/riichi-round-flow.md)
71+
6872
## Build
6973

7074
```powershell

docs/riichi-round-flow.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Riichi Round Flow Rules
2+
3+
This document describes the formal round-flow rules used by the Riichi mode in this repository.
4+
5+
## 1. Base Turn Flow
6+
7+
- Seat winds stay fixed as East, South, West, North.
8+
- Turn progression follows East -> South -> West -> North.
9+
- Seat labeling remains East, South, West, North.
10+
- A normal turn is "draw one tile, discard one tile".
11+
- In normal flow, each player keeps 13 concealed tiles after finishing their discard.
12+
13+
## 2. Draw Rules
14+
15+
- Normal draw: when your turn begins and no call interrupts flow, draw from the live wall front.
16+
- Wall consumption is modeled as a single forward sequence from the live wall front.
17+
- Dead wall reserve: the last 14 tiles are reserved and not used for normal draws.
18+
- Kan supplement draw: after kan (ankan, minkan, kakan), draw one rinshan tile from the dead wall side and then discard.
19+
- Exhaustive draw: when the live wall is exhausted (only dead wall reserve remains), the hand ends in an exhaustive draw if nobody has won.
20+
21+
## 3. Discard Rules
22+
23+
- Every discard is public and tracked as part of each player's discard history.
24+
- A tile claimed by another player (chii/pon/minkan) is still treated as a tile that the discarder has discarded for furiten logic.
25+
- Open calls interrupt normal draw order and transfer turn control to the caller.
26+
27+
## 4. Interrupting Actions
28+
29+
- Chii: only the next player in turn order after the discarder may chii.
30+
- Pon: any non-discarding player may pon if legal.
31+
- Kan: any non-discarding player may minkan if legal; caller then performs a rinshan draw.
32+
- Ron: a legal ron claim immediately ends the hand.
33+
- Priority when multiple claims compete on one discard: ron > pon/kan > chii.
34+
- After chii/pon, the caller does not take a normal draw and must discard directly.
35+
36+
## 5. Furiten
37+
38+
- If your winning waits include a tile you have already discarded, you are furiten.
39+
- While furiten, you cannot win by ron.
40+
- While furiten, you may still win by tsumo if otherwise legal.
41+
42+
## 6. Test Coverage Mapping
43+
44+
Current automated tests explicitly protect:
45+
46+
- turn progression order after a normal discard
47+
- chii eligibility limited to the next player
48+
- no normal draw before the caller's discard after chii
49+
- pon priority over chii on the same discard
50+
- furiten checks over discard history

src/main/kotlin/top/ellan/mahjong/riichi/RiichiPlayerState.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ open class RiichiPlayerState(
136136
fun chii(
137137
tile: TileInstance,
138138
tilePair: Pair<MahjongTile, MahjongTile>,
139+
claimTarget: ClaimTarget,
139140
target: RiichiPlayerState
140141
) {
141142
lastDrawnTile = null
@@ -147,7 +148,7 @@ open class RiichiPlayerState(
147148
val fuuro = Fuuro(
148149
type = MeldType.CHII,
149150
tileInstances = tileShuntsu,
150-
claimTarget = ClaimTarget.LEFT,
151+
claimTarget = claimTarget,
151152
claimTile = tile
152153
)
153154
hands -= tileShuntsu.filter { it != tile }.toSet()

src/main/kotlin/top/ellan/mahjong/riichi/RiichiRoundEngine.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,9 @@ class RiichiRoundEngine(
445445
}
446446
if (!pending.isChankan) {
447447
val ponKanCandidates = pending.options.filterValues { it.canPon || it.canMinkan }.keys
448+
if (pending.responses.keys.containsAll(ponKanCandidates).not()) {
449+
return
450+
}
448451
val anyPonKan = pending.responses.filterKeys { it in ponKanCandidates }.values.any { it.type == ReactionType.PON || it.type == ReactionType.MINKAN }
449452
if (!anyPonKan) {
450453
val chiiCandidates = pending.options.filterValues { it.chiiPairs.isNotEmpty() }.keys
@@ -497,7 +500,7 @@ class RiichiRoundEngine(
497500
if (chiiResponse != null) {
498501
val winner = seatPlayer(chiiResponse.key)!!
499502
val discarder = seatPlayer(pending.discarderUuid)!!
500-
winner.chii(pending.tile, chiiResponse.value.chiiPair!!, discarder)
503+
winner.chii(pending.tile, chiiResponse.value.chiiPair!!, claimTarget(winner, discarder), discarder)
501504
currentPlayerIndex = seats.indexOf(winner)
502505
currentDrawIsRinshan = false
503506
pendingAbortiveDraw = null

0 commit comments

Comments
 (0)