449 lines
12 KiB
Rust
449 lines
12 KiB
Rust
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)
|
|
}
|
|
}
|