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
293 changes: 172 additions & 121 deletions LANGUAGE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion bots/jojo.rasm
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

.const DRIVE_ID 1
.const TURRET_ID 2
.const DRIVE_SPEED 2.0
.const DRIVE_SPEED 1.0
.const FIRE_POWER 1.0
.const ARRIVAL_THRESHOLD 0.1
.const WEAVE_STATE_INDEX 0
Expand Down
46 changes: 35 additions & 11 deletions src/arena.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use crate::config::*;
use crate::particles::ParticleSystem;
use crate::robot::{Robot, RobotStatus};
use crate::types::*;
use ::rand::SeedableRng;
use ::rand::prelude::*;
use ::rand::rngs::StdRng;
use macroquad::prelude::*;
use macroquad::prelude::{ORANGE, SKYBLUE, Vec2, YELLOW};

Expand Down Expand Up @@ -42,9 +44,15 @@ impl Arena {
}
}

// Places obstacles randomly based on configured density
pub fn place_obstacles(&mut self) {
let mut rng = thread_rng();
// Places obstacles randomly based on configured density with optional seed for deterministic placement
pub fn place_obstacles_with_seed(&mut self, seed: Option<u64>) {
// Create RNG - either seeded or using thread_rng
let mut rng: Box<dyn RngCore> = if let Some(seed_value) = seed {
Box::new(StdRng::seed_from_u64(seed_value))
} else {
Box::new(thread_rng())
};

let total_cells = self.grid_width * self.grid_height;
let num_obstacles = (total_cells as f32 * OBSTACLE_DENSITY).floor() as u32;

Expand Down Expand Up @@ -390,11 +398,9 @@ impl Arena {
}
}

/// Adds an obstacle at the given robot's position (for wreckage)
pub fn add_obstacle_at_robot(&mut self, robot: &Robot) {
self.obstacles.push(Obstacle {
position: robot.position,
});
/// Adds an obstacle at the given position
pub fn add_obstacle_at_position(&mut self, position: Point) {
self.obstacles.push(Obstacle { position });
}
}

Expand Down Expand Up @@ -509,9 +515,21 @@ mod tests {
let robot1_start = Point { x: 0.25, y: 0.5 };
let robot2_start = Point { x: 0.75, y: 0.5 };
let arena_center = Point { x: 0.5, y: 0.5 }; // Define center point
let mut robot1 = Robot::new(1, "TestRobot1".to_string(), robot1_start, arena_center);
let mut robot1 = Robot::new(
1,
"TestRobot1".to_string(),
robot1_start,
arena_center,
None,
);
robot1.status = RobotStatus::Active; // Manually set active for test
let mut robot2 = Robot::new(2, "TestRobot2".to_string(), robot2_start, arena_center);
let mut robot2 = Robot::new(
2,
"TestRobot2".to_string(),
robot2_start,
arena_center,
None,
);
robot2.status = RobotStatus::Active; // <-- Manually set status for test
let mut particle_system = ParticleSystem::new(); // <-- Create dummy particle system
let audio_manager = AudioManager::new(); // <-- Create dummy manager
Expand Down Expand Up @@ -602,7 +620,13 @@ mod tests {
let mut arena = Arena::new();
let robot1_start = Point { x: 0.5, y: 0.5 };
let arena_center = Point { x: 0.5, y: 0.5 }; // Define center point
let mut robot1 = Robot::new(1, "TestRobot1".to_string(), robot1_start, arena_center);
let mut robot1 = Robot::new(
1,
"TestRobot1".to_string(),
robot1_start,
arena_center,
None,
);
robot1.status = RobotStatus::Active; // Set active
let mut particle_system = ParticleSystem::new(); // <-- Create dummy particle system
let audio_manager = AudioManager::new(); // <-- Create dummy manager
Expand Down
3 changes: 2 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ pub const DEFAULT_INITIAL_HEALTH: f64 = 100.0;
pub const DEFAULT_INITIAL_POWER: f64 = 1.0;

// Robot Physics/Movement Configuration
pub const MAX_DRIVE_UNITS_PER_TURN: f64 = 5.0;
pub const MAX_DRIVE_SPEED_NORMALIZED: f64 = 1.0; // Maximum normalized speed (0.0 to 1.0)
pub const MAX_DRIVE_UNITS_PER_TURN: f64 = 5.0; // Actual grid units per turn at max speed
pub const DRIVE_VELOCITY_FACTOR: f64 = UNIT_SIZE / CYCLES_PER_TURN as f64;
pub const MAX_ROTATION_PER_CYCLE: f64 = 90.0 / CYCLES_PER_TURN as f64; // Degrees/cycle (scaled automatically, e.g., 3.6 deg/cycle for 100 cycles/turn)

Expand Down
96 changes: 89 additions & 7 deletions src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct Game {
pub current_turn: u32,
pub current_cycle: u32,
pub max_turns: u32,
pub seed: Option<u64>, // Store the seed for deterministic operations
time_accumulator: f32,
cycle_duration: f32,
game_over: bool,
Expand All @@ -33,6 +34,7 @@ impl Game {
robot_files: &[String],
max_turns: u32,
audio_manager: AudioManager,
seed: Option<u64>,
) -> Result<Self, Box<dyn std::error::Error>> {
// Create arena
let arena = Arena::new();
Expand All @@ -57,6 +59,11 @@ impl Game {
let mut robots = Vec::with_capacity(num_robots);
info!("Simulating for a maximum of {} turns.", max_turns);

// Log seed information
if let Some(seed_value) = seed {
info!("Using deterministic seed: {}", seed_value);
}

// Define starting positions
let offset = 2.0 * config::UNIT_SIZE;
let positions = [
Expand Down Expand Up @@ -109,7 +116,9 @@ impl Game {
// Parse the program using the predefined constants
match crate::vm::parser::parse_assembly(&program_content, Some(&predefined_constants)) {
Ok(parsed_program) => {
let mut robot = Robot::new(robot_id, robot_name, position, center);
// Create robot with seed if provided
let robot_seed = seed.map(|s| s.wrapping_add(robot_id as u64));
let mut robot = Robot::new(robot_id, robot_name, position, center, robot_seed);
robot.load_program(parsed_program);
robots.push(robot);
}
Expand All @@ -136,13 +145,21 @@ impl Game {
current_turn: 1,
current_cycle: 0,
max_turns,
seed,
time_accumulator: 0.0,
cycle_duration: 1.0 / config::CYCLES_PER_TURN as f32,
game_over: false,
winner: None,
})
}

/// Place obstacles in the arena using the stored seed for deterministic placement
pub fn place_obstacles(&mut self) {
// For obstacle placement, use a derived seed to avoid affecting robot RNG sequences
let obstacle_seed = self.seed.map(|s| s.wrapping_mul(31).wrapping_add(12345));
self.arena.place_obstacles_with_seed(obstacle_seed);
}

/// Run the main game loop using the provided renderer
pub async fn run(&mut self, renderer: &mut Renderer) -> Result<(), Box<dyn std::error::Error>> {
info!("Starting main loop...");
Expand Down Expand Up @@ -328,16 +345,19 @@ impl Game {
self.particle_system.update(self.cycle_duration);

// --- Remove destroyed robots, add obstacles, check win/draw ---
// This block correctly calculates and uses its own `destroyed_robots`
let destroyed_robots: Vec<Robot> = self
// Collect positions of destroyed robots before removing them
let destroyed_robot_positions: Vec<Point> = self
.robots
.iter()
.filter(|r| r.status == RobotStatus::Destroyed)
.cloned()
.map(|r| r.position)
.collect();
for robot in &destroyed_robots {
self.arena.add_obstacle_at_robot(robot);

// Add obstacles at destroyed robot positions
for position in destroyed_robot_positions {
self.arena.add_obstacle_at_position(position);
}

// Remove destroyed robots from the robots vector
self.robots.retain(|r| r.status != RobotStatus::Destroyed);

Expand Down Expand Up @@ -399,6 +419,59 @@ impl Game {
}
}
}

/// Run headless simulation without graphics for fast testing
pub async fn run_simulation(&mut self) -> Result<(), Box<dyn std::error::Error>> {
info!("Starting headless simulation...");

while self.current_turn <= self.max_turns && !self.game_over {
// Run a full turn (all cycles)
for _ in 0..config::CYCLES_PER_TURN {
self.update_simulation();

// Break early if game ends mid-turn
if self.game_over {
break;
}
}
}

// Report final results
let alive_robots: Vec<&Robot> = self
.robots
.iter()
.filter(|r| r.status != RobotStatus::Destroyed)
.collect();

match alive_robots.len() {
0 => info!(
"Simulation complete: DRAW - No survivors after {} turns",
self.current_turn - 1
),
1 => {
let winner = alive_robots[0];
info!(
"Simulation complete: {} WINS with {:.2} health after {} turns",
winner.name,
winner.health,
self.current_turn - 1
);
}
_ => {
info!(
"Simulation complete: {} survivors after {} turns:",
alive_robots.len(),
self.current_turn - 1
);
for robot in &alive_robots {
info!(" {} - Health: {:.2}", robot.name, robot.health);
}
}
}

info!("Exiting headless simulation.");
Ok(())
}
}

#[cfg(test)]
Expand All @@ -411,7 +484,13 @@ mod tests {
fn dummy_robot(id: u32, pos: Point, status: RobotStatus) -> Robot {
// Use a default center for dummy robots in tests
let center = Point { x: 0.5, y: 0.5 };
let mut robot = Robot::new(id, format!("TestRobot_{}", id).to_string(), pos, center);
let mut robot = Robot::new(
id,
format!("TestRobot_{}", id).to_string(),
pos,
center,
None,
);
robot.status = status;
robot
}
Expand All @@ -429,6 +508,7 @@ mod tests {
current_turn: 1,
current_cycle: 0,
max_turns: 10,
seed: None,
time_accumulator: 0.0,
cycle_duration: 1.0,
game_over: false,
Expand Down Expand Up @@ -462,6 +542,7 @@ mod tests {
current_turn: 1,
current_cycle: 0,
max_turns: 10,
seed: None,
time_accumulator: 0.0,
cycle_duration: 1.0,
game_over: false,
Expand All @@ -480,6 +561,7 @@ mod tests {
current_turn: 1,
current_cycle: 0,
max_turns: 10,
seed: None,
time_accumulator: 0.0,
cycle_duration: 1.0,
game_over: false,
Expand Down
45 changes: 31 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ struct Args {
/// Disable sound effects
#[arg(long)]
no_audio: bool,

/// Random seed for deterministic simulation (u64)
#[arg(long)]
seed: Option<u64>,

/// Run simulation without graphics (headless mode for fast testing)
#[arg(long)]
simulate: bool,
}

fn window_conf() -> Conf {
Expand Down Expand Up @@ -90,22 +98,15 @@ async fn main() {

info!("Bot Arena starting...");

// Create Renderer and load fonts
let mut renderer = Renderer::new();
renderer.load_title_font().await; // Load title font
renderer.load_ui_font().await; // Load UI font
renderer.init_glow_resources();
renderer.init_scanner_material();

// Create AudioManager
let mut audio_manager = AudioManager::new();
// Load sounds only if --no-audio is NOT specified
if !args.no_audio {
if !args.no_audio && !args.simulate {
audio_manager.load_assets().await;
}

// Create Game instance (passing potentially empty audio_manager)
let mut game = match Game::new(&args.robot_files, args.max_turns, audio_manager) {
let mut game = match Game::new(&args.robot_files, args.max_turns, audio_manager, args.seed) {
Ok(g) => g,
Err(e) => {
error!("Failed to initialize game: {}", e);
Expand All @@ -114,13 +115,29 @@ async fn main() {
};

if !args.no_obstacles {
game.arena.place_obstacles();
game.place_obstacles();
}

// Run the game loop
if let Err(e) = game.run(&mut renderer).await {
error!("Game loop error: {}", e);
process::exit(1);
// Run the game loop - either simulate mode or with graphics
if args.simulate {
// Headless simulation mode
if let Err(e) = game.run_simulation().await {
error!("Simulation error: {}", e);
process::exit(1);
}
} else {
// Create Renderer and load fonts (only for graphics mode)
let mut renderer = Renderer::new();
renderer.load_title_font().await; // Load title font
renderer.load_ui_font().await; // Load UI font
renderer.init_glow_resources();
renderer.init_scanner_material();

// Run the game loop with graphics
if let Err(e) = game.run(&mut renderer).await {
error!("Game loop error: {}", e);
process::exit(1);
}
}

info!("Bot Arena finished.");
Expand Down
18 changes: 18 additions & 0 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,24 @@ void main() {
instr_params.clone(),
);

// --- Debug Register Value ---
let dbg_val = robot.vm_state.registers.get(Register::Dbg).unwrap_or(0.0);
let dbg_val_y = instr_val_y + row_v_spacing + 12.0; // Position below instruction

// Define params for debug value text (slightly different color)
let dbg_params = TextParams {
font_size: 11, // Even smaller font
color: Color::from_rgba(180, 180, 180, 255), // Slightly dimmed
..small_white_params // Inherit font
};
let dbg_text = format!("@dbg: {:.1}", dbg_val);
draw_text_ex(
&dbg_text,
panel_x + card_inner_padding_x,
dbg_val_y,
dbg_params,
);

// Update main y for next card
y += card_height + card_spacing;
}
Expand Down
Loading