NAME
Game::Mahjong::Rules - the state of one game: the deal, the turns, the window, the sixteen hands
VERSION
Version 0.01
SYNOPSIS
my $g = Game::Mahjong::Rules->new(seed => $bytes);
my @events = $g->take_outcomes; # the deal, its flowers and replacements
my ($seat) = $g->waiting_on; # the dealer, holding fourteen
my @legal = $g->legal($seat); # discards, kongs, a win
my $r = $g->apply($seat, { kind => 'discard', tile => $kind });
$r->error and warn $r->code; # a refusal is an object, not an exception
@events = $g->take_outcomes; # the discard, and what the engine did next
DESCRIPTION
The rules of play under the competition rulebook, as a state machine over four Game::Mahjong::Hands, a Game::Mahjong::Wall and four discard pools. Seats are 0 to 3, the dealer is seat (hand_no - 1) % 4, play runs counterclockwise (seat 0, 1, 2, 3), and a seat's wind is its distance from the dealer. The engine prints nothing, reads nothing and never calls rand: everything that happens after a move is a function of the seed.
The turn
The seat on turn holds fourteen tiles' worth and is in the discard phase. It may discard, declare a kong (concealed with four in hand, or promoted with an exposed pung and the fourth, never in a turn it came to by a chow or pung claim, 3.6.8), or win on the fourteen it holds when the scorer says eight or more.
The draw is not a move
After a window closes with no claim the next seat draws from the front of the wall; after a kong, or when a drawn tile is a flower, the seat draws a replacement from the back. None of that is a choice, so the engine does it and reports it (drew, flower). A draw from an empty wall ends the hand exhausted; the last tile of the wall is drawable and its discard may be claimed (fans 44 and 45).
The window
A discard opens a window if any other seat can use it: win (its waits hold the tile and the fourteen would score eight), kong (three in hand), pung (a pair), or chow (the next seat only, with two tiles that run). Seats with nothing are never asked. Each asked seat answers once: pass, or one of its claims. A win beats a pung or kong, which beats a chow; among wins the nearest seat after the discarder (3.7.1, 3.6.7, 3.7.2.4). waiting_on is the asked seats whose answer could still change the outcome, so a lodged claim that nothing unanswered could beat closes the window at once, and a seat made moot by a better claim is not waited on. A promoted kong opens the same window for a robbing win (fan 47) and is performed only if nobody takes it.
The end of a hand
A win is scored by Game::Mahjong::Score with the context the rules know (how, the winds, the waits, the last tile, the replacement, the flowers), settled by Game::Mahjong::Result, and recorded in history; the deal passes whatever happened (3.4.8) and the sixteenth hand ends the game.
Refusals are objects
apply returns a Game::Mahjong::Error for a move the rules refuse and true for one they took. Only programmer error dies.
ATTRIBUTES
seed, hand_no, dealer, totals
The seed; the hand number, 1 to 16; the dealer's seat; the four running totals.
wall, hands, pools
The Game::Mahjong::Wall; the four Game::Mahjong::Hands; the four discard pools, each the kinds discarded in order.
phase, turn, window
discard, claim, rob or finished; the seat on turn in the discard phase, undef otherwise; the open window, { kind, tile, from, may => { seat => [claims] }, answers => { seat => 'pass' | { kind, tiles } } }.
drawn, drawn_from, replacement
The kind the seat on turn just drew, where from (wall or back), and whether it was a replacement after a kong or a flower.
claimed_this_turn, last_draw_emptied, last_discard_is_last
Whether the seat on turn came to it by a chow or pung (no kong this turn); whether the last draw emptied the wall; whether the last discard was the tile that did.
status, winner, result, history, last, moves
active or finished; the winning seat or undef; score or draw; one record per finished hand; the last thing that happened, for a view; a count of player moves.
outcomes
The queue take_outcomes drains.
position
A written position for tests: { hands => [four notations], wall => [kinds], turn => seat, pools => [...], dealer, claimed_this_turn, drawn }.
METHODS
take_outcomes
The events since the last call, cleared: { actor => 'sys' | seat, kind, ... }.
waiting_on
The seats whose action is awaited: one in the discard phase, the seats that still matter in a window, none when finished.
legal
my @moves = $g->legal($seat);
Every move the seat may make now: discard {tile}, kong {tile}, win, or in a window pass, chow {tiles}, pung, kong, win.
apply
my $r = $g->apply($seat, $move);
Takes the move or returns the refusal.
prevailing, round, seat_wind, next_seat, hand_of, pool_of, is_active
As named.
check_invariants
Strings naming what is broken, empty when sound: every tile somewhere exactly once, thirteen between turns, totals summing to zero, one seat waited on outside a window, a window asking only seats with a claim.
to_string
The four hands and pools in notation, for a failing test.
SEE ALSO
Game::Mahjong::Score, Game::Mahjong::Result, Game::Mahjong::Hand
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under the Artistic License 2.0.