//! DEEPFALL — a pure, deterministic, headless engine for the dice-bag delve game.
//!
//! See `../DESIGN.md` for the full design and the simulation model. The crate is
//! consumed by `deepfall-sim` (the balance-by-bot harness). Determinism is
//! load-bearing: one `(Config, seed, roster)` reproduces one game bit-for-bit, so the
//! sweeps can attribute an outcome delta to the single knob that changed.

pub mod config;
pub mod content;
pub mod controller;
pub mod game;
pub mod rng;

pub use config::Config;
pub use content::{Calling, DieKind, Face, Lineage, Theme, ALL_CALLINGS, ALL_LINEAGES};
pub use controller::{Strategy, ALL_STRATEGIES};
pub use game::{Game, GameResult, PlayerResult};

/// Convenience: play one game from a roster of `(strategy, optional fixed combo)`.
pub fn simulate(
    cfg: Config,
    seed: u64,
    roster: &[(Strategy, Option<(Lineage, Calling)>)],
) -> GameResult {
    Game::new(cfg, seed, roster).run()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn roster4() -> Vec<(Strategy, Option<(Lineage, Calling)>)> {
        vec![
            (Strategy::CashOut, None),
            (Strategy::PressOn, None),
            (Strategy::WorthyEnd, None),
            (Strategy::Balanced, None),
        ]
    }

    #[test]
    fn determinism_same_seed_same_result() {
        let a = simulate(Config::default(), 12345, &roster4());
        let b = simulate(Config::default(), 12345, &roster4());
        assert_eq!(a.rounds, b.rounds);
        assert_eq!(a.winner, b.winner);
        for (x, y) in a.players.iter().zip(b.players.iter()) {
            assert!((x.glory - y.glory).abs() < 1e-9, "glory diverged: {} vs {}", x.glory, y.glory);
        }
    }

    #[test]
    fn different_seeds_vary() {
        let mut winners = std::collections::HashSet::new();
        let mut lengths = std::collections::HashSet::new();
        for seed in 0..50 {
            let r = simulate(Config::default(), seed, &roster4());
            winners.insert(r.winner);
            lengths.insert(r.rounds);
        }
        assert!(winners.len() > 1, "RNG should produce different winners across seeds");
        assert!(lengths.len() > 1, "game length should vary across seeds");
    }

    #[test]
    fn game_ends_in_sane_window() {
        // The Maw must reliably end the game; never hit the safety cap.
        for seed in 0..200 {
            let r = simulate(Config::default(), seed, &roster4());
            assert!(r.rounds >= 3, "game ended too fast: {} rounds", r.rounds);
            assert!(
                r.rounds < Config::default().max_rounds,
                "game hit the max_rounds safety cap at seed {seed} ({} rounds)",
                r.rounds
            );
        }
    }

    #[test]
    fn everyone_scores_something() {
        // No player should be hard-locked out of all glory across many games.
        let mut ever_scored = vec![false; 4];
        for seed in 0..100 {
            let r = simulate(Config::default(), seed, &roster4());
            for (i, p) in r.players.iter().enumerate() {
                if p.glory > 0.0 {
                    ever_scored[i] = true;
                }
            }
        }
        assert!(ever_scored.iter().all(|&b| b), "a seat never scored in 100 games");
    }

    #[test]
    fn fixed_combo_is_honored() {
        let roster = vec![
            (Strategy::Balanced, Some((Lineage::Dvergar, Calling::Reaver))),
            (Strategy::Balanced, Some((Lineage::Wisp, Calling::Lorekeeper))),
        ];
        let r = simulate(Config::default(), 7, &roster);
        assert_eq!(r.players[0].combo, Some((Lineage::Dvergar, Calling::Reaver)));
        assert_eq!(r.players[1].combo, Some((Lineage::Wisp, Calling::Lorekeeper)));
    }

    #[test]
    fn worthy_held_bonus_is_additive_and_off_by_default() {
        // Turning worthy_held_bonus on must only ever *raise* total glory, and the two
        // configs must be identical whenever no Worthy End occurs — proving the term is
        // a clean additive bonus (not a hidden double-count baked into the default).
        let base = Config::default();
        assert_eq!(base.worthy_held_bonus, 0.0, "default must not double-count held value");
        let mut hot = base.clone();
        hot.worthy_held_bonus = 1.0;
        for seed in 0..300 {
            let a = simulate(base.clone(), seed, &roster4());
            let b = simulate(hot.clone(), seed, &roster4());
            // Determinism: the only divergence allowed is extra glory for worthy-enders.
            for (x, y) in a.players.iter().zip(b.players.iter()) {
                assert!(y.glory >= x.glory - 1e-9, "held bonus must never reduce glory");
                if y.worthy_ends == 0 {
                    assert!((x.glory - y.glory).abs() < 1e-6, "no worthy end => identical glory");
                }
            }
        }
    }
}
