Skip to main content

bones_sim/
clock.rs

1use serde::{Deserialize, Serialize};
2
3/// Configuration for generating per-agent simulated clocks.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5pub struct ClockConfig {
6    /// Base timestamp in milliseconds.
7    pub base_millis: i64,
8    /// Logical tick size in milliseconds per simulation round.
9    pub tick_millis: i64,
10    /// Maximum absolute drift in parts-per-million assigned per agent.
11    pub max_abs_drift_ppm: i32,
12    /// Maximum absolute skew in milliseconds assigned per agent.
13    pub max_abs_skew_millis: i64,
14}
15
16impl Default for ClockConfig {
17    fn default() -> Self {
18        Self {
19            base_millis: 1_700_000_000_000,
20            tick_millis: 100,
21            max_abs_drift_ppm: 100,
22            max_abs_skew_millis: 25,
23        }
24    }
25}
26
27/// Concrete per-agent clock specification (assigned from [`ClockConfig`]).
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub struct ClockSpec {
30    /// Base timestamp in milliseconds.
31    pub base_millis: i64,
32    /// Tick size in milliseconds per simulation round.
33    pub tick_millis: i64,
34    /// Drift in parts-per-million.
35    pub drift_ppm: i32,
36    /// Constant offset from baseline.
37    pub skew_millis: i64,
38}
39
40/// Simulated wall clock with drift, skew, and freeze controls.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct SimulatedClock {
43    spec: ClockSpec,
44    frozen_at: Option<i64>,
45}
46
47impl SimulatedClock {
48    /// Create a simulated clock from an assigned spec.
49    #[must_use]
50    pub const fn new(spec: ClockSpec) -> Self {
51        Self {
52            spec,
53            frozen_at: None,
54        }
55    }
56
57    /// Return the assigned spec.
58    #[must_use]
59    pub const fn spec(&self) -> ClockSpec {
60        self.spec
61    }
62
63    /// Return current wall time in milliseconds for a simulation round.
64    #[must_use]
65    pub fn now_millis(&self, round: u64) -> i64 {
66        if let Some(frozen) = self.frozen_at {
67            return frozen;
68        }
69
70        let round_i64 = i64::try_from(round).unwrap_or(i64::MAX);
71        let base_progress = self.spec.tick_millis.saturating_mul(round_i64);
72        let drift_adjust = base_progress
73            .saturating_mul(i64::from(self.spec.drift_ppm))
74            .saturating_div(1_000_000);
75
76        self.spec
77            .base_millis
78            .saturating_add(self.spec.skew_millis)
79            .saturating_add(base_progress)
80            .saturating_add(drift_adjust)
81    }
82
83    /// Freeze this clock at the current time.
84    pub fn freeze(&mut self, round: u64) {
85        self.frozen_at = Some(self.now_millis(round));
86    }
87
88    /// Unfreeze this clock.
89    pub const fn unfreeze(&mut self) {
90        self.frozen_at = None;
91    }
92
93    /// Whether this clock is currently frozen.
94    #[must_use]
95    pub const fn is_frozen(&self) -> bool {
96        self.frozen_at.is_some()
97    }
98}