140 lines
4.5 KiB
Rust
140 lines
4.5 KiB
Rust
|
|
use std::time::Duration;
|
||
|
|
use crate::board::tests::open_board;
|
||
|
|
use crate::game::GameState;
|
||
|
|
use crate::script::Direction;
|
||
|
|
use super::{board_with_object, scripted_object, log_texts};
|
||
|
|
|
||
|
|
#[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);
|
||
|
|
game.run_init();
|
||
|
|
assert!(game.log.is_empty());
|
||
|
|
|
||
|
|
game.tick(Duration::from_millis(500));
|
||
|
|
assert_eq!(game.log.len(), 1);
|
||
|
|
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();
|
||
|
|
assert_eq!(log_texts(&game), vec!["3"]);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[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).
|
||
|
|
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 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"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ensure try_move from the player side also triggers scripted bump
|
||
|
|
#[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"));
|
||
|
|
}
|