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/.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
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..e7e96e0 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# Bot Arena
+[](https://github.com/sdeming/botarena/actions/workflows/botarena.yml)
+

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.
diff --git a/src/arena.rs b/src/arena.rs
index 47ece43..a89b884 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;
@@ -6,8 +7,6 @@ use crate::types::*;
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)]
@@ -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;
@@ -104,17 +102,6 @@ impl Arena {
}
}
- // 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 {
@@ -588,7 +575,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/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/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/logging.rs b/src/logging.rs
index 008ee43..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_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 +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_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 965fa5a..37c07ab 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)]
@@ -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/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..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,
@@ -911,10 +875,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..b11658b 100644
--- a/src/robot.rs
+++ b/src/robot.rs
@@ -8,16 +8,13 @@ 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;
-use crate::vm::executor::InstructionExecutor;
// Represents the possible states of a robot
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
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],
@@ -906,6 +883,7 @@ mod tests {
// Import ArenaCommand
use crate::types::Point;
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};
@@ -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,
@@ -1367,7 +1345,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 +1359,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 +1386,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,16 +1409,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))
- ],
- labels: std::collections::HashMap::new(), // Empty labels map
+ instructions: vec![Instruction::Mov(Register::D0, Operand::Value(10.0))],
};
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 6d99527..8fa3558 100644
--- a/src/vm/executor/mod.rs
+++ b/src/vm/executor/mod.rs
@@ -2,22 +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 instruction_executor; // Added
-mod misc_ops; // Added
-mod register_ops; // Added
-mod stack_ops; // Added
-mod trig_ops; // Added
-pub mod processor; // <-- 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
+pub mod component_ops;
+mod control_flow_ops;
+mod instruction_executor;
+mod misc_ops;
+pub mod processor;
+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..cae00fe 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),
@@ -799,11 +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,
- labels,
- // Constants are no longer stored here
- })
+ Ok(ParsedProgram { instructions })
}
// Helper: parse an operand (register, value, or constant)
@@ -923,7 +918,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 +1163,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)