NAME

Game::Checkers::Bot - an opponent, at five strengths

VERSION

Version 0.01

SYNOPSIS

use Game::Checkers;
use Game::Checkers::Bot;

my $game = Game::Checkers->new;
my $bot = Game::Checkers::Bot->new(level => 3, seed => 12345);

while ($game->status eq 'active') {
	my $move = $bot->choose($game);
	$game->move($move);
}

$bot->last_search->{nodes};   # what the last move cost

DESCRIPTION

Negamax with alpha beta, iterative deepening to a node budget, and an evaluation of material, position and mobility.

choose is a pure function of the position, the side to move, the level and the seed. The same four give the same move on every machine and every perl, because a game log that cannot be replayed is not a game log. Nothing here reads a clock, calls rand, or allocates a Game::Checkers::Move except the one it returns: the search runs on the raw position array through Game::Checkers::Rules.

  • The budget is in nodes, never in seconds. A loaded machine must produce the same move as an idle one, so no part of this module knows what time it is. An iteration that runs out of budget is discarded whole, and the move comes from the last iteration that finished.

  • A forced capture does not cost depth. When every legal move is a jump the position is not a choice, so the search goes on without decrementing, up to eight extension plies from any node. Without that, a search stops in the middle of an exchange and reads a position as a piece up when it is about to be a piece down.

  • Move ordering at every node: the transposition table's move, then jumps by how much they take, then promotions, then two killer moves for the ply, then the history heuristic, then generation order.

  • The transposition table is a plain hash keyed by the packed position and the side to move, emptied at the start of every choose, so one search cannot influence the next. A cutoff is taken only from an entry at least as deep as the node wants, and never from a score near mate, whose value depends on how far away it is.

  • Terminal nodes. A side with no move has lost, scored so that a quicker win beats a slower one. A repetition or the no progress rule scores nothing, counted against the game's own history as well as the search path, so the bot neither walks into a draw while winning nor misses one while losing.

The evaluation

In hundredths of a man, from the side to move's point of view:

  • Material: a man 100, a king 160.

  • A man is worth 4 more for every row it has advanced, 6 less on the outside file, and every piece is worth 4 more on the two centre files.

  • A piece still on its home row is worth 8, but only while the other side has a man that could crown there.

  • Mobility is 2 a move, counted as the steps and jumps each piece has rather than by generating the sequences, which costs four lookups a piece instead of a move list at every leaf. A king with nothing at all is worth 20 less.

  • With six pieces or fewer on the board, the side that is ahead loses 2 for every square between its kings and the enemy. That term is what makes a won ending get won instead of shuffled into the forty move draw.

The weights are in %WEIGHT and the search does not know what is in it.

The levels

level   depth   nodes     jitter
  1       2        300    yes
  2       4      2_000    yes
  3       7     20_000    no
  4      10    120_000    no
  5      13    600_000    no

Levels 1 and 2 add a deterministic offset of up to a quarter of a man to each move at the root, derived from the seed and the move, so an easy opponent does not play the same game every time while staying reproducible. Level 3 and above have none: there the point is strength, and a move that is a pure function of the position is what a replay wants.

PROPERTIES

level

Read and write integer 1 to 5, 3 by default.

$bot->level(5);

seed

Read and write integer feeding the jitter at levels 1 and 2. It changes nothing at level 3 and above.

$bot->seed(12345);

transposition

Read and write boolean, true by default. Turning it off makes the search slower and must not change the move it chooses, which is what the test asserts.

$bot->transposition(0);

Read and write hashref describing the last choose: depth reached, nodes visited, score in hundredths of a man, move, pv as a list of notations, and forced when there was only one legal move and no search happened.

$bot->last_search->{pv};

FUNCTIONS

choose

The move the bot would play, as one of the Game::Checkers::Move objects in the game's own legal list, or undef when the game is over. A position with one legal move returns it without searching.

my $move = $bot->choose($game);

CONSTANTS

MATE, INFINITY, MAX_EXTENSION, MAX_PV, NO_PROGRESS_PLIES and the transposition flags EXACT, LOWER and UPPER.

AUTHOR

LNATION, <email at lnation.org>

BUGS

Please report any bugs or feature requests to bug-game-checkers at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Game-Checkers. 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::Checkers

You can also look for information at:

ACKNOWLEDGEMENTS

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)