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>
60 lines
1.9 KiB
Rust
60 lines
1.9 KiB
Rust
//! Worked example: continuous-score outcomes via `Outcome::Scored`.
|
|
//!
|
|
//! Three players play a small round-robin where the score margin matters,
|
|
//! not just who won. We show how `score_sigma` controls how much weight
|
|
//! the engine places on the observed margin.
|
|
//!
|
|
//! Run with: `cargo run --example scored --release`
|
|
|
|
use smallvec::smallvec;
|
|
use trueskill_tt::{ConstantDrift, Event, History, Member, Outcome, Team};
|
|
|
|
fn main() {
|
|
let mut h = History::builder()
|
|
.mu(25.0)
|
|
.sigma(25.0 / 3.0)
|
|
.beta(25.0 / 6.0)
|
|
.drift(ConstantDrift(0.03))
|
|
.score_sigma(2.0) // tune to data; smaller = trust margins more
|
|
.build();
|
|
|
|
let events: Vec<Event<i64, &'static str>> = vec![
|
|
Event {
|
|
time: 1,
|
|
teams: smallvec![
|
|
Team::with_members([Member::new("alice")]),
|
|
Team::with_members([Member::new("bob")]),
|
|
],
|
|
outcome: Outcome::scores([21.0, 9.0]),
|
|
},
|
|
Event {
|
|
time: 2,
|
|
teams: smallvec![
|
|
Team::with_members([Member::new("bob")]),
|
|
Team::with_members([Member::new("carol")]),
|
|
],
|
|
outcome: Outcome::scores([21.0, 18.0]),
|
|
},
|
|
Event {
|
|
time: 3,
|
|
teams: smallvec![
|
|
Team::with_members([Member::new("alice")]),
|
|
Team::with_members([Member::new("carol")]),
|
|
],
|
|
outcome: Outcome::scores([21.0, 21.0]),
|
|
},
|
|
];
|
|
h.add_events(events).unwrap();
|
|
|
|
let report = h.converge().unwrap();
|
|
println!(
|
|
"converged={}, iterations={}, log_evidence={:.4}",
|
|
report.converged, report.iterations, report.log_evidence
|
|
);
|
|
|
|
for who in &["alice", "bob", "carol"] {
|
|
let s = h.current_skill(who).unwrap();
|
|
println!("{:>6}: mu={:>7.3} sigma={:.3}", who, s.mu(), s.sigma());
|
|
}
|
|
}
|