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>
97 lines
2.4 KiB
Rust
97 lines
2.4 KiB
Rust
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);
|
|
}
|