NAME
Game::Dominoes - draw dominoes with All Fives scoring, as a reusable engine
VERSION
Version 0.01
SYNOPSIS
use Game::Dominoes;
my $game = Game::Dominoes->new(seed => $bytes, players => 2);
$game->turn; # the seat to play
$game->legal($game->turn); # what it may do, never just a draw
$game->play(1, '6-4@L');
$game->scores; # { 1 => 10, 2 => 0 }
$game->hand_count(2); # public; hand(2) is not
$game->status; # 'active' until somebody reaches the target
$game->result->stringify;
DESCRIPTION
Draw dominoes with All Fives (Muggins) scoring on a double six set, for two to four players.
The engine does no input and no output. It never prints, never reads a handle, never sleeps and never calls rand, so a game is a pure function of its seed and its moves and replays anywhere.
It is the engine behind the dominoes at https://peer2peergames.com.
A player's mistake is returned as a Game::Dominoes::Error, never thrown. die is reserved for programmer error: a seat that does not exist, a face outside 0 to 6, a variant nobody defined.
The rules, and the one source they come from
Pinned to Pagat's All Fives page, verified against it rather than against a summary of it. Dominoes has no single rule book and the sources contradict each other on nearly everything, so taking the most popular answer to each question separately would produce a ruleset no publication describes, and therefore one with no citable test vectors at all.
- Hand size
-
Nine, seven and five tiles at two, three and four players. The rest is the boneyard.
- The lead
-
The first player of the first hand is determined by lot, which here means from the seed, so it is reproducible and checkable afterwards. The lead may be any tile; this engine does not require the highest double, which is what most other sources say and is the branch not taken.
In later hands the seat that went out leads. After a blocked hand the lead is determined by lot again, because nobody went out.
- Drawing
-
A seat that cannot play must draw until it can or the boneyard is empty, and a seat holding a playable tile may not draw at all. A drawn playable tile goes straight on the table and the turn ends, so a drawn tile is not a choice and only its arm may be.
The boneyard is drawn to empty. A reserve of one or two tiles that may never be drawn is a listed variation, not the rule, and is available as
reserve. - The end of a hand
-
A seat goes out, or every seat is blocked in succession.
- A blocked hand
-
The lightest hand wins and scores as if it had gone out. On a tie for lightest at two or four seats nobody scores. At three seats the two tied seats split the third seat's pips between them, which is the oddest rule on the page and is implemented as written.
- The target
-
250 at two seats and 200 at three or four, and the game stops the moment it is reached: tiles still in hand are not played or counted.
The Muggins claim rule is left out on purpose
In the parlour game a player must announce a score and an opponent may call "Muggins!" to steal it. It is a rule about human inattention with no engine meaning, it has no agreed resolution when three opponents race to call it, and Pagat's All Fives page does not mention it at all. This engine scores automatically.
A forced turn is not a decision
legal never returns a list whose only member is a draw. A seat that cannot play draws until it can or the boneyard runs out, and the engine does that itself before handing the turn on, recording each draw and each pass in the history.
This matters most away from a table. On a correspondence site a turn limit is a day or three, and spending one on a move with exactly one outcome is how a thirty play hand becomes a three month game.
The log is the serialisation, not the position
A snapshot of the table carries neither the hands nor the boneyard order, and both decide the result. A game is its seed and its moves.
PROPERTIES
seed
$game->seed;
Thirty-two bytes. Everything random in the game comes from here, so a finished game can be checked move by move once the seed is revealed. While a game is running the seed must not be shown to a player.
players, variant, target, scale, reserve
Game::Dominoes->new(seed => $s, players => 3, target => 61, scale => 5);
players is 2, 3 or 4. variant is all_fives, draw or block.
target defaults to 250 at two seats and 200 at three or four. scale divides every score, for the cited variation that keeps score on a cribbage board to a target of 61. The scale and the target are a matched pair: 61 with the raw scale, or 250 with a divided one, is the likeliest silent bug here, so set both or neither.
reserve is how many tiles at the back of the boneyard may never be drawn, nought by default.
status, turn, hand_number, scores, layout, boneyard, history, result
$game->status; # 'active' | 'finished'
$game->turn; # the seat to play, undef when finished
$game->hand_number; # which deal, and the hand argument of order_for
$game->scores; # { seat => points }
hands
$game->hands; # { seat => Game::Dominoes::Hand }
Every seat's tiles, keyed by seat. This is the secret the whole game turns on. Read one seat's own hand through hand, ask how many tiles another seat holds through hand_count, and never put this in a view.
leader
$game->leader;
The seat that leads the current hand: whoever went out of the last one, or a seat chosen by lot from the seed when the last hand was blocked and nobody did.
forced_tile
$game->forced_tile;
The tile a forced draw has committed the seat on turn to playing, or undef when that seat is free to choose.
A seat that cannot play draws until it can, and the tile it stops on must be the tile it plays: "When a player draws a playable tile, it goes on the table immediately and the player's turn ends." Only the arm is still a choice, so legal narrows to that one tile while this is set.
passes
$game->passes;
How many seats have passed in a row. Reaching the seat count means every seat is blocked and the hand is over. Any play resets it.
deductions
$game->deductions; # { seat => { face => 1 } }
What each seat has shown it cannot hold.
A seat that passes holds no tile matching any end that was open at the time, and because a pass only happens once the boneyard is exhausted, no tile ever enters that hand again: it only shrinks. So the deduction holds for the rest of the hand rather than just for the moment it was made.
This is derived from the public event log and from nothing else. Anybody watching the game could build the same table, which is what makes it fair to put in a view and fair for Game::Dominoes::Bot to use. A new deal clears it.
A draw is deliberately not recorded as a deduction. A seat that drew was short of the ends open at that moment, but the tiles it held then are now mixed in with what it drew and nothing public says which are which, so treating the whole hand as constrained would be unsound.
view
my $view = $game->view($seat);
my $view = $game->view('spectator');
What one seat may see, as a plain hashref: its own tiles, the layout, the open ends, how many tiles every seat holds, the boneyard count, the scores, the deductions, and whose turn it is.
It never carries another seat's tiles, the boneyard's contents, or the seed while the game is running. spectator is a valid seat and sees everything except any hand.
It is built up from nothing rather than down from the whole game by deleting keys, because a view built by deletion leaks the next field somebody adds to the engine, and in this game what is hidden is the whole point.
Game::Dominoes::Bot is handed this and reads nothing else, which t/19-bot-blind.t proves rather than assumes.
FUNCTIONS
seats, hand_size
$game->seats; # 1 .. players
$game->hand_size; # 9, 7 or 5
hand
$game->hand($seat);
That seat's Game::Dominoes::Hand. Private to the seat.
hand_count
$game->hand_count($seat);
How many tiles a seat holds. Public, and a separate method from hand so that the difference is visible at the call site.
boneyard_count
$game->boneyard_count;
How many tiles are left undealt. Public: a player at the table can see them.
open_ends, count
$game->open_ends; # the faces a tile may be matched against
$game->count; # the open end total, for scoring
legal
my $moves = $game->legal($seat);
What that seat may do now, as an arrayref of { kind => 'play', tile, arm, points }, where points is what the play would score.
Empty unless it is that seat's turn and the game is running. Never a lone draw.
play
my $out = $game->play($seat, '6-4@L');
my $out = $game->play($seat, { tile => $tile, arm => 'L' });
Plays one tile. Accepts a notation string, a hashref, or a Game::Dominoes::Play. The arm may be left out when only one is possible.
Returns the Game::Dominoes::Play on success, or a Game::Dominoes::Error. Scoring, the end of a hand, the next deal and the end of the game all happen inside this call.
resign
$game->resign($seat);
Ends the game at once with that seat last. Returns the Game::Dominoes::Result.
places
$game->places; # { 1 => 2, 2 => 1 }
The finishing order from the scores. Places start at 1 and repeat on a tie, so two seats level on second means nobody on third.
to_text
$game->to_text;
The whole game so far in this distribution's notation.
SEE ALSO
Game::Dominoes::Layout, Game::Dominoes::Scoring, Game::Dominoes::Error, Game::Dominoes::Result.
Game::Cribbage and Game::Checkers, the other two engines in this shape.
AUTHOR
LNATION <email@lnation.org>
BUGS
Please report any bugs or feature requests to bug-game-dominoes at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Game-Dominoes. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
SUPPORT
You can find documentation for this module with the perldoc command.
perldoc Game::Dominoes
You can also look for information at:
RT: CPAN's request tracker (report bugs here)
Search CPAN
ACKNOWLEDGEMENTS
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 (GPL Compatible)