Skip to main content

jugar_probar/
fuzzer.rs

1//! Monte Carlo fuzzing for game input testing.
2//!
3//! Per spec Section 6.4: Monte Carlo simulation for edge case discovery.
4//!
5//! # Example
6//!
7//! ```ignore
8//! let mut fuzzer = InputFuzzer::new(Seed::from_u64(12345));
9//! for _ in 0..10_000 {
10//!     let inputs = fuzzer.generate_valid_inputs();
11//!     game.update(inputs);
12//! }
13//! ```
14
15use crate::event::InputEvent;
16
17/// Deterministic seed for reproducible fuzzing
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub struct Seed(u64);
20
21impl Seed {
22    /// Create a seed from a u64 value
23    #[must_use]
24    pub const fn from_u64(value: u64) -> Self {
25        Self(value)
26    }
27
28    /// Get the raw seed value
29    #[must_use]
30    pub const fn value(self) -> u64 {
31        self.0
32    }
33}
34
35/// Simple xorshift64 PRNG for deterministic fuzzing
36#[derive(Debug, Clone)]
37struct Xorshift64 {
38    state: u64,
39}
40
41impl Xorshift64 {
42    const fn new(seed: Seed) -> Self {
43        // Ensure non-zero state
44        let state = if seed.0 == 0 { 1 } else { seed.0 };
45        Self { state }
46    }
47
48    const fn next(&mut self) -> u64 {
49        let mut x = self.state;
50        x ^= x << 13;
51        x ^= x >> 7;
52        x ^= x << 17;
53        self.state = x;
54        x
55    }
56
57    #[allow(clippy::cast_precision_loss)]
58    fn next_f32(&mut self) -> f32 {
59        (self.next() as f32) / (u64::MAX as f32)
60    }
61
62    const fn next_range(&mut self, min: u64, max: u64) -> u64 {
63        if min >= max {
64            return min;
65        }
66        min + (self.next() % (max - min))
67    }
68
69    #[allow(clippy::suboptimal_flops)]
70    fn next_f32_range(&mut self, min: f32, max: f32) -> f32 {
71        min + self.next_f32() * (max - min)
72    }
73}
74
75/// Configuration for input fuzzing
76#[derive(Debug, Clone)]
77pub struct FuzzerConfig {
78    /// Viewport width for coordinate generation
79    pub viewport_width: f32,
80    /// Viewport height for coordinate generation
81    pub viewport_height: f32,
82    /// Probability of generating a touch input (0.0-1.0)
83    pub touch_probability: f32,
84    /// Probability of generating a key input (0.0-1.0)
85    pub key_probability: f32,
86    /// Probability of generating a mouse input (0.0-1.0)
87    pub mouse_probability: f32,
88    /// Maximum swipe distance
89    pub max_swipe_distance: f32,
90    /// Maximum hold duration in ms
91    pub max_hold_duration: u32,
92}
93
94impl Default for FuzzerConfig {
95    fn default() -> Self {
96        Self {
97            viewport_width: 800.0,
98            viewport_height: 600.0,
99            touch_probability: 0.5,
100            key_probability: 0.3,
101            mouse_probability: 0.2,
102            max_swipe_distance: 200.0,
103            max_hold_duration: 1000,
104        }
105    }
106}
107
108impl FuzzerConfig {
109    /// Set viewport dimensions
110    #[must_use]
111    pub const fn with_viewport(mut self, width: f32, height: f32) -> Self {
112        self.viewport_width = width;
113        self.viewport_height = height;
114        self
115    }
116}
117
118/// Input fuzzer for Monte Carlo game testing
119///
120/// Generates random but valid game inputs for stress testing.
121#[derive(Debug, Clone)]
122pub struct InputFuzzer {
123    rng: Xorshift64,
124    config: FuzzerConfig,
125    inputs_generated: u64,
126}
127
128impl InputFuzzer {
129    /// Create a new fuzzer with the given seed
130    #[must_use]
131    pub fn new(seed: Seed) -> Self {
132        Self {
133            rng: Xorshift64::new(seed),
134            config: FuzzerConfig::default(),
135            inputs_generated: 0,
136        }
137    }
138
139    /// Create a fuzzer with custom configuration
140    #[must_use]
141    pub const fn with_config(seed: Seed, config: FuzzerConfig) -> Self {
142        Self {
143            rng: Xorshift64::new(seed),
144            config,
145            inputs_generated: 0,
146        }
147    }
148
149    /// Generate a batch of valid random inputs
150    #[must_use]
151    pub fn generate_valid_inputs(&mut self) -> Vec<InputEvent> {
152        let mut inputs = Vec::new();
153        let roll = self.rng.next_f32();
154
155        if roll < self.config.touch_probability {
156            inputs.push(self.generate_touch_input());
157        } else if roll < self.config.touch_probability + self.config.key_probability {
158            inputs.push(self.generate_key_input());
159        } else {
160            inputs.push(self.generate_mouse_input());
161        }
162
163        self.inputs_generated += inputs.len() as u64;
164        inputs
165    }
166
167    /// Generate a single random touch input
168    fn generate_touch_input(&mut self) -> InputEvent {
169        let x = self.rng.next_f32_range(0.0, self.config.viewport_width);
170        let y = self.rng.next_f32_range(0.0, self.config.viewport_height);
171        InputEvent::Touch { x, y }
172    }
173
174    /// Generate a single random key input
175    #[allow(clippy::cast_possible_truncation)]
176    fn generate_key_input(&mut self) -> InputEvent {
177        const VALID_KEYS: &[&str] = &[
178            "ArrowUp",
179            "ArrowDown",
180            "ArrowLeft",
181            "ArrowRight",
182            "Space",
183            "Enter",
184            "Escape",
185            "KeyW",
186            "KeyA",
187            "KeyS",
188            "KeyD",
189        ];
190
191        // Safe: VALID_KEYS.len() is small and fits in u64/usize
192        let idx = self.rng.next_range(0, VALID_KEYS.len() as u64) as usize;
193        InputEvent::key_press(VALID_KEYS[idx])
194    }
195
196    /// Generate a single random mouse input
197    fn generate_mouse_input(&mut self) -> InputEvent {
198        let x = self.rng.next_f32_range(0.0, self.config.viewport_width);
199        let y = self.rng.next_f32_range(0.0, self.config.viewport_height);
200        InputEvent::mouse_click(x, y)
201    }
202
203    /// Get the total number of inputs generated
204    #[must_use]
205    pub const fn inputs_generated(&self) -> u64 {
206        self.inputs_generated
207    }
208
209    /// Reset the fuzzer to its initial state with a new seed
210    pub const fn reset(&mut self, seed: Seed) {
211        self.rng = Xorshift64::new(seed);
212        self.inputs_generated = 0;
213    }
214
215    /// Get the current configuration
216    #[must_use]
217    pub const fn config(&self) -> &FuzzerConfig {
218        &self.config
219    }
220}
221
222/// Invariant checker for game state validation during fuzzing
223#[derive(Debug, Clone, Default)]
224pub struct InvariantChecker {
225    checks: Vec<InvariantCheck>,
226    violations: Vec<InvariantViolation>,
227}
228
229/// A single invariant check
230#[derive(Debug, Clone)]
231pub struct InvariantCheck {
232    /// Name of the invariant
233    pub name: String,
234    /// Description of what the invariant checks
235    pub description: String,
236}
237
238impl InvariantCheck {
239    /// Create a new invariant check
240    #[must_use]
241    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
242        Self {
243            name: name.into(),
244            description: description.into(),
245        }
246    }
247}
248
249/// A violation of an invariant
250#[derive(Debug, Clone)]
251pub struct InvariantViolation {
252    /// Name of the violated invariant
253    pub invariant_name: String,
254    /// Description of the violation
255    pub message: String,
256    /// Step at which the violation occurred
257    pub step: u64,
258}
259
260impl InvariantChecker {
261    /// Create a new invariant checker
262    #[must_use]
263    pub fn new() -> Self {
264        Self::default()
265    }
266
267    /// Add an invariant check
268    pub fn add_check(&mut self, check: InvariantCheck) {
269        self.checks.push(check);
270    }
271
272    /// Record a violation
273    pub fn record_violation(&mut self, invariant_name: &str, message: &str, step: u64) {
274        self.violations.push(InvariantViolation {
275            invariant_name: invariant_name.to_string(),
276            message: message.to_string(),
277            step,
278        });
279    }
280
281    /// Check if any violations occurred
282    #[must_use]
283    pub fn has_violations(&self) -> bool {
284        !self.violations.is_empty()
285    }
286
287    /// Get all violations
288    #[must_use]
289    pub fn violations(&self) -> &[InvariantViolation] {
290        &self.violations
291    }
292
293    /// Get the number of checks
294    #[must_use]
295    pub fn check_count(&self) -> usize {
296        self.checks.len()
297    }
298
299    /// Clear all violations
300    pub fn clear_violations(&mut self) {
301        self.violations.clear();
302    }
303}
304
305/// Standard game invariants per spec Section 6.4
306pub mod standard_invariants {
307    use super::InvariantCheck;
308
309    /// Health must be non-negative
310    #[must_use]
311    pub fn health_non_negative() -> InvariantCheck {
312        InvariantCheck::new("health_non_negative", "Player health must be >= 0")
313    }
314
315    /// Entity count must not explode
316    #[must_use]
317    pub fn entity_count_bounded(max: usize) -> InvariantCheck {
318        InvariantCheck::new(
319            "entity_count_bounded",
320            format!("Entity count must be < {max}"),
321        )
322    }
323
324    /// Physics must remain stable
325    #[must_use]
326    pub fn physics_stable() -> InvariantCheck {
327        InvariantCheck::new("physics_stable", "Physics simulation must remain stable")
328    }
329
330    /// Score must be valid
331    #[must_use]
332    pub fn score_valid() -> InvariantCheck {
333        InvariantCheck::new("score_valid", "Score must be a valid number")
334    }
335
336    /// Positions must be within bounds
337    #[must_use]
338    pub fn positions_in_bounds() -> InvariantCheck {
339        InvariantCheck::new(
340            "positions_in_bounds",
341            "All positions must be within world bounds",
342        )
343    }
344}
345
346#[cfg(test)]
347#[allow(clippy::unwrap_used, clippy::expect_used)]
348mod tests {
349    use super::*;
350
351    mod seed_tests {
352        use super::*;
353
354        #[test]
355        fn test_seed_from_u64() {
356            let seed = Seed::from_u64(12345);
357            assert_eq!(seed.value(), 12345);
358        }
359
360        #[test]
361        fn test_seed_default() {
362            let seed = Seed::default();
363            assert_eq!(seed.value(), 0);
364        }
365    }
366
367    mod xorshift_tests {
368        use super::*;
369
370        #[test]
371        fn test_xorshift_deterministic() {
372            let mut rng1 = Xorshift64::new(Seed::from_u64(42));
373            let mut rng2 = Xorshift64::new(Seed::from_u64(42));
374
375            for _ in 0..100 {
376                assert_eq!(rng1.next(), rng2.next());
377            }
378        }
379
380        #[test]
381        fn test_xorshift_different_seeds() {
382            let mut rng1 = Xorshift64::new(Seed::from_u64(1));
383            let mut rng2 = Xorshift64::new(Seed::from_u64(2));
384
385            // Should produce different sequences
386            let seq1: Vec<u64> = (0..10).map(|_| rng1.next()).collect();
387            let seq2: Vec<u64> = (0..10).map(|_| rng2.next()).collect();
388            assert_ne!(seq1, seq2);
389        }
390
391        #[test]
392        fn test_xorshift_f32_range() {
393            let mut rng = Xorshift64::new(Seed::from_u64(42));
394
395            for _ in 0..1000 {
396                let value = rng.next_f32_range(10.0, 20.0);
397                assert!((10.0..20.0).contains(&value));
398            }
399        }
400
401        #[test]
402        fn test_xorshift_range() {
403            let mut rng = Xorshift64::new(Seed::from_u64(42));
404
405            for _ in 0..1000 {
406                let value = rng.next_range(5, 15);
407                assert!((5..15).contains(&value));
408            }
409        }
410    }
411
412    mod fuzzer_tests {
413        use super::*;
414
415        #[test]
416        fn test_fuzzer_deterministic() {
417            let mut fuzzer1 = InputFuzzer::new(Seed::from_u64(12345));
418            let mut fuzzer2 = InputFuzzer::new(Seed::from_u64(12345));
419
420            for _ in 0..100 {
421                let inputs1 = fuzzer1.generate_valid_inputs();
422                let inputs2 = fuzzer2.generate_valid_inputs();
423
424                assert_eq!(inputs1.len(), inputs2.len());
425            }
426        }
427
428        #[test]
429        fn test_fuzzer_generates_inputs() {
430            let mut fuzzer = InputFuzzer::new(Seed::from_u64(42));
431
432            for _ in 0..100 {
433                let inputs = fuzzer.generate_valid_inputs();
434                assert!(!inputs.is_empty());
435            }
436        }
437
438        #[test]
439        fn test_fuzzer_tracks_count() {
440            let mut fuzzer = InputFuzzer::new(Seed::from_u64(42));
441            assert_eq!(fuzzer.inputs_generated(), 0);
442
443            for _ in 0..10 {
444                let _ = fuzzer.generate_valid_inputs();
445            }
446
447            assert!(fuzzer.inputs_generated() >= 10);
448        }
449
450        #[test]
451        fn test_fuzzer_reset() {
452            let mut fuzzer = InputFuzzer::new(Seed::from_u64(42));
453
454            // Generate some inputs
455            for _ in 0..10 {
456                let _ = fuzzer.generate_valid_inputs();
457            }
458
459            // Reset
460            fuzzer.reset(Seed::from_u64(42));
461            assert_eq!(fuzzer.inputs_generated(), 0);
462        }
463
464        #[test]
465        fn test_fuzzer_with_config() {
466            let config = FuzzerConfig::default().with_viewport(1920.0, 1080.0);
467            let fuzzer = InputFuzzer::with_config(Seed::from_u64(42), config);
468
469            assert!((fuzzer.config().viewport_width - 1920.0).abs() < f32::EPSILON);
470            assert!((fuzzer.config().viewport_height - 1080.0).abs() < f32::EPSILON);
471        }
472
473        #[test]
474        fn test_fuzzer_generates_all_input_types() {
475            let mut fuzzer = InputFuzzer::new(Seed::from_u64(42));
476            let mut has_touch = false;
477            let mut has_key = false;
478            let mut has_mouse = false;
479
480            // Generate many inputs to cover all types
481            for _ in 0..1000 {
482                let inputs = fuzzer.generate_valid_inputs();
483                for input in inputs {
484                    match input {
485                        InputEvent::Touch { .. } => has_touch = true,
486                        InputEvent::KeyPress { .. } => has_key = true,
487                        InputEvent::MouseClick { .. } => has_mouse = true,
488                        _ => {}
489                    }
490                }
491            }
492
493            assert!(has_touch, "Should generate touch inputs");
494            assert!(has_key, "Should generate key inputs");
495            assert!(has_mouse, "Should generate mouse inputs");
496        }
497
498        #[test]
499        fn test_fuzzer_touch_within_viewport() {
500            let config = FuzzerConfig::default().with_viewport(800.0, 600.0);
501            let mut fuzzer = InputFuzzer::with_config(Seed::from_u64(42), config);
502
503            for _ in 0..1000 {
504                let inputs = fuzzer.generate_valid_inputs();
505                for input in inputs {
506                    if let InputEvent::Touch { x, y, .. } = input {
507                        assert!(
508                            (0.0..=800.0).contains(&x),
509                            "Touch x={x} should be within viewport"
510                        );
511                        assert!(
512                            (0.0..=600.0).contains(&y),
513                            "Touch y={y} should be within viewport"
514                        );
515                    }
516                }
517            }
518        }
519    }
520
521    mod invariant_tests {
522        use super::*;
523
524        #[test]
525        fn test_invariant_checker_new() {
526            let checker = InvariantChecker::new();
527            assert!(!checker.has_violations());
528            assert_eq!(checker.check_count(), 0);
529        }
530
531        #[test]
532        fn test_add_check() {
533            let mut checker = InvariantChecker::new();
534            checker.add_check(standard_invariants::health_non_negative());
535            assert_eq!(checker.check_count(), 1);
536        }
537
538        #[test]
539        fn test_record_violation() {
540            let mut checker = InvariantChecker::new();
541            checker.record_violation("test", "Test violation", 42);
542
543            assert!(checker.has_violations());
544            assert_eq!(checker.violations().len(), 1);
545            assert_eq!(checker.violations()[0].step, 42);
546        }
547
548        #[test]
549        fn test_clear_violations() {
550            let mut checker = InvariantChecker::new();
551            checker.record_violation("test", "Test violation", 1);
552            assert!(checker.has_violations());
553
554            checker.clear_violations();
555            assert!(!checker.has_violations());
556        }
557
558        #[test]
559        fn test_standard_invariants() {
560            let health = standard_invariants::health_non_negative();
561            assert_eq!(health.name, "health_non_negative");
562
563            let entities = standard_invariants::entity_count_bounded(1000);
564            assert!(entities.description.contains("1000"));
565
566            let physics = standard_invariants::physics_stable();
567            assert_eq!(physics.name, "physics_stable");
568
569            let score = standard_invariants::score_valid();
570            assert_eq!(score.name, "score_valid");
571
572            let positions = standard_invariants::positions_in_bounds();
573            assert_eq!(positions.name, "positions_in_bounds");
574        }
575    }
576
577    mod monte_carlo_simulation_tests {
578        use super::*;
579
580        #[test]
581        fn test_10k_steps_no_panic() {
582            let mut fuzzer = InputFuzzer::new(Seed::from_u64(12345));
583            let mut checker = InvariantChecker::new();
584            checker.add_check(standard_invariants::health_non_negative());
585            checker.add_check(standard_invariants::entity_count_bounded(2000));
586
587            // Simulate 10,000 steps as per spec
588            for step in 0..10_000 {
589                let inputs = fuzzer.generate_valid_inputs();
590
591                // Verify inputs are valid
592                for input in &inputs {
593                    match input {
594                        InputEvent::Touch { x, y, .. } | InputEvent::MouseClick { x, y } => {
595                            assert!(x.is_finite() && y.is_finite());
596                        }
597                        InputEvent::KeyPress { key } => {
598                            assert!(!key.is_empty());
599                        }
600                        _ => {}
601                    }
602                }
603
604                // Simulated game state checks
605                let simulated_health = 100 - (step % 100) as i32;
606                if simulated_health < 0 {
607                    checker.record_violation(
608                        "health_non_negative",
609                        "Health dropped below zero",
610                        step,
611                    );
612                }
613            }
614
615            assert_eq!(fuzzer.inputs_generated(), 10_000);
616            // In this simulation, health is always >= 0
617            assert!(!checker.has_violations());
618        }
619    }
620}