NAME
Game::RoyalUr::Engine - the board of the Royal Game of Ur: cells, pieces, hands, routes
VERSION
Version 0.01
SYNOPSIS
use Game::RoyalUr::Engine ':all';
my $board = Game::RoyalUr::Engine->new;
print $board->to_string, "\n"; # 4xx2/8/4xx2 l 7 0 7 0
my $d2 = Game::RoyalUr::Engine->cell_of(3, 1);
print "a rosette\n" if Game::RoyalUr::Engine->is_rosette($d2);
my $first = Game::RoyalUr::Engine->route_cell(ROUTE_SHORT, SIDE_LIGHT, 1);
$board->put($first, LIGHT)->set_hand(SIDE_LIGHT, 6);
my ($other, $err) = Game::RoyalUr::Engine->of_string('4xx2/3d4/4xx2 l 7 0 6 0');
DESCRIPTION
A position: twenty squares, what stands on each, how many pieces each side has still to enter and how many it has brought home, and whose move it is. And the two routes a piece may be made to follow round the board.
put, lift, set_hand and set_home judge nothing: put will place eight light pieces on the board if asked, and set_hand will give a side a hand that does not add up. moves is where the rules of movement live, and apply, forfeit and status are where a move is made and a game ends.
A position holds no roll. The dice are not part of the board.
Most callers want Game::RoyalUr. This class is for building positions and asking what is on them.
The board
Three rows of eight squares with four missing, so that a block of twelve is joined to a block of six by a bridge of two:
3 a3 b3 c3 d3 . . g3 h3
2 a2 b2 c2 d2 e2 f2 g2 h2
1 a1 b1 c1 d1 . . g1 h1
Files run 0 to 7 for a to h and rows 0 to 2 for 1 to 3. Row 1 is light's row and row 3 is dark's. Five squares are rosettes: a1, g1, a3, g3 and d2.
A cell is an opaque number
A cell is a number handed out by cell_of and taken apart by file_of and row_of. Nothing should assume which number is which square, or any relationship between two of them.
A route, and a step
A route is the order in which a side's pieces visit cells. A step is a place on it, counted from 1. Step 0 is the hand, where pieces wait to enter, and the step after the last is home; neither is a cell.
There are two routes. Light's are below, and dark's are the same with rows 1 and 3 exchanged.
ROUTE_SHORT d1 c1 b1 a1 a2 b2 c2 d2 e2 f2 g2 h2 h1 g1
ROUTE_LONG d1 c1 b1 a1 a2 b2 c2 d2 e2 f2 g2 g3 h3 h2 h1 g1
On the short route the two sides share the eight squares of the middle row and nothing else. On the long route they share twelve: each side's last five steps run round the far end of the board through the other side's row.
The same cell can be a different step for each side. On the long route g3 is light's step 12 and dark's step 16. route_step takes the side for that reason.
The key is a hex string, never a number
key_hex is twelve lowercase hexadecimal characters. It is a string on every perl, because a perl built with 32-bit integers cannot hold the number it stands for.
The key is the cells, the two homes and the side to move, exactly: two positions have the same key if and only if they agree on all of those. The hands are not part of it. In a position where each side's pieces add up, the hands follow from the rest.
Every board is its own board
clone returns a board that shares nothing with the one it came from. A board is released when the last reference to it goes away.
A RULE SET
Methods that apply a rule take a rule set as their last argument. Leave it out, or pass undef, for the standard game.
A rule set is the name of one, 'finkel' or 'masters', or a hash reference naming what differs from 'finkel':
my @moves = $board->moves(3, { route => 'long', safe_rosettes => 0 });
route-
'short'or'long'. dice-
3 or 4.
zero_rolls-
0 or 4: what a throw with no die marked is worth.
safe_rosettes-
True when a piece standing on a rosette cannot be captured.
pieces-
How many a side has, 1 to 7.
'finkel' is the short route, four dice, nothing marked worth nothing, safe rosettes and seven pieces. 'masters' is the long route, three dice, nothing marked worth four, rosettes that are not safe, and seven pieces.
A key that is not one of the five, a name that is not one of the two, and a value a field may not hold all croak, so that a misspelt rule cannot quietly play the standard game.
How a piece moves
A piece moves forward along its side's route by exactly the roll. Entering is a move from the hand to the step the roll names, and leaving is a move from the board to home, which needs the exact roll: a piece that would overshoot does not move.
A piece may not land on a piece of its own side. It may land on an enemy piece, capturing it, unless the enemy stands on a rosette and rosettes are safe. Nothing in between matters: a piece passes over any piece of either side.
CONSTANTS
Exported on request, or all at once with :all.
EMPTY,LIGHT,DARK-
What
atreturns for a cell. SIDE_LIGHT,SIDE_DARK-
The two sides.
ROUTE_SHORT,ROUTE_LONG-
The two routes, of fourteen and sixteen steps.
FILES,ROWS,CELLS,PIECES_MAX-
8, 3, 20 and 7.
MOVES_MAX,ROLL_MAX-
7, the most moves any roll allows, and 4, the most a roll is worth.
WIN,DEPTH_MAX-
1,000,000, the value of a game seen to be won, and 32, the deepest a search goes.
ONGOING,WON,DRAWN-
What
statusanswers. BY_HOME,BY_PLY_CAP-
What
howanswers for a game that is over: a side brought its last piece home, or the game ran to the ply cap. POS_OK,POS_NULL,POS_ROWS,POS_WIDTH,POS_LETTER,POS_GAP,POS_X,POS_SIDE,POS_COUNT,POS_FIELD,POS_LONG-
Why a position string was refused: it was not; no string; not three rows; a row that is not eight wide; a character that means nothing; a piece or a run of empty squares on a square that does not exist; an
xon a square that does; a side to move that is notlord; a hand or a home that is not a single digit from 0 to 7; a field missing, or something after the last one; a string too long to be a position.
FUNCTIONS
Exported on request.
other
my $side = other(SIDE_LIGHT); # SIDE_DARK
The other side.
piece_of
my $piece = piece_of(SIDE_DARK); # DARK
What at returns for a cell holding that side's piece.
METHODS
new
my $board = Game::RoyalUr::Engine->new;
my $board = Game::RoyalUr::Engine->new(pieces => 5);
my $board = Game::RoyalUr::Engine->new(position => '4xx2/8/4xx2 d 7 0 7 0');
An empty board with light to move and seven pieces in each hand; the same with another number of pieces, from 0 to 7; or the position a string describes. Croaks on a string it will not take, and on a number of pieces outside that range. Use of_string to get a string's reason back instead.
of_string
my ($board, $err) = Game::RoyalUr::Engine->of_string($string);
A board and POS_OK, or undef and one of the POS_* codes. A string is refused for its shape and never for its sense: nine pieces of one side, or a hand that does not add up, both load.
to_string
4xx2/8/4xx2 l 7 0 7 0
Three rows from row 3 down to row 1 with a / between them, a digit for a run of empty squares, and an x for each square that does not exist. l is a light piece and d a dark one. Then, each after a space: the side to move, l or d; light's hand; light's home; dark's hand; dark's home.
Every field is always written, and of_string requires every one.
clone
A second board in the same position, sharing nothing with the first.
at
my $piece = $board->at($cell);
EMPTY, LIGHT or DARK, or -1 for a number that is not a cell.
put
$board->put($cell, LIGHT);
Stands a piece on a cell, replacing whatever was there, and returns the board. It asks nothing and changes neither hand. A value that is not EMPTY, LIGHT or DARK, or a number that is not a cell, is ignored.
lift
$board->lift($cell);
Empties a cell and returns the board. It changes neither hand.
hand
my $waiting = $board->hand(SIDE_LIGHT);
How many of a side's pieces have not yet entered the board.
set_hand
$board->set_hand(SIDE_LIGHT, 6);
Sets it, and returns the board. A number outside 0 to 7 is ignored.
home
my $finished = $board->home(SIDE_DARK);
How many of a side's pieces have left the board at the end of their route.
set_home
$board->set_home(SIDE_DARK, 2);
Sets it, and returns the board. A number outside 0 to 7 is ignored.
side
The side to move, SIDE_LIGHT or SIDE_DARK.
set_side
$board->set_side(SIDE_DARK);
Sets it, and returns the board. Anything else is ignored.
count
my $on_board = $board->count(SIDE_LIGHT);
How many of a side's pieces stand on the board.
consistent
$board->consistent(7) or die;
True when, for each side, the pieces in hand, on the board and at home add up to that number.
key_hex
Twelve hexadecimal characters that stand for the position. See "The key is a hex string, never a number".
cell_of
my $cell = Game::RoyalUr::Engine->cell_of($file, $row);
The cell at a file (0 to 7) and a row (0 to 2), or -1 where there is no square: outside the board, and at e1, f1, e3 and f3.
file_of
The file of a cell, 0 to 7, or -1 for a number that is not a cell.
row_of
The row of a cell, 0 to 2, or -1 for a number that is not a cell.
is_rosette
True for the five cells that are rosettes.
all_cells
The twenty cells, as a list.
route_len
my $steps = Game::RoyalUr::Engine->route_len(ROUTE_LONG); # 16
How many steps a route has, or -1 for a number that is not a route.
route_cell
my $cell = Game::RoyalUr::Engine->route_cell($route, $side, $step);
The cell at a step of a side's route, or -1 when the step is not on the board: step 0 is the hand, and the step after the last is home.
route_step
my $step = Game::RoyalUr::Engine->route_step($route, $side, $cell);
The step a cell is for that side, or 0 when that side's route does not visit it.
route_shared
my $both = Game::RoyalUr::Engine->route_shared($route, $cell);
True when the cell is on both sides' routes, which is where a piece can be captured. Eight cells on the short route and twelve on the long.
chances
my @chances = Game::RoyalUr::Engine->chances($dice, $zero_rolls);
The rolls that $dice dice can make, when a throw with nothing marked is worth $zero_rolls steps, in ascending order. Each is a reference to [$roll, $weight, $denominator]: the roll comes up $weight times in $denominator throws. With four dice and nothing marked worth nothing:
[0, 1, 16], [1, 4, 16], [2, 6, 16], [3, 4, 16], [4, 1, 16]
An empty list when $dice is not 3 or 4, or $zero_rolls is not 0 or 4.
No die is thrown here. For the dice themselves see Game::RoyalUr::Dice.
cell_name
my $name = Game::RoyalUr::Engine->cell_name($cell); # 'd2'
A cell's name, a file letter and a row digit, or undef for a number that is not a cell.
rules
my $rules = Game::RoyalUr::Engine->rules('masters');
The rule set a value stands for, spelled out as a hash reference with all five fields. See "A RULE SET".
moves
my @moves = $board->moves($roll);
my @moves = $board->moves($roll, 'masters');
The moves a roll allows the side to move, as Game::RoyalUr::Move objects: a piece entering from the hand first, if one can, and then the side's pieces in the order they stand along its route. In scalar context, how many.
A roll of 0 allows none. Croaks on a roll that is not a whole number from 0 to 4, and on a rule set it does not understand.
The moves are described and not made. The board is as it was.
count_positions
my $n = Game::RoyalUr::Engine->count_positions('finkel'); # '275827872'
How many positions a rule set has: every way of standing each side's pieces on its own route, with the rest in hand or at home, for either side to move. It is returned as a string of digits, because on some perls the number is too large to be anything else, and it takes about a second to count for seven pieces.
apply
my $undo = $board->apply($move);
my $undo = $board->apply($move, 'masters');
Makes a move: one of the Game::RoyalUr::Move objects moves returned. The piece leaves where it stood, an enemy piece on the square it lands on goes back to its owner's hand, the count of plies goes up by one, and the turn passes to the other side, unless the piece landed on a rosette, in which case the same side is to move again. A piece that goes home passes the turn.
Returns something to hand to unapply, or undef when the move was not made because the piece it names is not there.
It does not ask whether the move is legal. Hand it a move moves made for this position and this roll.
forfeit
my $undo = $board->forfeit;
A turn lost to the roll. The count of plies goes up by one and the turn passes to the other side, always. Returns something to hand to unapply.
unapply
$board->unapply($undo);
Takes back what apply or forfeit did, exactly: the squares, both hands, both homes, the side to move and the count of plies. Returns the board. Croaks on anything that did not come from one of those two.
Take moves back in the reverse of the order they were made.
ply
How many moves and forfeits have been made to reach this position. It is not part of the position: a board made from a string starts at 0, and two boards that differ only in this have the same key.
set_ply
$board->set_ply(40);
Sets it, and returns the board. A negative number is ignored.
ply_cap
The number of plies at which a game is drawn.
status
my $status = $board->status($rules);
ONGOING, WON or DRAWN. A side has won when every one of its pieces is home. A game is drawn when ply reaches ply_cap, which exists so that a game cannot go on for ever and is not expected to be reached by play.
how
BY_HOME or BY_PLY_CAP for a game that is over, and 0 for one that is not.
winner
SIDE_LIGHT or SIDE_DARK, or -1 when nobody has won.
walk
my $n = $board->walk($depth, $rules);
Plays every roll the rule set's dice can make, and every move each allows, to that many plies, and counts the positions at the end. A roll that allows nothing is a forfeit and counts as one branch; a finished game is an end. Each roll counts once, however likely it is. The board is left as it was.
The count is returned as a string of digits. It is for holding one implementation of the rules against another.
evaluate
my $value = $board->evaluate(SIDE_LIGHT);
my $value = $board->evaluate(SIDE_DARK, 'masters', { exposed => 16 });
What the position is worth to a side, in sixteenths of a step, and it is worth the opposite to the other side.
Plainly, it is progress: every step each of the side's pieces has taken, a piece at home counting one more than the route is long and a piece in hand counting nothing, less the same for the other side.
A third argument adds up to three more terms, as a hash reference of weights:
exposed-
Held against a side: what it stands to lose to the other side's next roll, which is, for each of its pieces an enemy piece could land on, the chance of the roll that does it times the steps the piece would be sent back. The weight is in sixteenths, so 16 holds all of it against the side.
rosette-
For each of a side's pieces standing on a rosette that both sides visit.
entry-
Against each of a side's pieces still in hand.
A weight that is not one of the three croaks.
greedy
my $index = $board->greedy($roll, $rules);
The move to make without looking ahead, as an index into what moves returns for the same roll: one that captures if any does, else one that lands on a rosette if any does, else the piece furthest along. -1 when the roll allows nothing.
search
my $found = $board->search($roll, depth => 3);
my $found = $board->search($roll, rules => 'masters', budget => 50_000,
weights => { exposed => 16 });
Looks ahead through the dice and returns the best move for the side to move. The board is as it was.
Every roll the dice can make is weighed by how likely it is; the side to move is taken to choose what is best for it and the other side what is worst; and a roll that allows nothing loses the turn like any other. A level is one roll and one move, or one roll and a lost turn.
depth-
How many levels to look. 0, the default, for no limit but the budget.
budget-
How many positions the search may visit. It stops when they are spent and answers from the deepest level it finished. The first level always finishes. A search is bounded in positions and never in seconds, so the same board, roll and options give the same answer on every machine.
rules,weights-
A rule set, and the weights of the evaluation, as
evaluatetakes them.
Returns a hash reference:
{ index => 1, depth => 3, value => 812, nodes => '3391', stopped => 0 }
index is the move, as an index into what moves returns for the same roll, or -1 when the roll allows nothing or the game is over. Moves of equal value go to the last of them, the piece furthest along. depth is the deepest level that finished, value what the move is worth to the side to move in sixteenths of a step, nodes how many positions were visited, as a string of digits, and stopped is true when the budget ran out part way through a level.
A value near WIN or its negative is a game seen to be won or lost.
Croaks on a roll, a budget, a depth or an option it does not understand.
abi_version
The version of the C interface this build provides.
live
How many boards exist that have not yet been released. For tests.
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 (GPL Compatible)