Skip to main content

jugar_probar/
simulation.rs

1//! Deterministic simulation recording and replay.
2//!
3//! Per spec Section 6.4: Deterministic simulation for regression testing.
4//!
5//! # Example
6//!
7//! ```ignore
8//! let recording = run_simulation(SimulationConfig {
9//!     seed: 42,
10//!     duration_frames: 3600, // 1 minute at 60fps
11//!     actions: Box::new(RandomWalkAgent::new()),
12//! });
13//!
14//! let replay_result = run_replay(&recording);
15//! assert_eq!(recording.final_state_hash, replay_result.final_state_hash);
16//! ```
17
18use crate::event::InputEvent;
19use crate::fuzzer::Seed;
20use std::collections::hash_map::DefaultHasher;
21use std::hash::{Hash, Hasher};
22
23/// Configuration for simulation runs
24#[derive(Debug, Clone, Copy)]
25pub struct SimulationConfig {
26    /// Seed for deterministic random generation
27    pub seed: u64,
28    /// Duration in frames (e.g., 3600 for 1 minute at 60fps)
29    pub duration_frames: u64,
30    /// Target frames per second for timing calculations
31    pub fps: u32,
32    /// Maximum entities allowed before stopping
33    pub max_entities: usize,
34    /// Whether to record full state history
35    pub record_states: bool,
36}
37
38impl Default for SimulationConfig {
39    fn default() -> Self {
40        Self {
41            seed: 0,
42            duration_frames: 3600, // 1 minute at 60fps
43            fps: 60,
44            max_entities: 2000,
45            record_states: false,
46        }
47    }
48}
49
50impl SimulationConfig {
51    /// Create a new config with the given seed and duration
52    #[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    /// Set the seed
64    #[must_use]
65    pub const fn with_seed(mut self, seed: u64) -> Self {
66        self.seed = seed;
67        self
68    }
69
70    /// Set the duration in frames
71    #[must_use]
72    pub const fn with_duration(mut self, frames: u64) -> Self {
73        self.duration_frames = frames;
74        self
75    }
76
77    /// Enable state recording
78    #[must_use]
79    pub const fn with_state_recording(mut self, enabled: bool) -> Self {
80        self.record_states = enabled;
81        self
82    }
83
84    /// Get the seed as a Seed type
85    #[must_use]
86    pub const fn as_seed(&self) -> Seed {
87        Seed::from_u64(self.seed)
88    }
89}
90
91/// A single frame's worth of recorded data
92#[derive(Debug, Clone)]
93pub struct RecordedFrame {
94    /// Frame number
95    pub frame: u64,
96    /// Input events for this frame
97    pub inputs: Vec<InputEvent>,
98    /// Hash of game state after this frame (for verification)
99    pub state_hash: u64,
100}
101
102/// A complete simulation recording
103#[derive(Debug, Clone)]
104pub struct SimulationRecording {
105    /// Configuration used for this recording
106    pub config: SimulationConfig,
107    /// All recorded frames
108    pub frames: Vec<RecordedFrame>,
109    /// Hash of the final game state
110    pub final_state_hash: u64,
111    /// Total frames recorded
112    pub total_frames: u64,
113    /// Whether the simulation completed successfully
114    pub completed: bool,
115    /// Error message if simulation failed
116    pub error: Option<String>,
117}
118
119impl SimulationRecording {
120    /// Create a new empty recording
121    #[must_use]
122    #[allow(clippy::missing_const_for_fn)] // Vec::new() not const in stable
123    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    /// Add a recorded frame
135    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    /// Mark simulation as completed
142    pub const fn mark_completed(&mut self) {
143        self.completed = true;
144    }
145
146    /// Mark simulation as failed
147    #[allow(clippy::missing_const_for_fn)] // String allocation
148    pub fn mark_failed(&mut self, error: &str) {
149        self.completed = false;
150        self.error = Some(error.to_string());
151    }
152
153    /// Get the duration in seconds
154    #[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    /// Check if recording matches another (same final state)
161    #[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/// Result of replaying a simulation
168#[derive(Debug, Clone)]
169pub struct ReplayResult {
170    /// Hash of the final state after replay
171    pub final_state_hash: u64,
172    /// Total frames replayed
173    pub frames_replayed: u64,
174    /// Whether replay matched original recording
175    pub determinism_verified: bool,
176    /// Frame where divergence occurred (if any)
177    pub divergence_frame: Option<u64>,
178    /// Error message if replay failed
179    pub error: Option<String>,
180}
181
182impl ReplayResult {
183    /// Create a successful replay result
184    #[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    /// Create a failed replay result due to divergence
196    #[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/// A simulated game state for testing
211#[derive(Debug, Clone, Default)]
212pub struct SimulatedGameState {
213    /// Current frame
214    pub frame: u64,
215    /// Player X position
216    pub player_x: f32,
217    /// Player Y position
218    pub player_y: f32,
219    /// Player health
220    pub health: i32,
221    /// Current score
222    pub score: i32,
223    /// Entity count
224    pub entity_count: usize,
225    /// Random state for determinism
226    random_state: u64,
227}
228
229impl SimulatedGameState {
230    /// Create a new game state with initial values
231    #[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    /// Update game state with inputs (deterministically)
245    pub fn update(&mut self, inputs: &[InputEvent]) {
246        self.frame += 1;
247
248        // Process inputs deterministically
249        for input in inputs {
250            match input {
251                InputEvent::Touch { x, y, .. } | InputEvent::MouseClick { x, y } => {
252                    // Move player toward touch/click
253                    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                    // Arrow keys move player
263                    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, // Action button scores
269                        _ => {}
270                    }
271                }
272                _ => {}
273            }
274        }
275
276        // Deterministic random events based on frame
277        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        // Spawn/despawn entities deterministically
281        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        // Clamp values
289        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    /// Compute a hash of the current state
294    #[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    /// Check if state is valid (invariants hold)
308    #[must_use]
309    pub const fn is_valid(&self) -> bool {
310        self.health >= 0 && self.entity_count < 2000
311    }
312}
313
314/// Run a simulation with the given configuration
315///
316/// # Arguments
317/// * `config` - Simulation configuration
318/// * `input_generator` - Function that generates inputs for each frame
319///
320/// # Returns
321/// A recording of the simulation
322#[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        // Generate inputs for this frame
332        let inputs = input_generator(frame);
333
334        // Update game state
335        state.update(&inputs);
336
337        // Check invariants
338        if !state.is_valid() {
339            recording.mark_failed(&format!("Invariant violation at frame {frame}"));
340            return recording;
341        }
342
343        // Check entity limit
344        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        // Record frame
353        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/// Replay a simulation recording and verify determinism
366///
367/// # Arguments
368/// * `recording` - The original recording to replay
369///
370/// # Returns
371/// Result of the replay including whether determinism was verified
372#[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        // Apply the same inputs
378        state.update(&recorded_frame.inputs);
379
380        // Verify state hash matches
381        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/// A random walk agent for testing
395#[derive(Debug, Clone)]
396pub struct RandomWalkAgent {
397    state: u64,
398}
399
400impl RandomWalkAgent {
401    /// Create a new random walk agent with a seed
402    #[must_use]
403    pub const fn new(seed: Seed) -> Self {
404        Self {
405            state: seed.value(),
406        }
407    }
408
409    /// Generate inputs for the next frame
410    pub fn next_inputs(&mut self) -> Vec<InputEvent> {
411        // Simple xorshift for determinism
412        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            // Per spec: 1 minute at 60fps = 3600 frames
625            let config = SimulationConfig::new(42, 3600);
626
627            let recording = run_simulation(config, |frame| {
628                // Alternate between movement and action
629                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            // Reset agent and replay
676            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            // Touch far away - should move toward it
736            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            // Click far away - should move toward it
748            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            // Touch very close - should not move (distance < 1.0)
762            state.update(&[InputEvent::Touch { x: 100.5, y: 100.5 }]);
763
764            // Account for frame update effects on random state
765            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            // Test all movement keys
782            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            // Test WASD alternatives
795            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            // Should not move from key, but state still updates
823            assert_eq!(state.frame, 1);
824            // Position unchanged from key input
825            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            // Move to edge
834            state.player_x = 0.0;
835            state.player_y = 0.0;
836
837            // Try to move past bounds
838            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            // Move to other edge
847            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}