Have more questions? Join our Discord

Offers, intents and decisions

The offers game function returns the seat's offers: every legal move now. The client draws only these, Boardweaver accepts only these, and bots choose only among these. Each offer carries an intent, and the intent decides how the move flows. Tagging intents right is the most important rule of a game.

Actions

pub enum Action {
    SpaceClick { space_id: String },
    PieceClick { space_id: String, index: usize },
    Command { name: String, value: Option<InputValue> },
}
  • SpaceClick and PieceClick are clicks on the board. A piece click names the piece by its index in the state the game logic receives.
  • A Command is every other move, such as passing or naming a city. The game names it. The client draws its button. Make one with Action::command("pass"), and match it with Action::Command { name, .. }.

Offers

Offer::new(Action::command("pass"), Intent::Confirm, "Pass")
  • label says what the move does in words. Bots and logs read it, and a client may show it on a command's button. Only the seat that receives the offer sees it.
  • .revealing() marks a reveal (Hidden information).
  • The same action may be offered more than once with different inputs. A move matches if it fits any of them.

Command inputs

A command may ask for one value:

Offer::command_with(
    "name-city",
    Input::Text { max_len: 24, suggestions: vec!["Aldmoor".into(), "Brennick".into()] },
    Intent::Choice,
    "Name your city",
)

Offer::command_with(
    "bid",
    Input::Integer { min: 0, max: coins, step: Some(5) },
    Intent::Confirm,
    "Bid coins",
)
  • Text takes 1 to max_len characters. It needs at least one suggestion, because bots give only suggestions.
  • Integer takes min to max, both included, and when step is set, only min plus a multiple of step.
  • command_with panics on an input nobody can give, such as min above max.
  • Read the value in apply_action with action.text() or action.integer(). The platform has already checked that it fits.

A move that needs several values is several steps, one value each, so each step can narrow the next. "Give 3 wood to Ana" is three steps: a choice of Ana, a choice of wood, then a confirm of the amount whose max is the wood the seat holds.

Intents

Intent Means Bots
Choice Adds new information to the open decision and moves it forward. take it
Confirm Ends the decision and sends it. take it
Cancel Changes or takes back something already chosen in the open decision. ignore it

Two rules follow, and a game must keep both:

  1. Every state inside an open decision offers at least one Choice or Confirm. A bot ignores Cancel, so it must always have something else to take.
  2. A Choice never returns the decision to a state it had before. A bot taking only Choice and Confirm must always reach the end.

Worked example: a draft of 12 cards

The seat stages one card, may switch to another, and then takes the staged card.

  • Nothing staged: each of the 12 cards is a Choice.
  • One card staged: the other 11 cards are still offered, so the seat can switch, but as Cancel. The take command is a Confirm.
#[derive(Default, Serialize, Deserialize)]
#[serde(crate = "boardweaver_sdk::serde", default)]
struct Pick {
    staged: Option<usize>,
}

fn offers(table: &Table, player_id: PlayerId) -> Vec<Offer> {
    let pick: Pick = table.state().read(StateOf::PlayerPrivate(player_id));
    let switch = if pick.staged.is_some() { Intent::Cancel } else { Intent::Choice };
    let mut offers: Vec<Offer> = (0..table.state().space("market").pieces.len())
        .filter(|&index| pick.staged != Some(index))
        .map(|index| {
            Offer::new(
                Action::PieceClick { space_id: "market".into(), index },
                switch,
                format!("Stage card {index}"),
            )
        })
        .collect();
    if pick.staged.is_some() {
        offers.push(Offer::new(Action::command("take"), Intent::Confirm, "Take the staged card"));
    }
    offers
}

The common mistake is to tag the 11 switches as Choice. Then staging card A, then card B, then card A again returns the decision to a state it had, and a bot can switch between cards forever.

What ends a decision

A decision stays open, as the seat's draft, until a step ends it. The first rule that applies decides:

  1. The step drew from Rng: the decision ends, whatever the step's intent.
  2. The step is a reveal: the decision ends.
  3. The step's intent is Confirm: the decision ends.
  4. Otherwise the decision stays open.

So only the last step of a decision can draw randomness or reveal. Plan multi-step moves so the random or revealing part comes last.

Undo

Undo is not an intent and not an action: it is the platform's, and the client shows its button. The game logic allows it with table.allow_undo() (Changing the state).