Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/botarena.yml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 0 additions & 23 deletions .github/workflows/rust.yml

This file was deleted.

8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions .idea/botarena.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 0 additions & 25 deletions Dockerfile

This file was deleted.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Bot Arena

[![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)

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.
Expand Down
29 changes: 10 additions & 19 deletions src/arena.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::audio::AudioManager;
use crate::config;
use crate::config::*;
use crate::particles::ParticleSystem;
Expand All @@ -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)]
Expand All @@ -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;
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"
Expand Down
41 changes: 25 additions & 16 deletions src/audio.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
Expand All @@ -51,4 +60,4 @@ impl AudioManager {
play_sound_once(sound);
}
}
}
}
3 changes: 0 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 7 additions & 5 deletions src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
);
}
Expand Down
6 changes: 4 additions & 2 deletions src/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<u32>()
Expand All @@ -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::<u32>() {
cycle = Some(c);
}
Expand Down
Loading