263 lines
8.5 KiB
Rust
263 lines
8.5 KiB
Rust
use std::time::Duration;
|
||
use crate::board::tests::open_board;
|
||
use crate::game::GameState;
|
||
use crate::utils::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"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn set_tag_adds_and_removes_via_my_id() {
|
||
// A script calls set_tag(MY_ID, "active", true) in init; the tag must be
|
||
// present on the object afterward.
|
||
let board = board_with_object(
|
||
Some("t"),
|
||
&[("t", r#"fn init() { set_tag(MY_ID, "active", true); }"#)],
|
||
);
|
||
let mut game = GameState::new(board);
|
||
game.run_init();
|
||
assert!(game.board().objects[&1].tags.contains("active"));
|
||
|
||
// A script removes a pre-existing tag.
|
||
let board2 = board_with_object(
|
||
Some("t2"),
|
||
&[("t2", r#"fn init() { set_tag(MY_ID, "active", false); }"#)],
|
||
);
|
||
// Seed the tag before construction.
|
||
let mut game2 = GameState::new({
|
||
let mut b = board2; // need mut to insert tag
|
||
b.objects.get_mut(&1).unwrap().tags.insert("active".to_string());
|
||
b
|
||
});
|
||
game2.run_init();
|
||
assert!(!game2.board().objects[&1].tags.contains("active"));
|
||
}
|
||
|
||
#[test]
|
||
fn has_tag_reads_own_tags() {
|
||
// set_tag queues an action; has_tag reads the board state. So set_tag in
|
||
// init() takes effect after init returns; a subsequent tick sees it.
|
||
let board = board_with_object(
|
||
Some("t"),
|
||
&[(
|
||
"t",
|
||
r#"fn init() { set_tag(MY_ID, "active", true); }
|
||
fn tick(dt) { log(has_tag("active").to_string()); }"#,
|
||
)],
|
||
);
|
||
let mut game = GameState::new(board);
|
||
game.run_init();
|
||
game.tick(Duration::from_millis(16));
|
||
assert!(log_texts(&game).iter().any(|t| t == "true"));
|
||
}
|
||
|
||
#[test]
|
||
fn objects_with_tag_returns_matching_ids() {
|
||
// A 5×1 board with two objects; one has tag "enemy". The script on the first
|
||
// object queries objects_with_tag("enemy") and logs the ids it finds.
|
||
let obj1 = scripted_object(0, 0, "q");
|
||
let mut obj2 = scripted_object(1, 0, "none");
|
||
// obj2 (id=2) has the "enemy" tag; obj1 (id=1) does not.
|
||
obj2.tags.insert("enemy".to_string());
|
||
let board = open_board(
|
||
5,
|
||
1,
|
||
(4, 0),
|
||
vec![obj1, obj2],
|
||
&[
|
||
("q", r#"fn init() {
|
||
let ids = objects_with_tag("enemy");
|
||
log(ids.len().to_string());
|
||
log(ids[0].to_string());
|
||
}"#),
|
||
("none", ""),
|
||
],
|
||
);
|
||
let mut game = GameState::new(board);
|
||
game.run_init();
|
||
let texts = log_texts(&game);
|
||
assert_eq!(texts[0], "1"); // exactly one match
|
||
assert_eq!(texts[1], "2"); // obj2 is id 2
|
||
}
|
||
|
||
#[test]
|
||
fn my_name_returns_name_or_empty_string() {
|
||
// An object with a name set on its ObjectDef should see it via my_name().
|
||
let mut board = board_with_object(
|
||
Some("n"),
|
||
&[("n", r#"fn init() { log(my_name()); }"#)],
|
||
);
|
||
board.objects.get_mut(&1).unwrap().name = Some("beacon".to_string());
|
||
let mut game = GameState::new(board);
|
||
game.run_init();
|
||
assert_eq!(log_texts(&game), vec!["beacon"]);
|
||
|
||
// An unnamed object should get an empty string.
|
||
let board2 = board_with_object(
|
||
Some("n"),
|
||
&[("n", r#"fn init() { log(my_name()); }"#)],
|
||
);
|
||
let mut game2 = GameState::new(board2);
|
||
game2.run_init();
|
||
assert_eq!(log_texts(&game2), vec![""]);
|
||
}
|
||
|
||
#[test]
|
||
fn object_id_for_name_finds_by_name() {
|
||
// A board with two objects; one is named. The querying object uses
|
||
// object_id_for_name to find the named one and logs its id.
|
||
let obj1 = scripted_object(0, 0, "q");
|
||
let mut obj2 = scripted_object(1, 0, "none");
|
||
obj2.name = Some("target".to_string());
|
||
let board = open_board(
|
||
5,
|
||
1,
|
||
(4, 0),
|
||
vec![obj1, obj2],
|
||
&[
|
||
("q", r#"fn init() {
|
||
log(object_id_for_name("target").to_string());
|
||
log(object_id_for_name("missing").to_string());
|
||
}"#),
|
||
("none", ""),
|
||
],
|
||
);
|
||
let mut game = GameState::new(board);
|
||
game.run_init();
|
||
let texts = log_texts(&game);
|
||
assert_eq!(texts[0], "2"); // obj2 is id 2
|
||
assert_eq!(texts[1], "0"); // 0 = not found
|
||
}
|
||
|
||
// 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"));
|
||
}
|