NAME

Game::Brandubh::Rules - a game of brandubh: the moves made, and how it ends

VERSION

Version 0.01

SYNOPSIS

use Game::Brandubh::Rules ':all';
use Game::Brandubh::Engine;

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

until ($game->is_over) {
    my @moves = $game->moves;
    $game->play($moves[0]);
}

print outcome_name($game->outcome), "\n";      # corner, capture, repetition...

my $from_here = Game::Brandubh::Rules->new(
    position => '7/7/7/3k3/7/7/a6 d',
    variant  => { repeat => 2 },
);

DESCRIPTION

A position together with the moves that led to it. This is the class that knows when a game is over, and the only one here that refuses a move.

Game::Brandubh::Engine holds a position and will play any move asked of it, past the capture of the king if need be. A Game::Brandubh::Rules stops.

Squares and moves are the numbers Game::Brandubh::Engine hands out.

How a game ends

Six ways: two wins for the defenders, one for the attackers, three draws.

BY_CORNER

The king's move ended on a corner. The defenders win.

BY_CAPTURE

An attacker's move captured the king. The attackers win.

BY_NO_PIECES

The attackers have no piece left. The defenders win.

DRAW_REPETITION

The position on the board, with the same side to move, has occurred for the third time.

DRAW_NO_MOVE

The side to move has pieces and none of them can move.

DRAW_PLY_CAP

The game reached its limit of moves without ending any other way.

They are asked in that order, because one move can satisfy two. A king who reaches a corner on the move that also repeats a position has won.

Resignation and a draw by agreement are not among them. They are something two people do, and the caller records them.

Where these rules come from, and where they are this distribution's own

The win by a corner, the win by capture, and the draws by repetition and by a side that cannot move are from the reconstruction of brandubh by Aage Nielsen: "The game is drawn if a position is repeated, if a player cannot move, or if the players otherwise agree it."

Three points are this distribution's reading of it.

The third time

"If a position is repeated" says twice. This distribution draws at the third occurrence, as draughts and chess do, so that a player has seen the position come round once before the game is taken away. repeat => 2 gives the sentence as written.

No pieces is not "cannot move"

Read literally the sentence draws a game in which the defenders have captured all eight attackers, because the attackers then cannot move. It is for a side that is blocked. Attackers with no piece left have lost.

The limit

The attackers can close all four corners, and then nothing above ends the game. It is drawn at ply_cap moves, counting both sides' moves.

A position that was set up, not played into

A game may start from any position. When that position is already an ending there is no move to read it from, so: a board with no king on it is a win by capture, and a board with a king standing where he wins is a win by a corner. Otherwise it is judged like any other.

The rule set is fixed when the game is made

variant takes the fields "A variant is a hash reference" in Game::Brandubh::Engine describes, and they hold for the whole game. Two of them belong to this class:

repeat

3 by default. Which occurrence of a position draws the game. A value below 2 would end every game at its first position and is taken as 2.

ply_cap

400 by default. A value of 0 or less, or above 4096, is taken as the default.

CONSTANTS

Exported on request, or all at once with :all.

ONGOING, BY_CORNER, BY_CAPTURE, BY_NO_PIECES, DRAW_REPETITION, DRAW_NO_MOVE, DRAW_PLY_CAP

What outcome returns. ONGOING is 0, so an outcome is true exactly when the game is over.

PLAY_OK, PLAY_OVER, PLAY_ILLEGAL

What play answers. PLAY_OK is 0.

FUNCTIONS

outcome_name

my $word = outcome_name(BY_CORNER);     # 'corner'

ongoing, corner, capture, no_pieces, repetition, no_move or ply_cap; undef for anything that is not an outcome. Exported on request.

METHODS

new

my $game = Game::Brandubh::Rules->new;
my $game = Game::Brandubh::Rules->new(position => $string, variant => \%fields);

A game from the set-up, or from a position string. Croaks on a string that is not a position and on a variant field that does not exist.

clone

A copy with the whole history, sharing nothing with the original.

play

my $answer = $game->play($mv);
my ($answer, $flags) = $game->play($mv);

Plays a move for the side to move. PLAY_OK when it was played; PLAY_OVER when the game had already ended; PLAY_ILLEGAL when the move is not one of moves. On either refusal the game is exactly as it was.

$flags is what "do_move" in Game::Brandubh::Engine reports for the move.

undo

Takes the last move back, and with it whatever ending that move brought. Returns true, or false when no move has been made.

moves

my @moves = $game->moves;
my $count = $game->moves;

The moves of the side to move: a list in list context, a count in scalar context. Empty once the game is over, whatever the pieces could do.

outcome

One of the outcome constants.

is_over

True when outcome is not ONGOING.

is_draw

True for the three draws.

winner

Game::Brandubh::Engine::ATTACKERS or DEFENDERS, or undef when the game is drawn or not over.

winner_of

my $side = Game::Brandubh::Rules->winner_of(BY_CAPTURE);

The side an outcome is a win for, or undef.

repeats

How many times the position on the board has occurred in this game, the present time included. 1 for a position seen for the first time.

ply

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

ply_cap

The limit this game is playing to.

variant

A hash reference of the rule set this game is playing, every field, as the game took them.

position

The position as a string. See "to_string" in Game::Brandubh::Engine.

board

A Game::Brandubh::Engine holding the same position. It is a copy: nothing done to it reaches the game.

side

Whose move it is.

at

my $piece = $game->at($square);

What stands on a square.

count

my $n = $game->count(Game::Brandubh::Engine::ATTACKER);

How many of one piece are on the board.

key_hex

The key of the position on the board. See "The key is a hex string, never a number" in Game::Brandubh::Engine.

key_at

my $hex = $game->key_at($ply);

The key of the position after that many moves, 0 being where the game began. undef for a ply the game has not reached.

preview

my ($flags, @squares) = $game->preview($mv);

What a move would report and which squares it would empty, under this game's rule set, without playing it.

why_not

my $why = $game->why_not($from, $to);

One of the WHY_* constants of Game::Brandubh::Engine, under this game's rule set. It answers for the position and does not know the game is over.

live

How many games exist in this process and have not been released.

my $found = $game->search(budget => 20_000);
my $found = $game->search(budget => 20_000, seed => 7, depth => 4, weights => \%weights);

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

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

budget

How many positions the search may look at. It is the only limit: the search never reads a clock, so the same game, budget and seed give the same move on a busy machine as on an idle one. The count is checked every 1,024 positions and may be passed by that many.

seed

A whole number below 4,294,967,296 that settles the choice among moves the search scores alike, and nothing else. 1 when left out.

depth

Stop after looking this many moves ahead, even with budget left. No limit when left out.

weights

A hash reference changing some of what weights returns.

What comes back is a hash reference: move, in the engine's numbers; depth, how many moves ahead the search finished looking; score, for the side to move, where anything above 29,000 is a win found and anything below -29,000 a loss; nodes, how many positions it looked at, as a string of digits; and stopped, true when the budget ran out part way through looking one move further, in which case that unfinished look is discarded.

Croaks on a budget, seed or depth that is not a whole number, and on a weight that does not exist.

evaluate

my $score = $game->evaluate;
my $score = $game->evaluate(\%weights);

The position as the search scores it when it looks no further: a whole number, positive when it favours the attackers.

weights

my $weights = Game::Brandubh::Rules->weights;

A hash reference of the seven numbers the evaluation is made of: attacker and defender for each piece on the board; lane_one for each winning square the king can reach in one move and lane_two for each he can reach in two; freedom for each square the king can move to; corner_guard for each attacker on one of the three squares that close a corner; ring for each attacker next to the king.

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)