NAME

Game::Brandubh - the Irish 7 by 7 tafl game: a king, four defenders, eight attackers

VERSION

Version 0.01

SYNOPSIS

use Game::Brandubh;

my $game = Game::Brandubh->new;

print $game->turn, "\n";                 # p1, who has the attackers
for my $move (@{ $game->legal }) {
    print "$move->{move} takes @{ $move->{captures} }\n" if @{ $move->{captures} };
}

my $refused = $game->play('d1c1');
print $refused->message, "\n" if $refused;

if ($game->status eq 'finished') {
    my $result = $game->result;
    print $result->how, ': ', $result->winner // 'a draw', "\n";
}

my $saved = $game->as_text;
my $again = Game::Brandubh->from_text($saved);

DESCRIPTION

Brandubh is the smallest of the tafl games, the one played in Ireland. A king and his four defenders start in the middle of a board of seven squares by seven, and eight attackers stand round them in a cross. Every piece moves like a rook. The king wins by reaching a corner; the attackers win by capturing him.

The two players do not have the same pieces or the same aim. That is the game.

This class is a whole game: it knows whose move it is, which moves are legal and what each would capture, why a move was refused, and when and how the game has ended. It keeps a log that a game can be played again from.

It is the engine behind the brandubh at https://peer2peergames.com.

The rules

No rules for brandubh survive. What survives is a handful of boards and a few lines of verse, which give the number of pieces and the king's corners. Every set of rules in use is a reconstruction.

This distribution follows the reconstruction by Aage Nielsen, as published in twelve numbered rules at http://tafl.cyningstan.com/page/171/brandub.

  • The attackers move first.

  • 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 central square, the throne, not even the king once he has left it. Only the king may stop on a corner.

  • A piece other than the king is captured when the enemy moves so that it stands between two enemy pieces on opposite sides. An empty corner, and the throne while it is empty, each count as an enemy to both sides.

  • The king is captured by attackers: four of them round him when he is on the throne, three when he stands beside it, and two, as for any piece, anywhere else.

  • The king wins on reaching a corner. The attackers win by capturing him.

  • The game is drawn when a position is repeated and when a side cannot move.

Where this distribution reads between the lines

Twelve rules leave some things unsaid. Each of these is this distribution's own reading, and each is a field of Game::Brandubh::Variant or is described there.

  • Only the piece that moved captures. A piece may move between two enemies and stand there unharmed.

  • One move captures every enemy it closes on, up to three.

  • A piece may slide across the empty throne on its way elsewhere.

  • The king captures as any defender does.

  • Nothing is captured against the edge of the board.

  • "Repeated" is taken as the third occurrence of a position, not the second.

  • Attackers with no piece left have lost. Read to the letter, the rule about a side that cannot move would call that a draw.

  • A game that will not end is drawn after 400 moves.

  • A finished game is shown with ++ after the move that took the king to a corner and # after the move that captured him.

Seats and sides

A side is attackers or defenders. A seat is p1 or p2, the two people at the table. attackers says which seat has the attackers, and so moves first; side_of and seat_of go between the two.

A refusal is returned, never thrown

play, resign and the three draw methods hand back 0, or a Game::Brandubh::Error saying what was wrong. Nothing was changed.

A bad construction, on the other hand, croaks. A refused move is an ordinary thing for a player to do; a seat called p3 in new is a mistake in the program.

An argument to new whose name is misspelt is not noticed. new takes the four names below and ignores any other.

METHODS

new

my $game = Game::Brandubh->new(
    attackers => 'p2',
    variant   => { repeat => 2 },
    position  => '7/7/7/3k3/7/7/a6 d',
    seed      => $thirty_two_bytes,
);

Every argument is optional.

attackers

The seat that has the attackers, p1 by default.

variant

A Game::Brandubh::Variant, or a hash reference of its fields, or the string its as_string writes. The default rule set when left out.

position

Where the game starts, as a position string. The set-up when left out.

seed

Exactly thirty-two bytes. The rules never read it: brandubh has no chance in it. It is held for whoever plays the game against a program, so that the program's choices can be made the same way twice.

Croaks when any of them is not what it should be.

attackers

The seat that has the attackers.

variant

The rule set, a Game::Brandubh::Variant.

start

The position the game started from.

status

active or finished.

turn

The seat whose move it is, or undef when the game is finished.

side_to_move

attackers or defenders, or undef when the game is finished.

side_of

my $side = $game->side_of('p1');

The side a seat has, or undef for something that is not a seat.

seat_of

my $seat = $game->seat_of('defenders');

The seat a side belongs to, or undef for something that is not a side.

my $moves = $game->legal;

