1use crate::event::InputEvent;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub struct Seed(u64);
20
21impl Seed {
22 #[must_use]
24 pub const fn from_u64(value: u64) -> Self {
25 Self(value)
26 }
27
28 #[must_use]
30 pub const fn value(self) -> u64 {
31 self.0
32 }
33}
34
35#[derive(Debug, Clone)]
37struct Xorshift64 {
38 state: u64,
39}
40
41impl Xorshift64 {
42 const fn new(seed: Seed) -> Self {
43 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#[derive(Debug, Clone)]
77pub struct FuzzerConfig {
78 pub viewport_width: f32,
80 pub viewport_height: f32,
82 pub touch_probability: f32,
84 pub key_probability: f32,
86 pub mouse_probability: f32,
88 pub max_swipe_distance: f32,
90 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 #[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#[derive(Debug, Clone)]
122pub struct InputFuzzer {
123 rng: Xorshift64,
124 config: FuzzerConfig,
125 inputs_generated: u64,
126}
127
128impl InputFuzzer {
129 #[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 #[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 #[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 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 #[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 let idx = self.rng.next_range(0, VALID_KEYS.len() as u64) as usize;
193 InputEvent::key_press(VALID_KEYS[idx])
194 }
195
196 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 #[must_use]
205 pub const fn inputs_generated(&self) -> u64 {
206 self.inputs_generated
207 }
208
209 pub const fn reset(&mut self, seed: Seed) {
211 self.rng = Xorshift64::new(seed);
212 self.inputs_generated = 0;
213 }
214
215 #[must_use]
217 pub const fn config(&self) -> &FuzzerConfig {
218 &self.config
219 }
220}
221
222#[derive(Debug, Clone, Default)]
224pub struct InvariantChecker {
225 checks: Vec<InvariantCheck>,
226 violations: Vec<InvariantViolation>,
227}
228
229#[derive(Debug, Clone)]
231pub struct InvariantCheck {
232 pub name: String,
234 pub description: String,
236}
237
238impl InvariantCheck {
239 #[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#[derive(Debug, Clone)]
251pub struct InvariantViolation {
252 pub invariant_name: String,
254 pub message: String,
256 pub step: u64,
258}
259
260impl InvariantChecker {
261 #[must_use]
263 pub fn new() -> Self {
264 Self::default()
265 }
266
267 pub fn add_check(&mut self, check: InvariantCheck) {
269 self.checks.push(check);
270 }
271
272 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 #[must_use]
283 pub fn has_violations(&self) -> bool {
284 !self.violations.is_empty()
285 }
286
287 #[must_use]
289 pub fn violations(&self) -> &[InvariantViolation] {
290 &self.violations
291 }
292
293 #[must_use]
295 pub fn check_count(&self) -> usize {
296 self.checks.len()
297 }
298
299 pub fn clear_violations(&mut self) {
301 self.violations.clear();
302 }
303}
304
305pub mod standard_invariants {
307 use super::InvariantCheck;
308
309 #[must_use]
311 pub fn health_non_negative() -> InvariantCheck {
312 InvariantCheck::new("health_non_negative", "Player health must be >= 0")
313 }
314
315 #[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 #[must_use]
326 pub fn physics_stable() -> InvariantCheck {
327 InvariantCheck::new("physics_stable", "Physics simulation must remain stable")
328 }
329
330 #[must_use]
332 pub fn score_valid() -> InvariantCheck {
333 InvariantCheck::new("score_valid", "Score must be a valid number")
334 }
335
336 #[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 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 for _ in 0..10 {
456 let _ = fuzzer.generate_valid_inputs();
457 }
458
459 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 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 for step in 0..10_000 {
589 let inputs = fuzzer.generate_valid_inputs();
590
591 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 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 assert!(!checker.has_violations());
618 }
619 }
620}