Skip to main content

fforge_core/
observer.rs

1//! The trace/telemetry spine, instrumented from Phase 1 (DESIGN.md §9).
2//!
3//! An `EventObserver` is a **passive consumer of the event stream — it never
4//! writes to the world.** This is the seam the reusable evaluation kernel
5//! grows along: trace capture, scenario replay, and scoring all subscribe
6//! here. `SeasonTelemetry` is the first concrete consumer, and doubles as the
7//! embryo of the Phase-2 calibration harness (goals/game, home advantage —
8//! the exact aggregates the harness will check against reality).
9
10use crate::event::Event;
11
12pub trait EventObserver {
13    fn on_event(&mut self, event: &Event);
14}
15
16#[derive(Debug, Default, Clone)]
17pub struct SeasonTelemetry {
18    pub matches: u32,
19    pub goals: u32,
20    pub home_wins: u32,
21    pub draws: u32,
22    pub away_wins: u32,
23    pub scoreline_counts: std::collections::BTreeMap<(u8, u8), u32>,
24}
25
26impl SeasonTelemetry {
27    pub fn goals_per_match(&self) -> f64 {
28        if self.matches == 0 {
29            return 0.0;
30        }
31        self.goals as f64 / self.matches as f64
32    }
33
34    pub fn home_win_rate(&self) -> f64 {
35        if self.matches == 0 {
36            return 0.0;
37        }
38        self.home_wins as f64 / self.matches as f64
39    }
40}
41
42impl EventObserver for SeasonTelemetry {
43    fn on_event(&mut self, event: &Event) {
44        if let Event::MatchPlayed {
45            home_goals,
46            away_goals,
47            ..
48        } = event
49        {
50            self.matches += 1;
51            self.goals += (*home_goals + *away_goals) as u32;
52            match home_goals.cmp(away_goals) {
53                std::cmp::Ordering::Greater => self.home_wins += 1,
54                std::cmp::Ordering::Equal => self.draws += 1,
55                std::cmp::Ordering::Less => self.away_wins += 1,
56            }
57            *self
58                .scoreline_counts
59                .entry((*home_goals, *away_goals))
60                .or_default() += 1;
61        }
62    }
63}