diff --git a/src/game.rs b/src/game.rs index 852d44f..76c9e61 100644 --- a/src/game.rs +++ b/src/game.rs @@ -102,24 +102,51 @@ impl Default for GameOptions { } } -/// Owned variant of `Game` returned by public constructors. +/// One match, fitted on its own. /// -/// Unlike `Game<'a, T, D>` (which borrows its result/weights slices from -/// History's internal state), `OwnedGame` owns the team ratings, so it -/// can be returned freely from public constructors. The inference inputs -/// themselves are not retained — nothing reads them back. +/// Rate a single match against ratings you already hold and read the updated +/// beliefs straight back. There is no history behind it: nothing is stored, +/// nothing propagates backward, and the priors you hand in are the only +/// evidence used. That makes it the wrong tool for the thing this crate exists +/// for — [`History`](crate::History) is what infers skill *through time*, +/// revising past estimates as later matches arrive, and a sequence of `Game`s +/// chained by hand is a forward-only filter, not the same answer. /// -/// A fitted single match, and nothing more: see [`Game`] for why that is not -/// the same as a step of a [`History`](crate::History). +/// Reach for it when a history would be overkill or unavailable: a one-off +/// matchup, replaying a rating step from stored numbers, checking the engine +/// against a reference, or a caller that keeps its own persistence and only +/// wants the update rule. +/// +/// ``` +/// use trueskill_tt::{ConstantDrift, Game, GameOptions, Gaussian, Outcome, Rating}; +/// +/// let strong: Rating = Rating::new(Gaussian::from_ms(30.0, 3.0), 1.0, ConstantDrift::new(0.0)); +/// let weak: Rating = Rating::new(Gaussian::from_ms(20.0, 3.0), 1.0, ConstantDrift::new(0.0)); +/// +/// // The underdog wins. +/// let game = Game::ranked( +/// &[&[weak], &[strong]], +/// Outcome::winner(0, 2), +/// &GameOptions::default(), +/// )?; +/// +/// let posteriors = game.posteriors(); +/// assert!(posteriors[0][0].mu() > weak.prior().mu(), "the winner gained"); +/// assert!(posteriors[1][0].mu() < strong.prior().mu(), "the loser lost"); +/// +/// // An upset is improbable, and `log_evidence` says so. +/// assert!(game.log_evidence() < 0.5_f64.ln()); +/// # Ok::<(), trueskill_tt::InferenceError>(()) +/// ``` #[derive(Debug)] #[must_use] -pub struct OwnedGame> { +pub struct Game> { teams: Vec>>, pub(crate) likelihoods: Vec>, pub(crate) log_evidence: f64, } -impl> OwnedGame { +impl> Game { pub(crate) fn new( teams: Vec>>, result: Vec, @@ -131,7 +158,8 @@ impl> OwnedGame { // `Game` takes the teams by value and is dropped here, so take the vec // back out of it rather than handing it a clone. - let g = Game::ranked_with_arena(teams, &result, &weights, p_draw, convergence, &mut arena); + let g = + GameRef::ranked_with_arena(teams, &result, &weights, p_draw, convergence, &mut arena); Self { teams: g.teams, @@ -149,7 +177,7 @@ impl> OwnedGame { ) -> Self { let mut arena = ScratchArena::new(); - let g = Game::scored_with_arena( + let g = GameRef::scored_with_arena( teams, &scores, &weights, @@ -204,27 +232,15 @@ impl> OwnedGame { } } -/// One match's factor graph, fitted on its own. +/// The borrowing form of [`Game`], used only inside the crate. /// -/// Rate a single match against ratings you already hold and get the updated -/// beliefs straight back. There is no history behind it: nothing is stored, -/// nothing propagates backward, and the priors you hand in are the only -/// evidence used. That makes it the wrong tool for the thing this crate exists -/// for — [`History`](crate::History) is what infers skill *through time*, -/// revising past estimates as later matches arrive, and a sequence of `Game`s -/// chained by hand is a forward-only filter, not the same answer. -/// -/// Reach for `Game` when a history would be overkill or unavailable: a -/// one-off matchup, replaying a rating step from stored numbers, checking the -/// engine against a reference, or a caller that keeps its own persistence and -/// only wants the update rule. -/// -/// The type is mostly a namespace. Its constructors — [`Game::ranked`], -/// [`Game::scored`], [`Game::one_v_one`], [`Game::free_for_all`] — return an -/// [`OwnedGame`], because `Game<'a, …>` borrows the result and weight slices -/// that `History` keeps internally and so cannot be handed out. +/// `History` keeps each event's result and weight slices in its own storage +/// and sweeps them thousands of times, so the inference core borrows them +/// rather than copying. That borrow is the whole difference between this and +/// [`Game`]; it is why this type cannot be handed to a caller, and why it is +/// not part of the public API. #[derive(Debug)] -pub struct Game<'a, T: Time = i64, D: Drift = crate::drift::ConstantDrift> { +pub(crate) struct GameRef<'a, T: Time = i64, D: Drift = crate::drift::ConstantDrift> { teams: Vec>>, result: &'a [f64], weights: &'a [Vec], @@ -234,7 +250,7 @@ pub struct Game<'a, T: Time = i64, D: Drift = crate::drift::ConstantDrift> { pub(crate) log_evidence: f64, } -impl<'a, T: Time, D: Drift> Game<'a, T, D> { +impl<'a, T: Time, D: Drift> GameRef<'a, T, D> { pub(crate) fn ranked_with_arena( teams: Vec>>, result: &'a [f64], @@ -476,11 +492,12 @@ impl<'a, T: Time, D: Drift> Game<'a, T, D> { self.likelihoods = likelihoods; } - /// Updated skill belief for every competitor, as `[team][member]` in the - /// order the teams and members were passed in — prior times this match's - /// likelihood, exactly as [`OwnedGame::posteriors`]. - #[must_use] - pub fn posteriors(&self) -> Vec> { + /// As [`Game::posteriors`]. + /// + /// Test-only: inference reads `likelihoods` directly, and `GameRef` is not + /// public, so the only callers are this module's own goldens. + #[cfg(test)] + pub(crate) fn posteriors(&self) -> Vec> { self.likelihoods .iter() .zip(self.teams.iter()) @@ -492,16 +509,9 @@ impl<'a, T: Time, D: Drift> Game<'a, T, D> { }) .collect::>() } - - /// Natural log of how probable this outcome was under the priors, summed - /// over the diff chain's links — as [`OwnedGame::log_evidence`]. - #[must_use] - pub fn log_evidence(&self) -> f64 { - self.log_evidence - } } -impl> Game<'_, T, D> { +impl> Game { /// Reject the team shapes inference cannot represent. /// /// `run_chain` builds one diff link per adjacent pair of teams, so fewer @@ -525,6 +535,12 @@ impl> Game<'_, T, D> { Ok(()) } + /// Fit one match from an ordinal result. + /// + /// `teams` is `[team][member]`, and `outcome` ranks those teams in the + /// same order. Read the result with [`posteriors`](Game::posteriors) and + /// [`log_evidence`](Game::log_evidence). + /// /// # Errors /// /// - `InvalidParameter` if `options.convergence` is out of range — an @@ -542,7 +558,7 @@ impl> Game<'_, T, D> { teams: &[&[Rating]], outcome: crate::Outcome, options: &GameOptions, - ) -> Result, crate::InferenceError> { + ) -> Result { options.convergence.validate()?; Self::validate_teams(teams)?; if !(0.0..1.0).contains(&options.p_draw) { @@ -581,7 +597,7 @@ impl> Game<'_, T, D> { let teams_owned: Vec>> = teams.iter().map(|t| t.to_vec()).collect(); let weights: Vec> = teams.iter().map(|t| vec![1.0; t.len()]).collect(); - Ok(OwnedGame::new( + Ok(Self::new( teams_owned, result, weights, @@ -590,6 +606,12 @@ impl> Game<'_, T, D> { )) } + /// Fit one match from continuous scores. + /// + /// Unlike [`ranked`](Game::ranked), the *size* of each adjacent gap is + /// evidence: beating a team by ten says more than beating them by one. + /// How much more is set by `options.score_sigma`. + /// /// # Errors /// /// - `InvalidParameter` if `options.score_sigma` is not strictly positive @@ -602,7 +624,7 @@ impl> Game<'_, T, D> { teams: &[&[Rating]], outcome: crate::Outcome, options: &GameOptions, - ) -> Result, crate::InferenceError> { + ) -> Result { options.convergence.validate()?; Self::validate_teams(teams)?; if options.score_sigma <= 0.0 || options.score_sigma.is_nan() { @@ -638,7 +660,7 @@ impl> Game<'_, T, D> { } let teams_owned: Vec>> = teams.iter().map(|t| t.to_vec()).collect(); let weights: Vec> = teams.iter().map(|t| vec![1.0; t.len()]).collect(); - Ok(OwnedGame::new_scored( + Ok(Self::new_scored( teams_owned, scores, weights, @@ -647,7 +669,24 @@ impl> Game<'_, T, D> { )) } - /// Convenience wrapper over [`Game::ranked`] for two single-competitor teams. + /// Two single-competitor teams: the common case, without the nesting. + /// + /// Returns a `Game` like every other constructor. It used to return + /// `(Gaussian, Gaussian)` — the posteriors alone — which made it the one + /// member of the family you could not ask for + /// [`log_evidence`](Game::log_evidence). Call `.posteriors()` for the old + /// shape: + /// + /// ``` + /// # use trueskill_tt::{ConstantDrift, Game, GameOptions, Gaussian, Outcome, Rating}; + /// # let a: Rating = Rating::new(Gaussian::from_ms(25.0, 8.0), 4.0, ConstantDrift::new(0.0)); + /// # let b = a; + /// let game = Game::one_v_one(&a, &b, Outcome::winner(0, 2), &GameOptions::default())?; + /// let post = game.posteriors(); + /// let (a_post, b_post) = (post[0][0], post[1][0]); + /// # let _ = (a_post, b_post); + /// # Ok::<(), trueskill_tt::InferenceError>(()) + /// ``` /// /// # Errors /// @@ -659,12 +698,12 @@ impl> Game<'_, T, D> { b: &Rating, outcome: crate::Outcome, options: &GameOptions, - ) -> Result<(Gaussian, Gaussian), crate::InferenceError> { - let game = Self::ranked(&[&[*a], &[*b]], outcome, options)?; - let post = game.posteriors(); - Ok((post[0][0], post[1][0])) + ) -> Result { + Self::ranked(&[&[*a], &[*b]], outcome, options) } + /// A free-for-all: every competitor is their own one-member team. + /// /// # Errors /// /// Wraps each competitor in a one-member team and delegates to @@ -673,7 +712,7 @@ impl> Game<'_, T, D> { competitors: &[&Rating], outcome: crate::Outcome, options: &GameOptions, - ) -> Result, crate::InferenceError> { + ) -> Result { let teams: Vec>> = competitors.iter().map(|p| vec![**p]).collect(); let team_refs: Vec<&[Rating]> = teams.iter().map(|t| t.as_slice()).collect(); Self::ranked(&team_refs, outcome, options) @@ -703,7 +742,7 @@ mod tests { ); let w = [vec![1.0], vec![1.0]]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![vec![t_a], vec![t_b]], &[0.0, 1.0], &w, @@ -731,7 +770,7 @@ mod tests { ); let w = [vec![1.0], vec![1.0]]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![vec![t_a], vec![t_b]], &[0.0, 1.0], &w, @@ -759,7 +798,7 @@ mod tests { ); let w = [vec![1.0], vec![1.0]]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![vec![t_a], vec![t_b]], &[0.0, 1.0], &w, @@ -793,7 +832,7 @@ mod tests { ]; let w = [vec![1.0], vec![1.0], vec![1.0]]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( teams.clone(), &[1.0, 2.0, 0.0], &w, @@ -810,7 +849,7 @@ mod tests { assert_ulps_eq!(b, Gaussian::from_ms(31.311358, 6.698818), epsilon = 1e-6); let w = [vec![1.0], vec![1.0], vec![1.0]]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( teams.clone(), &[2.0, 1.0, 0.0], &w, @@ -827,7 +866,7 @@ mod tests { assert_ulps_eq!(b, Gaussian::from_ms(25.000000, 6.238469), epsilon = 1e-6); let w = [vec![1.0], vec![1.0], vec![1.0]]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( teams, &[1.0, 2.0, 0.0], &w, @@ -867,7 +906,7 @@ mod tests { ); let w = [vec![1.0], vec![1.0]]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![vec![t_a], vec![t_b]], &[0.0, 0.0], &w, @@ -899,7 +938,7 @@ mod tests { ); let w = [vec![1.0], vec![1.0]]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![vec![t_a], vec![t_b]], &[0.0, 0.0], &w, @@ -935,7 +974,7 @@ mod tests { ); let w = [vec![1.0], vec![1.0], vec![1.0]]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![vec![t_a], vec![t_b], vec![t_c]], &[0.0, 0.0, 0.0], &w, @@ -972,7 +1011,7 @@ mod tests { ); let w = [vec![1.0], vec![1.0], vec![1.0]]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![vec![t_a], vec![t_b], vec![t_c]], &[0.0, 0.0, 0.0], &w, @@ -1024,7 +1063,7 @@ mod tests { ]; let w = [vec![1.0, 1.0], vec![1.0], vec![1.0, 1.0]]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![t_a, t_b, t_c], &[1.0, 0.0, 0.0], &w, @@ -1058,7 +1097,7 @@ mod tests { )]; let w = [w_a, w_b]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, @@ -1083,7 +1122,7 @@ mod tests { let w_b = vec![0.7]; let w = [w_a, w_b]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, @@ -1108,7 +1147,7 @@ mod tests { let w_b = vec![0.7]; let w = [w_a, w_b]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![t_a, t_b], &[1.0, 0.0], &w, @@ -1144,7 +1183,7 @@ mod tests { )]; let w = [w_a, w_b]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![t_a, t_b], &[1.0, 0.0], &w, @@ -1180,7 +1219,7 @@ mod tests { )]; let w = [w_a, w_b]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![t_a, t_b], &[1.0, 0.0], &w, @@ -1226,7 +1265,7 @@ mod tests { let result = vec![10.0, 0.0]; // a beat b by 10 let weights = [vec![1.0], vec![1.0]]; let mut arena = ScratchArena::new(); - let g = Game::scored_with_arena( + let g = GameRef::scored_with_arena( teams, &result, &weights, @@ -1246,7 +1285,7 @@ mod tests { // Tighter score_sigma should produce a stronger update. let mut arena2 = ScratchArena::new(); - let g_tight = Game::scored_with_arena( + let g_tight = GameRef::scored_with_arena( vec![vec![prior], vec![prior]], &result, &weights, @@ -1357,7 +1396,7 @@ mod tests { let w_b = vec![0.9, 0.6]; let w = [w_a, w_b]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, @@ -1392,7 +1431,7 @@ mod tests { let w_b = vec![0.7, 0.4]; let w = [w_a, w_b]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, @@ -1427,7 +1466,7 @@ mod tests { let w_b = vec![0.7, 2.4]; let w = [w_a, w_b]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, @@ -1459,7 +1498,7 @@ mod tests { ); let w = [vec![1.0, 1.0], vec![1.0]]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![ t_a.clone(), vec![R::new( @@ -1480,7 +1519,7 @@ mod tests { let w_b = vec![1.0, 0.0]; let w = [w_a, w_b]; - let g = Game::ranked_with_arena( + let g = GameRef::ranked_with_arena( vec![t_a, t_b.clone()], &[1.0, 0.0], &w, @@ -1505,7 +1544,7 @@ mod tests { // Capped at 1 iteration: cannot fully propagate down a 4-team chain. let mut arena = ScratchArena::new(); - let g_capped = Game::ranked_with_arena( + let g_capped = GameRef::ranked_with_arena( teams.clone(), &result, &weights, @@ -1520,7 +1559,7 @@ mod tests { // Same inputs, plenty of iterations: fully converged. let mut arena = ScratchArena::new(); - let g_full = Game::ranked_with_arena( + let g_full = GameRef::ranked_with_arena( teams, &result, &weights, @@ -1552,7 +1591,7 @@ mod tests { let weights = vec![vec![1.0]; 4]; let mut arena = ScratchArena::new(); - let g_undamped = Game::ranked_with_arena( + let g_undamped = GameRef::ranked_with_arena( teams.clone(), &result, &weights, @@ -1564,7 +1603,7 @@ mod tests { // alpha=0.5 with extra iterations: should reach the same fixed point. let mut arena = ScratchArena::new(); - let g_damped = Game::ranked_with_arena( + let g_damped = GameRef::ranked_with_arena( teams, &result, &weights, diff --git a/src/history.rs b/src/history.rs index b361780..e21a896 100644 --- a/src/history.rs +++ b/src/history.rs @@ -3053,8 +3053,8 @@ mod tests { use super::*; use crate::{ - ConstantDrift, EPSILON, Event, Game, Gaussian, Member, Outcome, P_DRAW, Team, - arena::ScratchArena, + ConstantDrift, EPSILON, Event, Gaussian, Member, Outcome, P_DRAW, Team, + arena::ScratchArena, game::GameRef, }; /// #17: a slice's footprint must be O(competitors in the slice), not @@ -3170,7 +3170,7 @@ mod tests { let observed = h.time_slices[1].skills.get(a).unwrap().posterior(); let w = [vec![1.0], vec![1.0]]; - let p = Game::ranked_with_arena( + let p = GameRef::ranked_with_arena( h.time_slices[1].events[0].within_priors( false, &h.time_slices[1].skills, diff --git a/src/lib.rs b/src/lib.rs index 98eb14c..d727c0d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -154,7 +154,7 @@ pub use drift::{ConstantDrift, Drift}; pub use error::{InferenceError, UnknownKeys}; pub use event::{Event, Member, Team}; pub use event_builder::EventBuilder; -pub use game::{Game, GameOptions, OwnedGame}; +pub use game::{Game, GameOptions}; pub use gaussian::Gaussian; pub use history::{History, HistoryBuilder, Joint}; use matrix::Matrix; diff --git a/src/time_slice.rs b/src/time_slice.rs index 5a5e1bd..1f81ab1 100644 --- a/src/time_slice.rs +++ b/src/time_slice.rs @@ -9,7 +9,7 @@ use crate::{ arena::ScratchArena, color_group::ColorGroups, drift::Drift, - game::Game, + game::GameRef, gaussian::Gaussian, rating::Rating, storage::{CompetitorStore, SkillStore}, @@ -141,10 +141,15 @@ impl Event { let teams = self.within_priors(false, skills, competitors); let result = self.outputs(); let g = match self.kind { - EventKind::Ranked => { - Game::ranked_with_arena(teams, &result, &self.weights, p_draw, convergence, arena) - } - EventKind::Scored { score_sigma } => Game::scored_with_arena( + EventKind::Ranked => GameRef::ranked_with_arena( + teams, + &result, + &self.weights, + p_draw, + convergence, + arena, + ), + EventKind::Scored { score_sigma } => GameRef::scored_with_arena( teams, &result, &self.weights, @@ -409,7 +414,7 @@ impl TimeSlice { let result = event.outputs(); let g = match event.kind { - EventKind::Ranked => Game::ranked_with_arena( + EventKind::Ranked => GameRef::ranked_with_arena( teams, &result, &event.weights, @@ -417,7 +422,7 @@ impl TimeSlice { self.convergence, &mut self.arena, ), - EventKind::Scored { score_sigma } => Game::scored_with_arena( + EventKind::Scored { score_sigma } => GameRef::scored_with_arena( teams, &result, &event.weights, @@ -724,7 +729,7 @@ impl TimeSlice { let result = event.outputs(); match event.kind { EventKind::Ranked => { - Game::ranked_with_arena( + GameRef::ranked_with_arena( teams, &result, &event.weights, @@ -735,7 +740,7 @@ impl TimeSlice { .log_evidence } EventKind::Scored { score_sigma } => { - Game::scored_with_arena( + GameRef::scored_with_arena( teams, &result, &event.weights, diff --git a/tests/equivalence.rs b/tests/equivalence.rs index 46db26b..592ea49 100644 --- a/tests/equivalence.rs +++ b/tests/equivalence.rs @@ -23,8 +23,10 @@ fn ts_rating(mu: f64, sigma: f64, beta: f64, gamma: f64) -> R { fn game_1v1_golden_matches_historical() { let a = ts_rating(25.0, 25.0 / 3.0, 25.0 / 6.0, 25.0 / 300.0); let b = ts_rating(25.0, 25.0 / 3.0, 25.0 / 6.0, 25.0 / 300.0); - let (a_post, b_post) = - Game::::one_v_one(&a, &b, Outcome::winner(0, 2), &GameOptions::default()).unwrap(); + let post = Game::::one_v_one(&a, &b, Outcome::winner(0, 2), &GameOptions::default()) + .unwrap() + .posteriors(); + let (a_post, b_post) = (post[0][0], post[1][0]); // Historical golden from pre-T2 test_1vs1 (team 0 wins): assert_ulps_eq!( a_post, diff --git a/tests/game.rs b/tests/game.rs index 6681990..35905b5 100644 --- a/tests/game.rs +++ b/tests/game.rs @@ -32,10 +32,16 @@ fn game_ranked_1v1_golden() { fn game_one_v_one_shortcut() { let a = default_rating(); let b = default_rating(); - let (a_post, b_post) = + let game = Game::::one_v_one(&a, &b, Outcome::winner(0, 2), &GameOptions::default()).unwrap(); + let post = game.posteriors(); + let (a_post, b_post) = (post[0][0], post[1][0]); assert!(a_post.mu() > 25.0); assert!(b_post.mu() < 25.0); + + // It returns a game like every other constructor, so evidence is askable. + // Two identical ratings make either result equally likely. + assert!((game.log_evidence() - 0.5_f64.ln()).abs() < 1e-12); } #[test] @@ -118,8 +124,10 @@ fn one_v_one_honours_the_draw_probability_it_is_given() { p_draw: 0.25, ..GameOptions::default() }; - let (a_post, b_post) = Game::::one_v_one(&a, &b, Outcome::draw(2), &options) - .expect("a draw is representable once p_draw is positive"); + let post = Game::::one_v_one(&a, &b, Outcome::draw(2), &options) + .expect("a draw is representable once p_draw is positive") + .posteriors(); + let (a_post, b_post) = (post[0][0], post[1][0]); // A symmetric draw leaves the means alone and sharpens both sides. assert!((a_post.mu() - b_post.mu()).abs() < 1e-9); @@ -135,8 +143,10 @@ fn one_v_one_honours_convergence_options() { convergence: ConvergenceOptions::default(), ..GameOptions::default() }; - let (a_post, _) = Game::::one_v_one(&a, &b, Outcome::winner(0, 2), &options).unwrap(); - assert!(a_post.mu() > 25.0); + let post = Game::::one_v_one(&a, &b, Outcome::winner(0, 2), &options) + .unwrap() + .posteriors(); + assert!(post[0][0].mu() > 25.0); } /// `Game` is a public entry point that does not pass through `History`'s