Have more questions? Join our Discord

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_id is the bot's seat.
  • turn.seen is the seat's view, with the bot's earlier steps of this decision applied. A bot sees only what its seat may see.
  • turn.offered holds only Choice and Confirm offers, with every command input expanded into one offer for each value. Piece indexes are positions in turn.seen.
  • turn.step counts the steps the bot has already taken in this decision.
  • turn.fuel_limit is 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 choice or confirm, 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 choice or confirm;
  • the match has not ended after max_decisions decisions.

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.