yahtzee-engine 0.1.0

Yahtzee rules, scoring, and bots: a fast heuristic and an exact optimal expected-value solver
Documentation
//! Statistical tripwires and the full-solve anchor.  Slow: every test
//! is `#[ignore]`d — run them in a release build:
//!
//! ```sh
//! cargo test --release --all-features -- --ignored
//! ```

use yahtzee_engine::{Solver, State};

/// Skip statistical work in debug builds, where it would take minutes.
fn debug_build() -> bool {
    if cfg!(debug_assertions) {
        eprintln!("skipped: run this tripwire with --release");
        return true;
    }
    false
}

/// The end-to-end anchor: the optimal expected score under the official
/// forced-joker rules.  (Verhoeff's widely cited 254.5896 uses a laxer
/// joker convention; lifting the forcing reproduces it exactly.)
#[test]
#[ignore = "solves the full game; run with --release -- --ignored"]
fn optimal_ev_matches_the_forced_joker_value() {
    if debug_build() {
        return;
    }
    let mut solver = Solver::new();
    solver.solve();
    let value = solver.value(State::START);
    assert!((value - 254.5877).abs() < 1e-3, "got {value}");
}

#[cfg(feature = "rand")]
mod empirical {
    use super::debug_build;
    use rand::SeedableRng;
    use yahtzee_engine::{Game, HeuristicBot, OptimalBot, Strategy, play_game};

    fn mean_score(strategy: &mut dyn Strategy, games: u32) -> f64 {
        let mut rng = rand::rngs::StdRng::seed_from_u64(0x5EED);
        let mut sum = 0.0;
        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 / f64::from(games)
    }

    /// The heuristic must not silently regress below its measured
    /// strength (216.0 as tuned; see `examples/arena.rs`).
    #[test]
    #[ignore = "statistical tripwire; run with --release -- --ignored"]
    fn heuristic_mean_score_clears_the_floor() {
        if debug_build() {
            return;
        }
        let mean = mean_score(&mut HeuristicBot::new(), 10_000);
        assert!(mean >= 205.0, "heuristic mean dropped to {mean}");
    }

    /// The optimal bot's empirical mean must straddle its computed
    /// expectation (σ ≈ 59.6, so 2000 games put the standard error
    /// near 1.3 points).
    #[test]
    #[ignore = "solves the full game and simulates; run with --release -- --ignored"]
    fn optimal_bot_scores_its_expected_value() {
        if debug_build() {
            return;
        }
        let mean = mean_score(&mut OptimalBot::presolved(), 2_000);
        assert!((mean - 254.5877).abs() < 5.0, "optimal bot averaged {mean}");
    }
}