Moved tests

This commit is contained in:
2026-06-07 00:46:38 -05:00
parent 5ea7e4ec4e
commit 9c2212520c
13 changed files with 1015 additions and 1031 deletions
-631
View File
@@ -187,634 +187,3 @@ fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<Object
bumped
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::time::Duration;
use crate::archetype::Archetype;
use crate::board::Board;
use crate::board::tests::{crate_at, open_board, stamp, wall_at};
use crate::object_def::ObjectDef;
use crate::utils::Player;
use super::*;
/// Builds a 1×1 board with a single object that optionally references a
/// script, plus the given `(name, source)` script table entries.
fn board_with_object(object_script: Option<&str>, scripts: &[(&str, &str)]) -> Board {
let mut object = ObjectDef::new(0, 0);
object.script_name = object_script.map(str::to_string);
Board {
name: "test".into(),
width: 1,
height: 1,
cells: vec![(Archetype::Empty.default_glyph(), Archetype::Empty)],
floor: vec![Archetype::Empty.default_glyph()],
floor_spec: None,
player: Player { x: 0, y: 0 },
objects: BTreeMap::from([(1, object)]),
next_object_id: 2,
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(),
}
}
/// An `ObjectDef` at `(x, y)` bound to the named script.
fn scripted_object(x: usize, y: usize, script: &str) -> ObjectDef {
let mut o = ObjectDef::new(x, y);
o.script_name = Some(script.to_string());
o
}
/// Flattens each log line into a single string for easy assertions.
fn log_texts(game: &GameState) -> Vec<String> {
game.log
.iter()
.map(|line| line.spans.iter().map(|s| s.text.as_str()).collect())
.collect()
}
#[test]
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);
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]
fn player_pushes_single_crate() {
let mut board = open_board(4, 1, (0, 0), vec![], &[]);
crate_at(&mut board, 1, 0);
let mut game = GameState::new(board);
game.try_move(Direction::East);
let b = game.board();
assert_eq!((b.player.x, b.player.y), (1, 0)); // player advanced onto crate's old cell
assert_eq!(b.get(1, 0).1, Archetype::Empty); // crate left this cell
assert_eq!(b.get(2, 0).1, Archetype::Crate); // crate moved one east
}
#[test]
fn push_blocked_by_wall_moves_nothing() {
let mut board = open_board(4, 1, (0, 0), vec![], &[]);
crate_at(&mut board, 1, 0);
wall_at(&mut board, 2, 0);
let mut game = GameState::new(board);
game.try_move(Direction::East);
let b = game.board();
assert_eq!((b.player.x, b.player.y), (0, 0)); // blocked
assert_eq!(b.get(1, 0).1, Archetype::Crate); // crate didn't move
assert_eq!(b.get(2, 0).1, Archetype::Wall); // wall didn't move
}
#[test]
fn push_blocked_by_edge_moves_nothing() {
// Crate on the last column; player pushes it toward the board edge.
let mut board = open_board(2, 1, (0, 0), vec![], &[]);
crate_at(&mut board, 1, 0);
let mut game = GameState::new(board);
game.try_move(Direction::East);
let b = game.board();
assert_eq!((b.player.x, b.player.y), (0, 0));
assert_eq!(b.get(1, 0).1, Archetype::Crate);
}
#[test]
fn cascade_pushes_two_crates() {
let mut board = open_board(5, 1, (0, 0), vec![], &[]);
crate_at(&mut board, 1, 0);
crate_at(&mut board, 2, 0);
let mut game = GameState::new(board);
game.try_move(Direction::East);
let b = game.board();
assert_eq!((b.player.x, b.player.y), (1, 0));
assert_eq!(b.get(1, 0).1, Archetype::Empty);
assert_eq!(b.get(2, 0).1, Archetype::Crate);
assert_eq!(b.get(3, 0).1, Archetype::Crate);
}
#[test]
fn cascade_blocked_by_wall_moves_nothing() {
let mut board = open_board(5, 1, (0, 0), vec![], &[]);
crate_at(&mut board, 1, 0);
crate_at(&mut board, 2, 0);
wall_at(&mut board, 3, 0);
let mut game = GameState::new(board);
game.try_move(Direction::East);
let b = game.board();
assert_eq!((b.player.x, b.player.y), (0, 0));
assert_eq!(b.get(1, 0).1, Archetype::Crate);
assert_eq!(b.get(2, 0).1, Archetype::Crate);
}
#[test]
fn player_pushes_pushable_solid_object() {
// A solid, pushable object is shoved exactly like a crate.
let mut obj = ObjectDef::new(1, 0); // solid by default
obj.pushable = true;
let board = open_board(4, 1, (0, 0), vec![obj], &[]);
let mut game = GameState::new(board);
game.try_move(Direction::East);
let b = game.board();
assert_eq!((b.player.x, b.player.y), (1, 0));
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); // object shoved east
}
#[test]
fn object_pushes_crate_on_init() {
// A scripted object moving east into a crate shoves it (move_object path).
let mut board = open_board(
4,
1,
(0, 0), // player behind the object, out of the push path
vec![scripted_object(1, 0, "m")],
&[("m", "fn init() { move(East); }")],
);
crate_at(&mut board, 2, 0);
let mut game = GameState::new(board);
game.run_init();
let b = game.board();
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); // object advanced
assert_eq!(b.get(2, 0).1, Archetype::Empty); // crate left (2,0)
assert_eq!(b.get(3, 0).1, Archetype::Crate); // crate pushed to (3,0)
}
#[test]
fn hcrate_pushes_east_but_not_north() {
// Pushing east: the HCrate slides.
let mut board = open_board(4, 1, (0, 0), vec![], &[]);
stamp(&mut board, 1, 0, Archetype::HCrate);
let mut game = GameState::new(board);
game.try_move(Direction::East);
let b = game.board();
assert_eq!((b.player.x, b.player.y), (1, 0));
assert_eq!(b.get(1, 0).1, Archetype::Empty);
assert_eq!(b.get(2, 0).1, Archetype::HCrate);
drop(b);
// Pushing north into an HCrate: blocked, nothing moves.
let mut board = open_board(1, 4, (0, 3), vec![], &[]);
stamp(&mut board, 0, 2, Archetype::HCrate);
let mut game = GameState::new(board);
game.try_move(Direction::North);
let b = game.board();
assert_eq!((b.player.x, b.player.y), (0, 3)); // blocked
assert_eq!(b.get(0, 2).1, Archetype::HCrate); // didn't move
}
#[test]
fn vcrate_pushes_north_but_not_east() {
// Pushing north: the VCrate slides.
let mut board = open_board(1, 4, (0, 3), vec![], &[]);
stamp(&mut board, 0, 2, Archetype::VCrate);
let mut game = GameState::new(board);
game.try_move(Direction::North);
let b = game.board();
assert_eq!((b.player.x, b.player.y), (0, 2));
assert_eq!(b.get(0, 2).1, Archetype::Empty);
assert_eq!(b.get(0, 1).1, Archetype::VCrate);
drop(b);
// Pushing east into a VCrate: blocked, nothing moves.
let mut board = open_board(4, 1, (0, 0), vec![], &[]);
stamp(&mut board, 1, 0, Archetype::VCrate);
let mut game = GameState::new(board);
game.try_move(Direction::East);
let b = game.board();
assert_eq!((b.player.x, b.player.y), (0, 0)); // blocked
assert_eq!(b.get(1, 0).1, Archetype::VCrate); // didn't move
}
#[test]
fn init_runs_only_on_run_init_not_at_construction() {
let board = board_with_object(
Some("greet"),
&[("greet", r#"fn init() { log("hello"); }"#)],
);
let mut game = GameState::new(board);
// init must not fire during construction / deserialization.
assert!(game.log.is_empty());
game.run_init();
assert_eq!(log_texts(&game), vec!["hello"]);
}
#[test]
fn tick_calls_script_tick_with_elapsed_seconds() {
let board = board_with_object(
Some("t"),
&[("t", r#"fn tick(dt) { log(dt.to_string()); }"#)],
);
let mut game = GameState::new(board);
// No tick hook runs until tick() is called.
game.run_init();
assert!(game.log.is_empty());
game.tick(Duration::from_millis(500));
assert_eq!(game.log.len(), 1);
// The dt argument reached the script as ~0.5 seconds.
let logged: f64 = log_texts(&game)[0].parse().unwrap();
assert!((logged - 0.5).abs() < 1e-9);
}
#[test]
fn missing_hooks_and_no_script_are_noops() {
// Object with no script: nothing happens.
let mut game = GameState::new(board_with_object(None, &[]));
game.run_init();
game.tick(Duration::from_millis(33));
assert!(game.log.is_empty());
// Script defines neither init nor tick: also a no-op.
let mut game = GameState::new(board_with_object(
Some("e"),
&[("e", "fn other() { log(\"x\"); }")],
));
game.run_init();
game.tick(Duration::from_millis(33));
assert!(game.log.is_empty());
}
#[test]
fn compile_and_unknown_script_errors_are_logged() {
// A reference to a script name that isn't in the table.
let game = GameState::new(board_with_object(Some("ghost"), &[]));
assert!(log_texts(&game)[0].contains("unknown script 'ghost'"));
// A script that fails to compile is reported at construction time.
let game = GameState::new(board_with_object(Some("bad"), &[("bad", "fn init( {")]));
assert!(log_texts(&game)[0].contains("failed to compile"));
}
#[test]
fn script_reads_board_through_view() {
let board = open_board(
5,
3,
(3, 1),
vec![scripted_object(2, 1, "r")],
&[("r", "fn init() { log(Board.player_x.to_string()); }")],
);
let mut game = GameState::new(board);
game.run_init();
// The view reported the player's x (3).
assert_eq!(log_texts(&game), vec!["3"]);
}
#[test]
fn move_command_relocates_the_source_object() {
let board = open_board(
5,
3,
(0, 0),
vec![scripted_object(2, 1, "m")],
&[("m", "fn init() { move(East); }")],
);
let mut game = GameState::new(board);
game.run_init();
let b = game.board();
// East increments x by one; the object started at (2, 1).
assert_eq!((b.objects[&1].x, b.objects[&1].y), (3, 1));
}
#[test]
fn move_into_a_wall_or_edge_is_a_noop() {
// Object at the west edge moving west: out of bounds, ignored.
let board = open_board(
5,
3,
(0, 0),
vec![scripted_object(0, 1, "m")],
&[("m", "fn init() { move(West); }")],
);
let mut game = GameState::new(board);
game.run_init();
assert_eq!(game.board().objects[&1].x, 0);
}
#[test]
fn set_tile_command_changes_the_source_glyph() {
let board = open_board(
5,
3,
(0, 0),
vec![scripted_object(2, 1, "s")],
&[("s", "fn init() { set_tile(7); }")],
);
let mut game = GameState::new(board);
game.run_init();
assert_eq!(game.board().objects[&1].glyph.tile, 7);
}
#[test]
fn start_map_greeter_runs_init() {
// End-to-end against the shipped example map: load it, run init, and
// confirm the greeter read the board (interpolated message) and wrote to
// itself (set_tile). Also guards the example from drifting out of sync.
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../maps/start.toml");
let board = crate::map_file::load(path).expect("load start.toml");
let mut game = GameState::new(board);
game.run_init();
assert!(
log_texts(&game)
.iter()
.any(|t| t.contains("hello from object")),
"greeter init should log a greeting"
);
assert!(
game.board().objects.values().any(|o| o.glyph.tile == 2),
"greeter set_tile(2) should change its glyph"
);
}
#[test]
fn commands_are_routed_to_their_own_source_object() {
// Two objects with different scripts move in opposite directions; each
// must affect only itself (the per-call tag routes the command source).
// Targets are kept apart so the impassable objects don't block each other.
let board = open_board(
5,
3,
(0, 0),
vec![scripted_object(0, 1, "e"), scripted_object(4, 1, "w")],
&[
("e", "fn init() { move(East); }"),
("w", "fn init() { move(West); }"),
],
);
let mut game = GameState::new(board);
game.run_init();
let b = game.board();
assert_eq!(b.objects[&1].x, 1); // moved east from 0
assert_eq!(b.objects[&2].x, 3); // moved west from 4
}
#[test]
fn move_cost_rate_limits_repeated_moves() {
// init queues two moves; only the first resolves immediately, the second must
// wait the full 250 ms cooldown.
let board = open_board(
5,
1,
(0, 0), // player to the west, out of the object's path
vec![scripted_object(1, 0, "m")],
&[("m", "fn init() { move(East); move(East); }")],
);
let mut game = GameState::new(board);
game.run_init();
assert_eq!(game.board().objects[&1].x, 2); // first move applied (1 -> 2)
// 200 ms of ticks: still inside the cooldown, no further movement.
game.tick(Duration::from_millis(100));
game.tick(Duration::from_millis(100));
assert_eq!(game.board().objects[&1].x, 2);
// Crossing the 250 ms mark releases the queued second move.
game.tick(Duration::from_millis(100));
assert_eq!(game.board().objects[&1].x, 3);
}
#[test]
fn blocked_move_still_costs_cooldown() {
// A move into a wall fails but still charges the 250 ms cooldown, so a second
// queued move (into open space) is delayed just as a successful move would be.
let mut board = open_board(
3,
3,
(0, 0),
vec![scripted_object(1, 1, "m")],
&[("m", "fn init() { move(East); move(South); }")],
);
wall_at(&mut board, 2, 1); // blocks the eastward move
let mut game = GameState::new(board);
game.run_init();
// First (eastward) move is blocked by the wall: object hasn't moved.
assert_eq!(
(game.board().objects[&1].x, game.board().objects[&1].y),
(1, 1)
);
// The blocked move charged the cooldown, so South is still pending here.
game.tick(Duration::from_millis(100));
game.tick(Duration::from_millis(100));
assert_eq!(
(game.board().objects[&1].x, game.board().objects[&1].y),
(1, 1)
);
// Past 250 ms the queued South move resolves.
game.tick(Duration::from_millis(100));
assert_eq!(
(game.board().objects[&1].x, game.board().objects[&1].y),
(1, 2)
);
}
#[test]
fn queue_length_reports_pending_actions() {
// Two zero-cost actions are queued before the length is read, then all three
// (incl. the log) drain in one pump.
let board = open_board(
3,
1,
(0, 0),
vec![scripted_object(1, 0, "q")],
&[(
"q",
"fn init() { set_tile(5); set_tile(6); log(`len=${Queue.length()}`); }",
)],
);
let mut game = GameState::new(board);
game.run_init();
assert!(log_texts(&game).iter().any(|t| t == "len=2"));
assert_eq!(game.board().objects[&1].glyph.tile, 6); // last set_tile won
}
#[test]
fn queue_clear_drops_pending_actions() {
// clear() empties the output queue mid-script, so the queued moves never run.
let board = open_board(
5,
1,
(0, 0),
vec![scripted_object(1, 0, "c")],
&[("c", "fn init() { move(East); move(East); Queue.clear(); }")],
);
let mut game = GameState::new(board);
game.run_init();
game.tick(Duration::from_millis(300));
assert_eq!(game.board().objects[&1].x, 1); // never moved
}
#[test]
fn blocked_reports_solid_and_clear() {
// Solid ahead (a wall): blocked() is true.
let mut board = open_board(
3,
1,
(0, 0),
vec![scripted_object(1, 0, "b")],
&[(
"b",
"fn init() { if blocked(East) { set_tile(9); } else { set_tile(7); } }",
)],
);
wall_at(&mut board, 2, 0);
let mut game = GameState::new(board);
game.run_init();
assert_eq!(game.board().objects[&1].glyph.tile, 9);
// Open ahead, nothing pending: blocked() is false.
let board = open_board(
3,
1,
(0, 0),
vec![scripted_object(1, 0, "b")],
&[(
"b",
"fn init() { if blocked(East) { set_tile(9); } else { set_tile(7); } }",
)],
);
let mut game = GameState::new(board);
game.run_init();
assert_eq!(game.board().objects[&1].glyph.tile, 7);
}
#[test]
fn blocked_sees_earlier_objects_pending_move() {
// obj0 (earlier in the array) queues a move into (2,1); obj1 checks blocked()
// toward that same cell and must see the pending move.
let board = open_board(
5,
3,
(0, 0),
vec![
scripted_object(1, 1, "mover"),
scripted_object(3, 1, "checker"),
],
&[
("mover", "fn tick(dt) { move(East); }"),
(
"checker",
"fn tick(dt) { if blocked(West) { set_tile(1); } else { set_tile(2); } }",
),
],
);
let mut game = GameState::new(board);
game.tick(Duration::from_millis(16));
let b = game.board();
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 1)); // mover advanced
assert_eq!(b.objects[&2].glyph.tile, 1); // checker saw the pending move
}
#[test]
fn collision_priority_resolves_in_array_order_and_bumps() {
// Two objects move into the same empty cell (1,0). obj0 (earlier) wins it;
// obj1 is blocked and bumps obj0. obj1 itself receives no bump.
let board = open_board(
3,
2,
(0, 1), // player off the contested row
vec![scripted_object(0, 0, "e"), scripted_object(2, 0, "w")],
&[
(
"e",
"fn init() { move(East); } fn bump(id) { log(`o0 by ${id}`); }",
),
(
"w",
"fn init() { move(West); } fn bump(id) { log(`o1 by ${id}`); }",
),
],
);
let mut game = GameState::new(board);
game.run_init();
// The bump fires during resolution but its log is emitted into obj0's queue;
// a later tick (past both objects' move cooldown) flushes it to the game log.
game.tick(Duration::from_millis(300));
{
let b = game.board();
assert_eq!((b.objects[&1].x, b.objects[&1].y), (1, 0)); // obj0 won the cell
assert_eq!((b.objects[&2].x, b.objects[&2].y), (2, 0)); // obj1 blocked
}
let logs = log_texts(&game);
assert!(logs.iter().any(|t| t == "o0 by 2")); // obj0 bumped by obj1
assert!(!logs.iter().any(|t| t.starts_with("o1 by"))); // obj1 not bumped
}
#[test]
fn player_bump_fires_with_negative_one() {
// The player walks into a solid scripted object; the object is bumped with -1
// and the player does not move onto it.
let board = open_board(
3,
1,
(0, 0),
vec![scripted_object(1, 0, "b")],
&[("b", "fn bump(id) { log(`bumped by ${id}`); }")],
);
let mut game = GameState::new(board);
game.run_init();
game.try_move(Direction::East);
assert_eq!((game.board().player.x, game.board().player.y), (0, 0)); // blocked
// bump's log is emitted into the object's queue; a tick flushes it.
game.tick(Duration::from_millis(16));
assert!(log_texts(&game).iter().any(|t| t == "bumped by -1"));
}
#[test]
fn object_push_into_player() {
// A scripted object moving into the player pushes the player when there's room.
let board = open_board(
5,
1,
(2, 0),
vec![scripted_object(1, 0, "m")],
&[("m", "fn init() { move(East); }")],
);
let mut game = GameState::new(board);
game.run_init();
{
let b = game.board();
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); // object took player's cell
assert_eq!((b.player.x, b.player.y), (3, 0)); // player shoved east
}
// With a wall behind the player, the object is blocked and nothing moves.
let mut board = open_board(
4,
1,
(2, 0),
vec![scripted_object(1, 0, "m")],
&[("m", "fn init() { move(East); }")],
);
wall_at(&mut board, 3, 0);
let mut game = GameState::new(board);
game.run_init();
let b = game.board();
assert_eq!((b.objects[&1].x, b.objects[&1].y), (1, 0)); // object blocked
assert_eq!((b.player.x, b.player.y), (2, 0)); // player not pushed
}
}