NAME

Game::Senet - Senet, the Egyptian game of passing

VERSION

Version 0.02

SYNOPSIS

use Game::Senet;

my $game = Game::Senet->new(seed => 'any bytes at all');
while (!$game->is_over) {
    printf "%s threw %s, worth %d\n", $game->side, join('', @{ $game->sticks }), $game->roll;
    my @moves = $game->legal;
    $game->play($moves[0]);
}
print $game->result->winner, " won\n";

my $skyruk = Game::Senet->new(seed => 'other bytes', rules => 'skyruk');
$skyruk->play('10-12') or print $skyruk->error->message, "\n";

DESCRIPTION

A game of Senet: thirty houses in three rows of ten, five pieces a side, four throwing sticks, and one S-shaped track both sides share from the first throw. Light stands on the even houses of the first row and dark on the odd, and light throws first.

The sticks are thrown from a seed: the same seed gives the same game of throws every time, and a game can be recorded and replayed exactly. Nothing here reads a clock or a random number generator.

The rules are after Timothy Kendall's reconstruction of 1978, as modern sets play it (kendall, the default), or the reconstruction published with an open-source implementation of the game (skyruk). Nobody knows the original rules. See Game::Senet::Variant.

It is the engine behind the Senet at https://peer2peergames.com.

A throw is not a roll

The four sticks land light or dark face up. The throw is how many show light, 0 to 4; the roll is what that is worth in houses, 1 to 5: a throw of none is a roll of 5. sticks gives the sticks, throw the count, roll the houses.

A turn is one throw or several

A roll of 1, 4 or 5 gives the side another throw, whatever was done with it, even nothing. A 2 or 3 passes the sticks. Under skyruk a piece that reaches house 26 is thrown for again, alone. So turns do not alternate, and side says whose throw it is.

The game moves on by itself when it must

When a throw allows no move at all, the game records a forfeit and throws again for whoever is next. A caller never sees a throw with nothing to play: between calls the game is either over or legal holds at least one move. A throw with one legal move waits for play like any other.

The board

The houses are numbered along the track, which runs as an S: left to right along the top row, right to left along the middle, left to right along the bottom, and off the board after house 30.

 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   >  off
                         X   ~ III  II   I

The marked houses, with the sign the terminal draws for each:

15  +    the House of Rebirth, where a piece from the water comes back
26  X    the House of Happiness, which every piece must stand on
27  ~    the House of Water
28  III  the House of Three Truths, left on a roll of exactly 3
29  II   the House of Re-Atoum, left on a roll of exactly 2
30  I    the House of Horus

The sticks

light faces up (the throw)    0    1    2    3    4
houses moved (the roll)       5    1    2    3    4
chance, in sixteenths         1    4    6    4    1
throws again                 yes  yes   no   no  yes

Moving

A piece moves forward by the roll. It may not land on a piece of its own side. Landing on a piece of the other side is a swap: the two pieces exchange houses. Wikipedia's account of Kendall's rules sends the piece landed on backward to the first empty house instead; the sets played here swap, as the other accounts do. A piece is protected, and cannot be swapped, when it stands in a run of its own side, and a run long enough is a blockade no piece of the other side may pass. When no piece can move forward, a piece must move backward if one can. No piece passes house 26 without standing on it.

The two rule sets

                     kendall                  skyruk
protected            every piece of a run     the first and the last
                     of two or more           of a run of two or more
blockade             three in a row           two in a row
reaching 26          on the exact roll        from 25, on any roll
on reaching 26       nothing more             one more throw, for that
                                              piece alone
27, the water        back to 15 at once       stood on, and left by a
                                              throw: 1 to 3 back to 15,
                                              4 off, 5 stays
30                   off on a roll of 1       off on any roll

Both start with five pieces a side, and pieces => 7 gives seven. Game::Senet::Variant has every field.

kendall is after Timothy Kendall's reconstruction (1978), as modern sets play it. Kendall's own text is not quoted anywhere these rules were taken from, and the accounts of it differ; where they do, the set follows the one complete account. skyruk is a reconstruction published with an open-source implementation. A third account, called masters after the site that gives it, was not chosen as the second set, because it leaves out what happens backward, in a run, and beyond house 26.

The words

A piece stands on a house. The two sides are light and dark. The sticks are thrown; the throw is how many fell light face up and the roll what it is worth. A swap exchanges two pieces; a run is houses held next to each other by one side; a piece leaving the board bears off and is home. A throw that allows no move is a forfeit.

Sources

