NAME

Game::RoyalUr::Notation - moves, positions and game records of the Royal Game of Ur, as text

VERSION

Version 0.01

SYNOPSIS

use Game::RoyalUr::Notation qw(parse_move format_display parse_record format_record);

my ($move, $error) = parse_move('hand-b1');      # { from => 'hand', to => 'b1' }

print format_display($move_object), "\n";        # 3: a2-d2x*

my ($record, $problem) = parse_record($text);
die "line $problem->{line}: $problem->{error}\n" unless $record;
print format_record($record);

DESCRIPTION

Functions that read and write a move, a turn and a whole game as text. There is no object here and nothing is kept between calls.

This module needs no compiled code. It loads, and does everything below, on a machine where the rest of the distribution could not be built.

A move, three ways

hand-b1          the wire form
3: a2-d2x*       the display form
0: -             a turn lost to the roll

The wire form names two places and nothing else. A place is a square, a file letter a to h and a row digit 1 to 3, or the word hand for a piece entering the board, or the word home for a piece leaving it. It carries no roll: a move cannot tell a game what was rolled.

The display form leads with the roll, then the wire form, then x if the move captures and * if it lands on a rosette, in that order.

A forfeit is the roll and a dash.

Everything is written in lower case. The two words and the file letters are read in either case, and nothing else is forgiven: no spaces, no other separator, and no x or * on a wire move.

A wire move is not checked against a board. b1-h3 is read without complaint. Whether it is a move in some position is a question for a game.

A game record

[rules finkel]
[first light]
[seed 5f1c9a]
1. l 3: hand-b1
2. d 0: -
3. l 4 1111: b1-c2
[result light resign]

A header and then one turn a line.

[rules ...]

finkel or masters, or the five fields of a rule set spelled out in order: route=long dice=3 zero_rolls=4 safe_rosettes=1 pieces=7.

[first ...]

light or dark: the side that moves first.

[seed ...]

Optional. The seed's bytes, in hexadecimal. With a seed, every roll in the record is checked against the dice of that seed, and a record that disagrees is refused. Without one the rolls are taken as written, which is how a game played with real dice is recorded.

[opening N]

Optional, and only with a seed: how many throws were spent deciding who moves first, so that the game's own throws are numbered from there.

a turn

Its number, counted from 1; the side, l or d; the roll; optionally the dice as they fell, a 1 for a marked die and a 0 for an unmarked one; a colon; and the move in wire form, or a dash for a turn lost.

The side is on every line because turns do not alternate, and it is checked and not trusted: after a move onto a rosette the same side moves again, and after anything else the other side does.

[result ...]

Optional, and last. light home or dark home for a game won by bringing the last piece home, light resign or dark resign naming the winner of a game that was resigned, and draw ply_cap.

A record ends with a newline, and a record that has been read and written again is the same text, byte for byte.

A record is checked for its own consistency: its numbering, its sides, its dice. It is not played. Whether each move was legal is a question for a game, which is asked by replaying it.

FUNCTIONS

None is exported unless asked for. :all exports everything.

square_ok

square_ok('d2')     # true
square_ok('e1')     # false: there is no such square

True for the name of one of the twenty squares, in lower case.

parse_move

my ($move, $error) = parse_move($text);

A reference to { from => ..., to => ... } in lower case and undef, or undef and one of:

empty

No text.

shape

Not two places with one dash between them.

place

One of the two is not a square, hand or home.

from_home

A piece that has come home does not move again.

to_hand

Nothing moves to the hand.

format_move

my $wire = format_move($move);

The wire form of a move: a Game::RoyalUr::Move, or a hash reference with from and to.

format_display

my $shown = format_display($move);
my $shown = format_display($move, $roll);

The display form. The roll, and whether the move captures or lands on a rosette, are read from the move; a roll given as the second argument is used instead of the move's own.

format_forfeit

my $shown = format_forfeit($roll);     # '0: -'

A turn lost to that roll, in the display form.

validate_position

my $code = validate_position($string);

POS_OK for a position string in the form "to_string" in Game::RoyalUr::Engine writes, or the reason it is not one. It agrees with "of_string" in Game::RoyalUr::Engine on every string, and needs no board to say so.

parse_record

my ($record, $problem) = parse_record($text);

A record and undef, or undef and a reference to { line => ..., error => ... } naming the first line that is wrong, counted from 1, and why.

A record is a hash reference:

{
    rules   => 'finkel',                # or a hash of the five fields
    first   => 'light',
    seed    => '5f1c9a',                # when the record has one
    opening => 2,                       # when the record has one
    turns   => [
        { side => 'light', roll => 3, move => 'hand-b1' },
        { side => 'dark',  roll => 0, move => undef },
        { side => 'light', roll => 4, faces => '1111', move => 'b1-c2' },
    ],
    result  => { winner => 'light', how => 'resign' },    # when it has one
}

format_record

my $text = format_record($record);

The text of a record in that shape.

CONSTANTS

POS_OK, POS_NULL, POS_ROWS, POS_WIDTH, POS_LETTER, POS_GAP, POS_X, POS_SIDE, POS_COUNT, POS_FIELD, POS_LONG

What validate_position answers, with the meanings and the values Game::RoyalUr::Engine gives the same names.

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)