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 search
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);
last_search
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:
RT: CPAN's request tracker (report bugs here)
Search CPAN
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)