NAME

Game::Senet::Engine - the board of Senet: thirty houses, two sides, the named rule sets

VERSION

Version 0.02

SYNOPSIS

use Game::Senet::Engine ':all';

my $board = Game::Senet::Engine->new;
$board->put($_, $_ % 2 ? DARK : LIGHT) for 1 .. 10;
print $board->to_string, "\n";      # dldldldldl/........../.......... l 5
print $board->key_hex, "\n";        # 0000000000066666

my $h = Game::Senet::Engine->house_of(1, 0);    # 20
print "marked\n" if Game::Senet::Engine->is_marked(15);

my $kendall = Game::Senet::Engine->rules('kendall');
print "@{ $kendall->{exits}{28}{3} }\n";        # off

my ($other, $err) = Game::Senet::Engine->of_string('.........l/........../.......... d 5');

DESCRIPTION

A position: thirty houses, what stands on each, how many pieces a side has, and whose move it is. And the two named rule sets as data.

put and lift judge nothing: put will place nine light pieces in a row if asked. Nothing in this class knows how a piece moves.

A position holds no throw and no roll. The sticks are not part of the board.

Most callers want Game::Senet. This class is for building positions and asking what is on them.

The houses

A house is its number, 1 to 30. The track runs from house 1 to house 30 and is the same for both sides. The board is drawn as three rows of ten, the track a reversed S:

 1  2  3  4  5  6  7  8  9 10
20 19 18 17 16 15 14 13 12 11
21 22 23 24 25 26 27 28 29 30

The grid is for drawing. Houses 10 and 11 are next to each other on the track; houses 1 and 20 are not, though one stands above the other.

Six houses are marked: 15, and 26 to 30.

The home

A side's home is the number of its pieces that have borne off. It is never stored: it is pieces less the side's pieces on the board. A board built with more pieces of a side than pieces has a negative home and is not consistent.

CONSTRUCTORS

new

my $board = Game::Senet::Engine->new;
my $board = Game::Senet::Engine->new(pieces => 7);
my $board = Game::Senet::Engine->new(position => $string);

An empty board, light to move, ply 0, with pieces a side: a whole number from 1 to 7, 5 when not given. Or the position a string describes, which croaks if the string is refused. A board is dropped when its last reference goes.

of_string

my ($board, $err) = Game::Senet::Engine->of_string($string);

The board a position string describes, and POS_OK; or undef and the code saying why the string was refused. It never croaks.

clone

my $copy = $board->clone;

A board of its own with the same houses, side, pieces and ply.

THE POSITION STRING

dldldldldl/........../.......... l 5

The thirty houses in track order, three groups of ten with a slash between, l for a light piece, d for a dark one and . for an empty house; then the side to move, l or d; then pieces, one digit from 1 to 7. Every field is required. Character k of the thirty is house k, so the middle group runs from house 11 to house 20, left to right, which the board draws right to left.

A string is refused, with one of these codes, for: POS_NULL no string or an empty one, POS_GROUPS not three groups, POS_WIDTH a group not ten long, POS_LETTER a character that is not l, d or ., POS_SIDE a side that is not l or d, POS_PIECES a pieces that is not one digit from 1 to 7, POS_EXCESS a side with more pieces on the board than pieces, POS_FIELD a field missing or something after the last, POS_LONG 48 characters or more.

METHODS

at

my $piece = $board->at($house);

EMPTY, LIGHT or DARK; -1 for a number that is not a house.

put

$board->put($house, LIGHT);

Places a piece, or EMPTY, on a house, judging nothing. A value that is not one of the three, or a house that is not one, is ignored. Returns the board.

lift

$board->lift($house);

Empties a house. Returns the board.

side

SIDE_LIGHT or SIDE_DARK: whose move it is.

set_side

$board->set_side(SIDE_DARK);

Returns the board. Anything but the two sides is ignored.

pieces

How many pieces a side has, on the board and home together.

count

my $n = $board->count(SIDE_LIGHT);

A side's pieces on the board; -1 for a side that is not one.

home

my $n = $board->home(SIDE_DARK);

pieces less count: a side's pieces that have borne off.

consistent

True when neither side has more pieces on the board than pieces.

ply

Moves and forfeits made to reach the position. It is not part of the key or of the position string, and a board read from a string is at ply 0.

set_ply

$board->set_ply(12);

A negative number is ignored. Returns the board.

moves

my @moves = $board->moves($roll, $rules);
my @moves = $board->moves($roll, $rules, $only);
my $count = $board->moves($roll, $rules);

The moves a roll of 1 to 5 allows the side to move under a rule set (a name, a hash, or undef for kendall; see "rules"), as Game::Senet::Moves in ascending order of the house moved from. In scalar context, how many.

A backward move is listed only when the rule set allows one and no forward move exists, so a list is all forward or all backward. $only, a house, lets the piece on that house alone move. A piece standing on house 26 or beyond is not moved by this method.

Croaks on a roll that is not 1 to 5: a throw of nothing is a roll of 5, and 0 is never a roll.

apply

my ($undo, $bound) = $board->apply($move, $rules);

Makes a move of the side to move: the piece leaves from, an enemy standing on to goes to from, and the piece lands, or bears off. The ply goes up by one, and the side to move follows the roll: the same side after a 1, 4 or 5, the other after a 2 or 3, and no change after the move that wins.

$bound is the house the next throw is bound to, or 0: under a rule set with throw_on_26, a piece arriving on 26 is thrown for again, alone, and $bound is then 26 with the same side to move. $undo is what "unapply" takes.

