Have more questions? Join our Discord

The game functions

A game is a type that implements the Game trait. A trait is Rust's name for an interface: a list of functions a type must provide. The game writes all six functions. They take no self, so the game keeps nothing between calls: everything lives in the state.

pub trait Game {
    fn score_labels(players: &[PlayerId]) -> Vec<String>;
    fn setup(table: &mut Table, players: &[PlayerId], rng: &mut Rng);
    fn offers(table: &Table, player_id: PlayerId) -> Vec<Offer>;
    fn apply_action(
        table: &mut Table,
        player_id: PlayerId,
        action: &Action,
        rng: &mut Rng,
    ) -> Result<(), String>;
    fn scores(table: &Table) -> Scores;
    fn is_game_over(table: &Table, scores: &Scores) -> bool;
}

The game writes pub struct MyGame;, then impl Game for MyGame { ... } with the six functions, then export_game! (below). For a complete game, read the tic-tac-toe seed with get_example, name tic-tac-toe-rust.

setup

Builds a new match: add spaces and pieces, write maps, shuffle and deal, choose who starts with set_active_players, and optionally write the opening's log lines with table.log.

  • The platform guarantees: players are seated in seat order, and all of them are active until setup says otherwise.
  • setup cannot refuse. It must succeed for every seat count the game allows.

offers

Every legal move for player_id now, each with its intent and label. See Offers, intents and decisions.

  • The platform calls it only for seats in active_player_ids, and never after the game is over.
  • It also runs on the seat's view, where hidden parts are missing (Hidden information).
  • Return an empty list when the seat has nothing to do.

apply_action

Applies one move. Before the call, the platform has checked that the game offered this action to this seat and that a command's value fits its input. So the game logic handles only moves it offered.

  • Return Ok(()) after the change.
  • Return Err(reason) to refuse a move by a rule the offer could not express, such as a city name another seat already took. The player sees the reason, and every change of the call is undone. Prefer narrowing the offers to refusing.
  • A panic is a bug. It fails the move and is reported as a match error.

Read a command's value with action.text() or action.integer(). Match a command by name:

match action {
    Action::Command { name, .. } if name == "pass" => {
        table.log("passes");
    }
    Action::PieceClick { space_id, index } => {
        table.move_piece(space_id, *index, "discard");
    }
    _ => {}
}

scores and score_labels

scores returns every seat's score after every move. score_labels names the score columns once, for the whole match.

pub struct Score {
    pub public_points: Vec<f64>,
    pub private_points: Vec<f64>,
}
  • Column i of a seat's score is public_points[i] + private_points[i]. score_labels()[i] names column i.
  • Seats rank by the first column, highest first. The second column breaks a tie on the first, and so on.
  • During play, every seat sees each other seat's public points and only its own private points. Use private points for scores a seat must not see yet, such as hidden objectives.

is_game_over

Returns whether the match has ended. It receives the scores scores just computed. Once it returns true, nobody is offered anything and every later move is refused.

export_game!

The last line of /src/lib.rs exports the game: boardweaver_sdk::export_game!(MyGame);. A game with bots lists them: export_game!(MyGame, bots: [Greedy, Careful]). See Bots. An empty list, bots: [], does not compile: leave the list out instead.