add unit tests
This commit is contained in:
parent
bf4b32236e
commit
5571266cfe
331
src/engine.rs
331
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<Tile>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
13
src/main.rs
13
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue