Skip to main content

subtr_actor/stats/
boost_invariants.rs

1use super::reducers::BoostStats;
2use crate::*;
3
4/// A small tolerance for replay-boost accounting checks.
5///
6/// Some pickup amounts are inferred from frame deltas before the pad is fully
7/// resolved, so nominal pad-value identities are expected to be approximate
8/// rather than bit-exact in production. The drift grows slowly with pickup
9/// count, so use a small base tolerance plus a per-pickup allowance.
10pub const BOOST_INVARIANT_BASE_TOLERANCE_RAW: f32 = 2.0;
11pub const BOOST_INVARIANT_PER_PICKUP_TOLERANCE_RAW: f32 = 0.3;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum BoostInvariantKind {
15    BucketAmounts,
16    NominalPickupAmount,
17    NominalStolenPickupAmount,
18    CurrentAmount,
19}
20
21impl BoostInvariantKind {
22    pub const ALL: [Self; 4] = [
23        Self::BucketAmounts,
24        Self::NominalPickupAmount,
25        Self::NominalStolenPickupAmount,
26        Self::CurrentAmount,
27    ];
28
29    pub fn label(self) -> &'static str {
30        match self {
31            Self::BucketAmounts => "bucket_amounts",
32            Self::NominalPickupAmount => "nominal_pickup_amount",
33            Self::NominalStolenPickupAmount => "nominal_stolen_pickup_amount",
34            Self::CurrentAmount => "current_amount",
35        }
36    }
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct BoostInvariantViolation {
41    pub kind: BoostInvariantKind,
42    pub expected: f32,
43    pub actual: f32,
44    pub diff: f32,
45    pub tolerance: f32,
46}
47
48impl BoostInvariantViolation {
49    pub fn message(&self) -> String {
50        match self.kind {
51            BoostInvariantKind::BucketAmounts => format!(
52                "amount_collected_big + amount_collected_small should match amount_collected \
53                 (actual={:.1}, expected={:.1}, diff={:.1}, tolerance={:.1})",
54                self.actual, self.expected, self.diff, self.tolerance
55            ),
56            BoostInvariantKind::NominalPickupAmount => format!(
57                "amount_collected + overfill_total should match nominal pad value from pickup counts \
58                 (actual={:.1}, expected={:.1}, diff={:.1}, tolerance={:.1})",
59                self.actual, self.expected, self.diff, self.tolerance
60            ),
61            BoostInvariantKind::NominalStolenPickupAmount => format!(
62                "amount_stolen + overfill_from_stolen should match nominal stolen pad value from pickup counts \
63                 (actual={:.1}, expected={:.1}, diff={:.1}, tolerance={:.1})",
64                self.actual, self.expected, self.diff, self.tolerance
65            ),
66            BoostInvariantKind::CurrentAmount => format!(
67                "amount_obtained - amount_used should match observed current boost \
68                 (actual={:.1}, expected={:.1}, diff={:.1}, tolerance={:.1})",
69                self.actual, self.expected, self.diff, self.tolerance
70            ),
71        }
72    }
73}
74
75pub fn nominal_pickup_amount_from_counts(stats: &BoostStats) -> f32 {
76    stats.big_pads_collected as f32 * BOOST_MAX_AMOUNT
77        + stats.small_pads_collected as f32 * boost_percent_to_amount(12.0)
78}
79
80pub fn nominal_stolen_pickup_amount_from_counts(stats: &BoostStats) -> f32 {
81    stats.big_pads_stolen as f32 * BOOST_MAX_AMOUNT
82        + stats.small_pads_stolen as f32 * boost_percent_to_amount(12.0)
83}
84
85fn nominal_pickup_tolerance(pickup_count: u32) -> f32 {
86    BOOST_INVARIANT_BASE_TOLERANCE_RAW
87        + BOOST_INVARIANT_PER_PICKUP_TOLERANCE_RAW * pickup_count as f32
88}
89
90fn push_violation(
91    violations: &mut Vec<BoostInvariantViolation>,
92    kind: BoostInvariantKind,
93    expected: f32,
94    actual: f32,
95    tolerance: f32,
96) {
97    let diff = (actual - expected).abs();
98    if diff > tolerance {
99        violations.push(BoostInvariantViolation {
100            kind,
101            expected,
102            actual,
103            diff,
104            tolerance,
105        });
106    }
107}
108
109/// Returns the per-snapshot boost accounting violations that can be checked
110/// from cumulative `BoostStats`, plus an optional observed current boost amount.
111pub fn boost_invariant_violations(
112    stats: &BoostStats,
113    observed_boost_amount: Option<f32>,
114) -> Vec<BoostInvariantViolation> {
115    let mut violations = Vec::new();
116
117    push_violation(
118        &mut violations,
119        BoostInvariantKind::BucketAmounts,
120        stats.amount_collected,
121        stats.amount_collected_big + stats.amount_collected_small,
122        1.0,
123    );
124    push_violation(
125        &mut violations,
126        BoostInvariantKind::NominalPickupAmount,
127        nominal_pickup_amount_from_counts(stats),
128        stats.amount_collected + stats.overfill_total,
129        nominal_pickup_tolerance(stats.big_pads_collected + stats.small_pads_collected),
130    );
131    push_violation(
132        &mut violations,
133        BoostInvariantKind::NominalStolenPickupAmount,
134        nominal_stolen_pickup_amount_from_counts(stats),
135        stats.amount_stolen + stats.overfill_from_stolen,
136        nominal_pickup_tolerance(stats.big_pads_stolen + stats.small_pads_stolen),
137    );
138    if let Some(current_boost_amount) = observed_boost_amount {
139        push_violation(
140            &mut violations,
141            BoostInvariantKind::CurrentAmount,
142            current_boost_amount,
143            stats.amount_obtained() - stats.amount_used,
144            1.0,
145        );
146    }
147
148    violations
149}