Skip to main content

jugar_web/
trace.rs

1//! Game Event Tracing and Deterministic Replay
2//!
3//! Implements the Renacer-Based Game Event Tracing specification (TRACE-001 v1.3).
4//!
5//! ## Key Components
6//!
7//! - [`Fixed32`]: Cross-platform deterministic 16.16 fixed-point math (Poka-Yoke)
8//! - [`FrameRecord`]: Lean per-frame trace data (Muda elimination)
9//! - [`TraceBuffer`]: Ring buffer with Andon Cord overflow policy (Jidoka)
10//! - [`AdaptiveSnapshotter`]: Entropy-based snapshot scheduling (Heijunka)
11//! - [`AndonState`]: Visual trace loss indicator (Soft Andon - v1.3)
12//! - [`ZobristTable`]: O(1) incremental state hashing (v1.3)
13//!
14//! ## Toyota Production System Principles
15//!
16//! - **Jidoka**: Buffer overflow with Soft Andon visual indicator (v1.3)
17//! - **Poka-Yoke**: Fixed32 with overflow checks (Regehr 2012) (v1.3)
18//! - **Heijunka**: Zobrist hashing for O(1) entropy detection (v1.3)
19//! - **Muda**: Frame number is the only clock; no physical timestamps
20//!
21//! ## References
22//!
23//! - Lamport (1978): Logical clocks for causal ordering
24//! - Monniaux (2008): IEEE 754 non-determinism across platforms
25//! - Dunlap (2002): ALL inputs must be logged for faithful replay
26//! - Elnozahy (2002): Adaptive checkpointing based on state mutation rates
27//! - Zobrist (1970): O(1) incremental hashing for games
28//! - Regehr (2012): Integer overflow detection and prevention
29//! - MacKenzie & Ware (1993): Input lag and human performance
30
31use core::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
32use serde::{Deserialize, Serialize};
33
34// =============================================================================
35// Fixed32: Cross-Platform Deterministic Math (Poka-Yoke)
36// =============================================================================
37
38/// Fixed-point number with 16.16 format (16 integer bits, 16 fractional bits).
39///
40/// Guarantees identical results across ALL platforms per Monniaux (2008):
41/// "Floating-point non-determinism is a primary source of divergence in
42/// cross-platform replay systems."
43///
44/// # Examples
45///
46/// ```
47/// use jugar_web::trace::Fixed32;
48///
49/// let a = Fixed32::from_int(5);
50/// let b = Fixed32::from_int(3);
51/// assert_eq!((a + b).to_int(), 8);
52/// assert_eq!((a - b).to_int(), 2);
53/// assert_eq!((a * b).to_int(), 15);
54/// assert_eq!((a / b).to_int(), 1); // Integer division
55/// ```
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
57#[repr(transparent)]
58pub struct Fixed32(pub i32);
59
60impl Fixed32 {
61    /// Number of fractional bits (16.16 format)
62    pub const FRAC_BITS: u32 = 16;
63
64    /// Scale factor (2^16 = 65536)
65    pub const SCALE: i32 = 1 << Self::FRAC_BITS;
66
67    /// Zero
68    pub const ZERO: Self = Self(0);
69
70    /// One (1.0 in fixed-point)
71    pub const ONE: Self = Self(Self::SCALE);
72
73    /// Half (0.5 in fixed-point)
74    pub const HALF: Self = Self(Self::SCALE / 2);
75
76    /// Minimum value
77    pub const MIN: Self = Self(i32::MIN);
78
79    /// Maximum value
80    pub const MAX: Self = Self(i32::MAX);
81
82    /// Epsilon (smallest positive value)
83    pub const EPSILON: Self = Self(1);
84
85    /// Pi approximation (accurate to ~5 decimal places)
86    pub const PI: Self = Self(205_887); // 3.14159 * 65536
87
88    /// Create from raw fixed-point value (internal representation).
89    #[inline]
90    #[must_use]
91    pub const fn from_raw(raw: i32) -> Self {
92        Self(raw)
93    }
94
95    /// Get the raw fixed-point value.
96    #[inline]
97    #[must_use]
98    pub const fn to_raw(self) -> i32 {
99        self.0
100    }
101
102    /// Create from integer.
103    #[inline]
104    #[must_use]
105    pub const fn from_int(n: i32) -> Self {
106        Self(n << Self::FRAC_BITS)
107    }
108
109    /// Convert to integer (truncates fractional part).
110    #[inline]
111    #[must_use]
112    pub const fn to_int(self) -> i32 {
113        self.0 >> Self::FRAC_BITS
114    }
115
116    /// Create from f32 (use only for constants, not runtime game logic).
117    ///
118    /// # Warning
119    ///
120    /// This conversion introduces platform-dependent rounding. Only use for
121    /// initialization from constants; never in game update loops.
122    #[inline]
123    #[must_use]
124    pub fn from_f32(f: f32) -> Self {
125        Self((f * Self::SCALE as f32) as i32)
126    }
127
128    /// Convert to f32 (for rendering only, not game logic).
129    ///
130    /// # Warning
131    ///
132    /// The result may differ slightly across platforms. Only use for rendering;
133    /// never store or compare these values for game logic.
134    #[inline]
135    #[must_use]
136    pub fn to_f32(self) -> f32 {
137        self.0 as f32 / Self::SCALE as f32
138    }
139
140    /// Saturating addition (clamps to MIN/MAX instead of overflowing).
141    #[inline]
142    #[must_use]
143    pub const fn saturating_add(self, other: Self) -> Self {
144        Self(self.0.saturating_add(other.0))
145    }
146
147    /// Saturating subtraction (clamps to MIN/MAX instead of overflowing).
148    #[inline]
149    #[must_use]
150    pub const fn saturating_sub(self, other: Self) -> Self {
151        Self(self.0.saturating_sub(other.0))
152    }
153
154    /// Fixed-point multiplication with proper scaling.
155    ///
156    /// Uses i64 intermediate to prevent overflow.
157    #[inline]
158    #[must_use]
159    pub const fn mul(self, other: Self) -> Self {
160        Self(((self.0 as i64 * other.0 as i64) >> Self::FRAC_BITS) as i32)
161    }
162
163    /// Saturating multiplication (clamps instead of overflowing).
164    #[inline]
165    #[must_use]
166    #[allow(clippy::cast_lossless)] // Can't use From in const fn yet
167    pub const fn saturating_mul(self, other: Self) -> Self {
168        let result = (self.0 as i64 * other.0 as i64) >> Self::FRAC_BITS;
169        if result > i32::MAX as i64 {
170            Self::MAX
171        } else if result < i32::MIN as i64 {
172            Self::MIN
173        } else {
174            Self(result as i32)
175        }
176    }
177
178    /// Fixed-point division with proper scaling.
179    ///
180    /// Uses i64 intermediate to prevent overflow.
181    ///
182    /// # Panics
183    ///
184    /// Panics if `other` is zero.
185    #[inline]
186    #[must_use]
187    pub const fn div(self, other: Self) -> Self {
188        Self((((self.0 as i64) << Self::FRAC_BITS) / other.0 as i64) as i32)
189    }
190
191    /// Checked division (returns None if divisor is zero).
192    #[inline]
193    #[must_use]
194    pub const fn checked_div(self, other: Self) -> Option<Self> {
195        if other.0 == 0 {
196            None
197        } else {
198            Some(self.div(other))
199        }
200    }
201
202    /// Absolute value.
203    #[inline]
204    #[must_use]
205    pub const fn abs(self) -> Self {
206        Self(self.0.abs())
207    }
208
209    /// Sign of the number (-1, 0, or 1).
210    #[inline]
211    #[must_use]
212    pub const fn signum(self) -> Self {
213        Self::from_int(self.0.signum())
214    }
215
216    /// Check if negative.
217    #[inline]
218    #[must_use]
219    pub const fn is_negative(self) -> bool {
220        self.0 < 0
221    }
222
223    /// Check if positive.
224    #[inline]
225    #[must_use]
226    pub const fn is_positive(self) -> bool {
227        self.0 > 0
228    }
229
230    /// Check if zero.
231    #[inline]
232    #[must_use]
233    pub const fn is_zero(self) -> bool {
234        self.0 == 0
235    }
236
237    /// Clamp value to range [min, max].
238    #[inline]
239    #[must_use]
240    pub const fn clamp(self, min: Self, max: Self) -> Self {
241        if self.0 < min.0 {
242            min
243        } else if self.0 > max.0 {
244            max
245        } else {
246            self
247        }
248    }
249
250    /// Linear interpolation between self and other.
251    ///
252    /// `t` should be between 0.0 and 1.0 (as Fixed32).
253    #[inline]
254    #[must_use]
255    pub const fn lerp(self, other: Self, t: Self) -> Self {
256        // self + (other - self) * t
257        let diff = Self(other.0 - self.0);
258        let scaled = diff.mul(t);
259        Self(self.0 + scaled.0)
260    }
261
262    /// Floor to integer (round toward negative infinity).
263    #[inline]
264    #[must_use]
265    pub const fn floor(self) -> Self {
266        Self((self.0 >> Self::FRAC_BITS) << Self::FRAC_BITS)
267    }
268
269    /// Ceiling to integer (round toward positive infinity).
270    #[inline]
271    #[must_use]
272    pub const fn ceil(self) -> Self {
273        let frac_mask = Self::SCALE - 1;
274        if self.0 & frac_mask == 0 {
275            self
276        } else {
277            Self(((self.0 >> Self::FRAC_BITS) + 1) << Self::FRAC_BITS)
278        }
279    }
280
281    /// Round to nearest integer.
282    #[inline]
283    #[must_use]
284    pub const fn round(self) -> Self {
285        Self(((self.0 + (Self::SCALE / 2)) >> Self::FRAC_BITS) << Self::FRAC_BITS)
286    }
287
288    /// Get fractional part.
289    #[inline]
290    #[must_use]
291    pub const fn fract(self) -> Self {
292        let frac_mask = Self::SCALE - 1;
293        Self(self.0 & frac_mask)
294    }
295
296    // =========================================================================
297    // v1.3 TPS Kaizen: Overflow Checks (Regehr 2012)
298    // =========================================================================
299
300    /// Checked multiplication - returns None on overflow (Regehr 2012).
301    ///
302    /// Use this in game logic where overflow indicates a bug that should
303    /// be caught early in development.
304    ///
305    /// # Examples
306    ///
307    /// ```
308    /// use jugar_web::trace::Fixed32;
309    ///
310    /// let a = Fixed32::from_int(100);
311    /// let b = Fixed32::from_int(50);
312    /// assert_eq!(a.checked_mul(b), Some(Fixed32::from_int(5000)));
313    ///
314    /// // Overflow case
315    /// let big = Fixed32::MAX;
316    /// assert_eq!(big.checked_mul(Fixed32::from_int(2)), None);
317    /// ```
318    #[inline]
319    #[must_use]
320    pub const fn checked_mul(self, other: Self) -> Option<Self> {
321        let result = self.0 as i64 * other.0 as i64;
322        let shifted = result >> Self::FRAC_BITS;
323
324        // Check for overflow when converting back to i32
325        if shifted > i32::MAX as i64 || shifted < i32::MIN as i64 {
326            return None;
327        }
328
329        Some(Self(shifted as i32))
330    }
331
332    /// Strict multiplication - panics on overflow in all builds (Regehr 2012).
333    ///
334    /// Use this in game logic where overflow indicates a bug.
335    /// This is the safest option for catching bugs early.
336    ///
337    /// # Panics
338    ///
339    /// Panics if the multiplication would overflow.
340    ///
341    /// # Examples
342    ///
343    /// ```
344    /// use jugar_web::trace::Fixed32;
345    ///
346    /// let a = Fixed32::from_int(100);
347    /// let b = Fixed32::from_int(50);
348    /// assert_eq!(a.strict_mul(b), Fixed32::from_int(5000));
349    /// ```
350    #[inline]
351    #[must_use]
352    #[track_caller]
353    #[allow(clippy::panic)] // Intentional: strict_* methods should panic on overflow (Regehr 2012)
354    pub const fn strict_mul(self, other: Self) -> Self {
355        match self.checked_mul(other) {
356            Some(result) => result,
357            None => panic!("Fixed32 multiplication overflow"),
358        }
359    }
360
361    /// Checked addition - returns None on overflow.
362    #[inline]
363    #[must_use]
364    pub const fn checked_add(self, other: Self) -> Option<Self> {
365        match self.0.checked_add(other.0) {
366            Some(result) => Some(Self(result)),
367            None => None,
368        }
369    }
370
371    /// Strict addition - panics on overflow.
372    ///
373    /// # Panics
374    ///
375    /// Panics if the addition would overflow.
376    #[inline]
377    #[must_use]
378    #[track_caller]
379    #[allow(clippy::panic)] // Intentional: strict_* methods should panic on overflow (Regehr 2012)
380    pub const fn strict_add(self, other: Self) -> Self {
381        match self.checked_add(other) {
382            Some(result) => result,
383            None => panic!("Fixed32 addition overflow"),
384        }
385    }
386
387    /// Checked subtraction - returns None on overflow.
388    #[inline]
389    #[must_use]
390    pub const fn checked_sub(self, other: Self) -> Option<Self> {
391        match self.0.checked_sub(other.0) {
392            Some(result) => Some(Self(result)),
393            None => None,
394        }
395    }
396
397    /// Strict subtraction - panics on overflow.
398    ///
399    /// # Panics
400    ///
401    /// Panics if the subtraction would overflow.
402    #[inline]
403    #[must_use]
404    #[track_caller]
405    #[allow(clippy::panic)] // Intentional: strict_* methods should panic on overflow (Regehr 2012)
406    pub const fn strict_sub(self, other: Self) -> Self {
407        match self.checked_sub(other) {
408            Some(result) => result,
409            None => panic!("Fixed32 subtraction overflow"),
410        }
411    }
412}
413
414// =============================================================================
415// deterministic! Macro: Compile-Time f32 Enforcement (Bessey 2010)
416// =============================================================================
417
418/// Poka-Yoke macro to enforce Fixed32 in deterministic game logic (Bessey 2010).
419///
420/// This macro wraps a block and causes compile errors if f32/f64 literals are used.
421/// It works by shadowing the f32/f64 types with unconstructable struct types.
422///
423/// # Examples
424///
425/// ```
426/// use jugar_web::{deterministic, trace::Fixed32};
427///
428/// deterministic! {
429///     let a = Fixed32::from_int(5);
430///     let b = Fixed32::from_int(3);
431///     let result = a.mul(b);
432///     // This would cause a compile error:
433///     // let bad = 1.0f32 * 2.0;  // ERROR: f32 is shadowed
434/// }
435/// ```
436///
437/// # Rationale
438///
439/// Per Monniaux (2008): "Floating-point non-determinism is a primary source of
440/// divergence in cross-platform replay systems."
441///
442/// Per Bessey (2010): "Using compile-time constraints to enforce invariants
443/// catches bugs earlier than runtime checks."
444#[macro_export]
445macro_rules! deterministic {
446    ($($body:tt)*) => {{
447        // Shadow f32/f64 types with unconstructable types
448        // Any use of f32/f64 literals will fail to compile
449        #[allow(non_camel_case_types)]
450        #[allow(dead_code)]
451        struct f32;
452        #[allow(non_camel_case_types)]
453        #[allow(dead_code)]
454        struct f64;
455
456        $($body)*
457    }};
458}
459
460// Implement standard traits for ergonomic usage
461impl Default for Fixed32 {
462    fn default() -> Self {
463        Self::ZERO
464    }
465}
466
467impl Add for Fixed32 {
468    type Output = Self;
469
470    #[inline]
471    fn add(self, other: Self) -> Self {
472        Self(self.0.wrapping_add(other.0))
473    }
474}
475
476impl AddAssign for Fixed32 {
477    #[inline]
478    fn add_assign(&mut self, other: Self) {
479        self.0 = self.0.wrapping_add(other.0);
480    }
481}
482
483impl Sub for Fixed32 {
484    type Output = Self;
485
486    #[inline]
487    fn sub(self, other: Self) -> Self {
488        Self(self.0.wrapping_sub(other.0))
489    }
490}
491
492impl SubAssign for Fixed32 {
493    #[inline]
494    fn sub_assign(&mut self, other: Self) {
495        self.0 = self.0.wrapping_sub(other.0);
496    }
497}
498
499impl Mul for Fixed32 {
500    type Output = Self;
501
502    #[inline]
503    fn mul(self, other: Self) -> Self {
504        self.mul(other)
505    }
506}
507
508impl Div for Fixed32 {
509    type Output = Self;
510
511    #[inline]
512    fn div(self, other: Self) -> Self {
513        self.div(other)
514    }
515}
516
517impl Neg for Fixed32 {
518    type Output = Self;
519
520    #[inline]
521    fn neg(self) -> Self {
522        Self(-self.0)
523    }
524}
525
526impl From<i32> for Fixed32 {
527    fn from(n: i32) -> Self {
528        Self::from_int(n)
529    }
530}
531
532impl core::fmt::Display for Fixed32 {
533    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
534        write!(f, "{:.4}", self.to_f32())
535    }
536}
537
538// =============================================================================
539// InputEvent: Lean Input Record (Muda Elimination)
540// =============================================================================
541
542/// Input event type for trace recording.
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
544#[allow(missing_docs)] // Enum variant fields are self-documenting
545pub enum InputEventType {
546    /// Key pressed (key code)
547    KeyDown(u8),
548    /// Key released (key code)
549    KeyUp(u8),
550    /// Mouse button pressed (button, x, y)
551    MouseDown { button: u8, x: i16, y: i16 },
552    /// Mouse button released (button, x, y)
553    MouseUp { button: u8, x: i16, y: i16 },
554    /// Mouse moved (x, y)
555    MouseMove { x: i16, y: i16 },
556    /// Touch started (id, x, y)
557    TouchStart { id: u8, x: i16, y: i16 },
558    /// Touch moved (id, x, y)
559    TouchMove { id: u8, x: i16, y: i16 },
560    /// Touch ended (id, x, y)
561    TouchEnd { id: u8, x: i16, y: i16 },
562    /// Gamepad button pressed (gamepad, button)
563    GamepadDown { gamepad: u8, button: u8 },
564    /// Gamepad button released (gamepad, button)
565    GamepadUp { gamepad: u8, button: u8 },
566    /// Gamepad axis moved (gamepad, axis, value)
567    GamepadAxis { gamepad: u8, axis: u8, value: i16 },
568}
569
570/// Input event with frame-relative timing.
571///
572/// Per Jain (2014): "Logical time is sufficient for reproducing bugs;
573/// physical time introduces unnecessary noise."
574#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
575pub struct InputEvent {
576    /// Event type
577    pub event_type: InputEventType,
578    /// Microseconds since frame start (for sub-frame ordering only).
579    /// Max value: 16666 (one 60fps frame in microseconds).
580    pub frame_offset_us: u16,
581}
582
583// =============================================================================
584// FrameRecord: Lean Per-Frame Trace (Muda Elimination)
585// =============================================================================
586
587/// Lean frame record - frame_number is the only clock needed.
588///
589/// Per Jain (2014): "Logical time is sufficient for reproducing bugs."
590#[derive(Debug, Clone, Default, Serialize, Deserialize)]
591pub struct FrameRecord {
592    /// Frame number (monotonic, IS the logical clock for deterministic games).
593    pub frame: u64,
594    /// Input events this frame (relative timing within frame if needed).
595    pub inputs: Vec<InputEvent>,
596    /// State hash for verification (optional in release traces).
597    pub state_hash: Option<[u8; 32]>,
598}
599
600impl FrameRecord {
601    /// Create a new frame record.
602    #[must_use]
603    pub const fn new(frame: u64) -> Self {
604        Self {
605            frame,
606            inputs: Vec::new(),
607            state_hash: None,
608        }
609    }
610
611    /// Add an input event to this frame.
612    pub fn add_input(&mut self, event: InputEvent) {
613        self.inputs.push(event);
614    }
615
616    /// Set the state hash for verification.
617    pub const fn set_state_hash(&mut self, hash: [u8; 32]) {
618        self.state_hash = Some(hash);
619    }
620
621    /// Check if this frame has any inputs.
622    #[must_use]
623    #[allow(clippy::missing_const_for_fn)] // Vec::is_empty is not const
624    pub fn has_inputs(&self) -> bool {
625        !self.inputs.is_empty()
626    }
627}
628
629// =============================================================================
630// BufferPolicy: Jidoka Safety (Andon Cord)
631// =============================================================================
632
633/// Buffer overflow policy per Jidoka principle.
634///
635/// Per Dunlap (2002): "A trace with missing input events is worse than no trace at all."
636#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
637pub enum BufferPolicy {
638    /// Production mode: drop oldest events on overflow (preserves responsiveness).
639    #[default]
640    DropOldest,
641    /// v1.3: Soft Andon - drop oldest but show visual indicator (MacKenzie & Ware 1993).
642    /// This preserves game responsiveness while making trace loss impossible to ignore.
643    SoftAndon,
644    /// Debug mode: block game loop until buffer drains (preserves correctness).
645    /// This is intentional per Jidoka - "Stop the Line" when quality is at risk.
646    AndonCord,
647}
648
649// =============================================================================
650// AndonState: Visual Trace Loss Indicator (v1.3 Soft Andon)
651// =============================================================================
652
653/// Soft Andon state for visual trace loss indication (MacKenzie & Ware 1993).
654///
655/// Per MacKenzie & Ware (1993): "Input lag degrades human performance."
656/// Hard blocking (AndonCord) creates lag; SoftAndon preserves responsiveness
657/// while making trace loss visually impossible to ignore.
658///
659/// # Examples
660///
661/// ```
662/// use jugar_web::trace::AndonState;
663///
664/// let state = AndonState::Normal;
665/// assert_eq!(state.overlay_color(), [0.0, 0.0, 0.0, 0.0]); // Invisible
666///
667/// let warning = AndonState::Warning { buffer_pct: 85 };
668/// assert!(warning.overlay_color()[3] > 0.0); // Visible yellow
669///
670/// let loss = AndonState::TraceLoss { dropped_count: 10 };
671/// assert!(loss.overlay_color()[3] > 0.0); // Visible red
672/// ```
673#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
674pub enum AndonState {
675    /// Normal operation - all events being recorded.
676    #[default]
677    Normal,
678    /// Warning - buffer at 80%+ capacity.
679    Warning {
680        /// Buffer fill percentage (80-99).
681        buffer_pct: u8,
682    },
683    /// Trace loss - events are being dropped.
684    TraceLoss {
685        /// Number of frames dropped since last normal state.
686        dropped_count: u64,
687    },
688}
689
690impl AndonState {
691    /// Get HUD overlay color (Visual Management).
692    ///
693    /// Returns RGBA color for drawing over the game canvas.
694    #[must_use]
695    pub const fn overlay_color(&self) -> [f32; 4] {
696        match self {
697            Self::Normal => [0.0, 0.0, 0.0, 0.0],           // Invisible
698            Self::Warning { .. } => [1.0, 0.8, 0.0, 0.3],   // Yellow 30% opacity
699            Self::TraceLoss { .. } => [1.0, 0.0, 0.0, 0.5], // Red 50% opacity
700        }
701    }
702
703    /// Get status text for HUD display.
704    #[must_use]
705    pub const fn status_text(&self) -> &'static str {
706        match self {
707            Self::Normal => "",
708            Self::Warning { .. } => "TRACE BUFFER WARNING",
709            Self::TraceLoss { .. } => "TRACE LOSS - EVENTS DROPPED",
710        }
711    }
712
713    /// Check if in error state (warning or trace loss).
714    #[must_use]
715    pub const fn is_error(&self) -> bool {
716        !matches!(self, Self::Normal)
717    }
718
719    /// Check if any events have been dropped.
720    #[must_use]
721    pub const fn has_dropped(&self) -> bool {
722        matches!(self, Self::TraceLoss { .. })
723    }
724
725    /// Get number of dropped frames (0 if none).
726    #[must_use]
727    pub const fn dropped_count(&self) -> u64 {
728        match self {
729            Self::TraceLoss { dropped_count } => *dropped_count,
730            _ => 0,
731        }
732    }
733}
734
735// =============================================================================
736// ZobristTable: O(1) Incremental State Hashing (Zobrist 1970)
737// =============================================================================
738
739/// Number of hash fields for Zobrist table.
740/// This should cover all game state fields that affect determinism.
741pub const NUM_ZOBRIST_FIELDS: usize = 32;
742
743/// Zobrist hash table for O(1) incremental state hashing (Zobrist 1970).
744///
745/// Used by [`ZobristSnapshotter`] for efficient entropy detection.
746/// Instead of computing SHA-256 every frame (O(N)), we can update the hash
747/// incrementally when state changes (O(1)).
748///
749/// # Theory
750///
751/// Per Zobrist (1970): "A hash value can be updated incrementally by XORing
752/// out the old value and XORing in the new value."
753///
754/// Per Tridgell (1999): "Rolling checksums enable efficient delta detection
755/// without full state comparison."
756///
757/// # Examples
758///
759/// ```
760/// use jugar_web::trace::ZobristTable;
761///
762/// let table = ZobristTable::new(42);
763/// let state = [0u8; 32];
764/// let hash = table.hash_bytes(&state);
765///
766/// // Incremental update is O(1)
767/// let new_hash = table.update_hash(hash, 0, 0, 255);
768/// assert_ne!(hash, new_hash);
769/// ```
770#[derive(Debug, Clone)]
771pub struct ZobristTable {
772    /// Random values for each (field, value) pair.
773    /// Pre-generated on initialization for reproducibility.
774    /// Heap-allocated to avoid stack overflow (65KB array).
775    table: Box<[[u64; 256]; NUM_ZOBRIST_FIELDS]>,
776}
777
778impl ZobristTable {
779    /// Create table with deterministic RNG (for reproducibility).
780    ///
781    /// Uses xorshift64 for fast, deterministic random generation.
782    /// Table is heap-allocated (65KB) to avoid stack overflow.
783    ///
784    /// # Panics
785    ///
786    /// Cannot panic - the Vec-to-array conversion is guaranteed to succeed
787    /// because we create exactly `NUM_ZOBRIST_FIELDS` elements.
788    #[must_use]
789    #[allow(clippy::expect_used)] // Infallible: Vec size matches array size exactly
790    pub fn new(seed: u64) -> Self {
791        let mut state = seed.max(1); // Avoid zero seed
792                                     // Heap allocate to avoid 65KB stack frame (clippy::large_stack_arrays)
793        let mut table = vec![[0u64; 256]; NUM_ZOBRIST_FIELDS];
794
795        for field in &mut table {
796            for value in field.iter_mut() {
797                // xorshift64 (Marsaglia 2003)
798                state ^= state << 13;
799                state ^= state >> 7;
800                state ^= state << 17;
801                *value = state;
802            }
803        }
804
805        // Convert Vec to Box<[_; N]> for fixed-size guarantee
806        // This cannot fail: we created exactly NUM_ZOBRIST_FIELDS elements above
807        let boxed: Box<[[u64; 256]; NUM_ZOBRIST_FIELDS]> = table
808            .into_boxed_slice()
809            .try_into()
810            .expect("Vec has exact NUM_ZOBRIST_FIELDS elements");
811
812        Self { table: boxed }
813    }
814
815    /// Compute Zobrist hash for byte slice (O(N) - used once per state).
816    #[must_use]
817    pub fn hash_bytes(&self, bytes: &[u8]) -> u64 {
818        let mut hash = 0u64;
819        for (i, &byte) in bytes.iter().enumerate() {
820            hash ^= self.table[i % NUM_ZOBRIST_FIELDS][byte as usize];
821        }
822        hash
823    }
824
825    /// Update hash incrementally when single byte changes (O(1)).
826    ///
827    /// This is the key optimization - no need to rehash entire state.
828    #[inline]
829    #[must_use]
830    #[allow(clippy::missing_const_for_fn)] // Indexing not const-stable
831    pub fn update_hash(&self, hash: u64, field: usize, old_byte: u8, new_byte: u8) -> u64 {
832        // XOR out old value, XOR in new value (Tridgell 1999)
833        hash ^ self.table[field % NUM_ZOBRIST_FIELDS][old_byte as usize]
834            ^ self.table[field % NUM_ZOBRIST_FIELDS][new_byte as usize]
835    }
836
837    /// Calculate entropy as Hamming distance between two hashes.
838    ///
839    /// Returns number of differing bits (0-64).
840    #[inline]
841    #[must_use]
842    pub const fn entropy(hash1: u64, hash2: u64) -> u32 {
843        (hash1 ^ hash2).count_ones()
844    }
845}
846
847impl Default for ZobristTable {
848    fn default() -> Self {
849        Self::new(0xDEAD_BEEF_CAFE_BABE)
850    }
851}
852
853/// Adaptive snapshotter using Zobrist hashing for O(1) entropy detection.
854///
855/// This is an optimized version of [`AdaptiveSnapshotter`] that uses
856/// Zobrist hashing instead of SHA-256 for entropy calculation.
857///
858/// # Performance
859///
860/// - Original (SHA-256): O(N) per frame where N = state size
861/// - Zobrist: O(1) per state change, O(N) only for full hash
862///
863/// Per spec v1.3 TPS Kaizen: "SHA-256 entropy check is O(N) and creates Muri"
864#[derive(Debug, Clone)]
865pub struct ZobristSnapshotter {
866    /// Zobrist hash table.
867    table: ZobristTable,
868    /// Current state hash (incrementally updated).
869    current_hash: u64,
870    /// Previous snapshot hash.
871    prev_snapshot_hash: u64,
872    /// Entropy threshold (Hamming distance between hashes).
873    entropy_threshold: u32,
874    /// Minimum frames between snapshots.
875    min_interval: u64,
876    /// Maximum frames between snapshots.
877    max_interval: u64,
878    /// Last snapshot frame.
879    last_snapshot_frame: u64,
880}
881
882impl ZobristSnapshotter {
883    /// Create with custom parameters.
884    #[must_use]
885    pub fn new(seed: u64, min_interval: u64, max_interval: u64, entropy_threshold: u32) -> Self {
886        Self {
887            table: ZobristTable::new(seed),
888            current_hash: 0,
889            prev_snapshot_hash: 0,
890            entropy_threshold,
891            min_interval,
892            max_interval,
893            last_snapshot_frame: 0,
894        }
895    }
896
897    /// Initialize hash from full state (O(N) - call once at start).
898    pub fn initialize(&mut self, state_bytes: &[u8]) {
899        self.current_hash = self.table.hash_bytes(state_bytes);
900        self.prev_snapshot_hash = self.current_hash;
901    }
902
903    /// Update hash incrementally when state changes (O(1)).
904    #[inline]
905    pub fn on_state_change(&mut self, field: usize, old_byte: u8, new_byte: u8) {
906        self.current_hash = self
907            .table
908            .update_hash(self.current_hash, field, old_byte, new_byte);
909    }
910
911    /// Check if snapshot should be taken (O(1) operation!).
912    pub fn should_snapshot(&mut self, frame: u64) -> SnapshotDecision {
913        let frames_since = frame.saturating_sub(self.last_snapshot_frame);
914
915        // Force snapshot at max interval
916        if frames_since >= self.max_interval {
917            self.take_snapshot(frame);
918            return SnapshotDecision::FullSnapshot;
919        }
920
921        // Calculate entropy as Hamming distance between hashes (O(1))
922        let entropy = ZobristTable::entropy(self.current_hash, self.prev_snapshot_hash);
923
924        // High entropy = state changed significantly
925        if entropy >= self.entropy_threshold && frames_since >= self.min_interval {
926            self.take_snapshot(frame);
927            return SnapshotDecision::DeltaSnapshot;
928        }
929
930        SnapshotDecision::Skip
931    }
932
933    /// Record that a snapshot was taken.
934    #[allow(clippy::missing_const_for_fn)] // const fn with mut ref not stable
935    fn take_snapshot(&mut self, frame: u64) {
936        self.prev_snapshot_hash = self.current_hash;
937        self.last_snapshot_frame = frame;
938    }
939
940    /// Get current hash value.
941    #[must_use]
942    pub const fn current_hash(&self) -> u64 {
943        self.current_hash
944    }
945
946    /// Get last snapshot frame.
947    #[must_use]
948    pub const fn last_snapshot_frame(&self) -> u64 {
949        self.last_snapshot_frame
950    }
951}
952
953impl Default for ZobristSnapshotter {
954    fn default() -> Self {
955        Self::new(
956            0xDEAD_BEEF_CAFE_BABE,
957            15,  // min_interval
958            120, // max_interval
959            16,  // entropy_threshold (bits changed)
960        )
961    }
962}
963
964// =============================================================================
965// SnapshotDecision: Adaptive Snapshot Scheduling (Heijunka)
966// =============================================================================
967
968/// Snapshot decision based on state entropy.
969///
970/// Per Elnozahy (2002): "Adaptive checkpointing based on state mutation rates
971/// significantly reduces log sizes."
972#[derive(Debug, Clone, Copy, PartialEq, Eq)]
973pub enum SnapshotDecision {
974    /// No snapshot needed.
975    Skip,
976    /// Take delta snapshot (high entropy detected).
977    DeltaSnapshot,
978    /// Take full snapshot (max interval reached).
979    FullSnapshot,
980}
981
982// =============================================================================
983// AdaptiveSnapshotter: Entropy-Based Scheduling (Heijunka/Mura)
984// =============================================================================
985
986/// Adaptive snapshot scheduler based on state entropy.
987///
988/// Fixed-interval snapshots create Mura (unevenness):
989/// - Menu screens waste space with identical snapshots
990/// - High-action moments lack granularity
991///
992/// Solution: Snapshot based on state entropy (magnitude of change).
993#[derive(Debug, Clone)]
994pub struct AdaptiveSnapshotter {
995    /// Minimum frames between snapshots.
996    pub min_interval: u64,
997    /// Maximum frames between snapshots (force snapshot).
998    pub max_interval: u64,
999    /// Entropy threshold to trigger snapshot.
1000    pub entropy_threshold: u32,
1001    /// Last snapshot frame.
1002    last_snapshot_frame: u64,
1003    /// Previous state hash for delta calculation.
1004    prev_state_hash: [u8; 32],
1005}
1006
1007impl Default for AdaptiveSnapshotter {
1008    fn default() -> Self {
1009        Self {
1010            min_interval: 15,      // At least 15 frames (~250ms at 60fps)
1011            max_interval: 120,     // Force snapshot every 120 frames (~2 sec)
1012            entropy_threshold: 64, // ~25% of bits changed
1013            last_snapshot_frame: 0,
1014            prev_state_hash: [0; 32],
1015        }
1016    }
1017}
1018
1019impl AdaptiveSnapshotter {
1020    /// Create with custom parameters.
1021    #[must_use]
1022    pub const fn new(min_interval: u64, max_interval: u64, entropy_threshold: u32) -> Self {
1023        Self {
1024            min_interval,
1025            max_interval,
1026            entropy_threshold,
1027            last_snapshot_frame: 0,
1028            prev_state_hash: [0; 32],
1029        }
1030    }
1031
1032    /// Determine if a snapshot should be taken this frame.
1033    pub fn should_snapshot(&mut self, frame: u64, state_hash: &[u8; 32]) -> SnapshotDecision {
1034        let frames_since = frame.saturating_sub(self.last_snapshot_frame);
1035
1036        // Force snapshot at max interval
1037        if frames_since >= self.max_interval {
1038            self.prev_state_hash = *state_hash;
1039            self.last_snapshot_frame = frame;
1040            return SnapshotDecision::FullSnapshot;
1041        }
1042
1043        // Calculate state entropy (Hamming distance between hashes)
1044        let entropy = self.calculate_entropy(state_hash);
1045
1046        // High entropy = take snapshot if past min interval
1047        if entropy >= self.entropy_threshold && frames_since >= self.min_interval {
1048            self.prev_state_hash = *state_hash;
1049            self.last_snapshot_frame = frame;
1050            return SnapshotDecision::DeltaSnapshot;
1051        }
1052
1053        SnapshotDecision::Skip
1054    }
1055
1056    /// Calculate entropy as Hamming distance between state hashes.
1057    fn calculate_entropy(&self, current: &[u8; 32]) -> u32 {
1058        self.prev_state_hash
1059            .iter()
1060            .zip(current.iter())
1061            .map(|(a, b)| (a ^ b).count_ones())
1062            .sum()
1063    }
1064
1065    /// Reset the snapshotter state.
1066    pub const fn reset(&mut self) {
1067        self.last_snapshot_frame = 0;
1068        self.prev_state_hash = [0; 32];
1069    }
1070
1071    /// Get the frame of the last snapshot.
1072    #[must_use]
1073    pub const fn last_snapshot_frame(&self) -> u64 {
1074        self.last_snapshot_frame
1075    }
1076}
1077
1078// =============================================================================
1079// TraceBuffer: Ring Buffer with Andon Cord (Jidoka)
1080// =============================================================================
1081
1082/// Trace error types.
1083#[derive(Debug, Clone, PartialEq, Eq)]
1084pub enum TraceError {
1085    /// Buffer is full (only in strict mode without Andon Cord).
1086    BufferFull,
1087    /// Invalid frame sequence.
1088    InvalidFrameSequence,
1089    /// Serialization error.
1090    SerializationError(String),
1091}
1092
1093impl core::fmt::Display for TraceError {
1094    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1095        match self {
1096            Self::BufferFull => write!(f, "Trace buffer is full"),
1097            Self::InvalidFrameSequence => write!(f, "Invalid frame sequence"),
1098            Self::SerializationError(e) => write!(f, "Serialization error: {e}"),
1099        }
1100    }
1101}
1102
1103impl core::error::Error for TraceError {}
1104
1105/// Ring buffer for trace events with Andon Cord policy.
1106///
1107/// Per Dunlap (2002): "A trace with missing input events is worse than no trace at all."
1108#[derive(Debug)]
1109pub struct TraceBuffer {
1110    /// Frame records.
1111    frames: Vec<Option<FrameRecord>>,
1112    /// Buffer capacity.
1113    capacity: usize,
1114    /// Write position.
1115    write_pos: usize,
1116    /// Read position.
1117    read_pos: usize,
1118    /// Number of elements in buffer.
1119    len: usize,
1120    /// Buffer policy.
1121    policy: BufferPolicy,
1122    /// Total frames dropped (only in DropOldest/SoftAndon mode).
1123    frames_dropped: u64,
1124    /// v1.3: Current Andon state for Soft Andon visual feedback.
1125    andon_state: AndonState,
1126}
1127
1128impl TraceBuffer {
1129    /// Create a new trace buffer with specified capacity and policy.
1130    #[must_use]
1131    pub fn new(capacity: usize, policy: BufferPolicy) -> Self {
1132        let mut frames = Vec::with_capacity(capacity);
1133        frames.resize_with(capacity, || None);
1134        Self {
1135            frames,
1136            capacity,
1137            write_pos: 0,
1138            read_pos: 0,
1139            len: 0,
1140            policy,
1141            frames_dropped: 0,
1142            andon_state: AndonState::Normal,
1143        }
1144    }
1145
1146    /// Create a Soft Andon buffer (visual indicator on overflow).
1147    #[must_use]
1148    pub fn soft_andon(capacity: usize) -> Self {
1149        Self::new(capacity, BufferPolicy::SoftAndon)
1150    }
1151
1152    /// Create a debug buffer (Andon Cord enabled).
1153    #[must_use]
1154    pub fn debug(capacity: usize) -> Self {
1155        Self::new(capacity, BufferPolicy::AndonCord)
1156    }
1157
1158    /// Create a production buffer (drop oldest on overflow).
1159    #[must_use]
1160    pub fn production(capacity: usize) -> Self {
1161        Self::new(capacity, BufferPolicy::DropOldest)
1162    }
1163
1164    /// Push a frame record to the buffer.
1165    ///
1166    /// In `AndonCord` mode, this will return `Err(BufferFull)` if the buffer is full.
1167    /// In `DropOldest` mode, this will drop the oldest frame to make room.
1168    /// In `SoftAndon` mode, this will drop oldest but update [`AndonState`] for visual feedback.
1169    ///
1170    /// # Errors
1171    ///
1172    /// Returns [`TraceError::BufferFull`] if the buffer is full and using `AndonCord` policy.
1173    pub fn push(&mut self, record: FrameRecord) -> Result<(), TraceError> {
1174        // Update Andon state based on buffer fill level (v1.3)
1175        self.update_andon_state();
1176
1177        if self.len >= self.capacity {
1178            match self.policy {
1179                BufferPolicy::DropOldest => {
1180                    // Drop oldest silently
1181                    self.read_pos = (self.read_pos + 1) % self.capacity;
1182                    self.len -= 1;
1183                    self.frames_dropped += 1;
1184                }
1185                BufferPolicy::SoftAndon => {
1186                    // v1.3: Drop oldest but update visual state
1187                    self.read_pos = (self.read_pos + 1) % self.capacity;
1188                    self.len -= 1;
1189                    self.frames_dropped += 1;
1190                    self.andon_state = AndonState::TraceLoss {
1191                        dropped_count: self.frames_dropped,
1192                    };
1193                }
1194                BufferPolicy::AndonCord => {
1195                    // STOP THE LINE: Return error (caller should block)
1196                    return Err(TraceError::BufferFull);
1197                }
1198            }
1199        }
1200
1201        self.frames[self.write_pos] = Some(record);
1202        self.write_pos = (self.write_pos + 1) % self.capacity;
1203        self.len += 1;
1204        Ok(())
1205    }
1206
1207    /// Update Andon state based on buffer fill level (v1.3).
1208    fn update_andon_state(&mut self) {
1209        if self.policy != BufferPolicy::SoftAndon {
1210            return;
1211        }
1212
1213        let fill_pct = (self.len * 100) / self.capacity.max(1);
1214
1215        self.andon_state = if self.frames_dropped > 0 {
1216            AndonState::TraceLoss {
1217                dropped_count: self.frames_dropped,
1218            }
1219        } else if fill_pct >= 80 {
1220            AndonState::Warning {
1221                buffer_pct: fill_pct as u8,
1222            }
1223        } else {
1224            AndonState::Normal
1225        };
1226    }
1227
1228    /// Get current Andon state for visual feedback (v1.3).
1229    #[must_use]
1230    pub const fn andon_state(&self) -> AndonState {
1231        self.andon_state
1232    }
1233
1234    /// Try to push without blocking.
1235    ///
1236    /// # Errors
1237    ///
1238    /// Returns [`TraceError::BufferFull`] if the buffer is full and using `AndonCord` policy.
1239    pub fn try_push(&mut self, record: FrameRecord) -> Result<(), TraceError> {
1240        // Only AndonCord returns error on full - DropOldest and SoftAndon drop oldest
1241        if self.len >= self.capacity && self.policy == BufferPolicy::AndonCord {
1242            return Err(TraceError::BufferFull);
1243        }
1244        self.push(record)
1245    }
1246
1247    /// Pop the oldest frame record from the buffer.
1248    pub fn pop(&mut self) -> Option<FrameRecord> {
1249        if self.len == 0 {
1250            return None;
1251        }
1252
1253        let record = self.frames[self.read_pos].take();
1254        self.read_pos = (self.read_pos + 1) % self.capacity;
1255        self.len -= 1;
1256        record
1257    }
1258
1259    /// Drain up to `count` frames from the buffer.
1260    pub fn drain(&mut self, count: usize) -> Vec<FrameRecord> {
1261        let to_drain = count.min(self.len);
1262        let mut result = Vec::with_capacity(to_drain);
1263
1264        for _ in 0..to_drain {
1265            if let Some(record) = self.pop() {
1266                result.push(record);
1267            }
1268        }
1269
1270        result
1271    }
1272
1273    /// Get current buffer length.
1274    #[must_use]
1275    pub const fn len(&self) -> usize {
1276        self.len
1277    }
1278
1279    /// Check if buffer is empty.
1280    #[must_use]
1281    pub const fn is_empty(&self) -> bool {
1282        self.len == 0
1283    }
1284
1285    /// Check if buffer is full.
1286    #[must_use]
1287    pub const fn is_full(&self) -> bool {
1288        self.len >= self.capacity
1289    }
1290
1291    /// Get buffer capacity.
1292    #[must_use]
1293    pub const fn capacity(&self) -> usize {
1294        self.capacity
1295    }
1296
1297    /// Get number of dropped frames (only relevant in DropOldest mode).
1298    #[must_use]
1299    pub const fn frames_dropped(&self) -> u64 {
1300        self.frames_dropped
1301    }
1302
1303    /// Get buffer policy.
1304    #[must_use]
1305    pub const fn policy(&self) -> BufferPolicy {
1306        self.policy
1307    }
1308
1309    /// Clear the buffer.
1310    pub fn clear(&mut self) {
1311        for frame in &mut self.frames {
1312            *frame = None;
1313        }
1314        self.write_pos = 0;
1315        self.read_pos = 0;
1316        self.len = 0;
1317    }
1318
1319    /// Iterate over all frames in the buffer (oldest to newest).
1320    pub fn iter(&self) -> impl Iterator<Item = &FrameRecord> {
1321        let capacity = self.capacity;
1322        let read_pos = self.read_pos;
1323        let len = self.len;
1324
1325        (0..len).filter_map(move |i| {
1326            let idx = (read_pos + i) % capacity;
1327            self.frames[idx].as_ref()
1328        })
1329    }
1330}
1331
1332// =============================================================================
1333// TraceQuery: Query-Based Debugging (Genchi Genbutsu)
1334// =============================================================================
1335
1336/// Query result containing frame and optional context.
1337#[derive(Debug, Clone)]
1338pub struct QueryResult {
1339    /// The frame number that matched.
1340    pub frame: u64,
1341    /// Context frames before (if requested).
1342    pub context_before: Vec<FrameRecord>,
1343    /// Context frames after (if requested).
1344    pub context_after: Vec<FrameRecord>,
1345}
1346
1347/// Query-based trace analysis per Ko & Myers (2008).
1348///
1349/// Enables "Whyline-style" queries like:
1350/// - "Find all frames where condition X is true"
1351/// - "Why did the ball miss the paddle at frame N?"
1352///
1353/// # Example
1354///
1355/// ```ignore
1356/// let query = TraceQuery::from_buffer(&buffer);
1357/// let frames_with_input = query.find_frames(|f| f.has_inputs());
1358/// ```
1359#[derive(Debug)]
1360pub struct TraceQuery<'a> {
1361    frames: Vec<&'a FrameRecord>,
1362}
1363
1364impl<'a> TraceQuery<'a> {
1365    /// Create a query interface from a trace buffer.
1366    #[must_use]
1367    pub fn from_buffer(buffer: &'a TraceBuffer) -> Self {
1368        Self {
1369            frames: buffer.iter().collect(),
1370        }
1371    }
1372
1373    /// Create a query interface from a slice of frame records.
1374    #[must_use]
1375    pub const fn from_frames(frames: Vec<&'a FrameRecord>) -> Self {
1376        Self { frames }
1377    }
1378
1379    /// Find all frames matching a predicate.
1380    pub fn find_frames(&self, predicate: impl Fn(&FrameRecord) -> bool) -> Vec<u64> {
1381        self.frames
1382            .iter()
1383            .filter(|f| predicate(f))
1384            .map(|f| f.frame)
1385            .collect()
1386    }
1387
1388    /// Find frames with context (frames before and after).
1389    pub fn find_frames_with_context(
1390        &self,
1391        predicate: impl Fn(&FrameRecord) -> bool,
1392        context_frames: usize,
1393    ) -> Vec<QueryResult> {
1394        let mut results = Vec::new();
1395
1396        for (idx, frame) in self.frames.iter().enumerate() {
1397            if predicate(frame) {
1398                let context_before: Vec<_> = self.frames[idx.saturating_sub(context_frames)..idx]
1399                    .iter()
1400                    .map(|f| (*f).clone())
1401                    .collect();
1402
1403                let context_after: Vec<_> = self.frames
1404                    [idx + 1..(idx + 1 + context_frames).min(self.frames.len())]
1405                    .iter()
1406                    .map(|f| (*f).clone())
1407                    .collect();
1408
1409                results.push(QueryResult {
1410                    frame: frame.frame,
1411                    context_before,
1412                    context_after,
1413                });
1414            }
1415        }
1416
1417        results
1418    }
1419
1420    /// Count frames matching a predicate.
1421    pub fn count_frames(&self, predicate: impl Fn(&FrameRecord) -> bool) -> usize {
1422        self.frames.iter().filter(|f| predicate(f)).count()
1423    }
1424
1425    /// Find frames with any input events.
1426    #[must_use]
1427    pub fn frames_with_inputs(&self) -> Vec<u64> {
1428        self.find_frames(FrameRecord::has_inputs)
1429    }
1430
1431    /// Find frames with specific input event type.
1432    pub fn frames_with_input_type(
1433        &self,
1434        event_type_matcher: impl Fn(&InputEventType) -> bool,
1435    ) -> Vec<u64> {
1436        self.find_frames(|f| {
1437            f.inputs
1438                .iter()
1439                .any(|input| event_type_matcher(&input.event_type))
1440        })
1441    }
1442
1443    /// Get frame at specific frame number.
1444    #[must_use]
1445    pub fn get_frame(&self, frame_number: u64) -> Option<&FrameRecord> {
1446        self.frames
1447            .iter()
1448            .find(|f| f.frame == frame_number)
1449            .copied()
1450    }
1451
1452    /// Get frame range (inclusive).
1453    #[must_use]
1454    pub fn get_frame_range(&self, start: u64, end: u64) -> Vec<&FrameRecord> {
1455        self.frames
1456            .iter()
1457            .filter(|f| f.frame >= start && f.frame <= end)
1458            .copied()
1459            .collect()
1460    }
1461
1462    /// Get total frame count.
1463    #[must_use]
1464    #[allow(clippy::missing_const_for_fn)] // Vec::len is not const
1465    pub fn frame_count(&self) -> usize {
1466        self.frames.len()
1467    }
1468
1469    /// Get first frame number (if any).
1470    #[must_use]
1471    pub fn first_frame(&self) -> Option<u64> {
1472        self.frames.first().map(|f| f.frame)
1473    }
1474
1475    /// Get last frame number (if any).
1476    #[must_use]
1477    pub fn last_frame(&self) -> Option<u64> {
1478        self.frames.last().map(|f| f.frame)
1479    }
1480
1481    /// Calculate input density (inputs per frame).
1482    #[must_use]
1483    pub fn input_density(&self) -> f64 {
1484        if self.frames.is_empty() {
1485            return 0.0;
1486        }
1487        let total_inputs: usize = self.frames.iter().map(|f| f.inputs.len()).sum();
1488        total_inputs as f64 / self.frames.len() as f64
1489    }
1490}
1491
1492// =============================================================================
1493// GameTracer: High-Level Game Tracing API
1494// =============================================================================
1495
1496/// Trace statistics for debugging display.
1497#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1498pub struct TraceStats {
1499    /// Current frame number.
1500    pub frame: u64,
1501    /// Number of frames in buffer.
1502    pub buffer_len: usize,
1503    /// Buffer capacity.
1504    pub buffer_capacity: usize,
1505    /// Total frames dropped.
1506    pub frames_dropped: u64,
1507    /// Total input events recorded.
1508    pub total_inputs: u64,
1509    /// Number of snapshots taken.
1510    pub snapshots_taken: u64,
1511    /// Whether recording is active.
1512    pub recording: bool,
1513    /// Buffer policy name.
1514    pub policy: String,
1515}
1516
1517/// Configuration for the game tracer.
1518#[derive(Debug, Clone)]
1519pub struct TracerConfig {
1520    /// Buffer capacity (frames).
1521    pub buffer_capacity: usize,
1522    /// Buffer policy.
1523    pub policy: BufferPolicy,
1524    /// Whether to record state hashes.
1525    pub record_state_hashes: bool,
1526    /// Snapshotter configuration.
1527    pub snapshotter: AdaptiveSnapshotter,
1528}
1529
1530impl Default for TracerConfig {
1531    fn default() -> Self {
1532        Self {
1533            buffer_capacity: 3600, // ~60 seconds at 60fps
1534            policy: BufferPolicy::DropOldest,
1535            record_state_hashes: true,
1536            snapshotter: AdaptiveSnapshotter::default(),
1537        }
1538    }
1539}
1540
1541impl TracerConfig {
1542    /// Create a debug configuration (Andon Cord enabled).
1543    #[must_use]
1544    pub fn debug() -> Self {
1545        Self {
1546            buffer_capacity: 3600,
1547            policy: BufferPolicy::AndonCord,
1548            record_state_hashes: true,
1549            snapshotter: AdaptiveSnapshotter::default(),
1550        }
1551    }
1552
1553    /// Create a production configuration (drop oldest on overflow).
1554    #[must_use]
1555    pub const fn production() -> Self {
1556        Self {
1557            buffer_capacity: 7200, // ~2 minutes at 60fps
1558            policy: BufferPolicy::DropOldest,
1559            record_state_hashes: false, // Disable hashes for performance
1560            snapshotter: AdaptiveSnapshotter::new(30, 300, 64), // Less frequent snapshots
1561        }
1562    }
1563}
1564
1565/// High-level game tracer that integrates all tracing components.
1566///
1567/// Provides a simple API for recording and querying game traces:
1568/// - `begin_frame()`: Start recording a new frame
1569/// - `record_input()`: Record an input event
1570/// - `end_frame()`: Finish the frame and optionally take a snapshot
1571/// - `stats()`: Get trace statistics for debugging
1572///
1573/// # Example
1574///
1575/// ```
1576/// use jugar_web::trace::{GameTracer, TracerConfig, InputEvent, InputEventType};
1577///
1578/// let mut tracer = GameTracer::new(TracerConfig::default());
1579///
1580/// // Each frame:
1581/// tracer.begin_frame();
1582/// tracer.record_input(InputEvent {
1583///     event_type: InputEventType::KeyDown(32), // Space
1584///     frame_offset_us: 0,
1585/// });
1586/// tracer.end_frame(None); // Or provide state hash
1587/// ```
1588#[derive(Debug)]
1589pub struct GameTracer {
1590    /// Trace buffer.
1591    buffer: TraceBuffer,
1592    /// Adaptive snapshotter.
1593    snapshotter: AdaptiveSnapshotter,
1594    /// Current frame number.
1595    current_frame: u64,
1596    /// Current frame record (being built).
1597    current_record: Option<FrameRecord>,
1598    /// Whether to record state hashes.
1599    record_state_hashes: bool,
1600    /// Whether tracing is active.
1601    recording: bool,
1602    /// Total input events recorded.
1603    total_inputs: u64,
1604    /// Total snapshots taken.
1605    snapshots_taken: u64,
1606}
1607
1608impl GameTracer {
1609    /// Create a new game tracer with the given configuration.
1610    #[must_use]
1611    pub fn new(config: TracerConfig) -> Self {
1612        Self {
1613            buffer: TraceBuffer::new(config.buffer_capacity, config.policy),
1614            snapshotter: config.snapshotter,
1615            current_frame: 0,
1616            current_record: None,
1617            record_state_hashes: config.record_state_hashes,
1618            recording: true,
1619            total_inputs: 0,
1620            snapshots_taken: 0,
1621        }
1622    }
1623
1624    /// Create a debug tracer (Andon Cord enabled).
1625    #[must_use]
1626    pub fn debug() -> Self {
1627        Self::new(TracerConfig::debug())
1628    }
1629
1630    /// Create a production tracer (drop oldest on overflow).
1631    #[must_use]
1632    pub fn production() -> Self {
1633        Self::new(TracerConfig::production())
1634    }
1635
1636    /// Start recording a new frame.
1637    ///
1638    /// Must be called at the start of each frame before recording inputs.
1639    pub fn begin_frame(&mut self) {
1640        if !self.recording {
1641            return;
1642        }
1643        self.current_record = Some(FrameRecord::new(self.current_frame));
1644    }
1645
1646    /// Record an input event for the current frame.
1647    ///
1648    /// # Panics
1649    ///
1650    /// Panics in debug builds if `begin_frame()` was not called.
1651    pub fn record_input(&mut self, event: InputEvent) {
1652        if !self.recording {
1653            return;
1654        }
1655        if let Some(ref mut record) = self.current_record {
1656            record.add_input(event);
1657            self.total_inputs += 1;
1658        } else {
1659            debug_assert!(false, "record_input called without begin_frame");
1660        }
1661    }
1662
1663    /// Record multiple input events for the current frame.
1664    pub fn record_inputs(&mut self, events: impl IntoIterator<Item = InputEvent>) {
1665        if !self.recording {
1666            return;
1667        }
1668        for event in events {
1669            self.record_input(event);
1670        }
1671    }
1672
1673    /// End the current frame and commit it to the trace buffer.
1674    ///
1675    /// # Arguments
1676    ///
1677    /// * `state_hash` - Optional state hash for verification and snapshot decisions.
1678    ///
1679    /// # Returns
1680    ///
1681    /// The snapshot decision (Skip, DeltaSnapshot, or FullSnapshot).
1682    pub fn end_frame(&mut self, state_hash: Option<[u8; 32]>) -> SnapshotDecision {
1683        if !self.recording {
1684            return SnapshotDecision::Skip;
1685        }
1686
1687        let Some(mut record) = self.current_record.take() else {
1688            debug_assert!(false, "end_frame called without begin_frame");
1689            return SnapshotDecision::Skip;
1690        };
1691
1692        // Determine if we should snapshot
1693        let decision = if self.record_state_hashes {
1694            if let Some(hash) = state_hash {
1695                let decision = self.snapshotter.should_snapshot(self.current_frame, &hash);
1696                if decision != SnapshotDecision::Skip {
1697                    record.set_state_hash(hash);
1698                    self.snapshots_taken += 1;
1699                }
1700                decision
1701            } else {
1702                SnapshotDecision::Skip
1703            }
1704        } else {
1705            SnapshotDecision::Skip
1706        };
1707
1708        // Push to buffer (ignore errors in production mode)
1709        let _ = self.buffer.push(record);
1710
1711        self.current_frame += 1;
1712        decision
1713    }
1714
1715    /// Get trace statistics for debugging display.
1716    #[must_use]
1717    pub fn stats(&self) -> TraceStats {
1718        TraceStats {
1719            frame: self.current_frame,
1720            buffer_len: self.buffer.len(),
1721            buffer_capacity: self.buffer.capacity(),
1722            frames_dropped: self.buffer.frames_dropped(),
1723            total_inputs: self.total_inputs,
1724            snapshots_taken: self.snapshots_taken,
1725            recording: self.recording,
1726            policy: match self.buffer.policy() {
1727                BufferPolicy::DropOldest => "DropOldest".to_string(),
1728                BufferPolicy::SoftAndon => "SoftAndon".to_string(),
1729                BufferPolicy::AndonCord => "AndonCord".to_string(),
1730            },
1731        }
1732    }
1733
1734    /// Start recording.
1735    #[allow(clippy::missing_const_for_fn)] // const fn with mut ref not stable
1736    pub fn start_recording(&mut self) {
1737        self.recording = true;
1738    }
1739
1740    /// Stop recording.
1741    pub fn stop_recording(&mut self) {
1742        self.recording = false;
1743        self.current_record = None;
1744    }
1745
1746    /// Check if recording is active.
1747    #[must_use]
1748    pub const fn is_recording(&self) -> bool {
1749        self.recording
1750    }
1751
1752    /// Get current frame number.
1753    #[must_use]
1754    pub const fn current_frame(&self) -> u64 {
1755        self.current_frame
1756    }
1757
1758    /// Get a reference to the underlying buffer.
1759    #[must_use]
1760    pub const fn buffer(&self) -> &TraceBuffer {
1761        &self.buffer
1762    }
1763
1764    /// Create a query interface for the trace buffer.
1765    #[must_use]
1766    pub fn query(&self) -> TraceQuery<'_> {
1767        TraceQuery::from_buffer(&self.buffer)
1768    }
1769
1770    /// Clear the trace buffer and reset statistics.
1771    pub fn clear(&mut self) {
1772        self.buffer.clear();
1773        self.snapshotter.reset();
1774        self.current_frame = 0;
1775        self.current_record = None;
1776        self.total_inputs = 0;
1777        self.snapshots_taken = 0;
1778    }
1779
1780    /// Drain frames from the buffer for export.
1781    pub fn drain(&mut self, count: usize) -> Vec<FrameRecord> {
1782        self.buffer.drain(count)
1783    }
1784
1785    /// Export all frames as JSON.
1786    ///
1787    /// # Errors
1788    ///
1789    /// Returns a serialization error if the frames cannot be serialized.
1790    pub fn export_json(&self) -> Result<String, TraceError> {
1791        let frames: Vec<_> = self.buffer.iter().cloned().collect();
1792        serde_json::to_string(&frames).map_err(|e| TraceError::SerializationError(e.to_string()))
1793    }
1794}
1795
1796// =============================================================================
1797// Tests
1798// =============================================================================
1799
1800#[cfg(test)]
1801#[allow(
1802    clippy::many_single_char_names,
1803    clippy::unreadable_literal,
1804    clippy::float_cmp,
1805    clippy::unwrap_used,
1806    clippy::panic,
1807    clippy::field_reassign_with_default,
1808    clippy::overly_complex_bool_expr,
1809    clippy::approx_constant,
1810    clippy::default_trait_access,
1811    clippy::redundant_closure_for_method_calls
1812)]
1813mod tests {
1814    use super::*;
1815
1816    // =========================================================================
1817    // Fixed32 Tests (100% coverage target)
1818    // =========================================================================
1819
1820    #[test]
1821    fn test_fixed32_zero() {
1822        assert_eq!(Fixed32::ZERO.to_raw(), 0);
1823        assert_eq!(Fixed32::ZERO.to_int(), 0);
1824        assert!(Fixed32::ZERO.is_zero());
1825    }
1826
1827    #[test]
1828    fn test_fixed32_one() {
1829        assert_eq!(Fixed32::ONE.to_raw(), 65536);
1830        assert_eq!(Fixed32::ONE.to_int(), 1);
1831        assert!(!Fixed32::ONE.is_zero());
1832    }
1833
1834    #[test]
1835    fn test_fixed32_half() {
1836        assert_eq!(Fixed32::HALF.to_raw(), 32768);
1837        assert_eq!(Fixed32::HALF.to_int(), 0); // Truncates
1838        let approx = Fixed32::HALF.to_f32();
1839        assert!((approx - 0.5).abs() < 0.0001);
1840    }
1841
1842    #[test]
1843    fn test_fixed32_from_int() {
1844        assert_eq!(Fixed32::from_int(5).to_int(), 5);
1845        assert_eq!(Fixed32::from_int(-3).to_int(), -3);
1846        assert_eq!(Fixed32::from_int(0).to_int(), 0);
1847        assert_eq!(Fixed32::from_int(1000).to_int(), 1000);
1848        assert_eq!(Fixed32::from_int(-1000).to_int(), -1000);
1849    }
1850
1851    #[test]
1852    fn test_fixed32_from_f32() {
1853        let f = Fixed32::from_f32(2.5);
1854        assert_eq!(f.to_int(), 2);
1855        let approx = f.to_f32();
1856        assert!((approx - 2.5).abs() < 0.0001);
1857
1858        // Note: to_int() truncates towards zero for positives, but negative
1859        // values truncate towards negative infinity due to arithmetic right shift
1860        let neg = Fixed32::from_f32(-1.25);
1861        assert_eq!(neg.to_int(), -2); // -1.25 truncates to -2 with right shift
1862        let neg_approx = neg.to_f32();
1863        assert!((neg_approx - (-1.25)).abs() < 0.0001);
1864    }
1865
1866    #[test]
1867    fn test_fixed32_addition() {
1868        let a = Fixed32::from_int(5);
1869        let b = Fixed32::from_int(3);
1870        assert_eq!((a + b).to_int(), 8);
1871
1872        let c = Fixed32::from_f32(1.5);
1873        let d = Fixed32::from_f32(2.5);
1874        let sum = c + d;
1875        assert!((sum.to_f32() - 4.0).abs() < 0.0001);
1876    }
1877
1878    #[test]
1879    fn test_fixed32_subtraction() {
1880        let a = Fixed32::from_int(10);
1881        let b = Fixed32::from_int(3);
1882        assert_eq!((a - b).to_int(), 7);
1883
1884        let c = Fixed32::from_int(3);
1885        let d = Fixed32::from_int(10);
1886        assert_eq!((c - d).to_int(), -7);
1887    }
1888
1889    #[test]
1890    fn test_fixed32_multiplication() {
1891        let a = Fixed32::from_int(5);
1892        let b = Fixed32::from_int(3);
1893        assert_eq!((a * b).to_int(), 15);
1894
1895        let c = Fixed32::from_f32(2.5);
1896        let d = Fixed32::from_f32(4.0);
1897        let product = c * d;
1898        assert!((product.to_f32() - 10.0).abs() < 0.001);
1899
1900        // Negative multiplication
1901        let neg = Fixed32::from_int(-5);
1902        assert_eq!((neg * b).to_int(), -15);
1903    }
1904
1905    #[test]
1906    fn test_fixed32_division() {
1907        let a = Fixed32::from_int(15);
1908        let b = Fixed32::from_int(3);
1909        assert_eq!((a / b).to_int(), 5);
1910
1911        let c = Fixed32::from_int(10);
1912        let d = Fixed32::from_int(4);
1913        let result = c / d;
1914        assert!((result.to_f32() - 2.5).abs() < 0.001);
1915
1916        // Negative division
1917        let neg = Fixed32::from_int(-15);
1918        assert_eq!((neg / b).to_int(), -5);
1919    }
1920
1921    #[test]
1922    fn test_fixed32_checked_div() {
1923        let a = Fixed32::from_int(10);
1924        let b = Fixed32::from_int(2);
1925        assert_eq!(a.checked_div(b), Some(Fixed32::from_int(5)));
1926
1927        let zero = Fixed32::ZERO;
1928        assert_eq!(a.checked_div(zero), None);
1929    }
1930
1931    #[test]
1932    fn test_fixed32_negation() {
1933        let a = Fixed32::from_int(5);
1934        assert_eq!((-a).to_int(), -5);
1935
1936        let b = Fixed32::from_int(-3);
1937        assert_eq!((-b).to_int(), 3);
1938    }
1939
1940    #[test]
1941    fn test_fixed32_abs() {
1942        assert_eq!(Fixed32::from_int(5).abs().to_int(), 5);
1943        assert_eq!(Fixed32::from_int(-5).abs().to_int(), 5);
1944        assert_eq!(Fixed32::ZERO.abs().to_int(), 0);
1945    }
1946
1947    #[test]
1948    fn test_fixed32_signum() {
1949        assert_eq!(Fixed32::from_int(5).signum().to_int(), 1);
1950        assert_eq!(Fixed32::from_int(-5).signum().to_int(), -1);
1951        assert_eq!(Fixed32::ZERO.signum().to_int(), 0);
1952    }
1953
1954    #[test]
1955    fn test_fixed32_is_negative_positive() {
1956        let pos = Fixed32::from_int(5);
1957        let neg = Fixed32::from_int(-5);
1958        let zero = Fixed32::ZERO;
1959
1960        assert!(pos.is_positive());
1961        assert!(!pos.is_negative());
1962
1963        assert!(neg.is_negative());
1964        assert!(!neg.is_positive());
1965
1966        assert!(!zero.is_positive());
1967        assert!(!zero.is_negative());
1968    }
1969
1970    #[test]
1971    fn test_fixed32_saturating_add() {
1972        let a = Fixed32::MAX;
1973        let b = Fixed32::ONE;
1974        assert_eq!(a.saturating_add(b), Fixed32::MAX);
1975
1976        let c = Fixed32::MIN;
1977        let d = Fixed32::from_int(-1);
1978        assert_eq!(c.saturating_add(d), Fixed32::MIN);
1979
1980        // Normal case
1981        let e = Fixed32::from_int(5);
1982        let f = Fixed32::from_int(3);
1983        assert_eq!(e.saturating_add(f).to_int(), 8);
1984    }
1985
1986    #[test]
1987    fn test_fixed32_saturating_sub() {
1988        let a = Fixed32::MIN;
1989        let b = Fixed32::ONE;
1990        assert_eq!(a.saturating_sub(b), Fixed32::MIN);
1991
1992        let c = Fixed32::MAX;
1993        let d = Fixed32::from_int(-1);
1994        assert_eq!(c.saturating_sub(d), Fixed32::MAX);
1995    }
1996
1997    #[test]
1998    fn test_fixed32_saturating_mul() {
1999        let a = Fixed32::MAX;
2000        let b = Fixed32::from_int(2);
2001        assert_eq!(a.saturating_mul(b), Fixed32::MAX);
2002
2003        let c = Fixed32::MIN;
2004        assert_eq!(c.saturating_mul(b), Fixed32::MIN);
2005
2006        // Normal case
2007        let d = Fixed32::from_int(5);
2008        let e = Fixed32::from_int(3);
2009        assert_eq!(d.saturating_mul(e).to_int(), 15);
2010    }
2011
2012    #[test]
2013    fn test_fixed32_clamp() {
2014        let min = Fixed32::from_int(0);
2015        let max = Fixed32::from_int(10);
2016
2017        assert_eq!(Fixed32::from_int(5).clamp(min, max).to_int(), 5);
2018        assert_eq!(Fixed32::from_int(-5).clamp(min, max).to_int(), 0);
2019        assert_eq!(Fixed32::from_int(15).clamp(min, max).to_int(), 10);
2020    }
2021
2022    #[test]
2023    fn test_fixed32_lerp() {
2024        let a = Fixed32::from_int(0);
2025        let b = Fixed32::from_int(10);
2026
2027        let t0 = Fixed32::ZERO;
2028        let t_half = Fixed32::HALF;
2029        let t1 = Fixed32::ONE;
2030
2031        assert_eq!(a.lerp(b, t0).to_int(), 0);
2032        assert_eq!(a.lerp(b, t_half).to_int(), 5);
2033        assert_eq!(a.lerp(b, t1).to_int(), 10);
2034    }
2035
2036    #[test]
2037    fn test_fixed32_floor_ceil_round() {
2038        let a = Fixed32::from_f32(2.7);
2039        assert_eq!(a.floor().to_int(), 2);
2040        assert_eq!(a.ceil().to_int(), 3);
2041        assert_eq!(a.round().to_int(), 3);
2042
2043        let b = Fixed32::from_f32(2.3);
2044        assert_eq!(b.floor().to_int(), 2);
2045        assert_eq!(b.ceil().to_int(), 3);
2046        assert_eq!(b.round().to_int(), 2);
2047
2048        // Exact integer
2049        let c = Fixed32::from_int(5);
2050        assert_eq!(c.floor().to_int(), 5);
2051        assert_eq!(c.ceil().to_int(), 5);
2052        assert_eq!(c.round().to_int(), 5);
2053    }
2054
2055    #[test]
2056    fn test_fixed32_fract() {
2057        let a = Fixed32::from_f32(2.75);
2058        let frac = a.fract();
2059        assert!((frac.to_f32() - 0.75).abs() < 0.001);
2060
2061        let b = Fixed32::from_int(5);
2062        assert_eq!(b.fract().to_raw(), 0);
2063    }
2064
2065    #[test]
2066    fn test_fixed32_add_assign() {
2067        let mut a = Fixed32::from_int(5);
2068        a += Fixed32::from_int(3);
2069        assert_eq!(a.to_int(), 8);
2070    }
2071
2072    #[test]
2073    fn test_fixed32_sub_assign() {
2074        let mut a = Fixed32::from_int(10);
2075        a -= Fixed32::from_int(3);
2076        assert_eq!(a.to_int(), 7);
2077    }
2078
2079    #[test]
2080    fn test_fixed32_from_trait() {
2081        let a: Fixed32 = 5.into();
2082        assert_eq!(a.to_int(), 5);
2083    }
2084
2085    #[test]
2086    fn test_fixed32_display() {
2087        let a = Fixed32::from_f32(3.14159);
2088        let s = format!("{a}");
2089        assert!(s.starts_with("3.14"));
2090    }
2091
2092    #[test]
2093    fn test_fixed32_ord() {
2094        let a = Fixed32::from_int(5);
2095        let b = Fixed32::from_int(3);
2096        let c = Fixed32::from_int(5);
2097
2098        assert!(a > b);
2099        assert!(b < a);
2100        assert!(a >= c);
2101        assert!(a <= c);
2102        assert_eq!(a.cmp(&c), std::cmp::Ordering::Equal);
2103    }
2104
2105    #[test]
2106    fn test_fixed32_default() {
2107        let d: Fixed32 = Default::default();
2108        assert_eq!(d, Fixed32::ZERO);
2109    }
2110
2111    #[test]
2112    fn test_fixed32_pi() {
2113        let pi = Fixed32::PI;
2114        let approx = pi.to_f32();
2115        assert!((approx - 3.14159).abs() < 0.0001);
2116    }
2117
2118    #[test]
2119    fn test_fixed32_cross_platform_determinism() {
2120        // This test verifies that Fixed32 math is IDENTICAL regardless of platform.
2121        // The raw values should be exactly the same on any architecture.
2122        let a = Fixed32::from_raw(327680); // 5.0
2123        let b = Fixed32::from_raw(196608); // 3.0
2124
2125        // Addition
2126        assert_eq!((a + b).to_raw(), 524288); // 8.0
2127
2128        // Multiplication (this is where f32 diverges across platforms)
2129        let product = a.mul(b);
2130        assert_eq!(product.to_raw(), 983040); // 15.0
2131
2132        // Division
2133        let quotient = a.div(b);
2134        assert_eq!(quotient.to_raw(), 109226); // ~1.6667
2135
2136        // These assertions will pass on EVERY platform - that's the point of Fixed32
2137    }
2138
2139    // =========================================================================
2140    // FrameRecord Tests
2141    // =========================================================================
2142
2143    #[test]
2144    fn test_frame_record_new() {
2145        let record = FrameRecord::new(42);
2146        assert_eq!(record.frame, 42);
2147        assert!(record.inputs.is_empty());
2148        assert!(record.state_hash.is_none());
2149    }
2150
2151    #[test]
2152    fn test_frame_record_add_input() {
2153        let mut record = FrameRecord::new(1);
2154        assert!(!record.has_inputs());
2155
2156        record.add_input(InputEvent {
2157            event_type: InputEventType::KeyDown(65),
2158            frame_offset_us: 1000,
2159        });
2160
2161        assert!(record.has_inputs());
2162        assert_eq!(record.inputs.len(), 1);
2163    }
2164
2165    #[test]
2166    fn test_frame_record_state_hash() {
2167        let mut record = FrameRecord::new(1);
2168        assert!(record.state_hash.is_none());
2169
2170        let hash = [0xab; 32];
2171        record.set_state_hash(hash);
2172        assert_eq!(record.state_hash, Some(hash));
2173    }
2174
2175    // =========================================================================
2176    // AdaptiveSnapshotter Tests
2177    // =========================================================================
2178
2179    #[test]
2180    fn test_snapshotter_default() {
2181        let snap = AdaptiveSnapshotter::default();
2182        assert_eq!(snap.min_interval, 15);
2183        assert_eq!(snap.max_interval, 120);
2184        assert_eq!(snap.entropy_threshold, 64);
2185    }
2186
2187    #[test]
2188    fn test_snapshotter_force_at_max_interval() {
2189        let mut snap = AdaptiveSnapshotter::new(10, 50, 64);
2190        let hash = [0; 32];
2191
2192        // Frame 0: First frame returns Skip (0 - 0 = 0, not >= max_interval)
2193        // The very first snapshot must be triggered by max interval or entropy
2194        assert_eq!(snap.should_snapshot(0, &hash), SnapshotDecision::Skip);
2195
2196        // Frame 50: Max interval reached from initial frame 0
2197        assert_eq!(
2198            snap.should_snapshot(50, &hash),
2199            SnapshotDecision::FullSnapshot
2200        );
2201
2202        // Frame 99: Not at max yet (50 + 50 = 100)
2203        assert_eq!(snap.should_snapshot(99, &hash), SnapshotDecision::Skip);
2204
2205        // Frame 100: Max interval reached again
2206        assert_eq!(
2207            snap.should_snapshot(100, &hash),
2208            SnapshotDecision::FullSnapshot
2209        );
2210    }
2211
2212    #[test]
2213    fn test_snapshotter_entropy_trigger() {
2214        let mut snap = AdaptiveSnapshotter::new(5, 120, 32);
2215
2216        // Initial snapshot - force one at max interval first
2217        let hash1 = [0; 32];
2218        let _ = snap.should_snapshot(120, &hash1); // Force first snapshot
2219
2220        // Low entropy (no change)
2221        let hash2 = [0; 32];
2222        assert_eq!(snap.should_snapshot(130, &hash2), SnapshotDecision::Skip);
2223
2224        // High entropy (all bits flipped) - past min_interval (5 frames)
2225        let hash3 = [0xFF; 32];
2226        assert_eq!(
2227            snap.should_snapshot(135, &hash3),
2228            SnapshotDecision::DeltaSnapshot
2229        );
2230    }
2231
2232    #[test]
2233    fn test_snapshotter_min_interval_respected() {
2234        let mut snap = AdaptiveSnapshotter::new(10, 120, 32);
2235
2236        // Force first snapshot at max interval
2237        let hash1 = [0; 32];
2238        let _ = snap.should_snapshot(120, &hash1);
2239
2240        // High entropy but before min interval (120 + 5 = 125)
2241        let hash2 = [0xFF; 32];
2242        assert_eq!(snap.should_snapshot(125, &hash2), SnapshotDecision::Skip);
2243
2244        // High entropy after min interval (120 + 15 = 135)
2245        assert_eq!(
2246            snap.should_snapshot(135, &hash2),
2247            SnapshotDecision::DeltaSnapshot
2248        );
2249    }
2250
2251    #[test]
2252    fn test_snapshotter_reset() {
2253        let mut snap = AdaptiveSnapshotter::new(10, 50, 64);
2254        let hash = [0xAB; 32];
2255
2256        let _ = snap.should_snapshot(100, &hash);
2257        assert_eq!(snap.last_snapshot_frame(), 100);
2258
2259        snap.reset();
2260        assert_eq!(snap.last_snapshot_frame(), 0);
2261    }
2262
2263    // =========================================================================
2264    // TraceBuffer Tests
2265    // =========================================================================
2266
2267    #[test]
2268    fn test_buffer_new() {
2269        let buf = TraceBuffer::new(10, BufferPolicy::DropOldest);
2270        assert_eq!(buf.capacity(), 10);
2271        assert_eq!(buf.len(), 0);
2272        assert!(buf.is_empty());
2273        assert!(!buf.is_full());
2274    }
2275
2276    #[test]
2277    fn test_buffer_push_pop() {
2278        let mut buf = TraceBuffer::new(10, BufferPolicy::DropOldest);
2279
2280        buf.push(FrameRecord::new(1)).unwrap();
2281        buf.push(FrameRecord::new(2)).unwrap();
2282        buf.push(FrameRecord::new(3)).unwrap();
2283
2284        assert_eq!(buf.len(), 3);
2285
2286        let r1 = buf.pop().unwrap();
2287        assert_eq!(r1.frame, 1);
2288
2289        let r2 = buf.pop().unwrap();
2290        assert_eq!(r2.frame, 2);
2291
2292        let r3 = buf.pop().unwrap();
2293        assert_eq!(r3.frame, 3);
2294
2295        assert!(buf.is_empty());
2296        assert!(buf.pop().is_none());
2297    }
2298
2299    #[test]
2300    fn test_buffer_drop_oldest_policy() {
2301        let mut buf = TraceBuffer::new(3, BufferPolicy::DropOldest);
2302
2303        buf.push(FrameRecord::new(1)).unwrap();
2304        buf.push(FrameRecord::new(2)).unwrap();
2305        buf.push(FrameRecord::new(3)).unwrap();
2306        assert!(buf.is_full());
2307
2308        // This should drop frame 1
2309        buf.push(FrameRecord::new(4)).unwrap();
2310
2311        assert_eq!(buf.len(), 3);
2312        assert_eq!(buf.frames_dropped(), 1);
2313
2314        // First pop should be frame 2 (frame 1 was dropped)
2315        let r = buf.pop().unwrap();
2316        assert_eq!(r.frame, 2);
2317    }
2318
2319    #[test]
2320    fn test_buffer_andon_cord_policy() {
2321        let mut buf = TraceBuffer::new(3, BufferPolicy::AndonCord);
2322
2323        buf.push(FrameRecord::new(1)).unwrap();
2324        buf.push(FrameRecord::new(2)).unwrap();
2325        buf.push(FrameRecord::new(3)).unwrap();
2326
2327        // This should return BufferFull error (Andon Cord)
2328        let result = buf.push(FrameRecord::new(4));
2329        assert_eq!(result, Err(TraceError::BufferFull));
2330
2331        // Buffer unchanged
2332        assert_eq!(buf.len(), 3);
2333        assert_eq!(buf.frames_dropped(), 0);
2334    }
2335
2336    #[test]
2337    fn test_buffer_drain() {
2338        let mut buf = TraceBuffer::new(10, BufferPolicy::DropOldest);
2339
2340        for i in 0..5 {
2341            buf.push(FrameRecord::new(i)).unwrap();
2342        }
2343
2344        let drained = buf.drain(3);
2345        assert_eq!(drained.len(), 3);
2346        assert_eq!(drained[0].frame, 0);
2347        assert_eq!(drained[1].frame, 1);
2348        assert_eq!(drained[2].frame, 2);
2349
2350        assert_eq!(buf.len(), 2);
2351    }
2352
2353    #[test]
2354    fn test_buffer_drain_more_than_available() {
2355        let mut buf = TraceBuffer::new(10, BufferPolicy::DropOldest);
2356
2357        buf.push(FrameRecord::new(1)).unwrap();
2358        buf.push(FrameRecord::new(2)).unwrap();
2359
2360        let drained = buf.drain(100);
2361        assert_eq!(drained.len(), 2);
2362        assert!(buf.is_empty());
2363    }
2364
2365    #[test]
2366    fn test_buffer_clear() {
2367        let mut buf = TraceBuffer::new(10, BufferPolicy::DropOldest);
2368
2369        for i in 0..5 {
2370            buf.push(FrameRecord::new(i)).unwrap();
2371        }
2372
2373        buf.clear();
2374        assert!(buf.is_empty());
2375        assert_eq!(buf.len(), 0);
2376    }
2377
2378    #[test]
2379    fn test_buffer_wrap_around() {
2380        let mut buf = TraceBuffer::new(3, BufferPolicy::DropOldest);
2381
2382        // Fill buffer
2383        buf.push(FrameRecord::new(1)).unwrap();
2384        buf.push(FrameRecord::new(2)).unwrap();
2385        buf.push(FrameRecord::new(3)).unwrap();
2386
2387        // Remove one
2388        let _ = buf.pop();
2389
2390        // Add one (tests wrap around)
2391        buf.push(FrameRecord::new(4)).unwrap();
2392
2393        assert_eq!(buf.len(), 3);
2394
2395        let r1 = buf.pop().unwrap();
2396        assert_eq!(r1.frame, 2);
2397
2398        let r2 = buf.pop().unwrap();
2399        assert_eq!(r2.frame, 3);
2400
2401        let r3 = buf.pop().unwrap();
2402        assert_eq!(r3.frame, 4);
2403    }
2404
2405    #[test]
2406    fn test_buffer_debug_constructor() {
2407        let buf = TraceBuffer::debug(100);
2408        assert_eq!(buf.policy(), BufferPolicy::AndonCord);
2409        assert_eq!(buf.capacity(), 100);
2410    }
2411
2412    #[test]
2413    fn test_buffer_production_constructor() {
2414        let buf = TraceBuffer::production(100);
2415        assert_eq!(buf.policy(), BufferPolicy::DropOldest);
2416        assert_eq!(buf.capacity(), 100);
2417    }
2418
2419    #[test]
2420    fn test_trace_error_display() {
2421        let e1 = TraceError::BufferFull;
2422        assert_eq!(format!("{e1}"), "Trace buffer is full");
2423
2424        let e2 = TraceError::InvalidFrameSequence;
2425        assert_eq!(format!("{e2}"), "Invalid frame sequence");
2426
2427        let e3 = TraceError::SerializationError("test".to_string());
2428        assert!(format!("{e3}").contains("test"));
2429    }
2430
2431    // =========================================================================
2432    // Property-Based Tests (using simple iteration)
2433    // =========================================================================
2434
2435    #[test]
2436    fn property_fixed32_add_commutative() {
2437        for a in [-100, -1, 0, 1, 100] {
2438            for b in [-100, -1, 0, 1, 100] {
2439                let fa = Fixed32::from_int(a);
2440                let fb = Fixed32::from_int(b);
2441                assert_eq!(fa + fb, fb + fa, "Addition should be commutative");
2442            }
2443        }
2444    }
2445
2446    #[test]
2447    fn property_fixed32_mul_commutative() {
2448        for a in [-10, -1, 0, 1, 10] {
2449            for b in [-10, -1, 0, 1, 10] {
2450                let fa = Fixed32::from_int(a);
2451                let fb = Fixed32::from_int(b);
2452                assert_eq!(
2453                    fa.mul(fb),
2454                    fb.mul(fa),
2455                    "Multiplication should be commutative"
2456                );
2457            }
2458        }
2459    }
2460
2461    #[test]
2462    fn property_fixed32_add_identity() {
2463        for a in [-1000, -1, 0, 1, 1000] {
2464            let fa = Fixed32::from_int(a);
2465            assert_eq!(fa + Fixed32::ZERO, fa, "Zero should be additive identity");
2466        }
2467    }
2468
2469    #[test]
2470    fn property_fixed32_mul_identity() {
2471        for a in [-1000, -1, 0, 1, 1000] {
2472            let fa = Fixed32::from_int(a);
2473            assert_eq!(
2474                fa.mul(Fixed32::ONE),
2475                fa,
2476                "One should be multiplicative identity"
2477            );
2478        }
2479    }
2480
2481    #[test]
2482    fn property_fixed32_neg_neg_identity() {
2483        for a in [-1000, -1, 0, 1, 1000] {
2484            let fa = Fixed32::from_int(a);
2485            assert_eq!(-(-fa), fa, "Double negation should be identity");
2486        }
2487    }
2488
2489    // =========================================================================
2490    // TraceQuery Tests
2491    // =========================================================================
2492
2493    fn create_test_buffer_with_frames() -> TraceBuffer {
2494        let mut buf = TraceBuffer::new(100, BufferPolicy::DropOldest);
2495
2496        // Add frames: 0, 1, 2 (no input), 3 (key input), 4, 5 (mouse input)
2497        buf.push(FrameRecord::new(0)).unwrap();
2498        buf.push(FrameRecord::new(1)).unwrap();
2499        buf.push(FrameRecord::new(2)).unwrap();
2500
2501        let mut frame3 = FrameRecord::new(3);
2502        frame3.add_input(InputEvent {
2503            event_type: InputEventType::KeyDown(65), // 'A' key
2504            frame_offset_us: 100,
2505        });
2506        buf.push(frame3).unwrap();
2507
2508        buf.push(FrameRecord::new(4)).unwrap();
2509
2510        let mut frame5 = FrameRecord::new(5);
2511        frame5.add_input(InputEvent {
2512            event_type: InputEventType::MouseDown {
2513                button: 0,
2514                x: 100,
2515                y: 200,
2516            },
2517            frame_offset_us: 500,
2518        });
2519        buf.push(frame5).unwrap();
2520
2521        buf
2522    }
2523
2524    #[test]
2525    fn test_query_from_buffer() {
2526        let buf = create_test_buffer_with_frames();
2527        let query = TraceQuery::from_buffer(&buf);
2528
2529        assert_eq!(query.frame_count(), 6);
2530        assert_eq!(query.first_frame(), Some(0));
2531        assert_eq!(query.last_frame(), Some(5));
2532    }
2533
2534    #[test]
2535    fn test_query_find_frames() {
2536        let buf = create_test_buffer_with_frames();
2537        let query = TraceQuery::from_buffer(&buf);
2538
2539        // Find all frames with inputs
2540        let frames_with_input = query.find_frames(|f| f.has_inputs());
2541        assert_eq!(frames_with_input, vec![3, 5]);
2542
2543        // Find all frames without inputs
2544        let frames_without_input = query.find_frames(|f| !f.has_inputs());
2545        assert_eq!(frames_without_input, vec![0, 1, 2, 4]);
2546    }
2547
2548    #[test]
2549    fn test_query_frames_with_inputs() {
2550        let buf = create_test_buffer_with_frames();
2551        let query = TraceQuery::from_buffer(&buf);
2552
2553        let frames = query.frames_with_inputs();
2554        assert_eq!(frames, vec![3, 5]);
2555    }
2556
2557    #[test]
2558    fn test_query_frames_with_input_type() {
2559        let buf = create_test_buffer_with_frames();
2560        let query = TraceQuery::from_buffer(&buf);
2561
2562        // Find frames with KeyDown
2563        let key_frames = query.frames_with_input_type(|e| matches!(e, InputEventType::KeyDown(_)));
2564        assert_eq!(key_frames, vec![3]);
2565
2566        // Find frames with MouseDown
2567        let mouse_frames =
2568            query.frames_with_input_type(|e| matches!(e, InputEventType::MouseDown { .. }));
2569        assert_eq!(mouse_frames, vec![5]);
2570    }
2571
2572    #[test]
2573    fn test_query_get_frame() {
2574        let buf = create_test_buffer_with_frames();
2575        let query = TraceQuery::from_buffer(&buf);
2576
2577        let frame3 = query.get_frame(3);
2578        assert!(frame3.is_some());
2579        assert!(frame3.unwrap().has_inputs());
2580
2581        let frame99 = query.get_frame(99);
2582        assert!(frame99.is_none());
2583    }
2584
2585    #[test]
2586    fn test_query_get_frame_range() {
2587        let buf = create_test_buffer_with_frames();
2588        let query = TraceQuery::from_buffer(&buf);
2589
2590        let range = query.get_frame_range(2, 4);
2591        assert_eq!(range.len(), 3);
2592        assert_eq!(range[0].frame, 2);
2593        assert_eq!(range[1].frame, 3);
2594        assert_eq!(range[2].frame, 4);
2595    }
2596
2597    #[test]
2598    fn test_query_count_frames() {
2599        let buf = create_test_buffer_with_frames();
2600        let query = TraceQuery::from_buffer(&buf);
2601
2602        let count = query.count_frames(|f| f.has_inputs());
2603        assert_eq!(count, 2);
2604    }
2605
2606    #[test]
2607    fn test_query_input_density() {
2608        let buf = create_test_buffer_with_frames();
2609        let query = TraceQuery::from_buffer(&buf);
2610
2611        let density = query.input_density();
2612        // 2 inputs across 6 frames = 0.333...
2613        assert!((density - (2.0 / 6.0)).abs() < 0.001);
2614    }
2615
2616    #[test]
2617    fn test_query_input_density_empty() {
2618        let buf = TraceBuffer::new(10, BufferPolicy::DropOldest);
2619        let query = TraceQuery::from_buffer(&buf);
2620
2621        let density = query.input_density();
2622        assert_eq!(density, 0.0);
2623    }
2624
2625    #[test]
2626    fn test_query_find_frames_with_context() {
2627        let buf = create_test_buffer_with_frames();
2628        let query = TraceQuery::from_buffer(&buf);
2629
2630        let results = query.find_frames_with_context(|f| f.has_inputs(), 2);
2631        assert_eq!(results.len(), 2);
2632
2633        // First result: frame 3 with context
2634        let r1 = &results[0];
2635        assert_eq!(r1.frame, 3);
2636        assert_eq!(r1.context_before.len(), 2); // frames 1, 2
2637        assert_eq!(r1.context_after.len(), 2); // frames 4, 5
2638
2639        // Second result: frame 5 with context
2640        let r2 = &results[1];
2641        assert_eq!(r2.frame, 5);
2642        assert_eq!(r2.context_before.len(), 2); // frames 3, 4
2643        assert_eq!(r2.context_after.len(), 0); // no frames after 5
2644    }
2645
2646    #[test]
2647    fn test_buffer_iter() {
2648        let mut buf = TraceBuffer::new(5, BufferPolicy::DropOldest);
2649
2650        buf.push(FrameRecord::new(10)).unwrap();
2651        buf.push(FrameRecord::new(20)).unwrap();
2652        buf.push(FrameRecord::new(30)).unwrap();
2653
2654        let frames: Vec<_> = buf.iter().collect();
2655        assert_eq!(frames.len(), 3);
2656        assert_eq!(frames[0].frame, 10);
2657        assert_eq!(frames[1].frame, 20);
2658        assert_eq!(frames[2].frame, 30);
2659    }
2660
2661    #[test]
2662    fn test_buffer_iter_wrap_around() {
2663        let mut buf = TraceBuffer::new(3, BufferPolicy::DropOldest);
2664
2665        // Fill and overflow to test wrap-around
2666        buf.push(FrameRecord::new(1)).unwrap();
2667        buf.push(FrameRecord::new(2)).unwrap();
2668        buf.push(FrameRecord::new(3)).unwrap();
2669        buf.push(FrameRecord::new(4)).unwrap(); // Drops frame 1
2670
2671        let frames: Vec<_> = buf.iter().collect();
2672        assert_eq!(frames.len(), 3);
2673        assert_eq!(frames[0].frame, 2);
2674        assert_eq!(frames[1].frame, 3);
2675        assert_eq!(frames[2].frame, 4);
2676    }
2677
2678    // =========================================================================
2679    // GameTracer Tests
2680    // =========================================================================
2681
2682    #[test]
2683    fn test_game_tracer_new() {
2684        let tracer = GameTracer::new(TracerConfig::default());
2685        assert_eq!(tracer.current_frame(), 0);
2686        assert!(tracer.is_recording());
2687        assert_eq!(tracer.buffer().len(), 0);
2688    }
2689
2690    #[test]
2691    fn test_game_tracer_debug() {
2692        let tracer = GameTracer::debug();
2693        assert!(tracer.is_recording());
2694        assert_eq!(tracer.buffer().policy(), BufferPolicy::AndonCord);
2695    }
2696
2697    #[test]
2698    fn test_game_tracer_production() {
2699        let tracer = GameTracer::production();
2700        assert!(tracer.is_recording());
2701        assert_eq!(tracer.buffer().policy(), BufferPolicy::DropOldest);
2702    }
2703
2704    #[test]
2705    fn test_game_tracer_basic_frame() {
2706        let mut tracer = GameTracer::new(TracerConfig::default());
2707
2708        tracer.begin_frame();
2709        let _ = tracer.end_frame(None);
2710
2711        assert_eq!(tracer.current_frame(), 1);
2712        assert_eq!(tracer.buffer().len(), 1);
2713    }
2714
2715    #[test]
2716    fn test_game_tracer_record_input() {
2717        let mut tracer = GameTracer::new(TracerConfig::default());
2718
2719        tracer.begin_frame();
2720        tracer.record_input(InputEvent {
2721            event_type: InputEventType::KeyDown(32), // Space
2722            frame_offset_us: 0,
2723        });
2724        let _ = tracer.end_frame(None);
2725
2726        let stats = tracer.stats();
2727        assert_eq!(stats.total_inputs, 1);
2728        assert_eq!(stats.frame, 1);
2729    }
2730
2731    #[test]
2732    fn test_game_tracer_record_multiple_inputs() {
2733        let mut tracer = GameTracer::new(TracerConfig::default());
2734
2735        tracer.begin_frame();
2736        tracer.record_inputs(vec![
2737            InputEvent {
2738                event_type: InputEventType::KeyDown(32),
2739                frame_offset_us: 0,
2740            },
2741            InputEvent {
2742                event_type: InputEventType::KeyDown(87),
2743                frame_offset_us: 100,
2744            },
2745        ]);
2746        let _ = tracer.end_frame(None);
2747
2748        let stats = tracer.stats();
2749        assert_eq!(stats.total_inputs, 2);
2750    }
2751
2752    #[test]
2753    fn test_game_tracer_stop_recording() {
2754        let mut tracer = GameTracer::new(TracerConfig::default());
2755
2756        tracer.begin_frame();
2757        let _ = tracer.end_frame(None);
2758        assert_eq!(tracer.buffer().len(), 1);
2759
2760        tracer.stop_recording();
2761        assert!(!tracer.is_recording());
2762
2763        // These should be no-ops
2764        tracer.begin_frame();
2765        tracer.record_input(InputEvent {
2766            event_type: InputEventType::KeyDown(32),
2767            frame_offset_us: 0,
2768        });
2769        let _ = tracer.end_frame(None);
2770
2771        // Still only 1 frame
2772        assert_eq!(tracer.buffer().len(), 1);
2773        assert_eq!(tracer.stats().total_inputs, 0);
2774    }
2775
2776    #[test]
2777    fn test_game_tracer_restart_recording() {
2778        let mut tracer = GameTracer::new(TracerConfig::default());
2779
2780        tracer.begin_frame();
2781        let _ = tracer.end_frame(None);
2782        tracer.stop_recording();
2783        tracer.start_recording();
2784
2785        tracer.begin_frame();
2786        let _ = tracer.end_frame(None);
2787
2788        assert_eq!(tracer.buffer().len(), 2);
2789    }
2790
2791    #[test]
2792    fn test_game_tracer_stats() {
2793        let mut config = TracerConfig::default();
2794        config.buffer_capacity = 100;
2795        let mut tracer = GameTracer::new(config);
2796
2797        for _ in 0..10 {
2798            tracer.begin_frame();
2799            tracer.record_input(InputEvent {
2800                event_type: InputEventType::KeyDown(32),
2801                frame_offset_us: 0,
2802            });
2803            let _ = tracer.end_frame(None);
2804        }
2805
2806        let stats = tracer.stats();
2807        assert_eq!(stats.frame, 10);
2808        assert_eq!(stats.buffer_len, 10);
2809        assert_eq!(stats.buffer_capacity, 100);
2810        assert_eq!(stats.total_inputs, 10);
2811        assert!(stats.recording);
2812        assert_eq!(stats.policy, "DropOldest");
2813    }
2814
2815    #[test]
2816    fn test_game_tracer_query() {
2817        let mut tracer = GameTracer::new(TracerConfig::default());
2818
2819        // Frame 0: no input
2820        tracer.begin_frame();
2821        let _ = tracer.end_frame(None);
2822
2823        // Frame 1: has input
2824        tracer.begin_frame();
2825        tracer.record_input(InputEvent {
2826            event_type: InputEventType::KeyDown(32),
2827            frame_offset_us: 0,
2828        });
2829        let _ = tracer.end_frame(None);
2830
2831        // Frame 2: no input
2832        tracer.begin_frame();
2833        let _ = tracer.end_frame(None);
2834
2835        let query = tracer.query();
2836        let frames_with_input = query.frames_with_inputs();
2837        assert_eq!(frames_with_input.len(), 1);
2838        assert_eq!(frames_with_input[0], 1);
2839    }
2840
2841    #[test]
2842    fn test_game_tracer_clear() {
2843        let mut tracer = GameTracer::new(TracerConfig::default());
2844
2845        for _ in 0..5 {
2846            tracer.begin_frame();
2847            tracer.record_input(InputEvent {
2848                event_type: InputEventType::KeyDown(32),
2849                frame_offset_us: 0,
2850            });
2851            let _ = tracer.end_frame(None);
2852        }
2853
2854        assert_eq!(tracer.current_frame(), 5);
2855        assert_eq!(tracer.buffer().len(), 5);
2856
2857        tracer.clear();
2858
2859        assert_eq!(tracer.current_frame(), 0);
2860        assert_eq!(tracer.buffer().len(), 0);
2861        assert_eq!(tracer.stats().total_inputs, 0);
2862    }
2863
2864    #[test]
2865    fn test_game_tracer_drain() {
2866        let mut tracer = GameTracer::new(TracerConfig::default());
2867
2868        for _ in 0..5 {
2869            tracer.begin_frame();
2870            let _ = tracer.end_frame(None);
2871        }
2872
2873        let drained = tracer.drain(3);
2874        assert_eq!(drained.len(), 3);
2875        assert_eq!(tracer.buffer().len(), 2);
2876    }
2877
2878    #[test]
2879    fn test_game_tracer_export_json() {
2880        let mut tracer = GameTracer::new(TracerConfig::default());
2881
2882        tracer.begin_frame();
2883        tracer.record_input(InputEvent {
2884            event_type: InputEventType::KeyDown(32),
2885            frame_offset_us: 100,
2886        });
2887        let _ = tracer.end_frame(None);
2888
2889        let json = tracer.export_json().unwrap();
2890        assert!(json.contains("\"frame\":0"));
2891        assert!(json.contains("KeyDown"));
2892    }
2893
2894    #[test]
2895    fn test_game_tracer_with_state_hash() {
2896        let mut config = TracerConfig::default();
2897        config.record_state_hashes = true;
2898        config.snapshotter = AdaptiveSnapshotter::new(1, 10, 64);
2899        let mut tracer = GameTracer::new(config);
2900
2901        // First frame with high entropy hash
2902        let hash1 = [0u8; 32];
2903        tracer.begin_frame();
2904        let decision = tracer.end_frame(Some(hash1));
2905        // First frame at max_interval might trigger FullSnapshot
2906        assert!(decision == SnapshotDecision::FullSnapshot || decision == SnapshotDecision::Skip);
2907
2908        // Second frame with same hash (low entropy)
2909        tracer.begin_frame();
2910        let decision = tracer.end_frame(Some(hash1));
2911        assert_eq!(decision, SnapshotDecision::Skip);
2912
2913        // Third frame with different hash (high entropy)
2914        let mut hash2 = [0u8; 32];
2915        hash2.fill(0xFF); // Maximum Hamming distance
2916        tracer.begin_frame();
2917        let decision = tracer.end_frame(Some(hash2));
2918        assert_eq!(decision, SnapshotDecision::DeltaSnapshot);
2919    }
2920
2921    #[test]
2922    fn test_tracer_config_default() {
2923        let config = TracerConfig::default();
2924        assert_eq!(config.buffer_capacity, 3600);
2925        assert_eq!(config.policy, BufferPolicy::DropOldest);
2926        assert!(config.record_state_hashes);
2927    }
2928
2929    #[test]
2930    fn test_tracer_config_debug() {
2931        let config = TracerConfig::debug();
2932        assert_eq!(config.policy, BufferPolicy::AndonCord);
2933        assert!(config.record_state_hashes);
2934    }
2935
2936    #[test]
2937    fn test_tracer_config_production() {
2938        let config = TracerConfig::production();
2939        assert_eq!(config.policy, BufferPolicy::DropOldest);
2940        assert!(!config.record_state_hashes); // Disabled for performance
2941    }
2942
2943    #[test]
2944    fn test_trace_stats_default() {
2945        let stats = TraceStats::default();
2946        assert_eq!(stats.frame, 0);
2947        assert_eq!(stats.buffer_len, 0);
2948        assert!(!stats.recording);
2949    }
2950
2951    #[test]
2952    #[cfg_attr(
2953        debug_assertions,
2954        should_panic(expected = "end_frame called without begin_frame")
2955    )]
2956    fn test_game_tracer_end_frame_without_begin() {
2957        let mut tracer = GameTracer::new(TracerConfig::default());
2958        // In debug mode: panics with debug_assert!
2959        // In release mode: returns Skip gracefully
2960        let decision = tracer.end_frame(None);
2961        assert_eq!(decision, SnapshotDecision::Skip);
2962    }
2963
2964    // =========================================================================
2965    // v1.3 TPS Kaizen Tests
2966    // =========================================================================
2967
2968    // -------------------------------------------------------------------------
2969    // Fixed32 Overflow Check Tests (Regehr 2012)
2970    // -------------------------------------------------------------------------
2971
2972    #[test]
2973    fn test_fixed32_checked_mul_success() {
2974        let a = Fixed32::from_int(100);
2975        let b = Fixed32::from_int(50);
2976        assert_eq!(a.checked_mul(b), Some(Fixed32::from_int(5000)));
2977    }
2978
2979    #[test]
2980    fn test_fixed32_checked_mul_overflow() {
2981        let big = Fixed32::MAX;
2982        assert_eq!(big.checked_mul(Fixed32::from_int(2)), None);
2983    }
2984
2985    #[test]
2986    fn test_fixed32_strict_mul_success() {
2987        let a = Fixed32::from_int(10);
2988        let b = Fixed32::from_int(20);
2989        assert_eq!(a.strict_mul(b), Fixed32::from_int(200));
2990    }
2991
2992    #[test]
2993    #[should_panic(expected = "Fixed32 multiplication overflow")]
2994    fn test_fixed32_strict_mul_overflow_panics() {
2995        let big = Fixed32::MAX;
2996        let _ = big.strict_mul(Fixed32::from_int(2));
2997    }
2998
2999    #[test]
3000    fn test_fixed32_checked_add_success() {
3001        let a = Fixed32::from_int(100);
3002        let b = Fixed32::from_int(50);
3003        assert_eq!(a.checked_add(b), Some(Fixed32::from_int(150)));
3004    }
3005
3006    #[test]
3007    fn test_fixed32_checked_add_overflow() {
3008        let big = Fixed32::MAX;
3009        assert_eq!(big.checked_add(Fixed32::from_int(1)), None);
3010    }
3011
3012    #[test]
3013    fn test_fixed32_checked_sub_success() {
3014        let a = Fixed32::from_int(100);
3015        let b = Fixed32::from_int(50);
3016        assert_eq!(a.checked_sub(b), Some(Fixed32::from_int(50)));
3017    }
3018
3019    #[test]
3020    fn test_fixed32_checked_sub_overflow() {
3021        let small = Fixed32::MIN;
3022        assert_eq!(small.checked_sub(Fixed32::from_int(1)), None);
3023    }
3024
3025    // -------------------------------------------------------------------------
3026    // AndonState Tests (MacKenzie & Ware 1993)
3027    // -------------------------------------------------------------------------
3028
3029    #[test]
3030    fn test_andon_state_normal() {
3031        let state = AndonState::Normal;
3032        assert_eq!(state.overlay_color(), [0.0, 0.0, 0.0, 0.0]);
3033        assert_eq!(state.status_text(), "");
3034        assert!(!state.is_error());
3035        assert!(!state.has_dropped());
3036        assert_eq!(state.dropped_count(), 0);
3037    }
3038
3039    #[test]
3040    fn test_andon_state_warning() {
3041        let state = AndonState::Warning { buffer_pct: 85 };
3042        assert_eq!(state.overlay_color(), [1.0, 0.8, 0.0, 0.3]);
3043        assert_eq!(state.status_text(), "TRACE BUFFER WARNING");
3044        assert!(state.is_error());
3045        assert!(!state.has_dropped());
3046        assert_eq!(state.dropped_count(), 0);
3047    }
3048
3049    #[test]
3050    fn test_andon_state_trace_loss() {
3051        let state = AndonState::TraceLoss { dropped_count: 42 };
3052        assert_eq!(state.overlay_color(), [1.0, 0.0, 0.0, 0.5]);
3053        assert_eq!(state.status_text(), "TRACE LOSS - EVENTS DROPPED");
3054        assert!(state.is_error());
3055        assert!(state.has_dropped());
3056        assert_eq!(state.dropped_count(), 42);
3057    }
3058
3059    #[test]
3060    fn test_andon_state_default() {
3061        let state = AndonState::default();
3062        assert_eq!(state, AndonState::Normal);
3063    }
3064
3065    // -------------------------------------------------------------------------
3066    // Soft Andon Buffer Tests
3067    // -------------------------------------------------------------------------
3068
3069    #[test]
3070    fn test_soft_andon_buffer_creation() {
3071        let buf = TraceBuffer::soft_andon(10);
3072        assert_eq!(buf.policy(), BufferPolicy::SoftAndon);
3073        assert_eq!(buf.capacity(), 10);
3074        assert_eq!(buf.andon_state(), AndonState::Normal);
3075    }
3076
3077    #[test]
3078    fn test_soft_andon_warning_at_80_percent() {
3079        let mut buf = TraceBuffer::soft_andon(10);
3080
3081        // Fill to 80%
3082        for i in 0..8 {
3083            buf.push(FrameRecord::new(i)).unwrap();
3084        }
3085
3086        // Push one more - the state check happens at the START of push
3087        // so we need to push again to see the warning for 80% fill
3088        buf.push(FrameRecord::new(8)).unwrap();
3089
3090        // Should be in warning state (now at 90%)
3091        match buf.andon_state() {
3092            AndonState::Warning { buffer_pct } => assert!(buffer_pct >= 80),
3093            _ => panic!("Expected Warning state at 80%+ fill"),
3094        }
3095    }
3096
3097    #[test]
3098    fn test_soft_andon_trace_loss_on_overflow() {
3099        let mut buf = TraceBuffer::soft_andon(3);
3100
3101        // Fill buffer
3102        for i in 0..3 {
3103            buf.push(FrameRecord::new(i)).unwrap();
3104        }
3105
3106        // Overflow - should trigger trace loss
3107        buf.push(FrameRecord::new(3)).unwrap();
3108
3109        assert!(buf.andon_state().has_dropped());
3110        assert_eq!(buf.andon_state().dropped_count(), 1);
3111    }
3112
3113    #[test]
3114    fn test_soft_andon_continues_after_overflow() {
3115        let mut buf = TraceBuffer::soft_andon(3);
3116
3117        // Fill and overflow multiple times
3118        for i in 0..10 {
3119            buf.push(FrameRecord::new(i)).unwrap();
3120        }
3121
3122        // All pushes should succeed (no error)
3123        assert_eq!(buf.len(), 3);
3124        assert_eq!(buf.frames_dropped(), 7);
3125        assert_eq!(buf.andon_state().dropped_count(), 7);
3126    }
3127
3128    // -------------------------------------------------------------------------
3129    // ZobristTable Tests (Zobrist 1970)
3130    // -------------------------------------------------------------------------
3131
3132    #[test]
3133    fn test_zobrist_table_creation() {
3134        let table = ZobristTable::new(42);
3135        // Just verify it doesn't panic
3136        let hash = table.hash_bytes(&[0, 1, 2, 3]);
3137        assert!(hash != 0 || hash == 0); // Any value is valid
3138    }
3139
3140    #[test]
3141    fn test_zobrist_table_deterministic() {
3142        let table1 = ZobristTable::new(42);
3143        let table2 = ZobristTable::new(42);
3144
3145        let data = [1, 2, 3, 4, 5];
3146        assert_eq!(table1.hash_bytes(&data), table2.hash_bytes(&data));
3147    }
3148
3149    #[test]
3150    fn test_zobrist_table_different_seeds() {
3151        let table1 = ZobristTable::new(42);
3152        let table2 = ZobristTable::new(43);
3153
3154        let data = [1, 2, 3, 4, 5];
3155        // Different seeds should produce different hashes (with high probability)
3156        assert_ne!(table1.hash_bytes(&data), table2.hash_bytes(&data));
3157    }
3158
3159    #[test]
3160    fn test_zobrist_incremental_update() {
3161        let table = ZobristTable::new(42);
3162        let data = [1, 2, 3, 4];
3163
3164        // Full hash
3165        let full_hash = table.hash_bytes(&data);
3166
3167        // Now change byte at position 0 from 1 to 5
3168        let updated_hash = table.update_hash(full_hash, 0, 1, 5);
3169
3170        // Verify it matches recalculating from scratch
3171        let new_data = [5, 2, 3, 4];
3172        assert_eq!(updated_hash, table.hash_bytes(&new_data));
3173    }
3174
3175    #[test]
3176    fn test_zobrist_entropy_calculation() {
3177        let hash1 = 0u64;
3178        let hash2 = 0xFFFF_FFFF_FFFF_FFFFu64;
3179
3180        // All bits different = 64 bit entropy
3181        assert_eq!(ZobristTable::entropy(hash1, hash2), 64);
3182
3183        // Same hash = 0 entropy
3184        assert_eq!(ZobristTable::entropy(hash1, hash1), 0);
3185
3186        // One bit different
3187        assert_eq!(ZobristTable::entropy(0, 1), 1);
3188    }
3189
3190    // -------------------------------------------------------------------------
3191    // ZobristSnapshotter Tests
3192    // -------------------------------------------------------------------------
3193
3194    #[test]
3195    fn test_zobrist_snapshotter_creation() {
3196        let snap = ZobristSnapshotter::new(42, 10, 120, 16);
3197        assert_eq!(snap.last_snapshot_frame(), 0);
3198    }
3199
3200    #[test]
3201    fn test_zobrist_snapshotter_default() {
3202        let snap = ZobristSnapshotter::default();
3203        assert_eq!(snap.last_snapshot_frame(), 0);
3204    }
3205
3206    #[test]
3207    fn test_zobrist_snapshotter_force_at_max_interval() {
3208        let mut snap = ZobristSnapshotter::new(42, 10, 50, 16);
3209
3210        // Before max interval - should skip
3211        for frame in 0..49 {
3212            assert_eq!(snap.should_snapshot(frame), SnapshotDecision::Skip);
3213        }
3214
3215        // At max interval - should force full snapshot
3216        assert_eq!(snap.should_snapshot(50), SnapshotDecision::FullSnapshot);
3217    }
3218
3219    #[test]
3220    fn test_zobrist_snapshotter_incremental_state_change() {
3221        let mut snap = ZobristSnapshotter::new(42, 5, 120, 4);
3222
3223        // Initialize with some state
3224        snap.initialize(&[0, 0, 0, 0]);
3225
3226        // Skip first few frames
3227        for frame in 1..5 {
3228            assert_eq!(snap.should_snapshot(frame), SnapshotDecision::Skip);
3229        }
3230
3231        // Make significant state changes (high entropy)
3232        for i in 0..32 {
3233            snap.on_state_change(i, 0, 255);
3234        }
3235
3236        // Now should trigger delta snapshot due to high entropy
3237        // (after min_interval)
3238        let decision = snap.should_snapshot(10);
3239        assert!(matches!(
3240            decision,
3241            SnapshotDecision::DeltaSnapshot | SnapshotDecision::Skip
3242        ));
3243    }
3244
3245    // -------------------------------------------------------------------------
3246    // deterministic! Macro Tests (Bessey 2010)
3247    // -------------------------------------------------------------------------
3248
3249    #[test]
3250    fn test_deterministic_macro_allows_fixed32() {
3251        let result = deterministic! {
3252            let a = Fixed32::from_int(5);
3253            let b = Fixed32::from_int(3);
3254            a.mul(b).to_int()
3255        };
3256        assert_eq!(result, 15);
3257    }
3258
3259    #[test]
3260    fn test_deterministic_macro_allows_integers() {
3261        let result = deterministic! {
3262            let a: i32 = 10;
3263            let b: i32 = 20;
3264            a + b
3265        };
3266        assert_eq!(result, 30);
3267    }
3268
3269    // Note: We can't test that f32 is rejected because it's a compile-time error.
3270    // The following would NOT compile:
3271    // deterministic! {
3272    //     let _x = 1.0f32;  // ERROR: f32 is shadowed
3273    // }
3274}