1use crate::event::InputEvent;
19use crate::fuzzer::Seed;
20use std::collections::hash_map::DefaultHasher;
21use std::hash::{Hash, Hasher};
22
23#[derive(Debug, Clone, Copy)]
25pub struct SimulationConfig {
26 pub seed: u64,
28 pub duration_frames: u64,
30 pub fps: u32,
32 pub max_entities: usize,
34 pub record_states: bool,
36}
37
38impl Default for SimulationConfig {
39 fn default() -> Self {
40 Self {
41 seed: 0,
42 duration_frames: 3600, fps: 60,
44 max_entities: 2000,
45 record_states: false,
46 }
47 }
48}
49
50impl SimulationConfig {
51 #[must_use]
53 pub const fn new(seed: u64, duration_frames: u64) -> Self {
54 Self {
55 seed,
56 duration_frames,
57 fps: 60,
58 max_entities: 2000,
59 record_states: false,
60 }
61 }
62
63 #[must_use]
65 pub const fn with_seed(mut self, seed: u64) -> Self {
66 self.seed = seed;
67 self
68 }
69
70 #[must_use]
72 pub const fn with_duration(mut self, frames: u64) -> Self {
73 self.duration_frames = frames;
74 self
75 }
76
77 #[must_use]
79 pub const fn with_state_recording(mut self, enabled: bool) -> Self {
80 self.record_states = enabled;
81 self
82 }
83
84 #[must_use]
86 pub const fn as_seed(&self) -> Seed {
87 Seed::from_u64(self.seed)
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct RecordedFrame {
94 pub frame: u64,
96 pub inputs: Vec<InputEvent>,
98 pub state_hash: u64,
100}
101
102#[derive(Debug, Clone)]
104pub struct SimulationRecording {
105 pub config: SimulationConfig,
107 pub frames: Vec<RecordedFrame>,
109 pub final_state_hash: u64,
111 pub total_frames: u64,
113 pub completed: bool,
115 pub error: Option<String>,
117}
118
119impl SimulationRecording {
120 #[must_use]
122 #[allow(clippy::missing_const_for_fn)] pub fn new(config: SimulationConfig) -> Self {
124 Self {
125 config,
126 frames: Vec::new(),
127 final_state_hash: 0,
128 total_frames: 0,
129 completed: false,
130 error: None,
131 }
132 }
133
134 pub fn add_frame(&mut self, frame: RecordedFrame) {
136 self.total_frames = frame.frame + 1;
137 self.final_state_hash = frame.state_hash;
138 self.frames.push(frame);
139 }
140
141 pub const fn mark_completed(&mut self) {
143 self.completed = true;
144 }
145
146 #[allow(clippy::missing_const_for_fn)] pub fn mark_failed(&mut self, error: &str) {
149 self.completed = false;
150 self.error = Some(error.to_string());
151 }
152
153 #[must_use]
155 #[allow(clippy::cast_precision_loss)]
156 pub fn duration_seconds(&self) -> f64 {
157 self.total_frames as f64 / f64::from(self.config.fps)
158 }
159
160 #[must_use]
162 pub const fn matches(&self, other: &Self) -> bool {
163 self.final_state_hash == other.final_state_hash && self.total_frames == other.total_frames
164 }
165}
166
167#[derive(Debug, Clone)]
169pub struct ReplayResult {
170 pub final_state_hash: u64,
172 pub frames_replayed: u64,
174 pub determinism_verified: bool,
176 pub divergence_frame: Option<u64>,
178 pub error: Option<String>,
180}
181
182impl ReplayResult {
183 #[must_use]
185 pub const fn success(final_state_hash: u64, frames_replayed: u64) -> Self {
186 Self {
187 final_state_hash,
188 frames_replayed,
189 determinism_verified: true,
190 divergence_frame: None,
191 error: None,
192 }
193 }
194
195 #[must_use]
197 pub fn diverged(divergence_frame: u64, expected_hash: u64, actual_hash: u64) -> Self {
198 Self {
199 final_state_hash: actual_hash,
200 frames_replayed: divergence_frame,
201 determinism_verified: false,
202 divergence_frame: Some(divergence_frame),
203 error: Some(format!(
204 "State diverged at frame {divergence_frame}: expected hash {expected_hash}, got {actual_hash}"
205 )),
206 }
207 }
208}
209
210#[derive(Debug, Clone, Default)]
212pub struct SimulatedGameState {
213 pub frame: u64,
215 pub player_x: f32,
217 pub player_y: f32,
219 pub health: i32,
221 pub score: i32,
223 pub entity_count: usize,
225 random_state: u64,
227}
228
229impl SimulatedGameState {
230 #[must_use]
232 pub const fn new(seed: u64) -> Self {
233 Self {
234 frame: 0,
235 player_x: 400.0,
236 player_y: 300.0,
237 health: 100,
238 score: 0,
239 entity_count: 1,
240 random_state: seed,
241 }
242 }
243
244 pub fn update(&mut self, inputs: &[InputEvent]) {
246 self.frame += 1;
247
248 for input in inputs {
250 match input {
251 InputEvent::Touch { x, y, .. } | InputEvent::MouseClick { x, y } => {
252 let dx = x - self.player_x;
254 let dy = y - self.player_y;
255 let dist = dx.hypot(dy);
256 if dist > 1.0 {
257 self.player_x += dx / dist * 5.0;
258 self.player_y += dy / dist * 5.0;
259 }
260 }
261 InputEvent::KeyPress { key } => {
262 match key.as_str() {
264 "ArrowUp" | "KeyW" => self.player_y -= 5.0,
265 "ArrowDown" | "KeyS" => self.player_y += 5.0,
266 "ArrowLeft" | "KeyA" => self.player_x -= 5.0,
267 "ArrowRight" | "KeyD" => self.player_x += 5.0,
268 "Space" => self.score += 10, _ => {}
270 }
271 }
272 _ => {}
273 }
274 }
275
276 self.random_state = self.random_state.wrapping_mul(6_364_136_223_846_793_005);
278 self.random_state = self.random_state.wrapping_add(1_442_695_040_888_963_407);
279
280 if self.random_state % 100 < 5 && self.entity_count < 1000 {
282 self.entity_count += 1;
283 }
284 if self.random_state % 100 > 95 && self.entity_count > 1 {
285 self.entity_count -= 1;
286 }
287
288 self.player_x = self.player_x.clamp(0.0, 800.0);
290 self.player_y = self.player_y.clamp(0.0, 600.0);
291 }
292
293 #[must_use]
295 pub fn compute_hash(&self) -> u64 {
296 let mut hasher = DefaultHasher::new();
297 self.frame.hash(&mut hasher);
298 self.player_x.to_bits().hash(&mut hasher);
299 self.player_y.to_bits().hash(&mut hasher);
300 self.health.hash(&mut hasher);
301 self.score.hash(&mut hasher);
302 self.entity_count.hash(&mut hasher);
303 self.random_state.hash(&mut hasher);
304 hasher.finish()
305 }
306
307 #[must_use]
309 pub const fn is_valid(&self) -> bool {
310 self.health >= 0 && self.entity_count < 2000
311 }
312}
313
314#[must_use]
323pub fn run_simulation<F>(config: SimulationConfig, mut input_generator: F) -> SimulationRecording
324where
325 F: FnMut(u64) -> Vec<InputEvent>,
326{
327 let mut recording = SimulationRecording::new(config);
328 let mut state = SimulatedGameState::new(config.seed);
329
330 for frame in 0..config.duration_frames {
331 let inputs = input_generator(frame);
333
334 state.update(&inputs);
336
337 if !state.is_valid() {
339 recording.mark_failed(&format!("Invariant violation at frame {frame}"));
340 return recording;
341 }
342
343 if state.entity_count >= config.max_entities {
345 recording.mark_failed(&format!(
346 "Entity explosion at frame {frame}: {} entities",
347 state.entity_count
348 ));
349 return recording;
350 }
351
352 let recorded_frame = RecordedFrame {
354 frame,
355 inputs,
356 state_hash: state.compute_hash(),
357 };
358 recording.add_frame(recorded_frame);
359 }
360
361 recording.mark_completed();
362 recording
363}
364
365#[must_use]
373pub fn run_replay(recording: &SimulationRecording) -> ReplayResult {
374 let mut state = SimulatedGameState::new(recording.config.seed);
375
376 for recorded_frame in &recording.frames {
377 state.update(&recorded_frame.inputs);
379
380 let current_hash = state.compute_hash();
382 if current_hash != recorded_frame.state_hash {
383 return ReplayResult::diverged(
384 recorded_frame.frame,
385 recorded_frame.state_hash,
386 current_hash,
387 );
388 }
389 }
390
391 ReplayResult::success(state.compute_hash(), recording.total_frames)
392}
393
394#[derive(Debug, Clone)]
396pub struct RandomWalkAgent {
397 state: u64,
398}
399
400impl RandomWalkAgent {
401 #[must_use]
403 pub const fn new(seed: Seed) -> Self {
404 Self {
405 state: seed.value(),
406 }
407 }
408
409 pub fn next_inputs(&mut self) -> Vec<InputEvent> {
411 self.state ^= self.state << 13;
413 self.state ^= self.state >> 7;
414 self.state ^= self.state << 17;
415
416 let direction = self.state % 5;
417 let key = match direction {
418 0 => "ArrowUp",
419 1 => "ArrowDown",
420 2 => "ArrowLeft",
421 3 => "ArrowRight",
422 _ => "Space",
423 };
424
425 vec![InputEvent::key_press(key)]
426 }
427}
428
429#[cfg(test)]
430#[allow(clippy::unwrap_used, clippy::expect_used)]
431mod tests {
432 use super::*;
433
434 mod config_tests {
435 use super::*;
436
437 #[test]
438 fn test_config_default() {
439 let config = SimulationConfig::default();
440 assert_eq!(config.duration_frames, 3600);
441 assert_eq!(config.fps, 60);
442 assert_eq!(config.max_entities, 2000);
443 }
444
445 #[test]
446 fn test_config_builder() {
447 let config = SimulationConfig::default()
448 .with_seed(42)
449 .with_duration(1000)
450 .with_state_recording(true);
451
452 assert_eq!(config.seed, 42);
453 assert_eq!(config.duration_frames, 1000);
454 assert!(config.record_states);
455 }
456
457 #[test]
458 fn test_config_as_seed() {
459 let config = SimulationConfig::new(12345, 100);
460 assert_eq!(config.as_seed().value(), 12345);
461 }
462 }
463
464 mod game_state_tests {
465 use super::*;
466
467 #[test]
468 fn test_game_state_initial() {
469 let state = SimulatedGameState::new(42);
470 assert_eq!(state.frame, 0);
471 assert_eq!(state.health, 100);
472 assert_eq!(state.score, 0);
473 assert!(state.is_valid());
474 }
475
476 #[test]
477 fn test_game_state_deterministic() {
478 let mut state1 = SimulatedGameState::new(42);
479 let mut state2 = SimulatedGameState::new(42);
480
481 let inputs = vec![InputEvent::key_press("ArrowUp")];
482
483 for _ in 0..100 {
484 state1.update(&inputs);
485 state2.update(&inputs);
486 }
487
488 assert_eq!(state1.compute_hash(), state2.compute_hash());
489 }
490
491 #[test]
492 fn test_game_state_movement() {
493 let mut state = SimulatedGameState::new(0);
494 let initial_y = state.player_y;
495
496 state.update(&[InputEvent::key_press("ArrowUp")]);
497
498 assert!(state.player_y < initial_y, "Player should move up");
499 }
500
501 #[test]
502 fn test_game_state_hash_changes() {
503 let mut state = SimulatedGameState::new(42);
504 let initial_hash = state.compute_hash();
505
506 state.update(&[InputEvent::key_press("Space")]);
507 let new_hash = state.compute_hash();
508
509 assert_ne!(initial_hash, new_hash, "Hash should change after update");
510 }
511 }
512
513 mod recording_tests {
514 use super::*;
515
516 #[test]
517 fn test_recording_new() {
518 let config = SimulationConfig::default();
519 let recording = SimulationRecording::new(config);
520
521 assert!(!recording.completed);
522 assert!(recording.frames.is_empty());
523 assert_eq!(recording.total_frames, 0);
524 }
525
526 #[test]
527 fn test_recording_add_frame() {
528 let mut recording = SimulationRecording::new(SimulationConfig::default());
529
530 recording.add_frame(RecordedFrame {
531 frame: 0,
532 inputs: vec![],
533 state_hash: 12345,
534 });
535
536 assert_eq!(recording.total_frames, 1);
537 assert_eq!(recording.final_state_hash, 12345);
538 }
539
540 #[test]
541 fn test_recording_duration() {
542 let config = SimulationConfig::default();
543 let mut recording = SimulationRecording::new(config);
544
545 for i in 0..60 {
546 recording.add_frame(RecordedFrame {
547 frame: i,
548 inputs: vec![],
549 state_hash: 0,
550 });
551 }
552
553 assert!((recording.duration_seconds() - 1.0).abs() < 0.01);
554 }
555 }
556
557 mod simulation_tests {
558 use super::*;
559
560 #[test]
561 fn test_run_simulation_completes() {
562 let config = SimulationConfig::new(42, 100);
563
564 let recording = run_simulation(config, |_frame| vec![]);
565
566 assert!(recording.completed);
567 assert_eq!(recording.total_frames, 100);
568 }
569
570 #[test]
571 fn test_simulation_deterministic() {
572 let config1 = SimulationConfig::new(42, 100);
573 let config2 = SimulationConfig::new(42, 100);
574
575 let recording1 = run_simulation(config1, |_| vec![InputEvent::key_press("Space")]);
576 let recording2 = run_simulation(config2, |_| vec![InputEvent::key_press("Space")]);
577
578 assert!(
579 recording1.matches(&recording2),
580 "Same seed should produce same result"
581 );
582 }
583
584 #[test]
585 fn test_simulation_different_seeds() {
586 let config1 = SimulationConfig::new(1, 100);
587 let config2 = SimulationConfig::new(2, 100);
588
589 let recording1 = run_simulation(config1, |_| vec![]);
590 let recording2 = run_simulation(config2, |_| vec![]);
591
592 assert!(
593 !recording1.matches(&recording2),
594 "Different seeds should produce different results"
595 );
596 }
597 }
598
599 mod replay_tests {
600 use super::*;
601
602 #[test]
603 fn test_replay_verifies_determinism() {
604 let config = SimulationConfig::new(42, 100);
605 let recording = run_simulation(config, |frame| {
606 if frame % 10 == 0 {
607 vec![InputEvent::key_press("Space")]
608 } else {
609 vec![]
610 }
611 });
612
613 let replay_result = run_replay(&recording);
614
615 assert!(
616 replay_result.determinism_verified,
617 "Replay should verify determinism"
618 );
619 assert_eq!(replay_result.final_state_hash, recording.final_state_hash);
620 }
621
622 #[test]
623 fn test_replay_full_session() {
624 let config = SimulationConfig::new(42, 3600);
626
627 let recording = run_simulation(config, |frame| {
628 let key = match frame % 5 {
630 0 => "ArrowUp",
631 1 => "ArrowRight",
632 2 => "ArrowDown",
633 3 => "ArrowLeft",
634 _ => "Space",
635 };
636 vec![InputEvent::key_press(key)]
637 });
638
639 assert!(recording.completed);
640
641 let replay_result = run_replay(&recording);
642 assert!(
643 replay_result.determinism_verified,
644 "Full session replay should be deterministic"
645 );
646 }
647 }
648
649 mod agent_tests {
650 use super::*;
651
652 #[test]
653 fn test_random_walk_agent_deterministic() {
654 let mut agent1 = RandomWalkAgent::new(Seed::from_u64(42));
655 let mut agent2 = RandomWalkAgent::new(Seed::from_u64(42));
656
657 for _ in 0..100 {
658 let inputs1 = agent1.next_inputs();
659 let inputs2 = agent2.next_inputs();
660
661 assert_eq!(inputs1.len(), inputs2.len());
662 }
663 }
664
665 #[test]
666 fn test_random_walk_simulation() {
667 let seed = Seed::from_u64(12345);
668 let mut agent = RandomWalkAgent::new(seed);
669
670 let config = SimulationConfig::new(seed.value(), 1000);
671 let recording = run_simulation(config, |_| agent.next_inputs());
672
673 assert!(recording.completed);
674
675 let mut agent2 = RandomWalkAgent::new(seed);
677 let mut recording2 =
678 SimulationRecording::new(SimulationConfig::new(seed.value(), 1000));
679 let mut state = SimulatedGameState::new(seed.value());
680
681 for frame in 0..1000 {
682 let inputs = agent2.next_inputs();
683 state.update(&inputs);
684 recording2.add_frame(RecordedFrame {
685 frame,
686 inputs,
687 state_hash: state.compute_hash(),
688 });
689 }
690
691 assert!(
692 recording.matches(&recording2),
693 "Replay with same agent should match"
694 );
695 }
696 }
697
698 mod additional_coverage_tests {
699 use super::*;
700
701 #[test]
702 fn test_recording_mark_failed() {
703 let mut recording = SimulationRecording::new(SimulationConfig::default());
704 recording.mark_failed("Test error");
705
706 assert!(!recording.completed);
707 assert_eq!(recording.error, Some("Test error".to_string()));
708 }
709
710 #[test]
711 fn test_recording_mark_completed() {
712 let mut recording = SimulationRecording::new(SimulationConfig::default());
713 recording.mark_completed();
714
715 assert!(recording.completed);
716 }
717
718 #[test]
719 fn test_replay_result_diverged() {
720 let result = ReplayResult::diverged(50, 12345, 67890);
721
722 assert!(!result.determinism_verified);
723 assert_eq!(result.divergence_frame, Some(50));
724 assert_eq!(result.final_state_hash, 67890);
725 assert!(result.error.is_some());
726 assert!(result.error.unwrap().contains("diverged at frame 50"));
727 }
728
729 #[test]
730 fn test_game_state_touch_input() {
731 let mut state = SimulatedGameState::new(0);
732 state.player_x = 100.0;
733 state.player_y = 100.0;
734
735 state.update(&[InputEvent::Touch { x: 200.0, y: 100.0 }]);
737
738 assert!(state.player_x > 100.0, "Player should move toward touch");
739 }
740
741 #[test]
742 fn test_game_state_mouse_click_input() {
743 let mut state = SimulatedGameState::new(0);
744 state.player_x = 100.0;
745 state.player_y = 100.0;
746
747 state.update(&[InputEvent::MouseClick { x: 100.0, y: 200.0 }]);
749
750 assert!(state.player_y > 100.0, "Player should move toward click");
751 }
752
753 #[test]
754 fn test_game_state_touch_close_no_move() {
755 let mut state = SimulatedGameState::new(0);
756 state.player_x = 100.0;
757 state.player_y = 100.0;
758 let initial_x = state.player_x;
759 let initial_y = state.player_y;
760
761 state.update(&[InputEvent::Touch { x: 100.5, y: 100.5 }]);
763
764 assert!(
766 (state.player_x - initial_x).abs() < 6.0,
767 "Player should barely move"
768 );
769 assert!(
770 (state.player_y - initial_y).abs() < 6.0,
771 "Player should barely move"
772 );
773 }
774
775 #[test]
776 fn test_game_state_movement_keys() {
777 let mut state = SimulatedGameState::new(0);
778 state.player_x = 400.0;
779 state.player_y = 300.0;
780
781 let initial_x = state.player_x;
783 state.update(&[InputEvent::key_press("ArrowRight")]);
784 assert!(state.player_x > initial_x, "ArrowRight should move right");
785
786 let initial_x = state.player_x;
787 state.update(&[InputEvent::key_press("ArrowLeft")]);
788 assert!(state.player_x < initial_x, "ArrowLeft should move left");
789
790 let initial_y = state.player_y;
791 state.update(&[InputEvent::key_press("ArrowDown")]);
792 assert!(state.player_y > initial_y, "ArrowDown should move down");
793
794 let initial_x = state.player_x;
796 state.update(&[InputEvent::key_press("KeyD")]);
797 assert!(state.player_x > initial_x, "KeyD should move right");
798
799 let initial_y = state.player_y;
800 state.update(&[InputEvent::key_press("KeyW")]);
801 assert!(state.player_y < initial_y, "KeyW should move up");
802
803 let initial_y = state.player_y;
804 state.update(&[InputEvent::key_press("KeyS")]);
805 assert!(state.player_y > initial_y, "KeyS should move down");
806
807 let initial_x = state.player_x;
808 state.update(&[InputEvent::key_press("KeyA")]);
809 assert!(state.player_x < initial_x, "KeyA should move left");
810 }
811
812 #[test]
813 fn test_game_state_unknown_key() {
814 let mut state = SimulatedGameState::new(0);
815 state.player_x = 400.0;
816 state.player_y = 300.0;
817 let initial_x = state.player_x;
818 let initial_y = state.player_y;
819
820 state.update(&[InputEvent::key_press("Unknown")]);
821
822 assert_eq!(state.frame, 1);
824 assert!((state.player_x - initial_x).abs() < 0.1);
826 assert!((state.player_y - initial_y).abs() < 0.1);
827 }
828
829 #[test]
830 fn test_game_state_clamp_bounds() {
831 let mut state = SimulatedGameState::new(0);
832
833 state.player_x = 0.0;
835 state.player_y = 0.0;
836
837 for _ in 0..100 {
839 state.update(&[InputEvent::key_press("ArrowUp")]);
840 state.update(&[InputEvent::key_press("ArrowLeft")]);
841 }
842
843 assert!(state.player_x >= 0.0, "X should be clamped at 0");
844 assert!(state.player_y >= 0.0, "Y should be clamped at 0");
845
846 state.player_x = 800.0;
848 state.player_y = 600.0;
849
850 for _ in 0..100 {
851 state.update(&[InputEvent::key_press("ArrowDown")]);
852 state.update(&[InputEvent::key_press("ArrowRight")]);
853 }
854
855 assert!(state.player_x <= 800.0, "X should be clamped at 800");
856 assert!(state.player_y <= 600.0, "Y should be clamped at 600");
857 }
858 }
859
860 mod prop_tests {
861 use super::*;
862 use proptest::prelude::*;
863
864 proptest! {
865 #[test]
866 fn prop_simulation_always_completes(seed in 0u64..10000, frames in 1u64..500) {
867 let config = SimulationConfig::new(seed, frames);
868 let recording = run_simulation(config, |_| vec![]);
869
870 prop_assert!(recording.completed);
871 prop_assert_eq!(recording.total_frames, frames);
872 }
873
874 #[test]
875 fn prop_simulation_deterministic(seed in 0u64..10000) {
876 let config1 = SimulationConfig::new(seed, 100);
877 let config2 = SimulationConfig::new(seed, 100);
878
879 let rec1 = run_simulation(config1, |f| {
880 if f % 2 == 0 { vec![InputEvent::key_press("Space")] } else { vec![] }
881 });
882 let rec2 = run_simulation(config2, |f| {
883 if f % 2 == 0 { vec![InputEvent::key_press("Space")] } else { vec![] }
884 });
885
886 prop_assert!(rec1.matches(&rec2));
887 }
888
889 #[test]
890 fn prop_game_state_always_valid(seed in 0u64..10000, frames in 1usize..1000) {
891 let mut state = SimulatedGameState::new(seed);
892
893 for _ in 0..frames {
894 state.update(&[InputEvent::key_press("Space")]);
895 prop_assert!(state.is_valid());
896 }
897 }
898
899 #[test]
900 fn prop_replay_verifies(seed in 0u64..1000) {
901 let config = SimulationConfig::new(seed, 100);
902 let recording = run_simulation(config, |_| vec![]);
903 let replay = run_replay(&recording);
904
905 prop_assert!(replay.determinism_verified);
906 prop_assert_eq!(replay.final_state_hash, recording.final_state_hash);
907 }
908 }
909 }
910}