An array reference of the moves of the side to move, empty when the game is finished. Each is a hash reference:

move

The move as it is stored, d1d3.

from, to

The two squares.

piece

attacker, defender or king.

captures

An array reference of the squares whose pieces the move would capture.

wins

True when the move wins the game.

play

my $refused = $game->play('d1c1');
my $refused = $game->play('d1-c1', 'p1');

Plays a move for the side to move. The move may be written as it is stored or as it is shown. Naming the seat is optional; when it is named, it must be that seat's turn. Returns 0, or a Game::Brandubh::Error.

play_or_die

$game->play_or_die('d1c1')->play_or_die('d3c3');

The same, for a caller who would sooner have an exception. Returns the game. Croaks with the refusal's sentence.

undo

Takes back the last thing that happened: a resignation or an agreed draw if that is how the game ended, and otherwise the last move. Any draw offer is withdrawn. Returns true, or false when there is nothing to take back.

resign

my $refused = $game->resign('p2');

The seat gives the game to the other side. Returns 0, or a refusal.

offer_draw

my $refused = $game->offer_draw('p1');

Either seat may offer a draw at any time, when no offer is waiting. An offer stands until it is answered or until a move is made.

accept_draw

decline_draw

my $refused = $game->accept_draw('p2');

The other seat answers. Accepting ends the game. A seat cannot answer its own offer.

draw_offered_by

The seat whose offer is waiting, or undef.

result

A Game::Brandubh::Result, or undef while the game is active.

winner

The seat that won, or undef while the game is active or when it was drawn.

position

The position on the board, as a string.

signature

A key for the position on the board: sixteen hexadecimal characters, the same for the same pieces on the same squares with the same side to move.

repeats

How many times the position on the board has occurred in this game, this time included.

ply

How many moves have been made, counting both sides'.

at

my $piece = $game->at('d4');

attacker, defender, king, or the empty string for an empty square. undef for something that is not the name of a square.

pieces

A hash reference from the name of every occupied square to what stands on it.

log

A copy of the moves made, as they are stored.

shown

A copy of the moves made, as they are shown: d1-d3xc3, Kg2-g1++.

seed

The seed the game was given, once the game is finished. undef before then, so that what a program will choose cannot be read off a game in progress.

my $found = $game->search(budget => 20_000);
my $found = $game->search(budget => 20_000, salt => $bytes, depth => 4);

$game->play($found->{move}) if $found;

The move a search of that many positions thinks best for the side to move, or undef when the game is finished. The game is not changed.

The search never reads a clock: its only limit is budget, a count of positions. The same game, budget and salt give the same move every time, on any machine.

salt is any string. It decides which move is chosen when the search scores several alike, so that two programs given different salts do not play the same game, and a program given the same salt plays the same game twice. depth stops the search after it has looked that many moves ahead. weights changes how a position is scored: see "weights" in Game::Brandubh::Rules.

What comes back is a hash reference: move, as it is stored; score, for the side to move; depth, how far ahead the search finished looking; nodes, how many positions it looked at; and stopped, true when the budget ran out part way through looking one move further.

Game::Brandubh::Bot is this with levels of play on top.

bot

my $class = $game->bot;         # 'Game::Brandubh::Bot'

The class that plays this game.

replay

my $game = Game::Brandubh->replay(attackers => 'p1', moves => \@moves);
my ($game, $refused, $at) = Game::Brandubh->replay(moves => \@moves);

my $again = $game->replay($game->log);

Builds a game and plays the moves into it. As a class method it takes what new takes, with moves and, optionally, result: { how => 'resign', by => 'attackers' } or { how => 'agreed' }. As an object method it starts from the same seats, rule set and position as the game it is called on.

In list context it returns the game, 0 or the refusal that stopped it, and how many moves were played. In scalar context it returns the game, or undef when a move was refused.

as_text

The game as text: its rule set, where it started when that was not the set-up, its moves, and its ending when the players ended it. See "A game" in Game::Brandubh::Notation.

from_text

my $game = Game::Brandubh->from_text($text, attackers => 'p2');

A game from that text. The seats are not part of the text and may be given after it. Returns as replay does.

SEE ALSO

Game::Brandubh::Variant, Game::Brandubh::Result, Game::Brandubh::Error, Game::Brandubh::Notation.

Game::Brandubh::Rules and Game::Brandubh::Engine are what this class is built on: a game and a position, in the numbers the compiled code uses.

AUTHOR

LNATION <email@lnation.org>

BUGS

Please report any bugs or feature requests to bug-game-brandubh at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Game-Brandubh. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

perldoc Game::Brandubh

You can also look for information at:

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)