NAME

Game::Fanorona::Engine - the fanorona board: points, lines, stones, steps and whole moves

VERSION

Version 0.03

SYNOPSIS

use Game::Fanorona::Engine ':all';

my $board = Game::Fanorona::Engine->new;
print $board->to_string, "\n";
# bbbbbbbbb/bbbbbbbbb/bwbw1bwbw/wwwwwwwww/wwwwwwwww w

my $e3 = Game::Fanorona::Engine->square_of(4, 2);
print "the centre is empty\n" if $board->at($e3) == EMPTY;
print "and eight lines meet there\n" if $board->degree($e3) == 8;

my ($other, $err) = Game::Fanorona::Engine->of_string('bbb/w1b/www w');

DESCRIPTION

A position: the points of a board, what stands on each, and whose move it is; and the lines that join one point to the next.

put and lift judge nothing: put will fill the board with white stones if asked. steps and step_why are where the rules of a step live.

A step is one stone going to one adjacent point. A move is everything one side does before the other plays: a single step that takes nothing, or a chain of steps by one stone, each of which takes. play, stop and moves are the game's; do_step and steps are the single step on its own, which always passes the turn, and are kept beside them for checking one against the other.

Nothing here knows how a game ends.

The board

The game is played on forty-five points, nine files by five ranks. Files are 0 to 8 for a to i and ranks 0 to 4 for 1 to 5, White on the two lower ranks.

Every point is joined to the points beside it along its rank and its file. Half the points are also joined diagonally. Those are the strong points, the ones whose file and rank add to an even number; the rest are weak. The corner a1 is strong and so is the centre.

A diagonal always runs from a strong point to a strong point.

Other sizes

new also builds any board of odd width 3 to 9 and of height 3 or 5. Three have names: sivy is the nine by five, dimy the five by five and telo the three by three.

On every size the two lower ranks (one, on a board three high) are White's, the upper ones Black's, and the centre is empty. The middle rank alternates outward from the centre, with a white stone directly left of it and a black stone directly right. For the nine by five that is the traditional set-up. For the smaller boards it is this distribution's reading of the same pattern, not a set-up taken from a source.

A point is an opaque number

A point is a number handed out by square_of and taken apart by file_of and rank_of. It is not rank * 9 + file, and it is the same number on every size of board: a1 on the three by three is a1 on the nine by five. Ask on_board whether a point belongs to a particular board, and next_to for the point one step along a line.

A direction

Eight of them, numbered clockwise from north and exported as N, NE, E, SE, S, SW, W and NW. North is towards Black's side.

The key is a hex string, never a number

key_hex is sixteen 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.

Two positions on the same size of board with the same stones on the same points and the same side to move have the same key, in every process, on every machine. The same stones on two different sizes of board do not.

A step

A stone steps along a line to the next point, which must be empty. A step that takes nothing is a paika.

A step takes by approach when the point beyond the one it moves to, further along the same line, holds an enemy stone; and by withdrawal when the point behind the one it leaves does. Either way that stone goes, and with it every enemy stone in an unbroken line behind it, up to the first empty point, the first stone of the mover's own, or the edge of the board. Only stones on the line of the step are taken.

When a step could take both ways it takes one, and the other line stands. It is listed twice, once as each.

Taking is compulsory. If any stone of the side to move can take, none of its stones may make a paika. Which capture to make is the mover's choice: a larger one has no priority over a smaller.

A step that could take, takes. There is no moving to a point and declining the capture there, so a step is a paika only when it takes nothing either way. The sources this distribution follows say that capture is obligatory and do not spell this case out; it is the distribution's reading.

A step is a hash reference:

{
    from  => $sq,
    to    => $sq,
    dir   => NE,
    kind  => APPROACH,            # or WITHDRAWAL, or PAIKA
    taken => [ $sq, $sq ],        # nearest first; empty for a paika
}