It does not ask whether the move is legal, only that a piece of the side to move stands on from; when none does, it returns nothing and changes nothing.

forfeit

my $undo = $board->forfeit($roll);

A throw that allows nothing. The ply goes up by one, and the side to move follows the roll as it does after a move: a forfeit on a 1, 4 or 5 keeps the turn. Croaks on a roll that is not 1 to 5.

unapply

$board->unapply($undo);

Takes back what "apply" or "forfeit" did, exactly: the houses, the side and the ply. Returns the board.

status

ONGOING, WON when a side has no piece left on the board, DRAWN at the ply cap.

how

0 while the game goes on, BY_HOME or BY_PLY_CAP.

winner

SIDE_LIGHT or SIDE_DARK, or -1.

ply_cap

The ply at which a game is drawn: swaps and the water send pieces back, so nothing else bounds a game.

walk

my $leaves = $board->walk($depth, $rules);

Every roll and every move each allows, to $depth plies, counting the positions at the end, as a string of digits. A roll that allows nothing is one branch; a finished game is an end. For comparing one generator with another.

evaluate

my $v = $board->evaluate(SIDE_LIGHT, $rules, \%weights);

The position valued for a side, in sixteenths of a house: the houses its pieces stand on, a piece at home counting 31, less the other side's, and the terms whose weight is given: blockade, own runs wide enough to block with an enemy piece behind them. ties chooses the earlier of two moves of equal value.

protected

print "safe from a swap\n" if $board->protected(12, 'kendall');

1 when the piece on a house cannot be swapped under a rule set, because it stands in a run of its own side the set protects: every piece of a run of two or more under kendall, the first and the last of one under skyruk. 0 for an empty house or a piece standing alone. Whether the house itself is safe is the rule set's safe list. A house that is not 1 to 30 croaks.

in_blockade

print "part of a blockade\n" if $board->in_blockade(12, 'kendall');

1 when the piece on a house is part of a run of its own side long enough that the other side may not pass it: three under kendall, two under skyruk. 0 for an empty house, and always under a rule set with no blockade.

greedy

my $index = $board->greedy($roll, $rules, $only);

The move to make without looking ahead, as an index into what "moves" returns: a bear-off, else a swap, else a landing on 26, else the move whose piece ends furthest on. -1 when the roll allows nothing.

my $found = $board->search($roll, rules => $rules, weights => \%w,
                           budget => $nodes, depth => $throws, only => $house);

The best move for the side to move, by expectiminimax through the sticks, as { index, nodes, depth, value, stopped }. The budget is in nodes and the depth in throws; the first level always finishes. The same arguments give the same answer every time.

expect

my $v = $board->expect($depth, rules => $rules, side => SIDE_LIGHT);

The value of the position BEFORE its throw, for a side, a number of throws deep. For checking the search by hand.

to_string

The position string.

key_hex

The position as sixteen lowercase hex characters: house h at bits 2(h - 1) and 2(h - 1) + 1, and the side to move at bit 60. Equal positions have equal keys and different positions different ones. Neither pieces nor the ply is in it.

CLASS METHODS

house_of

my $house = Game::Senet::Engine->house_of($row, $col);

The house drawn at a row, 0 to 2 from the top, and a column, 0 to 9 from the left; -1 for a place that is not on the board.

row_of

my $row = Game::Senet::Engine->row_of($house);

The row a house is drawn in, or -1.

col_of

my $col = Game::Senet::Engine->col_of($house);

The column a house is drawn in, or -1.

is_marked

my $yes = Game::Senet::Engine->is_marked($house);

True for the six marked houses: 15 and 26 to 30.

all_houses

The thirty house numbers, 1 to 30.

marked_houses

The six marked house numbers, in order.

rules

my $rules = Game::Senet::Engine->rules('skyruk');
my $rules = Game::Senet::Engine->rules({ exits => 'kendall', protect => 'none' });

A rule set as a hash: pieces, blockade, protect (none, run or ends), backward, reach26 (exact or any), throw_on_26, safe (the safe houses, as a list), and exits, which maps each of houses 26 to 30 and each roll from 1 to 5 to its chain: a list of house numbers and the words off, walk and stay, an empty list where nothing is possible.

The argument is the name of a set, undef for kendall, or a hash of what differs from a set: exits names the set to start from, and each other key sets one field. An unknown name, an unknown key, or a value a field may not hold croaks.

rules_named

my $rules = Game::Senet::Engine->rules_named(RULES_SKYRUK);

The same hash for a set by number, or undef when there is no such set.

rules_id

my $n = Game::Senet::Engine->rules_id('kendall');    # RULES_KENDALL

The number of the set a name stands for, or -1.

chances

my @rows = Game::Senet::Engine->chances;    # [1, 4, 16], [2, 6, 16] ...

Each roll four sticks can make, in ascending order of ROLL, as [roll, weight, denominator]: how many of the sixteen equally likely ways the sticks can fall make it. These are the odds a search weighs. Nothing here throws the sticks; see Game::Senet::Sticks.

extra

my $again = Game::Senet::Engine->extra($roll);

1 when a roll gives another throw, 0 when it does not, -1 for a number that is not a roll. "extra_throw" in Game::Senet::Sticks answers the same question in Perl.

abi_version

The version of the C table this build carries. A consumer of the table asks for >= the version it was written against, never ==.

live

Boards made and not yet dropped. It exists for a leak test.

FUNCTIONS

Exported on request, with the constants, or all of them with :all.

other

my $them = other(SIDE_LIGHT);    # SIDE_DARK

piece_of

my $piece = piece_of(SIDE_DARK);    # DARK

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)