NAME
Game::Fanorona::Notation - moves, positions and games of fanorona as text
VERSION
Version 0.03
SYNOPSIS
use Game::Fanorona::Notation ':all';
my ($move, $why) = parse_move('b2-c2A-c3A');
print format_move($move), "\n"; # b2-c2A-c3A
print join(' ', move_points('b2-c2A-c3A')), "\n"; # b2 c2 c3
my $position = parse_position(SETUP);
print $position->{stones}{d3}, "\n"; # w
print mirror_move('d3-e3A', 'files'), "\n"; # f3-e3A
DESCRIPTION
Functions that read and write fanorona as text. There is no object here and no compiled code is loaded: this module can tell a move from a typo, or read a saved game, on a machine where the rest of the distribution was never built.
It knows shape and not rules. parse_move will tell you that b2-c4 is not a step and that a diagonal does not leave e2. It will not tell you whether there is a stone on b2, whose turn it is, or whether the step takes what its letter says: that is Game::Fanorona::Rules.
Nothing is exported unless asked for; :all asks for everything.
A point
A file letter a to i and a rank digit 1 to 5: a1 is the corner on White's left, e3 the centre of the nine by five. Always lower case.
A move
b2-b1 a paika: one step that takes nothing
b2-c2A b2 to c2, taking by approach
f4-e4W f4 to e4, taking by withdrawal
b2-c2A-c3A a chain: two steps by the same stone
The stone's points in order, joined by dashes, with A or W after each point it arrived on by a step that took. This is the notation of the 2008 paper that solved the game, Schadd et al., "Best Play in Fanorona Leads to Draw".
A step that takes always carries its letter, even where only one of the two ways was possible, so a move can be read with no board beside it. Every step of a chain takes, so a move of two steps or more has a letter on each.
A position
bbbbbbbbb/bbbbbbbbb/bwbw1bwbw/wwwwwwwww/wwwwwwwww w
bbbbbbbbb/bbbbbbbbb/bwbw1bwbw/wwwwwwwww/wwwwwwwww w 0 1
Ranks from the top down, w and b for stones, a digit for a run of empty points, / between ranks; then the side to move. The size of the board is read from the string.
The second form adds two counts for a game in progress: how many moves running have taken nothing, and the number of the move about to be made, starting from 1. The first form is what "of_string" in Game::Fanorona::Engine reads; parse_position reads both and hands the first back as board.
A position is always between moves. There is no way to write a chain half made.
A game
fanorona 1
variant sivy
quiet_limit 60
white Rakoto
moves
d3-e3A
c3-d3W
A first line that says what this is, header lines of a word and a value, a line moves, and then one move a line. The headers are variant (sivy, dimy, telo, or a size such as 7x5; sivy when absent), start (a position, when the game does not begin from the set-up), quiet_limit, repeat, ply_cap, white, black, result, offer and chain.
result is how the game ended: a way and a winner, as resign black, or a way alone for a draw, as agreement. offer is white or black, the side whose offer of a draw is standing, and is never beside a result. chain is the steps made so far of a move not yet finished, written as a move is, as b2-c2A-c3A.
That is everything needed to play a game again, because nothing in fanorona is left to chance.
FUNCTIONS
parse_point
my ($file, $rank) = parse_point('e3');
my ($file, $rank) = parse_point('c3', cols => 3, rows => 3);
The file and rank of a point, each counted from 0, or an empty list when the name is not a point of a board that size. The size is the nine by five unless given.
format_point
format_point(4, 2) # e3
The name of a file and rank, or undef.
square_of
my $sq = square_of('e3');
The number Game::Fanorona::Engine uses for a point, or -1.
name_of
name_of($sq) # e3
The name of one of the engine's numbers, or undef.
parse_move
my $move = parse_move('b2-c2A-c3A');
my ($move, $why) = parse_move($string, cols => 5, rows => 5);
A move as a hash, or undef; in list context undef and a word for why.
{
from => $sq, # the engine's number for the first point
to => $sq, # and for the last
points => [ 'b2', 'c2', 'c3' ],
steps => [
{ from => $sq, to => $sq, dir => 2, kind => 1 },
{ from => $sq, to => $sq, dir => 0, kind => 1 },
],
}
Each step is what "step" in Game::Fanorona::Rules and "play" in Game::Fanorona::Engine take, so a parsed move can be played a step at a time as it stands. dir is 0 to 7 clockwise from north; kind is 0 for a paika, 1 for an approach and 2 for a withdrawal.
The reasons, in the order they are looked for:
bad_size-
The board asked for is not one.
empty-
No string.
bad_shape-
No dash, a dash at either end, two dashes together, or a character that is not a letter, a digit or a dash.
kind_on_start-
A letter after the first point. A stone does not arrive where it starts.
bad_point-
Something that is not a lower-case letter and a digit where a point belongs.
too_long-
More than forty-four steps. No move is.
bad_kind-
Something after a point that is not
AorW. A lower-caseais this. off_board-
A point that is not on a board of this size.
same_point-
A step from a point to itself.
not_adjacent-
A step between points that are not next to each other.
no_line-
A diagonal step from a point that has no diagonal.
paika_in_chain-
A move of two steps or more with a step that has no letter.
format_move
my $string = format_move($move);
The string for a move: either the hash parse_move returns or a Game::Fanorona::Move. undef for anything else.
move_points
my @names = move_points('b2-c2A-c3A'); # b2 c2 c3
Every point a move touches, in order; an empty list for a string that is not a move.
mirror_move
mirror_move('d3-e3A', 'files') # f3-e3A
mirror_move('d3-e3A', 'ranks') # d3-e3A
mirror_move('d2-e3A', 'turn') # f4-e3A
A move reflected: files swaps left and right, ranks swaps top and bottom, turn is half a turn of the board. The letters do not change: a reflected approach is an approach. undef for a string that is not a move.
parse_position
my $position = parse_position($string);
my ($position, $why) = parse_position($string);
A position as a hash, or undef and a word for why.
{
cols => 9,
rows => 5,
side => 'w',
stones => { a1 => 'w', ... },
board => 'bbbbbbbbb/.../wwwwwwwww w', # the two-field form, canonical
counted => 1, # the string had the counts
quiet => 0,
move => 1,
}
board is always written the one way the engine writes it: a string that spells a run of five empty points as 41 is read, and comes back as 5.
The reasons: empty; too_long; bad_shape for a string that is not two fields or four; bad_letter for a character in the board that is not w, b, / or a digit 1 to 9; rows for a number of ranks that is not three or five; width for ranks of different widths or a width that is not 3, 5, 7 or 9; side; and bad_count for counts that are not whole numbers, or a move number below 1.
format_position
my $string = format_position($board, $quiet, $move);
The four-field form from the two-field one and the two counts, which are 0 and 1 when left out. undef if the board is not a position or already has counts, or a count is not a number.
mirror_position
mirror_position($string, 'files')
A position reflected, in the three ways mirror_move reflects a move. The side to move and the counts are kept.
format_game
my $text = format_game({ variant => 'sivy', white => 'Rakoto', moves => \@strings });
The text of a game, or undef when a header is not one this module knows, a value is empty or has a character outside plain ASCII, or a move is not a move. What it writes is always something parse_game reads.
parse_game
my $game = parse_game($text);
my ($game, $why, $line) = parse_game($text);
A game as a hash: each header that was present, variant always, cols and rows for its board, moves, an array reference of move strings, moves_at, the number of the line the first move is on or would be on, and at, a hash reference from each header present to the number of its line. Or undef, a word for why, and the number of the line at fault.
The reasons: empty, not_a_game for a first line that is not fanorona 1, bad_header, unknown_header, repeated_header, bad_number, bad_offer for an offer by something that is not a side or an offer beside a result, no_moves_line, bad_variant, bad_start, and any reason parse_move gives for a move or for the chain. A move that parses is already written the one way format_move writes it, so nothing more is asked of it.
A game that parses is well formed, and may still not be a game. Whether each move was legal when it was made is found out by playing it.
CONSTANTS
SETUP
The position string of the set-up of the nine by five, in the two-field form.
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)