refactor!: close the remaining API gaps from #21
Three unrelated small defects, all requiring signature changes: - `Game::one_v_one` hardcoded `GameOptions::default()`, so a 1v1 could never set `p_draw` or convergence options — and a drawn 1v1 was therefore unreachable through it, since the default `p_draw` is zero. It now takes `&GameOptions` like every other constructor. - `Observer::on_batch_processed` was declared on the trait and never called from anywhere: implementors wired up a callback that could not fire. It is now called after each slice sweep, and renamed `on_slice_processed` to match the vocabulary the codebase adopted in T2 — the unit of work is a `TimeSlice`, not a batch. A slice is swept once travelling backward and once forward, so a multi-slice history fires it twice per slice per iteration; the doc comment says so. - `pub mod factors` sat beside `pub(crate) mod factor`, two module paths differing by one character with only one of them importable. The public facade is now `graph`. Tests cover each as a behaviour rather than a compile check: a drawn 1v1 succeeds only when p_draw is supplied, and the observer tests fail if any callback stops firing. BREAKING CHANGE: `Game::one_v_one` takes a fourth `&GameOptions` argument; `Observer::on_batch_processed` is renamed `on_slice_processed`; the `factors` module is renamed `graph`. Closes #21 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011hcFjNDmHXZF8URGLku5zZ
This commit is contained in:
+44
-1
@@ -32,7 +32,8 @@ fn game_ranked_1v1_golden() {
|
||||
fn game_one_v_one_shortcut() {
|
||||
let a = default_rating();
|
||||
let b = default_rating();
|
||||
let (a_post, b_post) = Game::<i64, _>::one_v_one(&a, &b, Outcome::winner(0, 2)).unwrap();
|
||||
let (a_post, b_post) =
|
||||
Game::<i64, _>::one_v_one(&a, &b, Outcome::winner(0, 2), &GameOptions::default()).unwrap();
|
||||
assert!(a_post.mu() > 25.0);
|
||||
assert!(b_post.mu() < 25.0);
|
||||
}
|
||||
@@ -95,3 +96,45 @@ fn game_log_evidence_is_finite() {
|
||||
assert!(g.log_evidence().is_finite());
|
||||
assert!(g.log_evidence() < 0.0);
|
||||
}
|
||||
|
||||
/// `one_v_one` used to hardcode `GameOptions::default()`, so a 1v1 could
|
||||
/// never set `p_draw` and a drawn 1v1 was unreachable through it.
|
||||
#[test]
|
||||
fn one_v_one_honours_the_draw_probability_it_is_given() {
|
||||
let a = default_rating();
|
||||
let b = default_rating();
|
||||
|
||||
// Default options still reject a draw, because the default p_draw is zero.
|
||||
let err = Game::<i64, _>::one_v_one(&a, &b, Outcome::draw(2), &GameOptions::default())
|
||||
.expect_err("a draw needs a positive p_draw");
|
||||
assert!(matches!(
|
||||
err,
|
||||
InferenceError::TieWithoutDrawProbability { .. }
|
||||
));
|
||||
|
||||
// With a draw probability supplied it succeeds — which was impossible
|
||||
// before the signature took options.
|
||||
let options = GameOptions {
|
||||
p_draw: 0.25,
|
||||
..GameOptions::default()
|
||||
};
|
||||
let (a_post, b_post) = Game::<i64, _>::one_v_one(&a, &b, Outcome::draw(2), &options)
|
||||
.expect("a draw is representable once p_draw is positive");
|
||||
|
||||
// A symmetric draw leaves the means alone and sharpens both sides.
|
||||
assert!((a_post.mu() - b_post.mu()).abs() < 1e-9);
|
||||
assert!(a_post.sigma() < 25.0 / 3.0);
|
||||
}
|
||||
|
||||
/// Convergence options reach the 1v1 path too, not just `p_draw`.
|
||||
#[test]
|
||||
fn one_v_one_honours_convergence_options() {
|
||||
let a = default_rating();
|
||||
let b = default_rating();
|
||||
let options = GameOptions {
|
||||
convergence: ConvergenceOptions::default(),
|
||||
..GameOptions::default()
|
||||
};
|
||||
let (a_post, _) = Game::<i64, _>::one_v_one(&a, &b, Outcome::winner(0, 2), &options).unwrap();
|
||||
assert!(a_post.mu() > 25.0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user