Bots
A game may ship bots: computer opponents written in Rust beside the game logic.
A bot
use boardweaver_sdk::{ApplyInput, Bot, BotTurn, Difficulty, Rng};
pub struct Greedy;
impl Bot for Greedy {
const ID: &'static str = "greedy";
const NAME: &'static str = "Greedy";
const DESCRIPTION: &'static str = "Takes whatever scores most right now.";
const DIFFICULTY: Option<Difficulty> = Some(Difficulty::Easy);
fn choose(turn: &BotTurn, _rng: &mut Rng) -> Option<usize> {
turn.offered
.iter()
.enumerate()
.filter_map(|(index, offer)| {
let mut simulated = turn.simulate::<MyGame>();
let output = simulated
.apply(ApplyInput {
player_id: turn.player_id,
action: offer.action.clone(),
seed: None,
})
.ok()?;
let points = output.evaluation.scores[&turn.player_id].public_points[0];
Some((index, points))
})
.max_by(|a, b| a.1.total_cmp(&b.1))
.map(|(index, _)| index)
}
}
boardweaver_sdk::export_game!(MyGame, bots: [Greedy]);
The profile
| Constant | Rule |
|---|---|
ID |
1 to 32 characters of a-z, 0-9 and -. Never change it: seats and history store it. |
NAME |
1 to 40 characters. The lobby shows it. |
DESCRIPTION |
1 to 280 characters. |
DIFFICULTY |
Some(Difficulty::Easy), Standard or Hard, or None. |
A game ships at most 16 bots, each with its own ID. export_game! checks the list when the game compiles, so a bad profile is a build error.
Write &'static str. The build refuses the word static on its own, but not the 'static lifetime.
choose
choose answers the index of one offer in turn.offered.
turn.player_idis the bot's seat.turn.seenis the seat's view, with the bot's earlier steps of this decision applied. A bot sees only what its seat may see.turn.offeredholds onlyChoiceandConfirmoffers, with every command input expanded into one offer for each value. Piece indexes are positions inturn.seen.turn.stepcounts the steps the bot has already taken in this decision.turn.fuel_limitis the work this call may do. Size a search to it.
A bot keeps nothing between calls and has no clock. When choose answers None, an index out of range, panics or runs past its budget, the platform plays a random offer for it.
Looking ahead
turn.simulate::<MyGame>() gives a match loaded from the seat's view. Apply offers to it to see what happens. Each call gives a fresh match, and apply changes the match it is called on.
A simulated step that draws randomness or reveals needs a seed: pass seed: Some(rng.seed()). That seed is the bot's own, and tells it nothing about the real move.
Testing a bot
boardweaver_sdk::checks::play_bots::<MyGame>(seats, seed, max_decisions) plays a whole match in a unit test, with a bot in every seat, as the game runner does. seats lists each seat's player id and the id of its bot, in seat order. max_decisions is the most decisions the match may take. It answers the final state.
It answers a BotMistake when:
- a bot answers
None, an index out of range, or panics, so the platform would play a random offer for it; - a seat names a bot the game does not ship, or two seats name the same player id;
- a decision offers the bot no
choiceorconfirm, the game refuses every offer the bot takes, or the decision does not end within 64 steps; - the game is not over and no seat has a
choiceorconfirm; - the match has not ended after
max_decisionsdecisions.
The message of a mistake at a step names the decision, the step, the seat and the bot.
Call it in a #[test] for each bot, with a few seeds. This test, shortened from the tic-tac-toe seed, plays four games between two perfect bots:
#[cfg(test)]
mod tests {
use super::*;
use boardweaver_sdk::Seed;
use boardweaver_sdk::checks::play_bots;
#[test]
fn perfect_finishes_every_game() {
for byte in 0..4 {
play_bots::<TicTacToe>(&[(11, "perfect"), (22, "perfect")], Seed([byte; 32]), 9)
.unwrap_or_else(|mistake| panic!("{mistake}"));
}
}
}
Play a few short games in each test. The limits are in Builds, limits and errors.