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 A or W. A lower-case a is 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)