use rand::prelude::*; use std::collections::HashMap; use std::fs; use std::fs::File; use std::io::Write; use std::path::PathBuf; pub mod TickFlag { pub const COLLIDED: usize = 1; pub const GOAL_REACHED: usize = 2; } pub type TickResult = usize; pub static MAX_ACCELERATION: isize = 127; pub static DRAG_FRACTION: (isize, isize) = (9, 10); pub static COLLISION_FRACTION: (isize, isize) = (1, 2); pub static MAX_COLLISION_RESOLUTIONS: usize = 5; pub static CELL_SIZE: isize = 10_000; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Racer { pub x: isize, pub y: isize, pub vx: isize, pub vy: isize, pub radius: isize, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Asteroid { pub x: isize, pub y: isize, pub radius: isize, } pub type Goal = Asteroid; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub(crate) struct Instruction { pub vx: isize, pub vy: isize, } impl Instruction { fn valid(vx: isize, vy: isize) -> bool { distance_squared(vx, vy, 0, 0) <= (MAX_ACCELERATION).pow(2) } pub fn new(vx: isize, vy: isize) -> Self { if !Self::valid(vx, vy) { // use float to properly normalize here let float_distance = ((vx as f64).powf(2.) + (vy as f64).powf(2.)).powf(1. / 2.); let mut vx = ((vx as f64 / float_distance) * MAX_ACCELERATION as f64) as isize; let mut vy = ((vy as f64 / float_distance) * MAX_ACCELERATION as f64) as isize; // if we're still over, decrement both values if !Self::valid(vx, vy) { vx -= vx.signum(); vy -= vy.signum(); } return Self { vx, vy }; } assert!(Self::valid(vx, vy)); Self { vx, vy, } } pub fn random() -> Self { let mut rng = rand::rng(); Self { vx: rng.random::<i64>() as isize, vy: rng.random::<i64>() as isize, } } pub fn load(path: &PathBuf) -> Vec<Instruction> { let contents = fs::read_to_string(path).expect("Failed reading a file!"); let mut lines = contents.lines(); let instruction_count = lines.next().unwrap().parse::<usize>().unwrap(); let mut instructions = vec![]; for _ in 0..instruction_count { let parts = lines .next() .expect("No more lines!") .split_whitespace() .collect::<Vec<&str>>(); instructions.push(Instruction { vx: parts[0].parse::<isize>().unwrap(), vy: parts[1].parse::<isize>().unwrap(), }) } instructions } pub fn save(path: &PathBuf, instructions: &Vec<Instruction>) { let mut file = File::create(path).expect("Failed creating a file!"); for instruction in instructions { file.write_all(format!("{} {}\n", instruction.vx, instruction.vy).as_bytes()) .expect("Failed writing to file!"); } } } #[derive(Debug, Clone, Copy)] pub struct BoundingBox { pub min_x: isize, pub min_y: isize, pub max_x: isize, pub max_y: isize, } impl BoundingBox { pub fn width(&self) -> isize { self.max_x - self.min_x } pub fn height(&self) -> isize { self.max_y - self.min_y } } /// Squared Euclidean distance; useful for distance checks. fn distance_squared(x1: isize, y1: isize, x2: isize, y2: isize) -> isize { (x1 - x2).pow(2) + (y1 - y2).pow(2) } /// Plain-old integer Euclidean distance. /// /// Note: this implementation might break for larger position values, but since /// the maps are never going to be this large, I'm not fixing it now. pub fn euclidean_distance(x1: isize, y1: isize, x2: isize, y2: isize) -> isize { (distance_squared(x1, y1, x2, y2) as f64).sqrt() as isize } #[derive(Debug, Clone)] pub struct Simulation { pub initial_racer: Racer, pub racer: Racer, pub asteroids: Vec<Asteroid>, pub goals: Vec<Goal>, pub bbox: BoundingBox, pub reached_goals: Vec<bool>, _grid: HashMap<(isize, isize), Vec<Asteroid>>, _cell_size: isize, } /// /// # Examples /// ``` /// let map_path = PathBuf::from("../../maps/test.txt"); /// /// let mut simulation = Simulation::load(&map_path); /// /// let mut tick_result: TickResult = 0; /// /// println!("Running simulation until collision..."); /// /// while tick_result & TickFlag::COLLIDED == 0 { /// tick_result = simulation.tick(Instruction::new(0, MAX_ACCELERATION)); /// /// println!("{:?}", simulation.racer); /// } /// /// println!("Bam!"); /// ``` /// impl Simulation { pub fn new(racer: Racer, asteroids: Vec<Asteroid>, goals: Vec<Goal>, bbox: BoundingBox) -> Self { let reached_goals = vec![false; goals.len()]; let mut simulation = Self { initial_racer: racer, racer, asteroids, goals, bbox, reached_goals, _grid: HashMap::new(), _cell_size: CELL_SIZE, }; for &asteroid in &simulation.asteroids { let (min_x, min_y) = simulation.coordinate_to_grid( asteroid.x - asteroid.radius - racer.radius, asteroid.y - asteroid.radius - racer.radius, ); let (max_x, max_y) = simulation.coordinate_to_grid( asteroid.x + asteroid.radius + racer.radius, asteroid.y + asteroid.radius + racer.radius, ); for grid_x in min_x..=max_x { for grid_y in min_y..=max_y { simulation ._grid .entry((grid_x, grid_y)) .or_insert(vec![]) .push(asteroid); } } } simulation } fn coordinate_to_grid(&self, x: isize, y: isize) -> (isize, isize) { (x / self._cell_size, y / self._cell_size) } fn move_racer(&mut self, instruction: Instruction) { self.racer.vx = (self.racer.vx * DRAG_FRACTION.0) / DRAG_FRACTION.1; self.racer.vy = (self.racer.vy * DRAG_FRACTION.0) / DRAG_FRACTION.1; self.racer.vx += instruction.vx; self.racer.vy += instruction.vy; self.racer.x += self.racer.vx as isize; self.racer.y += self.racer.vy as isize; } fn push_from_asteroids(&mut self) -> bool { let grid_coordinate = self.coordinate_to_grid(self.racer.x, self.racer.y); match self._grid.get(&grid_coordinate) { None => false, Some(asteroids) => { for asteroid in asteroids { // not colliding, nothing to be done if euclidean_distance(self.racer.x, self.racer.y, asteroid.x, asteroid.y) > self.racer.radius + asteroid.radius { continue; } // the vector to push the racer out by let nx = self.racer.x - asteroid.x; let ny = self.racer.y - asteroid.y; // how much to push by let distance = euclidean_distance(self.racer.x, self.racer.y, asteroid.x, asteroid.y); let push_by = distance - (self.racer.radius + asteroid.radius); // the actual push self.racer.x -= (nx * push_by) / distance; self.racer.y -= (ny * push_by) / distance; return true; } false } } } fn push_from_bounding_box(&mut self) -> bool { // not pretty but easy to read :) let mut collided = false; if self.racer.x - self.racer.radius < self.bbox.min_x { self.racer.x = self.bbox.min_x + self.racer.radius; collided = true; } if self.racer.x + self.racer.radius > self.bbox.max_x { self.racer.x = self.bbox.max_x - self.racer.radius; collided = true; } if self.racer.y - self.racer.radius < self.bbox.min_y { self.racer.y = self.bbox.min_y + self.racer.radius; collided = true; } if self.racer.y + self.racer.radius > self.bbox.max_y { self.racer.y = self.bbox.max_y - self.racer.radius; collided = true; } collided } fn check_goal(&mut self) -> bool { let mut new_goal_reached = false; for (i, goal) in self.goals.iter().enumerate() { if euclidean_distance(self.racer.x, self.racer.y, goal.x, goal.y) <= (self.racer.radius + goal.radius) { if !&self.reached_goals[i] { new_goal_reached = true; } self.reached_goals[i] = true; } } new_goal_reached } fn resolve_collisions(&mut self) -> bool { let mut collided = false; for _ in 0..MAX_COLLISION_RESOLUTIONS { let mut collided_this_iteration = false; if self.push_from_asteroids() { collided_this_iteration = true; collided = true; } if self.push_from_bounding_box() { collided_this_iteration = true; collided = true; } if !collided_this_iteration { break; } } if collided { self.racer.vx = (self.racer.vx * COLLISION_FRACTION.0) / COLLISION_FRACTION.1; self.racer.vy = (self.racer.vy * COLLISION_FRACTION.0) / COLLISION_FRACTION.1; } collided } pub fn finished(&self) -> bool { self.reached_goals.iter().all(|v| *v) } pub fn restart(&mut self) { self.racer.x = self.initial_racer.x; self.racer.y = self.initial_racer.y; self.racer.vx = 0; self.racer.vy = 0; self.reached_goals.fill(false); } pub fn tick(&mut self, instruction: Instruction) -> TickResult { self.move_racer(instruction); let collided = self.resolve_collisions(); let goal = self.check_goal(); let mut result: TickResult = 0; if collided { result |= TickFlag::COLLIDED; } if goal { result |= TickFlag::GOAL_REACHED; } result } pub fn simulate(&mut self, instructions: &Vec<Instruction>) -> Vec<TickResult> { self.restart(); let mut results = vec![]; for instruction in instructions { results.push(self.tick(*instruction)); } results } pub fn load(path: &PathBuf) -> Self { let binding = fs::read_to_string(path).unwrap(); let mut lines = binding.lines(); let mut parts_fn = || { lines .next() .unwrap() .split_whitespace() .collect::<Vec<&str>>() }; let racer_parts = parts_fn(); let racer = Racer { x: racer_parts[0].parse::<isize>().unwrap(), y: racer_parts[1].parse::<isize>().unwrap(), radius: racer_parts[2].parse::<isize>().unwrap(), vx: 0, vy: 0, }; let bb_parts = parts_fn(); let bbox = BoundingBox { min_x: bb_parts[0].parse::<isize>().unwrap(), min_y: bb_parts[1].parse::<isize>().unwrap(), max_x: bb_parts[2].parse::<isize>().unwrap(), max_y: bb_parts[3].parse::<isize>().unwrap(), }; let asteroid_count = parts_fn()[0].parse::<usize>().unwrap(); let mut asteroids = vec![]; for _ in 0..asteroid_count { let asteroid_parts = parts_fn(); asteroids.push(Asteroid { x: asteroid_parts[0].parse::<isize>().unwrap(), y: asteroid_parts[1].parse::<isize>().unwrap(), radius: asteroid_parts[2].parse::<isize>().unwrap(), }); } let goal_count = parts_fn()[0].parse::<usize>().unwrap(); let mut goals = vec![]; for _ in 0..goal_count { let goal_parts = parts_fn(); goals.push(Asteroid { x: goal_parts[0].parse::<isize>().unwrap(), y: goal_parts[1].parse::<isize>().unwrap(), radius: goal_parts[2].parse::<isize>().unwrap(), }); } Self::new(racer, asteroids, goals, bbox) } }