NAME

Game::Dominoes::Bot - a determinised search that plays without peeking

VERSION

Version 0.01

SYNOPSIS

use Game::Dominoes;
use Game::Dominoes::Bot;

my $game = Game::Dominoes->new(seed => $bytes, players => 2);
my $bot = Game::Dominoes::Bot->new(level => 3, seed => 12345);

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

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

DESCRIPTION

Dominoes hides the other hands and the boneyard, so this bot is built the way cribbage's is and not the way checkers' is: it is handed one seat's view and reads nothing else. Not hand($other), not the boneyard's tiles, not the seed. t/19-bot-blind.t proves that rather than trusting it.

How it plays

Levels 1 and 2 are greedy: take the highest scoring play, with a small term at level 2 for keeping a useful hand shape.

Levels 3 and up determinise. Sample a number of complete worlds consistent with what the seat can see, play each one out greedily, and average. This is the standard approach for trick and tile games.

Its weakness, named so nobody rediscovers it as a bug

A determinised search assumes it will know the hidden tiles from the next move onward. So it never plays to gain information, and it overvalues plays whose payoff depends on a tile falling a particular way. It will occasionally make a move a good human can see is optimistic.

That is the algorithm and not a defect. The fix, if one is ever wanted, is a different algorithm and a different plan, not a tweak here.

It also models every opponent as greedy inside a rollout, which is cheap and wrong in the same direction for everybody.

Sampling, which is where the strength comes from

A uniform guess at the hidden tiles is legal and weak. These constraints are what make the bot worth playing, and every one is public:

  • Hand sizes are public, so each sampled hand gets exactly its known count.

  • Tiles on the table and in our own hand are excluded.

  • A seat that passed holds nothing matching the ends that were open then. This is the deduction a good human player is making. It is a hard rejection rather than a weight, because a world that violates it is impossible.

The pass deduction is sound for the rest of the hand, not just the moment it was made: a pass only happens once the boneyard is exhausted, so no tile ever enters that hand afterwards and the hand only shrinks.

A draw is deliberately not used as a constraint. A seat that drew was short of the ends open at that moment, but the tiles it held then are mixed in with what it drew, and nothing public says which are which. Applying it as though the whole current hand were constrained would be unsound, so it is recorded in the log and not used.

If the constraints make a world hard to find, the sampler gives up after $SAMPLE_TRIES attempts and takes the loosest consistent sample. A bot that hangs inside a database transaction is worse than a bot that plays weakly.

This class cannot be subclassed, and the failure is silent

Object::Proto::Sugar installs new so that it blesses into the class that declared the attributes, not into the invocant. So this:

package My::Bot;
our @ISA = ('Game::Dominoes::Bot');
sub choose { ... }              # never runs

hands back a plain Game::Dominoes::Bot, and the override is never reached. Nothing warns. ref $bot is the only thing that gives it away.

To wrap this bot, compose it rather than inherit from it: hold one as an attribute and call through to it. The site adapter does exactly that, and t/19-bot-blind.t builds its deliberately cheating bot as a standalone package for the same reason, having first been written as a subclass and having silently passed for the wrong reason.

Budgets are in worlds and plies, never in seconds

A loaded smoker must return the same play as an idle laptop, because a game log that cannot be replayed is not a game log. Nothing here reads a clock and no test asserts a duration.

The randomness comes from the bot's own seed and the public state, never from the game's seed. The game seed generates the shuffle the bot is trying to estimate, so a sampler that reached for it would reconstruct the hands it is guessing at, and the bot would become a cheat through a line that looks like plumbing.

PROPERTIES

level

$bot->level;   # 1 to 5

How hard it plays. Levels 1 and 2 do not sample. See levels.

seed

$bot->seed;

The bot's own seed, which together with the public state decides every sample. Two bots with the same seed and level choose identically from the same view.

$bot->last_search;
# { level, worlds, depth, considered, score, points, play }

What the last call to choose cost and decided.

FUNCTIONS

choose

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

The play to make, as one of the { tile, arm } hashrefs Game::Dominoes::Rules generates, or undef when there is nothing to play.

Accepts a Game::Dominoes and a seat, or a view on its own. Either way only the view is read.

Equal scores are broken by the heavier tile, then the canonical tile id, then the arm order, so the choice is total and the same on every machine and every perl.

levels

Game::Dominoes::Bot->levels;

The level table as a hashref, for a caller that wants to show what a level means.

SEE ALSO

Game::Dominoes, whose view this reads; Game::Dominoes::Rules, which generates the moves.

AUTHOR

LNATION <email@lnation.org>

BUGS

Please report any bugs or feature requests to bug-game-dominoes at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Game-Dominoes.

SUPPORT

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

perldoc Game::Dominoes::Bot

ACKNOWLEDGEMENTS

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)