Skip to main content

shuyi_core/
bots.rs

1//! Playtest bots over any [`TurnRules`] game: players made of code, the
2//! loop that lets them play a game to its end, a seeded random bot, two
3//! bots that plan on copies of the game (over [`determinize`]): one that
4//! weighs its next command alone ([`rollout`]) and one that plans its
5//! side's whole turn, several commands deep ([`plan`]), the order of
6//! the few commands such a bot tries first ([`order`]), a playtest
7//! runner that lets several bots play a stage many times and checks how
8//! often each succeeds ([`playtest`]), and an autobalance search that
9//! changes the stage, never the bots, until each succeeds about as often
10//! as wanted ([`balance`]), and a score made of the game's named counts
11//! times a persona's weights, with personas as data ([`weigh`]).
12//!
13//! A game's own bots (a baseline, personas) implement [`Bot`] for its
14//! state, and its measures watch the loop through [`play_observed`]. Bots
15//! never rewind: the loop keeps no history. Like the rest of the core, a
16//! bot is pure: the same seed and the same states give the same commands.
17
18use alloc::vec::Vec;
19use core::fmt;
20
21use crate::rng::SimRng;
22use crate::turn::TurnRules;
23
24pub mod balance;
25pub mod determinize;
26pub mod order;
27pub mod plan;
28pub mod playtest;
29pub mod rollout;
30pub mod weigh;
31
32pub use balance::{BalanceSettings, Balanced, Knob, Stop, autobalance, autobalance_by};
33pub use determinize::{Determinize, Evaluate};
34pub use order::{Ranked, ranked_order};
35pub use plan::{PlanSearch, PlanSettings, TurnPlan};
36pub use rollout::{Horizon, Order, Playout, Rollout, RolloutSearch, RolloutSettings, Unordered};
37pub use weigh::{Persona, UnknownFeature, Weighted, Weights};
38
39/// A game whose state says whose move it is and which turn it is, so a
40/// loop can ask the right bot for each command.
41pub trait Sides: TurnRules {
42    /// Who moves: a player, a faction, a phase.
43    type Side: Copy + Eq + fmt::Debug;
44
45    /// Who issues the next command.
46    fn side_to_move(&self) -> Self::Side;
47
48    /// The turn being played, counting from the game's first.
49    fn turn(&self) -> u32;
50}
51
52/// A player made of code. [`play`] asks it for every command of the sides
53/// it plays.
54///
55/// A bot must never learn what the game hides from its players (the luck
56/// to come, above all): any randomness of its own comes from a seed it
57/// was built with. The same seed and the same states must give the same
58/// commands.
59pub trait Bot<R: TurnRules> {
60    /// The next command in `rules`, which is not over and has this bot's
61    /// side to move. It should be one [`TurnRules::check`] accepts; a
62    /// refused command ends the play with a [`PlayError`].
63    fn choose(&mut self, rules: &R) -> R::Command;
64}
65
66/// A function of the state is a bot (one with no seed, or a seed of its
67/// own captured).
68impl<R: TurnRules, F: FnMut(&R) -> R::Command> Bot<R> for F {
69    fn choose(&mut self, rules: &R) -> R::Command {
70        self(rules)
71    }
72}
73
74/// How a play went.
75pub struct Report<R: TurnRules> {
76    /// How the game ended; `None` if the turn limit stopped it first.
77    pub outcome: Option<R::Outcome>,
78    /// The turn the game was on when the play stopped: past the limit if
79    /// the limit stopped it.
80    pub turns: u32,
81    /// Every command applied, in order.
82    pub commands: Vec<R::Command>,
83}
84
85/// A command the game refused during a play: a bug in a bot, never
86/// skipped. Build it with [`PlayError::new`] (ADR-0039).
87#[non_exhaustive]
88pub struct PlayError<R: Sides> {
89    /// The refused command.
90    pub command: R::Command,
91    /// The side that issued it.
92    pub side: R::Side,
93    /// The turn it was issued on.
94    pub turn: u32,
95    /// Why it was refused.
96    pub error: R::Error,
97}
98
99impl<R: Sides> PlayError<R> {
100    /// `side`'s command `command`, issued on turn `turn`, refused with
101    /// `error`.
102    pub fn new(command: R::Command, side: R::Side, turn: u32, error: R::Error) -> Self {
103        Self {
104            command,
105            side,
106            turn,
107            error,
108        }
109    }
110}
111
112// By hand rather than derived: a derive would ask the game's state itself
113// to be `Clone`, `Debug` and so on, where only its commands and outcomes
114// need to be.
115
116impl<R: TurnRules> fmt::Debug for Report<R>
117where
118    R::Outcome: fmt::Debug,
119    R::Command: fmt::Debug,
120{
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        f.debug_struct("Report")
123            .field("outcome", &self.outcome)
124            .field("turns", &self.turns)
125            .field("commands", &self.commands)
126            .finish()
127    }
128}
129
130impl<R: TurnRules> Clone for Report<R>
131where
132    R::Outcome: Clone,
133    R::Command: Clone,
134{
135    fn clone(&self) -> Self {
136        Self {
137            outcome: self.outcome.clone(),
138            turns: self.turns,
139            commands: self.commands.clone(),
140        }
141    }
142}
143
144impl<R: TurnRules> PartialEq for Report<R>
145where
146    R::Outcome: PartialEq,
147    R::Command: PartialEq,
148{
149    fn eq(&self, other: &Self) -> bool {
150        self.outcome == other.outcome
151            && self.turns == other.turns
152            && self.commands == other.commands
153    }
154}
155
156impl<R: Sides> fmt::Debug for PlayError<R>
157where
158    R::Command: fmt::Debug,
159    R::Error: fmt::Debug,
160{
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        f.debug_struct("PlayError")
163            .field("command", &self.command)
164            .field("side", &self.side)
165            .field("turn", &self.turn)
166            .field("error", &self.error)
167            .finish()
168    }
169}
170
171impl<R: Sides> fmt::Display for PlayError<R>
172where
173    R::Command: fmt::Debug,
174    R::Error: fmt::Display,
175{
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        write!(
178            f,
179            "turn {}, {:?} to move: {:?} refused: {}",
180            self.turn, self.side, self.command, self.error
181        )
182    }
183}
184
185/// Plays `rules` until the game ends or turn `limit` has passed, asking
186/// the bot of the side to move for each command. `bots_by_side` gives one
187/// bot per side; a side with no bot there is a mistake in the caller and
188/// panics.
189///
190/// A play stopped by the limit has no outcome, and its
191/// [`turns`](Report::turns) is the turn that was starting.
192pub fn play<R: Sides>(
193    rules: &mut R,
194    bots_by_side: &mut [(R::Side, &mut dyn Bot<R>)],
195    limit: u32,
196) -> Result<Report<R>, PlayError<R>>
197where
198    R::Command: Clone,
199{
200    play_observed(rules, bots_by_side, limit, |_, _, _| {})
201}
202
203/// [`play`], telling `observe` about every applied command: the game
204/// after it, the command and the events it gave. A game's measures are
205/// built this way.
206pub fn play_observed<R: Sides>(
207    rules: &mut R,
208    bots_by_side: &mut [(R::Side, &mut dyn Bot<R>)],
209    limit: u32,
210    mut observe: impl FnMut(&R, &R::Command, &[R::Event]),
211) -> Result<Report<R>, PlayError<R>>
212where
213    R::Command: Clone,
214{
215    let mut commands = Vec::new();
216    while rules.outcome().is_none() && rules.turn() <= limit {
217        let side = rules.side_to_move();
218        let Some((_, bot)) = bots_by_side.iter_mut().find(|(s, _)| *s == side) else {
219            panic!("no bot plays side {side:?}");
220        };
221        let command = bot.choose(rules);
222        match rules.apply(&command) {
223            Ok(events) => {
224                observe(rules, &command, &events);
225                commands.push(command);
226            }
227            Err(error) => {
228                return Err(PlayError {
229                    command,
230                    side,
231                    turn: rules.turn(),
232                    error,
233                });
234            }
235        }
236    }
237    Ok(Report {
238        outcome: rules.outcome(),
239        turns: rules.turn(),
240        commands,
241    })
242}
243
244/// A bot that picks one of [`TurnRules::legal_commands`] at random, from
245/// its own seeded RNG.
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct RandomBot {
248    rng: SimRng,
249}
250
251impl RandomBot {
252    /// A random bot whose choices follow from `seed`.
253    pub fn new(seed: u64) -> Self {
254        Self {
255            rng: SimRng::new(seed),
256        }
257    }
258}
259
260impl<R: TurnRules> Bot<R> for RandomBot {
261    /// One of the legal commands, each as likely as the others.
262    ///
263    /// # Panics
264    ///
265    /// If the game lists no legal command though it is not over.
266    fn choose(&mut self, rules: &R) -> R::Command {
267        let mut legal = rules.legal_commands();
268        assert!(!legal.is_empty(), "no legal command to choose from");
269        let count = u32::try_from(legal.len()).unwrap_or(u32::MAX);
270        let pick = usize::try_from(self.rng.roll_below(count)).unwrap_or(0);
271        legal.swap_remove(pick)
272    }
273}
274
275#[cfg(test)]
276pub(crate) mod fixtures;
277#[cfg(test)]
278mod tests;