1use core::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
32use serde::{Deserialize, Serialize};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
57#[repr(transparent)]
58pub struct Fixed32(pub i32);
59
60impl Fixed32 {
61 pub const FRAC_BITS: u32 = 16;
63
64 pub const SCALE: i32 = 1 << Self::FRAC_BITS;
66
67 pub const ZERO: Self = Self(0);
69
70 pub const ONE: Self = Self(Self::SCALE);
72
73 pub const HALF: Self = Self(Self::SCALE / 2);
75
76 pub const MIN: Self = Self(i32::MIN);
78
79 pub const MAX: Self = Self(i32::MAX);
81
82 pub const EPSILON: Self = Self(1);
84
85 pub const PI: Self = Self(205_887); #[inline]
90 #[must_use]
91 pub const fn from_raw(raw: i32) -> Self {
92 Self(raw)
93 }
94
95 #[inline]
97 #[must_use]
98 pub const fn to_raw(self) -> i32 {
99 self.0
100 }
101
102 #[inline]
104 #[must_use]
105 pub const fn from_int(n: i32) -> Self {
106 Self(n << Self::FRAC_BITS)
107 }
108
109 #[inline]
111 #[must_use]
112 pub const fn to_int(self) -> i32 {
113 self.0 >> Self::FRAC_BITS
114 }
115
116 #[inline]
123 #[must_use]
124 pub fn from_f32(f: f32) -> Self {
125 Self((f * Self::SCALE as f32) as i32)
126 }
127
128 #[inline]
135 #[must_use]
136 pub fn to_f32(self) -> f32 {
137 self.0 as f32 / Self::SCALE as f32
138 }
139
140 #[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 #[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 #[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 #[inline]
165 #[must_use]
166 #[allow(clippy::cast_lossless)] 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 #[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 #[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 #[inline]
204 #[must_use]
205 pub const fn abs(self) -> Self {
206 Self(self.0.abs())
207 }
208
209 #[inline]
211 #[must_use]
212 pub const fn signum(self) -> Self {
213 Self::from_int(self.0.signum())
214 }
215
216 #[inline]
218 #[must_use]
219 pub const fn is_negative(self) -> bool {
220 self.0 < 0
221 }
222
223 #[inline]
225 #[must_use]
226 pub const fn is_positive(self) -> bool {
227 self.0 > 0
228 }
229
230 #[inline]
232 #[must_use]
233 pub const fn is_zero(self) -> bool {
234 self.0 == 0
235 }
236
237 #[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 #[inline]
254 #[must_use]
255 pub const fn lerp(self, other: Self, t: Self) -> Self {
256 let diff = Self(other.0 - self.0);
258 let scaled = diff.mul(t);
259 Self(self.0 + scaled.0)
260 }
261
262 #[inline]
264 #[must_use]
265 pub const fn floor(self) -> Self {
266 Self((self.0 >> Self::FRAC_BITS) << Self::FRAC_BITS)
267 }
268
269 #[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 #[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 #[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 #[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 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 #[inline]
351 #[must_use]
352 #[track_caller]
353 #[allow(clippy::panic)] 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 #[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 #[inline]
377 #[must_use]
378 #[track_caller]
379 #[allow(clippy::panic)] 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 #[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 #[inline]
403 #[must_use]
404 #[track_caller]
405 #[allow(clippy::panic)] 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#[macro_export]
445macro_rules! deterministic {
446 ($($body:tt)*) => {{
447 #[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
460impl 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
544#[allow(missing_docs)] pub enum InputEventType {
546 KeyDown(u8),
548 KeyUp(u8),
550 MouseDown { button: u8, x: i16, y: i16 },
552 MouseUp { button: u8, x: i16, y: i16 },
554 MouseMove { x: i16, y: i16 },
556 TouchStart { id: u8, x: i16, y: i16 },
558 TouchMove { id: u8, x: i16, y: i16 },
560 TouchEnd { id: u8, x: i16, y: i16 },
562 GamepadDown { gamepad: u8, button: u8 },
564 GamepadUp { gamepad: u8, button: u8 },
566 GamepadAxis { gamepad: u8, axis: u8, value: i16 },
568}
569
570#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
575pub struct InputEvent {
576 pub event_type: InputEventType,
578 pub frame_offset_us: u16,
581}
582
583#[derive(Debug, Clone, Default, Serialize, Deserialize)]
591pub struct FrameRecord {
592 pub frame: u64,
594 pub inputs: Vec<InputEvent>,
596 pub state_hash: Option<[u8; 32]>,
598}
599
600impl FrameRecord {
601 #[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 pub fn add_input(&mut self, event: InputEvent) {
613 self.inputs.push(event);
614 }
615
616 pub const fn set_state_hash(&mut self, hash: [u8; 32]) {
618 self.state_hash = Some(hash);
619 }
620
621 #[must_use]
623 #[allow(clippy::missing_const_for_fn)] pub fn has_inputs(&self) -> bool {
625 !self.inputs.is_empty()
626 }
627}
628
629#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
637pub enum BufferPolicy {
638 #[default]
640 DropOldest,
641 SoftAndon,
644 AndonCord,
647}
648
649#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
674pub enum AndonState {
675 #[default]
677 Normal,
678 Warning {
680 buffer_pct: u8,
682 },
683 TraceLoss {
685 dropped_count: u64,
687 },
688}
689
690impl AndonState {
691 #[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], Self::Warning { .. } => [1.0, 0.8, 0.0, 0.3], Self::TraceLoss { .. } => [1.0, 0.0, 0.0, 0.5], }
701 }
702
703 #[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 #[must_use]
715 pub const fn is_error(&self) -> bool {
716 !matches!(self, Self::Normal)
717 }
718
719 #[must_use]
721 pub const fn has_dropped(&self) -> bool {
722 matches!(self, Self::TraceLoss { .. })
723 }
724
725 #[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
735pub const NUM_ZOBRIST_FIELDS: usize = 32;
742
743#[derive(Debug, Clone)]
771pub struct ZobristTable {
772 table: Box<[[u64; 256]; NUM_ZOBRIST_FIELDS]>,
776}
777
778impl ZobristTable {
779 #[must_use]
789 #[allow(clippy::expect_used)] pub fn new(seed: u64) -> Self {
791 let mut state = seed.max(1); let mut table = vec![[0u64; 256]; NUM_ZOBRIST_FIELDS];
794
795 for field in &mut table {
796 for value in field.iter_mut() {
797 state ^= state << 13;
799 state ^= state >> 7;
800 state ^= state << 17;
801 *value = state;
802 }
803 }
804
805 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 #[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 #[inline]
829 #[must_use]
830 #[allow(clippy::missing_const_for_fn)] pub fn update_hash(&self, hash: u64, field: usize, old_byte: u8, new_byte: u8) -> u64 {
832 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 #[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#[derive(Debug, Clone)]
865pub struct ZobristSnapshotter {
866 table: ZobristTable,
868 current_hash: u64,
870 prev_snapshot_hash: u64,
872 entropy_threshold: u32,
874 min_interval: u64,
876 max_interval: u64,
878 last_snapshot_frame: u64,
880}
881
882impl ZobristSnapshotter {
883 #[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 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 #[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 pub fn should_snapshot(&mut self, frame: u64) -> SnapshotDecision {
913 let frames_since = frame.saturating_sub(self.last_snapshot_frame);
914
915 if frames_since >= self.max_interval {
917 self.take_snapshot(frame);
918 return SnapshotDecision::FullSnapshot;
919 }
920
921 let entropy = ZobristTable::entropy(self.current_hash, self.prev_snapshot_hash);
923
924 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 #[allow(clippy::missing_const_for_fn)] fn take_snapshot(&mut self, frame: u64) {
936 self.prev_snapshot_hash = self.current_hash;
937 self.last_snapshot_frame = frame;
938 }
939
940 #[must_use]
942 pub const fn current_hash(&self) -> u64 {
943 self.current_hash
944 }
945
946 #[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, 120, 16, )
961 }
962}
963
964#[derive(Debug, Clone, Copy, PartialEq, Eq)]
973pub enum SnapshotDecision {
974 Skip,
976 DeltaSnapshot,
978 FullSnapshot,
980}
981
982#[derive(Debug, Clone)]
994pub struct AdaptiveSnapshotter {
995 pub min_interval: u64,
997 pub max_interval: u64,
999 pub entropy_threshold: u32,
1001 last_snapshot_frame: u64,
1003 prev_state_hash: [u8; 32],
1005}
1006
1007impl Default for AdaptiveSnapshotter {
1008 fn default() -> Self {
1009 Self {
1010 min_interval: 15, max_interval: 120, entropy_threshold: 64, last_snapshot_frame: 0,
1014 prev_state_hash: [0; 32],
1015 }
1016 }
1017}
1018
1019impl AdaptiveSnapshotter {
1020 #[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 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 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 let entropy = self.calculate_entropy(state_hash);
1045
1046 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 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 pub const fn reset(&mut self) {
1067 self.last_snapshot_frame = 0;
1068 self.prev_state_hash = [0; 32];
1069 }
1070
1071 #[must_use]
1073 pub const fn last_snapshot_frame(&self) -> u64 {
1074 self.last_snapshot_frame
1075 }
1076}
1077
1078#[derive(Debug, Clone, PartialEq, Eq)]
1084pub enum TraceError {
1085 BufferFull,
1087 InvalidFrameSequence,
1089 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#[derive(Debug)]
1109pub struct TraceBuffer {
1110 frames: Vec<Option<FrameRecord>>,
1112 capacity: usize,
1114 write_pos: usize,
1116 read_pos: usize,
1118 len: usize,
1120 policy: BufferPolicy,
1122 frames_dropped: u64,
1124 andon_state: AndonState,
1126}
1127
1128impl TraceBuffer {
1129 #[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 #[must_use]
1148 pub fn soft_andon(capacity: usize) -> Self {
1149 Self::new(capacity, BufferPolicy::SoftAndon)
1150 }
1151
1152 #[must_use]
1154 pub fn debug(capacity: usize) -> Self {
1155 Self::new(capacity, BufferPolicy::AndonCord)
1156 }
1157
1158 #[must_use]
1160 pub fn production(capacity: usize) -> Self {
1161 Self::new(capacity, BufferPolicy::DropOldest)
1162 }
1163
1164 pub fn push(&mut self, record: FrameRecord) -> Result<(), TraceError> {
1174 self.update_andon_state();
1176
1177 if self.len >= self.capacity {
1178 match self.policy {
1179 BufferPolicy::DropOldest => {
1180 self.read_pos = (self.read_pos + 1) % self.capacity;
1182 self.len -= 1;
1183 self.frames_dropped += 1;
1184 }
1185 BufferPolicy::SoftAndon => {
1186 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 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 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 #[must_use]
1230 pub const fn andon_state(&self) -> AndonState {
1231 self.andon_state
1232 }
1233
1234 pub fn try_push(&mut self, record: FrameRecord) -> Result<(), TraceError> {
1240 if self.len >= self.capacity && self.policy == BufferPolicy::AndonCord {
1242 return Err(TraceError::BufferFull);
1243 }
1244 self.push(record)
1245 }
1246
1247 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 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 #[must_use]
1275 pub const fn len(&self) -> usize {
1276 self.len
1277 }
1278
1279 #[must_use]
1281 pub const fn is_empty(&self) -> bool {
1282 self.len == 0
1283 }
1284
1285 #[must_use]
1287 pub const fn is_full(&self) -> bool {
1288 self.len >= self.capacity
1289 }
1290
1291 #[must_use]
1293 pub const fn capacity(&self) -> usize {
1294 self.capacity
1295 }
1296
1297 #[must_use]
1299 pub const fn frames_dropped(&self) -> u64 {
1300 self.frames_dropped
1301 }
1302
1303 #[must_use]
1305 pub const fn policy(&self) -> BufferPolicy {
1306 self.policy
1307 }
1308
1309 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 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#[derive(Debug, Clone)]
1338pub struct QueryResult {
1339 pub frame: u64,
1341 pub context_before: Vec<FrameRecord>,
1343 pub context_after: Vec<FrameRecord>,
1345}
1346
1347#[derive(Debug)]
1360pub struct TraceQuery<'a> {
1361 frames: Vec<&'a FrameRecord>,
1362}
1363
1364impl<'a> TraceQuery<'a> {
1365 #[must_use]
1367 pub fn from_buffer(buffer: &'a TraceBuffer) -> Self {
1368 Self {
1369 frames: buffer.iter().collect(),
1370 }
1371 }
1372
1373 #[must_use]
1375 pub const fn from_frames(frames: Vec<&'a FrameRecord>) -> Self {
1376 Self { frames }
1377 }
1378
1379 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 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 pub fn count_frames(&self, predicate: impl Fn(&FrameRecord) -> bool) -> usize {
1422 self.frames.iter().filter(|f| predicate(f)).count()
1423 }
1424
1425 #[must_use]
1427 pub fn frames_with_inputs(&self) -> Vec<u64> {
1428 self.find_frames(FrameRecord::has_inputs)
1429 }
1430
1431 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 #[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 #[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 #[must_use]
1464 #[allow(clippy::missing_const_for_fn)] pub fn frame_count(&self) -> usize {
1466 self.frames.len()
1467 }
1468
1469 #[must_use]
1471 pub fn first_frame(&self) -> Option<u64> {
1472 self.frames.first().map(|f| f.frame)
1473 }
1474
1475 #[must_use]
1477 pub fn last_frame(&self) -> Option<u64> {
1478 self.frames.last().map(|f| f.frame)
1479 }
1480
1481 #[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1498pub struct TraceStats {
1499 pub frame: u64,
1501 pub buffer_len: usize,
1503 pub buffer_capacity: usize,
1505 pub frames_dropped: u64,
1507 pub total_inputs: u64,
1509 pub snapshots_taken: u64,
1511 pub recording: bool,
1513 pub policy: String,
1515}
1516
1517#[derive(Debug, Clone)]
1519pub struct TracerConfig {
1520 pub buffer_capacity: usize,
1522 pub policy: BufferPolicy,
1524 pub record_state_hashes: bool,
1526 pub snapshotter: AdaptiveSnapshotter,
1528}
1529
1530impl Default for TracerConfig {
1531 fn default() -> Self {
1532 Self {
1533 buffer_capacity: 3600, policy: BufferPolicy::DropOldest,
1535 record_state_hashes: true,
1536 snapshotter: AdaptiveSnapshotter::default(),
1537 }
1538 }
1539}
1540
1541impl TracerConfig {
1542 #[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 #[must_use]
1555 pub const fn production() -> Self {
1556 Self {
1557 buffer_capacity: 7200, policy: BufferPolicy::DropOldest,
1559 record_state_hashes: false, snapshotter: AdaptiveSnapshotter::new(30, 300, 64), }
1562 }
1563}
1564
1565#[derive(Debug)]
1589pub struct GameTracer {
1590 buffer: TraceBuffer,
1592 snapshotter: AdaptiveSnapshotter,
1594 current_frame: u64,
1596 current_record: Option<FrameRecord>,
1598 record_state_hashes: bool,
1600 recording: bool,
1602 total_inputs: u64,
1604 snapshots_taken: u64,
1606}
1607
1608impl GameTracer {
1609 #[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 #[must_use]
1626 pub fn debug() -> Self {
1627 Self::new(TracerConfig::debug())
1628 }
1629
1630 #[must_use]
1632 pub fn production() -> Self {
1633 Self::new(TracerConfig::production())
1634 }
1635
1636 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 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 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 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 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 let _ = self.buffer.push(record);
1710
1711 self.current_frame += 1;
1712 decision
1713 }
1714
1715 #[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 #[allow(clippy::missing_const_for_fn)] pub fn start_recording(&mut self) {
1737 self.recording = true;
1738 }
1739
1740 pub fn stop_recording(&mut self) {
1742 self.recording = false;
1743 self.current_record = None;
1744 }
1745
1746 #[must_use]
1748 pub const fn is_recording(&self) -> bool {
1749 self.recording
1750 }
1751
1752 #[must_use]
1754 pub const fn current_frame(&self) -> u64 {
1755 self.current_frame
1756 }
1757
1758 #[must_use]
1760 pub const fn buffer(&self) -> &TraceBuffer {
1761 &self.buffer
1762 }
1763
1764 #[must_use]
1766 pub fn query(&self) -> TraceQuery<'_> {
1767 TraceQuery::from_buffer(&self.buffer)
1768 }
1769
1770 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 pub fn drain(&mut self, count: usize) -> Vec<FrameRecord> {
1782 self.buffer.drain(count)
1783 }
1784
1785 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#[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 #[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); 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 let neg = Fixed32::from_f32(-1.25);
1861 assert_eq!(neg.to_int(), -2); 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 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 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 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 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 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 let a = Fixed32::from_raw(327680); let b = Fixed32::from_raw(196608); assert_eq!((a + b).to_raw(), 524288); let product = a.mul(b);
2130 assert_eq!(product.to_raw(), 983040); let quotient = a.div(b);
2134 assert_eq!(quotient.to_raw(), 109226); }
2138
2139 #[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 #[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 assert_eq!(snap.should_snapshot(0, &hash), SnapshotDecision::Skip);
2195
2196 assert_eq!(
2198 snap.should_snapshot(50, &hash),
2199 SnapshotDecision::FullSnapshot
2200 );
2201
2202 assert_eq!(snap.should_snapshot(99, &hash), SnapshotDecision::Skip);
2204
2205 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 let hash1 = [0; 32];
2218 let _ = snap.should_snapshot(120, &hash1); let hash2 = [0; 32];
2222 assert_eq!(snap.should_snapshot(130, &hash2), SnapshotDecision::Skip);
2223
2224 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 let hash1 = [0; 32];
2238 let _ = snap.should_snapshot(120, &hash1);
2239
2240 let hash2 = [0xFF; 32];
2242 assert_eq!(snap.should_snapshot(125, &hash2), SnapshotDecision::Skip);
2243
2244 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 #[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 buf.push(FrameRecord::new(4)).unwrap();
2310
2311 assert_eq!(buf.len(), 3);
2312 assert_eq!(buf.frames_dropped(), 1);
2313
2314 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 let result = buf.push(FrameRecord::new(4));
2329 assert_eq!(result, Err(TraceError::BufferFull));
2330
2331 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 buf.push(FrameRecord::new(1)).unwrap();
2384 buf.push(FrameRecord::new(2)).unwrap();
2385 buf.push(FrameRecord::new(3)).unwrap();
2386
2387 let _ = buf.pop();
2389
2390 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 #[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 fn create_test_buffer_with_frames() -> TraceBuffer {
2494 let mut buf = TraceBuffer::new(100, BufferPolicy::DropOldest);
2495
2496 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), 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 let frames_with_input = query.find_frames(|f| f.has_inputs());
2541 assert_eq!(frames_with_input, vec![3, 5]);
2542
2543 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 let key_frames = query.frames_with_input_type(|e| matches!(e, InputEventType::KeyDown(_)));
2564 assert_eq!(key_frames, vec![3]);
2565
2566 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 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 let r1 = &results[0];
2635 assert_eq!(r1.frame, 3);
2636 assert_eq!(r1.context_before.len(), 2); assert_eq!(r1.context_after.len(), 2); let r2 = &results[1];
2641 assert_eq!(r2.frame, 5);
2642 assert_eq!(r2.context_before.len(), 2); assert_eq!(r2.context_after.len(), 0); }
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 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(); 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 #[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), 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 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 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 tracer.begin_frame();
2821 let _ = tracer.end_frame(None);
2822
2823 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 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 let hash1 = [0u8; 32];
2903 tracer.begin_frame();
2904 let decision = tracer.end_frame(Some(hash1));
2905 assert!(decision == SnapshotDecision::FullSnapshot || decision == SnapshotDecision::Skip);
2907
2908 tracer.begin_frame();
2910 let decision = tracer.end_frame(Some(hash1));
2911 assert_eq!(decision, SnapshotDecision::Skip);
2912
2913 let mut hash2 = [0u8; 32];
2915 hash2.fill(0xFF); 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); }
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 let decision = tracer.end_frame(None);
2961 assert_eq!(decision, SnapshotDecision::Skip);
2962 }
2963
2964 #[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 #[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 #[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 for i in 0..8 {
3083 buf.push(FrameRecord::new(i)).unwrap();
3084 }
3085
3086 buf.push(FrameRecord::new(8)).unwrap();
3089
3090 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 for i in 0..3 {
3103 buf.push(FrameRecord::new(i)).unwrap();
3104 }
3105
3106 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 for i in 0..10 {
3119 buf.push(FrameRecord::new(i)).unwrap();
3120 }
3121
3122 assert_eq!(buf.len(), 3);
3124 assert_eq!(buf.frames_dropped(), 7);
3125 assert_eq!(buf.andon_state().dropped_count(), 7);
3126 }
3127
3128 #[test]
3133 fn test_zobrist_table_creation() {
3134 let table = ZobristTable::new(42);
3135 let hash = table.hash_bytes(&[0, 1, 2, 3]);
3137 assert!(hash != 0 || hash == 0); }
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 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 let full_hash = table.hash_bytes(&data);
3166
3167 let updated_hash = table.update_hash(full_hash, 0, 1, 5);
3169
3170 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 assert_eq!(ZobristTable::entropy(hash1, hash2), 64);
3182
3183 assert_eq!(ZobristTable::entropy(hash1, hash1), 0);
3185
3186 assert_eq!(ZobristTable::entropy(0, 1), 1);
3188 }
3189
3190 #[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 for frame in 0..49 {
3212 assert_eq!(snap.should_snapshot(frame), SnapshotDecision::Skip);
3213 }
3214
3215 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 snap.initialize(&[0, 0, 0, 0]);
3225
3226 for frame in 1..5 {
3228 assert_eq!(snap.should_snapshot(frame), SnapshotDecision::Skip);
3229 }
3230
3231 for i in 0..32 {
3233 snap.on_state_change(i, 0, 255);
3234 }
3235
3236 let decision = snap.should_snapshot(10);
3239 assert!(matches!(
3240 decision,
3241 SnapshotDecision::DeltaSnapshot | SnapshotDecision::Skip
3242 ));
3243 }
3244
3245 #[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 }