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.

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.

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)