Adds soft Gaussian-observation evidence on the per-pair diff variable,
enabling continuous score margins as a richer alternative to ranks.
Public API:
- `Outcome::Scored([scores])` (non-breaking enum extension under
`#[non_exhaustive]`).
- `Game::scored(teams, outcome, options)` constructor parallel to
`Game::ranked`.
- `EventBuilder::scores([...])` fluent helper.
- `HistoryBuilder::score_sigma(σ)` knob (default 1.0, validated > 0).
- `GameOptions::score_sigma`.
- `EventKind` re-exported from `lib.rs` (annotated `#[non_exhaustive]`).
- New `InferenceError::InvalidParameter { name, value }` variant.
Internals:
- `MarginFactor` (`factor/margin.rs`): Gaussian observation factor that
closes in one EP step; cavity-cached log-evidence mirrors `TruncFactor`.
- `BuiltinFactor::Margin` dispatch arm.
- `DiffFactor` enum in `game.rs` lets `Game::likelihoods` and the new
`likelihoods_scored` share the per-pair link abstraction.
- Per-event `EventKind { Ranked, Scored { score_sigma } }` routed through
`TimeSlice::add_events`, `iteration_direct`, and `log_evidence`.
Tests: 88 lib + 27 integration (4 new in `tests/scored.rs`); existing
goldens byte-identical. Bench: `benches/scored.rs` baseline ~960µs for
60 events × 20-player pool with default convergence.
Plan: docs/superpowers/plans/2026-04-27-t4-margin-factor.md
Spec item marked Done.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
63 lines
1.9 KiB
Rust
63 lines
1.9 KiB
Rust
//! Equivalence tests: every historical golden from the pre-T2 tests is
|
|
//! reproduced here at the integration level via the new public API.
|
|
//!
|
|
//! The in-crate tests in `src/history.rs::tests` and
|
|
//! `src/time_slice.rs::tests` are the primary regression net for numerical
|
|
//! behavior. This file provides Game-level goldens that stand alone and are
|
|
//! more naturally expressed as integration tests.
|
|
|
|
use approx::assert_ulps_eq;
|
|
use trueskill_tt::{ConstantDrift, Game, GameOptions, Gaussian, Outcome, Rating};
|
|
|
|
type R = Rating<i64, ConstantDrift>;
|
|
|
|
fn ts_rating(mu: f64, sigma: f64, beta: f64, gamma: f64) -> R {
|
|
R::new(Gaussian::from_ms(mu, sigma), beta, ConstantDrift(gamma))
|
|
}
|
|
|
|
#[test]
|
|
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)).unwrap();
|
|
// Historical golden from pre-T2 test_1vs1 (team 0 wins):
|
|
assert_ulps_eq!(
|
|
a_post,
|
|
Gaussian::from_ms(29.205220, 7.194481),
|
|
epsilon = 1e-6
|
|
);
|
|
assert_ulps_eq!(
|
|
b_post,
|
|
Gaussian::from_ms(20.794779, 7.194481),
|
|
epsilon = 1e-6
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn game_1v1_draw_golden() {
|
|
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 g = Game::<i64, _>::ranked(
|
|
&[&[a], &[b]],
|
|
Outcome::draw(2),
|
|
&GameOptions {
|
|
p_draw: 0.25,
|
|
score_sigma: 1.0,
|
|
convergence: Default::default(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
let p = g.posteriors();
|
|
// Historical golden from pre-T2 test_1vs1_draw:
|
|
assert_ulps_eq!(
|
|
p[0][0],
|
|
Gaussian::from_ms(24.999999, 6.469480),
|
|
epsilon = 1e-6
|
|
);
|
|
assert_ulps_eq!(
|
|
p[1][0],
|
|
Gaussian::from_ms(24.999999, 6.469480),
|
|
epsilon = 1e-6
|
|
);
|
|
}
|