From 5ea7e4ec4e82e02495e64afa2fe136350a5f6b6b Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Sun, 7 Jun 2026 00:33:16 -0500 Subject: [PATCH] move tests --- kiln-core/src/archetype.rs | 11 ++ kiln-core/src/board.rs | 175 ++++++++++++++++++++++++ kiln-core/src/game.rs | 269 +++---------------------------------- kiln-core/src/utils.rs | 22 +++ 4 files changed, 229 insertions(+), 248 deletions(-) diff --git a/kiln-core/src/archetype.rs b/kiln-core/src/archetype.rs index 1938cb2..a3c3830 100644 --- a/kiln-core/src/archetype.rs +++ b/kiln-core/src/archetype.rs @@ -150,3 +150,14 @@ impl TryFrom<&str> for Archetype { } } } + +#[cfg(test)] +mod tests { + use super::Archetype; + + #[test] + fn directional_crate_default_glyph_tiles() { + assert_eq!(Archetype::HCrate.default_glyph().tile, 29); + assert_eq!(Archetype::VCrate.default_glyph().tile, 18); + } +} diff --git a/kiln-core/src/board.rs b/kiln-core/src/board.rs index 87127af..f8c0340 100644 --- a/kiln-core/src/board.rs +++ b/kiln-core/src/board.rs @@ -308,4 +308,179 @@ impl Board { self.objects.insert(id, object); id } +} + +#[cfg(test)] +pub(crate) mod tests { + use std::collections::{BTreeMap, HashMap}; + use color::Rgba8; + use crate::archetype::Archetype; + use crate::glyph::Glyph; + use crate::object_def::ObjectDef; + use crate::script::Direction; + use crate::utils::{ObjectId, Player, Solid}; + use super::Board; + + /// Builds an all-empty `w×h` board with the given player position, objects, + /// and `(name, source)` scripts. Assigns sequential ids (1..=n) to objects. + pub(crate) fn open_board( + w: usize, + h: usize, + player: (i32, i32), + objects: Vec, + scripts: &[(&str, &str)], + ) -> Board { + let mut object_map: BTreeMap = BTreeMap::new(); + let mut next_object_id: ObjectId = 1; + for o in objects { + object_map.insert(next_object_id, o); + next_object_id += 1; + } + Board { + name: "test".into(), + width: w, + height: h, + cells: vec![(Archetype::Empty.default_glyph(), Archetype::Empty); w * h], + floor: vec![Archetype::Empty.default_glyph(); w * h], + floor_spec: None, + player: Player { x: player.0, y: player.1 }, + objects: object_map, + next_object_id, + portals: Vec::new(), + font: None, + zoom: 1, + scripts: scripts.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect::>(), + board_script_name: None, + load_errors: Vec::new(), + } + } + + /// Stamps a crate cell onto a board. + pub(crate) fn crate_at(board: &mut Board, x: usize, y: usize) { + *board.get_mut(x, y) = (Archetype::Crate.default_glyph(), Archetype::Crate); + } + + /// Stamps a wall cell onto a board. + pub(crate) fn wall_at(board: &mut Board, x: usize, y: usize) { + *board.get_mut(x, y) = (Archetype::Wall.default_glyph(), Archetype::Wall); + } + + /// Stamps an arbitrary archetype cell onto a board. + pub(crate) fn stamp(board: &mut Board, x: usize, y: usize, arch: Archetype) { + *board.get_mut(x, y) = (arch.default_glyph(), arch); + } + + #[test] + fn solid_at_reports_wall_object_and_empty() { + let mut board = open_board(4, 1, (3, 0), vec![], &[]); + board.cells[1] = (Archetype::Wall.default_glyph(), Archetype::Wall); + board.add_object(ObjectDef::new(2, 0)); + + assert!(board.solid_at(0, 0).is_none()); + assert!(board.is_passable(0, 0)); + + match board.solid_at(1, 0) { + Some(Solid::Cell(Archetype::Wall)) => {} + other => panic!("expected Solid::Cell(Wall), got {:?}", other.is_some()), + } + assert!(!board.is_passable(1, 0)); + + match board.solid_at(2, 0) { + Some(Solid::Object(id)) => assert_eq!((board.objects[&id].x, board.objects[&id].y), (2, 0)), + _ => panic!("expected Solid::Object"), + } + assert!(!board.is_passable(2, 0)); + } + + #[test] + fn non_solid_object_does_not_block() { + let mut obj = ObjectDef::new(1, 0); + obj.solid = false; + let board = open_board(3, 1, (0, 0), vec![obj], &[]); + assert!(board.solid_at(1, 0).is_none()); + assert!(board.is_passable(1, 0)); + } + + #[test] + fn solid_at_reports_player() { + let board = open_board(3, 1, (1, 0), vec![], &[]); + match board.solid_at(1, 0) { + Some(Solid::Player) => {} + _ => panic!("expected Solid::Player at the player's cell"), + } + assert!(!board.is_passable(1, 0)); + } + + #[test] + fn in_bounds_checks_grid_boundaries() { + let board = open_board(3, 2, (0, 0), vec![], &[]); + assert!(board.in_bounds((0, 0))); + assert!(board.in_bounds((2, 1))); + assert!(!board.in_bounds((-1, 0))); + assert!(!board.in_bounds((0, -1))); + assert!(!board.in_bounds((3, 0))); + assert!(!board.in_bounds((0, 2))); + } + + #[test] + fn can_push_is_read_only_and_correct() { + let mut board = open_board(3, 1, (0, 0), vec![], &[]); + crate_at(&mut board, 1, 0); + assert!(board.can_push(1, 0, Direction::East)); + assert_eq!(board.get(1, 0).1, Archetype::Crate); // no mutation + assert_eq!(board.get(2, 0).1, Archetype::Empty); + + let mut board = open_board(3, 1, (0, 0), vec![], &[]); + crate_at(&mut board, 1, 0); + wall_at(&mut board, 2, 0); + assert!(!board.can_push(1, 0, Direction::East)); + } + + #[test] + fn push_into_player_pushes_player() { + // Crate shoved east into the player slides the player along into open space. + let mut board = open_board(4, 1, (2, 0), vec![], &[]); + crate_at(&mut board, 1, 0); + assert!(board.can_push(1, 0, Direction::East)); + board.push(1, 0, Direction::East); + assert_eq!(board.get(1, 0).1, Archetype::Empty); + assert_eq!(board.get(2, 0).1, Archetype::Crate); + assert_eq!((board.player.x, board.player.y), (3, 0)); + } + + #[test] + fn push_into_player_blocked_by_wall() { + // Player backed against a wall: push has nowhere to go, nothing moves. + let mut board = open_board(4, 1, (2, 0), vec![], &[]); + crate_at(&mut board, 1, 0); + wall_at(&mut board, 3, 0); + assert!(!board.can_push(1, 0, Direction::East)); + board.push(1, 0, Direction::East); // no-op + assert_eq!(board.get(1, 0).1, Archetype::Crate); + assert_eq!((board.player.x, board.player.y), (2, 0)); + } + + #[test] + fn glyph_at_uses_floor_for_empty_and_grid_for_solid() { + // Player parked at (2,0) so it doesn't overlap either asserted cell. + let mut board = open_board(3, 1, (2, 0), vec![], &[]); + let floor_glyph = Glyph { + tile: '.' as u32, + fg: Rgba8 { r: 10, g: 20, b: 30, a: 255 }, + bg: Rgba8 { r: 1, g: 2, b: 3, a: 255 }, + }; + board.floor = vec![floor_glyph; 3]; + wall_at(&mut board, 0, 0); + assert_eq!(board.glyph_at(0, 0), Archetype::Wall.default_glyph()); + assert_eq!(board.glyph_at(1, 0), floor_glyph); + } + + #[test] + fn fresh_board_is_valid_and_reports_errors() { + let mut board = open_board(1, 1, (0, 0), vec![], &[]); + assert!(board.is_valid()); + board.report_error("something went wrong"); + assert!(!board.is_valid()); + assert_eq!(board.load_errors.len(), 1); + } } \ No newline at end of file diff --git a/kiln-core/src/game.rs b/kiln-core/src/game.rs index 563f467..ae59c86 100644 --- a/kiln-core/src/game.rs +++ b/kiln-core/src/game.rs @@ -189,14 +189,13 @@ fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option, - scripts: &[(&str, &str)], - ) -> Board { - // Assign sequential ids (1..=n) just like the map loader, so tests that - // index by id can rely on first-object == id 1. - let mut object_map: BTreeMap = BTreeMap::new(); - let mut next_object_id: ObjectId = 1; - for o in objects { - object_map.insert(next_object_id, o); - next_object_id += 1; - } - Board { - name: "test".into(), - width: w, - height: h, - cells: vec![(Archetype::Empty.default_glyph(), Archetype::Empty); w * h], - floor: vec![Archetype::Empty.default_glyph(); w * h], - floor_spec: None, - player: Player { - x: player.0, - y: player.1, - }, - objects: object_map, - next_object_id, - portals: Vec::new(), - font: None, - zoom: 1, - scripts: scripts - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(), - board_script_name: None, - load_errors: Vec::new(), - } - } - /// Flattens each log line into a single string for easy assertions. fn log_texts(game: &GameState) -> Vec { game.log @@ -286,81 +242,26 @@ mod tests { } #[test] - fn solid_at_reports_wall_object_and_empty() { - // A 4×1 board: empty floor, a wall, an object, and the player parked at (3,0) - // (kept off the asserted cells, since the player is itself solid). - let mut board = open_board(4, 1, (3, 0), vec![], &[]); - board.cells[1] = (Archetype::Wall.default_glyph(), Archetype::Wall); - // A solid object on the otherwise-empty cell (2, 0). - board.add_object(ObjectDef::new(2, 0)); // solid by default - - // Empty floor: nothing solid. - assert!(board.solid_at(0, 0).is_none()); - assert!(board.is_passable(0, 0)); - - // Wall cell: the grid archetype is the solid. - match board.solid_at(1, 0) { - Some(Solid::Cell(Archetype::Wall)) => {} - other => panic!("expected Solid::Cell(Wall), got {:?}", other.is_some()), - } - assert!(!board.is_passable(1, 0)); - - // Object cell: the solid object shadows the empty floor under it. - match board.solid_at(2, 0) { - Some(Solid::Object(id)) => { - let o = &board.objects[&id]; - assert_eq!((o.x, o.y), (2, 0)) - }, - _ => panic!("expected Solid::Object"), - } - assert!(!board.is_passable(2, 0)); - } - - #[test] - fn non_solid_object_does_not_block() { - // A non-solid object sits on empty floor: the cell stays passable. - let mut obj = ObjectDef::new(1, 0); - obj.solid = false; - let board = open_board(3, 1, (0, 0), vec![obj], &[]); - assert!(board.solid_at(1, 0).is_none()); - assert!(board.is_passable(1, 0)); - } - - /// Stamps a crate cell onto an otherwise-open board. - fn crate_at(board: &mut Board, x: usize, y: usize) { - *board.get_mut(x, y) = (Archetype::Crate.default_glyph(), Archetype::Crate); - } - - /// Stamps a wall cell onto an otherwise-open board. - fn wall_at(board: &mut Board, x: usize, y: usize) { - *board.get_mut(x, y) = (Archetype::Wall.default_glyph(), Archetype::Wall); - } - - #[test] - fn in_bounds_checks_grid_boundaries() { - let board = open_board(3, 2, (0, 0), vec![], &[]); - assert!(board.in_bounds((0, 0))); - assert!(board.in_bounds((2, 1))); - assert!(!board.in_bounds((-1, 0))); - assert!(!board.in_bounds((0, -1))); - assert!(!board.in_bounds((3, 0))); // x == width - assert!(!board.in_bounds((0, 2))); // y == height - } - - #[test] - fn can_push_is_read_only_and_correct() { - // Crate with empty space beyond: pushable, and the board is left untouched. - let mut board = open_board(3, 1, (0, 0), vec![], &[]); + fn pushing_a_crate_reveals_the_floor_underneath() { + // The cell a crate is pushed off of becomes Empty and glyph_at shows floor. + // After the push the player is at (1,0); check the cell the player vacated + // (0,0) — it is Empty and must reveal the floor glyph, not black-on-black. + use color::Rgba8; + use crate::glyph::Glyph; + let mut board = open_board(4, 1, (0, 0), vec![], &[]); + let floor_glyph = Glyph { + tile: ',' as u32, + fg: Rgba8 { r: 40, g: 60, b: 40, a: 255 }, + bg: Rgba8 { r: 5, g: 10, b: 5, a: 255 }, + }; + board.floor = vec![floor_glyph; 4]; crate_at(&mut board, 1, 0); - assert!(board.can_push(1, 0, Direction::East)); - assert_eq!(board.get(1, 0).1, Archetype::Crate); // no mutation - assert_eq!(board.get(2, 0).1, Archetype::Empty); - - // Crate against a wall: not pushable. - let mut board = open_board(3, 1, (0, 0), vec![], &[]); - crate_at(&mut board, 1, 0); - wall_at(&mut board, 2, 0); - assert!(!board.can_push(1, 0, Direction::East)); + let mut game = GameState::new(board); + game.try_move(Direction::East); + let b = game.board(); + assert_eq!(b.get(2, 0).1, Archetype::Crate); + assert_eq!(b.get(1, 0).1, Archetype::Empty); + assert_eq!(b.glyph_at(0, 0), floor_glyph); } #[test] @@ -460,24 +361,6 @@ mod tests { assert_eq!(b.get(3, 0).1, Archetype::Crate); // crate pushed to (3,0) } - #[test] - fn pushable_allows_only_its_axis() { - use Direction::*; - for d in [North, South, East, West] { - assert!(!Pushable::No.allows(d)); - assert!(Pushable::Any.allows(d)); - } - assert!(Pushable::Horizontal.allows(East) && Pushable::Horizontal.allows(West)); - assert!(!Pushable::Horizontal.allows(North) && !Pushable::Horizontal.allows(South)); - assert!(Pushable::Vertical.allows(North) && Pushable::Vertical.allows(South)); - assert!(!Pushable::Vertical.allows(East) && !Pushable::Vertical.allows(West)); - } - - /// Stamps an archetype cell onto an otherwise-open board. - fn stamp(board: &mut Board, x: usize, y: usize, arch: Archetype) { - *board.get_mut(x, y) = (arch.default_glyph(), arch); - } - #[test] fn hcrate_pushes_east_but_not_north() { // Pushing east: the HCrate slides. @@ -524,21 +407,6 @@ mod tests { assert_eq!(b.get(1, 0).1, Archetype::VCrate); // didn't move } - #[test] - fn fresh_board_is_valid_and_reports_errors() { - let mut board = open_board(1, 1, (0, 0), vec![], &[]); - assert!(board.is_valid()); - board.report_error("something went wrong"); - assert!(!board.is_valid()); - assert_eq!(board.load_errors.len(), 1); - } - - #[test] - fn directional_crate_default_glyph_tiles() { - assert_eq!(Archetype::HCrate.default_glyph().tile, 29); - assert_eq!(Archetype::VCrate.default_glyph().tile, 18); - } - #[test] fn init_runs_only_on_run_init_not_at_construction() { let board = board_with_object( @@ -916,101 +784,6 @@ mod tests { assert!(log_texts(&game).iter().any(|t| t == "bumped by -1")); } - #[test] - fn glyph_at_uses_floor_for_empty_and_grid_for_solid() { - // A board with a distinctive floor glyph; empty cells show the floor, while - // a wall keeps drawing its own grid glyph (floor ignored under solids). - // Player parked at (2,0) so it doesn't overlap either asserted cell. - let mut board = open_board(3, 1, (2, 0), vec![], &[]); - let floor_glyph = Glyph { - tile: '.' as u32, - fg: Rgba8 { - r: 10, - g: 20, - b: 30, - a: 255, - }, - bg: Rgba8 { - r: 1, - g: 2, - b: 3, - a: 255, - }, - }; - board.floor = vec![floor_glyph; 3]; - wall_at(&mut board, 0, 0); - assert_eq!(board.glyph_at(0, 0), Archetype::Wall.default_glyph()); // solid: grid glyph - assert_eq!(board.glyph_at(1, 0), floor_glyph); // empty: floor glyph - } - - #[test] - fn pushing_a_crate_reveals_the_floor_underneath() { - // The cell a crate is pushed off of becomes Empty, so its `glyph_at` - // should show the floor glyph rather than black-on-black. - let mut board = open_board(4, 1, (0, 0), vec![], &[]); - let floor_glyph = Glyph { - tile: ',' as u32, - fg: Rgba8 { - r: 40, - g: 60, - b: 40, - a: 255, - }, - bg: Rgba8 { - r: 5, - g: 10, - b: 5, - a: 255, - }, - }; - board.floor = vec![floor_glyph; 4]; - crate_at(&mut board, 1, 0); - let mut game = GameState::new(board); - game.try_move(Direction::East); // player at (0,0) pushes the crate east - let b = game.board(); - assert_eq!(b.get(2, 0).1, Archetype::Crate); // crate moved one east - // After the push the player is at (1,0); check the cell the player vacated - // (0,0) — it is Empty and must reveal the floor glyph, not black-on-black. - assert_eq!(b.get(1, 0).1, Archetype::Empty); // crate's old cell is empty - assert_eq!(b.glyph_at(0, 0), floor_glyph); // player's old cell shows floor - } - - #[test] - fn solid_at_reports_player() { - // The player's own cell is solid (and thus impassable to movers). - let board = open_board(3, 1, (1, 0), vec![], &[]); - match board.solid_at(1, 0) { - Some(Solid::Player) => {} - _ => panic!("expected Solid::Player at the player's cell"), - } - assert!(!board.is_passable(1, 0)); - } - - #[test] - fn push_into_player_pushes_player() { - // Crate shoved east into the player slides the player along into open space. - let mut board = open_board(4, 1, (2, 0), vec![], &[]); - crate_at(&mut board, 1, 0); - assert!(board.can_push(1, 0, Direction::East)); - board.push(1, 0, Direction::East); - assert_eq!(board.get(1, 0).1, Archetype::Empty); // crate left its cell - assert_eq!(board.get(2, 0).1, Archetype::Crate); // crate took the player's old cell - assert_eq!((board.player.x, board.player.y), (3, 0)); // player shoved east - } - - #[test] - fn push_into_player_blocked_by_wall() { - // Player backed against a wall: the push has nowhere to go, so nothing moves - // and the player is never overlapped. - let mut board = open_board(4, 1, (2, 0), vec![], &[]); - crate_at(&mut board, 1, 0); - wall_at(&mut board, 3, 0); - assert!(!board.can_push(1, 0, Direction::East)); - board.push(1, 0, Direction::East); // no-op - assert_eq!(board.get(1, 0).1, Archetype::Crate); - assert_eq!((board.player.x, board.player.y), (2, 0)); - } - #[test] fn object_push_into_player() { // A scripted object moving into the player pushes the player when there's room. diff --git a/kiln-core/src/utils.rs b/kiln-core/src/utils.rs index d8fa314..80581dc 100644 --- a/kiln-core/src/utils.rs +++ b/kiln-core/src/utils.rs @@ -110,4 +110,26 @@ pub struct Player { pub x: i32, /// Row position (0-indexed, increasing downward). pub y: i32, +} + +#[cfg(test)] +mod tests { + use crate::script::Direction; + use super::Pushable; + + #[test] + fn pushable_allows_only_its_axis() { + for d in [Direction::North, Direction::South, Direction::East, Direction::West] { + assert!(!Pushable::No.allows(d)); + assert!(Pushable::Any.allows(d)); + } + assert!(Pushable::Horizontal.allows(Direction::East)); + assert!(Pushable::Horizontal.allows(Direction::West)); + assert!(!Pushable::Horizontal.allows(Direction::North)); + assert!(!Pushable::Horizontal.allows(Direction::South)); + assert!(Pushable::Vertical.allows(Direction::North)); + assert!(Pushable::Vertical.allows(Direction::South)); + assert!(!Pushable::Vertical.allows(Direction::East)); + assert!(!Pushable::Vertical.allows(Direction::West)); + } } \ No newline at end of file