Under #[cfg(feature = "rayon")], the per-iteration event sweep processes events color-by-color: within a color, events touch disjoint Index values by construction, so par_iter is safe. Across colors, sequential ordering preserves async-EP semantics. Event::compute() is a pure function returning an owned EventOutput (new per-item likelihoods, evidence, and pre-computed new skill likelihoods). The apply phase runs sequentially after the parallel map, writing EventOutput values back to SkillStore and each event's item likelihoods. This avoids shared mutable state in the hot loop. Default build (no rayon) uses a sequential fallback that traverses the same color-group order — behaviorally identical to the parallel path. This keeps goldens bit-identical across feature configurations. Scenario 3b applied: event updates read from and write to the shared SkillStore, so the compute/apply split (Option A) was necessary. Part of T3 of docs/superpowers/specs/2026-04-23-trueskill-engine-redesign-design.md.
TrueSkill - Through Time
Rust port of TrueSkillThroughTime.py.
Other implementations
- ttt-scala
- ChessAnalysis #F
- TrueSkillThroughTime.jl
- TrueSkillThroughTime.R
- TrueSkill Through Time: Revisiting the History of Chess
- TrueSkill Through Time. The full scientific documentation
Drift
Skill drift models how a player's true skill can change between appearances. Each time a player reappears after a gap, their skill uncertainty is widened by the drift model before the new evidence is incorporated.
Drift is represented by the Drift trait:
pub trait Drift: Copy + Debug {
fn variance_delta(&self, elapsed: i64) -> f64;
}
variance_delta returns the amount to add to σ² given the elapsed time since the player last played. Internally, Gaussian::forget uses this to compute the new sigma: σ_new = sqrt(σ² + variance_delta).
ConstantDrift
The built-in ConstantDrift implements a linear random walk — skill uncertainty grows proportionally to time:
variance_delta = elapsed * γ²
This is the standard TrueSkill Through Time model. Use it by passing a ConstantDrift(gamma) when constructing a Player:
use trueskill_tt::{Player, Gaussian, drift::ConstantDrift};
// gamma = 0.1 means skill can shift ~0.1 per time unit
let player = Player::new(Gaussian::from_ms(0.0, 6.0), 1.0, ConstantDrift(0.1));
Custom drift
Implement Drift to express any other model. For example, a drift that saturates after a long absence (uncertainty grows with the square root of elapsed time instead of linearly):
use trueskill_tt::drift::Drift;
#[derive(Clone, Copy, Debug)]
struct SqrtDrift {
gamma: f64,
}
impl Drift for SqrtDrift {
fn variance_delta(&self, elapsed: i64) -> f64 {
(elapsed as f64).sqrt() * self.gamma * self.gamma
}
}
let player = Player::new(Gaussian::from_ms(0.0, 6.0), 1.0, SqrtDrift { gamma: 0.5 });
To use a custom drift type with History, use the .drift() builder method instead of .gamma():
let h = History::builder()
.drift(SqrtDrift { gamma: 0.5 })
.build();
Todo
- Implement approx for Gaussian
- Add more tests from
TrueSkillThroughTime.jl - Add tests for
quality()(Use sublee/trueskill as reference) - Benchmark Batch::iteration()
- Time needs to be an enum so we can have multiple states (see
batch::compute_elapsed()) - Add examples (use same TrueSkillThroughTime.(py|jl))
- Add Observer (see argmin for inspiration)