Anders Olsson 3680c54d3c feat(time-slice): parallel within-slice event iteration via rayon
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.
2026-04-24 13:48:41 +02:00
2026-03-23 14:21:23 +01:00
2026-04-23 20:24:10 +02:00
2026-04-23 20:26:52 +02:00

TrueSkill - Through Time

Rust port of TrueSkillThroughTime.py.

Other implementations

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)
Description
No description provided
Readme 9.1 MiB
Languages
Rust 99.9%