and wherever a method takes a step it takes either that hash or the three values $from, $dir, $kind.

A whole move, and the chain

A stone that has taken may step again in the same move, and again, so long as every step takes. This is a chain. Three things limit it:

  • It is the same stone throughout.

  • It may not step in the direction of the step directly before. The rule looks back one step only: north, east, north is allowed.

  • It may not arrive on a point it has already stood on in this chain, and the point it started from counts.

The mover may stop after any step of a chain. Taking is compulsory to begin with, and going on is not. So where a stone could take three times running there are three moves: take once, take twice, take three times.

Nothing of a chain carries into the next move. A stone may begin its next move in the direction its last one ended in.

While a chain is in progress the board remembers it: in_chain, chain_at, chain_dir and chain_seen. The turn stays with the mover until the chain ends, which it does when stop is called or, by itself, when no step could follow. So a board with a chain open always has a step to make.

A chain is not part of the position. The key and the position string describe the stones and the side to move and nothing else; a string written in the middle of a chain reads back with no chain open.

How many whole moves there can be

Usually about ten. But each beginning of each chain is a move, so a stone in the middle of many enemies can have thousands: a position is known with four white stones and 15,898 moves. moves therefore takes a limit and croaks past it, and count_moves takes a cap. To offer a person a move, do not list the moves: offer play_steps, the handful of steps that may come next, and stop where can_stop says so.

The position string

bbbbbbbbb/bbbbbbbbb/bwbw1bwbw/wwwwwwwww/wwwwwwwww w

Ranks from the top down, w and b for stones, a digit for a run of empty points, / between ranks, a space, and the side to move. The size of the board is read from the string.

A string is refused for its shape and never for its sense: a board of forty-five white stones loads.

CONSTRUCTORS

new

my $board = Game::Fanorona::Engine->new;                    # the game
my $small = Game::Fanorona::Engine->new(board => 'telo');
my $other = Game::Fanorona::Engine->new(cols => 5, rows => 3);
my $bare  = Game::Fanorona::Engine->new(empty => 1);
my $from  = Game::Fanorona::Engine->new(position => $string);

The set-up of a board, White to move. board names a size, or cols and rows give one. empty leaves every point empty. position loads a position string and takes its size from it.

Croaks on a name that is not a board, a size that cannot be built, a name given together with a size, or a position string that will not load.

of_string

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

A board from a position string, and POS_OK; or undef and the reason:

POS_NULL

No string, or an empty one.

POS_LONG

Longer than any position string can be.

POS_LETTER

A character in the board that is not w, b, / or a digit 1 to 9.

POS_ROWS

Neither three ranks nor five.

POS_WIDTH

Ranks of different widths, or a width that is not 3, 5, 7 or 9.

POS_SIDE

The side to move is missing, is not w or b, or is followed by something.

When a string has more than one fault, the first in that order is reported.

clone

A second board with the same stones, side and size, sharing nothing with the first.

METHODS

at

my $stone = $board->at($sq);

EMPTY, WHITE or BLACK, and BORDER for a number that is not a point of this board.

put

$board->put($sq, WHITE);

Places a stone, replacing whatever stood there. put($sq, EMPTY) is lift. A point off the board, or anything that is not a stone, is ignored. Returns the board.

lift

Empties a point. Returns the board.

side

The side to move: WHITE or BLACK.

set_side

$board->set_side(BLACK);

Returns the board.

count

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

The stones of one colour on the board.

to_string

The position string.

key_hex

The key of the position, sixteen hex characters.

key_full_hex

The same key worked out again from the stones on the board. It always equals key_hex; it is here so that a test can hold the two together.

cols

rows

The size of this board.

points

How many points it has.

centre

The point in the middle.

on_board

$board->on_board($sq)

True when the number is a point of this board.

next_to

my $to = $board->next_to($sq, NE);

The point one step from $sq in a direction, or -1 when no line of this board goes that way from there: off the edge, or a diagonal asked of a weak point.

