From b857b447f0e1464d7eada574698ff360d1e69a4d Mon Sep 17 00:00:00 2001 From: Scott Date: Sun, 27 Apr 2025 10:20:53 -0400 Subject: [PATCH 1/6] build: ci work --- .github/workflows/botarena.yml | 71 ++++++++++++++++++++++++++++++++++ .github/workflows/rust.yml | 23 ----------- Dockerfile | 25 ------------ README.md | 2 + 4 files changed, 73 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/botarena.yml delete mode 100644 .github/workflows/rust.yml delete mode 100644 Dockerfile diff --git a/.github/workflows/botarena.yml b/.github/workflows/botarena.yml new file mode 100644 index 0000000..44eb04e --- /dev/null +++ b/.github/workflows/botarena.yml @@ -0,0 +1,71 @@ +name: Bot Arena CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + rust: [stable] + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + components: clippy, rustfmt + + # Linux dependencies for Macroquad + - name: Install Linux dependencies + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y libx11-dev libxi-dev libgl1-mesa-dev libasound2-dev + + # Cache dependencies + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + # Check formatting + - name: Check formatting + run: cargo fmt -- --check + + # Run clippy + - name: Clippy + run: cargo clippy -- -D warnings + + # Run tests + - name: Run tests + run: cargo test --verbose + + # Build in release mode + - name: Build (release) + run: cargo build --release + + # Optional: Upload artifacts + - name: Upload artifact + if: matrix.os == 'ubuntu-latest' + uses: actions/upload-artifact@v4 + with: + name: botarena-build + path: target/release diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index e0fce90..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Rust - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Build docker image - run: docker build -t botarena . - - - name: Run tests - run: docker run --rm -v ${{ github.workspace }}:/app botarena cargo test --release diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3f0c037..0000000 --- a/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -# Use the official Rust image as a base image -FROM rust:latest - -# Set the working directory within the container -WORKDIR /app - -# Copy the project's Cargo.toml and Cargo.lock files -COPY Cargo.toml Cargo.lock ./ - -# Install the necessary packages for Rust -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - git \ - wget \ - libasound2-dev \ - && rm -rf /var/lib/apt/lists/* - -# Copy the project's source code into the container -COPY . ./ - -# Build the project -RUN cargo build --release - -# Run tests -RUN cargo test diff --git a/README.md b/README.md index b35604f..a96cb9e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Bot Arena +![GitHub Workflow Status](https://img.shields.io/github/workflow/status/sdeming/botarena/CI?label=test) + ![Bot Arena](https://raw.githubusercontent.com/sdeming/botarena/main/screenshot.png) A programmable robot battle simulator written in Rust. Program your own battle bots in a custom stack-based assembly language (RASM), then watch them fight in a dynamic, obstacle-filled arena! Supports up to 4 robots per match, real-time rendering, and detailed logging for debugging and analysis. From 430fd4d97396bfaf9f758e7540e861e98c009215 Mon Sep 17 00:00:00 2001 From: Scott Date: Sun, 27 Apr 2025 10:26:35 -0400 Subject: [PATCH 2/6] style: cargo fmt --- src/arena.rs | 8 ++++++-- src/audio.rs | 41 +++++++++++++++++++++++++---------------- src/game.rs | 12 +++++++----- src/main.rs | 6 +++--- src/particles.rs | 6 ++++-- src/render.rs | 9 +++++++-- src/robot.rs | 32 ++++++++++++++++++++++---------- src/vm/executor/mod.rs | 14 +++++++------- 8 files changed, 81 insertions(+), 47 deletions(-) diff --git a/src/arena.rs b/src/arena.rs index 47ece43..eb9e073 100644 --- a/src/arena.rs +++ b/src/arena.rs @@ -1,3 +1,4 @@ +use crate::audio::AudioManager; use crate::config; use crate::config::*; use crate::particles::ParticleSystem; @@ -7,7 +8,6 @@ use ::rand::prelude::*; use macroquad::prelude::*; use macroquad::prelude::{ORANGE, SKYBLUE, Vec2, YELLOW}; use std::f64::INFINITY; -use crate::audio::AudioManager; // Represents an obstacle in the arena #[derive(Debug, Clone, Copy, PartialEq)] @@ -588,7 +588,11 @@ mod tests { source_robot: 1, }; arena.spawn_projectile(projectile2); - arena.update_projectiles(&mut robots, &mut particle_system_lethal, &audio_manager_lethal); + arena.update_projectiles( + &mut robots, + &mut particle_system_lethal, + &audio_manager_lethal, + ); assert!( arena.projectiles.is_empty(), "Lethal projectile should be removed" diff --git a/src/audio.rs b/src/audio.rs index bf5d26c..28ed606 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -1,5 +1,5 @@ -use macroquad::audio::{load_sound, play_sound_once, Sound}; use log::warn; +use macroquad::audio::{Sound, load_sound, play_sound_once}; #[derive(Default)] pub struct AudioManager { @@ -15,20 +15,29 @@ impl AudioManager { // Load all required sound assets pub async fn load_assets(&mut self) { - self.fire_sound = load_sound("assets/fire1.ogg").await.map_err(|e| { - warn!("Failed to load fire sound 'assets/fire1.ogg': {}", e); - e - }).ok(); - - self.hit_sound = load_sound("assets/boom1.ogg").await.map_err(|e| { - warn!("Failed to load hit sound 'assets/boom1.ogg': {}", e); - e - }).ok(); - - self.death_sound = load_sound("assets/death1.ogg").await.map_err(|e| { - warn!("Failed to load death sound 'assets/death1.ogg': {}", e); - e - }).ok(); + self.fire_sound = load_sound("assets/fire1.ogg") + .await + .map_err(|e| { + warn!("Failed to load fire sound 'assets/fire1.ogg': {}", e); + e + }) + .ok(); + + self.hit_sound = load_sound("assets/boom1.ogg") + .await + .map_err(|e| { + warn!("Failed to load hit sound 'assets/boom1.ogg': {}", e); + e + }) + .ok(); + + self.death_sound = load_sound("assets/death1.ogg") + .await + .map_err(|e| { + warn!("Failed to load death sound 'assets/death1.ogg': {}", e); + e + }) + .ok(); } // Play the fire sound if loaded @@ -51,4 +60,4 @@ impl AudioManager { play_sound_once(sound); } } -} \ No newline at end of file +} diff --git a/src/game.rs b/src/game.rs index f321b62..39c9d4c 100644 --- a/src/game.rs +++ b/src/game.rs @@ -290,9 +290,13 @@ impl Game { // Update Phase 2: Physics and Interactions // Collect projectile movements for trail spawning *before* moving them - let mut projectile_movements: Vec<(Vec2, Vec2)> = Vec::with_capacity(self.arena.projectiles.len()); + let mut projectile_movements: Vec<(Vec2, Vec2)> = + Vec::with_capacity(self.arena.projectiles.len()); for projectile in &self.arena.projectiles { - let start_pos = Vec2::new(projectile.prev_position.x as f32, projectile.prev_position.y as f32); + let start_pos = Vec2::new( + projectile.prev_position.x as f32, + projectile.prev_position.y as f32, + ); // Estimate end position based on current velocity and cycle duration // Note: This might differ slightly from the final position after collision checks let direction_rad = projectile.direction.to_radians(); // Convert degrees to radians @@ -316,9 +320,7 @@ impl Game { // Note: We iterate using the collected movements, not the potentially modified projectile list for (start_pos, end_pos) in projectile_movements { self.particle_system.spawn_projectile_trail( - start_pos, - end_pos, - 2, // Number of particles per tick per projectile + start_pos, end_pos, 2, // Number of particles per tick per projectile 0.25, // Lifetime of trail particles (in seconds) ); } diff --git a/src/main.rs b/src/main.rs index 965fa5a..6756237 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod arena; +mod audio; mod config; mod game; mod logging; @@ -8,18 +9,17 @@ mod robot; mod types; mod utils; mod vm; -mod audio; use crate::config::{ARENA_WIDTH, UI_PANEL_WIDTH, WINDOW_HEIGHT}; use clap::Parser; -use log::{error, info, LevelFilter}; +use log::{LevelFilter, error, info}; use macroquad::prelude::*; use std::process; +use crate::audio::AudioManager; use crate::game::Game; use crate::logging::init_logger; use crate::render::Renderer; -use crate::audio::AudioManager; // Command line arguments structure #[derive(Parser, Debug)] diff --git a/src/particles.rs b/src/particles.rs index ca41997..6e29415 100644 --- a/src/particles.rs +++ b/src/particles.rs @@ -149,10 +149,12 @@ impl ParticleSystem { // Give particles a slight random drift, perpendicular to the trail direction let perpendicular_dir = Vec2::new(-direction.y, direction.x).normalize_or_zero(); let drift_speed = self.rng.r#gen_range(0.0..=(config::UNIT_SIZE * 0.5)) as f32; // Small drift - let drift_velocity = perpendicular_dir * drift_speed * (self.rng.r#gen::() - 0.5) * 2.0; // Random direction + let drift_velocity = + perpendicular_dir * drift_speed * (self.rng.r#gen::() - 0.5) * 2.0; // Random direction // Base velocity can be zero or slightly backward to simulate dissipating smoke - let base_velocity = -direction.normalize_or_zero() * self.rng.r#gen_range(0.0..=(config::UNIT_SIZE*0.1)) as f32; + let base_velocity = -direction.normalize_or_zero() + * self.rng.r#gen_range(0.0..=(config::UNIT_SIZE * 0.1)) as f32; let final_velocity = base_velocity + drift_velocity; diff --git a/src/render.rs b/src/render.rs index 7d40193..0994e9d 100644 --- a/src/render.rs +++ b/src/render.rs @@ -911,10 +911,15 @@ void main() { // Interpolate position for smooth rendering between game ticks let current_interp_pos = utils::lerp_point(projectile.prev_position, projectile.position, alpha); - let current_screen_pos = point_to_vec2(current_interp_pos, arena_screen_width, arena_screen_height); + let current_screen_pos = + point_to_vec2(current_interp_pos, arena_screen_width, arena_screen_height); // Get the screen position from the *start* of the current tick - let prev_tick_screen_pos = point_to_vec2(projectile.prev_position, arena_screen_width, arena_screen_height); + let prev_tick_screen_pos = point_to_vec2( + projectile.prev_position, + arena_screen_width, + arena_screen_height, + ); // Draw the vapor trail line (fading gray) let trail_color = faded_color(LIGHTGRAY, 0.5); // Use helper for faded color diff --git a/src/robot.rs b/src/robot.rs index c446510..5f755d2 100644 --- a/src/robot.rs +++ b/src/robot.rs @@ -3,6 +3,7 @@ use crate::config; use crate::types::Scanner; use crate::types::*; use crate::vm; +use crate::vm::executor::InstructionExecutor; use crate::vm::instruction::Instruction; use crate::vm::parser; use crate::vm::state::VMState; @@ -10,7 +11,6 @@ use rand::prelude::*; use std::collections::VecDeque; use std::f64::INFINITY; use std::f64::consts::PI; -use crate::vm::executor::InstructionExecutor; // Represents the possible states of a robot #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1367,7 +1367,10 @@ mod tests { .expect("MOV instruction execution failed"); // Verify the MOV worked - assert_eq!(robot.vm_state.registers.get(Register::D0).unwrap(), test_value); + assert_eq!( + robot.vm_state.registers.get(Register::D0).unwrap(), + test_value + ); } #[test] @@ -1378,12 +1381,15 @@ mod tests { // We need the executor to process the instruction let executor = vm::executor::InstructionExecutor::new(); - // --- Test setting velocity to 1.0 --- + // --- Test setting velocity to 1.0 --- let target_grid_velocity = 1.0; let drive_instr = Instruction::Drive(Operand::Value(target_grid_velocity)); // Explicitly select the Drive component (ID 1) before executing - robot.vm_state.set_selected_component(1).expect("Failed to select drive component"); + robot + .vm_state + .set_selected_component(1) + .expect("Failed to select drive component"); // Execute the Drive(1.0) instruction executor @@ -1402,11 +1408,14 @@ mod tests { robot.drive.velocity ); - // --- Test setting velocity to 0.0 --- + // --- Test setting velocity to 0.0 --- let stop_instr = Instruction::Drive(Operand::Value(0.0)); // Ensure Drive component is still selected (or re-select if necessary) - robot.vm_state.set_selected_component(1).expect("Failed to select drive component"); + robot + .vm_state + .set_selected_component(1) + .expect("Failed to select drive component"); // Execute the Drive(0.0) instruction executor @@ -1422,15 +1431,18 @@ mod tests { // Added back the missing helper function fn setup_test_robot() -> (Robot, Arena) { - let mut robot = Robot::new(1, "Test".to_string(), Point{ x: 0.5, y: 0.5}, Point{ x: 0.5, y: 0.5}); + let mut robot = Robot::new( + 1, + "Test".to_string(), + Point { x: 0.5, y: 0.5 }, + Point { x: 0.5, y: 0.5 }, + ); let arena = Arena::new(); // Add a simple program if needed, e.g., MOV D0, 10 // Note: load_program expects ParsedProgram, not Vec // Creating a dummy ParsedProgram for now let dummy_program = crate::vm::parser::ParsedProgram { - instructions: vec![ - Instruction::Mov(Register::D0, Operand::Value(10.0)) - ], + instructions: vec![Instruction::Mov(Register::D0, Operand::Value(10.0))], labels: std::collections::HashMap::new(), // Empty labels map }; robot.load_program(dummy_program); diff --git a/src/vm/executor/mod.rs b/src/vm/executor/mod.rs index 6d99527..157a7fa 100644 --- a/src/vm/executor/mod.rs +++ b/src/vm/executor/mod.rs @@ -2,16 +2,16 @@ // Publicly expose submodules (corrected based on directory listing) mod arithmetic_ops; -mod bitwise_ops; // Added +mod bitwise_ops; // Added mod combat_ops; mod component_ops; -mod control_flow_ops;// Changed from control_ops +mod control_flow_ops; // Changed from control_ops mod instruction_executor; // Added -mod misc_ops; // Added -mod register_ops; // Added -mod stack_ops; // Added -mod trig_ops; // Added -pub mod processor; // <-- Make public +mod misc_ops; // Added +pub mod processor; +mod register_ops; // Added +mod stack_ops; // Added +mod trig_ops; // Added // <-- Make public // Removed: branch_ops, control_ops, memory_ops, movement_ops (files don't exist) // Re-export key components for easier access From 9fb4286b935ac7c3d223529449fff657e450c614 Mon Sep 17 00:00:00 2001 From: Scott Date: Sun, 27 Apr 2025 10:29:02 -0400 Subject: [PATCH 3/6] chore: add rustrover .idea changes --- .idea/.gitignore | 8 ++++++++ .idea/botarena.iml | 12 ++++++++++++ .idea/modules.xml | 8 ++++++++ .idea/vcs.xml | 6 ++++++ 4 files changed, 34 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/botarena.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/botarena.iml b/.idea/botarena.iml new file mode 100644 index 0000000..7c12fe5 --- /dev/null +++ b/.idea/botarena.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2a5785e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From 9e4b60e072f50a9b688a2c2b4f2eabbc1740f9bb Mon Sep 17 00:00:00 2001 From: Scott Date: Sun, 27 Apr 2025 11:03:22 -0400 Subject: [PATCH 4/6] style: fix lots of clippy errors --- src/arena.rs | 23 +++------- src/config.rs | 3 -- src/logging.rs | 4 +- src/main.rs | 4 +- src/render.rs | 56 +++++-------------------- src/robot.rs | 31 ++------------ src/vm/error.rs | 12 ------ src/vm/executor/component_ops.rs | 2 +- src/vm/executor/instruction_executor.rs | 22 +++++----- src/vm/executor/mod.rs | 20 ++++----- src/vm/parser.rs | 19 ++++----- src/vm/registers.rs | 2 +- src/vm/stack.rs | 12 +----- src/vm/state.rs | 7 ---- 14 files changed, 51 insertions(+), 166 deletions(-) diff --git a/src/arena.rs b/src/arena.rs index eb9e073..c9d45ce 100644 --- a/src/arena.rs +++ b/src/arena.rs @@ -7,7 +7,6 @@ use crate::types::*; use ::rand::prelude::*; use macroquad::prelude::*; use macroquad::prelude::{ORANGE, SKYBLUE, Vec2, YELLOW}; -use std::f64::INFINITY; // Represents an obstacle in the arena #[derive(Debug, Clone, Copy, PartialEq)] @@ -28,7 +27,6 @@ pub struct Arena { } impl Arena { - // Creates a new Arena instance pub fn new() -> Self { let width = ARENA_WIDTH_UNITS as f64 * UNIT_SIZE; let height = ARENA_HEIGHT_UNITS as f64 * UNIT_SIZE; @@ -103,18 +101,7 @@ impl Arena { y: (grid_y as f64 + 0.5) * self.unit_size, } } - - // Converts world coordinates (f64) to grid coordinates (u32) - pub fn world_to_grid(&self, world_x: f64, world_y: f64) -> (u32, u32) { - let grid_x = (world_x / self.unit_size).floor() as u32; - let grid_y = (world_y / self.unit_size).floor() as u32; - // Clamp to grid boundaries - ( - grid_x.min(self.grid_width - 1), - grid_y.min(self.grid_height - 1), - ) - } - + // Adds a projectile to the arena's list pub fn spawn_projectile(&mut self, projectile: Projectile) { log::debug!( @@ -279,11 +266,11 @@ impl Arena { let sin_a = angle_rad.sin(); let robot_radius = self.unit_size / 2.0; - let _min_dist = INFINITY; + let _min_dist = f64::INFINITY; // --- Check Walls --- // Calculate distance for the CENTER point hitting the wall first. - let mut min_dist_wall_center = INFINITY; + let mut min_dist_wall_center = f64::INFINITY; if cos_a.abs() > 1e-9 { let dist_x0 = -start_point.x / cos_a; if dist_x0 > 1e-9 { @@ -326,12 +313,12 @@ impl Arena { let inv_dx = if cos_a.abs() > 1e-9 { 1.0 / cos_a } else { - INFINITY + f64::INFINITY }; let inv_dy = if sin_a.abs() > 1e-9 { 1.0 / sin_a } else { - INFINITY + f64::INFINITY }; for obstacle in &self.obstacles { diff --git a/src/config.rs b/src/config.rs index 47b08e2..568ab9b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,9 +15,6 @@ pub const UI_PANEL_WIDTH: i32 = 200; // Width of the side panel pub const ARENA_WIDTH: i32 = WINDOW_WIDTH - UI_PANEL_WIDTH; // Width for the arena rendering pub const ARENA_HEIGHT: i32 = WINDOW_HEIGHT; // Arena uses full height -// Game rules -pub const MAX_TURNS: u32 = 1000; // Maximum turns before draw - // Scanner configuration pub const DEFAULT_SCANNER_FOV: f64 = 22.5; // +/- 11.25 degrees from center pub const DEFAULT_SCANNER_RANGE: f64 = 1.414; // Maximum arena diagonal (1.0 width + 1.0 height) diff --git a/src/logging.rs b/src/logging.rs index 008ee43..4f365b2 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -60,7 +60,7 @@ impl log::Log for BotArenaLogger { // Look for "Robot N" pattern if robot_id.is_none() { if let Some(robot_idx) = message.find("Robot ") { - if let Some(end_idx) = message[robot_idx + 6..].find(|c: char| !c.is_digit(10)) + if let Some(end_idx) = message[robot_idx + 6..].find(|c: char| !c.is_ascii_digit()) { if let Ok(id) = message[robot_idx + 6..robot_idx + 6 + end_idx].parse::() @@ -73,7 +73,7 @@ impl log::Log for BotArenaLogger { // Look for Cycle N pattern if let Some(cycle_idx) = message.find("Cycle ") { - if let Some(end_idx) = message[cycle_idx + 6..].find(|c: char| !c.is_digit(10)) { + if let Some(end_idx) = message[cycle_idx + 6..].find(|c: char| !c.is_ascii_digit()) { if let Ok(c) = message[cycle_idx + 6..cycle_idx + 6 + end_idx].parse::() { cycle = Some(c); } diff --git a/src/main.rs b/src/main.rs index 6756237..37c07ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,8 +53,8 @@ struct Args { fn window_conf() -> Conf { Conf { window_title: "Bot Arena".to_owned(), - window_width: (ARENA_WIDTH + UI_PANEL_WIDTH) as i32, - window_height: WINDOW_HEIGHT as i32, + window_width: (ARENA_WIDTH + UI_PANEL_WIDTH), + window_height: WINDOW_HEIGHT, high_dpi: true, ..Default::default() } diff --git a/src/render.rs b/src/render.rs index 0994e9d..763c3c0 100644 --- a/src/render.rs +++ b/src/render.rs @@ -56,7 +56,6 @@ fn get_health_gradient_color(ratio: f32) -> Color { // Handles rendering the simulation state using macroquad pub struct Renderer { - material: Option, scene_rt: Option, bright_rt: Option, blur_rt1: Option, @@ -64,16 +63,15 @@ pub struct Renderer { brightness_material: Option, h_blur_material: Option, v_blur_material: Option, - additive_material: Option, // Material for final additive blend - scanner_material: Option, // Material for scanner visualization - title_font: Option, // Font for UI title - ui_font: Option, // Font for general UI elements + additive_material: Option, + scanner_material: Option, + title_font: Option, + ui_font: Option, } impl Renderer { pub fn new() -> Self { Renderer { - material: None, scene_rt: None, bright_rt: None, blur_rt1: None, @@ -83,43 +81,11 @@ impl Renderer { v_blur_material: None, additive_material: None, scanner_material: None, - title_font: None, // Initialize title_font as None - ui_font: None, // Initialize ui_font as None + title_font: None, + ui_font: None, } } - pub fn init_material(&mut self) { - let material = load_material( - ShaderSource::Glsl { - vertex: "#version 100 -attribute vec3 position; -attribute vec2 texcoord; // We don't use texcoord here, but need it for macroquad's default mesh -varying vec4 frag_color; // Pass color through -uniform mat4 Model; -uniform mat4 Projection; -void main() { - gl_Position = Projection * Model * vec4(position, 1.0); - // Assign a default color or pass vertex color if available - // Since we're tinting everything drawn with this shader, - // the actual input color doesn't matter as much, - // but let's just use white. - frag_color = vec4(1.0, 1.0, 1.0, 1.0); -}", - fragment: "#version 100 -precision mediump float; -varying vec4 frag_color; // Receive color from vertex shader -void main() { - // Apply red tint to the incoming fragment color - gl_FragColor = frag_color * vec4(1.0, 0.3, 0.3, 1.0); // Stronger red tint -}", - }, - // Note: No MaterialParams needed if not using textures/uniforms beyond default Model/Projection - MaterialParams::default(), // Use default params - ) - .unwrap(); - self.material = Some(material); - } - pub fn init_scanner_material(&mut self) { let vertex_shader = "#version 100 attribute vec3 position; @@ -270,7 +236,6 @@ void main() { color_blend: None, ..Default::default() }, - ..Default::default() }; let h_blur_params = MaterialParams { @@ -280,7 +245,6 @@ void main() { color_blend: None, ..Default::default() }, - ..Default::default() }; let v_blur_params = MaterialParams { textures: vec!["InputTexture".to_string()], @@ -289,7 +253,6 @@ void main() { color_blend: None, ..Default::default() }, - ..Default::default() }; self.brightness_material = Some( @@ -308,7 +271,7 @@ void main() { load_material( ShaderSource::Glsl { vertex: post_process_vertex_shader, - fragment: &blur_fragment_shader, + fragment: blur_fragment_shader, }, h_blur_params, ) @@ -320,7 +283,7 @@ void main() { load_material( ShaderSource::Glsl { vertex: post_process_vertex_shader, - fragment: &blur_fragment_shader, + fragment: blur_fragment_shader, }, v_blur_params, ) @@ -356,13 +319,13 @@ void main() { textures: vec!["InputTexture".to_string()], // Still needs texture input uniforms: vec![UniformDesc::new("GlowIntensity", UniformType::Float1)], // Add uniform desc pipeline_params: additive_pipeline_params, - ..Default::default() }, ) .unwrap(), ); } + #[allow(clippy::too_many_arguments)] pub fn draw_frame( &mut self, arena: &Arena, @@ -859,6 +822,7 @@ void main() { ); } + #[allow(clippy::too_many_arguments)] fn draw_triangle_at_angle( center_pos: Vec2, radius: f32, diff --git a/src/robot.rs b/src/robot.rs index 5f755d2..53108d7 100644 --- a/src/robot.rs +++ b/src/robot.rs @@ -3,13 +3,11 @@ use crate::config; use crate::types::Scanner; use crate::types::*; use crate::vm; -use crate::vm::executor::InstructionExecutor; use crate::vm::instruction::Instruction; use crate::vm::parser; use crate::vm::state::VMState; use rand::prelude::*; use std::collections::VecDeque; -use std::f64::INFINITY; use std::f64::consts::PI; // Represents the possible states of a robot @@ -17,7 +15,6 @@ use std::f64::consts::PI; pub enum RobotStatus { Idle, // Just loaded, hasn't run yet Active, - Stunned(u32), // Stores remaining stun duration in cycles Destroyed, } @@ -198,7 +195,7 @@ impl Robot { let scanner_pos = self.position; let scanner_dir_rad = self.turret.direction.to_radians(); let scan_fov_half_rad = (self.turret.scanner.fov / 2.0).to_radians(); - let mut closest_target_dist_sq = INFINITY; + let mut closest_target_dist_sq = f64::INFINITY; let mut target_found = false; let mut best_target_angle_deg = 0.0; let mut best_target_dist = 0.0; @@ -272,24 +269,6 @@ impl Robot { self.status = RobotStatus::Idle; } - // Main update logic called once per cycle for the robot - // Needs Arena reference for collision checks during movement processing - pub fn update(&mut self, arena: &Arena) { - // Process actions that occur automatically each cycle - self.process_cycle_updates(arena); - - // Update VM state registers before executing instructions for this cycle - self.update_vm_state_registers(arena); - - // TODO: Implement stun handling - // REMOVED VM execution from here - handled by main loop - /* - if self.status == RobotStatus::Active { - let _maybe_projectile = self.execute_vm_cycle(); - } - */ - } - /// Updates the read-only registers in the VM state before each VM cycle execution pub fn update_vm_state_registers(&mut self, arena: &Arena) { // Update @rand register @@ -353,13 +332,11 @@ impl Robot { registers .set_internal(vm::registers::Register::WeaponCooldown, 0.0) .unwrap(); // Placeholder - // REMOVED ScanDistance and ScanAngle updates - handled by Scan instruction now - // registers.set_internal(vm::registers::Register::ScanDistance, self.turret.scanner.last_scan_distance).unwrap(); - // registers.set_internal(vm::registers::Register::ScanAngle, self.turret.scanner.last_scan_angle).unwrap(); } /// Execute one simulation cycle's worth of VM instructions. /// Requires the context of all robots and the arena state. + #[allow(dead_code)] pub fn execute_vm_cycle( &mut self, all_robots: &[Robot], @@ -905,6 +882,7 @@ mod tests { use crate::types::ArenaCommand; // Import ArenaCommand use crate::types::Point; + use crate::vm::executor::component_ops::ComponentOperations; use crate::vm::executor::Operand; use crate::vm::executor::processor::InstructionProcessor; use crate::vm::instruction::Instruction; @@ -1112,7 +1090,7 @@ mod tests { // Set velocity to 0.5 grid units per turn (using the Drive instruction directly) let drive_instruction = Instruction::Drive(Operand::Value(0.5)); - let processor = vm::executor::ComponentOperations::new(); + let processor = ComponentOperations::new(); processor .process( &mut robot, @@ -1443,7 +1421,6 @@ mod tests { // Creating a dummy ParsedProgram for now let dummy_program = crate::vm::parser::ParsedProgram { instructions: vec![Instruction::Mov(Register::D0, Operand::Value(10.0))], - labels: std::collections::HashMap::new(), // Empty labels map }; robot.load_program(dummy_program); (robot, arena) diff --git a/src/vm/error.rs b/src/vm/error.rs index e54893b..91b64de 100644 --- a/src/vm/error.rs +++ b/src/vm/error.rs @@ -40,19 +40,7 @@ pub enum VMFault { #[error("Selected component invalid for operation")] InvalidComponentForOp, #[error("Not enough power for operation")] - InsufficientPower, - #[error("Weapon overheated")] - WeaponOverheated, - #[error("Invalid weapon power value")] - InvalidWeaponPower, - #[error("Invalid scan result")] - InvalidScanResult, - #[error("Error during projectile handling")] - ProjectileError, - #[error("Call stack overflow")] CallStackOverflow, #[error("Call stack underflow")] CallStackUnderflow, - #[error("Instruction or feature not implemented")] - NotImplemented, } diff --git a/src/vm/executor/component_ops.rs b/src/vm/executor/component_ops.rs index 49933c9..567754c 100644 --- a/src/vm/executor/component_ops.rs +++ b/src/vm/executor/component_ops.rs @@ -55,7 +55,7 @@ impl InstructionProcessor for ComponentOperations { selected_component ); match component_id { - 0 | 1 | 2 => { + 0..=2 => { let res = robot.vm_state.set_selected_component(component_id); crate::debug_instructions!( robot.id, diff --git a/src/vm/executor/instruction_executor.rs b/src/vm/executor/instruction_executor.rs index 921f988..51604eb 100644 --- a/src/vm/executor/instruction_executor.rs +++ b/src/vm/executor/instruction_executor.rs @@ -25,17 +25,17 @@ pub struct InstructionExecutor { impl InstructionExecutor { /// Create a new executor with all processors registered pub fn new() -> Self { - let mut processors: Vec> = Vec::new(); - - processors.push(Box::new(StackOperations::new())); - processors.push(Box::new(RegisterOperations::new())); - processors.push(Box::new(ArithmeticOperations::new())); - processors.push(Box::new(TrigonometricOperations::new())); - processors.push(Box::new(BitwiseOperations::new())); - processors.push(Box::new(ControlFlowOperations::new())); - processors.push(Box::new(ComponentOperations::new())); - processors.push(Box::new(CombatOperations::new())); - processors.push(Box::new(MiscellaneousOperations::new())); + let processors: Vec> = vec![ + Box::new(StackOperations::new()), + Box::new(RegisterOperations::new()), + Box::new(ArithmeticOperations::new()), + Box::new(TrigonometricOperations::new()), + Box::new(BitwiseOperations::new()), + Box::new(ControlFlowOperations::new()), + Box::new(ComponentOperations::new()), + Box::new(CombatOperations::new()), + Box::new(MiscellaneousOperations::new()), + ]; InstructionExecutor { processors } } diff --git a/src/vm/executor/mod.rs b/src/vm/executor/mod.rs index 157a7fa..8fa3558 100644 --- a/src/vm/executor/mod.rs +++ b/src/vm/executor/mod.rs @@ -4,20 +4,14 @@ mod arithmetic_ops; mod bitwise_ops; // Added mod combat_ops; -mod component_ops; -mod control_flow_ops; // Changed from control_ops -mod instruction_executor; // Added -mod misc_ops; // Added +pub mod component_ops; +mod control_flow_ops; +mod instruction_executor; +mod misc_ops; pub mod processor; -mod register_ops; // Added -mod stack_ops; // Added -mod trig_ops; // Added // <-- Make public -// Removed: branch_ops, control_ops, memory_ops, movement_ops (files don't exist) - -// Re-export key components for easier access -// pub use arithmetic_ops::ArithmeticOperations; // Unused -// pub use combat_ops::CombatOperations; // Unused -pub use component_ops::ComponentOperations; // Reinstated for tests +mod register_ops; +mod stack_ops; +mod trig_ops; pub use crate::vm::instruction::Instruction; pub use crate::vm::operand::Operand; diff --git a/src/vm/parser.rs b/src/vm/parser.rs index b5635df..5506448 100644 --- a/src/vm/parser.rs +++ b/src/vm/parser.rs @@ -16,7 +16,6 @@ pub struct ParseError { #[derive(Debug, Clone)] pub struct ParsedProgram { pub instructions: Vec, - pub labels: HashMap, } /// Parse and evaluate a constant expression @@ -52,8 +51,8 @@ fn parse_constant_expression( let tokens: Vec<&str> = expr.split_whitespace().collect(); // Define a recursive parsing function - fn parse_expr<'a>( - tokens: &'a [&str], + fn parse_expr( + tokens: &[&str], pos: &mut usize, constants: &HashMap, line: usize, @@ -77,8 +76,8 @@ fn parse_constant_expression( Ok(left) } - fn parse_term<'a>( - tokens: &'a [&str], + fn parse_term( + tokens: &[&str], pos: &mut usize, constants: &HashMap, line: usize, @@ -120,8 +119,8 @@ fn parse_constant_expression( Ok(left) } - fn parse_factor<'a>( - tokens: &'a [&str], + fn parse_factor( + tokens: &[&str], pos: &mut usize, constants: &HashMap, line: usize, @@ -237,7 +236,7 @@ pub fn parse_assembly( if parts.len() >= 3 { let name = parts[1].to_string(); // Check for conflict with predefined constants - if predefined_constants.map_or(false, |pre| pre.contains_key(&name)) { + if predefined_constants.is_some_and(|pre| pre.contains_key(&name)) { return Err(ParseError { line: line_num, message: format!("Attempted to redefine built-in constant: {}", name), @@ -801,8 +800,6 @@ pub fn parse_assembly( Ok(ParsedProgram { instructions, - labels, - // Constants are no longer stored here }) } @@ -923,7 +920,6 @@ mod tests { ); let program = result.unwrap(); assert_eq!(program.instructions.len(), 4); - assert_eq!(*program.labels.get("start").unwrap(), 0); assert!(matches!(program.instructions[3], Instruction::Jmp(0))); } @@ -1169,7 +1165,6 @@ mod tests { ); let program = result.unwrap(); assert_eq!(program.instructions.len(), 3); - assert_eq!(*program.labels.get("target").unwrap(), 1); // target label points to 'add' assert!(matches!(program.instructions[0], Instruction::Jmp(1))); assert!(matches!(program.instructions[1], Instruction::Add)); assert!(matches!(program.instructions[2], Instruction::Jz(1))); diff --git a/src/vm/registers.rs b/src/vm/registers.rs index cbac746..901bdc0 100644 --- a/src/vm/registers.rs +++ b/src/vm/registers.rs @@ -160,7 +160,7 @@ impl Registers { /// Set the value of a register (enforces write permissions) pub fn set(&mut self, reg: Register, value: f64) -> Result<(), RegisterError> { - if !reg.is_writable() { + if reg.is_readonly() { return Err(RegisterError::ReadOnlyRegister); } self.set_internal(reg, value) diff --git a/src/vm/stack.rs b/src/vm/stack.rs index d813ea7..9f281a4 100644 --- a/src/vm/stack.rs +++ b/src/vm/stack.rs @@ -11,11 +11,6 @@ pub struct Stack { } impl Stack { - /// Creates a new stack with a fixed maximum size - pub fn new() -> Self { - Self::with_size(32) // Default size - } - /// Creates a new stack with the specified maximum size pub fn with_size(max_size: usize) -> Self { Stack { @@ -61,11 +56,6 @@ impl Stack { Ok(()) } - /// Checks if the stack is empty - pub fn is_empty(&self) -> bool { - self.data.is_empty() - } - /// Returns a slice representing the current stack data (top is last element) pub fn view(&self) -> &[f64] { self.data.as_slices().0 // VecDeque can be non-contiguous, just get the main slice for debug @@ -78,7 +68,7 @@ mod tests { #[test] fn test_stack_push_pop() { - let mut stack = Stack::new(); + let mut stack = Stack::with_size(32); assert!(stack.push(1.0).is_ok()); assert!(stack.push(2.0).is_ok()); assert_eq!(stack.pop().unwrap(), 2.0); diff --git a/src/vm/state.rs b/src/vm/state.rs index 7569a12..621dab7 100644 --- a/src/vm/state.rs +++ b/src/vm/state.rs @@ -52,15 +52,8 @@ impl VMState { VMFault::DivisionByZero => 6, VMFault::NoComponentSelected => 7, VMFault::InvalidComponentForOp => 8, - // Add new fault codes - VMFault::InsufficientPower => 9, - VMFault::WeaponOverheated => 10, // Placeholder, not implemented yet - VMFault::InvalidWeaponPower => 11, - VMFault::InvalidScanResult => 12, // Placeholder, not implemented yet - VMFault::ProjectileError => 13, // Placeholder, not implemented yet VMFault::CallStackOverflow => 14, VMFault::CallStackUnderflow => 15, - VMFault::NotImplemented => 99, // Fault code for unimplemented instructions }; self.registers .set_internal(Register::Fault, fault_code as f64) From 1385ceb6adb3e7f9308d5f027567363294da5261 Mon Sep 17 00:00:00 2001 From: Scott Date: Sun, 27 Apr 2025 11:04:05 -0400 Subject: [PATCH 5/6] style: cargo fmt... --- src/arena.rs | 2 +- src/logging.rs | 6 ++++-- src/robot.rs | 2 +- src/vm/parser.rs | 4 +--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/arena.rs b/src/arena.rs index c9d45ce..a89b884 100644 --- a/src/arena.rs +++ b/src/arena.rs @@ -101,7 +101,7 @@ impl Arena { y: (grid_y as f64 + 0.5) * self.unit_size, } } - + // Adds a projectile to the arena's list pub fn spawn_projectile(&mut self, projectile: Projectile) { log::debug!( diff --git a/src/logging.rs b/src/logging.rs index 4f365b2..2546f0c 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -60,7 +60,8 @@ impl log::Log for BotArenaLogger { // Look for "Robot N" pattern if robot_id.is_none() { if let Some(robot_idx) = message.find("Robot ") { - if let Some(end_idx) = message[robot_idx + 6..].find(|c: char| !c.is_ascii_digit()) + if let Some(end_idx) = + message[robot_idx + 6..].find(|c: char| !c.is_ascii_digit()) { if let Ok(id) = message[robot_idx + 6..robot_idx + 6 + end_idx].parse::() @@ -73,7 +74,8 @@ impl log::Log for BotArenaLogger { // Look for Cycle N pattern if let Some(cycle_idx) = message.find("Cycle ") { - if let Some(end_idx) = message[cycle_idx + 6..].find(|c: char| !c.is_ascii_digit()) { + if let Some(end_idx) = message[cycle_idx + 6..].find(|c: char| !c.is_ascii_digit()) + { if let Ok(c) = message[cycle_idx + 6..cycle_idx + 6 + end_idx].parse::() { cycle = Some(c); } diff --git a/src/robot.rs b/src/robot.rs index 53108d7..b11658b 100644 --- a/src/robot.rs +++ b/src/robot.rs @@ -882,8 +882,8 @@ mod tests { use crate::types::ArenaCommand; // Import ArenaCommand use crate::types::Point; - use crate::vm::executor::component_ops::ComponentOperations; use crate::vm::executor::Operand; + use crate::vm::executor::component_ops::ComponentOperations; use crate::vm::executor::processor::InstructionProcessor; use crate::vm::instruction::Instruction; use crate::vm::parser::{ParsedProgram, parse_assembly}; diff --git a/src/vm/parser.rs b/src/vm/parser.rs index 5506448..cae00fe 100644 --- a/src/vm/parser.rs +++ b/src/vm/parser.rs @@ -798,9 +798,7 @@ pub fn parse_assembly( // Check for any errors during parsing and collect valid instructions let instructions: Vec = collected_results.into_iter().collect::>()?; - Ok(ParsedProgram { - instructions, - }) + Ok(ParsedProgram { instructions }) } // Helper: parse an operand (register, value, or constant) From cb0cfa402cec578e6248d4c99a8af9090555c087 Mon Sep 17 00:00:00 2001 From: Scott Date: Sun, 27 Apr 2025 11:11:57 -0400 Subject: [PATCH 6/6] docs: add badge to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a96cb9e..e7e96e0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Bot Arena -![GitHub Workflow Status](https://img.shields.io/github/workflow/status/sdeming/botarena/CI?label=test) +[![Bot Arena CI](https://github.com/sdeming/botarena/actions/workflows/botarena.yml/badge.svg)](https://github.com/sdeming/botarena/actions/workflows/botarena.yml) ![Bot Arena](https://raw.githubusercontent.com/sdeming/botarena/main/screenshot.png)