From 8c9a4c3ff2cbc3691e501f8253a1eb1632324ad4 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 24 Oct 2025 12:31:33 -0400 Subject: [PATCH 01/19] Add game mechanics and UI components for Rust-Pong - Implement ball physics and behavior in `ball.rs` - Create racket mechanics and controls in `racket.rs` - Add score tracking functionality in `score.rs` - Introduce AI controller logic in `controller.rs` - Develop HUD and pause screen stubs in `hud.rs` and `pause_screen.rs` - Update module structure in `mod.rs` files for better organization --- src/audio/mod.rs | 18 ++++ src/game/ball.rs | 59 ++++++++++ src/game/mod.rs | 10 ++ src/game/physics.rs | 65 +++++++++++ src/game/racket.rs | 57 ++++++++++ src/game/score.rs | 60 +++++++++++ src/player/controller.rs | 220 ++++++++++++++++++++++++++++++++++++++ src/player/mod.rs | 5 + src/player/player_type.rs | 76 +++++++++++++ src/ui/hud.rs | 7 ++ src/ui/pause_screen.rs | 7 ++ 11 files changed, 584 insertions(+) create mode 100644 src/audio/mod.rs create mode 100644 src/game/ball.rs create mode 100644 src/game/mod.rs create mode 100644 src/game/physics.rs create mode 100644 src/game/racket.rs create mode 100644 src/game/score.rs create mode 100644 src/player/controller.rs create mode 100644 src/player/mod.rs create mode 100644 src/player/player_type.rs create mode 100644 src/ui/hud.rs create mode 100644 src/ui/pause_screen.rs diff --git a/src/audio/mod.rs b/src/audio/mod.rs new file mode 100644 index 0000000..0c0394f --- /dev/null +++ b/src/audio/mod.rs @@ -0,0 +1,18 @@ +use ggez::audio::SoundSource; +use rust_embed::RustEmbed; + +#[derive(RustEmbed)] +#[folder = "assets/sfx"] +pub struct Asset; + +pub fn play_embedded_sound(ctx: &mut ggez::Context, name: &str) -> ggez::GameResult<()> { + if let Some(data) = Asset::get(name) { + let bytes = data.data.as_ref(); + let sound_data = ggez::audio::SoundData::from_bytes(bytes); + let mut src = ggez::audio::Source::from_data(ctx, sound_data)?; + src.play_detached(ctx)?; + Ok(()) + } else { + Err(ggez::GameError::ResourceLoadError(format!("Embedded SFX not found: {}", name))) + } +} diff --git a/src/game/ball.rs b/src/game/ball.rs new file mode 100644 index 0000000..e90c75f --- /dev/null +++ b/src/game/ball.rs @@ -0,0 +1,59 @@ +use ggez::{Context, GameResult, glam::Vec2, graphics}; +use rand::Rng; + +pub const BALL_SPEED: f32 = 750.0; +pub const BALL_SIZE: f32 = 20.0; +pub const BALL_SPEED_INCREMENT: f32 = 1.1; +pub const BALL_SPEED_MAX: f32 = 2500.0; + +pub struct Ball { + pub position: Vec2, + pub velocity: Vec2, + pub speed: f32, + ball_mesh: graphics::Mesh, +} + +pub fn randomize_velocity(vector: &mut Vec2, x: f32, y: f32) { + let mut random_thread = rand::rng(); + vector.x = match random_thread.random_bool(0.5) { + true => x, + false => -x, + }; + vector.y = match random_thread.random_bool(0.5) { + true => y, + false => -y, + }; +} + +impl Ball { + // Draw the ball on the provided canvas. + pub fn draw_on_canvas(&self, canvas: &mut graphics::Canvas) { + canvas.draw(&self.ball_mesh, graphics::DrawParam::default().dest(self.position)); + } + + // Reset ball position and speed, and randomize its direction. + pub fn reset(&mut self, position_x: f32, position_y: f32) { + self.position = Vec2::new(position_x, position_y); + self.speed = BALL_SPEED; + randomize_velocity(&mut self.velocity, self.speed, self.speed); + self.velocity = self.velocity.normalize() * self.speed; + } + + pub fn new(position_x: f32, position_y: f32, context: &mut Context) -> GameResult { + let mut ball_velocity = Vec2::new(0.0, 0.0); + randomize_velocity(&mut ball_velocity, BALL_SPEED, BALL_SPEED); + + let ball_rectangle = graphics::Rect::new(-BALL_SIZE / 2.0, -BALL_SIZE / 2.0, BALL_SIZE, BALL_SIZE); + let ball_mesh = graphics::Mesh::new_rectangle(context, graphics::DrawMode::fill(), ball_rectangle, graphics::Color::WHITE)?; + Ok(Ball { + position: Vec2::new(position_x, position_y), + velocity: ball_velocity.normalize() * BALL_SPEED, + speed: BALL_SPEED, + ball_mesh, + }) + } + + pub fn move_ball(&mut self, delta_time: f32) { + self.position += self.velocity * delta_time; + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs new file mode 100644 index 0000000..3438144 --- /dev/null +++ b/src/game/mod.rs @@ -0,0 +1,10 @@ +pub mod ball; +pub mod physics; +pub mod racket; +pub mod score; + +// Re-export commonly used items at crate::game::* if desired by callers. +pub use ball::*; +pub use physics::*; +pub use racket::*; +pub use score::*; diff --git a/src/game/physics.rs b/src/game/physics.rs new file mode 100644 index 0000000..e554a0d --- /dev/null +++ b/src/game/physics.rs @@ -0,0 +1,65 @@ +use crate::game::ball::{BALL_SIZE, BALL_SPEED_INCREMENT, BALL_SPEED_MAX, Ball}; +use crate::game::racket::{RACKET_HEIGHT_HALF, RACKET_WIDTH_HALF, Racket}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Player { + Left, + Right, +} + +// Bounce the ball off the top/bottom walls if needed. +pub fn bounce_borders(ball: &mut Ball, screen_h: f32) -> bool { + if ball.position.y - BALL_SIZE / 2.0 <= 0.0 && ball.velocity.y < 0.0 || ball.position.y + BALL_SIZE / 2.0 >= screen_h && ball.velocity.y > 0.0 { + ball.velocity.y = -ball.velocity.y; + true + } else { + false + } +} + +pub fn racket_collision(ball: &mut Ball, racket: &Racket) -> bool { + // Generalized collision: determine the ball contact x (edge) and the racket edge to compare against + let contact_x = if ball.velocity.x < 0.0 { + ball.position.x - BALL_SIZE / 2.0 + } else { + ball.position.x + BALL_SIZE / 2.0 + }; + + let racket_edge = if ball.velocity.x < 0.0 { + racket.position_x + RACKET_WIDTH_HALF + } else { + racket.position_x - RACKET_WIDTH_HALF + }; + + let horizontal_overlap = if ball.velocity.x < 0.0 { + contact_x <= racket_edge + } else { + contact_x >= racket_edge + }; + + let vertical_overlap = ball.position.y >= racket.position_y - RACKET_HEIGHT_HALF && ball.position.y <= racket.position_y + RACKET_HEIGHT_HALF; + + // Only reflect if ball is actually approaching the racket (prevents accidental reflections) + let approaching = (ball.velocity.x < 0.0 && ball.position.x > racket.position_x) || (ball.velocity.x > 0.0 && ball.position.x < racket.position_x); + + if horizontal_overlap && vertical_overlap && approaching { + ball.velocity.x = -ball.velocity.x; + let offset = (ball.position.y - racket.position_y) / RACKET_HEIGHT_HALF; + ball.velocity.y = ball.speed * offset; + ball.speed = (ball.speed * BALL_SPEED_INCREMENT).min(BALL_SPEED_MAX); + ball.velocity = ball.velocity.normalize() * ball.speed; + return true; + } + + false +} + +pub fn check_score(ball: &Ball, screen_w: f32) -> Option { + if ball.position.x < 0.0 { + return Some(Player::Right); + } + if ball.position.x > screen_w { + return Some(Player::Left); + } + None +} diff --git a/src/game/racket.rs b/src/game/racket.rs new file mode 100644 index 0000000..bca01ff --- /dev/null +++ b/src/game/racket.rs @@ -0,0 +1,57 @@ +use crate::player::controller::{Controller, RacketAction::*}; +use ggez::graphics::{Canvas, Color, DrawParam, Mesh, Rect}; +use ggez::{Context, GameResult}; + +const RACKET_SPEED: f32 = 650.0; +pub const RACKET_HEIGHT: f32 = 150.0; +pub const RACKET_WIDTH: f32 = 20.0; +pub const RACKET_HEIGHT_HALF: f32 = RACKET_HEIGHT / 2.0; +pub const RACKET_WIDTH_HALF: f32 = RACKET_WIDTH / 2.0; +pub const RACKET_OFFSET: f32 = RACKET_WIDTH * 2.0; + +pub struct Racket { + pub position_y: f32, + pub position_x: f32, + racket_mesh: Mesh, + pub controller: Box, +} + +impl Racket { + pub fn new(x: f32, y: f32, context: &mut Context, controller: Box) -> GameResult { + let rect = Rect::new(-RACKET_WIDTH / 2.0, -RACKET_HEIGHT / 2.0, RACKET_WIDTH, RACKET_HEIGHT); + let racket_mesh = Mesh::new_rectangle(context, ggez::graphics::DrawMode::fill(), rect, Color::WHITE)?; + + Ok(Self { + position_x: x, + position_y: y, + racket_mesh, + controller, + }) + } + + pub fn draw_on_canvas(&self, canvas: &mut Canvas) { + canvas.draw(&self.racket_mesh, DrawParam::default().dest([self.position_x, self.position_y])); + } + + pub fn update(&mut self, input: &crate::player::controller::ControllerInput, delta_time: f32) { + match self.controller.get_action(input) { + MoveUp => { + self.position_y -= RACKET_SPEED * delta_time; + } + MoveDown => { + self.position_y += RACKET_SPEED * delta_time; + } + Stay => {} + } + + // Keep the racket inside the screen bounds + let half_height = RACKET_HEIGHT / 2.0; + if self.position_y < half_height { + self.position_y = half_height; + } + let lower_limit = input.screen_height - half_height; + if self.position_y > lower_limit { + self.position_y = lower_limit; + } + } +} diff --git a/src/game/score.rs b/src/game/score.rs new file mode 100644 index 0000000..e0843e6 --- /dev/null +++ b/src/game/score.rs @@ -0,0 +1,60 @@ +use ggez::graphics::{Canvas, Color, DrawParam, PxScale, Text}; +use ggez::{Context, GameResult, glam::Vec2}; + +pub struct Score { + p1: u8, + p2: u8, + text: Text, + position: Vec2, + scale: f32, +} + +impl Score { + pub fn new(context: &mut Context) -> GameResult { + let p1 = 0; + let p2 = 0; + let scale = context.gfx.drawable_size().1 / 3.0; + let mut text = Text::new(format!("{} {}", p1, p2)); + text.set_scale(PxScale::from(scale)); + let text_dimensions = text.measure(context)?; + let position = Vec2::new( + context.gfx.drawable_size().0 / 2.0 - text_dimensions.x / 2.0, + context.gfx.drawable_size().1 / 2.0 - text_dimensions.y / 2.0, + ); + + Ok(Self { p1, p2, text, position, scale }) + } + + fn update_text(&mut self, context: &mut Context) -> GameResult<()> { + self.text = Text::new(format!("{} {}", self.p1, self.p2)); + self.text.set_scale(PxScale::from(self.scale)); + let text_dimensions = self.text.measure(context)?; + self.position = Vec2::new( + context.gfx.drawable_size().0 / 2.0 - text_dimensions.x / 2.0, + context.gfx.drawable_size().1 / 2.0 - text_dimensions.y / 2.0, + ); + Ok(()) + } + + pub fn increment_p1(&mut self, context: &mut Context) -> GameResult<()> { + self.p1 = self.p1.saturating_add(1); + self.update_text(context) + } + + pub fn increment_p2(&mut self, context: &mut Context) -> GameResult<()> { + self.p2 = self.p2.saturating_add(1); + self.update_text(context) + } + + pub fn draw_on_canvas(&self, canvas: &mut Canvas) { + canvas.draw(&self.text, DrawParam::default().dest(self.position).color(Color::from_rgb(50, 50, 50))); + } + + pub fn get_p1_score(&self) -> u8 { + self.p1 + } + + pub fn get_p2_score(&self) -> u8 { + self.p2 + } +} diff --git a/src/player/controller.rs b/src/player/controller.rs new file mode 100644 index 0000000..bf82bd3 --- /dev/null +++ b/src/player/controller.rs @@ -0,0 +1,220 @@ +use crate::game::racket::RACKET_HEIGHT_HALF; +use ggez::{glam::Vec2, input::keyboard::KeyCode}; +use std::collections::HashSet; + +const AI_RACKET_PERCEPTION: f32 = 0.75; + +pub trait Controller { + fn get_action(&mut self, input: &ControllerInput) -> RacketAction; +} + +pub enum RacketAction { + MoveUp, + MoveDown, + Stay, +} + +pub struct ControllerInput { + pub ball_position: Vec2, + pub ball_velocity: Vec2, + pub racket_position: f32, + pub racket_x: f32, + pub screen_height: f32, + pub pressed_keys: HashSet, +} + +pub struct HumanController { + pub up_key: KeyCode, + pub down_key: KeyCode, +} + +impl HumanController { + pub fn new(up_key: KeyCode, down_key: KeyCode) -> Self { + Self { up_key, down_key } + } +} + +impl Controller for HumanController { + fn get_action(&mut self, input: &ControllerInput) -> RacketAction { + if input.pressed_keys.contains(&self.up_key) { + RacketAction::MoveUp + } else if input.pressed_keys.contains(&self.down_key) { + RacketAction::MoveDown + } else { + RacketAction::Stay + } + } +} + +trait AiBehavior { + // Choose a vertical target (y) for the racket based on the controller input. + fn choose_target(&mut self, input: &ControllerInput) -> f32; +} + +struct ReactiveBehavior {} + +impl ReactiveBehavior { + fn new() -> Self { + Self {} + } +} + +impl AiBehavior for ReactiveBehavior { + fn choose_target(&mut self, input: &ControllerInput) -> f32 { + input.ball_position.y + } +} + +pub struct PredictiveBehavior {} + +impl PredictiveBehavior { + fn new() -> Self { + Self {} + } + + // Predict where the ball will be vertically when it reaches racket_x. + pub fn predict_ball_y(&self, input: &ControllerInput) -> f32 { + // time until ball reaches racket x + let delta_x = input.racket_x - input.ball_position.x; + if input.ball_velocity.x == 0.0 { + return input.ball_position.y; + } + let time_to_reach = delta_x / input.ball_velocity.x; + + // projected vertical position at that time (may be outside bounds) + let projected_y = input.ball_position.y + input.ball_velocity.y * time_to_reach; + + // reflect across top/bottom using mirror modulus to account for bounces + let screen_height = input.screen_height; + if screen_height <= 0.0 { + return projected_y; + } + let wrap_period = 2.0 * screen_height; + let mut modded = projected_y % wrap_period; + if modded < 0.0 { + modded += wrap_period; + } + if modded <= screen_height { modded } else { 2.0 * screen_height - modded } + } +} + +impl AiBehavior for PredictiveBehavior { + fn choose_target(&mut self, input: &ControllerInput) -> f32 { + self.predict_ball_y(input) + } +} + +pub struct BalancedBehavior { + // Fields to store necessary state +} + +impl BalancedBehavior { + pub fn new() -> Self { + Self { + // Initialize fields + } + } +} + +impl AiBehavior for BalancedBehavior { + fn choose_target(&mut self, input: &ControllerInput) -> f32 { + // Logic to average between Reactive and Predictive behavior + let reactive_target = ReactiveBehavior::new().choose_target(input); + let predictive_target = PredictiveBehavior::new().choose_target(input); + (reactive_target + predictive_target) / 2.0 + } +} + +pub struct AIController { + strategy: Box, +} + +impl AIController { + pub fn easy() -> Self { + Self { + strategy: Box::new(ReactiveBehavior::new()), + } + } + + pub fn medium() -> Self { + Self { + strategy: Box::new(BalancedBehavior::new()), + } + } + + pub fn hard() -> Self { + Self { + strategy: Box::new(PredictiveBehavior::new()), + } + } +} + +impl Controller for AIController { + fn get_action(&mut self, input: &ControllerInput) -> RacketAction { + let perceived_half_height = RACKET_HEIGHT_HALF * AI_RACKET_PERCEPTION; + + let racket_top = input.racket_position - perceived_half_height; + let racket_bottom = input.racket_position + perceived_half_height; + + // Decide whether the ball is approaching this racket (works for either side) + let ball_approaching = + (input.ball_velocity.x > 0.0 && input.racket_x > input.ball_position.x) || (input.ball_velocity.x < 0.0 && input.racket_x < input.ball_position.x); + + if ball_approaching { + let target_y = self.strategy.choose_target(input); + if target_y < racket_top { + RacketAction::MoveUp + } else if target_y > racket_bottom { + RacketAction::MoveDown + } else { + RacketAction::Stay + } + } else { + let center_y = input.screen_height / 2.0; + let deadzone = perceived_half_height; + if input.racket_position < center_y - deadzone { + RacketAction::MoveDown + } else if input.racket_position > center_y + deadzone { + RacketAction::MoveUp + } else { + RacketAction::Stay + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{ControllerInput, PredictiveBehavior}; + use ggez::glam::Vec2; + use std::collections::HashSet; + + fn base_input() -> ControllerInput { + ControllerInput { + ball_position: Vec2::new(100.0, 100.0), + ball_velocity: Vec2::new(200.0, 50.0), + racket_position: 200.0, + racket_x: 600.0, + screen_height: 400.0, + pressed_keys: HashSet::new(), + } + } + + #[test] + fn predictive_handles_simple_projection() { + let predictive_behavior = PredictiveBehavior::new(); + let input = base_input(); + let y = predictive_behavior.predict_ball_y(&input); + // sanity: should be within bounds + assert!(y >= 0.0 && y <= input.screen_height); + } + + #[test] + fn predictive_handles_vertical_wrap() { + let predictive_behavior = PredictiveBehavior::new(); + let mut input = base_input(); + input.ball_velocity = Vec2::new(50.0, 500.0); + let y = predictive_behavior.predict_ball_y(&input); + assert!(y >= 0.0 && y <= input.screen_height); + } +} diff --git a/src/player/mod.rs b/src/player/mod.rs new file mode 100644 index 0000000..9c4330e --- /dev/null +++ b/src/player/mod.rs @@ -0,0 +1,5 @@ +pub mod controller; +pub mod player_type; + +pub use controller::*; +pub use player_type::*; diff --git a/src/player/player_type.rs b/src/player/player_type.rs new file mode 100644 index 0000000..24a82ac --- /dev/null +++ b/src/player/player_type.rs @@ -0,0 +1,76 @@ +use crate::game::physics::Player; +use crate::player::controller::{AIController, Controller, HumanController}; +use ggez::input::keyboard::KeyCode; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PlayerType { + Human, + AIEasy, + AIMedium, + AIHard, +} + +impl PlayerType { + pub fn next(&self) -> Self { + match self { + PlayerType::Human => PlayerType::AIEasy, + PlayerType::AIEasy => PlayerType::AIMedium, + PlayerType::AIMedium => PlayerType::AIHard, + PlayerType::AIHard => PlayerType::Human, + } + } + + pub fn previous(&self) -> Self { + match self { + PlayerType::Human => PlayerType::AIHard, + PlayerType::AIEasy => PlayerType::Human, + PlayerType::AIMedium => PlayerType::AIEasy, + PlayerType::AIHard => PlayerType::AIMedium, + } + } + + pub fn display_name(&self) -> &str { + match self { + PlayerType::Human => "Human", + PlayerType::AIEasy => "AI - Easy", + PlayerType::AIMedium => "AI - Medium", + PlayerType::AIHard => "AI - Hard", + } + } + + pub fn create_controller_for_player(&self, player: Player) -> Box { + match self { + PlayerType::Human => { + if player == Player::Right { + Box::new(HumanController::new(KeyCode::W, KeyCode::S)) + } else { + Box::new(HumanController::new(KeyCode::Up, KeyCode::Down)) + } + } + PlayerType::AIEasy => Box::new(AIController::easy()), + PlayerType::AIMedium => Box::new(AIController::medium()), + PlayerType::AIHard => Box::new(AIController::hard()), + } + } +} + +#[cfg(test)] +mod tests { + use super::PlayerType; + + #[test] + fn next_cycles_in_order() { + assert_eq!(PlayerType::Human.next(), PlayerType::AIEasy); + assert_eq!(PlayerType::AIEasy.next(), PlayerType::AIMedium); + assert_eq!(PlayerType::AIMedium.next(), PlayerType::AIHard); + assert_eq!(PlayerType::AIHard.next(), PlayerType::Human); + } + + #[test] + fn previous_cycles_in_order() { + assert_eq!(PlayerType::Human.previous(), PlayerType::AIHard); + assert_eq!(PlayerType::AIEasy.previous(), PlayerType::Human); + assert_eq!(PlayerType::AIMedium.previous(), PlayerType::AIEasy); + assert_eq!(PlayerType::AIHard.previous(), PlayerType::AIMedium); + } +} diff --git a/src/ui/hud.rs b/src/ui/hud.rs new file mode 100644 index 0000000..4faf912 --- /dev/null +++ b/src/ui/hud.rs @@ -0,0 +1,7 @@ +use ggez::graphics::Canvas; +use ggez::{Context, GameResult}; + +pub fn draw_hud(_context: &mut Context, _canvas: &mut Canvas) -> GameResult { + // Placeholder stub for HUD + Ok(()) +} diff --git a/src/ui/pause_screen.rs b/src/ui/pause_screen.rs new file mode 100644 index 0000000..05da45e --- /dev/null +++ b/src/ui/pause_screen.rs @@ -0,0 +1,7 @@ +use ggez::graphics::Canvas; +use ggez::{Context, GameResult}; + +pub fn draw_pause_screen(_context: &mut Context, _canvas: &mut Canvas) -> GameResult { + // Placeholder stub for pause screen UI + Ok(()) +} From 3a3aff34d3abbcd8f17ecd6acedef29527ff8418 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 24 Oct 2025 12:31:39 -0400 Subject: [PATCH 02/19] Remove unused modules and refactor imports for cleaner code structure --- src/audio.rs | 18 ---- src/ball.rs | 59 ------------ src/controller.rs | 220 --------------------------------------------- src/main.rs | 8 +- src/main_state.rs | 8 +- src/physics.rs | 65 -------------- src/player_type.rs | 76 ---------------- src/racket.rs | 57 ------------ src/score.rs | 60 ------------- src/ui/menu.rs | 2 +- src/ui/mod.rs | 2 + 11 files changed, 12 insertions(+), 563 deletions(-) delete mode 100644 src/audio.rs delete mode 100644 src/ball.rs delete mode 100644 src/controller.rs delete mode 100644 src/physics.rs delete mode 100644 src/player_type.rs delete mode 100644 src/racket.rs delete mode 100644 src/score.rs diff --git a/src/audio.rs b/src/audio.rs deleted file mode 100644 index 0c0394f..0000000 --- a/src/audio.rs +++ /dev/null @@ -1,18 +0,0 @@ -use ggez::audio::SoundSource; -use rust_embed::RustEmbed; - -#[derive(RustEmbed)] -#[folder = "assets/sfx"] -pub struct Asset; - -pub fn play_embedded_sound(ctx: &mut ggez::Context, name: &str) -> ggez::GameResult<()> { - if let Some(data) = Asset::get(name) { - let bytes = data.data.as_ref(); - let sound_data = ggez::audio::SoundData::from_bytes(bytes); - let mut src = ggez::audio::Source::from_data(ctx, sound_data)?; - src.play_detached(ctx)?; - Ok(()) - } else { - Err(ggez::GameError::ResourceLoadError(format!("Embedded SFX not found: {}", name))) - } -} diff --git a/src/ball.rs b/src/ball.rs deleted file mode 100644 index e90c75f..0000000 --- a/src/ball.rs +++ /dev/null @@ -1,59 +0,0 @@ -use ggez::{Context, GameResult, glam::Vec2, graphics}; -use rand::Rng; - -pub const BALL_SPEED: f32 = 750.0; -pub const BALL_SIZE: f32 = 20.0; -pub const BALL_SPEED_INCREMENT: f32 = 1.1; -pub const BALL_SPEED_MAX: f32 = 2500.0; - -pub struct Ball { - pub position: Vec2, - pub velocity: Vec2, - pub speed: f32, - ball_mesh: graphics::Mesh, -} - -pub fn randomize_velocity(vector: &mut Vec2, x: f32, y: f32) { - let mut random_thread = rand::rng(); - vector.x = match random_thread.random_bool(0.5) { - true => x, - false => -x, - }; - vector.y = match random_thread.random_bool(0.5) { - true => y, - false => -y, - }; -} - -impl Ball { - // Draw the ball on the provided canvas. - pub fn draw_on_canvas(&self, canvas: &mut graphics::Canvas) { - canvas.draw(&self.ball_mesh, graphics::DrawParam::default().dest(self.position)); - } - - // Reset ball position and speed, and randomize its direction. - pub fn reset(&mut self, position_x: f32, position_y: f32) { - self.position = Vec2::new(position_x, position_y); - self.speed = BALL_SPEED; - randomize_velocity(&mut self.velocity, self.speed, self.speed); - self.velocity = self.velocity.normalize() * self.speed; - } - - pub fn new(position_x: f32, position_y: f32, context: &mut Context) -> GameResult { - let mut ball_velocity = Vec2::new(0.0, 0.0); - randomize_velocity(&mut ball_velocity, BALL_SPEED, BALL_SPEED); - - let ball_rectangle = graphics::Rect::new(-BALL_SIZE / 2.0, -BALL_SIZE / 2.0, BALL_SIZE, BALL_SIZE); - let ball_mesh = graphics::Mesh::new_rectangle(context, graphics::DrawMode::fill(), ball_rectangle, graphics::Color::WHITE)?; - Ok(Ball { - position: Vec2::new(position_x, position_y), - velocity: ball_velocity.normalize() * BALL_SPEED, - speed: BALL_SPEED, - ball_mesh, - }) - } - - pub fn move_ball(&mut self, delta_time: f32) { - self.position += self.velocity * delta_time; - } -} diff --git a/src/controller.rs b/src/controller.rs deleted file mode 100644 index 5a1bdc2..0000000 --- a/src/controller.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::racket::RACKET_HEIGHT_HALF; -use ggez::{glam::Vec2, input::keyboard::KeyCode}; -use std::collections::HashSet; - -const AI_RACKET_PERCEPTION: f32 = 0.75; - -pub trait Controller { - fn get_action(&mut self, input: &ControllerInput) -> RacketAction; -} - -pub enum RacketAction { - MoveUp, - MoveDown, - Stay, -} - -pub struct ControllerInput { - pub ball_position: Vec2, - pub ball_velocity: Vec2, - pub racket_position: f32, - pub racket_x: f32, - pub screen_height: f32, - pub pressed_keys: HashSet, -} - -pub struct HumanController { - pub up_key: KeyCode, - pub down_key: KeyCode, -} - -impl HumanController { - pub fn new(up_key: KeyCode, down_key: KeyCode) -> Self { - Self { up_key, down_key } - } -} - -impl Controller for HumanController { - fn get_action(&mut self, input: &ControllerInput) -> RacketAction { - if input.pressed_keys.contains(&self.up_key) { - RacketAction::MoveUp - } else if input.pressed_keys.contains(&self.down_key) { - RacketAction::MoveDown - } else { - RacketAction::Stay - } - } -} - -trait AiBehavior { - // Choose a vertical target (y) for the racket based on the controller input. - fn choose_target(&mut self, input: &ControllerInput) -> f32; -} - -struct ReactiveBehavior {} - -impl ReactiveBehavior { - fn new() -> Self { - Self {} - } -} - -impl AiBehavior for ReactiveBehavior { - fn choose_target(&mut self, input: &ControllerInput) -> f32 { - input.ball_position.y - } -} - -pub struct PredictiveBehavior {} - -impl PredictiveBehavior { - fn new() -> Self { - Self {} - } - - // Predict where the ball will be vertically when it reaches racket_x. - pub fn predict_ball_y(&self, input: &ControllerInput) -> f32 { - // time until ball reaches racket x - let delta_x = input.racket_x - input.ball_position.x; - if input.ball_velocity.x == 0.0 { - return input.ball_position.y; - } - let time_to_reach = delta_x / input.ball_velocity.x; - - // projected vertical position at that time (may be outside bounds) - let projected_y = input.ball_position.y + input.ball_velocity.y * time_to_reach; - - // reflect across top/bottom using mirror modulus to account for bounces - let screen_height = input.screen_height; - if screen_height <= 0.0 { - return projected_y; - } - let wrap_period = 2.0 * screen_height; - let mut modded = projected_y % wrap_period; - if modded < 0.0 { - modded += wrap_period; - } - if modded <= screen_height { modded } else { 2.0 * screen_height - modded } - } -} - -impl AiBehavior for PredictiveBehavior { - fn choose_target(&mut self, input: &ControllerInput) -> f32 { - self.predict_ball_y(input) - } -} - -pub struct BalancedBehavior { - // Fields to store necessary state -} - -impl BalancedBehavior { - pub fn new() -> Self { - Self { - // Initialize fields - } - } -} - -impl AiBehavior for BalancedBehavior { - fn choose_target(&mut self, input: &ControllerInput) -> f32 { - // Logic to average between Reactive and Predictive behavior - let reactive_target = ReactiveBehavior::new().choose_target(input); - let predictive_target = PredictiveBehavior::new().choose_target(input); - (reactive_target + predictive_target) / 2.0 - } -} - -pub struct AIController { - strategy: Box, -} - -impl AIController { - pub fn easy() -> Self { - Self { - strategy: Box::new(ReactiveBehavior::new()), - } - } - - pub fn medium() -> Self { - Self { - strategy: Box::new(BalancedBehavior::new()), - } - } - - pub fn hard() -> Self { - Self { - strategy: Box::new(PredictiveBehavior::new()), - } - } -} - -impl Controller for AIController { - fn get_action(&mut self, input: &ControllerInput) -> RacketAction { - let perceived_half_height = RACKET_HEIGHT_HALF * AI_RACKET_PERCEPTION; - - let racket_top = input.racket_position - perceived_half_height; - let racket_bottom = input.racket_position + perceived_half_height; - - // Decide whether the ball is approaching this racket (works for either side) - let ball_approaching = - (input.ball_velocity.x > 0.0 && input.racket_x > input.ball_position.x) || (input.ball_velocity.x < 0.0 && input.racket_x < input.ball_position.x); - - if ball_approaching { - let target_y = self.strategy.choose_target(input); - if target_y < racket_top { - RacketAction::MoveUp - } else if target_y > racket_bottom { - RacketAction::MoveDown - } else { - RacketAction::Stay - } - } else { - let center_y = input.screen_height / 2.0; - let deadzone = perceived_half_height; - if input.racket_position < center_y - deadzone { - RacketAction::MoveDown - } else if input.racket_position > center_y + deadzone { - RacketAction::MoveUp - } else { - RacketAction::Stay - } - } - } -} - -#[cfg(test)] -mod tests { - use super::{ControllerInput, PredictiveBehavior}; - use ggez::glam::Vec2; - use std::collections::HashSet; - - fn base_input() -> ControllerInput { - ControllerInput { - ball_position: Vec2::new(100.0, 100.0), - ball_velocity: Vec2::new(200.0, 50.0), - racket_position: 200.0, - racket_x: 600.0, - screen_height: 400.0, - pressed_keys: HashSet::new(), - } - } - - #[test] - fn predictive_handles_simple_projection() { - let predictive_behavior = PredictiveBehavior::new(); - let input = base_input(); - let y = predictive_behavior.predict_ball_y(&input); - // sanity: should be within bounds - assert!(y >= 0.0 && y <= input.screen_height); - } - - #[test] - fn predictive_handles_vertical_wrap() { - let predictive_behavior = PredictiveBehavior::new(); - let mut input = base_input(); - input.ball_velocity = Vec2::new(50.0, 500.0); - let y = predictive_behavior.predict_ball_y(&input); - assert!(y >= 0.0 && y <= input.screen_height); - } -} diff --git a/src/main.rs b/src/main.rs index 043b184..d5552f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,10 @@ #![windows_subsystem = "windows"] mod audio; -mod ball; -mod controller; mod debug; +mod game; mod main_state; -mod physics; -mod player_type; -mod racket; -mod score; +mod player; mod ui; use crate::main_state::MainState; diff --git a/src/main_state.rs b/src/main_state.rs index 23831e1..e684b39 100644 --- a/src/main_state.rs +++ b/src/main_state.rs @@ -5,8 +5,14 @@ // - Mouse Click: Select and cycle player type // - SPACE/ENTER: Start game +use crate::game::ball::*; +use crate::game::physics::*; +use crate::game::racket::*; +use crate::game::score::Score; +use crate::player::controller::ControllerInput; +use crate::player::player_type::PlayerType; use crate::ui::menu as ui_menu; -use crate::{audio::play_embedded_sound, ball::*, controller::ControllerInput, debug::DebugInfo, physics::*, player_type::PlayerType, racket::*, score::Score}; +use crate::{audio::play_embedded_sound, debug::DebugInfo}; use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, Rect, Text}; use ggez::{Context, GameResult, event, glam::Vec2, input::keyboard::KeyCode}; use std::collections::HashSet; diff --git a/src/physics.rs b/src/physics.rs deleted file mode 100644 index c7ea1c7..0000000 --- a/src/physics.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::ball::{BALL_SIZE, BALL_SPEED_INCREMENT, BALL_SPEED_MAX, Ball}; -use crate::racket::{RACKET_HEIGHT_HALF, RACKET_WIDTH_HALF, Racket}; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Player { - Left, - Right, -} - -// Bounce the ball off the top/bottom walls if needed. -pub fn bounce_borders(ball: &mut Ball, screen_h: f32) -> bool { - if ball.position.y - BALL_SIZE / 2.0 <= 0.0 && ball.velocity.y < 0.0 || ball.position.y + BALL_SIZE / 2.0 >= screen_h && ball.velocity.y > 0.0 { - ball.velocity.y = -ball.velocity.y; - true - } else { - false - } -} - -pub fn racket_collision(ball: &mut Ball, racket: &Racket) -> bool { - // Generalized collision: determine the ball contact x (edge) and the racket edge to compare against - let contact_x = if ball.velocity.x < 0.0 { - ball.position.x - BALL_SIZE / 2.0 - } else { - ball.position.x + BALL_SIZE / 2.0 - }; - - let racket_edge = if ball.velocity.x < 0.0 { - racket.position_x + RACKET_WIDTH_HALF - } else { - racket.position_x - RACKET_WIDTH_HALF - }; - - let horizontal_overlap = if ball.velocity.x < 0.0 { - contact_x <= racket_edge - } else { - contact_x >= racket_edge - }; - - let vertical_overlap = ball.position.y >= racket.position_y - RACKET_HEIGHT_HALF && ball.position.y <= racket.position_y + RACKET_HEIGHT_HALF; - - // Only reflect if ball is actually approaching the racket (prevents accidental reflections) - let approaching = (ball.velocity.x < 0.0 && ball.position.x > racket.position_x) || (ball.velocity.x > 0.0 && ball.position.x < racket.position_x); - - if horizontal_overlap && vertical_overlap && approaching { - ball.velocity.x = -ball.velocity.x; - let offset = (ball.position.y - racket.position_y) / RACKET_HEIGHT_HALF; - ball.velocity.y = ball.speed * offset; - ball.speed = (ball.speed * BALL_SPEED_INCREMENT).min(BALL_SPEED_MAX); - ball.velocity = ball.velocity.normalize() * ball.speed; - return true; - } - - false -} - -pub fn check_score(ball: &Ball, screen_w: f32) -> Option { - if ball.position.x < 0.0 { - return Some(Player::Right); - } - if ball.position.x > screen_w { - return Some(Player::Left); - } - None -} diff --git a/src/player_type.rs b/src/player_type.rs deleted file mode 100644 index 24f2f52..0000000 --- a/src/player_type.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::controller::{AIController, Controller, HumanController}; -use crate::physics::Player; -use ggez::input::keyboard::KeyCode; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum PlayerType { - Human, - AIEasy, - AIMedium, - AIHard, -} - -impl PlayerType { - pub fn next(&self) -> Self { - match self { - PlayerType::Human => PlayerType::AIEasy, - PlayerType::AIEasy => PlayerType::AIMedium, - PlayerType::AIMedium => PlayerType::AIHard, - PlayerType::AIHard => PlayerType::Human, - } - } - - pub fn previous(&self) -> Self { - match self { - PlayerType::Human => PlayerType::AIHard, - PlayerType::AIEasy => PlayerType::Human, - PlayerType::AIMedium => PlayerType::AIEasy, - PlayerType::AIHard => PlayerType::AIMedium, - } - } - - pub fn display_name(&self) -> &str { - match self { - PlayerType::Human => "Human", - PlayerType::AIEasy => "AI - Easy", - PlayerType::AIMedium => "AI - Medium", - PlayerType::AIHard => "AI - Hard", - } - } - - pub fn create_controller_for_player(&self, player: Player) -> Box { - match self { - PlayerType::Human => { - if player == Player::Right { - Box::new(HumanController::new(KeyCode::W, KeyCode::S)) - } else { - Box::new(HumanController::new(KeyCode::Up, KeyCode::Down)) - } - } - PlayerType::AIEasy => Box::new(AIController::easy()), - PlayerType::AIMedium => Box::new(AIController::medium()), - PlayerType::AIHard => Box::new(AIController::hard()), - } - } -} - -#[cfg(test)] -mod tests { - use super::PlayerType; - - #[test] - fn next_cycles_in_order() { - assert_eq!(PlayerType::Human.next(), PlayerType::AIEasy); - assert_eq!(PlayerType::AIEasy.next(), PlayerType::AIMedium); - assert_eq!(PlayerType::AIMedium.next(), PlayerType::AIHard); - assert_eq!(PlayerType::AIHard.next(), PlayerType::Human); - } - - #[test] - fn previous_cycles_in_order() { - assert_eq!(PlayerType::Human.previous(), PlayerType::AIHard); - assert_eq!(PlayerType::AIEasy.previous(), PlayerType::Human); - assert_eq!(PlayerType::AIMedium.previous(), PlayerType::AIEasy); - assert_eq!(PlayerType::AIHard.previous(), PlayerType::AIMedium); - } -} diff --git a/src/racket.rs b/src/racket.rs deleted file mode 100644 index ad19e98..0000000 --- a/src/racket.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::controller::{Controller, RacketAction::*}; -use ggez::graphics::{Canvas, Color, DrawParam, Mesh, Rect}; -use ggez::{Context, GameResult}; - -const RACKET_SPEED: f32 = 650.0; -pub const RACKET_HEIGHT: f32 = 150.0; -pub const RACKET_WIDTH: f32 = 20.0; -pub const RACKET_HEIGHT_HALF: f32 = RACKET_HEIGHT / 2.0; -pub const RACKET_WIDTH_HALF: f32 = RACKET_WIDTH / 2.0; -pub const RACKET_OFFSET: f32 = RACKET_WIDTH * 2.0; - -pub struct Racket { - pub position_y: f32, - pub position_x: f32, - racket_mesh: Mesh, - pub controller: Box, -} - -impl Racket { - pub fn new(x: f32, y: f32, context: &mut Context, controller: Box) -> GameResult { - let rect = Rect::new(-RACKET_WIDTH / 2.0, -RACKET_HEIGHT / 2.0, RACKET_WIDTH, RACKET_HEIGHT); - let racket_mesh = Mesh::new_rectangle(context, ggez::graphics::DrawMode::fill(), rect, Color::WHITE)?; - - Ok(Self { - position_x: x, - position_y: y, - racket_mesh, - controller, - }) - } - - pub fn draw_on_canvas(&self, canvas: &mut Canvas) { - canvas.draw(&self.racket_mesh, DrawParam::default().dest([self.position_x, self.position_y])); - } - - pub fn update(&mut self, input: &crate::controller::ControllerInput, delta_time: f32) { - match self.controller.get_action(input) { - MoveUp => { - self.position_y -= RACKET_SPEED * delta_time; - } - MoveDown => { - self.position_y += RACKET_SPEED * delta_time; - } - Stay => {} - } - - // Keep the racket inside the screen bounds - let half_height = RACKET_HEIGHT / 2.0; - if self.position_y < half_height { - self.position_y = half_height; - } - let lower_limit = input.screen_height - half_height; - if self.position_y > lower_limit { - self.position_y = lower_limit; - } - } -} diff --git a/src/score.rs b/src/score.rs deleted file mode 100644 index e0843e6..0000000 --- a/src/score.rs +++ /dev/null @@ -1,60 +0,0 @@ -use ggez::graphics::{Canvas, Color, DrawParam, PxScale, Text}; -use ggez::{Context, GameResult, glam::Vec2}; - -pub struct Score { - p1: u8, - p2: u8, - text: Text, - position: Vec2, - scale: f32, -} - -impl Score { - pub fn new(context: &mut Context) -> GameResult { - let p1 = 0; - let p2 = 0; - let scale = context.gfx.drawable_size().1 / 3.0; - let mut text = Text::new(format!("{} {}", p1, p2)); - text.set_scale(PxScale::from(scale)); - let text_dimensions = text.measure(context)?; - let position = Vec2::new( - context.gfx.drawable_size().0 / 2.0 - text_dimensions.x / 2.0, - context.gfx.drawable_size().1 / 2.0 - text_dimensions.y / 2.0, - ); - - Ok(Self { p1, p2, text, position, scale }) - } - - fn update_text(&mut self, context: &mut Context) -> GameResult<()> { - self.text = Text::new(format!("{} {}", self.p1, self.p2)); - self.text.set_scale(PxScale::from(self.scale)); - let text_dimensions = self.text.measure(context)?; - self.position = Vec2::new( - context.gfx.drawable_size().0 / 2.0 - text_dimensions.x / 2.0, - context.gfx.drawable_size().1 / 2.0 - text_dimensions.y / 2.0, - ); - Ok(()) - } - - pub fn increment_p1(&mut self, context: &mut Context) -> GameResult<()> { - self.p1 = self.p1.saturating_add(1); - self.update_text(context) - } - - pub fn increment_p2(&mut self, context: &mut Context) -> GameResult<()> { - self.p2 = self.p2.saturating_add(1); - self.update_text(context) - } - - pub fn draw_on_canvas(&self, canvas: &mut Canvas) { - canvas.draw(&self.text, DrawParam::default().dest(self.position).color(Color::from_rgb(50, 50, 50))); - } - - pub fn get_p1_score(&self) -> u8 { - self.p1 - } - - pub fn get_p2_score(&self) -> u8 { - self.p2 - } -} diff --git a/src/ui/menu.rs b/src/ui/menu.rs index da268fa..b5f7768 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -1,7 +1,7 @@ use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, Rect, Text}; use ggez::{Context, GameResult, glam::Vec2}; -use crate::player_type::PlayerType; +use crate::player::player_type::PlayerType; // Layout ratios for the menu UI (tweak here to adjust spacing/size) const BOX_WIDTH_RATIO: f32 = 0.35; diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b9a0e3e..59e9662 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1 +1,3 @@ +pub mod hud; pub mod menu; +pub mod pause_screen; From 27cd4bf0c63ad76ed96e04c04e5929d9b4b4ba27 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 24 Oct 2025 12:33:10 -0400 Subject: [PATCH 03/19] Remove re-exports from game and player modules for cleaner structure --- src/game/mod.rs | 6 ------ src/player/mod.rs | 2 -- 2 files changed, 8 deletions(-) diff --git a/src/game/mod.rs b/src/game/mod.rs index 3438144..9179b11 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -2,9 +2,3 @@ pub mod ball; pub mod physics; pub mod racket; pub mod score; - -// Re-export commonly used items at crate::game::* if desired by callers. -pub use ball::*; -pub use physics::*; -pub use racket::*; -pub use score::*; diff --git a/src/player/mod.rs b/src/player/mod.rs index 9c4330e..b79a90f 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -1,5 +1,3 @@ pub mod controller; pub mod player_type; -pub use controller::*; -pub use player_type::*; From 84cbaef2672c10e6bfcdb85a520c4d018c612b5a Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Fri, 24 Oct 2025 13:08:35 -0400 Subject: [PATCH 04/19] bump version to 1.0.1 in Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 827c659..254149f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "Rust-Pong" -version = "1.0.0" +version = "1.0.1" authors = ["Vianpyro"] edition = "2024" From 3d515b54b21ec2ce7f4daad4098dcfc310d98420 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sat, 25 Oct 2025 10:57:22 -0400 Subject: [PATCH 05/19] Refactor UI components: implement game over and pause screen rendering, update HUD drawing --- src/main_state.rs | 75 ++++-------------------------------------- src/ui/game_over.rs | 37 +++++++++++++++++++++ src/ui/hud.rs | 7 ++-- src/ui/mod.rs | 1 + src/ui/pause_screen.rs | 28 +++++++++++++--- 5 files changed, 73 insertions(+), 75 deletions(-) create mode 100644 src/ui/game_over.rs diff --git a/src/main_state.rs b/src/main_state.rs index bba7b11..5fcc7ce 100644 --- a/src/main_state.rs +++ b/src/main_state.rs @@ -13,7 +13,7 @@ use crate::player::controller::ControllerInput; use crate::player::player_type::PlayerType; use crate::ui::menu as ui_menu; use crate::{audio::play_embedded_sound, debug::DebugInfo}; -use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, Rect, Text}; +use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, Rect}; use ggez::{Context, GameResult, event, glam::Vec2, input::keyboard::KeyCode}; use std::collections::HashSet; @@ -75,16 +75,6 @@ impl MainState { }) } - fn draw_centered_title(&self, canvas: &mut Canvas, context: &mut Context, text: &str, color: Color) -> GameResult { - let (screen_width, screen_height) = context.gfx.drawable_size(); - let mut title = Text::new(text); - title.set_scale(screen_height / 10.0); - let title_dimensions = title.measure(context)?; - let title_position = Vec2::new((screen_width - title_dimensions.x) / 2.0, screen_height / 3.0); - canvas.draw(&title, DrawParam::default().dest(title_position).color(color)); - Ok(()) - } - fn update_controllers(&mut self, context: &mut Context) -> GameResult { let screen_height = context.gfx.drawable_size().1; let screen_height_center = screen_height / 2.0; @@ -251,14 +241,16 @@ impl event::EventHandler for MainState { self.draw_playing(&mut canvas); } GameState::Paused => { - self.draw_paused(context, &mut canvas)?; + self.draw_playing(&mut canvas); + crate::ui::pause_screen::draw_pause_screen(context, &mut canvas)?; } GameState::GameOver { winner } => { - self.draw_game_over(context, &mut canvas, *winner)?; + self.draw_playing(&mut canvas); + crate::ui::game_over::draw_game_over(context, &mut canvas, *winner)?; } } - self.debug.draw(&mut canvas); + crate::ui::hud::draw_hud(context, &mut canvas, &self.debug)?; canvas.finish(context)?; Ok(()) } @@ -350,59 +342,4 @@ impl MainState { self.player_right.draw_on_canvas(canvas); self.ball.draw_on_canvas(canvas); } - - fn draw_game_over(&self, context: &mut Context, canvas: &mut Canvas, winner: Player) -> GameResult { - // First draw the game state - self.draw_playing(canvas); - - // Semi-transparent overlay - let overlay_rect = Rect::new(0.0, 0.0, context.gfx.drawable_size().0, context.gfx.drawable_size().1); - let overlay_mesh = Mesh::new_rectangle(context, DrawMode::fill(), overlay_rect, Color::from_rgba(0, 0, 0, 180))?; - canvas.draw(&overlay_mesh, DrawParam::default()); - - let (screen_width, screen_height) = context.gfx.drawable_size(); - - // Winner text - let winner_text = match winner { - Player::Left => "Player 1 Wins!", - Player::Right => "Player 2 Wins!", - }; - self.draw_centered_title(canvas, context, winner_text, Color::WHITE)?; - - // Press to continue - let mut continue_text = Text::new("SPACE/ENTER: Menu | R: Restart | Esc: Menu"); - continue_text.set_scale(screen_height / 30.0); - let continue_dimensions = continue_text.measure(context)?; - let continue_position = Vec2::new((screen_width - continue_dimensions.x) / 2.0, screen_height * 0.65); - canvas.draw( - &continue_text, - DrawParam::default().dest(continue_position).color(Color::from_rgb(200, 200, 200)), - ); - - Ok(()) - } - - fn draw_paused(&self, context: &mut Context, canvas: &mut Canvas) -> GameResult { - // Draw current game state in the background - self.draw_playing(canvas); - - // Semi-transparent overlay - let overlay_rect = Rect::new(0.0, 0.0, context.gfx.drawable_size().0, context.gfx.drawable_size().1); - let overlay_mesh = Mesh::new_rectangle(context, DrawMode::fill(), overlay_rect, Color::from_rgba(0, 0, 0, 160))?; - canvas.draw(&overlay_mesh, DrawParam::default()); - - let (screen_width, screen_height) = context.gfx.drawable_size(); - - // Paused title - self.draw_centered_title(canvas, context, "Paused", Color::WHITE)?; - - // Hints - let mut hint = Text::new("P: Resume | Esc: Menu"); - hint.set_scale(screen_height / 30.0); - let hint_dimensions = hint.measure(context)?; - let hint_position = Vec2::new((screen_width - hint_dimensions.x) / 2.0, screen_height * 0.65); - canvas.draw(&hint, DrawParam::default().dest(hint_position).color(Color::from_rgb(200, 200, 200))); - - Ok(()) - } } diff --git a/src/ui/game_over.rs b/src/ui/game_over.rs new file mode 100644 index 0000000..b401756 --- /dev/null +++ b/src/ui/game_over.rs @@ -0,0 +1,37 @@ +use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, Rect, Text}; +use ggez::{Context, GameResult, glam::Vec2}; + +use crate::game::physics::Player; + +pub fn draw_game_over(context: &mut Context, canvas: &mut Canvas, winner: Player) -> GameResult { + // Semi-transparent overlay + let overlay_rect = Rect::new(0.0, 0.0, context.gfx.drawable_size().0, context.gfx.drawable_size().1); + let overlay_mesh = Mesh::new_rectangle(context, DrawMode::fill(), overlay_rect, Color::from_rgba(0, 0, 0, 180))?; + canvas.draw(&overlay_mesh, DrawParam::default()); + + let (screen_width, screen_height) = context.gfx.drawable_size(); + + // Winner text + let winner_text = match winner { + Player::Left => "Player 1 Wins!", + Player::Right => "Player 2 Wins!", + }; + + let mut title = Text::new(winner_text); + title.set_scale(screen_height / 10.0); + let title_dimensions = title.measure(context)?; + let title_position = Vec2::new((screen_width - title_dimensions.x) / 2.0, screen_height / 3.0); + canvas.draw(&title, DrawParam::default().dest(title_position).color(Color::WHITE)); + + // Press to continue + let mut continue_text = Text::new("R: Restart | Esc: Menu"); + continue_text.set_scale(screen_height / 30.0); + let continue_dimensions = continue_text.measure(context)?; + let continue_position = Vec2::new((screen_width - continue_dimensions.x) / 2.0, screen_height * 0.65); + canvas.draw( + &continue_text, + DrawParam::default().dest(continue_position).color(Color::from_rgb(200, 200, 200)), + ); + + Ok(()) +} diff --git a/src/ui/hud.rs b/src/ui/hud.rs index 4faf912..96ac5c3 100644 --- a/src/ui/hud.rs +++ b/src/ui/hud.rs @@ -1,7 +1,10 @@ use ggez::graphics::Canvas; use ggez::{Context, GameResult}; -pub fn draw_hud(_context: &mut Context, _canvas: &mut Canvas) -> GameResult { - // Placeholder stub for HUD +use crate::debug::DebugInfo; + +pub fn draw_hud(_context: &mut Context, canvas: &mut Canvas, debug: &DebugInfo) -> GameResult { + // Delegate debug drawing to the DebugInfo helper + debug.draw(canvas); Ok(()) } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 59e9662..125edb1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,3 +1,4 @@ +pub mod game_over; pub mod hud; pub mod menu; pub mod pause_screen; diff --git a/src/ui/pause_screen.rs b/src/ui/pause_screen.rs index 05da45e..ec844a9 100644 --- a/src/ui/pause_screen.rs +++ b/src/ui/pause_screen.rs @@ -1,7 +1,27 @@ -use ggez::graphics::Canvas; -use ggez::{Context, GameResult}; +use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, Rect, Text}; +use ggez::{Context, GameResult, glam::Vec2}; + +pub fn draw_pause_screen(context: &mut Context, canvas: &mut Canvas) -> GameResult { + // Semi-transparent overlay + let overlay_rect = Rect::new(0.0, 0.0, context.gfx.drawable_size().0, context.gfx.drawable_size().1); + let overlay_mesh = Mesh::new_rectangle(context, DrawMode::fill(), overlay_rect, Color::from_rgba(0, 0, 0, 160))?; + canvas.draw(&overlay_mesh, DrawParam::default()); + + let (screen_width, screen_height) = context.gfx.drawable_size(); + + // Paused title + let mut title = Text::new("Paused"); + title.set_scale(screen_height / 10.0); + let title_dimensions = title.measure(context)?; + let title_position = Vec2::new((screen_width - title_dimensions.x) / 2.0, screen_height / 3.0); + canvas.draw(&title, DrawParam::default().dest(title_position).color(Color::WHITE)); + + // Hints + let mut hint = Text::new("P: Resume | Esc: Menu"); + hint.set_scale(screen_height / 30.0); + let hint_dimensions = hint.measure(context)?; + let hint_position = Vec2::new((screen_width - hint_dimensions.x) / 2.0, screen_height * 0.65); + canvas.draw(&hint, DrawParam::default().dest(hint_position).color(Color::from_rgb(200, 200, 200))); -pub fn draw_pause_screen(_context: &mut Context, _canvas: &mut Canvas) -> GameResult { - // Placeholder stub for pause screen UI Ok(()) } From d331c9d468efc8992ce99a4f63fb6da6f399fb2b Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sat, 25 Oct 2025 11:00:16 -0400 Subject: [PATCH 06/19] Fix formatting of continue text in game over screen --- src/ui/game_over.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/game_over.rs b/src/ui/game_over.rs index b401756..d6089f5 100644 --- a/src/ui/game_over.rs +++ b/src/ui/game_over.rs @@ -24,7 +24,7 @@ pub fn draw_game_over(context: &mut Context, canvas: &mut Canvas, winner: Player canvas.draw(&title, DrawParam::default().dest(title_position).color(Color::WHITE)); // Press to continue - let mut continue_text = Text::new("R: Restart | Esc: Menu"); + let mut continue_text = Text::new("R: Restart | Esc: Menu"); continue_text.set_scale(screen_height / 30.0); let continue_dimensions = continue_text.measure(context)?; let continue_position = Vec2::new((screen_width - continue_dimensions.x) / 2.0, screen_height * 0.65); From aaaf04edcb701c260b6f3ddd62cb948180690279 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sat, 25 Oct 2025 11:45:34 -0400 Subject: [PATCH 07/19] Refactor UI: centralize title drawing logic into common module --- src/ui/common.rs | 12 ++++++++++++ src/ui/game_over.rs | 6 +----- src/ui/mod.rs | 1 + src/ui/pause_screen.rs | 7 +------ 4 files changed, 15 insertions(+), 11 deletions(-) create mode 100644 src/ui/common.rs diff --git a/src/ui/common.rs b/src/ui/common.rs new file mode 100644 index 0000000..a28acc5 --- /dev/null +++ b/src/ui/common.rs @@ -0,0 +1,12 @@ +use ggez::graphics::{Canvas, Color, DrawParam, Text}; +use ggez::{Context, GameResult, glam::Vec2}; + +pub fn draw_centered_title(context: &mut Context, canvas: &mut Canvas, text: &str, color: Color) -> GameResult { + let (screen_width, screen_height) = context.gfx.drawable_size(); + let mut title = Text::new(text); + title.set_scale(screen_height / 10.0); + let title_dimensions = title.measure(context)?; + let title_position = Vec2::new((screen_width - title_dimensions.x) / 2.0, screen_height / 3.0); + canvas.draw(&title, DrawParam::default().dest(title_position).color(color)); + Ok(()) +} diff --git a/src/ui/game_over.rs b/src/ui/game_over.rs index d6089f5..b2d5130 100644 --- a/src/ui/game_over.rs +++ b/src/ui/game_over.rs @@ -17,11 +17,7 @@ pub fn draw_game_over(context: &mut Context, canvas: &mut Canvas, winner: Player Player::Right => "Player 2 Wins!", }; - let mut title = Text::new(winner_text); - title.set_scale(screen_height / 10.0); - let title_dimensions = title.measure(context)?; - let title_position = Vec2::new((screen_width - title_dimensions.x) / 2.0, screen_height / 3.0); - canvas.draw(&title, DrawParam::default().dest(title_position).color(Color::WHITE)); + super::common::draw_centered_title(context, canvas, winner_text, Color::WHITE)?; // Press to continue let mut continue_text = Text::new("R: Restart | Esc: Menu"); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 125edb1..db4cdcb 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,3 +1,4 @@ +mod common; pub mod game_over; pub mod hud; pub mod menu; diff --git a/src/ui/pause_screen.rs b/src/ui/pause_screen.rs index ec844a9..0eec983 100644 --- a/src/ui/pause_screen.rs +++ b/src/ui/pause_screen.rs @@ -9,12 +9,7 @@ pub fn draw_pause_screen(context: &mut Context, canvas: &mut Canvas) -> GameResu let (screen_width, screen_height) = context.gfx.drawable_size(); - // Paused title - let mut title = Text::new("Paused"); - title.set_scale(screen_height / 10.0); - let title_dimensions = title.measure(context)?; - let title_position = Vec2::new((screen_width - title_dimensions.x) / 2.0, screen_height / 3.0); - canvas.draw(&title, DrawParam::default().dest(title_position).color(Color::WHITE)); + super::common::draw_centered_title(context, canvas, "Paused", Color::WHITE)?; // Hints let mut hint = Text::new("P: Resume | Esc: Menu"); From 343e7eb463adb45ec2c4c45bc720fed0b658eaa5 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sat, 25 Oct 2025 12:00:29 -0400 Subject: [PATCH 08/19] Update README.md with project details and add LICENSE file --- LICENSE | 21 ++++++++++++++++ README.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a5bb322 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Vianney Veremme + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 3a7d956..9431762 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,72 @@ -# Template +# Rust-Pong -Solid GitHub template repository +Welcome to the **Rust-Pong** repository! This repository contains a small Pong clone implemented in Rust to demonstrate modular game structure, audio, and simple UI. + +## 🚀 Getting Started + +To get started with this project: + +1. Clone the repository: + ```bash + git clone https://github.com/Vianpyro/Rust-Pong.git + cd Rust-Pong + ``` +2. Install the Rust toolchain if you don't already have it: https://rustup.rs/ +3. Build the project: + ```bash + cargo build + ``` +4. Run the game: + ```bash + cargo run --release + ``` + +Notes: +- Assets (sound effects, etc.) are located in the `assets/` directory. Make sure they remain next to the executable when distributing or running the game. +- If you encounter platform-specific audio or windowing issues, ensure the required system libraries (for audio/display) are available for your OS. + +## 📁 Project Structure + +The repository contains the following directories and files (high level): + +- `assets/` - Game assets (sounds, images, etc.) +- `src/` - Application source code + - `audio/` - Audio handling + - `game/` - Game objects and physics (ball, racket, score) + - `player/` - Player and controller code + - `ui/` - Menus, HUD, and screens + - `main.rs` - Application entry point + - `main_state.rs`, `debug.rs` - Game state and debugging helpers +- `Cargo.toml` - Rust package manifest +- `LICENSE` - Project license (see file for terms) + +## 🛠 Features + +- Rust-based Pong clone demonstrating basic game loop, physics, and UI. +- Modular code organization (audio, game logic, players, UI). +- Lightweight and easy to extend for experimentation or learning. + +## 📖 Documentation + +The code is organized into clear modules under `src/`. For details, explore the following files and folders: +- `src/game/` — core game logic and physics +- `src/audio/` — audio playback and resource handling +- `src/ui/` — UI screens (menu, HUD, pause, game over) + +Expand this README as the project grows to include contribution guidelines, a development roadmap, and detailed architecture notes. + +## 🤝 Contributing + +Contributions are welcome. Suggested workflow: +1. Fork the repository. +2. Create a feature branch: `git checkout -b feature/your-feature` +3. Make your changes and add tests where applicable. +4. Open a pull request to the main repository. + +When opening issues or PRs, provide reproduction steps and any relevant logs or OS details. + +## 📝 License + +See the [`LICENSE`](/LICENSE) file in this repository for license terms. + +Happy coding! 🎉 From 5863b044a2bf09a3aec4bb171fbc186f6ca90b2d Mon Sep 17 00:00:00 2001 From: Vianpyro <10519369+Vianpyro@users.noreply.github.com> Date: Sat, 25 Oct 2025 16:02:32 +0000 Subject: [PATCH 09/19] Super-Linter: Fix linting issues --- README.md | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9431762..ed77e0a 100644 --- a/README.md +++ b/README.md @@ -7,21 +7,22 @@ Welcome to the **Rust-Pong** repository! This repository contains a small Pong c To get started with this project: 1. Clone the repository: - ```bash - git clone https://github.com/Vianpyro/Rust-Pong.git - cd Rust-Pong - ``` -2. Install the Rust toolchain if you don't already have it: https://rustup.rs/ + ```bash + git clone https://github.com/Vianpyro/Rust-Pong.git + cd Rust-Pong + ``` +2. Install the Rust toolchain if you don't already have it: 3. Build the project: - ```bash - cargo build - ``` + ```bash + cargo build + ``` 4. Run the game: - ```bash - cargo run --release - ``` + ```bash + cargo run --release + ``` Notes: + - Assets (sound effects, etc.) are located in the `assets/` directory. Make sure they remain next to the executable when distributing or running the game. - If you encounter platform-specific audio or windowing issues, ensure the required system libraries (for audio/display) are available for your OS. @@ -31,12 +32,12 @@ The repository contains the following directories and files (high level): - `assets/` - Game assets (sounds, images, etc.) - `src/` - Application source code - - `audio/` - Audio handling - - `game/` - Game objects and physics (ball, racket, score) - - `player/` - Player and controller code - - `ui/` - Menus, HUD, and screens - - `main.rs` - Application entry point - - `main_state.rs`, `debug.rs` - Game state and debugging helpers + - `audio/` - Audio handling + - `game/` - Game objects and physics (ball, racket, score) + - `player/` - Player and controller code + - `ui/` - Menus, HUD, and screens + - `main.rs` - Application entry point + - `main_state.rs`, `debug.rs` - Game state and debugging helpers - `Cargo.toml` - Rust package manifest - `LICENSE` - Project license (see file for terms) @@ -49,15 +50,17 @@ The repository contains the following directories and files (high level): ## 📖 Documentation The code is organized into clear modules under `src/`. For details, explore the following files and folders: + - `src/game/` — core game logic and physics - `src/audio/` — audio playback and resource handling - `src/ui/` — UI screens (menu, HUD, pause, game over) -Expand this README as the project grows to include contribution guidelines, a development roadmap, and detailed architecture notes. +Expand this readme as the project grows to include contribution guidelines, a development roadmap, and detailed architecture notes. ## 🤝 Contributing Contributions are welcome. Suggested workflow: + 1. Fork the repository. 2. Create a feature branch: `git checkout -b feature/your-feature` 3. Make your changes and add tests where applicable. From 57b3f7542aaeda48d038605b07cf1c2fa4b645df Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sat, 25 Oct 2025 12:06:36 -0400 Subject: [PATCH 10/19] Update README.md: clarify notes on sound effect files and distribution --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9431762..ae10c76 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ To get started with this project: cargo run --release ``` -Notes: -- Assets (sound effects, etc.) are located in the `assets/` directory. Make sure they remain next to the executable when distributing or running the game. -- If you encounter platform-specific audio or windowing issues, ensure the required system libraries (for audio/display) are available for your OS. +> [!NOTE] +> Sound effect files (sfx) are embedded in the binary, so you don't need to include them when distributing the executable. +> If you encounter platform-specific audio or windowing issues, ensure the required system libraries (for audio/display) are available for your OS. ## 📁 Project Structure From 384ec57df6e91d5ea542b3c1fa47b9573d823e9a Mon Sep 17 00:00:00 2001 From: Vianney Veremme <10519369+Vianpyro@users.noreply.github.com> Date: Sat, 25 Oct 2025 12:07:34 -0400 Subject: [PATCH 11/19] Fix formatting issues in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4a39651..43bbebd 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ To get started with this project: ``` > [!NOTE] -> Sound effect files (sfx) are embedded in the binary, so you don't need to include them when distributing the executable. -> If you encounter platform-specific audio or windowing issues, ensure the required system libraries (for audio/display) are available for your OS. +> - Sound effect files (sfx) are embedded in the binary, so you don't need to include them when distributing the executable. +> - If you encounter platform-specific audio or windowing issues, ensure the required system libraries (for audio/display) are available for your OS. ## 📁 Project Structure From d0120b7867d52d3b6dcb1f24e13af4391dc8fed0 Mon Sep 17 00:00:00 2001 From: Vianpyro <10519369+Vianpyro@users.noreply.github.com> Date: Sat, 25 Oct 2025 16:09:23 +0000 Subject: [PATCH 12/19] Super-Linter: Fix linting issues --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 43bbebd..c7e198a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ To get started with this project: ``` > [!NOTE] +> > - Sound effect files (sfx) are embedded in the binary, so you don't need to include them when distributing the executable. > - If you encounter platform-specific audio or windowing issues, ensure the required system libraries (for audio/display) are available for your OS. From 429704b3163f8306dc37c86837831925ce4ad401 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sat, 25 Oct 2025 12:14:21 -0400 Subject: [PATCH 13/19] Update super-linter workflow: upgrade actions and dependencies --- .github/workflows/super-linter.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index 073ac01..ad83e6a 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -33,7 +33,7 @@ jobs: prettier --write . - name: Super-linter - uses: super-linter/super-linter/slim@v7 + uses: super-linter/super-linter/slim@v8 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VALIDATE_ALL_CODEBASE: false @@ -50,7 +50,7 @@ jobs: - name: Commit and push fixes if: github.event_name == 'pull_request' && github.event.pull_request.head.ref != github.event.repository.default_branch - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@v7 with: branch: ${{ github.event.pull_request.head.ref }} commit_message: "Super-Linter: Fix linting issues" From c2d0dce9f2d62443e8febb5f87b72197b104e0e9 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sat, 25 Oct 2025 12:39:56 -0400 Subject: [PATCH 14/19] Update super-linter workflow: adjust credential persistence and filter regex --- .github/workflows/super-linter.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index ad83e6a..ade0026 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -23,6 +23,7 @@ jobs: uses: actions/checkout@v5 with: fetch-depth: 0 + persist-credentials: false - name: Run Prettier uses: actions/setup-node@v6 @@ -37,8 +38,9 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VALIDATE_ALL_CODEBASE: false - FILTER_REGEX_EXCLUDE: "(.devcontainer/Dockerfile|.github/pull_request_template.md|.github/ISSUE_TEMPLATE/*.md)" + FILTER_REGEX_EXCLUDE: "(.devcontainer/Dockerfile|.github/**/*.md|.github/workflows/*.(yml|yaml))" # Disable problematic validators + VALIDATE_BIOME_FORMAT: false VALIDATE_DOCKERFILE_HADOLINT: false VALIDATE_GIT_COMMITLINT: false # Enable fixers for PR events From 816464b9e1dee6e5b1f30abfff2f4b5108a08ebd Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sat, 25 Oct 2025 12:46:01 -0400 Subject: [PATCH 15/19] Update super-linter workflow: refine filter regex and add Rust Clippy options --- .github/workflows/super-linter.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index ade0026..02dfc29 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -38,7 +38,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VALIDATE_ALL_CODEBASE: false - FILTER_REGEX_EXCLUDE: "(.devcontainer/Dockerfile|.github/**/*.md|.github/workflows/*.(yml|yaml))" + FILTER_REGEX_EXCLUDE: "(.devcontainer/Dockerfile|.github/pull_request_template.md|.github/ISSUE_TEMPLATE/*.md|.github/workflows/*.(yml|yaml))" # Disable problematic validators VALIDATE_BIOME_FORMAT: false VALIDATE_DOCKERFILE_HADOLINT: false @@ -49,6 +49,8 @@ jobs: FIX_MARKDOWN: true FIX_MARKDOWN_PRETTIER: true FIX_NATURAL_LANGUAGE: ${{ github.event_name == 'pull_request' }} + FIX_RUST_CLIPPY: ${{ github.event_name == 'pull_request' }} + RUST_CLIPPY_COMMAND_OPTIONS: "--config max_width=160" - name: Commit and push fixes if: github.event_name == 'pull_request' && github.event.pull_request.head.ref != github.event.repository.default_branch From 6518e285e518197216d790048ce707144de6d773 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sat, 25 Oct 2025 12:50:03 -0400 Subject: [PATCH 16/19] Update super-linter workflow: add validation for GitHub Actions Zizmor --- .github/workflows/super-linter.yml | 2 ++ src/debug.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index 02dfc29..f50ca64 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -43,6 +43,7 @@ jobs: VALIDATE_BIOME_FORMAT: false VALIDATE_DOCKERFILE_HADOLINT: false VALIDATE_GIT_COMMITLINT: false + VALIDATE_GITHUB_ACTIONS_ZIZMOR: false # Enable fixers for PR events FIX_JSON: true FIX_JSON_PRETTIER: true @@ -50,6 +51,7 @@ jobs: FIX_MARKDOWN_PRETTIER: true FIX_NATURAL_LANGUAGE: ${{ github.event_name == 'pull_request' }} FIX_RUST_CLIPPY: ${{ github.event_name == 'pull_request' }} + # Custom options for linters RUST_CLIPPY_COMMAND_OPTIONS: "--config max_width=160" - name: Commit and push fixes diff --git a/src/debug.rs b/src/debug.rs index 580f53e..48eac93 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,5 +1,5 @@ -use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, PxScale, Text}; use ggez::{Context, GameResult, glam::Vec2}; +use ggez::graphics::{Canvas, Color, DrawMode, DrawParam, Mesh, PxScale, Text}; pub struct DebugInfo { enabled: bool, From 48dfa3438d88db26d2a8d44dda9bebd5a3b3143a Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sat, 25 Oct 2025 12:58:36 -0400 Subject: [PATCH 17/19] Update super-linter workflow: add fetch for PR base branch to improve changed-files detection --- .github/workflows/super-linter.yml | 6 ++++++ src/ui/common.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index f50ca64..c47d03a 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -33,6 +33,12 @@ jobs: npm install -g prettier prettier --write . + - name: Fetch PR base branch + if: github.event_name == 'pull_request' + run: | + # fetch the base ref so changed-files detection can compare correctly + git fetch --no-tags --prune origin +refs/heads/${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }} + - name: Super-linter uses: super-linter/super-linter/slim@v8 env: diff --git a/src/ui/common.rs b/src/ui/common.rs index a28acc5..77943c1 100644 --- a/src/ui/common.rs +++ b/src/ui/common.rs @@ -1,5 +1,5 @@ -use ggez::graphics::{Canvas, Color, DrawParam, Text}; use ggez::{Context, GameResult, glam::Vec2}; +use ggez::graphics::{Canvas, Color, DrawParam, Text}; pub fn draw_centered_title(context: &mut Context, canvas: &mut Canvas, text: &str, color: Color) -> GameResult { let (screen_width, screen_height) = context.gfx.drawable_size(); From dc1ed85ee8325cdc3482e48d6b121f831d814539 Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sat, 25 Oct 2025 13:06:05 -0400 Subject: [PATCH 18/19] Update super-linter workflow: add debug step for workspace and event details; enable validation for all codebase --- .github/workflows/super-linter.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index c47d03a..3c4f21d 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -39,11 +39,18 @@ jobs: # fetch the base ref so changed-files detection can compare correctly git fetch --no-tags --prune origin +refs/heads/${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }} + - name: Debug workspace & event + run: | + echo "=== Git tracked files ===" + git ls-files | sed -n '1,200p' + echo "=== Event payload (partial) ===" + cat $GITHUB_EVENT_PATH | sed -n '1,200p' + - name: Super-linter uses: super-linter/super-linter/slim@v8 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VALIDATE_ALL_CODEBASE: false + VALIDATE_ALL_CODEBASE: true FILTER_REGEX_EXCLUDE: "(.devcontainer/Dockerfile|.github/pull_request_template.md|.github/ISSUE_TEMPLATE/*.md|.github/workflows/*.(yml|yaml))" # Disable problematic validators VALIDATE_BIOME_FORMAT: false From 56ce5e181ee1acbdfebdf17992e00902ea8a7fcb Mon Sep 17 00:00:00 2001 From: Vianpyro Date: Sat, 25 Oct 2025 13:17:22 -0400 Subject: [PATCH 19/19] Update super-linter workflow: enable Rust Clippy fixer for all events --- .github/workflows/super-linter.yml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml index 3c4f21d..fb759f4 100644 --- a/.github/workflows/super-linter.yml +++ b/.github/workflows/super-linter.yml @@ -23,7 +23,6 @@ jobs: uses: actions/checkout@v5 with: fetch-depth: 0 - persist-credentials: false - name: Run Prettier uses: actions/setup-node@v6 @@ -33,19 +32,6 @@ jobs: npm install -g prettier prettier --write . - - name: Fetch PR base branch - if: github.event_name == 'pull_request' - run: | - # fetch the base ref so changed-files detection can compare correctly - git fetch --no-tags --prune origin +refs/heads/${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }} - - - name: Debug workspace & event - run: | - echo "=== Git tracked files ===" - git ls-files | sed -n '1,200p' - echo "=== Event payload (partial) ===" - cat $GITHUB_EVENT_PATH | sed -n '1,200p' - - name: Super-linter uses: super-linter/super-linter/slim@v8 env: @@ -63,7 +49,7 @@ jobs: FIX_MARKDOWN: true FIX_MARKDOWN_PRETTIER: true FIX_NATURAL_LANGUAGE: ${{ github.event_name == 'pull_request' }} - FIX_RUST_CLIPPY: ${{ github.event_name == 'pull_request' }} + FIX_RUST_CLIPPY: true # Custom options for linters RUST_CLIPPY_COMMAND_OPTIONS: "--config max_width=160"