neighbours

my @points = $board->neighbours($sq);

Every point joined to $sq, clockwise from north.

degree

How many lines meet at a point: three at a corner, eight at a strong point in the open.

segments

for my $pair ($board->segments) { my ($from, $to) = @$pair; ... }

Every joined pair of points, once each, the lower point first. The nine by five has 108.

all_squares

The points of this board, a1 first, along each rank in turn.

steps

my @steps = $board->steps;

The steps of the side to move, with the obligation applied: if any of them takes, none of them is a paika. In point order, a1 first; for each stone clockwise from north; an approach before a withdrawal. In scalar context, how many there are.

A side with no stones has none.

steps_of

my @steps = $board->steps_of($sq);

Every step the stone on a point could make, whoever is to move and with no obligation applied: its paikas are listed even when another stone could take. Its enemy is the other colour from its own.

can_capture

$board->can_capture          # the side to move
$board->can_capture(BLACK)

True when some stone of a side has a step that takes.

step_why

my $why = $board->step_why($from, $dir, $kind);

WHY_OK when that is a step of the side to move, and otherwise the first of these that applies:

WHY_OFF_BOARD

$from is not a point of this board.

WHY_BAD_STEP

Not a direction, or not a kind.

WHY_NO_STONE

Nothing stands there.

WHY_NOT_YOURS

The stone belongs to the side that is not to move.

WHY_NO_LINE

No line goes that way from there: the edge, or a diagonal from a weak point.

WHY_OCCUPIED

A stone stands where the step would go.

WHY_TAKES_NOTHING

Asked to take by approach or by withdrawal, and no enemy stone is there.

WHY_NOT_A_PAIKA

Asked as a paika, and the step takes.

WHY_MUST_CAPTURE

A paika, while some stone of the mover's can take.

step_why answers WHY_OK for exactly the steps steps lists.

step

my $step = $board->step($from, $dir, $kind);

The step as a hash, or undef when there is none to build. This judges only the geometry: a stone on $from, a line that way to an empty point, and, for an approach or a withdrawal, an enemy line to take. It does not ask whose turn it is or what else could take, and a PAIKA is built with nothing taken even where the step would take.

do_step

my $token = $board->do_step($step);
my ($token, $removed) = $board->do_step($from, $dir, $kind);

Moves the stone, removes what the step takes, and always passes the turn: this is the step on its own, and play is the game's. Returns a token for undo_step, and in list context the number of stones removed as well. It does not ask whether the step is legal; ask step_why. When there is no such step to build it does nothing and returns nothing.

undo_step

$board->undo_step($token);

Puts the position back exactly as it was before the step the token came from, key included. Steps are undone in the reverse of the order they were made. Returns the board. Croaks on something that is not a token.

perft_steps

my $count = $board->perft_steps($depth);

How many sequences of $depth single steps there are from here, each step passing the turn. A string of digits, because the number outgrows a small integer within a few steps. From the set-up: 5, 17, 100, 647, 4404.

This is a check on the list of steps and not a count of games: in the game a stone that has taken may step again, and perft_moves counts that.

play_steps

my @steps = $board->play_steps;

The steps the game allows next. With no chain open they are steps. With one open they are the steps of that stone alone that take, that do not go the way it last went, and that do not arrive where it has stood. Stopping is not in the list: ask can_stop. In scalar context, how many there are.

can_stop

True while a chain is open: the mover may end the move here.

play_why

my $why = $board->play_why($from, $dir, $kind);

step_why for the game. With no chain open it is step_why. With one open it answers, in the order asked, WHY_OFF_BOARD, WHY_BAD_STEP, WHY_NO_STONE, WHY_NOT_YOURS, then:

WHY_NOT_THE_STONE

A chain is open and this is another stone.

then WHY_NO_LINE and WHY_OCCUPIED, then:

WHY_SAME_DIRECTION

The direction of the step directly before.

