1use glam::Vec2;
8use serde::{Deserialize, Serialize};
9use wasm_bindgen::prelude::*;
10
11use crate::ai::PongAI;
12use crate::audio::{AudioEvent, ProceduralAudio};
13use crate::demo::{DemoState, GameMode, SpeedMultiplier};
14use crate::input::{process_input_events, InputTranslationError};
15use crate::juice::JuiceEffects;
16use crate::render::{Canvas2DCommand, Color, RenderFrame, TextAlign, TextBaseline};
17use crate::time::FrameTimer;
18use crate::trace::{GameTracer, TracerConfig};
19use jugar_input::{InputState, MouseButton};
20
21#[derive(Debug, Clone, Copy, Default)]
23struct ButtonRect {
24 x: f32,
25 y: f32,
26 width: f32,
27 height: f32,
28}
29
30impl ButtonRect {
31 const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
32 Self {
33 x,
34 y,
35 width,
36 height,
37 }
38 }
39
40 fn contains(&self, px: f32, py: f32) -> bool {
42 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
43 }
44}
45
46#[derive(Debug, Clone, Default)]
48struct HudButtons {
49 mode_demo: ButtonRect,
51 mode_1p: ButtonRect,
52 mode_2p: ButtonRect,
53 speed_1x: ButtonRect,
55 speed_5x: ButtonRect,
56 speed_10x: ButtonRect,
57 speed_50x: ButtonRect,
58 speed_100x: ButtonRect,
59 speed_1000x: ButtonRect,
60 ai_decrease: ButtonRect,
62 ai_increase: ButtonRect,
63 download: ButtonRect,
65 model_info: ButtonRect,
67 sound_toggle: ButtonRect,
69}
70
71impl HudButtons {
72 #[inline]
75 fn button_width(text: &str, char_width: f32, padding: f32) -> f32 {
76 (text.len() as f32).mul_add(char_width, padding * 2.0)
77 }
78
79 #[must_use]
82 fn calculate(width: f32, height: f32) -> Self {
83 let hud_y = 10.0;
84 let button_height = 28.0;
85 let button_padding = 8.0;
86 let char_width = 8.0;
87
88 let mode_x = 10.0;
90 let demo_width = Self::button_width("Demo", char_width, button_padding);
91 let p1_width = Self::button_width("1P", char_width, button_padding);
92 let p2_width = Self::button_width("2P", char_width, button_padding);
93
94 let mode_demo = ButtonRect::new(mode_x, hud_y, demo_width, button_height);
95 let mode_1p = ButtonRect::new(mode_x + demo_width + 5.0, hud_y, p1_width, button_height);
96 let mode_2p = ButtonRect::new(
97 mode_x + demo_width + 5.0 + p1_width + 5.0,
98 hud_y,
99 p2_width,
100 button_height,
101 );
102
103 let w1x = Self::button_width("1x", char_width, button_padding);
105 let w5x = Self::button_width("5x", char_width, button_padding);
106 let w10x = Self::button_width("10x", char_width, button_padding);
107 let w50x = Self::button_width("50x", char_width, button_padding);
108 let w100x = Self::button_width("100x", char_width, button_padding);
109 let w1000x = Self::button_width("1000x", char_width, button_padding);
110 let total_width = w1x + w5x + w10x + w50x + w100x + w1000x + 25.0;
111
112 let mut speed_x = width - total_width - 10.0;
113 let speed_1x = ButtonRect::new(speed_x, hud_y, w1x, button_height);
114 speed_x += w1x + 5.0;
115 let speed_5x = ButtonRect::new(speed_x, hud_y, w5x, button_height);
116 speed_x += w5x + 5.0;
117 let speed_10x = ButtonRect::new(speed_x, hud_y, w10x, button_height);
118 speed_x += w10x + 5.0;
119 let speed_50x = ButtonRect::new(speed_x, hud_y, w50x, button_height);
120 speed_x += w50x + 5.0;
121 let speed_100x = ButtonRect::new(speed_x, hud_y, w100x, button_height);
122 speed_x += w100x + 5.0;
123 let speed_1000x = ButtonRect::new(speed_x, hud_y, w1000x, button_height);
124
125 let ai_y = hud_y + button_height + 15.0;
128 let ai_btn_size = 20.0;
129 let ai_btn_y = ai_y - 2.0;
130 let bar_x = 40.0;
131 let bar_width = 100.0;
132 let ai_btn_x = bar_x + bar_width + 100.0; let ai_plus_x = ai_btn_x + ai_btn_size + 5.0; let ai_decrease = ButtonRect::new(ai_btn_x, ai_btn_y, ai_btn_size, ai_btn_size);
135 let ai_increase = ButtonRect::new(ai_plus_x, ai_btn_y, ai_btn_size, ai_btn_size);
136
137 let download_y = height - 45.0;
139 let download_width = Self::button_width("Download .apr", char_width, button_padding);
140 let download = ButtonRect::new(10.0, download_y, download_width, button_height);
141
142 let info_width = Self::button_width("Info", char_width, button_padding);
144 let model_info = ButtonRect::new(
145 10.0 + download_width + 5.0,
146 download_y,
147 info_width,
148 button_height,
149 );
150
151 let sound_width = Self::button_width("Sound", char_width, button_padding);
153 let info_x = 10.0 + download_width + 5.0;
154 let sound_toggle = ButtonRect::new(
155 info_x + info_width + 25.0, download_y,
157 sound_width,
158 button_height,
159 );
160
161 Self {
162 mode_demo,
163 mode_1p,
164 mode_2p,
165 speed_1x,
166 speed_5x,
167 speed_10x,
168 speed_50x,
169 speed_100x,
170 speed_1000x,
171 ai_decrease,
172 ai_increase,
173 download,
174 model_info,
175 sound_toggle,
176 }
177 }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct WebConfig {
183 #[serde(default = "default_width")]
185 pub width: u32,
186 #[serde(default = "default_height")]
188 pub height: u32,
189 #[serde(default = "default_fps")]
191 pub target_fps: u32,
192 #[serde(default)]
194 pub debug: bool,
195 #[serde(default = "default_ai_enabled")]
197 pub ai_enabled: bool,
198}
199
200const fn default_ai_enabled() -> bool {
201 true }
203
204const fn default_width() -> u32 {
205 800
206}
207
208const fn default_height() -> u32 {
209 600
210}
211
212const fn default_fps() -> u32 {
213 60
214}
215
216impl Default for WebConfig {
217 fn default() -> Self {
218 Self {
219 width: 800,
220 height: 600,
221 target_fps: 60,
222 debug: false,
223 ai_enabled: true,
224 }
225 }
226}
227
228impl WebConfig {
229 #[must_use]
231 pub const fn new(width: u32, height: u32) -> Self {
232 Self {
233 width,
234 height,
235 target_fps: 60,
236 debug: false,
237 ai_enabled: true,
238 }
239 }
240
241 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
247 serde_json::from_str(json)
248 }
249
250 pub fn to_json(&self) -> Result<String, serde_json::Error> {
256 serde_json::to_string(self)
257 }
258}
259
260#[derive(Debug, Clone, PartialEq, Eq)]
262pub enum WebPlatformError {
263 InvalidConfig(String),
265 InputError(String),
267 RenderError(String),
269}
270
271impl core::fmt::Display for WebPlatformError {
272 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
273 match self {
274 Self::InvalidConfig(msg) => write!(f, "Invalid config: {msg}"),
275 Self::InputError(msg) => write!(f, "Input error: {msg}"),
276 Self::RenderError(msg) => write!(f, "Render error: {msg}"),
277 }
278 }
279}
280
281impl core::error::Error for WebPlatformError {}
282
283impl From<InputTranslationError> for WebPlatformError {
284 fn from(err: InputTranslationError) -> Self {
285 Self::InputError(err.to_string())
286 }
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291#[serde(tag = "type")]
292pub enum JsAction {
293 DownloadAiModel,
295 OpenUrl {
297 url: String,
299 },
300 EnterFullscreen,
302 ExitFullscreen,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct FrameOutput {
309 pub commands: Vec<Canvas2DCommand>,
311 #[serde(skip_serializing_if = "Vec::is_empty", default)]
313 pub audio_events: Vec<AudioEvent>,
314 #[serde(skip_serializing_if = "Vec::is_empty", default)]
316 pub actions: Vec<JsAction>,
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub debug_info: Option<DebugInfo>,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct DebugInfo {
325 pub dt_ms: f64,
327 pub fps: f64,
329 pub frame_count: u64,
331 pub input_summary: String,
333 pub game_mode: String,
335 pub speed_multiplier: u32,
337 pub left_paddle_y: f32,
339 pub right_paddle_y: f32,
341 pub ball_x: f32,
343 pub ball_y: f32,
345 #[serde(skip_serializing_if = "Option::is_none")]
347 pub trace_buffer_usage: Option<String>,
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub trace_inputs: Option<u64>,
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub trace_dropped: Option<u64>,
354}
355
356pub trait WebGame: Send {
360 fn update(&mut self, input: &InputState, dt: f64);
367
368 fn render(&mut self, frame: &mut RenderFrame);
375
376 fn resize(&mut self, width: u32, height: u32);
383}
384
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
387pub enum GameState {
388 #[default]
390 Menu,
391 Playing,
393 Paused,
395 GameOver,
397}
398
399#[derive(Debug, Clone)]
401#[allow(clippy::struct_excessive_bools)] pub struct PongGame {
403 width: f32,
405 height: f32,
407 left_paddle_y: f32,
409 right_paddle_y: f32,
411 ball_x: f32,
413 ball_y: f32,
415 ball_vx: f32,
417 ball_vy: f32,
419 left_score: u32,
421 right_score: u32,
423 paddle_height: f32,
425 paddle_width: f32,
427 ball_radius: f32,
429 paddle_speed: f32,
431 ai: Option<PongAI>,
433 juice: JuiceEffects,
435 prev_ball_y: f32,
437 state: GameState,
439 winning_score: u32,
441 space_was_pressed: bool,
443 escape_was_pressed: bool,
445 rally_count: u32,
447 high_score: u32,
449 bg_time: f32,
451 audio: ProceduralAudio,
453 speed_multiplier: SpeedMultiplier,
455 game_mode: GameMode,
457 #[allow(dead_code)] demo_state: DemoState,
460 left_ai: Option<PongAI>,
462 key_1_was_pressed: bool,
464 key_2_was_pressed: bool,
465 key_3_was_pressed: bool,
466 key_4_was_pressed: bool,
467 key_5_was_pressed: bool,
468 key_6_was_pressed: bool,
469 key_m_was_pressed: bool,
470 key_d_was_pressed: bool,
471 #[allow(dead_code)] key_plus_was_pressed: bool,
473 #[allow(dead_code)] key_minus_was_pressed: bool,
475 mouse_was_pressed: bool,
477 hud_buttons: HudButtons,
479 download_requested: bool,
481 show_model_info: bool,
483 key_i_was_pressed: bool,
485 sound_enabled: bool,
487 key_f_was_pressed: bool,
489 fullscreen_requested: bool,
491 is_fullscreen: bool,
493}
494
495impl Default for PongGame {
496 fn default() -> Self {
497 Self::new(800.0, 600.0, true)
498 }
499}
500
501impl PongGame {
502 #[must_use]
504 pub fn new(width: f32, height: f32, ai_enabled: bool) -> Self {
505 let ai = if ai_enabled {
506 Some(PongAI::default())
507 } else {
508 None
509 };
510
511 Self {
512 width,
513 height,
514 left_paddle_y: height / 2.0,
515 right_paddle_y: height / 2.0,
516 ball_x: width / 2.0,
517 ball_y: height / 2.0,
518 ball_vx: 200.0,
519 ball_vy: 150.0,
520 left_score: 0,
521 right_score: 0,
522 paddle_height: 100.0,
523 paddle_width: 15.0,
524 ball_radius: 10.0,
525 paddle_speed: 400.0,
526 ai,
527 juice: JuiceEffects::new(),
528 prev_ball_y: height / 2.0,
529 state: GameState::Playing, winning_score: 11,
531 space_was_pressed: false,
532 escape_was_pressed: false,
533 rally_count: 0,
534 high_score: 0,
535 bg_time: 0.0,
536 audio: ProceduralAudio::new(),
537 speed_multiplier: SpeedMultiplier::default(),
538 game_mode: GameMode::default(),
539 demo_state: DemoState::default(),
540 left_ai: Some(PongAI::with_difficulty(6)), key_1_was_pressed: false,
542 key_2_was_pressed: false,
543 key_3_was_pressed: false,
544 key_4_was_pressed: false,
545 key_5_was_pressed: false,
546 key_6_was_pressed: false,
547 key_m_was_pressed: false,
548 key_d_was_pressed: false,
549 key_plus_was_pressed: false,
550 key_minus_was_pressed: false,
551 mouse_was_pressed: false,
552 hud_buttons: HudButtons::calculate(width, height),
553 download_requested: false,
554 show_model_info: false,
555 key_i_was_pressed: false,
556 sound_enabled: true, key_f_was_pressed: false,
558 fullscreen_requested: false,
559 is_fullscreen: false,
560 }
561 }
562
563 #[must_use]
565 pub const fn speed_multiplier(&self) -> SpeedMultiplier {
566 self.speed_multiplier
567 }
568
569 #[allow(clippy::missing_const_for_fn)] pub fn set_speed_multiplier(&mut self, speed: SpeedMultiplier) {
572 self.speed_multiplier = speed;
573 }
574
575 #[must_use]
577 pub const fn game_mode(&self) -> GameMode {
578 self.game_mode
579 }
580
581 pub fn set_game_mode(&mut self, mode: GameMode) {
583 self.game_mode = mode;
584 self.reset_game();
585 }
586
587 pub fn reset_game(&mut self) {
589 self.left_paddle_y = self.height / 2.0;
590 self.right_paddle_y = self.height / 2.0;
591 self.ball_x = self.width / 2.0;
592 self.ball_y = self.height / 2.0;
593 self.ball_vx = 200.0;
594 self.ball_vy = 150.0;
595 self.left_score = 0;
596 self.right_score = 0;
597 self.prev_ball_y = self.height / 2.0;
598 self.rally_count = 0;
599 self.juice.reset();
601 if let Some(ref mut ai) = self.ai {
602 ai.reset();
603 }
604 }
605
606 #[must_use]
608 pub const fn rally_count(&self) -> u32 {
609 self.rally_count
610 }
611
612 #[must_use]
614 pub const fn high_score(&self) -> u32 {
615 self.high_score
616 }
617
618 pub fn take_audio_events(&mut self) -> Vec<AudioEvent> {
620 if self.sound_enabled {
621 self.audio.take_events()
622 } else {
623 drop(self.audio.take_events());
625 Vec::new()
626 }
627 }
628
629 #[must_use]
631 pub fn export_ai_model(&self) -> String {
632 self.ai.as_ref().map_or_else(
633 || crate::ai::PongAIModel::default().to_json(),
634 crate::ai::PongAI::export_model,
635 )
636 }
637
638 #[must_use]
640 pub fn ai_info(&self) -> String {
641 self.ai.as_ref().map_or_else(
642 || r#"{"enabled": false}"#.to_string(),
643 crate::ai::PongAI::model_info_json,
644 )
645 }
646
647 pub fn set_ai_difficulty(&mut self, level: u8) {
649 if let Some(ref mut ai) = self.ai {
650 ai.set_difficulty(level);
651 }
652 }
653
654 #[must_use]
656 pub fn ai_difficulty(&self) -> u8 {
657 self.ai.as_ref().map_or(5, crate::ai::PongAI::difficulty)
658 }
659
660 #[must_use]
662 pub const fn state(&self) -> GameState {
663 self.state
664 }
665
666 #[cfg(test)]
668 #[allow(clippy::missing_const_for_fn)] pub fn set_state(&mut self, state: GameState) {
670 self.state = state;
671 }
672
673 pub fn start(&mut self) {
675 if self.state == GameState::Menu {
676 self.reset_game();
677 self.state = GameState::Playing;
678 }
679 }
680
681 fn reset_ball(&mut self) {
683 self.ball_x = self.width / 2.0;
684 self.ball_y = self.height / 2.0;
685 self.ball_vx = -self.ball_vx.signum() * 200.0;
687 self.ball_vy = if fastrand::bool() { 150.0 } else { -150.0 };
688 }
689
690 #[must_use]
692 pub const fn left_score(&self) -> u32 {
693 self.left_score
694 }
695
696 #[must_use]
698 pub const fn right_score(&self) -> u32 {
699 self.right_score
700 }
701
702 #[must_use]
704 pub const fn ball_position(&self) -> (f32, f32) {
705 (self.ball_x, self.ball_y)
706 }
707
708 #[must_use]
712 pub const fn ball_x(&self) -> f32 {
713 self.ball_x
714 }
715
716 #[must_use]
718 pub const fn ball_y(&self) -> f32 {
719 self.ball_y
720 }
721
722 #[must_use]
724 pub const fn ball_vx(&self) -> f32 {
725 self.ball_vx
726 }
727
728 #[must_use]
730 pub const fn ball_vy(&self) -> f32 {
731 self.ball_vy
732 }
733
734 #[must_use]
736 pub const fn left_paddle_y(&self) -> f32 {
737 self.left_paddle_y
738 }
739
740 #[must_use]
742 pub const fn right_paddle_y(&self) -> f32 {
743 self.right_paddle_y
744 }
745
746 #[must_use]
748 pub const fn paddle_height(&self) -> f32 {
749 self.paddle_height
750 }
751
752 #[must_use]
754 pub const fn paddle_speed(&self) -> f32 {
755 self.paddle_speed
756 }
757
758 #[must_use]
760 pub const fn is_fullscreen(&self) -> bool {
761 self.is_fullscreen
762 }
763
764 #[cfg(test)]
766 pub fn set_ball_position(&mut self, x: f32, y: f32) {
767 self.ball_x = x;
768 self.ball_y = y;
769 }
770
771 #[cfg(test)]
773 pub fn set_ball_velocity(&mut self, vx: f32, vy: f32) {
774 self.ball_vx = vx;
775 self.ball_vy = vy;
776 }
777
778 #[cfg(test)]
780 pub fn set_left_paddle_y(&mut self, y: f32) {
781 self.left_paddle_y = y;
782 }
783
784 #[cfg(test)]
786 pub fn set_right_paddle_y(&mut self, y: f32) {
787 self.right_paddle_y = y;
788 }
789
790 #[cfg(test)]
792 pub fn set_left_score(&mut self, score: u32) {
793 self.left_score = score;
794 }
795
796 #[cfg(test)]
798 pub fn set_right_score(&mut self, score: u32) {
799 self.right_score = score;
800 }
801
802 #[cfg(test)]
804 pub fn set_rally_count(&mut self, count: u32) {
805 self.rally_count = count;
806 }
807
808 #[cfg(test)]
810 pub fn reset(&mut self) {
811 self.reset_game();
812 }
813
814 #[cfg(test)]
816 pub fn enable_sound(&mut self, enabled: bool) {
817 self.sound_enabled = enabled;
818 }
819}
820
821impl WebGame for PongGame {
822 #[allow(clippy::too_many_lines)] fn update(&mut self, input: &InputState, dt: f64) {
824 let base_dt = dt as f32;
825
826 let key_1 = input.is_key_pressed(jugar_input::KeyCode::Number(1));
828 let key_2 = input.is_key_pressed(jugar_input::KeyCode::Number(2));
829 let key_3 = input.is_key_pressed(jugar_input::KeyCode::Number(3));
830 let key_4 = input.is_key_pressed(jugar_input::KeyCode::Number(4));
831 let key_5 = input.is_key_pressed(jugar_input::KeyCode::Number(5));
832 let key_6 = input.is_key_pressed(jugar_input::KeyCode::Number(6));
833 let key_m = input.is_key_pressed(jugar_input::KeyCode::Letter('M'));
834 let key_d = input.is_key_pressed(jugar_input::KeyCode::Letter('D'));
835 let key_i = input.is_key_pressed(jugar_input::KeyCode::Letter('I'));
836 let key_f = input.is_key_pressed(jugar_input::KeyCode::Letter('F'));
837 let key_f11 = input.is_key_pressed(jugar_input::KeyCode::Function(11));
838
839 if key_1 && !self.key_1_was_pressed {
840 self.speed_multiplier = SpeedMultiplier::Normal;
841 }
842 if key_2 && !self.key_2_was_pressed {
843 self.speed_multiplier = SpeedMultiplier::Fast5x;
844 }
845 if key_3 && !self.key_3_was_pressed {
846 self.speed_multiplier = SpeedMultiplier::Fast10x;
847 }
848 if key_4 && !self.key_4_was_pressed {
849 self.speed_multiplier = SpeedMultiplier::Fast50x;
850 }
851 if key_5 && !self.key_5_was_pressed {
852 self.speed_multiplier = SpeedMultiplier::Fast100x;
853 }
854 if key_6 && !self.key_6_was_pressed {
855 self.speed_multiplier = SpeedMultiplier::Fast1000x;
856 }
857 if key_m && !self.key_m_was_pressed {
858 self.game_mode = self.game_mode.next();
859 }
860 if key_d && !self.key_d_was_pressed {
862 self.game_mode = if self.game_mode == GameMode::Demo {
863 GameMode::SinglePlayer
864 } else {
865 GameMode::Demo
866 };
867 }
868 if key_i && !self.key_i_was_pressed {
870 self.show_model_info = !self.show_model_info;
871 }
872 let fullscreen_key = (key_f || key_f11) && !self.key_f_was_pressed;
874 if fullscreen_key {
875 self.fullscreen_requested = true;
876 self.is_fullscreen = !self.is_fullscreen;
877 }
878
879 self.key_1_was_pressed = key_1;
880 self.key_2_was_pressed = key_2;
881 self.key_3_was_pressed = key_3;
882 self.key_4_was_pressed = key_4;
883 self.key_5_was_pressed = key_5;
884 self.key_6_was_pressed = key_6;
885 self.key_m_was_pressed = key_m;
886 self.key_d_was_pressed = key_d;
887 self.key_i_was_pressed = key_i;
888 self.key_f_was_pressed = key_f || key_f11;
889
890 let mouse_pressed = input.mouse_button(MouseButton::Left).is_down();
892 let mouse_just_clicked = mouse_pressed && !self.mouse_was_pressed;
893 self.mouse_was_pressed = mouse_pressed;
894
895 if mouse_just_clicked {
896 let pos = input.mouse_position;
897 self.handle_hud_click(pos.x, pos.y);
898 }
899
900 let space_pressed = input.is_key_pressed(jugar_input::KeyCode::Space);
902 let escape_pressed = input.is_key_pressed(jugar_input::KeyCode::Escape);
903
904 let space_just_pressed = space_pressed && !self.space_was_pressed;
906 let escape_just_pressed = escape_pressed && !self.escape_was_pressed;
907
908 self.space_was_pressed = space_pressed;
909 self.escape_was_pressed = escape_pressed;
910
911 match self.state {
912 GameState::Menu => {
913 if space_just_pressed {
914 self.reset_game();
915 self.state = GameState::Playing;
916 self.audio.on_game_start();
917 }
918 return; }
920 GameState::Playing => {
921 if escape_just_pressed {
922 self.state = GameState::Paused;
923 return;
924 }
925 }
926 GameState::Paused => {
927 if space_just_pressed || escape_just_pressed {
928 self.state = GameState::Playing;
929 }
930 return; }
932 GameState::GameOver => {
933 if space_just_pressed {
934 self.reset_game();
935 self.state = GameState::Playing;
936 self.audio.on_game_start();
937 }
938 return; }
940 }
941
942 let speed_mult = self.speed_multiplier.value();
945 let dt = base_dt * speed_mult as f32; let half_paddle = self.paddle_height / 2.0;
947
948 self.prev_ball_y = self.ball_y;
950
951 if self.game_mode.left_is_ai() {
956 if let Some(ref mut left_ai) = self.left_ai {
958 let ai_velocity = left_ai.update(
959 self.ball_x,
960 self.ball_y,
961 -self.ball_vx, self.ball_vy,
963 self.left_paddle_y,
964 self.paddle_height,
965 self.width,
966 self.height,
967 dt,
968 );
969 self.left_paddle_y += ai_velocity * dt;
970 }
971 } else {
972 if input.is_key_pressed(jugar_input::KeyCode::Letter('W')) {
974 self.left_paddle_y -= self.paddle_speed * dt;
975 }
976 if input.is_key_pressed(jugar_input::KeyCode::Letter('S')) {
977 self.left_paddle_y += self.paddle_speed * dt;
978 }
979 }
980
981 if self.game_mode.right_is_ai() {
986 if let Some(ref mut ai) = self.ai {
988 let ai_velocity = ai.update(
989 self.ball_x,
990 self.ball_y,
991 self.ball_vx,
992 self.ball_vy,
993 self.right_paddle_y,
994 self.paddle_height,
995 self.width,
996 self.height,
997 dt,
998 );
999 self.right_paddle_y += ai_velocity * dt;
1000 }
1001 } else {
1002 if input.is_key_pressed(jugar_input::KeyCode::Up) {
1004 self.right_paddle_y -= self.paddle_speed * dt;
1005 }
1006 if input.is_key_pressed(jugar_input::KeyCode::Down) {
1007 self.right_paddle_y += self.paddle_speed * dt;
1008 }
1009 }
1010
1011 self.left_paddle_y = self
1013 .left_paddle_y
1014 .clamp(half_paddle, self.height - half_paddle);
1015 self.right_paddle_y = self
1016 .right_paddle_y
1017 .clamp(half_paddle, self.height - half_paddle);
1018
1019 self.ball_x += self.ball_vx * dt;
1021 self.ball_y += self.ball_vy * dt;
1022
1023 let mut wall_bounced = false;
1025 if self.ball_y - self.ball_radius < 0.0 {
1026 self.ball_y = self.ball_radius;
1027 self.ball_vy = self.ball_vy.abs();
1028 wall_bounced = true;
1029 } else if self.ball_y + self.ball_radius > self.height {
1030 self.ball_y = self.height - self.ball_radius;
1031 self.ball_vy = -self.ball_vy.abs();
1032 wall_bounced = true;
1033 }
1034
1035 if wall_bounced {
1037 self.juice.on_wall_bounce();
1038 let ball_speed = self.ball_vx.hypot(self.ball_vy);
1040 self.audio.on_wall_bounce_with_velocity(ball_speed, 250.0);
1041 }
1042
1043 let left_paddle_x = 20.0 + self.paddle_width;
1045 let right_paddle_x = self.width - 20.0 - self.paddle_width;
1046
1047 if self.ball_x - self.ball_radius < left_paddle_x
1049 && self.ball_x - self.ball_radius > 20.0
1050 && self.ball_y > self.left_paddle_y - half_paddle
1051 && self.ball_y < self.left_paddle_y + half_paddle
1052 {
1053 self.ball_x = left_paddle_x + self.ball_radius;
1054 self.ball_vx = self.ball_vx.abs() * 1.05; self.rally_count += 1;
1058
1059 self.juice.on_paddle_hit_at(self.ball_x, self.ball_y, false);
1061
1062 self.audio
1064 .on_paddle_hit(self.ball_y, self.left_paddle_y, self.paddle_height);
1065
1066 if self.rally_count % 5 == 0 {
1068 self.audio.on_rally_milestone(self.rally_count);
1069 }
1070
1071 if let Some(ref mut ai) = self.ai {
1073 ai.record_player_hit();
1074 }
1075 }
1076
1077 if self.ball_x + self.ball_radius > right_paddle_x
1079 && self.ball_x + self.ball_radius < self.width - 20.0
1080 && self.ball_y > self.right_paddle_y - half_paddle
1081 && self.ball_y < self.right_paddle_y + half_paddle
1082 {
1083 self.ball_x = right_paddle_x - self.ball_radius;
1084 self.ball_vx = -self.ball_vx.abs() * 1.05; self.rally_count += 1;
1088
1089 self.juice.on_paddle_hit_at(self.ball_x, self.ball_y, true);
1091
1092 self.audio
1094 .on_paddle_hit(self.ball_y, self.right_paddle_y, self.paddle_height);
1095
1096 if self.rally_count % 5 == 0 {
1098 self.audio.on_rally_milestone(self.rally_count);
1099 }
1100 }
1101
1102 if self.ball_x < 0.0 {
1104 self.right_score += 1;
1106
1107 if self.rally_count > self.high_score {
1109 self.high_score = self.rally_count;
1110 }
1111 self.rally_count = 0;
1112
1113 self.juice.on_goal(self.width * 0.75, 50.0, "+1");
1115
1116 self.audio.on_goal(false);
1118
1119 if self.game_mode != GameMode::Demo {
1121 if let Some(ref mut ai) = self.ai {
1122 ai.record_player_miss();
1123 ai.adapt_difficulty();
1124 }
1125 }
1126
1127 if self.right_score >= self.winning_score {
1129 self.state = GameState::GameOver;
1130 } else {
1131 self.reset_ball();
1132 }
1133 } else if self.ball_x > self.width {
1134 self.left_score += 1;
1136
1137 if self.rally_count > self.high_score {
1139 self.high_score = self.rally_count;
1140 }
1141 self.rally_count = 0;
1142
1143 self.juice.on_goal(self.width * 0.25, 50.0, "+1");
1145
1146 self.audio.on_goal(true);
1148
1149 if self.game_mode != GameMode::Demo {
1151 if let Some(ref mut ai) = self.ai {
1152 ai.adapt_difficulty();
1153 }
1154 }
1155
1156 if self.left_score >= self.winning_score {
1158 self.state = GameState::GameOver;
1159 } else {
1160 self.reset_ball();
1161 }
1162 }
1163
1164 self.juice.update(self.ball_x, self.ball_y, dt);
1166
1167 self.bg_time += dt;
1169 }
1170
1171 #[allow(clippy::too_many_lines, clippy::suboptimal_flops)] fn render(&mut self, frame: &mut RenderFrame) {
1173 let (shake_x, shake_y) = self.juice.screen_shake.offset();
1175
1176 frame.clear_screen(Color::BLACK);
1178
1179 let dot_spacing = 40.0;
1181 let dot_radius = 1.5;
1182 let wave_speed = 0.5;
1183 let wave_amplitude = 0.3;
1184 let num_cols = (self.width / dot_spacing).ceil() as i32;
1185 let num_rows = (self.height / dot_spacing).ceil() as i32;
1186
1187 for row in 0..num_rows {
1188 for col in 0..num_cols {
1189 let base_x = (col as f32) * dot_spacing + dot_spacing / 2.0;
1190 let base_y = (row as f32) * dot_spacing + dot_spacing / 2.0;
1191
1192 let dx = base_x - self.width / 2.0;
1194 let dy = base_y - self.height / 2.0;
1195 let dist = dx.hypot(dy);
1196 let wave_phase = dist * 0.02 - self.bg_time * wave_speed;
1197 let alpha = 0.1 + wave_amplitude * (wave_phase.sin() * 0.5 + 0.5);
1198
1199 let dot_color = Color::new(0.2, 0.2, 0.4, alpha);
1200 frame.fill_circle(base_x + shake_x, base_y + shake_y, dot_radius, dot_color);
1201 }
1202 }
1203
1204 let dash_height = 20.0;
1206 let gap = 15.0;
1207 let center_x = self.width / 2.0 + shake_x;
1208 let num_dashes = (self.height / (dash_height + gap)).ceil() as usize;
1209 for i in 0..num_dashes {
1210 let y = (i as f32) * (dash_height + gap) + shake_y;
1211 frame.fill_rect(center_x - 2.0, y, 4.0, dash_height, Color::WHITE);
1212 }
1213
1214 for (x, y, alpha) in self.juice.ball_trail.get_points() {
1216 let trail_color = Color::new(1.0, 1.0, 1.0, alpha * 0.5);
1217 let trail_radius = self.ball_radius * (0.3 + 0.7 * alpha);
1218 frame.fill_circle(x + shake_x, y + shake_y, trail_radius, trail_color);
1219 }
1220
1221 let (left_flash, right_flash, flash_intensity) = self.juice.hit_flash.flash_state();
1223
1224 let half_paddle = self.paddle_height / 2.0;
1226 let left_paddle_x = 20.0 + shake_x;
1227 let left_paddle_top = self.left_paddle_y - half_paddle + shake_y;
1228
1229 let left_paddle_color = if left_flash {
1230 Color::new(1.0, 1.0, flash_intensity, 1.0)
1231 } else {
1232 Color::WHITE
1233 };
1234 frame.fill_rect(
1235 left_paddle_x,
1236 left_paddle_top,
1237 self.paddle_width,
1238 self.paddle_height,
1239 left_paddle_color,
1240 );
1241
1242 let left_label = self.game_mode.left_paddle_label();
1244 let label_y = left_paddle_top - 8.0; let label_color = Color::new(0.7, 0.7, 0.7, 0.9); frame.fill_text_aligned(
1247 left_label,
1248 left_paddle_x + self.paddle_width / 2.0,
1249 label_y.max(15.0), "12px monospace",
1251 label_color,
1252 crate::render::TextAlign::Center,
1253 crate::render::TextBaseline::Bottom,
1254 );
1255
1256 let right_paddle_x = self.width - 20.0 - self.paddle_width + shake_x;
1258 let right_paddle_top = self.right_paddle_y - half_paddle + shake_y;
1259
1260 let right_paddle_color = if right_flash {
1261 Color::new(1.0, 1.0, flash_intensity, 1.0)
1262 } else {
1263 Color::WHITE
1264 };
1265 frame.fill_rect(
1266 right_paddle_x,
1267 right_paddle_top,
1268 self.paddle_width,
1269 self.paddle_height,
1270 right_paddle_color,
1271 );
1272
1273 let right_label = self.game_mode.right_paddle_label();
1275 let right_label_y = right_paddle_top - 8.0; frame.fill_text_aligned(
1277 right_label,
1278 right_paddle_x + self.paddle_width / 2.0,
1279 right_label_y.max(15.0), "12px monospace",
1281 label_color,
1282 crate::render::TextAlign::Center,
1283 crate::render::TextBaseline::Bottom,
1284 );
1285
1286 let speed = self.ball_vx.hypot(self.ball_vy);
1289 let base_speed = 250.0; let stretch_factor = (speed / base_speed).clamp(0.8, 1.5);
1291
1292 let angle = self.ball_vy.atan2(self.ball_vx);
1294
1295 let cos_a = angle.cos();
1298 let sin_a = angle.sin();
1299
1300 let stretch_along = stretch_factor;
1302 let stretch_perp = 1.0 / stretch_factor;
1303
1304 let rx = self.ball_radius * (stretch_along * cos_a).hypot(stretch_perp * sin_a);
1306 let ry = self.ball_radius * (stretch_along * sin_a).hypot(stretch_perp * cos_a);
1307
1308 let avg_radius = (rx + ry) / 2.0;
1311 frame.fill_circle(
1312 self.ball_x + shake_x,
1313 self.ball_y + shake_y,
1314 avg_radius,
1315 Color::WHITE,
1316 );
1317
1318 frame.fill_text_aligned(
1320 &self.left_score.to_string(),
1321 self.width / 4.0 + shake_x,
1322 50.0 + shake_y,
1323 "48px monospace",
1324 Color::WHITE,
1325 TextAlign::Center,
1326 TextBaseline::Top,
1327 );
1328 frame.fill_text_aligned(
1329 &self.right_score.to_string(),
1330 3.0 * self.width / 4.0 + shake_x,
1331 50.0 + shake_y,
1332 "48px monospace",
1333 Color::WHITE,
1334 TextAlign::Center,
1335 TextBaseline::Top,
1336 );
1337
1338 for popup in &self.juice.score_popups {
1340 let popup_color = Color::new(1.0, 1.0, 0.0, popup.alpha()); frame.fill_text_aligned(
1342 &popup.text,
1343 popup.x + shake_x,
1344 popup.y + shake_y,
1345 "32px monospace",
1346 popup_color,
1347 TextAlign::Center,
1348 TextBaseline::Middle,
1349 );
1350 }
1351
1352 for particle in self.juice.particles.get_active() {
1354 let (r, g, b) = particle.rgb();
1355 let particle_color = Color::new(r, g, b, particle.alpha());
1356 frame.fill_circle(
1357 particle.x + shake_x,
1358 particle.y + shake_y,
1359 particle.size,
1360 particle_color,
1361 );
1362 }
1363
1364 if self.state == GameState::Playing && self.rally_count > 0 {
1366 let rally_text = format!("Rally: {}", self.rally_count);
1367 let intensity = (self.rally_count as f32 / 20.0).min(1.0);
1369 let rally_color = Color::new(0.5 + intensity * 0.5, 1.0, 0.5 + intensity * 0.5, 0.8);
1370 frame.fill_text_aligned(
1371 &rally_text,
1372 self.width / 2.0 + shake_x,
1373 self.height - 30.0 + shake_y,
1374 "20px monospace",
1375 rally_color,
1376 TextAlign::Center,
1377 TextBaseline::Bottom,
1378 );
1379 }
1380
1381 if self.high_score > 0 {
1383 let high_score_text = format!("Best Rally: {}", self.high_score);
1384 frame.fill_text_aligned(
1385 &high_score_text,
1386 self.width / 2.0 + shake_x,
1387 20.0 + shake_y,
1388 "14px monospace",
1389 Color::new(0.5, 0.5, 0.5, 0.7),
1390 TextAlign::Center,
1391 TextBaseline::Top,
1392 );
1393 }
1394
1395 match self.state {
1397 GameState::Menu => {
1398 frame.fill_rect(
1400 0.0,
1401 0.0,
1402 self.width,
1403 self.height,
1404 Color::new(0.0, 0.0, 0.0, 0.7),
1405 );
1406
1407 frame.fill_text_aligned(
1409 "PONG",
1410 self.width / 2.0,
1411 self.height / 3.0,
1412 "64px monospace",
1413 Color::WHITE,
1414 TextAlign::Center,
1415 TextBaseline::Middle,
1416 );
1417
1418 frame.fill_text_aligned(
1420 "Press SPACE to Start",
1421 self.width / 2.0,
1422 self.height / 2.0,
1423 "24px monospace",
1424 Color::new(0.7, 0.7, 0.7, 1.0),
1425 TextAlign::Center,
1426 TextBaseline::Middle,
1427 );
1428
1429 frame.fill_text_aligned(
1431 "W/S - Move Paddle | ESC - Pause",
1432 self.width / 2.0,
1433 self.height * 0.65,
1434 "16px monospace",
1435 Color::new(0.5, 0.5, 0.5, 1.0),
1436 TextAlign::Center,
1437 TextBaseline::Middle,
1438 );
1439 }
1440 GameState::Paused => {
1441 frame.fill_rect(
1443 0.0,
1444 0.0,
1445 self.width,
1446 self.height,
1447 Color::new(0.0, 0.0, 0.0, 0.5),
1448 );
1449
1450 frame.fill_text_aligned(
1451 "PAUSED",
1452 self.width / 2.0,
1453 self.height / 2.0 - 30.0,
1454 "48px monospace",
1455 Color::WHITE,
1456 TextAlign::Center,
1457 TextBaseline::Middle,
1458 );
1459
1460 frame.fill_text_aligned(
1461 "Press SPACE or ESC to Resume",
1462 self.width / 2.0,
1463 self.height / 2.0 + 30.0,
1464 "20px monospace",
1465 Color::new(0.7, 0.7, 0.7, 1.0),
1466 TextAlign::Center,
1467 TextBaseline::Middle,
1468 );
1469 }
1470 GameState::GameOver => {
1471 frame.fill_rect(
1473 0.0,
1474 0.0,
1475 self.width,
1476 self.height,
1477 Color::new(0.0, 0.0, 0.0, 0.7),
1478 );
1479
1480 let winner = if self.left_score >= self.winning_score {
1482 "YOU WIN!"
1483 } else {
1484 "GAME OVER"
1485 };
1486 let winner_color = if self.left_score >= self.winning_score {
1487 Color::new(0.2, 1.0, 0.2, 1.0) } else {
1489 Color::new(1.0, 0.3, 0.3, 1.0) };
1491
1492 frame.fill_text_aligned(
1493 winner,
1494 self.width / 2.0,
1495 self.height / 2.0 - 40.0,
1496 "48px monospace",
1497 winner_color,
1498 TextAlign::Center,
1499 TextBaseline::Middle,
1500 );
1501
1502 let score_text = format!("{} - {}", self.left_score, self.right_score);
1504 frame.fill_text_aligned(
1505 &score_text,
1506 self.width / 2.0,
1507 self.height / 2.0 + 20.0,
1508 "32px monospace",
1509 Color::WHITE,
1510 TextAlign::Center,
1511 TextBaseline::Middle,
1512 );
1513
1514 frame.fill_text_aligned(
1515 "Press SPACE to Play Again",
1516 self.width / 2.0,
1517 self.height / 2.0 + 80.0,
1518 "20px monospace",
1519 Color::new(0.7, 0.7, 0.7, 1.0),
1520 TextAlign::Center,
1521 TextBaseline::Middle,
1522 );
1523 }
1524 GameState::Playing => {
1525 }
1527 }
1528
1529 self.render_hud(frame);
1533 }
1534
1535 fn resize(&mut self, width: u32, height: u32) {
1536 let old_width = self.width;
1537 let old_height = self.height;
1538 self.width = width as f32;
1539 self.height = height as f32;
1540
1541 if old_width > 0.0 && old_height > 0.0 {
1543 self.ball_x = self.ball_x * self.width / old_width;
1544 self.ball_y = self.ball_y * self.height / old_height;
1545 self.left_paddle_y = self.left_paddle_y * self.height / old_height;
1546 self.right_paddle_y = self.right_paddle_y * self.height / old_height;
1547 }
1548
1549 self.hud_buttons = HudButtons::calculate(self.width, self.height);
1551 }
1552}
1553
1554impl PongGame {
1555 fn handle_hud_click(&mut self, mx: f32, my: f32) {
1557 if self.show_model_info {
1559 self.show_model_info = false;
1560 return; }
1562
1563 if self.hud_buttons.mode_demo.contains(mx, my) {
1565 self.game_mode = GameMode::Demo;
1566 self.reset_game();
1567 self.state = GameState::Playing;
1568 self.audio.on_game_start();
1569 } else if self.hud_buttons.mode_1p.contains(mx, my) {
1570 self.game_mode = GameMode::SinglePlayer;
1571 self.reset_game();
1572 self.state = GameState::Playing;
1573 self.audio.on_game_start();
1574 } else if self.hud_buttons.mode_2p.contains(mx, my) {
1575 self.game_mode = GameMode::TwoPlayer;
1576 self.reset_game();
1577 self.state = GameState::Playing;
1578 self.audio.on_game_start();
1579 }
1580 else if self.hud_buttons.speed_1x.contains(mx, my) {
1582 self.speed_multiplier = SpeedMultiplier::Normal;
1583 } else if self.hud_buttons.speed_5x.contains(mx, my) {
1584 self.speed_multiplier = SpeedMultiplier::Fast5x;
1585 } else if self.hud_buttons.speed_10x.contains(mx, my) {
1586 self.speed_multiplier = SpeedMultiplier::Fast10x;
1587 } else if self.hud_buttons.speed_50x.contains(mx, my) {
1588 self.speed_multiplier = SpeedMultiplier::Fast50x;
1589 } else if self.hud_buttons.speed_100x.contains(mx, my) {
1590 self.speed_multiplier = SpeedMultiplier::Fast100x;
1591 } else if self.hud_buttons.speed_1000x.contains(mx, my) {
1592 self.speed_multiplier = SpeedMultiplier::Fast1000x;
1593 }
1594 else if self.hud_buttons.ai_decrease.contains(mx, my) {
1596 if let Some(ref mut ai) = self.ai {
1597 let level = ai.difficulty();
1598 if level > 0 {
1599 ai.set_difficulty(level - 1);
1600 }
1601 }
1602 } else if self.hud_buttons.ai_increase.contains(mx, my) {
1603 if let Some(ref mut ai) = self.ai {
1604 let level = ai.difficulty();
1605 if level < 9 {
1606 ai.set_difficulty(level + 1);
1607 }
1608 }
1609 }
1610 else if self.hud_buttons.download.contains(mx, my) {
1612 self.download_requested = true;
1613 }
1614 else if self.hud_buttons.model_info.contains(mx, my) {
1616 self.show_model_info = !self.show_model_info;
1617 }
1618 else if self.hud_buttons.sound_toggle.contains(mx, my) {
1620 self.sound_enabled = !self.sound_enabled;
1621 self.audio.on_sound_toggle(self.sound_enabled);
1623 }
1624 }
1625
1626 #[allow(clippy::too_many_lines, clippy::suboptimal_flops)]
1628 fn render_hud(&mut self, frame: &mut RenderFrame) {
1629 let hud_y = 10.0;
1630 let button_height = 28.0;
1631 let button_padding = 8.0;
1632 let font_size = "14px monospace";
1633 let small_font = "12px monospace";
1634
1635 let mut mode_x = 10.0;
1639
1640 let render_mode_button =
1642 |frame: &mut RenderFrame, x: f32, label: &str, is_selected: bool| -> ButtonRect {
1643 let bw = (label.len() as f32) * 10.0 + button_padding * 2.0;
1644 let bg_color = if is_selected {
1645 Color::new(0.3, 0.6, 1.0, 0.9)
1646 } else {
1647 Color::new(0.2, 0.2, 0.2, 0.8)
1648 };
1649 let border_color = if is_selected {
1650 Color::WHITE
1651 } else {
1652 Color::new(0.5, 0.5, 0.5, 1.0)
1653 };
1654 frame.fill_rect(x, hud_y, bw, button_height, bg_color);
1655 frame.stroke_rect(x, hud_y, bw, button_height, border_color, 1.0);
1656 frame.fill_text_aligned(
1657 label,
1658 x + bw / 2.0,
1659 hud_y + button_height / 2.0,
1660 font_size,
1661 Color::WHITE,
1662 TextAlign::Center,
1663 TextBaseline::Middle,
1664 );
1665 ButtonRect::new(x, hud_y, bw, button_height)
1666 };
1667
1668 self.hud_buttons.mode_demo =
1670 render_mode_button(frame, mode_x, "Demo", self.game_mode == GameMode::Demo);
1671 mode_x += self.hud_buttons.mode_demo.width + 5.0;
1672
1673 self.hud_buttons.mode_1p = render_mode_button(
1675 frame,
1676 mode_x,
1677 "1P",
1678 self.game_mode == GameMode::SinglePlayer,
1679 );
1680 mode_x += self.hud_buttons.mode_1p.width + 5.0;
1681
1682 self.hud_buttons.mode_2p =
1684 render_mode_button(frame, mode_x, "2P", self.game_mode == GameMode::TwoPlayer);
1685 mode_x += self.hud_buttons.mode_2p.width + 5.0;
1686
1687 frame.fill_text_aligned(
1689 "[M]",
1690 mode_x + 5.0,
1691 hud_y + button_height / 2.0,
1692 small_font,
1693 Color::new(0.5, 0.5, 0.5, 0.8),
1694 TextAlign::Left,
1695 TextBaseline::Middle,
1696 );
1697
1698 let render_speed_button = |frame: &mut RenderFrame,
1704 x: f32,
1705 label: &str,
1706 key_hint: &str,
1707 is_selected: bool|
1708 -> ButtonRect {
1709 let bw = (label.len() as f32) * 8.0 + button_padding * 2.0;
1710 let bg_color = if is_selected {
1711 Color::new(1.0, 0.6, 0.2, 0.9)
1712 } else {
1713 Color::new(0.2, 0.2, 0.2, 0.8)
1714 };
1715 let border_color = if is_selected {
1716 Color::WHITE
1717 } else {
1718 Color::new(0.5, 0.5, 0.5, 1.0)
1719 };
1720 frame.fill_rect(x, hud_y, bw, button_height, bg_color);
1721 frame.stroke_rect(x, hud_y, bw, button_height, border_color, 1.0);
1722 frame.fill_text_aligned(
1723 label,
1724 x + bw / 2.0,
1725 hud_y + button_height / 2.0,
1726 font_size,
1727 Color::WHITE,
1728 TextAlign::Center,
1729 TextBaseline::Middle,
1730 );
1731 frame.fill_text_aligned(
1732 key_hint,
1733 x + bw / 2.0,
1734 hud_y + button_height + 3.0,
1735 "10px monospace",
1736 Color::new(0.4, 0.4, 0.4, 0.7),
1737 TextAlign::Center,
1738 TextBaseline::Top,
1739 );
1740 ButtonRect::new(x, hud_y, bw, button_height)
1741 };
1742
1743 let w1x = "1x".len() as f32 * 8.0 + button_padding * 2.0;
1745 let w5x = "5x".len() as f32 * 8.0 + button_padding * 2.0;
1746 let w10x = "10x".len() as f32 * 8.0 + button_padding * 2.0;
1747 let w50x = "50x".len() as f32 * 8.0 + button_padding * 2.0;
1748 let w100x = "100x".len() as f32 * 8.0 + button_padding * 2.0;
1749 let w1000x = "1000x".len() as f32 * 8.0 + button_padding * 2.0;
1750 let total_width = w1x + w5x + w10x + w50x + w100x + w1000x + 25.0; let mut speed_x = self.width - total_width - 10.0;
1753
1754 self.hud_buttons.speed_1x = render_speed_button(
1755 frame,
1756 speed_x,
1757 "1x",
1758 "1",
1759 self.speed_multiplier == SpeedMultiplier::Normal,
1760 );
1761 speed_x += w1x + 5.0;
1762
1763 self.hud_buttons.speed_5x = render_speed_button(
1764 frame,
1765 speed_x,
1766 "5x",
1767 "2",
1768 self.speed_multiplier == SpeedMultiplier::Fast5x,
1769 );
1770 speed_x += w5x + 5.0;
1771
1772 self.hud_buttons.speed_10x = render_speed_button(
1773 frame,
1774 speed_x,
1775 "10x",
1776 "3",
1777 self.speed_multiplier == SpeedMultiplier::Fast10x,
1778 );
1779 speed_x += w10x + 5.0;
1780
1781 self.hud_buttons.speed_50x = render_speed_button(
1782 frame,
1783 speed_x,
1784 "50x",
1785 "4",
1786 self.speed_multiplier == SpeedMultiplier::Fast50x,
1787 );
1788 speed_x += w50x + 5.0;
1789
1790 self.hud_buttons.speed_100x = render_speed_button(
1791 frame,
1792 speed_x,
1793 "100x",
1794 "5",
1795 self.speed_multiplier == SpeedMultiplier::Fast100x,
1796 );
1797 speed_x += w100x + 5.0;
1798
1799 self.hud_buttons.speed_1000x = render_speed_button(
1800 frame,
1801 speed_x,
1802 "1000x",
1803 "6",
1804 self.speed_multiplier == SpeedMultiplier::Fast1000x,
1805 );
1806
1807 let ai_y = hud_y + button_height + 15.0;
1811 let ai_level = self.ai.as_ref().map_or(5, crate::ai::PongAI::difficulty);
1812 let ai_name = self
1813 .ai
1814 .as_ref()
1815 .map_or("Normal", crate::ai::PongAI::difficulty_name);
1816
1817 frame.fill_text_aligned(
1819 "AI:",
1820 10.0,
1821 ai_y,
1822 font_size,
1823 Color::new(0.7, 0.7, 0.7, 1.0),
1824 TextAlign::Left,
1825 TextBaseline::Top,
1826 );
1827
1828 let bar_x = 40.0;
1830 let bar_width = 100.0;
1831 let bar_height = 12.0;
1832 frame.fill_rect(
1833 bar_x,
1834 ai_y + 2.0,
1835 bar_width,
1836 bar_height,
1837 Color::new(0.2, 0.2, 0.2, 0.8),
1838 );
1839
1840 let fill_width = (f32::from(ai_level) / 9.0) * bar_width;
1842 let fill_color = match ai_level {
1843 0..=2 => Color::new(0.2, 0.8, 0.2, 1.0), 3..=5 => Color::new(0.8, 0.8, 0.2, 1.0), 6..=7 => Color::new(0.8, 0.5, 0.2, 1.0), _ => Color::new(0.8, 0.2, 0.2, 1.0), };
1848 frame.fill_rect(bar_x, ai_y + 2.0, fill_width, bar_height, fill_color);
1849
1850 frame.stroke_rect(
1852 bar_x,
1853 ai_y + 2.0,
1854 bar_width,
1855 bar_height,
1856 Color::new(0.5, 0.5, 0.5, 1.0),
1857 1.0,
1858 );
1859
1860 let ai_text = format!("{ai_level}/9 {ai_name}");
1862 frame.fill_text_aligned(
1863 &ai_text,
1864 bar_x + bar_width + 10.0,
1865 ai_y,
1866 small_font,
1867 Color::new(0.7, 0.7, 0.7, 1.0),
1868 TextAlign::Left,
1869 TextBaseline::Top,
1870 );
1871
1872 let ai_btn_size = 20.0;
1874 let ai_btn_y = ai_y - 2.0;
1875 let ai_btn_x = bar_x + bar_width + 100.0;
1876
1877 frame.fill_rect(
1879 ai_btn_x,
1880 ai_btn_y,
1881 ai_btn_size,
1882 ai_btn_size,
1883 Color::new(0.3, 0.3, 0.3, 0.9),
1884 );
1885 frame.stroke_rect(
1886 ai_btn_x,
1887 ai_btn_y,
1888 ai_btn_size,
1889 ai_btn_size,
1890 Color::new(0.5, 0.5, 0.5, 1.0),
1891 1.0,
1892 );
1893 frame.fill_text_aligned(
1894 "-",
1895 ai_btn_x + ai_btn_size / 2.0,
1896 ai_btn_y + ai_btn_size / 2.0,
1897 font_size,
1898 Color::WHITE,
1899 TextAlign::Center,
1900 TextBaseline::Middle,
1901 );
1902 self.hud_buttons.ai_decrease =
1903 ButtonRect::new(ai_btn_x, ai_btn_y, ai_btn_size, ai_btn_size);
1904
1905 let ai_plus_x = ai_btn_x + ai_btn_size + 5.0;
1907 frame.fill_rect(
1908 ai_plus_x,
1909 ai_btn_y,
1910 ai_btn_size,
1911 ai_btn_size,
1912 Color::new(0.3, 0.3, 0.3, 0.9),
1913 );
1914 frame.stroke_rect(
1915 ai_plus_x,
1916 ai_btn_y,
1917 ai_btn_size,
1918 ai_btn_size,
1919 Color::new(0.5, 0.5, 0.5, 1.0),
1920 1.0,
1921 );
1922 frame.fill_text_aligned(
1923 "+",
1924 ai_plus_x + ai_btn_size / 2.0,
1925 ai_btn_y + ai_btn_size / 2.0,
1926 font_size,
1927 Color::WHITE,
1928 TextAlign::Center,
1929 TextBaseline::Middle,
1930 );
1931 self.hud_buttons.ai_increase =
1932 ButtonRect::new(ai_plus_x, ai_btn_y, ai_btn_size, ai_btn_size);
1933
1934 let download_y = self.height - 45.0;
1938 let download_text = "Download .apr";
1939 let download_width = (download_text.len() as f32) * 8.0 + button_padding * 2.0;
1940
1941 frame.fill_rect(
1943 10.0,
1944 download_y,
1945 download_width,
1946 button_height,
1947 Color::new(0.1, 0.4, 0.2, 0.8),
1948 );
1949
1950 frame.stroke_rect(
1952 10.0,
1953 download_y,
1954 download_width,
1955 button_height,
1956 Color::new(0.3, 0.7, 0.4, 1.0),
1957 1.0,
1958 );
1959
1960 self.hud_buttons.download =
1962 ButtonRect::new(10.0, download_y, download_width, button_height);
1963
1964 frame.fill_text_aligned(
1966 download_text,
1967 10.0 + download_width / 2.0,
1968 download_y + button_height / 2.0,
1969 font_size,
1970 Color::WHITE,
1971 TextAlign::Center,
1972 TextBaseline::Middle,
1973 );
1974
1975 let info_text = "Info";
1979 let info_width = (info_text.len() as f32) * 8.0 + button_padding * 2.0;
1980 let info_x = 10.0 + download_width + 5.0;
1981
1982 let info_bg = if self.show_model_info {
1984 Color::new(0.3, 0.3, 0.7, 0.9) } else {
1986 Color::new(0.2, 0.2, 0.4, 0.8)
1987 };
1988 frame.fill_rect(info_x, download_y, info_width, button_height, info_bg);
1989
1990 frame.stroke_rect(
1992 info_x,
1993 download_y,
1994 info_width,
1995 button_height,
1996 Color::new(0.4, 0.4, 0.8, 1.0),
1997 1.0,
1998 );
1999
2000 self.hud_buttons.model_info =
2002 ButtonRect::new(info_x, download_y, info_width, button_height);
2003
2004 frame.fill_text_aligned(
2006 info_text,
2007 info_x + info_width / 2.0,
2008 download_y + button_height / 2.0,
2009 font_size,
2010 Color::WHITE,
2011 TextAlign::Center,
2012 TextBaseline::Middle,
2013 );
2014
2015 frame.fill_text_aligned(
2017 "[I]",
2018 info_x + info_width + 5.0,
2019 download_y + button_height / 2.0,
2020 "10px monospace",
2021 Color::new(0.4, 0.4, 0.4, 0.7),
2022 TextAlign::Left,
2023 TextBaseline::Middle,
2024 );
2025
2026 let sound_text = if self.sound_enabled { "Sound" } else { "Muted" };
2030 let sound_width = 64.0; let sound_x = info_x + info_width + 25.0; let sound_bg = if self.sound_enabled {
2035 Color::new(0.2, 0.4, 0.2, 0.8)
2036 } else {
2037 Color::new(0.4, 0.2, 0.2, 0.8)
2038 };
2039 frame.fill_rect(sound_x, download_y, sound_width, button_height, sound_bg);
2040
2041 let sound_border = if self.sound_enabled {
2043 Color::new(0.3, 0.7, 0.3, 1.0)
2044 } else {
2045 Color::new(0.7, 0.3, 0.3, 1.0)
2046 };
2047 frame.stroke_rect(
2048 sound_x,
2049 download_y,
2050 sound_width,
2051 button_height,
2052 sound_border,
2053 1.0,
2054 );
2055
2056 self.hud_buttons.sound_toggle =
2058 ButtonRect::new(sound_x, download_y, sound_width, button_height);
2059
2060 frame.fill_text_aligned(
2062 sound_text,
2063 sound_x + sound_width / 2.0,
2064 download_y + button_height / 2.0,
2065 font_size,
2066 Color::WHITE,
2067 TextAlign::Center,
2068 TextBaseline::Middle,
2069 );
2070
2071 self.render_ai_explain_widget(frame);
2075
2076 if self.show_model_info {
2080 self.render_model_info_panel(frame);
2081 }
2082
2083 let footer_x = self.width - 10.0;
2085 let footer_y = self.height - 10.0;
2086 let link_color = Color::new(0.4, 0.4, 0.4, 0.7);
2087 let link_spacing = 14.0;
2088
2089 frame.fill_text_aligned(
2091 "paiml.com",
2092 footer_x,
2093 footer_y - link_spacing * 2.0,
2094 small_font,
2095 link_color,
2096 TextAlign::Right,
2097 TextBaseline::Bottom,
2098 );
2099
2100 frame.fill_text_aligned(
2102 "github.com/paiml/jugar",
2103 footer_x,
2104 footer_y - link_spacing,
2105 small_font,
2106 link_color,
2107 TextAlign::Right,
2108 TextBaseline::Bottom,
2109 );
2110
2111 frame.fill_text_aligned(
2113 ".apr format: github.com/paiml/aprender",
2114 footer_x,
2115 footer_y,
2116 small_font,
2117 link_color,
2118 TextAlign::Right,
2119 TextBaseline::Bottom,
2120 );
2121 }
2122
2123 #[allow(clippy::too_many_lines)]
2131 fn render_model_info_panel(&self, frame: &mut RenderFrame) {
2132 let panel_width = 320.0;
2134 let panel_height = 340.0;
2135 let panel_x = (self.width - panel_width) / 2.0;
2136 let panel_y = (self.height - panel_height) / 2.0;
2137
2138 frame.fill_rect(
2140 panel_x,
2141 panel_y,
2142 panel_width,
2143 panel_height,
2144 Color::new(0.05, 0.05, 0.1, 0.95),
2145 );
2146
2147 frame.stroke_rect(
2149 panel_x,
2150 panel_y,
2151 panel_width,
2152 panel_height,
2153 Color::new(0.3, 0.3, 0.8, 1.0),
2154 2.0,
2155 );
2156
2157 let title_font = "16px monospace";
2158 let label_font = "12px monospace";
2159 let value_font = "14px monospace";
2160
2161 let text_color = Color::WHITE;
2162 let label_color = Color::new(0.6, 0.6, 0.8, 1.0);
2163 let highlight_color = Color::new(0.4, 0.8, 1.0, 1.0);
2164
2165 let mut y = panel_y + 25.0;
2166 let left_margin = panel_x + 15.0;
2167 let value_x = panel_x + 140.0;
2168 let line_height = 22.0;
2169
2170 frame.fill_text_aligned(
2172 ".apr Model Info",
2173 panel_x + panel_width / 2.0,
2174 y,
2175 title_font,
2176 highlight_color,
2177 TextAlign::Center,
2178 TextBaseline::Middle,
2179 );
2180 y += line_height + 8.0;
2181
2182 frame.fill_rect(
2184 panel_x + 10.0,
2185 y,
2186 panel_width - 20.0,
2187 1.0,
2188 Color::new(0.3, 0.3, 0.5, 0.8),
2189 );
2190 y += 12.0;
2191
2192 let ai = self.ai.as_ref();
2194 let default_model = crate::ai::PongAIModel::default();
2195 let model = ai.map_or(&default_model, crate::ai::PongAI::model);
2196
2197 frame.fill_text_aligned(
2199 "METADATA",
2200 left_margin,
2201 y,
2202 label_font,
2203 label_color,
2204 TextAlign::Left,
2205 TextBaseline::Middle,
2206 );
2207 y += line_height;
2208
2209 frame.fill_text_aligned(
2211 "Model:",
2212 left_margin,
2213 y,
2214 label_font,
2215 label_color,
2216 TextAlign::Left,
2217 TextBaseline::Middle,
2218 );
2219 frame.fill_text_aligned(
2220 &model.metadata.name,
2221 value_x,
2222 y,
2223 value_font,
2224 text_color,
2225 TextAlign::Left,
2226 TextBaseline::Middle,
2227 );
2228 y += line_height;
2229
2230 frame.fill_text_aligned(
2232 "Version:",
2233 left_margin,
2234 y,
2235 label_font,
2236 label_color,
2237 TextAlign::Left,
2238 TextBaseline::Middle,
2239 );
2240 frame.fill_text_aligned(
2241 &model.metadata.version,
2242 value_x,
2243 y,
2244 value_font,
2245 text_color,
2246 TextAlign::Left,
2247 TextBaseline::Middle,
2248 );
2249 y += line_height;
2250
2251 frame.fill_text_aligned(
2253 "Author:",
2254 left_margin,
2255 y,
2256 label_font,
2257 label_color,
2258 TextAlign::Left,
2259 TextBaseline::Middle,
2260 );
2261 frame.fill_text_aligned(
2262 &model.metadata.author,
2263 value_x,
2264 y,
2265 value_font,
2266 text_color,
2267 TextAlign::Left,
2268 TextBaseline::Middle,
2269 );
2270 y += line_height;
2271
2272 frame.fill_text_aligned(
2274 "License:",
2275 left_margin,
2276 y,
2277 label_font,
2278 label_color,
2279 TextAlign::Left,
2280 TextBaseline::Middle,
2281 );
2282 frame.fill_text_aligned(
2283 &model.metadata.license,
2284 value_x,
2285 y,
2286 value_font,
2287 text_color,
2288 TextAlign::Left,
2289 TextBaseline::Middle,
2290 );
2291 y += line_height + 8.0;
2292
2293 frame.fill_text_aligned(
2295 "FLOW THEORY (DDA)",
2296 left_margin,
2297 y,
2298 label_font,
2299 label_color,
2300 TextAlign::Left,
2301 TextBaseline::Middle,
2302 );
2303 y += line_height;
2304
2305 frame.fill_text_aligned(
2307 "Adapt Rate:",
2308 left_margin,
2309 y,
2310 label_font,
2311 label_color,
2312 TextAlign::Left,
2313 TextBaseline::Middle,
2314 );
2315 let adapt_text = format!("{:.0}%", model.flow_theory.adaptation_rate * 100.0);
2316 frame.fill_text_aligned(
2317 &adapt_text,
2318 value_x,
2319 y,
2320 value_font,
2321 text_color,
2322 TextAlign::Left,
2323 TextBaseline::Middle,
2324 );
2325 y += line_height;
2326
2327 frame.fill_text_aligned(
2329 "Target Win:",
2330 left_margin,
2331 y,
2332 label_font,
2333 label_color,
2334 TextAlign::Left,
2335 TextBaseline::Middle,
2336 );
2337 let target_text = format!("{:.0}%", model.flow_theory.target_win_rate * 100.0);
2338 frame.fill_text_aligned(
2339 &target_text,
2340 value_x,
2341 y,
2342 value_font,
2343 text_color,
2344 TextAlign::Left,
2345 TextBaseline::Middle,
2346 );
2347 y += line_height;
2348
2349 frame.fill_text_aligned(
2351 "Flow Range:",
2352 left_margin,
2353 y,
2354 label_font,
2355 label_color,
2356 TextAlign::Left,
2357 TextBaseline::Middle,
2358 );
2359 let range_text = format!(
2360 "{:.0}%-{:.0}%",
2361 model.flow_theory.anxiety_threshold * 100.0,
2362 model.flow_theory.boredom_threshold * 100.0
2363 );
2364 frame.fill_text_aligned(
2365 &range_text,
2366 value_x,
2367 y,
2368 value_font,
2369 text_color,
2370 TextAlign::Left,
2371 TextBaseline::Middle,
2372 );
2373 y += line_height + 8.0;
2374
2375 frame.fill_text_aligned(
2377 "CURRENT STATE",
2378 left_margin,
2379 y,
2380 label_font,
2381 label_color,
2382 TextAlign::Left,
2383 TextBaseline::Middle,
2384 );
2385 y += line_height;
2386
2387 let ai_level = ai.map_or(5, crate::ai::PongAI::difficulty);
2389 let ai_name = ai.map_or("Normal", crate::ai::PongAI::difficulty_name);
2390 frame.fill_text_aligned(
2391 "Difficulty:",
2392 left_margin,
2393 y,
2394 label_font,
2395 label_color,
2396 TextAlign::Left,
2397 TextBaseline::Middle,
2398 );
2399 let level_text = format!("{ai_level}/9 {ai_name}");
2400 frame.fill_text_aligned(
2401 &level_text,
2402 value_x,
2403 y,
2404 value_font,
2405 highlight_color,
2406 TextAlign::Left,
2407 TextBaseline::Middle,
2408 );
2409 y += line_height;
2410
2411 let bar_x = left_margin;
2413 let bar_width = panel_width - 30.0;
2414 let bar_height = 12.0;
2415
2416 frame.fill_rect(
2418 bar_x,
2419 y,
2420 bar_width,
2421 bar_height,
2422 Color::new(0.15, 0.15, 0.2, 1.0),
2423 );
2424
2425 let fill_width = (f32::from(ai_level) / 9.0) * bar_width;
2427 let fill_color = match ai_level {
2428 0..=2 => Color::new(0.2, 0.8, 0.2, 1.0), 3..=5 => Color::new(0.8, 0.8, 0.2, 1.0), 6..=7 => Color::new(0.8, 0.5, 0.2, 1.0), _ => Color::new(0.8, 0.2, 0.2, 1.0), };
2433 frame.fill_rect(bar_x, y, fill_width, bar_height, fill_color);
2434
2435 frame.stroke_rect(
2437 bar_x,
2438 y,
2439 bar_width,
2440 bar_height,
2441 Color::new(0.4, 0.4, 0.6, 1.0),
2442 1.0,
2443 );
2444 y += bar_height + 8.0;
2445
2446 frame.fill_text_aligned(
2448 "File Size:",
2449 left_margin,
2450 y,
2451 label_font,
2452 label_color,
2453 TextAlign::Left,
2454 TextBaseline::Middle,
2455 );
2456 let size_text = format!("{} bytes", model.serialized_size());
2457 frame.fill_text_aligned(
2458 &size_text,
2459 value_x,
2460 y,
2461 value_font,
2462 text_color,
2463 TextAlign::Left,
2464 TextBaseline::Middle,
2465 );
2466
2467 frame.fill_text_aligned(
2469 "Press [I] or click Info to close",
2470 panel_x + panel_width / 2.0,
2471 panel_y + panel_height - 15.0,
2472 "10px monospace",
2473 Color::new(0.5, 0.5, 0.5, 0.8),
2474 TextAlign::Center,
2475 TextBaseline::Middle,
2476 );
2477 }
2478
2479 fn render_ai_explain_widget(&self, frame: &mut RenderFrame) {
2486 if !matches!(self.game_mode, GameMode::Demo | GameMode::SinglePlayer) {
2488 return;
2489 }
2490
2491 if self.game_mode == GameMode::Demo {
2493 if let Some(left_ai) = self.left_ai.as_ref() {
2495 self.render_single_ai_widget(frame, left_ai, 10.0, "P1 .apr Model");
2496 }
2497 if let Some(right_ai) = self.ai.as_ref() {
2499 self.render_single_ai_widget(frame, right_ai, self.width - 210.0, "P2 .apr Model");
2500 }
2501 } else {
2502 if let Some(left_ai) = self.left_ai.as_ref() {
2504 self.render_single_ai_widget(frame, left_ai, 10.0, ".apr ML Model");
2505 }
2506 }
2507 }
2508
2509 #[allow(clippy::unused_self, clippy::too_many_lines)]
2511 fn render_single_ai_widget(
2512 &self,
2513 frame: &mut RenderFrame,
2514 ai: &PongAI,
2515 widget_x: f32,
2516 title: &str,
2517 ) {
2518 let explanation = ai.explanation();
2519
2520 let widget_width = 200.0;
2522 let widget_height = 180.0;
2523 let widget_y = 80.0; frame.fill_rect(
2527 widget_x,
2528 widget_y,
2529 widget_width,
2530 widget_height,
2531 Color::new(0.02, 0.02, 0.08, 0.85),
2532 );
2533
2534 frame.stroke_rect(
2536 widget_x,
2537 widget_y,
2538 widget_width,
2539 widget_height,
2540 Color::new(0.2, 0.8, 0.9, 0.9),
2541 1.0,
2542 );
2543
2544 let title_font = "12px monospace";
2545 let small_font = "10px monospace";
2546 let tiny_font = "9px monospace";
2547
2548 let text_color = Color::WHITE;
2549 let label_color = Color::new(0.6, 0.7, 0.8, 1.0);
2550 let cyan = Color::new(0.2, 0.8, 0.9, 1.0);
2551
2552 let mut y = widget_y + 15.0;
2553 let left_margin = widget_x + 8.0;
2554 let line_height = 14.0;
2555
2556 let state_color = match explanation.state {
2558 crate::ai::DecisionState::Idle => Color::new(0.5, 0.5, 0.5, 1.0),
2559 crate::ai::DecisionState::Reacting => Color::new(1.0, 0.8, 0.2, 1.0),
2560 crate::ai::DecisionState::Tracking => Color::new(0.2, 1.0, 0.4, 1.0),
2561 crate::ai::DecisionState::Ready => Color::new(0.2, 0.6, 1.0, 1.0),
2562 };
2563
2564 frame.fill_text_aligned(
2565 title,
2566 left_margin,
2567 y,
2568 title_font,
2569 cyan,
2570 TextAlign::Left,
2571 TextBaseline::Middle,
2572 );
2573
2574 frame.fill_text_aligned(
2576 explanation.state.code(),
2577 widget_x + widget_width - 10.0,
2578 y,
2579 title_font,
2580 state_color,
2581 TextAlign::Right,
2582 TextBaseline::Middle,
2583 );
2584 y += line_height + 4.0;
2585
2586 frame.fill_rect(
2588 left_margin,
2589 y,
2590 widget_width - 16.0,
2591 1.0,
2592 Color::new(0.3, 0.4, 0.5, 0.6),
2593 );
2594 y += 6.0;
2595
2596 let bar_max_width = 80.0;
2598 let bar_height = 8.0;
2599 let bar_x = widget_x + widget_width - bar_max_width - 10.0;
2600
2601 for contrib in explanation.contributions.iter().take(4) {
2603 let name = if contrib.name.len() > 12 {
2605 format!("{}...", &contrib.name[..9])
2606 } else {
2607 contrib.name.clone()
2608 };
2609
2610 frame.fill_text_aligned(
2611 &name,
2612 left_margin,
2613 y + bar_height / 2.0,
2614 tiny_font,
2615 label_color,
2616 TextAlign::Left,
2617 TextBaseline::Middle,
2618 );
2619
2620 frame.fill_rect(
2622 bar_x,
2623 y,
2624 bar_max_width,
2625 bar_height,
2626 Color::new(0.1, 0.1, 0.15, 1.0),
2627 );
2628
2629 let bar_width = contrib.importance * bar_max_width;
2631 let bar_color = if contrib.contribution >= 0.0 {
2632 Color::new(0.2, 0.7, 0.3, 0.9) } else {
2634 Color::new(0.7, 0.3, 0.2, 0.9) };
2636 frame.fill_rect(bar_x, y, bar_width, bar_height, bar_color);
2637
2638 y += line_height;
2639 }
2640
2641 y += 4.0;
2642
2643 frame.fill_rect(
2645 left_margin,
2646 y,
2647 widget_width - 16.0,
2648 1.0,
2649 Color::new(0.3, 0.4, 0.5, 0.6),
2650 );
2651 y += 6.0;
2652
2653 let params_text = format!(
2655 "L{} | {:.0}ms | {:.0}%acc",
2656 explanation.difficulty_level,
2657 explanation.reaction_delay_ms,
2658 explanation.prediction_accuracy * 100.0
2659 );
2660 frame.fill_text_aligned(
2661 ¶ms_text,
2662 left_margin,
2663 y,
2664 tiny_font,
2665 label_color,
2666 TextAlign::Left,
2667 TextBaseline::Middle,
2668 );
2669 y += line_height;
2670
2671 let rationale = if explanation.rationale.len() > 30 {
2673 format!("{}...", &explanation.rationale[..27])
2674 } else {
2675 explanation.rationale.clone()
2676 };
2677 frame.fill_text_aligned(
2678 &rationale,
2679 left_margin,
2680 y,
2681 small_font,
2682 text_color,
2683 TextAlign::Left,
2684 TextBaseline::Middle,
2685 );
2686 }
2687}
2688
2689#[wasm_bindgen]
2694#[allow(missing_debug_implementations)] pub struct WebPlatform {
2696 config: WebConfig,
2698 timer: FrameTimer,
2700 input: InputState,
2702 render_frame: RenderFrame,
2704 #[allow(dead_code)]
2706 game: Option<Box<dyn WebGame>>,
2707 pong: PongGame,
2709 frame_count: u64,
2711 canvas_offset_x: f32,
2713 canvas_offset_y: f32,
2715 tracer: GameTracer,
2717}
2718
2719#[wasm_bindgen]
2720impl WebPlatform {
2721 #[wasm_bindgen(constructor)]
2731 pub fn new(config_json: &str) -> Result<Self, JsValue> {
2732 let config: WebConfig = serde_json::from_str(config_json)
2733 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
2734
2735 let fixed_dt = 1.0 / f64::from(config.target_fps);
2736 let mut timer = FrameTimer::new();
2737 timer.set_fixed_dt(fixed_dt);
2738
2739 let pong = PongGame::new(config.width as f32, config.height as f32, config.ai_enabled);
2740
2741 let tracer = if config.debug {
2743 GameTracer::debug()
2744 } else {
2745 GameTracer::production()
2746 };
2747
2748 Ok(Self {
2749 config,
2750 timer,
2751 input: InputState::new(),
2752 render_frame: RenderFrame::with_capacity(100),
2753 game: None,
2754 pong,
2755 frame_count: 0,
2756 canvas_offset_x: 0.0,
2757 canvas_offset_y: 0.0,
2758 tracer,
2759 })
2760 }
2761
2762 #[wasm_bindgen(js_name = "newDefault")]
2764 #[must_use]
2765 pub fn new_default() -> Self {
2766 let config = WebConfig::default();
2767 let pong = PongGame::new(config.width as f32, config.height as f32, config.ai_enabled);
2768
2769 Self {
2770 config,
2771 timer: FrameTimer::new(),
2772 input: InputState::new(),
2773 render_frame: RenderFrame::with_capacity(100),
2774 game: None,
2775 pong,
2776 frame_count: 0,
2777 canvas_offset_x: 0.0,
2778 canvas_offset_y: 0.0,
2779 tracer: GameTracer::production(), }
2781 }
2782
2783 #[wasm_bindgen(js_name = "setCanvasOffset")]
2785 #[allow(clippy::missing_const_for_fn)]
2786 pub fn set_canvas_offset(&mut self, x: f32, y: f32) {
2787 self.canvas_offset_x = x;
2788 self.canvas_offset_y = y;
2789 }
2790
2791 #[wasm_bindgen]
2804 pub fn frame(&mut self, timestamp: f64, input_events_json: &str) -> String {
2805 self.tracer.begin_frame();
2807
2808 let dt = self.timer.update(timestamp);
2810
2811 let canvas_offset = Vec2::new(self.canvas_offset_x, self.canvas_offset_y);
2813 let _ = process_input_events(input_events_json, &mut self.input, canvas_offset);
2815
2816 self.pong.update(&self.input, dt);
2818
2819 self.input.clear_events();
2821
2822 self.render_frame.clear();
2824 self.pong.render(&mut self.render_frame);
2825
2826 if self.config.debug {
2828 self.render_debug_info(dt);
2829 }
2830
2831 let audio_events = self.pong.take_audio_events();
2833
2834 let mut actions = Vec::new();
2836 if self.pong.download_requested {
2837 actions.push(JsAction::DownloadAiModel);
2838 self.pong.download_requested = false; }
2840 if self.pong.fullscreen_requested {
2841 if self.pong.is_fullscreen {
2842 actions.push(JsAction::EnterFullscreen);
2843 } else {
2844 actions.push(JsAction::ExitFullscreen);
2845 }
2846 self.pong.fullscreen_requested = false; }
2848
2849 let _ = self.tracer.end_frame(None);
2851
2852 let output = FrameOutput {
2854 commands: self.render_frame.commands.clone(),
2855 audio_events,
2856 actions,
2857 debug_info: if self.config.debug {
2858 let stats = self.tracer.stats();
2859 Some(DebugInfo {
2860 dt_ms: dt * 1000.0,
2861 fps: self.timer.average_fps(),
2862 frame_count: self.frame_count,
2863 input_summary: String::new(),
2864 game_mode: self.pong.game_mode().name().to_string(),
2865 speed_multiplier: self.pong.speed_multiplier().value(),
2866 left_paddle_y: self.pong.left_paddle_y(),
2867 right_paddle_y: self.pong.right_paddle_y(),
2868 ball_x: self.pong.ball_x(),
2869 ball_y: self.pong.ball_y(),
2870 trace_buffer_usage: Some(format!(
2871 "{}/{}",
2872 stats.buffer_len, stats.buffer_capacity
2873 )),
2874 trace_inputs: Some(stats.total_inputs),
2875 trace_dropped: Some(stats.frames_dropped),
2876 })
2877 } else {
2878 None
2879 },
2880 };
2881 self.frame_count += 1;
2882
2883 serde_json::to_string(&output).unwrap_or_else(|_| r#"{"commands":[]}"#.to_string())
2885 }
2886
2887 #[wasm_bindgen]
2894 pub fn resize(&mut self, width: u32, height: u32) {
2895 self.config.width = width;
2896 self.config.height = height;
2897 self.pong.resize(width, height);
2898 }
2899
2900 #[wasm_bindgen(js_name = "getConfig")]
2902 #[must_use]
2903 pub fn get_config(&self) -> String {
2904 self.config.to_json().unwrap_or_else(|_| "{}".to_string())
2905 }
2906
2907 #[wasm_bindgen(js_name = "getStats")]
2909 #[must_use]
2910 pub fn get_stats(&self) -> String {
2911 let stats = serde_json::json!({
2912 "fps": self.timer.average_fps(),
2913 "frame_count": self.timer.frame_count(),
2914 "total_time": self.timer.total_time(),
2915 });
2916 stats.to_string()
2917 }
2918
2919 #[wasm_bindgen(js_name = "resetTimer")]
2921 pub fn reset_timer(&mut self) {
2922 self.timer.reset();
2923 }
2924
2925 #[wasm_bindgen(js_name = "getAiModel")]
2927 #[must_use]
2928 pub fn get_ai_model(&self) -> String {
2929 self.pong.export_ai_model()
2930 }
2931
2932 #[wasm_bindgen(js_name = "getAiInfo")]
2934 #[must_use]
2935 pub fn get_ai_info(&self) -> String {
2936 self.pong.ai_info()
2937 }
2938
2939 #[wasm_bindgen(js_name = "setAiDifficulty")]
2941 pub fn set_ai_difficulty(&mut self, level: u8) {
2942 self.pong.set_ai_difficulty(level);
2943 }
2944
2945 #[wasm_bindgen(js_name = "getAiDifficulty")]
2947 #[must_use]
2948 pub fn get_ai_difficulty(&self) -> u8 {
2949 self.pong.ai_difficulty()
2950 }
2951
2952 #[wasm_bindgen(js_name = "setSpeed")]
2954 #[allow(clippy::match_same_arms)] pub fn set_speed(&mut self, speed: u32) {
2956 self.pong.set_speed_multiplier(match speed {
2957 5 => SpeedMultiplier::Fast5x,
2958 10 => SpeedMultiplier::Fast10x,
2959 50 => SpeedMultiplier::Fast50x,
2960 100 => SpeedMultiplier::Fast100x,
2961 1000 => SpeedMultiplier::Fast1000x,
2962 _ => SpeedMultiplier::Normal, });
2964 }
2965
2966 #[wasm_bindgen(js_name = "getSpeed")]
2968 #[must_use]
2969 #[allow(clippy::missing_const_for_fn)] pub fn get_speed(&self) -> u32 {
2971 self.pong.speed_multiplier().value()
2972 }
2973
2974 #[wasm_bindgen(js_name = "setGameMode")]
2976 #[allow(clippy::match_same_arms)] pub fn set_game_mode(&mut self, mode: &str) {
2978 self.pong.set_game_mode(match mode.to_lowercase().as_str() {
2979 "demo" => GameMode::Demo,
2980 "2p" | "two" | "twoplayer" => GameMode::TwoPlayer,
2981 _ => GameMode::SinglePlayer, });
2983 }
2984
2985 #[wasm_bindgen(js_name = "getGameMode")]
2987 #[must_use]
2988 pub fn get_game_mode(&self) -> String {
2989 self.pong.game_mode().short_label().to_string()
2990 }
2991
2992 fn render_debug_info(&mut self, dt: f64) {
2993 let fps = if dt > 0.0 { 1.0 / dt } else { 0.0 };
2994 let debug_text = format!("FPS: {:.0} | Frame: {}", fps, self.timer.frame_count());
2995
2996 self.render_frame.fill_text_aligned(
2997 &debug_text,
2998 10.0,
2999 self.config.height as f32 - 10.0,
3000 "14px monospace",
3001 Color::GREEN,
3002 TextAlign::Left,
3003 TextBaseline::Bottom,
3004 );
3005 }
3006}
3007
3008impl WebPlatform {
3010 #[must_use]
3012 pub fn new_for_test(config: WebConfig) -> Self {
3013 let pong = PongGame::new(config.width as f32, config.height as f32, config.ai_enabled);
3014 let tracer = if config.debug {
3015 GameTracer::debug()
3016 } else {
3017 GameTracer::new(TracerConfig::default())
3018 };
3019
3020 Self {
3021 config,
3022 timer: FrameTimer::new(),
3023 input: InputState::new(),
3024 render_frame: RenderFrame::with_capacity(100),
3025 game: None,
3026 pong,
3027 frame_count: 0,
3028 canvas_offset_x: 0.0,
3029 canvas_offset_y: 0.0,
3030 tracer,
3031 }
3032 }
3033
3034 #[must_use]
3036 pub const fn input(&self) -> &InputState {
3037 &self.input
3038 }
3039
3040 #[allow(clippy::missing_const_for_fn)] pub fn input_mut(&mut self) -> &mut InputState {
3043 &mut self.input
3044 }
3045
3046 #[must_use]
3048 pub const fn timer(&self) -> &FrameTimer {
3049 &self.timer
3050 }
3051
3052 #[must_use]
3054 pub const fn pong(&self) -> &PongGame {
3055 &self.pong
3056 }
3057
3058 #[must_use]
3060 pub const fn tracer(&self) -> &GameTracer {
3061 &self.tracer
3062 }
3063
3064 #[must_use]
3066 pub const fn config(&self) -> &WebConfig {
3067 &self.config
3068 }
3069}
3070
3071#[cfg(test)]
3072#[allow(
3073 clippy::unwrap_used,
3074 clippy::expect_used,
3075 clippy::panic,
3076 clippy::float_cmp,
3077 clippy::manual_range_contains,
3078 clippy::cast_lossless,
3079 clippy::suboptimal_flops,
3080 clippy::uninlined_format_args
3081)]
3082mod tests {
3083 use super::*;
3084
3085 #[test]
3086 fn test_web_config_default() {
3087 let config = WebConfig::default();
3088 assert_eq!(config.width, 800);
3089 assert_eq!(config.height, 600);
3090 assert_eq!(config.target_fps, 60);
3091 assert!(!config.debug);
3092 }
3093
3094 #[test]
3095 fn test_web_config_new() {
3096 let config = WebConfig::new(1920, 1080);
3097 assert_eq!(config.width, 1920);
3098 assert_eq!(config.height, 1080);
3099 assert_eq!(config.target_fps, 60);
3100 }
3101
3102 #[test]
3103 fn test_web_config_from_json() {
3104 let json = r#"{"width":1024,"height":768,"target_fps":30,"debug":true}"#;
3105 let config = WebConfig::from_json(json).unwrap();
3106 assert_eq!(config.width, 1024);
3107 assert_eq!(config.height, 768);
3108 assert_eq!(config.target_fps, 30);
3109 assert!(config.debug);
3110 }
3111
3112 #[test]
3113 fn test_web_config_from_json_invalid() {
3114 let result = WebConfig::from_json("not valid json");
3115 assert!(result.is_err());
3116 }
3117
3118 #[test]
3119 fn test_web_config_to_json() {
3120 let config = WebConfig::new(800, 600);
3121 let json = config.to_json().unwrap();
3122 assert!(json.contains("800"));
3123 assert!(json.contains("600"));
3124 }
3125
3126 #[test]
3127 fn test_web_platform_error_display() {
3128 let err = WebPlatformError::InvalidConfig("bad config".to_string());
3129 assert_eq!(err.to_string(), "Invalid config: bad config");
3130
3131 let err = WebPlatformError::InputError("input failed".to_string());
3132 assert_eq!(err.to_string(), "Input error: input failed");
3133
3134 let err = WebPlatformError::RenderError("render failed".to_string());
3135 assert_eq!(err.to_string(), "Render error: render failed");
3136 }
3137
3138 #[test]
3139 fn test_web_platform_error_from_input_error() {
3140 let input_err = InputTranslationError::InvalidJson("test".to_string());
3141 let platform_err: WebPlatformError = input_err.into();
3142 assert!(matches!(platform_err, WebPlatformError::InputError(_)));
3143 }
3144
3145 #[test]
3146 fn test_pong_game_new() {
3147 let game = PongGame::new(800.0, 600.0, false);
3148 assert!((game.width - 800.0).abs() < f32::EPSILON);
3149 assert!((game.height - 600.0).abs() < f32::EPSILON);
3150 assert_eq!(game.left_score(), 0);
3151 assert_eq!(game.right_score(), 0);
3152 }
3153
3154 #[test]
3155 fn test_pong_game_default() {
3156 let game = PongGame::default();
3157 assert!((game.width - 800.0).abs() < f32::EPSILON);
3158 assert!((game.height - 600.0).abs() < f32::EPSILON);
3159 }
3160
3161 #[test]
3162 fn test_pong_game_ball_position() {
3163 let game = PongGame::new(800.0, 600.0, false);
3164 let (x, y) = game.ball_position();
3165 assert!((x - 400.0).abs() < f32::EPSILON);
3166 assert!((y - 300.0).abs() < f32::EPSILON);
3167 }
3168
3169 #[test]
3170 fn test_pong_game_update_no_input() {
3171 let mut game = PongGame::new(800.0, 600.0, false);
3172 game.set_state(GameState::Playing); let input = InputState::new();
3174 let initial_ball_x = game.ball_x;
3175
3176 game.update(&input, 0.016);
3177
3178 assert!((game.ball_x - initial_ball_x).abs() > 1.0);
3180 }
3181
3182 #[test]
3183 fn test_pong_game_paddle_movement() {
3184 let mut game = PongGame::new(800.0, 600.0, false);
3185 game.set_state(GameState::Playing); game.game_mode = GameMode::SinglePlayer; let mut input = InputState::new();
3188 let initial_y = game.right_paddle_y;
3189
3190 input.set_key_pressed(jugar_input::KeyCode::Up, true);
3192 game.update(&input, 0.1);
3193
3194 assert!(game.right_paddle_y < initial_y);
3196 }
3197
3198 #[test]
3199 fn test_pong_game_paddle_clamping() {
3200 let mut game = PongGame::new(800.0, 600.0, false);
3201 game.set_state(GameState::Playing); game.game_mode = GameMode::SinglePlayer; let mut input = InputState::new();
3204
3205 input.set_key_pressed(jugar_input::KeyCode::Letter('W'), true);
3207 for _ in 0..100 {
3208 game.update(&input, 0.1);
3209 }
3210
3211 let half_paddle = game.paddle_height / 2.0;
3213 assert!(game.left_paddle_y >= half_paddle);
3214 }
3215
3216 #[test]
3217 fn test_pong_game_render() {
3218 let mut game = PongGame::new(800.0, 600.0, false);
3219 let mut frame = RenderFrame::new();
3220
3221 game.render(&mut frame);
3222
3223 assert!(frame.len() > 5);
3225 }
3226
3227 #[test]
3228 fn test_pong_game_resize() {
3229 let mut game = PongGame::new(800.0, 600.0, false);
3230 game.resize(1600, 1200);
3231
3232 assert!((game.width - 1600.0).abs() < f32::EPSILON);
3233 assert!((game.height - 1200.0).abs() < f32::EPSILON);
3234 }
3235
3236 #[test]
3237 fn test_web_platform_new_for_test() {
3238 let config = WebConfig::default();
3239 let platform = WebPlatform::new_for_test(config);
3240
3241 assert_eq!(platform.config().width, 800);
3242 assert_eq!(platform.config().height, 600);
3243 }
3244
3245 #[test]
3246 fn test_web_platform_frame() {
3247 let config = WebConfig::default();
3248 let mut platform = WebPlatform::new_for_test(config);
3249
3250 let result = platform.frame(0.0, "[]");
3252 assert!(!result.is_empty());
3253 assert!(result.contains("Clear"));
3254
3255 let result = platform.frame(16.667, "[]");
3257 assert!(!result.is_empty());
3258 }
3259
3260 #[test]
3261 fn test_web_platform_frame_with_input() {
3262 let config = WebConfig::default();
3263 let mut platform = WebPlatform::new_for_test(config);
3264
3265 let input_json = r#"[{"event_type":"KeyDown","timestamp":0,"data":{"key":"KeyW"}}]"#;
3266 let _ = platform.frame(0.0, input_json);
3267
3268 assert!(platform
3270 .input()
3271 .is_key_pressed(jugar_input::KeyCode::Letter('W')));
3272 }
3273
3274 #[test]
3275 fn test_web_platform_resize() {
3276 let config = WebConfig::default();
3277 let mut platform = WebPlatform::new_for_test(config);
3278
3279 platform.resize(1920, 1080);
3280
3281 assert_eq!(platform.config().width, 1920);
3282 assert_eq!(platform.config().height, 1080);
3283 }
3284
3285 #[test]
3286 fn test_web_platform_get_config() {
3287 let config = WebConfig::default();
3288 let platform = WebPlatform::new_for_test(config);
3289
3290 let config_json = platform.get_config();
3291 assert!(config_json.contains("800"));
3292 assert!(config_json.contains("600"));
3293 }
3294
3295 #[test]
3296 fn test_web_platform_get_stats() {
3297 let config = WebConfig::default();
3298 let mut platform = WebPlatform::new_for_test(config);
3299
3300 let _ = platform.frame(0.0, "[]");
3302 let _ = platform.frame(16.667, "[]");
3303 let _ = platform.frame(33.333, "[]");
3304
3305 let stats = platform.get_stats();
3306 assert!(stats.contains("fps"));
3307 assert!(stats.contains("frame_count"));
3308 }
3309
3310 #[test]
3311 fn test_web_platform_reset_timer() {
3312 let config = WebConfig::default();
3313 let mut platform = WebPlatform::new_for_test(config);
3314
3315 let _ = platform.frame(0.0, "[]");
3316 let _ = platform.frame(1000.0, "[]");
3317
3318 platform.reset_timer();
3319
3320 assert_eq!(platform.timer().frame_count(), 0);
3321 }
3322
3323 #[test]
3324 fn test_web_platform_debug_mode() {
3325 let config = WebConfig {
3326 debug: true,
3327 ..WebConfig::default()
3328 };
3329 let mut platform = WebPlatform::new_for_test(config);
3330
3331 let _ = platform.frame(0.0, "[]");
3332 let result = platform.frame(16.667, "[]");
3333
3334 assert!(result.contains("FPS"));
3336 }
3337
3338 #[test]
3339 fn test_web_platform_input_accessors() {
3340 let config = WebConfig::default();
3341 let mut platform = WebPlatform::new_for_test(config);
3342
3343 platform
3345 .input_mut()
3346 .set_key_pressed(jugar_input::KeyCode::Space, true);
3347
3348 assert!(platform.input().is_key_pressed(jugar_input::KeyCode::Space));
3350 }
3351
3352 #[test]
3353 fn test_web_platform_pong_accessor() {
3354 let config = WebConfig::default();
3355 let platform = WebPlatform::new_for_test(config);
3356
3357 let pong = platform.pong();
3358 assert_eq!(pong.left_score(), 0);
3359 assert_eq!(pong.right_score(), 0);
3360 }
3361
3362 #[test]
3363 fn test_pong_ball_wall_collision_top() {
3364 let mut game = PongGame::new(800.0, 600.0, false);
3365 game.set_state(GameState::Playing); game.ball_y = 5.0; game.ball_vy = -100.0; let input = InputState::new();
3370 game.update(&input, 0.1);
3371
3372 assert!(game.ball_vy > 0.0);
3374 }
3375
3376 #[test]
3377 fn test_pong_ball_wall_collision_bottom() {
3378 let mut game = PongGame::new(800.0, 600.0, false);
3379 game.set_state(GameState::Playing); game.ball_y = 595.0; game.ball_vy = 100.0; let input = InputState::new();
3384 game.update(&input, 0.1);
3385
3386 assert!(game.ball_vy < 0.0);
3388 }
3389
3390 #[test]
3391 fn test_pong_scoring_right() {
3392 let mut game = PongGame::new(800.0, 600.0, false);
3393 game.set_state(GameState::Playing); game.ball_x = -10.0; let input = InputState::new();
3397 game.update(&input, 0.016);
3398
3399 assert_eq!(game.right_score(), 1);
3401 }
3402
3403 #[test]
3404 fn test_pong_scoring_left() {
3405 let mut game = PongGame::new(800.0, 600.0, false);
3406 game.set_state(GameState::Playing); game.ball_x = 810.0; let input = InputState::new();
3410 game.update(&input, 0.016);
3411
3412 assert_eq!(game.left_score(), 1);
3414 }
3415
3416 #[test]
3417 fn test_frame_output_serialization() {
3418 let output = FrameOutput {
3419 commands: vec![Canvas2DCommand::Clear {
3420 color: Color::BLACK,
3421 }],
3422 audio_events: vec![],
3423 actions: vec![],
3424 debug_info: None,
3425 };
3426
3427 let json = serde_json::to_string(&output).unwrap();
3428 assert!(json.contains("commands"));
3429 assert!(!json.contains("debug_info")); assert!(!json.contains("audio_events")); }
3432
3433 #[test]
3434 fn test_frame_output_with_debug() {
3435 let output = FrameOutput {
3436 commands: vec![],
3437 audio_events: vec![],
3438 actions: vec![],
3439 debug_info: Some(DebugInfo {
3440 dt_ms: 16.667,
3441 fps: 60.0,
3442 frame_count: 100,
3443 input_summary: "W pressed".to_string(),
3444 game_mode: "SinglePlayer".to_string(),
3445 speed_multiplier: 1,
3446 left_paddle_y: 300.0,
3447 right_paddle_y: 300.0,
3448 ball_x: 400.0,
3449 ball_y: 300.0,
3450 trace_buffer_usage: Some("100/3600".to_string()),
3451 trace_inputs: Some(42),
3452 trace_dropped: Some(0),
3453 }),
3454 };
3455
3456 let json = serde_json::to_string(&output).unwrap();
3457 assert!(json.contains("debug_info"));
3458 assert!(json.contains("fps"));
3459 }
3460
3461 #[test]
3462 fn test_frame_output_with_audio() {
3463 let output = FrameOutput {
3464 commands: vec![],
3465 audio_events: vec![AudioEvent::GameStart { volume: 0.7 }],
3466 actions: vec![],
3467 debug_info: None,
3468 };
3469
3470 let json = serde_json::to_string(&output).unwrap();
3471 assert!(json.contains("audio_events"));
3472 assert!(json.contains("GameStart"));
3473 }
3474
3475 #[test]
3476 fn test_debug_info_serialization() {
3477 let info = DebugInfo {
3478 dt_ms: 16.667,
3479 fps: 60.0,
3480 frame_count: 42,
3481 input_summary: "test".to_string(),
3482 game_mode: "Demo".to_string(),
3483 speed_multiplier: 10,
3484 left_paddle_y: 300.0,
3485 right_paddle_y: 300.0,
3486 ball_x: 400.0,
3487 ball_y: 300.0,
3488 trace_buffer_usage: None,
3489 trace_inputs: None,
3490 trace_dropped: None,
3491 };
3492
3493 let json = serde_json::to_string(&info).unwrap();
3494 assert!(json.contains("16.667"));
3495 assert!(json.contains("60"));
3496 assert!(!json.contains("trace_buffer_usage"));
3498 assert!(json.contains("42"));
3499 assert!(json.contains("Demo"));
3500 assert!(json.contains("10"));
3501 assert!(json.contains("left_paddle_y"));
3502 assert!(json.contains("ball_x"));
3503 }
3504
3505 #[test]
3506 fn test_pong_right_paddle_controls() {
3507 let mut game = PongGame::new(800.0, 600.0, false);
3508 game.set_state(GameState::Playing); game.game_mode = GameMode::TwoPlayer; let mut input = InputState::new();
3511 let initial_y = game.right_paddle_y;
3512
3513 input.set_key_pressed(jugar_input::KeyCode::Down, true);
3515 game.update(&input, 0.1);
3516
3517 assert!(game.right_paddle_y > initial_y);
3519 }
3520
3521 #[test]
3522 fn test_pong_right_paddle_up() {
3523 let mut game = PongGame::new(800.0, 600.0, false);
3524 game.set_state(GameState::Playing); game.game_mode = GameMode::TwoPlayer; let mut input = InputState::new();
3527 let initial_y = game.right_paddle_y;
3528
3529 input.set_key_pressed(jugar_input::KeyCode::Up, true);
3531 game.update(&input, 0.1);
3532
3533 assert!(game.right_paddle_y < initial_y);
3535 }
3536
3537 #[test]
3538 fn test_pong_right_paddle_down() {
3539 let mut game = PongGame::new(800.0, 600.0, false);
3540 game.set_state(GameState::Playing); game.game_mode = GameMode::SinglePlayer; let mut input = InputState::new();
3543 let initial_y = game.right_paddle_y;
3544
3545 input.set_key_pressed(jugar_input::KeyCode::Down, true);
3547 game.update(&input, 0.1);
3548
3549 assert!(game.right_paddle_y > initial_y);
3551 }
3552
3553 #[test]
3554 fn test_pong_paddle_collision_left() {
3555 let mut game = PongGame::new(800.0, 600.0, false);
3556 game.set_state(GameState::Playing); game.ball_x = 46.0;
3562 game.ball_y = game.left_paddle_y;
3563 game.ball_vx = -100.0; let input = InputState::new();
3566 game.update(&input, 0.016);
3567
3568 assert!(game.ball_vx > 0.0);
3570 }
3571
3572 #[test]
3573 fn test_pong_paddle_collision_right() {
3574 let mut game = PongGame::new(800.0, 600.0, false);
3575 game.set_state(GameState::Playing); game.ball_x = 758.0;
3580 game.ball_y = game.right_paddle_y;
3581 game.ball_vx = 200.0;
3582
3583 let input = InputState::new();
3584 game.update(&input, 0.016);
3585
3586 assert!(game.ball_vx < 0.0);
3588 }
3589
3590 #[test]
3591 fn test_pong_resize_scales_positions() {
3592 let mut game = PongGame::new(800.0, 600.0, false);
3593 game.ball_x = 400.0;
3595 game.ball_y = 300.0;
3596
3597 game.resize(1600, 1200);
3598
3599 assert!((game.ball_x - 800.0).abs() < 1.0);
3601 assert!((game.ball_y - 600.0).abs() < 1.0);
3602 }
3603
3604 #[test]
3609 fn test_game_state_default_is_playing() {
3610 let game = PongGame::new(800.0, 600.0, false);
3612 assert_eq!(game.state(), GameState::Playing);
3613 }
3614
3615 #[test]
3616 fn test_game_state_start_from_menu() {
3617 let mut game = PongGame::new(800.0, 600.0, false);
3619 game.set_state(GameState::Menu);
3620 assert_eq!(game.state(), GameState::Menu);
3621
3622 game.start();
3623
3624 assert_eq!(game.state(), GameState::Playing);
3625 }
3626
3627 #[test]
3628 fn test_game_state_pause_transitions() {
3629 let mut game = PongGame::new(800.0, 600.0, false);
3630 game.set_state(GameState::Playing);
3631
3632 let mut input = InputState::new();
3634 input.set_key_pressed(jugar_input::KeyCode::Escape, true);
3635 game.update(&input, 0.016);
3636
3637 assert_eq!(game.state(), GameState::Paused);
3638 }
3639
3640 #[test]
3641 fn test_game_state_unpause_with_space() {
3642 let mut game = PongGame::new(800.0, 600.0, false);
3643 game.set_state(GameState::Paused);
3644
3645 let input = InputState::new();
3647 game.update(&input, 0.016);
3648
3649 let mut input = InputState::new();
3651 input.set_key_pressed(jugar_input::KeyCode::Space, true);
3652 game.update(&input, 0.016);
3653
3654 assert_eq!(game.state(), GameState::Playing);
3655 }
3656
3657 #[test]
3658 fn test_game_state_menu_space_starts_game() {
3659 let mut game = PongGame::new(800.0, 600.0, false);
3661 game.set_state(GameState::Menu); assert_eq!(game.state(), GameState::Menu);
3663
3664 let input = InputState::new();
3666 game.update(&input, 0.016);
3667
3668 let mut input = InputState::new();
3670 input.set_key_pressed(jugar_input::KeyCode::Space, true);
3671 game.update(&input, 0.016);
3672
3673 assert_eq!(game.state(), GameState::Playing);
3674 }
3675
3676 #[test]
3677 fn test_game_state_game_over_on_winning_score() {
3678 let mut game = PongGame::new(800.0, 600.0, false);
3679 game.set_state(GameState::Playing);
3680 game.left_score = 10; game.ball_x = 810.0; let input = InputState::new();
3684 game.update(&input, 0.016);
3685
3686 assert_eq!(game.left_score(), 11);
3687 assert_eq!(game.state(), GameState::GameOver);
3688 }
3689
3690 #[test]
3691 fn test_game_state_reset_from_game_over() {
3692 let mut game = PongGame::new(800.0, 600.0, false);
3693 game.set_state(GameState::GameOver);
3694 game.left_score = 11;
3695 game.right_score = 5;
3696
3697 let input = InputState::new();
3699 game.update(&input, 0.016);
3700
3701 let mut input = InputState::new();
3703 input.set_key_pressed(jugar_input::KeyCode::Space, true);
3704 game.update(&input, 0.016);
3705
3706 assert_eq!(game.state(), GameState::Playing);
3707 assert_eq!(game.left_score(), 0);
3708 assert_eq!(game.right_score(), 0);
3709 }
3710
3711 #[test]
3712 fn test_game_state_no_update_when_paused() {
3713 let mut game = PongGame::new(800.0, 600.0, false);
3714 game.set_state(GameState::Paused);
3715 let initial_ball_x = game.ball_x;
3716
3717 let input = InputState::new();
3718 game.update(&input, 0.016);
3719
3720 assert!((game.ball_x - initial_ball_x).abs() < 0.001);
3722 }
3723
3724 #[test]
3725 fn test_game_state_reset_game() {
3726 let mut game = PongGame::new(800.0, 600.0, false);
3727 game.set_state(GameState::Playing);
3728 game.left_score = 5;
3729 game.right_score = 3;
3730 game.ball_x = 100.0;
3731
3732 game.reset_game();
3733
3734 assert_eq!(game.left_score(), 0);
3735 assert_eq!(game.right_score(), 0);
3736 assert!((game.ball_x - 400.0).abs() < 1.0); }
3738
3739 #[test]
3744 fn test_rally_counter_starts_at_zero() {
3745 let game = PongGame::new(800.0, 600.0, false);
3746 assert_eq!(game.rally_count(), 0);
3747 }
3748
3749 #[test]
3750 fn test_rally_counter_increments_on_paddle_hit() {
3751 let mut game = PongGame::new(800.0, 600.0, false);
3752 game.set_state(GameState::Playing);
3753 assert_eq!(game.rally_count(), 0);
3754
3755 game.ball_x = 46.0;
3757 game.ball_y = game.left_paddle_y;
3758 game.ball_vx = -100.0;
3759
3760 let input = InputState::new();
3761 game.update(&input, 0.016);
3762
3763 assert_eq!(game.rally_count(), 1);
3765 }
3766
3767 #[test]
3768 fn test_rally_counter_resets_on_scoring() {
3769 let mut game = PongGame::new(800.0, 600.0, false);
3770 game.set_state(GameState::Playing);
3771 game.rally_count = 5; game.ball_x = -10.0; let input = InputState::new();
3775 game.update(&input, 0.016);
3776
3777 assert_eq!(game.rally_count(), 0);
3779 }
3780
3781 #[test]
3782 fn test_rally_counter_resets_on_game_reset() {
3783 let mut game = PongGame::new(800.0, 600.0, false);
3784 game.rally_count = 10;
3785
3786 game.reset_game();
3787
3788 assert_eq!(game.rally_count(), 0);
3789 }
3790
3791 #[test]
3796 fn test_high_score_starts_at_zero() {
3797 let game = PongGame::new(800.0, 600.0, false);
3798 assert_eq!(game.high_score(), 0);
3799 }
3800
3801 #[test]
3802 fn test_high_score_updates_when_rally_ends() {
3803 let mut game = PongGame::new(800.0, 600.0, false);
3804 game.set_state(GameState::Playing);
3805 game.rally_count = 8;
3806 game.ball_x = -10.0; let input = InputState::new();
3809 game.update(&input, 0.016);
3810
3811 assert_eq!(game.high_score(), 8);
3813 }
3814
3815 #[test]
3816 fn test_high_score_only_updates_if_higher() {
3817 let mut game = PongGame::new(800.0, 600.0, false);
3818 game.set_state(GameState::Playing);
3819 game.high_score = 10;
3820 game.rally_count = 5;
3821 game.ball_x = -10.0; let input = InputState::new();
3824 game.update(&input, 0.016);
3825
3826 assert_eq!(game.high_score(), 10);
3828 }
3829
3830 #[test]
3831 fn test_high_score_persists_across_reset() {
3832 let mut game = PongGame::new(800.0, 600.0, false);
3833 game.high_score = 15;
3834
3835 game.reset_game();
3836
3837 assert_eq!(game.high_score(), 15);
3839 }
3840
3841 #[test]
3842 fn test_high_score_updates_on_both_sides_scoring() {
3843 let mut game = PongGame::new(800.0, 600.0, false);
3844 game.set_state(GameState::Playing);
3845
3846 game.rally_count = 7;
3848 game.ball_x = -10.0;
3849 let input = InputState::new();
3850 game.update(&input, 0.016);
3851 assert_eq!(game.high_score(), 7);
3852
3853 game.ball_x = 400.0;
3855
3856 game.rally_count = 12;
3858 game.ball_x = 810.0;
3859 game.update(&input, 0.016);
3860 assert_eq!(game.high_score(), 12);
3861 }
3862
3863 #[test]
3868 fn test_bg_time_starts_at_zero() {
3869 let game = PongGame::new(800.0, 600.0, false);
3870 assert!((game.bg_time - 0.0).abs() < f32::EPSILON);
3871 }
3872
3873 #[test]
3874 fn test_bg_time_increments_during_gameplay() {
3875 let mut game = PongGame::new(800.0, 600.0, false);
3876 game.set_state(GameState::Playing);
3877 assert!((game.bg_time - 0.0).abs() < f32::EPSILON);
3878
3879 let input = InputState::new();
3880 game.update(&input, 0.5);
3881
3882 assert!((game.bg_time - 0.5).abs() < 0.001);
3883 }
3884
3885 #[test]
3890 fn test_hud_buttons_default() {
3891 let hud = HudButtons::default();
3892 assert_eq!(hud.mode_demo.x, 0.0);
3894 assert_eq!(hud.mode_1p.x, 0.0);
3895 assert_eq!(hud.mode_2p.x, 0.0);
3896 assert_eq!(hud.speed_1x.x, 0.0);
3897 }
3898
3899 #[test]
3900 fn test_hud_buttons_calculate() {
3901 let hud = HudButtons::calculate(800.0, 600.0);
3902 assert!(hud.mode_demo.x > 0.0);
3904 assert!(hud.mode_1p.x > hud.mode_demo.x);
3905 assert!(hud.mode_2p.x > hud.mode_1p.x);
3906 assert!(hud.speed_1x.x > 0.0);
3908 }
3909
3910 #[test]
3911 fn test_hud_buttons_hit_test() {
3912 let hud = HudButtons::calculate(800.0, 600.0);
3913
3914 let test_x = hud.mode_demo.x + hud.mode_demo.width / 2.0;
3916 let test_y = hud.mode_demo.y + hud.mode_demo.height / 2.0;
3917
3918 assert!(hud.mode_demo.contains(test_x, test_y));
3920 }
3921
3922 #[test]
3923 fn test_button_rect_new() {
3924 let btn = ButtonRect::new(10.0, 20.0, 100.0, 50.0);
3925 assert_eq!(btn.x, 10.0);
3926 assert_eq!(btn.y, 20.0);
3927 assert_eq!(btn.width, 100.0);
3928 assert_eq!(btn.height, 50.0);
3929 }
3930
3931 #[test]
3932 fn test_button_rect_contains() {
3933 let btn = ButtonRect::new(10.0, 10.0, 100.0, 50.0);
3934 assert!(btn.contains(50.0, 30.0));
3936 assert!(!btn.contains(5.0, 30.0));
3938 assert!(!btn.contains(120.0, 30.0));
3940 assert!(!btn.contains(50.0, 5.0));
3942 assert!(!btn.contains(50.0, 65.0));
3944 }
3945
3946 #[test]
3947 fn test_web_platform_mode_toggle_via_d_key() {
3948 let config = WebConfig::default();
3949 let mut platform = WebPlatform::new_for_test(config);
3950
3951 let _ = platform.frame(0.0, "[]");
3953
3954 let d_down = r#"[{"event_type":"KeyDown","timestamp":100,"data":{"key":"KeyD"}}]"#;
3956 let result = platform.frame(100.0, d_down);
3957 assert!(result.contains("Demo") || result.contains("SinglePlayer"));
3958
3959 let d_up = r#"[{"event_type":"KeyUp","timestamp":116,"data":{"key":"KeyD"}}]"#;
3961 let _ = platform.frame(116.0, d_up);
3962 }
3963
3964 #[test]
3965 fn test_web_platform_mode_cycle_via_m_key() {
3966 let config = WebConfig::default();
3967 let mut platform = WebPlatform::new_for_test(config);
3968
3969 let _ = platform.frame(0.0, "[]");
3970
3971 let m_down = r#"[{"event_type":"KeyDown","timestamp":100,"data":{"key":"KeyM"}}]"#;
3973 let _ = platform.frame(100.0, m_down);
3974
3975 let m_up = r#"[{"event_type":"KeyUp","timestamp":116,"data":{"key":"KeyM"}}]"#;
3976 let _ = platform.frame(116.0, m_up);
3977 }
3978
3979 #[test]
3980 fn test_web_platform_speed_keys_1_through_6() {
3981 let config = WebConfig::default();
3982 let mut platform = WebPlatform::new_for_test(config);
3983
3984 let _ = platform.frame(0.0, "[]");
3985
3986 let key_3 = r#"[{"event_type":"KeyDown","timestamp":100,"data":{"key":"Digit3"}}]"#;
3988 let _ = platform.frame(100.0, key_3);
3989
3990 let key_6 = r#"[{"event_type":"KeyDown","timestamp":200,"data":{"key":"Digit6"}}]"#;
3992 let _ = platform.frame(200.0, key_6);
3993
3994 let key_1 = r#"[{"event_type":"KeyDown","timestamp":300,"data":{"key":"Digit1"}}]"#;
3996 let _ = platform.frame(300.0, key_1);
3997 }
3998
3999 #[test]
4000 fn test_web_platform_fullscreen_via_f_key() {
4001 let config = WebConfig::default();
4002 let mut platform = WebPlatform::new_for_test(config);
4003
4004 let _ = platform.frame(0.0, "[]");
4005
4006 let key_f = r#"[{"event_type":"KeyDown","timestamp":100,"data":{"key":"KeyF"}}]"#;
4008 let output = platform.frame(100.0, key_f);
4009
4010 assert!(
4012 output.contains("EnterFullscreen"),
4013 "F key should trigger EnterFullscreen action"
4014 );
4015
4016 let key_f_up = r#"[{"event_type":"KeyUp","timestamp":116,"data":{"key":"KeyF"}}]"#;
4018 let _ = platform.frame(116.0, key_f_up);
4019
4020 let output = platform.frame(200.0, key_f);
4022 assert!(
4023 output.contains("ExitFullscreen"),
4024 "F key should toggle to ExitFullscreen action"
4025 );
4026 }
4027
4028 #[test]
4029 fn test_web_platform_fullscreen_via_f11_key() {
4030 let config = WebConfig::default();
4031 let mut platform = WebPlatform::new_for_test(config);
4032
4033 let _ = platform.frame(0.0, "[]");
4034
4035 let key_f11 = r#"[{"event_type":"KeyDown","timestamp":100,"data":{"key":"F11"}}]"#;
4037 let output = platform.frame(100.0, key_f11);
4038
4039 assert!(
4041 output.contains("EnterFullscreen"),
4042 "F11 key should trigger EnterFullscreen action"
4043 );
4044 }
4045
4046 #[test]
4047 fn test_web_platform_pause_resume_via_escape() {
4048 let config = WebConfig::default();
4049 let mut platform = WebPlatform::new_for_test(config);
4050
4051 let space_down = r#"[{"event_type":"KeyDown","timestamp":0,"data":{"key":"Space"}}]"#;
4053 let _ = platform.frame(0.0, space_down);
4054 let space_up = r#"[{"event_type":"KeyUp","timestamp":16,"data":{"key":"Space"}}]"#;
4055 let _ = platform.frame(16.0, space_up);
4056 let _ = platform.frame(33.0, "[]");
4057
4058 let esc_down = r#"[{"event_type":"KeyDown","timestamp":100,"data":{"key":"Escape"}}]"#;
4060 let result = platform.frame(100.0, esc_down);
4061 assert!(result.contains("PAUSED") || result.contains("commands"));
4062
4063 let esc_up = r#"[{"event_type":"KeyUp","timestamp":116,"data":{"key":"Escape"}}]"#;
4065 let _ = platform.frame(116.0, esc_up);
4066 let _ = platform.frame(200.0, esc_down);
4067 }
4068
4069 #[test]
4070 fn test_web_platform_info_panel_toggle() {
4071 let config = WebConfig::default();
4072 let mut platform = WebPlatform::new_for_test(config);
4073
4074 let _ = platform.frame(0.0, "[]");
4075
4076 let i_down = r#"[{"event_type":"KeyDown","timestamp":100,"data":{"key":"KeyI"}}]"#;
4078 let result = platform.frame(100.0, i_down);
4079 assert!(result.contains("Pong AI") || result.contains("commands"));
4080 }
4081
4082 #[test]
4083 fn test_web_platform_mouse_click_hud() {
4084 let config = WebConfig::default();
4085 let mut platform = WebPlatform::new_for_test(config);
4086
4087 let _ = platform.frame(0.0, "[]");
4088
4089 let click =
4091 r#"[{"event_type":"MouseDown","timestamp":100,"data":{"button":0,"x":100,"y":20}}]"#;
4092 let _ = platform.frame(100.0, click);
4093
4094 let release =
4095 r#"[{"event_type":"MouseUp","timestamp":116,"data":{"button":0,"x":100,"y":20}}]"#;
4096 let _ = platform.frame(116.0, release);
4097 }
4098
4099 #[test]
4100 fn test_web_platform_download_button_click() {
4101 let config = WebConfig::default();
4102 let mut platform = WebPlatform::new_for_test(config);
4103
4104 let _ = platform.frame(0.0, "[]");
4105
4106 let click =
4108 r#"[{"event_type":"MouseDown","timestamp":100,"data":{"button":0,"x":70,"y":569}}]"#;
4109 let result = platform.frame(100.0, click);
4110 assert!(result.contains("DownloadAiModel") || result.contains("commands"));
4112 }
4113
4114 #[test]
4115 fn test_web_platform_get_ai_model() {
4116 let config = WebConfig::default();
4117 let platform = WebPlatform::new_for_test(config);
4118
4119 let model_json = platform.get_ai_model();
4120 assert!(model_json.contains("metadata"));
4121 assert!(model_json.contains("Pong AI"));
4122 assert!(model_json.contains("difficulty_profiles"));
4123 }
4124
4125 #[test]
4126 fn test_web_platform_set_game_mode() {
4127 let config = WebConfig::default();
4128 let mut platform = WebPlatform::new_for_test(config);
4129
4130 platform.set_game_mode("demo");
4131 let _ = platform.frame(0.0, "[]");
4132
4133 platform.set_game_mode("twoplayer");
4134 let _ = platform.frame(16.0, "[]");
4135
4136 platform.set_game_mode("singleplayer");
4137 let _ = platform.frame(33.0, "[]");
4138 }
4139
4140 #[test]
4141 fn test_web_platform_ai_difficulty_accessors() {
4142 let config = WebConfig::default();
4143 let mut platform = WebPlatform::new_for_test(config);
4144
4145 let initial = platform.get_ai_difficulty();
4146 assert!(initial >= 1 && initial <= 10);
4147
4148 platform.set_ai_difficulty(3);
4149 assert_eq!(platform.get_ai_difficulty(), 3);
4150
4151 platform.set_ai_difficulty(9);
4152 assert_eq!(platform.get_ai_difficulty(), 9);
4153 }
4154
4155 #[test]
4156 fn test_pong_game_victory_screen_renders() {
4157 let mut game = PongGame::new(800.0, 600.0, false);
4158 game.set_state(GameState::GameOver);
4159 game.left_score = 11;
4160
4161 let mut frame = RenderFrame::new();
4162 game.render(&mut frame);
4163
4164 assert!(frame.len() > 5);
4166 }
4167
4168 #[test]
4169 fn test_pong_game_paused_screen_renders() {
4170 let mut game = PongGame::new(800.0, 600.0, false);
4171 game.set_state(GameState::Paused);
4172
4173 let mut frame = RenderFrame::new();
4174 game.render(&mut frame);
4175
4176 assert!(frame.len() > 3);
4178 }
4179
4180 #[test]
4181 fn test_pong_game_menu_screen_renders() {
4182 let mut game = PongGame::new(800.0, 600.0, false);
4183 game.set_state(GameState::Menu);
4184
4185 let mut frame = RenderFrame::new();
4186 game.render(&mut frame);
4187
4188 assert!(frame.len() > 3);
4190 }
4191
4192 #[test]
4193 fn test_pong_game_demo_mode_ai_controls_both_paddles() {
4194 let mut game = PongGame::new(800.0, 600.0, false);
4195 game.set_state(GameState::Playing);
4196 game.game_mode = GameMode::Demo;
4197
4198 let input = InputState::new();
4199
4200 for _ in 0..60 {
4202 game.update(&input, 0.016);
4203 }
4204
4205 assert!(game.state() == GameState::Playing || game.state() == GameState::Menu);
4207 }
4208
4209 #[test]
4210 fn test_pong_game_two_player_mode() {
4211 let mut game = PongGame::new(800.0, 600.0, false);
4212 game.set_state(GameState::Playing);
4213 game.game_mode = GameMode::TwoPlayer;
4214
4215 let mut input = InputState::new();
4216 let initial_left_y = game.left_paddle_y;
4217 let initial_right_y = game.right_paddle_y;
4218
4219 input.set_key_pressed(jugar_input::KeyCode::Letter('W'), true);
4221 input.set_key_pressed(jugar_input::KeyCode::Up, true);
4222 game.update(&input, 0.1);
4223
4224 assert!(game.left_paddle_y < initial_left_y);
4226 assert!(game.right_paddle_y < initial_right_y);
4227 }
4228
4229 #[test]
4230 fn test_render_frame_commands_count() {
4231 let config = WebConfig::default();
4232 let mut platform = WebPlatform::new_for_test(config);
4233
4234 let result = platform.frame(0.0, "[]");
4235 let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
4236
4237 assert!(parsed["commands"].is_array());
4239 assert!(parsed["commands"].as_array().unwrap().len() > 5);
4240 }
4241
4242 #[test]
4243 fn test_speed_multiplier_affects_physics() {
4244 let config = WebConfig::default();
4245 let mut platform = WebPlatform::new_for_test(config);
4246
4247 let space = r#"[{"event_type":"KeyDown","timestamp":0,"data":{"key":"Space"}}]"#;
4249 let _ = platform.frame(0.0, space);
4250 let _ = platform.frame(16.0, "[]");
4251
4252 let key_3 = r#"[{"event_type":"KeyDown","timestamp":100,"data":{"key":"Digit3"}}]"#;
4254 let _ = platform.frame(100.0, key_3);
4255
4256 let result = platform.frame(116.0, "[]");
4258 assert!(!result.is_empty());
4259 }
4260
4261 #[test]
4262 fn test_js_action_download_ai_model() {
4263 let action = JsAction::DownloadAiModel;
4264 let json = serde_json::to_string(&action).unwrap();
4265 assert!(json.contains("DownloadAiModel"));
4266 }
4267
4268 #[test]
4269 fn test_js_action_open_url() {
4270 let action = JsAction::OpenUrl {
4271 url: "https://example.com".to_string(),
4272 };
4273 let json = serde_json::to_string(&action).unwrap();
4274 assert!(json.contains("OpenUrl"));
4275 assert!(json.contains("example.com"));
4276 }
4277
4278 #[test]
4279 fn test_js_action_fullscreen() {
4280 let enter = JsAction::EnterFullscreen;
4281 let exit = JsAction::ExitFullscreen;
4282
4283 let enter_json = serde_json::to_string(&enter).unwrap();
4284 let exit_json = serde_json::to_string(&exit).unwrap();
4285
4286 assert!(enter_json.contains("EnterFullscreen"));
4287 assert!(exit_json.contains("ExitFullscreen"));
4288 }
4289
4290 #[test]
4291 fn test_frame_output_with_actions() {
4292 let output = FrameOutput {
4293 commands: vec![],
4294 audio_events: vec![],
4295 actions: vec![JsAction::DownloadAiModel],
4296 debug_info: None,
4297 };
4298
4299 let json = serde_json::to_string(&output).unwrap();
4300 assert!(json.contains("actions"));
4301 assert!(json.contains("DownloadAiModel"));
4302 }
4303
4304 #[test]
4305 fn test_web_platform_full_gameplay_simulation() {
4306 let config = WebConfig::default();
4307 let mut platform = WebPlatform::new_for_test(config);
4308
4309 let space = r#"[{"event_type":"KeyDown","timestamp":0,"data":{"key":"Space"}}]"#;
4311 let _ = platform.frame(0.0, space);
4312
4313 for i in 1..100 {
4315 let ts = i as f64 * 16.667;
4316 let _ = platform.frame(ts, "[]");
4317 }
4318
4319 let stats = platform.get_stats();
4321 assert!(stats.contains("frame_count"));
4322 }
4323
4324 #[test]
4325 fn test_web_platform_continuous_input() {
4326 let config = WebConfig::default();
4327 let mut platform = WebPlatform::new_for_test(config);
4328
4329 let space = r#"[{"event_type":"KeyDown","timestamp":0,"data":{"key":"Space"}}]"#;
4331 let _ = platform.frame(0.0, space);
4332 let _ = platform.frame(16.0, "[]");
4333
4334 for i in 0..30 {
4336 let ts = 100.0 + (i as f64 * 16.667);
4337 let w_down = format!(
4338 r#"[{{"event_type":"KeyDown","timestamp":{},"data":{{"key":"KeyW"}}}}]"#,
4339 ts
4340 );
4341 let _ = platform.frame(ts, &w_down);
4342 }
4343 }
4344
4345 #[test]
4346 fn test_pong_game_ball_hits_track_rally() {
4347 let mut game = PongGame::new(800.0, 600.0, false);
4348 game.set_state(GameState::Playing);
4349
4350 assert_eq!(game.rally_count(), 0);
4351
4352 game.ball_x = 46.0;
4354 game.ball_y = game.left_paddle_y;
4355 game.ball_vx = -100.0;
4356 let input = InputState::new();
4357 game.update(&input, 0.016);
4358
4359 assert!(game.rally_count() > 0 || game.ball_vx > 0.0);
4361 }
4362
4363 #[test]
4364 fn test_all_speed_buttons_calculated() {
4365 let hud = HudButtons::calculate(800.0, 600.0);
4366 assert!(hud.speed_1x.width > 0.0);
4368 assert!(hud.speed_5x.width > 0.0);
4369 assert!(hud.speed_10x.width > 0.0);
4370 assert!(hud.speed_50x.width > 0.0);
4371 assert!(hud.speed_100x.width > 0.0);
4372 assert!(hud.speed_1000x.width > 0.0);
4373 }
4374
4375 #[test]
4376 fn test_ai_buttons_calculated() {
4377 let hud = HudButtons::calculate(800.0, 600.0);
4378 assert!(hud.ai_decrease.width > 0.0);
4380 assert!(hud.ai_increase.width > 0.0);
4381 }
4382
4383 #[test]
4384 fn test_footer_buttons_calculated() {
4385 let hud = HudButtons::calculate(800.0, 600.0);
4386 assert!(hud.download.width > 0.0);
4388 assert!(hud.model_info.width > 0.0);
4389 }
4390
4391 #[test]
4392 fn test_ai_shap_widget_renders_in_single_player_mode() {
4393 let config = WebConfig::default();
4395 let mut platform = WebPlatform::new_for_test(config);
4396
4397 let result = platform.frame(0.0, "[]");
4399
4400 assert!(
4402 result.contains(".apr ML Model"),
4403 "Expected frame output to contain '.apr ML Model' widget text. Output: {}",
4404 &result[..result.len().min(2000)]
4405 );
4406 }
4407
4408 #[test]
4409 fn test_ai_shap_widget_not_rendered_in_two_player_mode() {
4410 let config = WebConfig::default();
4411 let mut platform = WebPlatform::new_for_test(config);
4412
4413 platform.set_game_mode("2p");
4415
4416 let result = platform.frame(0.0, "[]");
4418
4419 assert!(
4421 !result.contains(".apr ML Model"),
4422 "Expected frame output to NOT contain '.apr ML Model' widget text in 2P mode"
4423 );
4424 }
4425
4426 #[test]
4427 fn test_footer_buttons_positions_match_render() {
4428 let hud = HudButtons::calculate(800.0, 600.0);
4430
4431 assert_eq!(hud.download.x, 10.0, "Download button should be at x=10");
4433 assert!(
4434 hud.download.y > 500.0,
4435 "Download button should be near bottom"
4436 );
4437
4438 assert!(
4440 hud.model_info.x > hud.download.x + hud.download.width,
4441 "Info button should be after download button"
4442 );
4443
4444 let expected_sound_x = hud.model_info.x + hud.model_info.width + 25.0;
4446 assert_eq!(
4447 hud.sound_toggle.x, expected_sound_x,
4448 "Sound button should be 25px after info button (accounting for [I] hint)"
4449 );
4450 }
4451
4452 #[test]
4453 fn test_info_button_click_toggles_panel() {
4454 let config = WebConfig::default();
4455 let mut platform = WebPlatform::new_for_test(config);
4456
4457 let _ = platform.frame(0.0, "[]");
4459
4460 let hud = HudButtons::calculate(800.0, 600.0);
4462 let click_x = hud.model_info.x + hud.model_info.width / 2.0;
4463 let click_y = hud.model_info.y + hud.model_info.height / 2.0;
4464
4465 let click = format!(
4467 r#"[{{"event_type":"MouseDown","timestamp":100,"data":{{"button":0,"x":{},"y":{}}}}}]"#,
4468 click_x, click_y
4469 );
4470 let result = platform.frame(100.0, &click);
4471
4472 assert!(
4474 result.contains("Model Info") || result.contains("difficulty_profiles"),
4475 "Info panel should be visible after clicking Info button"
4476 );
4477 }
4478
4479 #[test]
4480 fn test_sound_button_click_toggles_sound() {
4481 let config = WebConfig::default();
4482 let mut platform = WebPlatform::new_for_test(config);
4483
4484 let result1 = platform.frame(0.0, "[]");
4486 assert!(
4487 result1.contains("Sound"),
4488 "Should show 'Sound' when enabled"
4489 );
4490
4491 let hud = HudButtons::calculate(800.0, 600.0);
4493 let click_x = hud.sound_toggle.x + hud.sound_toggle.width / 2.0;
4494 let click_y = hud.sound_toggle.y + hud.sound_toggle.height / 2.0;
4495
4496 let click = format!(
4498 r#"[{{"event_type":"MouseDown","timestamp":100,"data":{{"button":0,"x":{},"y":{}}}}}]"#,
4499 click_x, click_y
4500 );
4501 let result2 = platform.frame(100.0, &click);
4502
4503 assert!(
4505 result2.contains("Muted"),
4506 "Should show 'Muted' after clicking sound button"
4507 );
4508 }
4509
4510 #[test]
4511 fn test_ai_buttons_dont_overlap_text() {
4512 let hud = HudButtons::calculate(800.0, 600.0);
4513
4514 let text_end_estimate = 40.0 + 100.0 + 10.0 + 96.0; assert!(
4521 hud.ai_decrease.x >= text_end_estimate - 10.0, "AI decrease button (x={}) should not overlap AI text (ends ~{})",
4523 hud.ai_decrease.x,
4524 text_end_estimate
4525 );
4526 }
4527
4528 #[test]
4531 fn test_button_rect_boundary_conditions() {
4532 let btn = ButtonRect::new(10.0, 20.0, 100.0, 50.0);
4533
4534 assert!(btn.contains(10.0, 20.0), "Top-left corner should be inside");
4536
4537 assert!(
4539 btn.contains(110.0, 70.0),
4540 "Bottom-right corner should be inside"
4541 );
4542
4543 assert!(btn.contains(50.0, 45.0), "Center should be inside");
4545
4546 assert!(!btn.contains(9.9, 45.0), "Left of button should be outside");
4548
4549 assert!(
4551 !btn.contains(110.1, 45.0),
4552 "Right of button should be outside"
4553 );
4554
4555 assert!(!btn.contains(50.0, 19.9), "Above button should be outside");
4557
4558 assert!(!btn.contains(50.0, 70.1), "Below button should be outside");
4560 }
4561
4562 #[test]
4563 fn test_button_width_calculation() {
4564 let width = HudButtons::button_width("Test", 8.0, 10.0);
4566 assert!((width - 52.0).abs() < 0.01);
4567
4568 let empty_width = HudButtons::button_width("", 8.0, 10.0);
4570 assert!((empty_width - 20.0).abs() < 0.01); let long_width = HudButtons::button_width("HelloWorld", 8.0, 5.0);
4574 assert!((long_width - 90.0).abs() < 0.01);
4576 }
4577
4578 #[test]
4579 fn test_hud_buttons_ultrawide_layout() {
4580 let hud = HudButtons::calculate(3840.0, 1080.0);
4582
4583 assert!(hud.mode_demo.width > 0.0);
4585 assert!(hud.mode_1p.width > 0.0);
4586 assert!(hud.mode_2p.width > 0.0);
4587
4588 assert!(hud.speed_1x.width > 0.0);
4590 assert!(hud.speed_1000x.width > 0.0);
4591
4592 assert!(hud.mode_demo.x + hud.mode_demo.width < 3840.0);
4594 assert!(hud.download.x + hud.download.width < 3840.0);
4595 }
4596
4597 #[test]
4598 fn test_hud_buttons_mobile_layout() {
4599 let hud = HudButtons::calculate(320.0, 640.0);
4601
4602 assert!(hud.mode_demo.x >= 0.0);
4604 assert!(hud.mode_demo.x + hud.mode_demo.width <= 320.0);
4605
4606 let all_buttons = [&hud.mode_demo, &hud.mode_1p, &hud.mode_2p, &hud.speed_1x];
4608
4609 for btn in all_buttons {
4610 assert!(
4611 btn.x + btn.width <= 320.0,
4612 "Button at x={} width={} exceeds screen width",
4613 btn.x,
4614 btn.width
4615 );
4616 }
4617 }
4618
4619 #[test]
4620 fn test_hud_buttons_square_layout() {
4621 let hud = HudButtons::calculate(600.0, 600.0);
4623
4624 assert!(hud.mode_demo.width > 0.0);
4626 assert!(hud.ai_decrease.width > 0.0);
4627 assert!(hud.ai_increase.width > 0.0);
4628 }
4629
4630 #[test]
4633 fn test_web_config_from_json_full() {
4634 let json =
4635 r#"{"width":1920,"height":1080,"target_fps":144,"debug":true,"ai_enabled":false}"#;
4636 let config = WebConfig::from_json(json);
4637 assert!(config.is_ok());
4638 let config = config.unwrap();
4639 assert_eq!(config.width, 1920);
4640 assert_eq!(config.height, 1080);
4641 assert_eq!(config.target_fps, 144);
4642 assert!(config.debug);
4643 assert!(!config.ai_enabled);
4644 }
4645
4646 #[test]
4647 fn test_web_config_from_json_partial() {
4648 let json = r#"{"width":640}"#;
4650 let config = WebConfig::from_json(json);
4651 assert!(config.is_ok());
4652 let config = config.unwrap();
4653 assert_eq!(config.width, 640);
4654 assert_eq!(config.height, 600); assert_eq!(config.target_fps, 60); assert!(!config.debug); assert!(config.ai_enabled); }
4659
4660 #[test]
4661 fn test_web_config_from_json_malformed() {
4662 let result = WebConfig::from_json("not json at all {{{");
4663 assert!(result.is_err());
4664 }
4665
4666 #[test]
4667 fn test_web_platform_error_from_conversion() {
4668 let input_err = InputTranslationError::InvalidJson("test error".to_string());
4669 let platform_err: WebPlatformError = input_err.into();
4670 assert!(matches!(platform_err, WebPlatformError::InputError(_)));
4671 let err_str = platform_err.to_string();
4672 assert!(err_str.contains("Input error"));
4673 }
4674
4675 #[test]
4676 fn test_web_platform_error_all_variants() {
4677 let config_err = WebPlatformError::InvalidConfig("bad config".to_string());
4678 assert_eq!(config_err.to_string(), "Invalid config: bad config");
4679
4680 let input_err = WebPlatformError::InputError("bad input".to_string());
4681 assert_eq!(input_err.to_string(), "Input error: bad input");
4682
4683 let render_err = WebPlatformError::RenderError("bad render".to_string());
4684 assert_eq!(render_err.to_string(), "Render error: bad render");
4685 }
4686
4687 #[test]
4688 fn test_button_rect_constructor() {
4689 let btn = ButtonRect::new(10.0, 20.0, 100.0, 50.0);
4690 assert!((btn.x - 10.0).abs() < 0.001);
4691 assert!((btn.y - 20.0).abs() < 0.001);
4692 assert!((btn.width - 100.0).abs() < 0.001);
4693 assert!((btn.height - 50.0).abs() < 0.001);
4694 }
4695
4696 #[test]
4697 fn test_hud_buttons_model_info_button_layout() {
4698 let hud = HudButtons::calculate(800.0, 600.0);
4699 assert!(hud.model_info.width > 0.0);
4701 assert!(hud.model_info.height > 0.0);
4702 assert!(hud.model_info.x + hud.model_info.width <= 800.0);
4704 }
4705
4706 #[test]
4707 fn test_hud_buttons_sound_toggle_layout() {
4708 let hud = HudButtons::calculate(800.0, 600.0);
4709 assert!(hud.sound_toggle.width > 0.0);
4711 assert!(hud.sound_toggle.height > 0.0);
4712 assert!(hud.sound_toggle.x >= 0.0);
4713 }
4714
4715 #[test]
4716 fn test_hud_buttons_download_button_layout() {
4717 let hud = HudButtons::calculate(800.0, 600.0);
4718 assert!(hud.download.width > 0.0);
4720 assert!(hud.download.height > 0.0);
4721 }
4722}