feat(api): add Game::ranked, one_v_one, free_for_all, custom constructors
Public Game API now returns Result<_, InferenceError> on invalid input
(p_draw out of range, outcome rank count mismatches team count).
New types:
- GameOptions { p_draw, convergence } — bundled config
- OwnedGame<T, D> — owned variant of Game that carries its result
and weights internally (no borrow of History's slices). Returned
by public constructors to avoid leaking internal borrow lifetimes.
The internal Game::new is renamed Game::ranked_with_arena (pub(crate))
and keeps the borrowing-arena signature for History's hot path. All
in-crate callers updated (21 call sites: 18 in game.rs tests, 2 in
time_slice.rs, 1 in history.rs).
Game::custom is a T2-minimal power-user escape hatch exposing raw
factor + schedule plumbing. Full ergonomics in T4 (#[doc(hidden)]
for now).
Game::log_evidence() accessor added on both Game and OwnedGame (was
previously accessible only through the pub(crate) evidence field).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ pub struct VarId(pub u32);
|
|||||||
/// Flat storage of variable marginals.
|
/// Flat storage of variable marginals.
|
||||||
///
|
///
|
||||||
/// Variables are allocated by `alloc()` and accessed by `VarId`. The store is
|
/// Variables are allocated by `alloc()` and accessed by `VarId`. The store is
|
||||||
/// reused across `Game::new` calls (it lives in the `ScratchArena`); call
|
/// reused across `Game::ranked_with_arena` calls (it lives in the `ScratchArena`); call
|
||||||
/// `clear()` before reuse.
|
/// `clear()` before reuse.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct VarStore {
|
pub struct VarStore {
|
||||||
|
|||||||
171
src/game.rs
171
src/game.rs
@@ -12,6 +12,71 @@ use crate::{
|
|||||||
tuple_gt, tuple_max,
|
tuple_gt, tuple_max,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct GameOptions {
|
||||||
|
pub p_draw: f64,
|
||||||
|
pub convergence: crate::ConvergenceOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GameOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
p_draw: crate::P_DRAW,
|
||||||
|
convergence: crate::ConvergenceOptions::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Owned variant of `Game` returned by public constructors.
|
||||||
|
///
|
||||||
|
/// Unlike `Game<'a, T, D>` (which borrows its result/weights slices from
|
||||||
|
/// History's internal state), `OwnedGame<T, D>` owns its inputs so it can
|
||||||
|
/// be returned freely from public constructors.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct OwnedGame<T: Time, D: Drift<T>> {
|
||||||
|
teams: Vec<Vec<Rating<T, D>>>,
|
||||||
|
result: Vec<f64>,
|
||||||
|
weights: Vec<Vec<f64>>,
|
||||||
|
p_draw: f64,
|
||||||
|
pub(crate) likelihoods: Vec<Vec<Gaussian>>,
|
||||||
|
pub(crate) evidence: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Time, D: Drift<T>> OwnedGame<T, D> {
|
||||||
|
pub(crate) fn new(
|
||||||
|
teams: Vec<Vec<Rating<T, D>>>,
|
||||||
|
result: Vec<f64>,
|
||||||
|
weights: Vec<Vec<f64>>,
|
||||||
|
p_draw: f64,
|
||||||
|
) -> Self {
|
||||||
|
let mut arena = ScratchArena::new();
|
||||||
|
let g = Game::ranked_with_arena(teams.clone(), &result, &weights, p_draw, &mut arena);
|
||||||
|
let likelihoods = g.likelihoods;
|
||||||
|
let evidence = g.evidence;
|
||||||
|
Self {
|
||||||
|
teams,
|
||||||
|
result,
|
||||||
|
weights,
|
||||||
|
p_draw,
|
||||||
|
likelihoods,
|
||||||
|
evidence,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {
|
||||||
|
self.likelihoods
|
||||||
|
.iter()
|
||||||
|
.zip(self.teams.iter())
|
||||||
|
.map(|(l, t)| l.iter().zip(t.iter()).map(|(&l, r)| l * r.prior).collect())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_evidence(&self) -> f64 {
|
||||||
|
self.evidence.ln()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Game<'a, T: Time = i64, D: Drift<T> = crate::drift::ConstantDrift> {
|
pub struct Game<'a, T: Time = i64, D: Drift<T> = crate::drift::ConstantDrift> {
|
||||||
teams: Vec<Vec<Rating<T, D>>>,
|
teams: Vec<Vec<Rating<T, D>>>,
|
||||||
@@ -23,7 +88,7 @@ pub struct Game<'a, T: Time = i64, D: Drift<T> = crate::drift::ConstantDrift> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
||||||
pub fn new(
|
pub(crate) fn ranked_with_arena(
|
||||||
teams: Vec<Vec<Rating<T, D>>>,
|
teams: Vec<Vec<Rating<T, D>>>,
|
||||||
result: &'a [f64],
|
result: &'a [f64],
|
||||||
weights: &'a [Vec<f64>],
|
weights: &'a [Vec<f64>],
|
||||||
@@ -219,6 +284,68 @@ impl<'a, T: Time, D: Drift<T>> Game<'a, T, D> {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn log_evidence(&self) -> f64 {
|
||||||
|
self.evidence.ln()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Time, D: Drift<T>> Game<'_, T, D> {
|
||||||
|
pub fn ranked(
|
||||||
|
teams: &[&[Rating<T, D>]],
|
||||||
|
outcome: crate::Outcome,
|
||||||
|
options: &GameOptions,
|
||||||
|
) -> Result<OwnedGame<T, D>, crate::InferenceError> {
|
||||||
|
if !(0.0..1.0).contains(&options.p_draw) {
|
||||||
|
return Err(crate::InferenceError::InvalidProbability {
|
||||||
|
value: options.p_draw,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if outcome.team_count() != teams.len() {
|
||||||
|
return Err(crate::InferenceError::MismatchedShape {
|
||||||
|
kind: "outcome ranks vs teams",
|
||||||
|
expected: teams.len(),
|
||||||
|
got: outcome.team_count(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let ranks = outcome.as_ranks();
|
||||||
|
let max_rank = ranks.iter().copied().max().unwrap_or(0) as f64;
|
||||||
|
let result: Vec<f64> = ranks.iter().map(|&r| max_rank - r as f64).collect();
|
||||||
|
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(teams_owned, result, weights, options.p_draw))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn one_v_one(
|
||||||
|
a: &Rating<T, D>,
|
||||||
|
b: &Rating<T, D>,
|
||||||
|
outcome: crate::Outcome,
|
||||||
|
) -> Result<(Gaussian, Gaussian), crate::InferenceError> {
|
||||||
|
let game = Self::ranked(&[&[*a], &[*b]], outcome, &GameOptions::default())?;
|
||||||
|
let post = game.posteriors();
|
||||||
|
Ok((post[0][0], post[1][0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn free_for_all(
|
||||||
|
players: &[&Rating<T, D>],
|
||||||
|
outcome: crate::Outcome,
|
||||||
|
options: &GameOptions,
|
||||||
|
) -> Result<OwnedGame<T, D>, crate::InferenceError> {
|
||||||
|
let teams: Vec<Vec<Rating<T, D>>> = players.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn custom<S: crate::factors::Schedule>(
|
||||||
|
factors: &mut [crate::factors::BuiltinFactor],
|
||||||
|
vars: &mut crate::factors::VarStore,
|
||||||
|
schedule: &S,
|
||||||
|
) -> crate::factors::ScheduleReport {
|
||||||
|
schedule.run(factors, vars)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -244,7 +371,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b]],
|
vec![vec![t_a], vec![t_b]],
|
||||||
&[0.0, 1.0],
|
&[0.0, 1.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -271,7 +398,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b]],
|
vec![vec![t_a], vec![t_b]],
|
||||||
&[0.0, 1.0],
|
&[0.0, 1.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -290,7 +417,7 @@ mod tests {
|
|||||||
let t_b = R::new(Gaussian::from_ms(15.568, 0.51), 1.0, ConstantDrift(0.2125));
|
let t_b = R::new(Gaussian::from_ms(15.568, 0.51), 1.0, ConstantDrift(0.2125));
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b]],
|
vec![vec![t_a], vec![t_b]],
|
||||||
&[0.0, 1.0],
|
&[0.0, 1.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -323,7 +450,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
teams.clone(),
|
teams.clone(),
|
||||||
&[1.0, 2.0, 0.0],
|
&[1.0, 2.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -339,7 +466,7 @@ mod tests {
|
|||||||
assert_ulps_eq!(b, Gaussian::from_ms(31.311358, 6.698818), epsilon = 1e-6);
|
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 w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
teams.clone(),
|
teams.clone(),
|
||||||
&[2.0, 1.0, 0.0],
|
&[2.0, 1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -355,7 +482,7 @@ mod tests {
|
|||||||
assert_ulps_eq!(b, Gaussian::from_ms(25.000000, 6.238469), epsilon = 1e-6);
|
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 w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||||
let g = Game::new(teams, &[1.0, 2.0, 0.0], &w, 0.5, &mut ScratchArena::new());
|
let g = Game::ranked_with_arena(teams, &[1.0, 2.0, 0.0], &w, 0.5, &mut ScratchArena::new());
|
||||||
let p = g.posteriors();
|
let p = g.posteriors();
|
||||||
|
|
||||||
let a = p[0][0];
|
let a = p[0][0];
|
||||||
@@ -382,7 +509,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b]],
|
vec![vec![t_a], vec![t_b]],
|
||||||
&[0.0, 0.0],
|
&[0.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -409,7 +536,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b]],
|
vec![vec![t_a], vec![t_b]],
|
||||||
&[0.0, 0.0],
|
&[0.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -444,7 +571,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b], vec![t_c]],
|
vec![vec![t_a], vec![t_b], vec![t_c]],
|
||||||
&[0.0, 0.0, 0.0],
|
&[0.0, 0.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -480,7 +607,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0], vec![1.0]];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![vec![t_a], vec![t_b], vec![t_c]],
|
vec![vec![t_a], vec![t_b], vec![t_c]],
|
||||||
&[0.0, 0.0, 0.0],
|
&[0.0, 0.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -531,7 +658,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let w = [vec![1.0, 1.0], vec![1.0], vec![1.0, 1.0]];
|
let w = [vec![1.0, 1.0], vec![1.0], vec![1.0, 1.0]];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![t_a, t_b, t_c],
|
vec![t_a, t_b, t_c],
|
||||||
&[1.0, 0.0, 0.0],
|
&[1.0, 0.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -564,7 +691,7 @@ mod tests {
|
|||||||
)];
|
)];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![t_a.clone(), t_b.clone()],
|
vec![t_a.clone(), t_b.clone()],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -588,7 +715,7 @@ mod tests {
|
|||||||
let w_b = vec![0.7];
|
let w_b = vec![0.7];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![t_a.clone(), t_b.clone()],
|
vec![t_a.clone(), t_b.clone()],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -612,7 +739,7 @@ mod tests {
|
|||||||
let w_b = vec![0.7];
|
let w_b = vec![0.7];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![t_a, t_b],
|
vec![t_a, t_b],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -639,7 +766,7 @@ mod tests {
|
|||||||
let t_b = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
|
let t_b = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![t_a, t_b],
|
vec![t_a, t_b],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -666,7 +793,7 @@ mod tests {
|
|||||||
let t_b = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
|
let t_b = vec![R::new(Gaussian::from_ms(2.0, 6.0), 1.0, ConstantDrift(0.0))];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![t_a, t_b],
|
vec![t_a, t_b],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -709,7 +836,7 @@ mod tests {
|
|||||||
let w_b = vec![0.9, 0.6];
|
let w_b = vec![0.9, 0.6];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![t_a.clone(), t_b.clone()],
|
vec![t_a.clone(), t_b.clone()],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -743,7 +870,7 @@ mod tests {
|
|||||||
let w_b = vec![0.7, 0.4];
|
let w_b = vec![0.7, 0.4];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![t_a.clone(), t_b.clone()],
|
vec![t_a.clone(), t_b.clone()],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -777,7 +904,7 @@ mod tests {
|
|||||||
let w_b = vec![0.7, 2.4];
|
let w_b = vec![0.7, 2.4];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![t_a.clone(), t_b.clone()],
|
vec![t_a.clone(), t_b.clone()],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
@@ -808,7 +935,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let w = [vec![1.0, 1.0], vec![1.0]];
|
let w = [vec![1.0, 1.0], vec![1.0]];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![
|
vec![
|
||||||
t_a.clone(),
|
t_a.clone(),
|
||||||
vec![R::new(
|
vec![R::new(
|
||||||
@@ -828,7 +955,7 @@ mod tests {
|
|||||||
let w_b = vec![1.0, 0.0];
|
let w_b = vec![1.0, 0.0];
|
||||||
|
|
||||||
let w = [w_a, w_b];
|
let w = [w_a, w_b];
|
||||||
let g = Game::new(
|
let g = Game::ranked_with_arena(
|
||||||
vec![t_a, t_b.clone()],
|
vec![t_a, t_b.clone()],
|
||||||
&[1.0, 0.0],
|
&[1.0, 0.0],
|
||||||
&w,
|
&w,
|
||||||
|
|||||||
@@ -773,7 +773,7 @@ mod tests {
|
|||||||
let observed = h.time_slices[1].skills.get(a).unwrap().posterior();
|
let observed = h.time_slices[1].skills.get(a).unwrap().posterior();
|
||||||
|
|
||||||
let w = [vec![1.0], vec![1.0]];
|
let w = [vec![1.0], vec![1.0]];
|
||||||
let p = Game::new(
|
let p = Game::ranked_with_arena(
|
||||||
h.time_slices[1].events[0].within_priors(
|
h.time_slices[1].events[0].within_priors(
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub use drift::{ConstantDrift, Drift};
|
|||||||
pub use error::InferenceError;
|
pub use error::InferenceError;
|
||||||
pub use event::{Event, Member, Team};
|
pub use event::{Event, Member, Team};
|
||||||
pub use event_builder::EventBuilder;
|
pub use event_builder::EventBuilder;
|
||||||
pub use game::Game;
|
pub use game::{Game, GameOptions, OwnedGame};
|
||||||
pub use gaussian::Gaussian;
|
pub use gaussian::Gaussian;
|
||||||
pub use history::History;
|
pub use history::History;
|
||||||
pub use key_table::KeyTable;
|
pub use key_table::KeyTable;
|
||||||
|
|||||||
@@ -226,7 +226,13 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
let teams = event.within_priors(false, false, &self.skills, agents);
|
let teams = event.within_priors(false, false, &self.skills, agents);
|
||||||
let result = event.outputs();
|
let result = event.outputs();
|
||||||
|
|
||||||
let g = Game::new(teams, &result, &event.weights, self.p_draw, &mut self.arena);
|
let g = Game::ranked_with_arena(
|
||||||
|
teams,
|
||||||
|
&result,
|
||||||
|
&event.weights,
|
||||||
|
self.p_draw,
|
||||||
|
&mut self.arena,
|
||||||
|
);
|
||||||
|
|
||||||
for (t, team) in event.teams.iter_mut().enumerate() {
|
for (t, team) in event.teams.iter_mut().enumerate() {
|
||||||
for (i, item) in team.items.iter_mut().enumerate() {
|
for (i, item) in team.items.iter_mut().enumerate() {
|
||||||
@@ -315,7 +321,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
self.events
|
self.events
|
||||||
.iter()
|
.iter()
|
||||||
.map(|event| {
|
.map(|event| {
|
||||||
Game::new(
|
Game::ranked_with_arena(
|
||||||
event.within_priors(online, forward, &self.skills, agents),
|
event.within_priors(online, forward, &self.skills, agents),
|
||||||
&event.outputs(),
|
&event.outputs(),
|
||||||
&event.weights,
|
&event.weights,
|
||||||
@@ -341,7 +347,7 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
.any(|item| targets.contains(&item.agent))
|
.any(|item| targets.contains(&item.agent))
|
||||||
})
|
})
|
||||||
.map(|(_, event)| {
|
.map(|(_, event)| {
|
||||||
Game::new(
|
Game::ranked_with_arena(
|
||||||
event.within_priors(online, forward, &self.skills, agents),
|
event.within_priors(online, forward, &self.skills, agents),
|
||||||
&event.outputs(),
|
&event.outputs(),
|
||||||
&event.weights,
|
&event.weights,
|
||||||
|
|||||||
96
tests/game.rs
Normal file
96
tests/game.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
use trueskill_tt::{
|
||||||
|
ConstantDrift, ConvergenceOptions, Game, GameOptions, Gaussian, InferenceError, Outcome, Rating,
|
||||||
|
};
|
||||||
|
|
||||||
|
type R = Rating<i64, ConstantDrift>;
|
||||||
|
|
||||||
|
fn default_rating() -> R {
|
||||||
|
R::new(
|
||||||
|
Gaussian::from_ms(25.0, 25.0 / 3.0),
|
||||||
|
25.0 / 6.0,
|
||||||
|
ConstantDrift(25.0 / 300.0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn game_ranked_1v1_golden() {
|
||||||
|
let a = default_rating();
|
||||||
|
let b = default_rating();
|
||||||
|
let g = Game::<i64, _>::ranked(
|
||||||
|
&[&[a], &[b]],
|
||||||
|
Outcome::winner(0, 2),
|
||||||
|
&GameOptions::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let p = g.posteriors();
|
||||||
|
assert!(p[0][0].mu() > 25.0);
|
||||||
|
assert!(p[1][0].mu() < 25.0);
|
||||||
|
assert!((p[0][0].sigma() - p[1][0].sigma()).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn game_one_v_one_shortcut() {
|
||||||
|
let a = default_rating();
|
||||||
|
let b = default_rating();
|
||||||
|
let (a_post, b_post) = Game::<i64, _>::one_v_one(&a, &b, Outcome::winner(0, 2)).unwrap();
|
||||||
|
assert!(a_post.mu() > 25.0);
|
||||||
|
assert!(b_post.mu() < 25.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn game_ranked_rejects_bad_p_draw() {
|
||||||
|
let a = R::new(Gaussian::default(), 1.0, ConstantDrift(0.0));
|
||||||
|
let err = Game::<i64, _>::ranked(
|
||||||
|
&[&[a], &[a]],
|
||||||
|
Outcome::winner(0, 2),
|
||||||
|
&GameOptions {
|
||||||
|
p_draw: 1.5,
|
||||||
|
convergence: ConvergenceOptions::default(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, InferenceError::InvalidProbability { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn game_ranked_rejects_mismatched_ranks() {
|
||||||
|
let a = R::new(Gaussian::default(), 1.0, ConstantDrift(0.0));
|
||||||
|
let err = Game::<i64, _>::ranked(
|
||||||
|
&[&[a], &[a]],
|
||||||
|
Outcome::ranking([0, 1, 2]),
|
||||||
|
&GameOptions::default(),
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, InferenceError::MismatchedShape { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn game_free_for_all_three_players() {
|
||||||
|
let a = default_rating();
|
||||||
|
let b = default_rating();
|
||||||
|
let c = default_rating();
|
||||||
|
let g = Game::<i64, _>::free_for_all(
|
||||||
|
&[&a, &b, &c],
|
||||||
|
Outcome::ranking([0, 1, 2]),
|
||||||
|
&GameOptions::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let p = g.posteriors();
|
||||||
|
assert_eq!(p.len(), 3);
|
||||||
|
assert!(p[0][0].mu() > p[1][0].mu());
|
||||||
|
assert!(p[1][0].mu() > p[2][0].mu());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn game_log_evidence_is_finite() {
|
||||||
|
let a = default_rating();
|
||||||
|
let b = default_rating();
|
||||||
|
let g = Game::<i64, _>::ranked(
|
||||||
|
&[&[a], &[b]],
|
||||||
|
Outcome::winner(0, 2),
|
||||||
|
&GameOptions::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(g.log_evidence().is_finite());
|
||||||
|
assert!(g.log_evidence() < 0.0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user