The rules were taken on 17 Oct 2026 from: Wikipedia's article "Senet" (revision 1375538342); the rules page of Masters Traditional Games; Catherine Soubeyrand's page at The Game Cabinet, which gives Kendall's rules as Lhote summarised them (1994); the README of the gym-senet project, which gives both sets as a specification; and a Tabletop Simulator workshop item based on Kendall's rules. Nobody knows how Senet was played when it was played.

CONSTRUCTOR

new

my $game = Game::Senet->new(seed => $bytes, %options);

seed is any non-empty string of bytes. The options:

rules

A rule set: a name (kendall or skyruk), a hash of fields, or a Game::Senet::Variant. kendall when not given.

first

light (the default) or dark: which side throws first. No source has dark first; it is there for a caller who wants it.

light

p1 (the default) or p2: the seat that plays light. See "side_of".

position

A position string to start from instead of the start, with as many pieces a side as the rules. Its side to move is the one who throws first unless first is given.

script

The throws themselves, in place of a seed: a reference to an array of rolls, or of { roll, sticks }. For replaying a record with no seed.

A key that is not one of these is ignored in silence: rulez plays kendall. A game with no seed and no script croaks.

METHODS

side

light or dark: whose throw it is. undef once the game is over.

sticks

The four sticks in hand, each 1 for a light face, as an array reference.

throw

How many sticks show light, 0 to 4.

roll

What the throw is worth in houses, 1 to 5.

The moves the throw allows, as Game::Senet::Moves. Never empty while the game goes on.

play

$game->play('12-15') or die $game->error->message;
$game->play($move);

Plays a move: a wire string (see Game::Senet::Notation) or a move legal returned. A move is played if and only if legal holds it. Returns the move, or undef with error saying why not, having changed nothing.

play_or_die

As play, croaking with the reason.

error

The Game::Senet::Error of the last refusal, or undef.

undo

Takes back the last move, and every forfeit the game made after it, to the throw the move was made with. Or takes back a resignation. 0 when there is nothing to take back.

resign

$game->resign('dark');

Ends the game for a side, the side to move by default.

is_over

result

A Game::Senet::Result once the game is over, undef before.

turn

my $turn = $game->turn;    # { side, segment, bound, extra }

Whose turn it is, which throw of the turn this is (1 for the first), the house whose piece alone this throw may move or undef, and whether this throw gives another.

side_of

my $side = $game->side_of('p2');

The side a seat plays.

seat_of

my $seat = $game->seat_of('light');

The seat that plays a side.

variant

The rule set, a Game::Senet::Variant.

first

The side that threw first.

seed

The seed, as given.

throws

How many throws have been made, the one in hand included.

ply

Moves and forfeits made.

position

The position string.

key

The position's key: sixteen hex characters.

home

my $n = $game->home('light');

at

my $who = $game->at(15);    # 'light', 'dark' or undef

protected

in_blockade

print "safe from a swap\n"  if $game->protected(12);
print "nobody passes it\n"  if $game->in_blockade(12);

Whether the piece on a house cannot be swapped, because it stands in a run of its own side the rules protect, and whether it is part of a run the other side may not pass. Each is 0 for an empty house, so a page can draw both and know no rule. A house that is not 1 to 30 croaks.

rules_revision

my $rev = Game::Senet->rules_revision;    # 1

A number that goes up when a release changes what a game does: a different move allowed, refused or made, from the same seed. A game stored under one revision is a different game under another. A release that changes nothing a game does keeps the number, whatever its version.

chances

Each roll the sticks can make, with its throw, how many of the sixteen ways four sticks fall make it, and whether it gives another throw.

log

Every move and forfeit, in order, each a hash: ply, side, throw (its number), roll, sticks, move (the wire form, or undef for a forfeit), and again, whether the same side threw next.

forfeits_since

my @forfeits = $game->forfeits_since($ply);

The forfeits at or after a ply, from the log, so that a caller can say that a side threw and could not move.

to_record

The game as a record (see Game::Senet::Notation).

to_text

The same.

from_text

my $game = Game::Senet->from_text($text);

The game a record describes, or a croak saying where it disagreed.

replay

my ($game, $error) = Game::Senet->replay($record_or_text);
my ($game, $error) = Game::Senet->replay($record, rules => 'skyruk');

Plays a record again through play, throw by throw. Answers the game, or undef and an error naming the ply. With a seed every throw is thrown again and checked; a record the rules no longer accept is refused where they part.

AUTHOR

LNATION <email@lnation.org>

BUGS

Please report any bugs or feature requests to bug-game-senet at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Game-Senet. 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::Senet

You can also look for information at:

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)