Merge api/game-rename (#69)

This commit is contained in:
2026-09-09 22:55:05 +02:00
6 changed files with 157 additions and 101 deletions
+120 -81
View File
@@ -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<T, D>` 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<T: Time, D: Drift<T>> {
pub struct Game<T: Time, D: Drift<T>> {
teams: Vec<Vec<Rating<T, D>>>,
pub(crate) likelihoods: Vec<Vec<Gaussian>>,
pub(crate) log_evidence: f64,
}
impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
impl<T: Time, D: Drift<T>> Game<T, D> {
pub(crate) fn new(
teams: Vec<Vec<Rating<T, D>>>,
result: Vec<f64>,
@@ -131,7 +158,8 @@ impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
// `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<T: Time, D: Drift<T>> OwnedGame<T, D> {
) -> 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<T: Time, D: Drift<T>> OwnedGame<T, D> {
}
}
/// 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<T> = crate::drift::ConstantDrift> {
pub(crate) struct GameRef<'a, T: Time = i64, D: Drift<T> = crate::drift::ConstantDrift> {
teams: Vec<Vec<Rating<T, D>>>,
result: &'a [f64],
weights: &'a [Vec<f64>],
@@ -234,7 +250,7 @@ pub struct Game<'a, T: Time = i64, D: Drift<T> = crate::drift::ConstantDrift> {
pub(crate) log_evidence: f64,
}
impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
impl<'a, T: Time, D: Drift<T>> GameRef<'a, T, D> {
pub(crate) fn ranked_with_arena(
teams: Vec<Vec<Rating<T, D>>>,
result: &'a [f64],
@@ -476,11 +492,12 @@ impl<'a, T: Time, D: Drift<T>> 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<Vec<Gaussian>> {
/// 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<Vec<Gaussian>> {
self.likelihoods
.iter()
.zip(self.teams.iter())
@@ -492,16 +509,9 @@ impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
})
.collect::<Vec<_>>()
}
/// 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<T: Time, D: Drift<T>> Game<'_, T, D> {
impl<T: Time, D: Drift<T>> Game<T, D> {
/// 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<T: Time, D: Drift<T>> 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<T: Time, D: Drift<T>> Game<'_, T, D> {
teams: &[&[Rating<T, D>]],
outcome: crate::Outcome,
options: &GameOptions,
) -> Result<OwnedGame<T, D>, crate::InferenceError> {
) -> Result<Self, crate::InferenceError> {
options.convergence.validate()?;
Self::validate_teams(teams)?;
if !(0.0..1.0).contains(&options.p_draw) {
@@ -581,7 +597,7 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
let teams_owned: Vec<Vec<Rating<T, D>>> = teams.iter().map(|t| t.to_vec()).collect();
let weights: Vec<Vec<f64>> = 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<T: Time, D: Drift<T>> 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<T: Time, D: Drift<T>> Game<'_, T, D> {
teams: &[&[Rating<T, D>]],
outcome: crate::Outcome,
options: &GameOptions,
) -> Result<OwnedGame<T, D>, crate::InferenceError> {
) -> Result<Self, crate::InferenceError> {
options.convergence.validate()?;
Self::validate_teams(teams)?;
if options.score_sigma <= 0.0 || options.score_sigma.is_nan() {
@@ -638,7 +660,7 @@ impl<T: Time, D: Drift<T>> Game<'_, T, D> {
}
let teams_owned: Vec<Vec<Rating<T, D>>> = teams.iter().map(|t| t.to_vec()).collect();
let weights: Vec<Vec<f64>> = 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<T: Time, D: Drift<T>> 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<T: Time, D: Drift<T>> Game<'_, T, D> {
b: &Rating<T, D>,
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, crate::InferenceError> {
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<T: Time, D: Drift<T>> Game<'_, T, D> {
competitors: &[&Rating<T, D>],
outcome: crate::Outcome,
options: &GameOptions,
) -> Result<OwnedGame<T, D>, crate::InferenceError> {
) -> Result<Self, crate::InferenceError> {
let teams: Vec<Vec<Rating<T, D>>> = competitors.iter().map(|p| vec![**p]).collect();
let team_refs: Vec<&[Rating<T, D>]> = 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,
+3 -3
View File
@@ -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,
+1 -1
View File
@@ -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;
+14 -9
View File
@@ -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<T: Time> TimeSlice<T> {
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<T: Time> TimeSlice<T> {
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<T: Time> TimeSlice<T> {
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<T: Time> TimeSlice<T> {
.log_evidence
}
EventKind::Scored { score_sigma } => {
Game::scored_with_arena(
GameRef::scored_with_arena(
teams,
&result,
&event.weights,
+4 -2
View File
@@ -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::<i64, _>::one_v_one(&a, &b, Outcome::winner(0, 2), &GameOptions::default()).unwrap();
let post = Game::<i64, _>::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,
+15 -5
View File
@@ -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::<i64, _>::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::<i64, _>::one_v_one(&a, &b, Outcome::draw(2), &options)
.expect("a draw is representable once p_draw is positive");
let post = Game::<i64, _>::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::<i64, _>::one_v_one(&a, &b, Outcome::winner(0, 2), &options).unwrap();
assert!(a_post.mu() > 25.0);
let post = Game::<i64, _>::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