Four README code blocks no longer compiled: `Player` was renamed `Rating`
in T2, the `Drift` trait gained a `T: Time` parameter and a second method,
and two blocks were missing imports outright. The `Rating` example needed
more than a rename — with the binding unused, `T` is ambiguous because
`ConstantDrift` implements `Drift<T>` for every `T`, so it now carries an
explicit annotation.
Nothing compiled those blocks. `src/lib.rs` gains a `cfg(doctest)` struct
carrying `#[doc = include_str!("../README.md")]`, which turns every `rust`
block into a doctest without displacing the curated crate docs as the
front page. Verified it bites: reintroducing `Player` fails the build with
E0432 rather than shipping. Illustrative blocks are fenced `text` — note
that a bare fence defaults to `rust` under rustdoc, which is how the
`variance_delta = elapsed * γ²` formula became a compile error.
Prose fixes: README claimed `Gaussian::forget` takes a square root (it
works in variance space) and pointed at a `.gamma()` builder method that
does not exist. CLAUDE.md's data-flow diagram spliced the public ingestion
shape into the internal one — `Team` is not in that chain — listed
`cdf()`/`erfc()` as public when they are `pub(crate)` and private, and
called `SkillStore` public when only `CompetitorStore` escapes the crate.
Rustdoc fixes: `EventBuilder::scores_with_sigma` claimed a debug-assert
that `Outcome::scores_with_sigma` never had and whose own docs contradict;
rejection happens at ingestion as `InvalidParameter`. `event.rs` described
`add_events_with_prior` as replaced when it is still the ingestion
chokepoint. `factors.rs` advertised `Game::custom` without noting it is
`#[doc(hidden)]`. Internal T2/T4 milestone labels are dropped from public
items; the ones in the private `time_slice` module are left alone.
Closes #35
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014b6wy2q8rnFK8U8GPJVQNU
78 lines
2.4 KiB
Rust
78 lines
2.4 KiB
Rust
use crate::{
|
|
drift::{ConstantDrift, Drift},
|
|
gaussian::Gaussian,
|
|
rating::Rating,
|
|
time::Time,
|
|
};
|
|
|
|
/// Per-history, temporal state for someone competing.
|
|
///
|
|
/// The mutable half of a competitor: `Rating` holds their static
|
|
/// configuration, this holds what inference learns as it sweeps.
|
|
#[derive(Debug)]
|
|
pub struct Competitor<T: Time = i64, D: Drift<T> = ConstantDrift> {
|
|
pub rating: Rating<T, D>,
|
|
/// The forward message carried from this competitor's last appearance, or
|
|
/// `None` before they have appeared anywhere.
|
|
///
|
|
/// Previously an improper `N_INF` served as the unset sentinel, which made
|
|
/// "no message yet" indistinguishable from "a legitimately improper
|
|
/// message" at the type level and required every reader to know the
|
|
/// convention.
|
|
pub message: Option<Gaussian>,
|
|
pub last_time: Option<T>,
|
|
}
|
|
|
|
impl<T: Time, D: Drift<T>> Competitor<T, D> {
|
|
/// Compute the message received at time `now`, with drift accumulated
|
|
/// from `self.last_time` (if any) to `now`.
|
|
pub(crate) fn receive(&self, now: &T) -> Gaussian {
|
|
match self.message {
|
|
Some(message) => {
|
|
let elapsed_variance = match &self.last_time {
|
|
Some(last) => self.rating.drift_variance_delta(last, now),
|
|
None => 0.0,
|
|
};
|
|
|
|
message.forget(elapsed_variance)
|
|
}
|
|
None => self.rating.prior,
|
|
}
|
|
}
|
|
|
|
/// Compute the message using a pre-cached elapsed count (in `Time::elapsed_to` units).
|
|
///
|
|
/// Used in convergence sweeps where the elapsed was cached at slice-construction time
|
|
/// and should not be recomputed from `last_time` (which may have shifted).
|
|
pub(crate) fn receive_for_elapsed(&self, elapsed: i64) -> Gaussian {
|
|
match self.message {
|
|
Some(message) => message.forget(self.rating.drift_variance_for_elapsed(elapsed)),
|
|
None => self.rating.prior,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Competitor<i64, ConstantDrift> {
|
|
fn default() -> Self {
|
|
Self {
|
|
rating: Rating::default(),
|
|
message: None,
|
|
last_time: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn clean<'a, T, D, C>(competitors: C, last_time: bool)
|
|
where
|
|
T: Time + 'a,
|
|
D: Drift<T> + 'a,
|
|
C: Iterator<Item = &'a mut Competitor<T, D>>,
|
|
{
|
|
for c in competitors {
|
|
c.message = None;
|
|
if last_time {
|
|
c.last_time = None;
|
|
}
|
|
}
|
|
}
|