yahtzee-engine 0.1.0

Yahtzee rules, scoring, and bots: a fast heuristic and an exact optimal expected-value solver
Documentation
//! Measures bot strength: mean solo score over many games.
//!
//! ```sh
//! cargo run --release --example arena -- 10000
//! cargo run --release --example arena --features parallel -- 1000 --optimal
//! ```

use rand::SeedableRng;
use yahtzee_engine::{Game, HeuristicBot, OptimalBot, Strategy, play_game};

fn measure(name: &str, strategy: &mut dyn Strategy, games: u32) {
    let mut rng = rand::rngs::StdRng::seed_from_u64(0x5EED);
    let (mut sum, mut sum_sq) = (0.0f64, 0.0f64);
    for _ in 0..games {
        let mut game = Game::new(1);
        let total = play_game(&mut game, &mut [strategy], &mut rng).expect("bots play legally")[0];
        sum += f64::from(total);
        sum_sq += f64::from(total) * f64::from(total);
    }
    let mean = sum / f64::from(games);
    let variance = sum_sq / f64::from(games) - mean * mean;
    let stderr = (variance / f64::from(games)).sqrt();
    println!("{name:12} {mean:7.2} ± {stderr:.2} over {games} games");
}

fn main() {
    // Order-independent: any numeric argument is the game count, so
    // `arena -- --optimal` alone works too.
    let args: Vec<String> = std::env::args().skip(1).collect();
    let games: u32 = args
        .iter()
        .find_map(|arg| arg.parse().ok())
        .unwrap_or(10_000);
    let optimal = args.iter().any(|arg| arg == "--optimal");

    measure("heuristic", &mut HeuristicBot::new(), games);
    if optimal {
        eprintln!("solving the full table...");
        measure("optimal", &mut OptimalBot::presolved(), games);
    }
}