From 5571266cfef7166df376eabdb5c8da21405dcf7b Mon Sep 17 00:00:00 2001 From: Fabien Freling Date: Fri, 3 Apr 2020 17:12:33 +0200 Subject: [PATCH] add unit tests --- Makefile | 7 -- src/engine.rs | 331 ++++++++++++++++++++++++++++++++++---------------- src/main.rs | 13 +- 3 files changed, 235 insertions(+), 116 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 4905d27..0000000 --- a/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -all: run - -build: - cargo build - -run: - cargo run diff --git a/src/engine.rs b/src/engine.rs index c606037..77e2732 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -4,7 +4,7 @@ use piston_window::*; use std::f64::consts::*; #[derive(Copy, Clone, PartialEq, Debug)] -struct Position { +pub struct Position { x: f64, y: f64, } @@ -14,9 +14,9 @@ impl Position { ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt() } - pub fn distance_sqr(&self, other: Position) -> f64 { - (self.x - other.x).powi(2) + (self.y - other.y.powi(2)) - } + // pub fn distance_sqr(&self, other: Position) -> f64 { + // (self.x - other.x).powi(2) + (self.y - other.y.powi(2)) + // } } type Degree = f64; @@ -28,7 +28,17 @@ struct Player { angle: Degree, } -#[derive(Copy, Clone, PartialEq)] +impl Player { + pub fn player_space_distance(&self, other: Position) ->f64 { + let rad = self.angle.to_radians(); + let x = (other.x - self.pos.x) * rad.cos(); + let y = (other.y - self.pos.y) * rad.sin(); + x + y + } +} + + +#[derive(Debug, Copy, Clone, PartialEq)] pub enum Tile { Empty, Wall, @@ -40,6 +50,17 @@ pub struct Level { pub tiles: Vec, } +impl Level { + pub fn contains(&self, pos: Position) -> bool { + 0.0 <= pos.x && pos.x <= self.width as f64 && 0.0 <= pos.y && pos.y <= self.height as f64 + } + + fn tile_at(&self, pos: Position) -> Tile { + assert!(self.contains(pos)); + self.tiles[(pos.x.trunc() as usize) + (pos.y.trunc() as usize) * self.width] + } +} + #[derive(Copy, Clone, PartialEq, Debug)] pub enum Movement { Forward, @@ -48,12 +69,81 @@ pub enum Movement { TurnRight, } -impl Level { - pub fn contains(&self, pos: Position) -> bool { - 0.0 <= pos.x && pos.x <= self.width as f64 && 0.0 <= pos.y && pos.y <= self.height as f64 +fn step_for_angle(angle: Radian) -> (f64, f64) { + match angle { + x if x == 0.0 => (0.0, std::f64::INFINITY), + x if x == PI * 0.5 => (std::f64::INFINITY, 0.0), + x if x == PI => (0.0, std::f64::NEG_INFINITY), + x if x == PI * 1.5 => (std::f64::NEG_INFINITY, 0.0), + x if (0.0..(PI * 0.5)).contains(&x) => (angle.tan(), ((PI * 0.5) - angle).tan()), + x if ((PI * 0.5)..PI).contains(&x) => (-x.tan(), -((x - (PI * 0.5)).tan())), + x if (PI..(PI * 1.5)).contains(&x) => (-x.tan(), -(((PI * 1.5) - x).tan())), + x if ((PI * 1.5)..(PI * 2.0)).contains(&x) => (x.tan(), ((x - (PI * 1.5)).tan())), + _ => panic!("Invalid angle value {}.", angle), } } +fn closest_point(level: &Level, pos: &Position, angle: Radian) -> (Tile, Position) { + assert!((0.0..(PI * 2.0)).contains(&angle)); + + let (y_step, x_step) = step_for_angle(angle); + // println!("step: ({}, {})", x_step, y_step); + + let (x_remain, y_remain) = match (x_step, y_step) { + (x, y) if x >= 0.0 && y >= 0.0 => (1.0 - pos.x.fract(), 1.0 - pos.y.fract()), + (x, y) if x <= 0.0 && y >= 0.0 => (-pos.x.fract(), 1.0 - pos.y.fract()), + (x, y) if x >= 0.0 && y <= 0.0 => (1.0 - pos.x.fract(), -pos.y.fract()), + (x, y) if x <= 0.0 && y <= 0.0 => (-pos.x.fract(), -pos.y.fract()), + _ => panic!("Invalid steps"), + }; + + let x_dist_factor = x_remain / x_step; + let y_dist_factor = y_remain / y_step; + + let mut x_candidate = Position { + x: pos.x + x_remain, // x_remain = x_step * x_dist_factor + y: pos.y + y_step * x_dist_factor, + }; + let mut y_candidate = Position { + x: pos.x + x_step * y_dist_factor, + y: pos.y + y_remain, // y_remain = y_step * y_dist_factor + }; + + let mut next_point: Position = *pos; + let mut tile = Tile::Empty; + + while tile == Tile::Empty && level.contains(next_point) { + if next_point.distance(x_candidate) < next_point.distance(y_candidate) { + next_point = x_candidate; + x_candidate = Position { + x: x_candidate.x + x_step.signum(), + y: x_candidate.y + y_step, + }; + } else { + next_point = y_candidate; + y_candidate = Position { + x: y_candidate.x + x_step, + y: y_candidate.y + y_step.signum(), + }; + } + // println!("next candidate: {:?}", next_point); + + tile = if next_point.x.fract() == 0.0 { + let mut position = next_point; + position.x += 0.5 * x_step.signum(); + level.tile_at(position) + } else { + let mut position = next_point; + position.y += 0.5 * y_step.signum(); + level.tile_at(position) + }; + } + + assert!(tile != Tile::Empty); + + (tile, next_point) +} + pub struct Engine { w: f64, h: f64, @@ -70,8 +160,8 @@ impl Engine { h: size.height as f64, horiz_fov: 90., player: Player { - pos: Position { x: 2., y: 2. }, - angle: 90., + pos: Position { x: 1.5, y: 2. }, + angle: 0., }, level: Level { width: 0, @@ -82,83 +172,7 @@ impl Engine { } } - fn closest_point(level: &Level, pos: &Position, angle: Radian) -> (Tile, Position) { - assert!((0.0..(PI * 2.0)).contains(&angle)); - - let (y_step, x_step) = match angle { - x if x == 0.0 => (0.0, std::f64::INFINITY), - x if x == PI * 0.5 => (std::f64::INFINITY, 0.0), - x if x == PI => (0.0, std::f64::NEG_INFINITY), - x if x == PI * 1.5 => (std::f64::NEG_INFINITY, 0.0), - x if (0.0..(PI * 0.5)).contains(&x) => (angle.tan(), ((PI / 2.0) - angle).tan()), - x if ((PI * 0.5)..PI).contains(&x) => ((PI - x).tan(), -((x - (PI / 2.0)).tan())), - x if (PI..(PI * 1.5)).contains(&x) => (-((x - PI).tan()), -(((PI * 1.5) - x).tan())), - x if ((PI * 1.5)..(PI * 2.0)).contains(&x) => (((PI * 2.0) - x).tan(), -((x - (PI * 1.5)).tan())), - _ => panic!("Invalid angle value {}.", angle), - }; - - let (x_remain, y_remain) = match (x_step, y_step) { - (x, y) if x >= 0.0 && y >= 0.0 => (1.0 - pos.x.fract(), 1.0 - pos.y.fract()), - (x, y) if x <= 0.0 && y >= 0.0 => (-pos.x.fract(), 1.0 - pos.y.fract()), - (x, y) if x >= 0.0 && y <= 0.0 => (1.0 - pos.x.fract(), -pos.y.fract()), - (x, y) if x <= 0.0 && y <= 0.0 => (-pos.x.fract(), -pos.y.fract()), - _ => panic!("Invalid steps"), - }; - - let x_dist_factor = x_remain / x_step; - let y_dist_factor = y_remain / y_step; - - let mut x_candidate = Position { - x: pos.x + x_remain, // x_remain = x_step * x_dist_factor - y: pos.y + y_step * x_dist_factor, - }; - let mut y_candidate = Position { - x: pos.x + x_step * y_dist_factor, - y: pos.y + y_remain, // y_remain = y_step * y_dist_factor - }; - - let mut next_point: Position = *pos; - let mut tile = Tile::Empty; - - while tile == Tile::Empty && level.contains(next_point) { - if next_point.distance(x_candidate) < next_point.distance(y_candidate) { - next_point = x_candidate; - x_candidate = Position { - x: x_candidate.x + x_step.signum(), - y: x_candidate.y + y_step, - }; - } else { - next_point = y_candidate; - y_candidate = Position { - x: y_candidate.x + x_step, - y: y_candidate.y + y_step.signum(), - }; - } - - tile = if next_point.x.fract() == 0.0 { - let x_index = (next_point.x.trunc() + x_step.signum()) as usize; - assert!(x_index < level.width); - let y_index = next_point.y.trunc() as usize; - assert!(y_index < level.height); - let index: usize = x_index + y_index * level.width; - level.tiles[index] - } else { - let x_index = next_point.x.trunc() as usize; - assert!(x_index < level.width); - let y_index = (next_point.y.trunc() + y_step.signum()) as usize; - assert!(y_index < level.height); - let index: usize = x_index + y_index * level.width; - level.tiles[index] - }; - } - - assert!(tile != Tile::Empty); - - (tile, next_point) - } - pub fn render(&mut self, context: Context, graphics: &mut G2d) { - clear([1.0; 4], graphics); // Ceiling @@ -176,30 +190,29 @@ impl Engine { graphics); let left = self.player.angle + (self.horiz_fov / 2.0); - let right = self.player.angle - (self.horiz_fov / 2.0); let step = self.horiz_fov / self.w; - let mut ray_angle = left; let width = self.w as i32; for n in 0..width { let ray_angle = ((left - (n as f64) * step) + 360.0) % 360.0; let ray_radian = ray_angle.to_radians(); - //println!("degree: {} -> radian: {}", ray_angle, ray_radian); - let (tile, pos) = Engine::closest_point(&self.level, &self.player.pos, ray_radian); - let distance = self.player.pos.distance(pos); - let player_space_distance = (self.player.pos.x - pos.x).abs() * self.player.angle.to_radians().cos() - - (self.player.pos.y - pos.y).abs() * self.player.angle.to_radians().sin(); + let (tile, pos) = closest_point(&self.level, &self.player.pos, ray_radian); + let distance = self.player.player_space_distance(pos); + //assert!(distance.is_sign_positive()); if tile == Tile::Wall { - //println!("ray: {}, wall at {:?}, distance: {}", n, pos, distance); let wall_height = (self.h / (distance * 3.0)).min(self.h); - let wall_color = [0.2, 0.2, 0.9, 1.0]; - //println!("wall height: {}", wall_height); + let wall_color = match pos { + p if p.x.fract() == 0.0 => [0.2, 0.2, 0.9, 1.0], + _ => [0.2, 0.9, 0.2, 1.0], + }; + println!("ray: {}, angle: {}, wall at {:?}, distance: {}", n, ray_angle, pos, distance); rectangle(wall_color, - [n as f64, (self.h - wall_height) / 2.0, (n + 1) as f64, wall_height], + [n as f64, (self.h - wall_height) / 2.0, 1.0, wall_height], context.transform, graphics); - }; } + + //std::process::exit(0); } pub fn load_level(&mut self, level: Level) { @@ -212,6 +225,7 @@ impl Engine { pub fn update(&mut self, dt: f64) { for input in &self.inputs { + let previous = self.player.pos; match input { Movement::Forward => { self.player.pos.x += self.player.angle.to_radians().cos() * dt; @@ -230,6 +244,10 @@ impl Engine { self.player.angle = (self.player.angle + 360.0) % 360.0; } } + if !self.level.contains(self.player.pos) || self.level.tile_at(self.player.pos) == Tile::Wall { + println!("Invalid position {:?}, tile = {:?}", self.player.pos, self.level.tile_at(self.player.pos)); + self.player.pos = previous; + } } self.inputs.clear(); @@ -241,11 +259,120 @@ impl Engine { mod tests { use super::*; + fn fcmp(a: f64, b: f64) { + let epsilon = 1e-5; + assert!((a - b).abs() < epsilon); + } + + #[test] + fn player_space_distance() { + let mut player = Player { + pos: super::Position { x: 2., y: 2. }, + angle: 0., + }; + + fcmp(player.player_space_distance(super::Position { x: 4., y: 1. }), 2.); + fcmp(player.player_space_distance(super::Position { x: 4., y: 2. }), 2.); + fcmp(player.player_space_distance(super::Position { x: 4., y: 3. }), 2.); + + player.angle = 90.; + fcmp(player.player_space_distance(super::Position { x: 1., y: 4. }), 2.); + fcmp(player.player_space_distance(super::Position { x: 2., y: 4. }), 2.); + fcmp(player.player_space_distance(super::Position { x: 3., y: 4. }), 2.); + + player.angle = 180.; + fcmp(player.player_space_distance(super::Position { x: 0., y: 1. }), 2.); + fcmp(player.player_space_distance(super::Position { x: 0., y: 2. }), 2.); + fcmp(player.player_space_distance(super::Position { x: 0., y: 3. }), 2.); + + player.angle = 270.; + fcmp(player.player_space_distance(super::Position { x: 1., y: 0. }), 2.); + fcmp(player.player_space_distance(super::Position { x: 2., y: 0. }), 2.); + fcmp(player.player_space_distance(super::Position { x: 3., y: 0. }), 2.); + } + + #[test] + fn tile_at() { + let tiles = vec![ + Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, + Tile::Wall, Tile::Empty, Tile::Empty, Tile::Empty, Tile::Wall, + Tile::Wall, Tile::Empty, Tile::Empty, Tile::Empty, Tile::Wall, + Tile::Wall, Tile::Empty, Tile::Empty, Tile::Empty, Tile::Wall, + Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, + ]; + let level = Level { + width: 5, + height: 5, + tiles + }; + assert_eq!(level.tile_at(super::Position { x: 2.0, y: 2.0 }), Tile::Empty); + } + + #[test] + fn step_for_angle() { + let step_cmp = |angle: Degree, y: f64, x: f64| { + let (step_y, step_x) = super::step_for_angle(angle.to_radians()); + println!("angle: {}, step: {}, {}", angle, step_y, step_x); + fcmp(step_y, y); + fcmp(step_x, x); + }; + + step_cmp(45.0, 1.0, 1.0); + step_cmp(90.0 + 45.0, 1.0, -1.0); + step_cmp(180.0 + 45.0, -1.0, -1.0); + step_cmp(270.0 + 45.0, -1.0, 1.0); + } + #[test] fn closest_point() { - let origin = super::Position { x: 2.2, y: 2.3 }; - let angle = 0.; - let closest = Engine::closest_point(origin, angle); - assert_eq!(closest, super::Position { x: 3.0, y: 2.3 }); + let tiles = vec![ + Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, + Tile::Wall, Tile::Empty, Tile::Empty, Tile::Empty, Tile::Wall, + Tile::Wall, Tile::Empty, Tile::Empty, Tile::Empty, Tile::Wall, + Tile::Wall, Tile::Empty, Tile::Empty, Tile::Empty, Tile::Wall, + Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, + ]; + let level = Level { + width: 5, + height: 5, + tiles + }; + + let position = super::Position { x: 2.5, y: 2.0 }; + { + let radian = 0.1; + let (tile, pos) = super::closest_point(&level, &position, radian); + + println!("pos: {:?}", pos); + assert_eq!(tile, Tile::Wall); + fcmp(pos.x, 4.0); + assert!(2.0 <= pos.y && pos.y <= 2.2); + } + { + let radian = 2.0 * PI - 0.1; + let (tile, pos) = super::closest_point(&level, &position, radian); + + println!("pos: {:?}", pos); + assert_eq!(tile, Tile::Wall); + fcmp(pos.x, 4.0); + assert!(1.8 <= pos.y && pos.y <= 2.0); + } + let fov = 90.0; + let step = fov / 40.0; + for n in 0..20 { + let left_ray_angle = (((n as f64) * step) + 360.0) % 360.0; + let left_ray_radian = left_ray_angle.to_radians(); + let (left_tile, left_pos) = super::closest_point(&level, &position, left_ray_radian); + + let right_ray_angle = ((-(n as f64) * step) + 360.0) % 360.0; + let right_ray_radian = right_ray_angle.to_radians(); + let (right_tile, right_pos) = super::closest_point(&level, &position, right_ray_radian); + + println!("left: {:?}, angle: {}", left_pos, left_ray_angle); + println!("right: {:?}, angle: {}", right_pos, right_ray_angle); + assert_eq!((left_ray_angle + right_ray_angle) % 360.0, 0.0); + fcmp(left_pos.x, right_pos.x); + fcmp(left_pos.y - 2.0, 2.0 - right_pos.y); + } } } diff --git a/src/main.rs b/src/main.rs index a2b00fb..0694c71 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use engine::Tile; fn main() { let mut window: PistonWindow = - WindowSettings::new("Rustenstein", [640, 480]) + WindowSettings::new("Rustenstein", [320, 240]) .exit_on_esc(true) .resizable(false) .build() @@ -22,7 +22,7 @@ fn main() { Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, Tile::Wall, ]; let level = engine::Level { - width:5, + width: 5, height: 5, tiles }; @@ -31,10 +31,10 @@ fn main() { while let Some(event) = window.next() { if let Some(Button::Keyboard(key)) = event.press_args() { match key { - Key::W => engine.add_movement(engine::Movement::Forward), - Key::S => engine.add_movement(engine::Movement::Backward), - Key::A => engine.add_movement(engine::Movement::TurnLeft), - Key::D => engine.add_movement(engine::Movement::TurnRight), + Key::W | Key::Up => engine.add_movement(engine::Movement::Forward), + Key::S | Key::Down => engine.add_movement(engine::Movement::Backward), + Key::A | Key::Left => engine.add_movement(engine::Movement::TurnLeft), + Key::D | Key::Right => engine.add_movement(engine::Movement::TurnRight), _ => (), }; }; @@ -46,6 +46,5 @@ fn main() { if let Some(args) = event.update_args() { engine.update(args.dt); } - } }