NAME
Game::Sternhalma::Engine - the sternhalma board: holes, points, seats and the key
SYNOPSIS
use Game::Sternhalma::Engine qw(:all);
my $board = Game::Sternhalma::Engine->new(seats => 3);
print $board->to_string, "\n";
print $board->key, "\n";
my $top = 0;
my @beside = Game::Sternhalma::Engine->neighbours($top);
my ($copy, $code) = Game::Sternhalma::Engine->from_string($board->to_string);
DESCRIPTION
The board of star halma and nothing that judges a move: the 121 holes, which are neighbours, the six points, the hexagon and its ring, the four hop classes, which points a seat count starts in, whose turn it is, a key that names the position, and the position written as a string.
Holes are numbered 0 to 120 in reading order, the top tip first and then each row from the top, left to right. A hole also has cube coordinates (x, y, z) with x + y + z == 0: y is the row, from -8 at the top, and x grows to the right along a row.
The six points are numbered 0 to 5 clockwise from the top: top, top right, bottom right, bottom, bottom left, top left. A point's opposite is three round. A hole in the middle hexagon is in CENTRE.
The six directions are E, NE, NW, W, SW and SE, numbered 0 to 5, and a direction's opposite is three round.
Seats are numbered from 0, and seat 0 moves first. Two seats start at the top and the bottom; three at the top, the bottom right and the bottom left; four at the top, the top right, the bottom and the bottom left; six everywhere.
CONSTRUCTORS
new
Game::Sternhalma::Engine->new(seats => 2, pieces => 10)
Game::Sternhalma::Engine->new(seats => 4, empty => 1)
Game::Sternhalma::Engine->new(position => $string)
A board with every seat in its start and seat 0 to move. seats is 2, 3, 4 or 6 (default 2), or 1 for one seat crossing alone. pieces is 10 (the default), or 15 for one, two or three seats, where no two starting points are neighbours. With empty, every hole is empty. With position, the board that string writes. Croaks when it cannot build what it was asked for.
from_string
my ($board, $code) = Game::Sternhalma::Engine->from_string($string);
The board a position string writes, and POS_OK; or undef and the code that says why the string was refused (see "reason").
clone
A board of its own with the same position.
THE POSITION
to_string
The position as a string: the seventeen rows from the top, / between them; in a row, a digit 0 to 5 is that seat's piece and [n] is a run of n empty holes. Then a space, the seat to move, a space, the seat count, a space, the piece count, and once the game has moved on, a ; and the game (see "Positions" in Game::Sternhalma::Notation). The two-seat set-up is
0/00/000/0000/[13]/[12]/[11]/[10]/[9]/[10]/[11]/[12]/[13]/1111/111/11/1 0 2 10
reason
Game::Sternhalma::Engine->reason($code)
The refusal code from "from_string" in words. The codes, which are exportable, are POS_OK, POS_NULL, POS_LONG, POS_LETTER, POS_ROWS, POS_WIDTH, POS_FIELDS, POS_SEAT, POS_COUNT and POS_GAME, and a string is checked in that order.
key
The position's key as sixteen hex digits. It names the pieces, the seat to move and the seat count, and two boards with the same key are the same position.
at
The seat whose piece is in a hole, or undef for an empty hole or a number that is not a hole.
put
$board->put($hole, $seat)
Puts a piece of $seat in $hole, replacing what was there. It judges nothing: the counts and the key follow, and a hole or seat the board does not have is ignored. Returns the board.
lift
$board->lift($hole)
Empties a hole. Returns the board.
to_move
The seat whose turn it is.
set_to_move
Makes it a seat's turn, and is ignored for a seat the board does not have. Returns the board.
seats
How many seats the board has.
pieces
How many pieces each seat has.
count
$board->count($seat)
How many of a seat's pieces are on the board.
seat_point
$board->seat_point($seat)
The point that seat starts in.
ONE STEP AND ONE HOP
These are the atoms a move is made of. None of them asks whose turn it is.
steps
my @to = $board->steps($hole);
The empty holes next to a hole, in direction order.
hops
my @to = $board->hops($hole);
my @to = $board->hops($hole, $from);
Where one hop from a hole lands: over one neighbouring piece, of any seat, into the empty hole straight beyond it, in direction order. Nothing is taken.
$from is the hole the moving piece left, for a piece in the middle of a chain of hops: it counts as empty, so a hop may land on it and cannot go over it, even while the board still shows the piece there. It defaults to $hole; -1 means the piece came from nowhere on the board.
do_move
$board->do_move($from, $to)
Moves the piece on $from to the empty hole $to, wherever that is, and keeps the key. Returns 1, or 0 and does nothing when $from holds no piece or $to is not an empty hole. The seat to move stays as it was.
undo_move
$board->undo_move($from, $to)
Takes back the move from $from to $to: the piece on $to goes back to $from. Returns 1, or 0 and does nothing.
A MOVE
A move is a step, or a chain of hops by one piece that may stop after any hop. A step and a hop never go in the same move. Nothing is captured, so a move is no more than where it started and where it stopped.
reach
my @to = $board->reach($from);
Every hole the piece on $from can end its move on, in hole order: its steps, and every hole a chain of hops lands on, wherever the chain stops. Never $from itself. Empty when $from holds no piece.
path
my @holes = $board->path($from, $to);
The move from $from to $to as the holes it goes through, $from first and $to last: two holes for a step, and for a chain every hole it lands on. A hole a step reaches is always shown as the step. A chain is a shortest one, and among those always the same one. Empty when $to is not in the reach.
legal
for my $move ($board->legal) { my ($from, $to) = @$move; ... }
The moves of the seat to move, as array references of [$from, $to]: for each of its pieces in hole order, its reach in hole order.
A GAME
The board also keeps a game: whose turn it is, who is home, and how it ended. Only "every piece home" is the rule of the game. The rest are this distribution's own rules, because the rules as written say nothing:
A parked piece cannot stop you. A seat is home when every hole of its home holds a piece and at least one of those pieces is its own, so a rival who never moves a piece out of your home does not keep you from finishing.
A seat with no move passes, and when every seat still racing has passed in a row the game ends.
A game ends at its cap, a number of plies (moves and passes), set for now at 1,000 a seat.
A seat that resigns is placed behind every seat that finishes or is still racing, and its pieces stay on the board.
When a seat gets home it takes the next place and the others play on; with two seats the first home wins. A seat can be made home by a rival's move, when the rival's piece fills the last hole of its home. At the cap, or when nobody can move, the seats still racing are placed by "distance_left", equal distances sharing a place.
play
my ($why, $undo) = $board->play($from, $to);
The seat to move moves its piece from $from to $to, which must be in the piece's reach. $why is PLAY_OK, and then $undo takes it back with "unplay"; or it says why not, and nothing changed: WHY_OVER, WHY_NOT_A_HOLE, WHY_NO_PIECE, WHY_NOT_YOURS or WHY_UNREACHABLE.
pass
my ($why, $undo) = $board->pass;
The seat to move passes. Refused with WHY_HAS_MOVE unless it has no move.
resign
my ($why, $undo) = $board->resign($seat);
A seat still racing resigns, on its turn or not. Refused with WHY_NOT_A_SEAT or WHY_SEAT_DONE (home already, or resigned).
unplay
$board->unplay($undo)
Takes back a play, pass or resign. Undo them last first. Returns 1, or 0 when it cannot.
can_pass
True when the seat to move has no move and the game is not over.
home_complete
$board->home_complete($seat)
The seat's home is full, at least one piece in it its own.
finished
$board->finished($seat)
The seat has been counted home in this game.
place_of
$board->place_of($seat)
The seat's place, 1 the best; 0 while the game runs and the seat is not home.
places
The place of every seat, in seat order.
distance_left
$board->distance_left($seat)
How far the seat's pieces are from home: 0 for a home full of its own pieces, more for every piece out of place.
over
True when the game has ended.
how
How it ended: ONGOING, BY_HOME, BY_CAP, BY_DEADLOCK or BY_RESIGN.
ply
Moves and passes made.
cap
The ply the game stops at.
set_cap
$board->set_cap($plies)
Sets the cap. Returns the board.
NAMES AND MOVES AS TEXT
The notation is Game::Sternhalma::Notation's.
name
$board->name($hole)
A hole's name, a1 to q1.
hole_named
$board->hole_named('i5')
The hole a name names.
move_text
$board->move_text($from, $to)
The move as text, by the path it is shown with: e5-f5 for a step, e5:g5:g7 for a chain. undef when it is not a move.
read_move
my ($move, $why) = $board->read_move('e5-g7');
A move read from text against this board. from-to is accepted for any move, a chain included; a chain written out is accepted by any legal path between the same two holes, since it is the same move. The answer is { kind => 'move', from, to, path } with the path the move is shown by, or the pass or resignation as "parse_move" in Game::Sternhalma::Notation gives it; or undef and why not, naming the hop that failed.
CHOOSING A MOVE
weights_default
A hash of the four weights the evaluation adds: distance, straggler, classes and blocking.
evaluate
$board->evaluate($seat, weights => { classes => 0 })
How good the position is for a seat, higher better: a large constant (WIN_SCORE) for a seat home, otherwise minus the weighted sum of how far its pieces are from home, how far behind its rearmost piece is, how far its pieces' hop classes are from its home's, and how many of its pieces sit in the home in front of an empty hole deeper in.
search
my $r = $board->search(budget => 100_000, seed => 7, depth => 0, mode => SEARCH_AUTO, avoid => \@keys);
The move the seat to move should make: { from, to, pass, nodes, depth, score, stopped }, or undef when the game is over. budget is positions looked at, never seconds; the first ply is always looked at whole, and after that a search that runs out plays the best move of the last depth it finished. seed settles ties and nothing else. depth is the most plies to look ahead, 0 for as deep as the budget goes. mode is SEARCH_AUTO or SEARCH_PARANOID (every rival against the seat to move, which for two seats is the plain game) or SEARCH_MAXN (every seat for itself). weights replaces any of the defaults. avoid is the "own_key"s the seat to move's pieces have already had: a move that puts them back as they were is not made while there is another, without which searching seats can shuffle in their homes for ever.
own_key
$board->own_key($seat)
A key over one seat's pieces alone, sixteen hex digits: where they stand, and nothing of anyone else's.
random_forward
my $r = $board->random_forward($seed);
The weakest player: a seeded random choice among the moves that take a piece nearer home, or among all of them when none does.
THE STAR
These are the same for every board, and may be called on the class.
holes
121.
xy
my ($x, $y, $z) = Game::Sternhalma::Engine->xy($hole);
A hole's cube coordinates.
hole_at
Game::Sternhalma::Engine->hole_at($x, $y)
The hole at those coordinates, or undef.
row_of
The row of a hole, 0 at the top to 16 at the bottom.
col_of
Where a hole is in its row, from 0 at the left.
row_len
How many holes a row has.
row_start
The first hole of a row.
next_to
Game::Sternhalma::Engine->next_to($hole, $dir)
The neighbour of a hole in a direction, or undef.
neighbours
Every neighbour of a hole.
degree
How many neighbours a hole has: 6, 5, 4, or 2 at a tip.
dir_opposite
The opposite of a direction.
point_of
The point a hole belongs to, 0 to 5, or CENTRE.
on_ring
True for the 24 holes round the edge of the hexagon.
class_of
A hole's hop class, 0 to 3. A move of two holes in a line stays in its class.
point_holes
my @holes = Game::Sternhalma::Engine->point_holes($point);
The ten holes of a point, in hole order.
front
The five ring holes in front of a point, which a fifteen-piece start also fills.
pairs
Every pair of neighbouring holes once, as array references, the lower hole first. There are 312.
layout_ok
Game::Sternhalma::Engine->layout_ok($seats, $pieces)
True when the engine builds that many seats with that many pieces.
layout_point
Game::Sternhalma::Engine->layout_point($seats, $seat)
The point a seat starts in, for that many seats.
zobrist
zobrist_move
zobrist_seats
The numbers the key is made of, as hex: one for each seat's piece in each hole, one for each seat to move, and one for each seat count.
FOR TESTS
abi_version
The version of the board's C interface.
tables_ok
True when the star was built and every count agreed with what it should be.
live
How many boards exist and have not been let go.
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)