WHY_REVISIT

A point the chain has already stood on.

WHY_PAIKA_IN_CHAIN

A step that takes nothing, after one that took.

and WHY_NOT_A_PAIKA or WHY_TAKES_NOTHING as step_why would. play_why answers WHY_OK for exactly the steps play_steps lists.

play

my $token = $board->play($step);
my ($token, $removed) = $board->play($from, $dir, $kind);

The game's step. Moves the stone and removes what it takes. A step that took nothing passes the turn. A step that took leaves the turn with the mover and the chain open on that stone, unless nothing could follow, in which case the chain is closed and the turn passed there and then.

It does not ask whether the step is legal; ask play_why. Returns a token for unplay, with the number of stones removed in list context, and nothing when there is no such step to build.

stop

my $token = $board->stop;

Ends the move: closes the chain and passes the turn. Returns a token for unplay, or undef when no chain was open.

unplay

$board->unplay($token);

Takes back one play or one stop, the chain included. Returns the board.

in_chain

True while a stone is in the middle of its move.

chain_at

The point of that stone, or -1.

chain_dir

The direction of its last step, or -1.

chain_seen

my @points = $board->chain_seen;

Every point the chain has stood on, the start included; none when no chain is open.

moves

my @moves = $board->moves;
my @moves = $board->moves($limit);

The whole moves of the side to move, as Game::Fanorona::Move objects, each chain's shorter beginnings before its longer ones. None while a chain is open. Croaks when there are more than $limit, which is 100,000 unless given, without listing any.

count_moves

my $n = $board->count_moves;
my $n = $board->count_moves($cap);

How many whole moves there are, as a string of digits. With a cap the count stops there, for a caller who only wants to know whether there are too many.

play_move

my $tokens = $board->play_move($move);

Plays a whole move: each of its steps, and then a stop if the chain is still open, which it is whenever the move is the shorter beginning of a longer one. Each step is asked of play_why first, so a move that does not belong to this position is not played: the board is left as it was and undef is returned. Returns an array reference of tokens for unplay_move.

unplay_move

$board->unplay_move($tokens);

Takes a whole move back. Returns the board.

perft_moves

my $count = $board->perft_moves($depth);

How many sequences of $depth whole moves there are from here, as a string of digits. From the set-up: 5, 39, 724, 18026. A side with no move ends its line: nothing here rules on how a game is won.

CLASS METHODS

chain_max

The most steps one move can have: 44. A chain never stands on a point twice, and there are forty-five points.

steps_max

The most steps any position can have: 216. A step runs along a segment from the end with a stone to the end without, there are 108 segments, and a step is at most two entries.

line_max

The most stones one step can take: seven.

square_of

my $sq = Game::Fanorona::Engine->square_of($file, $rank);

The point at a file and rank, or -1 outside the nine by five.

file_of

rank_of

The file and the rank of a point, or -1.

is_strong

True for a point with diagonal lines.

opposite

Game::Fanorona::Engine->opposite(NE)    # SW

is_diagonal

True for NE, SE, SW and NW.

size_ok

Game::Fanorona::Engine->size_ok($cols, $rows)

True for a size new will build.

size_of

my ($cols, $rows) = Game::Fanorona::Engine->size_of('dimy');

The size a name stands for, or an empty list.

stride

The difference between a point and the point one rank above it. The point one file to the right is always one more.

zobrist_hex

Game::Fanorona::Engine->zobrist_hex(WHITE, $sq)

What one stone on one point contributes to a key.

zobrist_side_hex

What Black to move contributes.

zobrist_size_hex

Game::Fanorona::Engine->zobrist_size_hex($cols, $rows)

What the size of the board contributes.

abi_version

The version of the table of functions this distribution publishes to other compiled code.

live

How many boards exist that have not been destroyed.

FUNCTIONS

other

other(WHITE)    # BLACK

Exported on request, with the constants, by :all.

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)