NAME
Game::Brandubh::Engine - the brandubh board: squares, pieces, the side to move
VERSION
Version 0.01
SYNOPSIS
use Game::Brandubh::Engine ':all';
my $board = Game::Brandubh::Engine->new;
print $board->to_string, "\n"; # 3a3/3a3/3d3/aadkdaa/3d3/3a3/3a3 a
my $d4 = Game::Brandubh::Engine->square_of(3, 3);
print "the king\n" if $board->at($d4) == KING;
my ($other, $err) = Game::Brandubh::Engine->of_string('7/7/7/3k3/7/7/7 d');
DESCRIPTION
A position: forty-nine squares, what stands on each, and whose move it is; where each piece of the side to move may go; and what a move captures.
put, lift and relocate judge nothing: put will place two kings on a corner if asked. moves, is_legal and why_not are where the rules of movement live, and do_move is where the rules of capture do.
Nothing here knows how a game ends. do_move reports that the king was captured, or that he reached a corner, and plays on regardless.
Most callers want Game::Brandubh, which holds one of these and plays the game. This class is for building positions and asking what is on them.
A square is an opaque number
A square is a number handed out by square_of and taken apart by file_of and rank_of. It is not rank * 7 + file and nothing should assume a relationship between two squares except through stride: the square one file to the right is $sq + 1, and the square one rank up is $sq + stride.
Files run 0 to 6 for a to g and ranks 0 to 6 for 1 to 7, so square_of(3, 3) is d4, the throne.
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 with the same pieces on the same squares and the same side to move have the same key, in every process, on every machine.
How a piece moves
A piece moves any distance along a rank or a file. It may not land on another piece and may not jump one.
No piece may stop on the throne, the king included once he has left it. Only the king may stop on a corner.
A piece may slide across the empty throne on its way somewhere else. The rules this distribution follows forbid landing there and forbid jumping pieces, and say nothing of an empty square in the way, so this is the distribution's own reading; throne_pass turns it off.
How a piece is captured
A piece other than the king is captured when the enemy moves a piece so that it stands between two enemies on opposite sides, along a rank or a file.
An empty corner, and the throne while it is empty, each count as an enemy to both sides: a piece next to one is captured by a single enemy moving to the other side of it. The edge of the board never counts.
Only the piece that moved captures. A piece may move between two enemies and stand there unharmed. This is the usual tafl rule and the rules this distribution follows do not state it, so it is the distribution's reading.
One move may close on an enemy in up to three directions and takes every piece it closes on.
The king captures as any defender does, by moving or by standing still.
How the king is captured
By attackers only, and it depends where he stands.
- On the throne
-
All four squares beside him hold attackers.
- Beside the throne
-
The three squares beside him that are not the throne hold attackers.
- Anywhere else
-
As any other piece: between two attackers, or between an attacker and an empty corner.
Two variant fields change this. king_everywhere_two takes him as any other piece wherever he stands, the throne included. king_strong requires every side of him closed, by an attacker or by the empty throne, wherever he stands; the edge closes nothing, so a king on it cannot be taken.
A move is an opaque number
Build one with move from two squares and take it apart with move_from and move_to. What a move captures is decided by the position and is not part of the move.
A variant is a hash reference
Every method that applies a rule takes an optional last argument naming what differs from the default game. Leave it out, or pass undef, for the default.
my @moves = $board->moves({ throne_pass => 0 });
throne_pass-
True by default: a piece may slide across the empty throne.
throne_reentry-
False by default: the king may not return to the throne.
king_everywhere_two,king_strong-
Both false by default. See "How the king is captured".
escape-
'corner'by default:do_movereports the king home when his move ends on a corner. With'edge', on any square of the edge. repeat,ply_cap-
Accepted, and without effect until endings arrive.
A key that is not one of these croaks, so that a misspelt field cannot quietly play the default game.
Every board is its own board
clone returns a board that shares nothing with the one it came from. A board is released when the last reference to it goes away.
CONSTANTS
Exported on request, or all at once with :all.
EMPTY,ATTACKER,DEFENDER,KING-
What
atreturns for a square of the board. BORDER-
What
atreturns for a number that is not a square of the board. ATTACKERS,DEFENDERS-
The two sides. The king is on the defenders' side.
SIZE,SQUARES-
7 and 49.
POS_OK,POS_NULL,POS_ROWS,POS_WIDTH,POS_LETTER,POS_SIDE,POS_LONG-
Why a position string was refused: no string, not seven rows, a row that is not seven wide, a character that names no piece, a missing or unknown side, a string too long to be a position.
WHY_OK,WHY_OFF_BOARD,WHY_NO_PIECE,WHY_NOT_YOURS,WHY_NO_MOVE,WHY_NOT_A_LINE,WHY_THRONE,WHY_CORNER,WHY_BLOCKED-
What
why_notanswers: the move is legal, a square is off the board, nothing stands on the first square, the piece belongs to the side not to move, the two squares are one square, they share neither rank nor file, the move would stop on the throne or cross one it may not cross, only the king may stand on a corner, a piece is in the way. DID_CAPTURE,KING_TAKEN,KING_HOME-
Bits of what
do_moveandpreviewreport: at least one piece was captured; a king was among them; the king moved and ended where he wins.
FUNCTIONS
other
my $side = other(ATTACKERS); # DEFENDERS
The other side. Exported on request.
METHODS
new
my $board = Game::Brandubh::Engine->new;
my $board = Game::Brandubh::Engine->new(empty => 1);
my $board = Game::Brandubh::Engine->new(position => '3a3/3a3/3d3/aadkdaa/3d3/3a3/3a3 a');
The set-up, an empty board with the attackers to move, or the position a string describes. Croaks on a string it will not take; use of_string to get the reason back instead.
of_string
my ($board, $err) = Game::Brandubh::Engine->of_string($string);
A board and POS_OK, or undef and one of the POS_* codes. A string is refused for its shape and never for its sense: two kings, or none, both load.
to_string
Seven rows from rank 7 down to rank 1 with a / between them, a digit for a run of empty squares, then a space and the side to move. a is an attacker, d a defender, k the king, and the side is a or d.
clone
A copy that shares nothing with the original.
at
my $piece = $board->at($square);
EMPTY, ATTACKER, DEFENDER or KING, or BORDER for a number that is not a square.
put
$board->put($square, DEFENDER);
Places a piece, replacing whatever stood there. Judges nothing. Returns the board.
lift
$board->lift($square);
Empties a square. Returns the board.
side
ATTACKERS or DEFENDERS: whose move it is.
set_side
$board->set_side(DEFENDERS);
Returns the board.
count
my $n = $board->count(ATTACKER);
How many of one piece are on the board.
king_square
The king's square, or -1 when there is no king on the board.
key_hex
The position's key. See "The key is a hex string, never a number".
key_full_hex
The same key worked out again from the squares. It always equals key_hex; it exists so a test can show that.
square_of
my $sq = Game::Brandubh::Engine->square_of($file, $rank);
A square, or -1 when the file or the rank is off the board.
file_of
rank_of
The file or the rank of a square, 0 to 6, or -1 for a number that is not a square.
on_board
True for the forty-nine squares.
is_throne
True for d4 and for no other square, whether or not the king stands on it.
is_corner
True for a1, a7, g1 and g7.
beside_throne
True for d3, d5, c4 and e4: the four squares that share an edge with the throne.
side_of
my $side = Game::Brandubh::Engine->side_of(KING); # DEFENDERS
The side a piece is on, or -1 for something that is not a piece.
stride
The distance between a square and the one a rank above it.
all_squares
The forty-nine squares, a1 to g1 first and a7 to g7 last.
zobrist_hex
my $hex = Game::Brandubh::Engine->zobrist_hex(KING, $square);
The contribution of one piece on one square to a key.
zobrist_side_hex
The contribution of the defenders being the side to move.
abi_version
The version of the table of functions other compiled code may call.
live
How many boards exist in this process and have not been released.
move
my $mv = Game::Brandubh::Engine->move($from, $to);
A move between two squares. Nothing is checked.
move_from
move_to
The square a move leaves and the square it arrives on.
moves
my @moves = $board->moves;
my $count = $board->moves;
my @moves = $board->moves(\%variant);
Every move of the side to move: a list in list context, a count in scalar context. The list is in square order, a1 first, and for each piece left, right, down, then up, nearest square first.
moves_max
The most moves any position can have, whatever stands on the board. Along one line an empty square can be reached by at most two pieces, the nearest on each side, which bounds the whole board at 140.
is_legal
if ($board->is_legal($mv)) { ... }
if ($board->is_legal($mv, \%variant)) { ... }
True when the move is one of moves.
why_not
my $why = $board->why_not($from, $to);
my $why = $board->why_not($from, $to, \%variant);
WHY_OK for a legal move, and otherwise the first thing wrong with it, one of the WHY_* constants. It is WHY_OK exactly when is_legal is true.
relocate
$board->relocate($mv);
Moves whatever stands on the first square to the second, replacing what was there, and passes the turn. It asks nothing and captures nothing. Returns the board.
perft_slides
my $nodes = $board->perft_slides(3);
my $nodes = $board->perft_slides(3, \%variant);
How many sequences of that many moves there are from this position when nothing is ever captured. A string of decimal digits, because the number outgrows a 32-bit integer within a few moves. The board is left as it was.
hostile_to
if ($board->hostile_to($square, DEFENDERS)) { ... }
True when the square counts as an enemy of that side in a capture: it holds a piece of the other side, or it is an empty corner, or the empty throne.
captures_at
my @squares = $board->captures_at($square);
my @squares = $board->captures_at($square, \%variant);
The squares whose pieces are captured by the piece standing on $square, as if it had just arrived there. Nothing is removed.
do_move
my ($flags, $undo) = $board->do_move($mv);
my ($flags, $undo) = $board->do_move($mv, \%variant);
Moves the piece, removes what it captured and passes the turn. $flags is DID_CAPTURE, KING_TAKEN and KING_HOME or'd together, or 0.
It does not ask whether the move is legal; ask is_legal first. It asks only that a piece stands on the first square and nothing on the second, and otherwise does nothing and reports 0.
$undo is an opaque string for undo_move.
undo_move
$board->undo_move($undo);
Takes back the move that produced $undo: the piece returns, the captured pieces return, the turn returns. Moves are taken back in the reverse of the order they were made. Returns the board. Croaks on a string that is not an undo.
preview
my ($flags, @squares) = $board->preview($mv);
my ($flags, @squares) = $board->preview($mv, \%variant);
What do_move would report and which squares it would empty, without touching the board.
perft
my $nodes = $board->perft(3);
my $nodes = $board->perft(3, \%variant);
How many sequences of that many moves there are from this position, with captures played. A string of decimal digits. Nothing ends a sequence early: a side whose king has been captured goes on moving what it has left. The board is left as it was.
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)