diff --git a/src/assets/mod.rs b/src/assets/mod.rs index 6a1e49e..48fa519 100644 --- a/src/assets/mod.rs +++ b/src/assets/mod.rs @@ -3,3 +3,6 @@ mod gamemap; pub use atlas::{Atlas, Frame}; pub use gamemap::{GameMap, Object, Tile}; + +#[cfg(test)] +pub use gamemap::{Behaviour, BehaviourType, Mob}; diff --git a/src/world/behaviour.rs b/src/world/behaviour.rs index d1644c8..fbe5c20 100644 --- a/src/world/behaviour.rs +++ b/src/world/behaviour.rs @@ -76,3 +76,140 @@ pub fn make_step(curr_state: &mut State, input_state: &InputSnapshot) { mob.y += vec_move.1; } } + +#[cfg(test)] +mod tests { + use super::State; + use crate::assets::GameMap; + + use super::*; + + #[test] + fn test_abs_vector_zero() { + assert_eq!(abs_vector((0.0, 0.0)), 0.0); + } + + #[test] + fn test_abs_vector_nonzero() { + let len = abs_vector((3.0, 4.0)); + assert!((len - 5.0).abs() < 1e-5); + } + + #[test] + fn test_normalize_vector_basic() { + let n = normalize_vector((3.0, 4.0)); + assert!(((n.0 * n.0 + n.1 * n.1).sqrt() - 1.0).abs() < 1e-5); + } + + #[test] + fn test_normalize_vector_small_vector_returns_zero() { + let n = normalize_vector((0.01, 0.01)); + assert_eq!(n, (0.0, 0.0)); + } + + fn make_test_state() -> State { + let game_map = GameMap::load("input.json").expect("failed to load game map for tests"); + + let mut state = State::new(&game_map); + + state.player.x = 0.0; + state.player.y = 0.0; + state.player.x_speed = 0.0; + state.player.y_speed = 0.0; + + if state.mobs.is_empty() { + state.mobs.push(crate::world::Unit { + x: 100.0, + y: 0.0, + x_speed: -0.5, + y_speed: 0.0, + ..Default::default() + }); + } + + state + } + + #[test] + fn test_player_moves_right() { + let mut state = make_test_state(); + + let input = crate::input::InputSnapshot { + up: false, + down: false, + left: false, + right: true, + escape: false, + }; + + make_step(&mut state, &input); + + assert!((state.player.x - 0.75).abs() < 1e-5); + assert!((state.player.y - 0.0).abs() < 1e-5); + } + + #[test] + fn test_player_moves_up_left_diagonal() { + let mut state = make_test_state(); + + let input = crate::input::InputSnapshot { + up: true, + down: false, + left: true, + right: false, + escape: false, + }; + + make_step(&mut state, &input); + + let dx = state.player.x; + let dy = state.player.y; + let len = (dx * dx + dy * dy).sqrt(); + assert!((len - 0.75).abs() < 1e-5); + assert!(dx < 0.0 && dy < 0.0); + } + + #[test] + fn test_mob_moves_toward_player() { + let mut state = make_test_state(); + state.mobs[0].x = 50.0; + state.mobs[0].y = 0.0; + state.mobs[0].x_speed = -0.5; + state.mobs[0].y_speed = 0.0; + + let input = crate::input::InputSnapshot { + up: false, + down: false, + left: false, + right: false, + escape: false, + }; + + make_step(&mut state, &input); + + assert!(state.mobs[0].x < 50.0); + assert!(state.mobs[0].y.abs() < 1e-3); + } + + #[test] + fn test_collision_pushes_mob_back() { + let mut state = make_test_state(); + + state.mobs[0].x = 2.0; + state.mobs[0].y = 0.0; + + let input = crate::input::InputSnapshot { + up: false, + down: false, + left: false, + right: false, + escape: false, + }; + + make_step(&mut state, &input); + + let vec_from = (state.mobs[0].x - state.player.x, state.mobs[0].y - state.player.y); + let dist = (vec_from.0 * vec_from.0 + vec_from.1 * vec_from.1).sqrt(); + assert!((dist - 10.0).abs() < 1e-3); + } +} diff --git a/src/world/initiator.rs b/src/world/initiator.rs index 639667b..4fb0274 100644 --- a/src/world/initiator.rs +++ b/src/world/initiator.rs @@ -24,3 +24,109 @@ pub fn get_visible_objects(cur_state: &State, camera: &Camera) -> Vec { units.into_iter().filter(|mob| camera.is_visible(mob.x, mob.y)).collect() } + +#[cfg(test)] +mod visible_objects_tests { + use super::*; + + #[derive(Clone)] + struct DummyUnit { + x: f32, + y: f32, + x_speed: f32, + y_speed: f32, + } + + #[derive(Clone)] + struct DummyState { + player: DummyUnit, + mobs: Vec, + } + + impl DummyState { + fn to_real_state(&self) -> State { + State { + player: Unit { + x: self.player.x, + y: self.player.y, + x_speed: self.player.x_speed, + y_speed: self.player.y_speed, + }, + mobs: self + .mobs + .iter() + .map(|m| Unit { x: m.x, y: m.y, x_speed: m.x_speed, y_speed: m.y_speed }) + .collect(), + } + } + } + + #[test] + fn test_get_visible_objects_player_included() { + let dummy_state = DummyState { + player: DummyUnit { x: 0.0, y: 0.0, x_speed: 0.0, y_speed: 0.0 }, + mobs: vec![], + }; + let state = dummy_state.to_real_state(); + + let camera = Camera::new(0.0, 0.0, 800, 600); + let visible = get_visible_objects(&state, &camera); + + assert_eq!(visible.len(), 1); + assert_eq!(visible[0].x, state.player.x); + assert_eq!(visible[0].y, state.player.y); + } + + #[test] + fn test_get_visible_objects_mobs_visible() { + let dummy_state = DummyState { + player: DummyUnit { x: 0.0, y: 0.0, x_speed: 0.0, y_speed: 0.0 }, + mobs: vec![ + DummyUnit { x: 10.0, y: 10.0, x_speed: 0.0, y_speed: 0.0 }, + DummyUnit { x: 1000.0, y: 1000.0, x_speed: 0.0, y_speed: 0.0 }, + ], + }; + let state = dummy_state.to_real_state(); + let camera = Camera::new(0.0, 0.0, 50, 50); + + let visible = get_visible_objects(&state, &camera); + assert_eq!(visible.len(), 2); + assert_eq!(visible[1].x, 10.0); + assert_eq!(visible[1].y, 10.0); + } + + #[test] + fn test_get_visible_objects_mobs_outside_not_included() { + let dummy_state = DummyState { + player: DummyUnit { x: 0.0, y: 0.0, x_speed: 0.0, y_speed: 0.0 }, + mobs: vec![DummyUnit { x: 100.0, y: 100.0, x_speed: 0.0, y_speed: 0.0 }], + }; + let state = dummy_state.to_real_state(); + let camera = Camera::new(0.0, 0.0, 50, 50); + + let visible = get_visible_objects(&state, &camera); + assert_eq!(visible.len(), 1); + assert_eq!(visible[0].x, state.player.x); + } + + #[test] + fn test_get_visible_objects_multiple_mobs() { + let dummy_state = DummyState { + player: DummyUnit { x: 0.0, y: 0.0, x_speed: 0.0, y_speed: 0.0 }, + mobs: vec![ + DummyUnit { x: 5.0, y: 5.0, x_speed: 0.0, y_speed: 0.0 }, + DummyUnit { x: 20.0, y: 20.0, x_speed: 0.0, y_speed: 0.0 }, + DummyUnit { x: 100.0, y: 100.0, x_speed: 0.0, y_speed: 0.0 }, + ], + }; + let state = dummy_state.to_real_state(); + let camera = Camera::new(0.0, 0.0, 50, 50); + + let visible = get_visible_objects(&state, &camera); + assert_eq!(visible.len(), 3); + let positions: Vec<_> = visible.iter().map(|u| (u.x, u.y)).collect(); + assert!(positions.contains(&(0.0, 0.0))); + assert!(positions.contains(&(5.0, 5.0))); + assert!(positions.contains(&(20.0, 20.0))); + } +} diff --git a/src/world/state.rs b/src/world/state.rs index 3c9a890..62d9b7e 100644 --- a/src/world/state.rs +++ b/src/world/state.rs @@ -4,7 +4,7 @@ use crate::assets::GameMap; /// /// The `State` struct manages the player unit and all mob units in the game, /// tracking their positions and movement speeds for game simulation. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct State { /// The player-controlled unit pub player: Unit, @@ -16,7 +16,7 @@ pub struct State { /// /// Units can be either player-controlled or game-controlled mobs. Each unit has /// a position in 2D space and speed components for movement simulation. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Unit { /// X-coordinate position in the game world pub x: f32, @@ -120,3 +120,174 @@ impl State { Self { player: player.unwrap(), mobs } } } + +#[cfg(test)] +mod state_tests { + use crate::assets::{Behaviour, BehaviourType, GameMap, Mob}; + use crate::world::State; + + fn make_test_map() -> GameMap { + let mut mobs = std::collections::HashMap::new(); + + mobs.insert( + "player".to_string(), + Mob { + name: "player".to_string(), + x_start: 0, + y_start: 0, + asset: "knight".to_string(), + is_player: true, + behaviour: None, + }, + ); + + mobs.insert( + "mob_right".to_string(), + Mob { + name: "mob_right".to_string(), + x_start: 10, + y_start: 0, + asset: "imp".to_string(), + is_player: false, + behaviour: Some(Behaviour { + behaviour_type: BehaviourType::Walker, + direction: Some("right".to_string()), + speed: Some(1.0), + }), + }, + ); + + mobs.insert( + "mob_up".to_string(), + Mob { + name: "mob_up".to_string(), + x_start: 0, + y_start: 10, + asset: "ghost".to_string(), + is_player: false, + behaviour: Some(Behaviour { + behaviour_type: BehaviourType::Walker, + direction: Some("up".to_string()), + speed: Some(0.5), + }), + }, + ); + + GameMap { + name: "test_map".to_string(), + tile_size: 16, + size: [5, 5], + mobs, + objects: std::collections::HashMap::new(), + tiles: std::collections::HashMap::new(), + } + } + + #[test] + fn test_state_new_creates_player_and_mobs() { + let map = make_test_map(); + let state = State::new(&map); + + assert_eq!(state.player.x, 0.0); + assert_eq!(state.player.y, 0.0); + assert_eq!(state.player.x_speed, 10.0); + assert_eq!(state.player.y_speed, 10.0); + + assert_eq!(state.mobs.len(), 2); + + let mob_right = state.mobs.iter().find(|m| m.x_speed > 0.0).unwrap(); + assert_eq!(mob_right.x_speed, 1.0); + assert_eq!(mob_right.y_speed, 0.0); + assert_eq!(mob_right.x, 10.0); + assert_eq!(mob_right.y, 0.0); + + let mob_up = state.mobs.iter().find(|m| m.y_speed < 0.0).unwrap(); + assert_eq!(mob_up.x_speed, 0.0); + assert_eq!(mob_up.y_speed, -0.5); + assert_eq!(mob_up.x, 0.0); + assert_eq!(mob_up.y, 10.0); + } + + #[test] + fn test_state_with_no_mobs_other_than_player() { + let mut map = make_test_map(); + map.mobs.retain(|_, mob| mob.is_player); + let state = State::new(&map); + + assert_eq!(state.player.x, 0.0); + assert_eq!(state.player.y, 0.0); + assert!(state.mobs.is_empty()); + } + + #[test] + fn test_mob_with_unknown_or_none_behaviour_defaults_to_zero_speed() { + let mut mobs = std::collections::HashMap::new(); + mobs.insert( + "player".to_string(), + Mob { + name: "player".to_string(), + x_start: 0, + y_start: 0, + asset: "knight".to_string(), + is_player: true, + behaviour: None, + }, + ); + mobs.insert( + "mob_none".to_string(), + Mob { + name: "mob_none".to_string(), + x_start: 5, + y_start: 5, + asset: "dummy".to_string(), + is_player: false, + behaviour: None, + }, + ); + mobs.insert( + "mob_unknown".to_string(), + Mob { + name: "mob_unknown".to_string(), + x_start: 10, + y_start: 10, + asset: "dummy".to_string(), + is_player: false, + behaviour: Some(Behaviour { + behaviour_type: BehaviourType::Unknown, + direction: Some("left".to_string()), + speed: Some(2.0), + }), + }, + ); + + let map = GameMap { + name: "test_map".to_string(), + tile_size: 16, + size: [5, 5], + mobs, + objects: std::collections::HashMap::new(), + tiles: std::collections::HashMap::new(), + }; + + let state = State::new(&map); + assert_eq!(state.mobs.len(), 2); + + let mob_none = state.mobs.iter().find(|m| m.x == 5.0).unwrap(); + assert_eq!(mob_none.x_speed, 0.0); + assert_eq!(mob_none.y_speed, 0.0); + + let mob_unknown = state.mobs.iter().find(|m| m.x == 10.0).unwrap(); + assert_eq!(mob_unknown.x_speed, -2.0); + assert_eq!(mob_unknown.y_speed, 0.0); + } + + #[test] + fn test_player_position_does_not_change_from_map() { + let map = make_test_map(); + let state = State::new(&map); + + let player_map = map.get_mob("player").unwrap(); + assert_eq!(state.player.x, player_map.x_start as f32); + assert_eq!(state.player.y, player_map.y_start as f32); + } +}