1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
16pub enum SpeedMultiplier {
17 #[default]
19 Normal = 1,
20 Fast5x = 5,
22 Fast10x = 10,
24 Fast50x = 50,
26 Fast100x = 100,
28 Fast1000x = 1000,
30}
31
32impl SpeedMultiplier {
33 #[must_use]
35 pub const fn value(self) -> u32 {
36 match self {
37 Self::Normal => 1,
38 Self::Fast5x => 5,
39 Self::Fast10x => 10,
40 Self::Fast50x => 50,
41 Self::Fast100x => 100,
42 Self::Fast1000x => 1000,
43 }
44 }
45
46 #[must_use]
50 pub const fn requires_warning(self) -> bool {
51 matches!(self, Self::Fast50x | Self::Fast100x | Self::Fast1000x)
52 }
53
54 #[must_use]
56 pub const fn label(self) -> &'static str {
57 match self {
58 Self::Normal => "1x",
59 Self::Fast5x => "5x",
60 Self::Fast10x => "10x",
61 Self::Fast50x => "50x",
62 Self::Fast100x => "100x",
63 Self::Fast1000x => "1000x",
64 }
65 }
66
67 #[must_use]
69 pub const fn next(self) -> Self {
70 match self {
71 Self::Normal => Self::Fast5x,
72 Self::Fast5x => Self::Fast10x,
73 Self::Fast10x => Self::Fast50x,
74 Self::Fast50x => Self::Fast100x,
75 Self::Fast100x => Self::Fast1000x,
76 Self::Fast1000x => Self::Normal,
77 }
78 }
79
80 #[must_use]
82 pub const fn from_key(key: u8) -> Option<Self> {
83 match key {
84 1 => Some(Self::Normal),
85 2 => Some(Self::Fast5x),
86 3 => Some(Self::Fast10x),
87 4 => Some(Self::Fast50x),
88 5 => Some(Self::Fast100x),
89 6 => Some(Self::Fast1000x),
90 _ => None,
91 }
92 }
93
94 #[must_use]
96 pub const fn all() -> [Self; 6] {
97 [
98 Self::Normal,
99 Self::Fast5x,
100 Self::Fast10x,
101 Self::Fast50x,
102 Self::Fast100x,
103 Self::Fast1000x,
104 ]
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
112pub enum GameMode {
113 Demo,
115 #[default]
117 SinglePlayer,
118 TwoPlayer,
120}
121
122impl GameMode {
123 #[must_use]
126 pub const fn name(self) -> &'static str {
127 match self {
128 Self::Demo => "Demo",
129 Self::SinglePlayer => "SinglePlayer",
130 Self::TwoPlayer => "TwoPlayer",
131 }
132 }
133
134 #[must_use]
136 pub const fn label(self) -> &'static str {
137 match self {
138 Self::Demo => "Demo",
139 Self::SinglePlayer => "1 Player",
140 Self::TwoPlayer => "2 Player",
141 }
142 }
143
144 #[must_use]
146 pub const fn short_label(self) -> &'static str {
147 match self {
148 Self::Demo => "Demo",
149 Self::SinglePlayer => "1P",
150 Self::TwoPlayer => "2P",
151 }
152 }
153
154 #[must_use]
158 pub const fn left_is_ai(self) -> bool {
159 matches!(self, Self::Demo | Self::SinglePlayer)
160 }
161
162 #[must_use]
166 pub const fn right_is_ai(self) -> bool {
167 matches!(self, Self::Demo)
168 }
169
170 #[must_use]
175 pub const fn left_paddle_label(self) -> &'static str {
176 match self {
177 Self::Demo | Self::SinglePlayer => "AI",
178 Self::TwoPlayer => "P2 [W/S]",
179 }
180 }
181
182 #[must_use]
187 pub const fn right_paddle_label(self) -> &'static str {
188 match self {
189 Self::Demo => "AI",
190 Self::SinglePlayer | Self::TwoPlayer => "P1 [^/v]",
191 }
192 }
193
194 #[must_use]
196 pub const fn next(self) -> Self {
197 match self {
198 Self::Demo => Self::SinglePlayer,
199 Self::SinglePlayer => Self::TwoPlayer,
200 Self::TwoPlayer => Self::Demo,
201 }
202 }
203
204 #[must_use]
206 pub const fn all() -> [Self; 3] {
207 [Self::Demo, Self::SinglePlayer, Self::TwoPlayer]
208 }
209}
210
211#[derive(Debug, Clone)]
215pub struct DemoState {
216 idle_time: f64,
218 auto_engage_threshold: f64,
220 auto_engaged: bool,
222 difficulty_cycle_time: f64,
224 difficulty_cycle_period: f64,
226 left_ai_difficulty: u8,
228 right_ai_difficulty: u8,
230}
231
232impl Default for DemoState {
233 fn default() -> Self {
234 Self {
235 idle_time: 0.0,
236 auto_engage_threshold: 10.0, auto_engaged: false,
238 difficulty_cycle_time: 0.0,
239 difficulty_cycle_period: 60.0, left_ai_difficulty: 7, right_ai_difficulty: 5, }
243 }
244}
245
246impl DemoState {
247 #[must_use]
249 pub fn new(auto_engage_threshold: f64, difficulty_cycle_period: f64) -> Self {
250 Self {
251 auto_engage_threshold,
252 difficulty_cycle_period,
253 ..Default::default()
254 }
255 }
256
257 #[allow(clippy::missing_const_for_fn)] pub fn record_input(&mut self) {
260 self.idle_time = 0.0;
261 self.auto_engaged = false;
262 }
263
264 pub fn update(&mut self, dt: f64, has_input: bool) -> bool {
266 if has_input {
267 self.record_input();
268 return false;
269 }
270
271 self.idle_time += dt;
272
273 if !self.auto_engaged && self.idle_time >= self.auto_engage_threshold {
275 self.auto_engaged = true;
276 return true;
277 }
278
279 false
280 }
281
282 pub fn update_difficulty_cycle(&mut self, dt: f64) {
284 self.difficulty_cycle_time += dt;
285
286 if self.difficulty_cycle_time >= self.difficulty_cycle_period {
288 self.difficulty_cycle_time = 0.0;
289 core::mem::swap(&mut self.left_ai_difficulty, &mut self.right_ai_difficulty);
291 }
292 }
293
294 #[must_use]
296 pub const fn is_auto_engaged(&self) -> bool {
297 self.auto_engaged
298 }
299
300 #[must_use]
302 pub const fn left_difficulty(&self) -> u8 {
303 self.left_ai_difficulty
304 }
305
306 #[must_use]
308 pub const fn right_difficulty(&self) -> u8 {
309 self.right_ai_difficulty
310 }
311
312 #[must_use]
314 pub const fn idle_time(&self) -> f64 {
315 self.idle_time
316 }
317
318 #[allow(clippy::missing_const_for_fn)] pub fn reset(&mut self) {
321 self.idle_time = 0.0;
322 self.auto_engaged = false;
323 self.difficulty_cycle_time = 0.0;
324 }
325}
326
327#[derive(Debug, Clone, Default, Serialize, Deserialize)]
329pub struct PerformanceStats {
330 pub physics_updates_per_sec: u32,
332 pub render_fps: f64,
334 pub backend_name: String,
336 pub speed_multiplier: u32,
338}
339
340impl PerformanceStats {
341 #[must_use]
343 pub fn new(physics_ups: u32, render_fps: f64, backend: &str, speed: u32) -> Self {
344 Self {
345 physics_updates_per_sec: physics_ups,
346 render_fps,
347 backend_name: backend.to_string(),
348 speed_multiplier: speed,
349 }
350 }
351
352 #[must_use]
354 pub fn format_display(&self) -> String {
355 format!(
356 "Backend: {} | Physics: {}/s | Render: {:.0} FPS",
357 self.backend_name, self.physics_updates_per_sec, self.render_fps
358 )
359 }
360}
361
362#[derive(Debug, Clone)]
364pub struct Attribution {
365 pub engine_version: String,
367 pub github_url: String,
369 pub org_url: String,
371 pub model_filename: String,
373 pub model_size: u32,
375}
376
377impl Default for Attribution {
378 fn default() -> Self {
379 Self {
380 engine_version: "Jugar Engine v0.1.0".to_string(),
381 github_url: "https://github.com/paiml/jugar".to_string(),
382 org_url: "https://paiml.com".to_string(),
383 model_filename: "pong-ai-v1.apr".to_string(),
384 model_size: 491,
385 }
386 }
387}
388
389impl Attribution {
390 #[must_use]
392 pub fn github_label(&self) -> String {
393 "github.com/paiml/jugar".to_string()
394 }
395
396 #[must_use]
398 pub fn org_label(&self) -> String {
399 "paiml.com".to_string()
400 }
401
402 #[must_use]
404 pub fn model_label(&self) -> String {
405 format!("{} ({} bytes)", self.model_filename, self.model_size)
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
416 fn test_speed_multiplier_values() {
417 assert_eq!(SpeedMultiplier::Normal.value(), 1);
418 assert_eq!(SpeedMultiplier::Fast5x.value(), 5);
419 assert_eq!(SpeedMultiplier::Fast10x.value(), 10);
420 assert_eq!(SpeedMultiplier::Fast50x.value(), 50);
421 assert_eq!(SpeedMultiplier::Fast100x.value(), 100);
422 assert_eq!(SpeedMultiplier::Fast1000x.value(), 1000);
423 }
424
425 #[test]
426 fn test_speed_multiplier_warning_threshold() {
427 assert!(!SpeedMultiplier::Normal.requires_warning());
429 assert!(!SpeedMultiplier::Fast5x.requires_warning());
430 assert!(!SpeedMultiplier::Fast10x.requires_warning());
431
432 assert!(SpeedMultiplier::Fast50x.requires_warning());
434 assert!(SpeedMultiplier::Fast100x.requires_warning());
435 assert!(SpeedMultiplier::Fast1000x.requires_warning());
436 }
437
438 #[test]
439 fn test_speed_multiplier_labels() {
440 assert_eq!(SpeedMultiplier::Normal.label(), "1x");
441 assert_eq!(SpeedMultiplier::Fast5x.label(), "5x");
442 assert_eq!(SpeedMultiplier::Fast1000x.label(), "1000x");
443 }
444
445 #[test]
446 fn test_speed_multiplier_cycling() {
447 let speed = SpeedMultiplier::Normal;
448 assert_eq!(speed.next(), SpeedMultiplier::Fast5x);
449 assert_eq!(speed.next().next(), SpeedMultiplier::Fast10x);
450 assert_eq!(SpeedMultiplier::Fast1000x.next(), SpeedMultiplier::Normal);
452 }
453
454 #[test]
455 fn test_speed_multiplier_from_key() {
456 assert_eq!(SpeedMultiplier::from_key(1), Some(SpeedMultiplier::Normal));
457 assert_eq!(SpeedMultiplier::from_key(2), Some(SpeedMultiplier::Fast5x));
458 assert_eq!(
459 SpeedMultiplier::from_key(6),
460 Some(SpeedMultiplier::Fast1000x)
461 );
462 assert_eq!(SpeedMultiplier::from_key(0), None);
463 assert_eq!(SpeedMultiplier::from_key(7), None);
464 }
465
466 #[test]
467 fn test_speed_multiplier_all() {
468 let all = SpeedMultiplier::all();
469 assert_eq!(all.len(), 6);
470 assert_eq!(all[0], SpeedMultiplier::Normal);
471 assert_eq!(all[5], SpeedMultiplier::Fast1000x);
472 }
473
474 #[test]
477 fn test_game_mode_default_is_single_player() {
478 assert_eq!(GameMode::default(), GameMode::SinglePlayer);
480 }
481
482 #[test]
483 fn test_game_mode_labels() {
484 assert_eq!(GameMode::Demo.label(), "Demo");
485 assert_eq!(GameMode::SinglePlayer.label(), "1 Player");
486 assert_eq!(GameMode::TwoPlayer.label(), "2 Player");
487 }
488
489 #[test]
490 fn test_game_mode_short_labels() {
491 assert_eq!(GameMode::Demo.short_label(), "Demo");
492 assert_eq!(GameMode::SinglePlayer.short_label(), "1P");
493 assert_eq!(GameMode::TwoPlayer.short_label(), "2P");
494 }
495
496 #[test]
497 fn test_game_mode_ai_control() {
498 assert!(GameMode::Demo.left_is_ai());
500 assert!(GameMode::Demo.right_is_ai());
501
502 assert!(GameMode::SinglePlayer.left_is_ai());
505 assert!(!GameMode::SinglePlayer.right_is_ai());
506
507 assert!(!GameMode::TwoPlayer.left_is_ai());
509 assert!(!GameMode::TwoPlayer.right_is_ai());
510 }
511
512 #[test]
513 fn test_game_mode_cycling() {
514 assert_eq!(GameMode::Demo.next(), GameMode::SinglePlayer);
515 assert_eq!(GameMode::SinglePlayer.next(), GameMode::TwoPlayer);
516 assert_eq!(GameMode::TwoPlayer.next(), GameMode::Demo);
517 }
518
519 #[test]
520 fn test_game_mode_paddle_labels() {
521 assert_eq!(GameMode::Demo.left_paddle_label(), "AI");
523 assert_eq!(GameMode::Demo.right_paddle_label(), "AI");
524
525 assert_eq!(GameMode::SinglePlayer.left_paddle_label(), "AI");
527 assert_eq!(GameMode::SinglePlayer.right_paddle_label(), "P1 [^/v]");
528
529 assert_eq!(GameMode::TwoPlayer.left_paddle_label(), "P2 [W/S]");
531 assert_eq!(GameMode::TwoPlayer.right_paddle_label(), "P1 [^/v]");
532 }
533
534 #[test]
537 #[allow(clippy::float_cmp)]
538 fn test_demo_state_default_threshold() {
539 let state = DemoState::default();
540 assert_eq!(state.auto_engage_threshold, 10.0); }
542
543 #[test]
544 fn test_demo_state_auto_engage_after_timeout() {
545 let mut state = DemoState::default();
546
547 let should_engage = state.update(9.0, false);
549 assert!(!should_engage);
550 assert!(!state.is_auto_engaged());
551
552 let should_engage = state.update(1.0, false);
554 assert!(should_engage);
555 assert!(state.is_auto_engaged());
556 }
557
558 #[test]
559 fn test_demo_state_input_resets_idle() {
560 let mut state = DemoState::default();
561
562 let _ = state.update(8.0, false);
564 assert!(state.idle_time() > 7.0);
565
566 let _ = state.update(0.016, true);
568 assert!(state.idle_time() < 0.1);
569 assert!(!state.is_auto_engaged());
570 }
571
572 #[test]
573 fn test_demo_state_difficulty_defaults() {
574 let state = DemoState::default();
575 assert_eq!(state.left_difficulty(), 7); assert_eq!(state.right_difficulty(), 5); }
578
579 #[test]
580 fn test_demo_state_difficulty_cycling() {
581 let mut state = DemoState::default();
582 let initial_left = state.left_difficulty();
583 let initial_right = state.right_difficulty();
584
585 state.update_difficulty_cycle(60.0);
587
588 assert_eq!(state.left_difficulty(), initial_right);
590 assert_eq!(state.right_difficulty(), initial_left);
591 }
592
593 #[test]
594 fn test_demo_state_reset() {
595 let mut state = DemoState::default();
596 let _ = state.update(15.0, false); assert!(state.is_auto_engaged());
598
599 state.reset();
600 assert!(!state.is_auto_engaged());
601 assert!(state.idle_time() < 0.1);
602 }
603
604 #[test]
607 fn test_performance_stats_format() {
608 let stats = PerformanceStats::new(60000, 60.0, "WASM-SIMD", 1000);
609 let display = stats.format_display();
610
611 assert!(display.contains("WASM-SIMD"));
612 assert!(display.contains("60000/s"));
613 assert!(display.contains("60 FPS"));
614 }
615
616 #[test]
619 fn test_attribution_defaults() {
620 let attr = Attribution::default();
621 assert!(attr.engine_version.contains("Jugar"));
622 assert!(attr.github_url.contains("paiml/jugar"));
623 assert!(attr.org_url.contains("paiml.com"));
624 assert_eq!(attr.model_filename, "pong-ai-v1.apr");
625 assert_eq!(attr.model_size, 491);
626 }
627
628 #[test]
629 fn test_attribution_labels() {
630 let attr = Attribution::default();
631 assert!(attr.github_label().contains("github.com"));
632 assert!(attr.org_label().contains("paiml.com"));
633 assert!(attr.model_label().contains("491 bytes"));
634 }
635
636 #[test]
639 fn test_demo_state_record_input_direct() {
640 let mut state = DemoState {
641 idle_time: 5.0,
642 auto_engaged: true,
643 ..Default::default()
644 };
645
646 state.record_input();
647
648 assert!(state.idle_time() < 0.001);
649 assert!(!state.is_auto_engaged());
650 }
651
652 #[test]
653 fn test_demo_state_partial_difficulty_cycle() {
654 let mut state = DemoState::default();
655 let initial_left = state.left_difficulty();
656 let initial_right = state.right_difficulty();
657
658 state.update_difficulty_cycle(30.0); assert_eq!(state.left_difficulty(), initial_left);
663 assert_eq!(state.right_difficulty(), initial_right);
664
665 state.update_difficulty_cycle(30.0); assert_eq!(state.left_difficulty(), initial_right);
670 assert_eq!(state.right_difficulty(), initial_left);
671 }
672
673 #[test]
674 fn test_demo_state_new_accessors() {
675 let state = DemoState::new(10.0, 30.0);
676
677 assert!(state.idle_time() < 0.001);
678 assert_eq!(state.left_difficulty(), 7); assert_eq!(state.right_difficulty(), 5); }
681}