Board layers
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
use std::time::Duration;
|
||||
use super::{log_texts, scripted_object, scripts_from};
|
||||
use crate::archetype::Archetype;
|
||||
use crate::board::tests::{crate_at, open_board, wall_at};
|
||||
use crate::game::GameState;
|
||||
use super::{scripted_object, log_texts, scripts_from};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn move_command_relocates_the_source_object() {
|
||||
let board = open_board(5, 3, (0, 0), vec![scripted_object(2, 1, "m")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); }")]));
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); }")]));
|
||||
game.run_init();
|
||||
let b = game.board();
|
||||
// East increments x by one; the object started at (2, 1).
|
||||
@@ -18,7 +19,8 @@ fn move_command_relocates_the_source_object() {
|
||||
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")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(West); }")]));
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(West); }")]));
|
||||
game.run_init();
|
||||
assert_eq!(game.board().objects[&1].x, 0);
|
||||
}
|
||||
@@ -26,7 +28,8 @@ fn move_into_a_wall_or_edge_is_a_noop() {
|
||||
#[test]
|
||||
fn set_tile_command_changes_the_source_glyph() {
|
||||
let board = open_board(5, 3, (0, 0), vec![scripted_object(2, 1, "s")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("s", "fn init() { set_tile(7); }")]));
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("s", "fn init() { set_tile(7); }")]));
|
||||
game.run_init();
|
||||
assert_eq!(game.board().objects[&1].glyph.tile, 7);
|
||||
}
|
||||
@@ -36,19 +39,21 @@ fn object_pushes_crate_on_init() {
|
||||
// A scripted object moving east into a crate shoves it (step_object path).
|
||||
let mut board = open_board(4, 1, (0, 0), vec![scripted_object(1, 0, "m")]);
|
||||
crate_at(&mut board, 2, 0);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); }")]));
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); }")]));
|
||||
game.run_init();
|
||||
let b = game.board();
|
||||
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0));
|
||||
assert_eq!(b.get(2, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.get(3, 0).1, Archetype::Crate);
|
||||
assert_eq!(b.get(0, 2, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.get(0, 3, 0).1, Archetype::Crate);
|
||||
}
|
||||
|
||||
#[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")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); }")]));
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); }")]));
|
||||
game.run_init();
|
||||
{
|
||||
let b = game.board();
|
||||
@@ -59,7 +64,8 @@ fn object_push_into_player() {
|
||||
// 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")]);
|
||||
wall_at(&mut board, 3, 0);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); }")]));
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); }")]));
|
||||
game.run_init();
|
||||
let b = game.board();
|
||||
assert_eq!((b.objects[&1].x, b.objects[&1].y), (1, 0));
|
||||
@@ -71,7 +77,10 @@ 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), vec![scripted_object(1, 0, "m")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); move(East); }")]));
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("m", "fn init() { move(East); move(East); }")]),
|
||||
);
|
||||
game.run_init();
|
||||
assert_eq!(game.board().objects[&1].x, 2); // first move applied (1 -> 2)
|
||||
|
||||
@@ -91,19 +100,31 @@ fn inline_delay_paces_subsequent_moves() {
|
||||
// is held back by that delay even if the first move was blocked by a wall.
|
||||
let mut board = open_board(3, 3, (0, 0), vec![scripted_object(1, 1, "m")]);
|
||||
wall_at(&mut board, 2, 1);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("m", "fn init() { move(East); move(South); }")]));
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("m", "fn init() { move(East); move(South); }")]),
|
||||
);
|
||||
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));
|
||||
assert_eq!(
|
||||
(game.board().objects[&1].x, game.board().objects[&1].y),
|
||||
(1, 1)
|
||||
);
|
||||
|
||||
// The Delay(250 ms) appended by move(East) is still live, so South is pending.
|
||||
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));
|
||||
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));
|
||||
assert_eq!(
|
||||
(game.board().objects[&1].x, game.board().objects[&1].y),
|
||||
(1, 2)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -111,10 +132,13 @@ 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")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[(
|
||||
"q",
|
||||
"fn init() { set_tile(5); set_tile(6); log(`len=${Queue.length()}`); }",
|
||||
)]));
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[(
|
||||
"q",
|
||||
"fn init() { set_tile(5); set_tile(6); log(`len=${Queue.length()}`); }",
|
||||
)]),
|
||||
);
|
||||
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
|
||||
@@ -124,7 +148,10 @@ fn queue_length_reports_pending_actions() {
|
||||
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")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("c", "fn init() { move(East); move(East); Queue.clear(); }")]));
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("c", "fn init() { move(East); move(East); Queue.clear(); }")]),
|
||||
);
|
||||
game.run_init();
|
||||
game.tick(Duration::from_millis(300));
|
||||
assert_eq!(game.board().objects[&1].x, 1); // never moved
|
||||
@@ -135,19 +162,25 @@ 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")]);
|
||||
wall_at(&mut board, 2, 0);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[(
|
||||
"b",
|
||||
"fn init() { if blocked(East) { set_tile(9); } else { set_tile(7); } }",
|
||||
)]));
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[(
|
||||
"b",
|
||||
"fn init() { if blocked(East) { set_tile(9); } else { set_tile(7); } }",
|
||||
)]),
|
||||
);
|
||||
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")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[(
|
||||
"b",
|
||||
"fn init() { if blocked(East) { set_tile(9); } else { set_tile(7); } }",
|
||||
)]));
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[(
|
||||
"b",
|
||||
"fn init() { if blocked(East) { set_tile(9); } else { set_tile(7); } }",
|
||||
)]),
|
||||
);
|
||||
game.run_init();
|
||||
assert_eq!(game.board().objects[&1].glyph.tile, 7);
|
||||
}
|
||||
@@ -165,10 +198,16 @@ fn blocked_sees_earlier_objects_pending_move() {
|
||||
scripted_object(3, 1, "checker"),
|
||||
],
|
||||
);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[
|
||||
("mover", "fn tick(dt) { move(East); }"),
|
||||
("checker", "fn tick(dt) { if blocked(West) { set_tile(1); } else { set_tile(2); } }"),
|
||||
]));
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[
|
||||
("mover", "fn tick(dt) { move(East); }"),
|
||||
(
|
||||
"checker",
|
||||
"fn tick(dt) { if blocked(West) { set_tile(1); } else { set_tile(2); } }",
|
||||
),
|
||||
]),
|
||||
);
|
||||
game.tick(Duration::from_millis(16));
|
||||
let b = game.board();
|
||||
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 1));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::time::Duration;
|
||||
use super::{log_texts, scripted_object, scripts_from};
|
||||
use crate::board::tests::open_board;
|
||||
use crate::game::GameState;
|
||||
use super::{scripted_object, log_texts, scripts_from};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn collision_priority_resolves_in_array_order_and_bumps() {
|
||||
@@ -13,10 +13,19 @@ fn collision_priority_resolves_in_array_order_and_bumps() {
|
||||
(0, 1), // player off the contested row
|
||||
vec![scripted_object(0, 0, "e"), scripted_object(2, 0, "w")],
|
||||
);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[
|
||||
("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::with_scripts(
|
||||
board,
|
||||
scripts_from(&[
|
||||
(
|
||||
"e",
|
||||
"fn init() { move(East); } fn bump(id) { log(`o0 by ${id}`); }",
|
||||
),
|
||||
(
|
||||
"w",
|
||||
"fn init() { move(West); } fn bump(id) { log(`o1 by ${id}`); }",
|
||||
),
|
||||
]),
|
||||
);
|
||||
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.
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
use crate::archetype::Archetype;
|
||||
use crate::board::Board;
|
||||
use crate::game::GameState;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::layer::Layer;
|
||||
use crate::utils::{Direction, Player, PortalDef};
|
||||
use crate::world::World;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Builds a 3×3 board with the player at `(px, py)` and the given portals.
|
||||
fn make_board(px: i32, py: i32, portals: Vec<PortalDef>) -> Board {
|
||||
@@ -13,9 +15,9 @@ fn make_board(px: i32, py: i32, portals: Vec<PortalDef>) -> Board {
|
||||
name: "test".into(),
|
||||
width: 3,
|
||||
height: 3,
|
||||
cells: vec![(Archetype::Empty.default_glyph(), Archetype::Empty); 9],
|
||||
floor: vec![Archetype::Empty.default_glyph(); 9],
|
||||
floor_spec: None,
|
||||
layers: vec![Layer {
|
||||
cells: vec![(Glyph::transparent(), Archetype::Empty); 9],
|
||||
}],
|
||||
player: Player { x: px, y: py },
|
||||
objects: BTreeMap::new(),
|
||||
next_object_id: 1,
|
||||
@@ -31,12 +33,30 @@ fn make_board(px: i32, py: i32, portals: Vec<PortalDef>) -> Board {
|
||||
/// - `"b1"`: player at `(0, 0)`, portal `"to_b2"` at `(2, 0)` → `"b2"` / `"from_b1"`
|
||||
/// - `"b2"`: player at `(0, 0)`, portal `"from_b1"` at `(1, 1)` → `"b1"` / `"to_b2"`
|
||||
fn two_board_world() -> World {
|
||||
let b1 = make_board(0, 0, vec![
|
||||
PortalDef { name: "to_b2".into(), x: 2, y: 0, target_map: "b2".into(), target_entry: "from_b1".into() },
|
||||
]);
|
||||
let b2 = make_board(0, 0, vec![
|
||||
PortalDef { name: "from_b1".into(), x: 1, y: 1, target_map: "b1".into(), target_entry: "to_b2".into() },
|
||||
]);
|
||||
let b1 = make_board(
|
||||
0,
|
||||
0,
|
||||
vec![PortalDef {
|
||||
name: "to_b2".into(),
|
||||
x: 2,
|
||||
y: 0,
|
||||
z: 0,
|
||||
target_map: "b2".into(),
|
||||
target_entry: "from_b1".into(),
|
||||
}],
|
||||
);
|
||||
let b2 = make_board(
|
||||
0,
|
||||
0,
|
||||
vec![PortalDef {
|
||||
name: "from_b1".into(),
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 0,
|
||||
target_map: "b1".into(),
|
||||
target_entry: "to_b2".into(),
|
||||
}],
|
||||
);
|
||||
World {
|
||||
name: "test".into(),
|
||||
start: "b1".into(),
|
||||
|
||||
@@ -1,53 +1,59 @@
|
||||
use super::{layer, load_board, map};
|
||||
use crate::archetype::Archetype;
|
||||
use crate::board::Board;
|
||||
use crate::map_file::MapFile;
|
||||
use super::minimal_toml;
|
||||
|
||||
#[test]
|
||||
fn grid_wrong_row_count_returns_error() {
|
||||
// height = 3 but only 2 rows in the grid
|
||||
let toml = minimal_toml(3, 3, &["...", "..."]);
|
||||
// height = 3 but only 2 rows in the layer grid.
|
||||
let toml = map(3, 3, &[layer("...\n...", &[(".", "kind = \"empty\"")])]);
|
||||
let mf: MapFile = toml::from_str(&toml).unwrap();
|
||||
let result = Board::try_from(mf);
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap();
|
||||
assert!(msg.contains("2 rows"), "expected row count in error, got: {msg}");
|
||||
assert!(msg.contains("height = 3"), "expected declared height in error, got: {msg}");
|
||||
assert!(
|
||||
msg.contains("2 rows"),
|
||||
"expected row count in error, got: {msg}"
|
||||
);
|
||||
assert!(
|
||||
msg.contains("3 tall"),
|
||||
"expected declared height in error, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grid_wrong_row_width_returns_error() {
|
||||
// width = 4 but second row is only 3 characters
|
||||
let toml = minimal_toml(4, 2, &["....", "..."]);
|
||||
// width = 4 but second row is only 3 characters.
|
||||
let toml = map(4, 2, &[layer("....\n...", &[(".", "kind = \"empty\"")])]);
|
||||
let mf: MapFile = toml::from_str(&toml).unwrap();
|
||||
let result = Board::try_from(mf);
|
||||
assert!(result.is_err());
|
||||
let msg = result.err().unwrap();
|
||||
assert!(msg.contains("3 characters"), "expected col count in error, got: {msg}");
|
||||
assert!(msg.contains("width = 4"), "expected declared width in error, got: {msg}");
|
||||
assert!(
|
||||
msg.contains("3 characters"),
|
||||
"expected col count in error, got: {msg}"
|
||||
);
|
||||
assert!(
|
||||
msg.contains("4 wide"),
|
||||
"expected declared width in error, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_archetype_in_palette_produces_error_block() {
|
||||
// "object" is no longer a valid palette archetype; it should produce ErrorBlock.
|
||||
// Player is placed away from the O cell so it isn't cleared.
|
||||
let toml = r##"
|
||||
[map]
|
||||
name = "Test"
|
||||
width = 2
|
||||
height = 1
|
||||
player_start = [1, 0]
|
||||
|
||||
[palette]
|
||||
"O" = { archetype = "object", tile = 64, fg = "#00FFFF", bg = "#000000" }
|
||||
"." = { archetype = "empty", tile = 46, fg = "#000000", bg = "#000000" }
|
||||
|
||||
[grid]
|
||||
content = """
|
||||
O.
|
||||
"""
|
||||
"##;
|
||||
let mf: MapFile = toml::from_str(toml).unwrap();
|
||||
let board = Board::try_from(mf).unwrap();
|
||||
assert_eq!(*board.get(0, 0), (Archetype::ErrorBlock.default_glyph(), Archetype::ErrorBlock));
|
||||
fn unknown_kind_produces_error_block() {
|
||||
// A palette `kind` that is neither a meta-kind nor a known archetype name
|
||||
// becomes a visible ErrorBlock. Player placed away from the X cell.
|
||||
let toml = map(
|
||||
2,
|
||||
1,
|
||||
&[layer(
|
||||
"X@",
|
||||
&[("X", "kind = \"frobnicate\""), ("@", "kind = \"player\"")],
|
||||
)],
|
||||
);
|
||||
let board = load_board(&toml);
|
||||
assert_eq!(
|
||||
*board.get(0, 0, 0),
|
||||
(Archetype::ErrorBlock.default_glyph(), Archetype::ErrorBlock)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,79 +12,53 @@ fn load_board(toml: &str) -> Board {
|
||||
Board::try_from(mf).expect("convert to board")
|
||||
}
|
||||
|
||||
/// A 3×1 map (`empty` palette `.`) whose grid is `grid`, plus the given
|
||||
/// `[[objects]]` TOML appended. Keeps placement tests compact.
|
||||
fn map_3x1(grid: &str, objects: &str) -> String {
|
||||
format!(
|
||||
r##"
|
||||
[map]
|
||||
name = "Test"
|
||||
width = 3
|
||||
height = 1
|
||||
player_start = [2, 0]
|
||||
|
||||
[palette]
|
||||
"." = {{ archetype = "empty", tile = 46, fg = "#000000", bg = "#000000" }}
|
||||
"#" = {{ archetype = "wall", tile = 35, fg = "#808080", bg = "#606060" }}
|
||||
|
||||
[grid]
|
||||
content = """
|
||||
{grid}
|
||||
"""
|
||||
{objects}
|
||||
"##
|
||||
)
|
||||
/// Builds one `[[layers]]` block: a triple-quoted `content` grid plus an inline
|
||||
/// `palette` table. Each palette entry is `(char_key, inline-body)` where the
|
||||
/// body is the inside of the entry's `{ ... }` (e.g. `kind = "wall"`).
|
||||
fn layer(content: &str, palette: &[(&str, &str)]) -> String {
|
||||
let pal = palette
|
||||
.iter()
|
||||
.map(|(k, body)| format!("\"{k}\" = {{ {body} }}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("\n[[layers]]\ncontent = \"\"\"\n{content}\n\"\"\"\npalette = {{ {pal} }}\n")
|
||||
}
|
||||
|
||||
/// A standard `[[objects]]` block placed by palette char `ch`.
|
||||
fn obj_palette(ch: &str, extra: &str) -> String {
|
||||
format!(
|
||||
"[[objects]]\npalette = \"{ch}\"\ntile = 64\nfg = \"#00FFFF\"\nbg = \"#000000\"\n{extra}"
|
||||
)
|
||||
/// Wraps a `[map]` header of the given size around the supplied `[[layers]]`
|
||||
/// blocks (each produced by [`layer`]).
|
||||
fn map(width: usize, height: usize, layers: &[String]) -> String {
|
||||
let mut s = format!("[map]\nname = \"Test\"\nwidth = {width}\nheight = {height}\n");
|
||||
for l in layers {
|
||||
s.push_str(l);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// A 1-row map of the given `width` with a raw `player_start` TOML value
|
||||
/// (`start`, e.g. `[1, 0]` or `"@"`), grid, and objects block.
|
||||
fn player_map(width: usize, start: &str, grid: &str, objects: &str) -> String {
|
||||
format!(
|
||||
r##"
|
||||
[map]
|
||||
name = "Test"
|
||||
width = {width}
|
||||
height = 1
|
||||
player_start = {start}
|
||||
|
||||
[palette]
|
||||
"." = {{ archetype = "empty", tile = 46, fg = "#000000", bg = "#000000" }}
|
||||
"#" = {{ archetype = "wall", tile = 35, fg = "#808080", bg = "#606060" }}
|
||||
|
||||
[grid]
|
||||
content = """
|
||||
{grid}
|
||||
"""
|
||||
{objects}
|
||||
"##
|
||||
)
|
||||
}
|
||||
|
||||
/// Minimal valid TOML with a configurable grid, used as a base for dimension tests.
|
||||
fn minimal_toml(width: usize, height: usize, grid_rows: &[&str]) -> String {
|
||||
let content = grid_rows.join("\n");
|
||||
format!(
|
||||
r##"
|
||||
[map]
|
||||
name = "Test"
|
||||
width = {width}
|
||||
height = {height}
|
||||
player_start = [0, 0]
|
||||
|
||||
[palette]
|
||||
"." = {{ archetype = "empty", tile = 46, fg = "#000000", bg = "#000000" }}
|
||||
|
||||
[grid]
|
||||
content = """
|
||||
{content}
|
||||
"""
|
||||
"##
|
||||
/// A 3×1 single-layer map: an `empty`/`wall` palette plus one `object` entry
|
||||
/// placed by char `ch` (cyan `@` glyph), with `extra` appended to its body. The
|
||||
/// player is placed at the far-right cell via a second char where room allows;
|
||||
/// callers that need the player elsewhere build the map directly.
|
||||
fn map_3x1_object(grid: &str, ch: &str, extra: &str) -> String {
|
||||
let body = if extra.is_empty() {
|
||||
"kind = \"object\", tile = 64, fg = \"#00FFFF\", bg = \"#000000\"".to_string()
|
||||
} else {
|
||||
format!("kind = \"object\", tile = 64, fg = \"#00FFFF\", bg = \"#000000\", {extra}")
|
||||
};
|
||||
map(
|
||||
3,
|
||||
1,
|
||||
&[layer(
|
||||
grid,
|
||||
&[
|
||||
(" ", "kind = \"empty\""),
|
||||
(".", "kind = \"empty\""),
|
||||
(
|
||||
"#",
|
||||
"kind = \"wall\", tile = 35, fg = \"#808080\", bg = \"#606060\"",
|
||||
),
|
||||
("@", "kind = \"player\""),
|
||||
(ch, &body),
|
||||
],
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
use super::{layer, load_board, map, map_3x1_object};
|
||||
use crate::archetype::Archetype;
|
||||
use super::{load_board, map_3x1, obj_palette};
|
||||
|
||||
/// Palette shorthands.
|
||||
const EMPTY: (&str, &str) = (".", "kind = \"empty\"");
|
||||
const PLAYER: (&str, &str) = ("@", "kind = \"player\"");
|
||||
const WALL: (&str, &str) = (
|
||||
"#",
|
||||
"kind = \"wall\", tile = 35, fg = \"#808080\", bg = \"#606060\"",
|
||||
);
|
||||
|
||||
/// An object palette entry body with the given `extra` fields appended.
|
||||
fn obj(extra: &str) -> String {
|
||||
let base = "kind = \"object\", tile = 64, fg = \"#00FFFF\", bg = \"#000000\"";
|
||||
if extra.is_empty() {
|
||||
base.to_string()
|
||||
} else {
|
||||
format!("{base}, {extra}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_name_clears_second_but_keeps_both_objects() {
|
||||
// Two objects share the name "gate": first keeps it, second is cleared.
|
||||
// Both objects still appear on the board; the map is flagged invalid.
|
||||
let objs = format!(
|
||||
"{}\n{}",
|
||||
"[[objects]]\nx = 0\ny = 0\ntile = 64\nfg = \"#ffffff\"\nbg = \"#000000\"\nname = \"gate\"\n",
|
||||
"[[objects]]\nx = 1\ny = 0\ntile = 64\nfg = \"#ffffff\"\nbg = \"#000000\"\nname = \"gate\"\nsolid = false\n",
|
||||
);
|
||||
let board = load_board(&map_3x1("...", &objs));
|
||||
// Two object entries share the name "gate": first keeps it, second is cleared.
|
||||
let board = load_board(&map(
|
||||
3,
|
||||
1,
|
||||
&[layer(
|
||||
"GH@",
|
||||
&[
|
||||
("G", &obj("name = \"gate\"")),
|
||||
("H", &obj("name = \"gate\", solid = false")),
|
||||
PLAYER,
|
||||
],
|
||||
)],
|
||||
));
|
||||
assert_eq!(board.objects.len(), 2, "both objects survive");
|
||||
// First object (id 1) keeps the name; second (id 2) is cleared.
|
||||
assert_eq!(board.objects[&1].name.as_deref(), Some("gate"));
|
||||
assert_eq!(board.objects[&2].name, None);
|
||||
assert!(!board.is_valid(), "duplicate name is a nonfatal load error");
|
||||
@@ -20,80 +42,76 @@ fn duplicate_name_clears_second_but_keeps_both_objects() {
|
||||
|
||||
#[test]
|
||||
fn palette_placement_puts_object_on_empty_floor() {
|
||||
// `G` appears once, isn't a palette key → object lands there, cell is Empty.
|
||||
let board = load_board(&map_3x1("G..", &obj_palette("G", "")));
|
||||
// `G` lands an object; its cell stays Empty (transparent).
|
||||
let board = load_board(&map_3x1_object("G.@", "G", ""));
|
||||
assert_eq!(board.objects.len(), 1);
|
||||
assert_eq!((board.objects[&1].x, board.objects[&1].y), (0, 0));
|
||||
assert_eq!(board.get(0, 0).1, Archetype::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_char_absent_from_grid_drops_object() {
|
||||
let board = load_board(&map_3x1("...", &obj_palette("G", "")));
|
||||
assert!(board.objects.is_empty());
|
||||
assert!(!board.is_valid());
|
||||
assert_eq!(board.get(0, 0, 0).1, Archetype::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_char_appearing_twice_spawns_two_objects() {
|
||||
// `G` appears at (0,0) and (1,0); player is at (2,0) so neither G conflicts.
|
||||
// Two independent ObjectDefs are spawned.
|
||||
let board = load_board(&map_3x1("GG.", &obj_palette("G", "")));
|
||||
// `G` appears at (0,0) and (1,0); two independent ObjectDefs are spawned.
|
||||
let board = load_board(&map_3x1_object("GG@", "G", "solid = false"));
|
||||
assert_eq!(board.objects.len(), 2);
|
||||
// IDs are assigned in reading order (left-to-right).
|
||||
assert_eq!((board.objects[&1].x, board.objects[&1].y), (0, 0));
|
||||
assert_eq!((board.objects[&2].x, board.objects[&2].y), (1, 0));
|
||||
// Both cells load as Empty in the grid.
|
||||
assert_eq!(board.get(0, 0).1, Archetype::Empty);
|
||||
assert_eq!(board.get(1, 0).1, Archetype::Empty);
|
||||
assert!(board.is_valid(), "multiple occurrences are not an error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_char_multi_occurrence_only_first_keeps_name() {
|
||||
// Two occurrences of a named entry (solid = false to avoid player conflict at (2,0)).
|
||||
// First instance gets the name; second is anonymous.
|
||||
let entry = obj_palette("G", "solid = false\nname = \"guard\"\n");
|
||||
let board = load_board(&map_3x1("GGG", &entry));
|
||||
// Three occurrences of a named entry: the first keeps the name, the rest don't.
|
||||
let board = load_board(&map(
|
||||
4,
|
||||
1,
|
||||
&[layer(
|
||||
"GGG@",
|
||||
&[("G", &obj("solid = false, name = \"guard\"")), PLAYER],
|
||||
)],
|
||||
));
|
||||
assert_eq!(board.objects.len(), 3);
|
||||
assert_eq!(board.objects[&1].name.as_deref(), Some("guard"));
|
||||
assert_eq!(board.objects[&2].name, None);
|
||||
assert_eq!(board.objects[&3].name, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_char_reused_by_two_objects_keeps_first() {
|
||||
// Two objects claim `G`: the first keeps it, the later one is dropped.
|
||||
let two = format!("{}\n{}", obj_palette("G", ""), obj_palette("G", ""));
|
||||
let board = load_board(&map_3x1("G..", &two));
|
||||
assert_eq!(board.objects.len(), 1);
|
||||
assert!(!board.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_char_colliding_with_palette_key_drops_object() {
|
||||
// `#` is a terrain palette key, so it can't be an object placeholder.
|
||||
let board = load_board(&map_3x1("#..", &obj_palette("#", "")));
|
||||
assert!(board.objects.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solid_object_on_wall_is_dropped_but_non_solid_is_kept() {
|
||||
// Solid object at the wall cell (0,0): dropped for conflicting with the wall.
|
||||
let solid = "[[objects]]\nx = 0\ny = 0\ntile = 64\nfg = \"#00FFFF\"\nbg = \"#000000\"\n";
|
||||
let board = load_board(&map_3x1("#..", solid));
|
||||
assert!(board.objects.is_empty());
|
||||
// Solid object stacked on a wall (different layer): dropped for the conflict.
|
||||
let solid = map(
|
||||
3,
|
||||
1,
|
||||
&[
|
||||
layer("#.@", &[WALL, EMPTY, PLAYER]),
|
||||
layer("O..", &[("O", &obj("")), EMPTY]),
|
||||
],
|
||||
);
|
||||
assert!(load_board(&solid).objects.is_empty());
|
||||
|
||||
// Same placement but non-solid: kept (it doesn't claim the cell's solidity).
|
||||
let nonsolid = format!("{solid}solid = false\n");
|
||||
let board = load_board(&map_3x1("#..", &nonsolid));
|
||||
assert_eq!(board.objects.len(), 1);
|
||||
let nonsolid = map(
|
||||
3,
|
||||
1,
|
||||
&[
|
||||
layer("#.@", &[WALL, EMPTY, PLAYER]),
|
||||
layer("O..", &[("O", &obj("solid = false")), EMPTY]),
|
||||
],
|
||||
);
|
||||
assert_eq!(load_board(&nonsolid).objects.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_solid_object_on_a_cell_is_dropped() {
|
||||
let obj = "[[objects]]\nx = 1\ny = 0\ntile = 64\nfg = \"#00FFFF\"\nbg = \"#000000\"\n";
|
||||
let board = load_board(&map_3x1("...", &format!("{obj}{obj}")));
|
||||
// Two solid objects on the same cell across layers: the upper one is dropped.
|
||||
let board = load_board(&map(
|
||||
3,
|
||||
1,
|
||||
&[
|
||||
layer("@O.", &[PLAYER, ("O", &obj("")), EMPTY]),
|
||||
layer(".O.", &[EMPTY, ("O", &obj(""))]),
|
||||
],
|
||||
));
|
||||
assert_eq!(board.objects.len(), 1);
|
||||
assert!(!board.is_valid());
|
||||
}
|
||||
|
||||
@@ -1,70 +1,81 @@
|
||||
use super::{layer, load_board, map};
|
||||
use crate::archetype::Archetype;
|
||||
use super::{load_board, player_map};
|
||||
|
||||
#[test]
|
||||
fn player_coords_place_the_player() {
|
||||
let b = load_board(&player_map(3, "[1, 0]", "...", ""));
|
||||
assert_eq!((b.player.x, b.player.y), (1, 0));
|
||||
assert!(b.is_valid());
|
||||
}
|
||||
/// Palette shorthands shared by these tests.
|
||||
const EMPTY: (&str, &str) = (".", "kind = \"empty\"");
|
||||
const PLAYER: (&str, &str) = ("@", "kind = \"player\"");
|
||||
const WALL: (&str, &str) = (
|
||||
"#",
|
||||
"kind = \"wall\", tile = 35, fg = \"#808080\", bg = \"#606060\"",
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn player_char_places_on_empty_floor() {
|
||||
let b = load_board(&player_map(3, "\"@\"", ".@.", ""));
|
||||
let b = load_board(&map(3, 1, &[layer(".@.", &[EMPTY, PLAYER])]));
|
||||
assert_eq!((b.player.x, b.player.y), (1, 0));
|
||||
assert_eq!(b.get(1, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.get(0, 1, 0).1, Archetype::Empty);
|
||||
assert!(b.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_wins_solid_terrain_silently() {
|
||||
// Player coords land on a wall: the wall is cleared, no error reported.
|
||||
let b = load_board(&player_map(3, "[0, 0]", "#..", ""));
|
||||
// Wall on layer 0, player on layer 1 at the same cell: the wall is cleared,
|
||||
// no error reported.
|
||||
let b = load_board(&map(
|
||||
3,
|
||||
1,
|
||||
&[layer("#..", &[WALL, EMPTY]), layer("@..", &[PLAYER, EMPTY])],
|
||||
));
|
||||
assert_eq!((b.player.x, b.player.y), (0, 0));
|
||||
assert_eq!(b.get(0, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.get(0, 0, 0).1, Archetype::Empty); // wall cleared on layer 0
|
||||
assert!(b.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_wins_against_a_solid_object_silently() {
|
||||
// A solid object on the player's cell is dropped, silently (player wins).
|
||||
let obj = "[[objects]]\nx = 1\ny = 0\ntile = 64\nfg = \"#00FFFF\"\nbg = \"#000000\"\n";
|
||||
let b = load_board(&player_map(3, "[1, 0]", "...", obj));
|
||||
// A solid object on the player's cell (different layer) is dropped silently.
|
||||
let b = load_board(&map(
|
||||
3,
|
||||
1,
|
||||
&[
|
||||
layer(
|
||||
".O.",
|
||||
&[
|
||||
EMPTY,
|
||||
(
|
||||
"O",
|
||||
"kind = \"object\", tile = 64, fg = \"#00FFFF\", bg = \"#000000\"",
|
||||
),
|
||||
],
|
||||
),
|
||||
layer(".@.", &[EMPTY, PLAYER]),
|
||||
],
|
||||
));
|
||||
assert_eq!((b.player.x, b.player.y), (1, 0));
|
||||
assert!(b.objects.is_empty());
|
||||
assert!(b.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_char_is_reserved_from_objects() {
|
||||
// An object trying to use '@' is dropped; the player is still placed.
|
||||
let obj = "[[objects]]\npalette = \"@\"\ntile = 64\nfg = \"#00FFFF\"\nbg = \"#000000\"\n";
|
||||
let b = load_board(&player_map(3, "\"@\"", "@..", obj));
|
||||
assert_eq!((b.player.x, b.player.y), (0, 0));
|
||||
assert!(b.objects.is_empty());
|
||||
assert!(!b.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_char_appearing_twice_uses_first() {
|
||||
let b = load_board(&player_map(3, "\"@\"", "@.@", ""));
|
||||
let b = load_board(&map(3, 1, &[layer("@.@", &[EMPTY, PLAYER])]));
|
||||
assert_eq!((b.player.x, b.player.y), (0, 0));
|
||||
assert!(!b.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_char_missing_falls_back_to_origin() {
|
||||
let b = load_board(&player_map(3, "\"@\"", "...", ""));
|
||||
let b = load_board(&map(3, 1, &[layer("...", &[EMPTY])]));
|
||||
assert_eq!((b.player.x, b.player.y), (0, 0));
|
||||
assert!(!b.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_fallback_to_origin_clears_solid_terrain() {
|
||||
// The '@' char is absent, so the player falls back to (0, 0) — which holds a
|
||||
// wall. The player wins its cell: the wall is cleared so one-solid-per-cell
|
||||
// holds. The fallback is still reported.
|
||||
let b = load_board(&player_map(3, "\"@\"", "#..", ""));
|
||||
// No player cell, so the player falls back to (0, 0) — which holds a wall. The
|
||||
// player wins its cell: the wall is cleared. The fallback is still reported.
|
||||
let b = load_board(&map(3, 1, &[layer("#..", &[WALL, EMPTY])]));
|
||||
assert_eq!((b.player.x, b.player.y), (0, 0));
|
||||
assert_eq!(b.get(0, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.get(0, 0, 0).1, Archetype::Empty);
|
||||
assert!(!b.is_valid());
|
||||
}
|
||||
|
||||
@@ -1,47 +1,56 @@
|
||||
use super::{load_board, map_3x1};
|
||||
use super::{layer, load_board, map};
|
||||
|
||||
fn portal_toml(portals: &str) -> String {
|
||||
format!(
|
||||
"{}{}",
|
||||
map_3x1("...", ""),
|
||||
portals
|
||||
)
|
||||
}
|
||||
const EMPTY: (&str, &str) = (".", "kind = \"empty\"");
|
||||
const PLAYER: (&str, &str) = ("@", "kind = \"player\"");
|
||||
|
||||
#[test]
|
||||
fn portal_duplicate_name_drops_second() {
|
||||
let toml = portal_toml(
|
||||
"[[portals]]\nname = \"a\"\nx = 0\ny = 0\ntarget_map = \"x\"\ntarget_entry = \"b\"\n\
|
||||
[[portals]]\nname = \"a\"\nx = 1\ny = 0\ntarget_map = \"x\"\ntarget_entry = \"c\"\n",
|
||||
// Two portal cells share the name "a": the second is dropped.
|
||||
let board = load_board(&map(
|
||||
3,
|
||||
1,
|
||||
&[layer(
|
||||
"12@",
|
||||
&[
|
||||
(
|
||||
"1",
|
||||
"kind = \"portal\", name = \"a\", target_map = \"x\", target_entry = \"b\"",
|
||||
),
|
||||
(
|
||||
"2",
|
||||
"kind = \"portal\", name = \"a\", target_map = \"x\", target_entry = \"c\"",
|
||||
),
|
||||
PLAYER,
|
||||
],
|
||||
)],
|
||||
));
|
||||
assert_eq!(
|
||||
board.portals.len(),
|
||||
1,
|
||||
"second portal with duplicate name should be dropped"
|
||||
);
|
||||
let board = load_board(&toml);
|
||||
assert_eq!(board.portals.len(), 1, "second portal with duplicate name should be dropped");
|
||||
assert_eq!(board.portals[0].x, 0);
|
||||
assert!(!board.is_valid(), "duplicate name should be a load error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portal_palette_char_places_portal_at_grid_position() {
|
||||
let toml = format!(
|
||||
"{}{}",
|
||||
map_3x1("1..", ""),
|
||||
"[[portals]]\nname = \"p\"\npalette = \"1\"\ntarget_map = \"x\"\ntarget_entry = \"e\"\n"
|
||||
);
|
||||
let board = load_board(&toml);
|
||||
let board = load_board(&map(
|
||||
3,
|
||||
1,
|
||||
&[layer(
|
||||
"1.@",
|
||||
&[
|
||||
(
|
||||
"1",
|
||||
"kind = \"portal\", name = \"p\", target_map = \"x\", target_entry = \"e\"",
|
||||
),
|
||||
EMPTY,
|
||||
PLAYER,
|
||||
],
|
||||
)],
|
||||
));
|
||||
assert_eq!(board.portals.len(), 1);
|
||||
assert_eq!(board.portals[0].x, 0);
|
||||
assert_eq!(board.portals[0].y, 0);
|
||||
assert_eq!((board.portals[0].x, board.portals[0].y), (0, 0));
|
||||
assert_eq!(board.portals[0].name, "p");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portal_palette_char_colliding_with_object_is_dropped() {
|
||||
let toml = format!(
|
||||
"{}{}{}",
|
||||
map_3x1("1..", ""),
|
||||
"[[objects]]\npalette = \"1\"\ntile = 64\nfg = \"#ffffff\"\nbg = \"#000000\"\n",
|
||||
"[[portals]]\nname = \"p\"\npalette = \"1\"\ntarget_map = \"x\"\ntarget_entry = \"e\"\n"
|
||||
);
|
||||
let board = load_board(&toml);
|
||||
assert_eq!(board.portals.len(), 0, "portal whose palette char is already an object char should be dropped");
|
||||
}
|
||||
|
||||
@@ -1,203 +1,147 @@
|
||||
use color::Rgba8;
|
||||
use crate::board::Board;
|
||||
use super::{layer, load_board, map};
|
||||
use crate::glyph::Glyph;
|
||||
use crate::map_file::{MapFile, parse_color};
|
||||
use super::{load_board, map_3x1, obj_palette};
|
||||
use color::Rgba8;
|
||||
|
||||
const EMPTY: (&str, &str) = (".", "kind = \"empty\"");
|
||||
const PLAYER: (&str, &str) = ("@", "kind = \"player\"");
|
||||
|
||||
/// An object palette entry body with the given `extra` fields appended.
|
||||
fn obj(extra: &str) -> String {
|
||||
let base = "kind = \"object\", tile = 64, fg = \"#00FFFF\", bg = \"#000000\"";
|
||||
if extra.is_empty() {
|
||||
base.to_string()
|
||||
} else {
|
||||
format!("{base}, {extra}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Round-trips a board through save→load and returns the reloaded board.
|
||||
fn round_trip(toml: &str) -> crate::board::Board {
|
||||
let board = load_board(toml);
|
||||
let toml_out = toml::to_string_pretty(&MapFile::from(&board)).unwrap();
|
||||
load_board(&toml_out)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_glyph_round_trips_through_toml() {
|
||||
// Objects must survive a save→load cycle with their glyph intact.
|
||||
let toml = r##"
|
||||
[map]
|
||||
name = "Test"
|
||||
width = 3
|
||||
height = 3
|
||||
player_start = [0, 0]
|
||||
|
||||
[palette]
|
||||
"." = { archetype = "empty", tile = 46, fg = "#000000", bg = "#000000" }
|
||||
|
||||
[grid]
|
||||
content = """
|
||||
...
|
||||
...
|
||||
...
|
||||
"""
|
||||
|
||||
[[objects]]
|
||||
x = 1
|
||||
y = 1
|
||||
tile = 64
|
||||
fg = "#00FFFF"
|
||||
bg = "#000000"
|
||||
"##;
|
||||
let mf: MapFile = toml::from_str(toml).unwrap();
|
||||
let board = Board::try_from(mf).unwrap();
|
||||
let toml = map(3, 1, &[layer("@O.", &[PLAYER, ("O", &obj("")), EMPTY])]);
|
||||
let board = load_board(&toml);
|
||||
assert_eq!(board.objects.len(), 1);
|
||||
let obj = &board.objects[&1];
|
||||
assert_eq!(obj.x, 1);
|
||||
assert_eq!(obj.y, 1);
|
||||
assert_eq!(obj.glyph.tile, 64);
|
||||
assert_eq!(obj.glyph.fg, Rgba8 { r: 0x00, g: 0xFF, b: 0xFF, a: 255 });
|
||||
assert_eq!(obj.glyph.bg, Rgba8 { r: 0, g: 0, b: 0, a: 255 });
|
||||
let obj0 = &board.objects[&1];
|
||||
assert_eq!((obj0.x, obj0.y), (1, 0));
|
||||
assert_eq!(obj0.glyph.tile, 64);
|
||||
assert_eq!(
|
||||
obj0.glyph.fg,
|
||||
Rgba8 {
|
||||
r: 0x00,
|
||||
g: 0xFF,
|
||||
b: 0xFF,
|
||||
a: 255
|
||||
}
|
||||
);
|
||||
|
||||
// Save back to TOML and reload; glyph must survive.
|
||||
let toml_out = toml::to_string_pretty(&MapFile::from(&board)).unwrap();
|
||||
let mf2: MapFile = toml::from_str(&toml_out).unwrap();
|
||||
let board2 = Board::try_from(mf2).unwrap();
|
||||
let board2 = round_trip(&toml);
|
||||
let obj2 = &board2.objects[&1];
|
||||
assert_eq!(obj2.glyph.tile, obj.glyph.tile);
|
||||
assert_eq!(obj2.glyph.fg, obj.glyph.fg);
|
||||
assert_eq!(obj2.glyph.bg, obj.glyph.bg);
|
||||
assert_eq!(obj2.glyph.tile, obj0.glyph.tile);
|
||||
assert_eq!(obj2.glyph.fg, obj0.glyph.fg);
|
||||
assert_eq!(obj2.glyph.bg, obj0.glyph.bg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_tags_round_trip_through_toml() {
|
||||
// Tags declared in the map file must survive a save→load cycle, sorted.
|
||||
let toml = r##"
|
||||
[map]
|
||||
name = "Test"
|
||||
width = 3
|
||||
height = 3
|
||||
player_start = [0, 0]
|
||||
let toml = map(
|
||||
3,
|
||||
1,
|
||||
&[layer(
|
||||
"@O.",
|
||||
&[PLAYER, ("O", &obj("tags = [\"enemy\", \"boss\"]")), EMPTY],
|
||||
)],
|
||||
);
|
||||
let board = load_board(&toml);
|
||||
let obj0 = &board.objects[&1];
|
||||
assert!(obj0.tags.contains("enemy") && obj0.tags.contains("boss"));
|
||||
assert_eq!(obj0.tags.len(), 2);
|
||||
|
||||
[palette]
|
||||
"." = { archetype = "empty", tile = 46, fg = "#000000", bg = "#000000" }
|
||||
|
||||
[grid]
|
||||
content = """
|
||||
...
|
||||
...
|
||||
...
|
||||
"""
|
||||
|
||||
[[objects]]
|
||||
x = 1
|
||||
y = 1
|
||||
tile = 64
|
||||
fg = "#ffffff"
|
||||
bg = "#000000"
|
||||
tags = ["enemy", "boss"]
|
||||
"##;
|
||||
let mf: MapFile = toml::from_str(toml).unwrap();
|
||||
let board = Board::try_from(mf).unwrap();
|
||||
let obj = &board.objects[&1];
|
||||
assert!(obj.tags.contains("enemy"));
|
||||
assert!(obj.tags.contains("boss"));
|
||||
assert_eq!(obj.tags.len(), 2);
|
||||
|
||||
// Save and reload; tags must survive, and be sorted alphabetically.
|
||||
// Saved tags must be sorted alphabetically (boss before enemy).
|
||||
let toml_out = toml::to_string_pretty(&MapFile::from(&board)).unwrap();
|
||||
// Both tags must appear; "boss" must come before "enemy" (sorted).
|
||||
let boss_pos = toml_out.find("\"boss\"").expect("boss in TOML");
|
||||
let enemy_pos = toml_out.find("\"enemy\"").expect("enemy in TOML");
|
||||
assert!(boss_pos < enemy_pos, "tags must be sorted: boss before enemy");
|
||||
let mf2: MapFile = toml::from_str(&toml_out).unwrap();
|
||||
let board2 = Board::try_from(mf2).unwrap();
|
||||
let obj2 = &board2.objects[&1];
|
||||
assert_eq!(obj2.tags, obj.tags);
|
||||
let boss = toml_out.find("\"boss\"").expect("boss in TOML");
|
||||
let enemy = toml_out.find("\"enemy\"").expect("enemy in TOML");
|
||||
assert!(boss < enemy, "tags must be sorted: boss before enemy");
|
||||
|
||||
let board2 = load_board(&toml_out);
|
||||
assert_eq!(board2.objects[&1].tags, obj0.tags);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_empty_tags_omitted_from_toml() {
|
||||
// An object with no tags must not emit a `tags` key in TOML.
|
||||
let toml = r##"
|
||||
[map]
|
||||
name = "Test"
|
||||
width = 1
|
||||
height = 1
|
||||
player_start = [0, 0]
|
||||
|
||||
[palette]
|
||||
"." = { archetype = "empty", tile = 46, fg = "#000000", bg = "#000000" }
|
||||
|
||||
[grid]
|
||||
content = """
|
||||
.
|
||||
"""
|
||||
|
||||
[[objects]]
|
||||
x = 0
|
||||
y = 0
|
||||
tile = 64
|
||||
fg = "#ffffff"
|
||||
bg = "#000000"
|
||||
"##;
|
||||
let board = load_board(toml);
|
||||
let toml = map(3, 1, &[layer("@O.", &[PLAYER, ("O", &obj("")), EMPTY])]);
|
||||
let board = load_board(&toml);
|
||||
let toml_out = toml::to_string_pretty(&MapFile::from(&board)).unwrap();
|
||||
assert!(!toml_out.contains("tags"), "empty tags must not appear in TOML output");
|
||||
assert!(
|
||||
!toml_out.contains("tags"),
|
||||
"empty tags must not appear in TOML output"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_name_round_trips_through_toml() {
|
||||
// A named object must survive a save→load cycle with its name intact.
|
||||
let board = load_board(&map_3x1("G..", &obj_palette("G", "name = \"beacon\"\n")));
|
||||
let toml = map(
|
||||
3,
|
||||
1,
|
||||
&[layer(
|
||||
"@O.",
|
||||
&[PLAYER, ("O", &obj("name = \"beacon\"")), EMPTY],
|
||||
)],
|
||||
);
|
||||
let board = load_board(&toml);
|
||||
assert_eq!(board.objects[&1].name.as_deref(), Some("beacon"));
|
||||
|
||||
let toml_out = toml::to_string_pretty(&MapFile::from(&board)).unwrap();
|
||||
assert!(toml_out.contains("\"beacon\""), "name must appear in saved TOML");
|
||||
assert!(
|
||||
toml_out.contains("\"beacon\""),
|
||||
"name must appear in saved TOML"
|
||||
);
|
||||
let board2 = load_board(&toml_out);
|
||||
assert_eq!(board2.objects[&1].name.as_deref(), Some("beacon"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unnamed_object_omits_name_from_toml() {
|
||||
// An unnamed object must not emit a `name` key in TOML; check via the parsed
|
||||
// value so the map header's `name = "Test"` doesn't produce a false positive.
|
||||
let board = load_board(&map_3x1("G..", &obj_palette("G", "")));
|
||||
let toml_out = toml::to_string_pretty(&MapFile::from(&board)).unwrap();
|
||||
let val: toml::Value = toml_out.parse().unwrap();
|
||||
let objects = val["objects"].as_array().unwrap();
|
||||
assert!(
|
||||
!objects[0].as_table().unwrap().contains_key("name"),
|
||||
"unnamed object must not emit name key"
|
||||
fn unnamed_object_name_stays_none_through_toml() {
|
||||
let toml = map(3, 1, &[layer("@O.", &[PLAYER, ("O", &obj("")), EMPTY])]);
|
||||
let board2 = round_trip(&toml);
|
||||
assert_eq!(
|
||||
board2.objects[&1].name, None,
|
||||
"unnamed object must round-trip as None"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floor_spec_round_trips_through_toml() {
|
||||
// A 2×2 board with a grid floor (a generator and a fixed glyph) must survive
|
||||
// save→load with its `[floor]` declaration intact, and the reloaded board's
|
||||
// computed floor must place the fixed glyph back at its declared cell.
|
||||
let toml = r##"
|
||||
[map]
|
||||
name = "Test"
|
||||
width = 2
|
||||
height = 2
|
||||
player_start = [0, 0]
|
||||
|
||||
[palette]
|
||||
"." = { archetype = "empty", tile = 46, fg = "#000000", bg = "#000000" }
|
||||
|
||||
[grid]
|
||||
content = """
|
||||
..
|
||||
..
|
||||
"""
|
||||
|
||||
[floor]
|
||||
content = """
|
||||
g.
|
||||
.g
|
||||
"""
|
||||
|
||||
[floor.palette]
|
||||
"g" = "grass"
|
||||
"." = { tile = "#", fg = "#010203", bg = "#040506" }
|
||||
"##;
|
||||
let board = load_board(toml);
|
||||
assert!(board.floor_spec.is_some());
|
||||
fn fixed_floor_glyph_round_trips_through_toml() {
|
||||
// A fixed floor glyph (visual-only Empty cell) must survive save→load.
|
||||
let toml = map(
|
||||
3,
|
||||
1,
|
||||
&[layer(
|
||||
"@F.",
|
||||
&[
|
||||
PLAYER,
|
||||
(
|
||||
"F",
|
||||
"kind = \"floor\", tile = \"#\", fg = \"#010203\", bg = \"#040506\"",
|
||||
),
|
||||
EMPTY,
|
||||
],
|
||||
)],
|
||||
);
|
||||
let fixed = Glyph {
|
||||
tile: '#' as u32,
|
||||
fg: parse_color("#010203"),
|
||||
bg: parse_color("#040506"),
|
||||
};
|
||||
// (1,0) is the fixed `.` floor glyph (the cell archetype is Empty).
|
||||
let board = load_board(&toml);
|
||||
assert_eq!(board.glyph_at(1, 0), fixed);
|
||||
|
||||
// Save back to TOML and reload; the floor declaration must be preserved.
|
||||
let toml_out = toml::to_string_pretty(&MapFile::from(&board)).unwrap();
|
||||
let board2 = load_board(&toml_out);
|
||||
assert!(board2.floor_spec.is_some());
|
||||
let board2 = round_trip(&toml);
|
||||
assert_eq!(board2.glyph_at(1, 0), fixed);
|
||||
}
|
||||
|
||||
@@ -5,18 +5,23 @@ mod map_file;
|
||||
mod movement;
|
||||
mod scripting;
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use crate::archetype::Archetype;
|
||||
use crate::board::Board;
|
||||
use crate::game::GameState;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::layer::Layer;
|
||||
use crate::object_def::ObjectDef;
|
||||
use crate::utils::Player;
|
||||
use crate::game::GameState;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
/// Builds a 1×1 board with a single object that optionally references a script,
|
||||
/// and returns both the board and the `(name, source)` entries as a script map.
|
||||
///
|
||||
/// Pass the returned scripts to [`GameState::with_scripts`] so they are compiled.
|
||||
fn board_with_object(object_script: Option<&str>, scripts: &[(&str, &str)]) -> (Board, HashMap<String, String>) {
|
||||
fn board_with_object(
|
||||
object_script: Option<&str>,
|
||||
scripts: &[(&str, &str)],
|
||||
) -> (Board, HashMap<String, String>) {
|
||||
let mut object = ObjectDef::new(0, 0);
|
||||
object.id = 1;
|
||||
object.script_name = object_script.map(str::to_string);
|
||||
@@ -24,9 +29,9 @@ fn board_with_object(object_script: Option<&str>, scripts: &[(&str, &str)]) -> (
|
||||
name: "test".into(),
|
||||
width: 1,
|
||||
height: 1,
|
||||
cells: vec![(Archetype::Empty.default_glyph(), Archetype::Empty)],
|
||||
floor: vec![Archetype::Empty.default_glyph()],
|
||||
floor_spec: None,
|
||||
layers: vec![Layer {
|
||||
cells: vec![(Glyph::transparent(), Archetype::Empty)],
|
||||
}],
|
||||
player: Player { x: 0, y: 0 },
|
||||
objects: BTreeMap::from([(1, object)]),
|
||||
next_object_id: 2,
|
||||
@@ -41,7 +46,10 @@ fn board_with_object(object_script: Option<&str>, scripts: &[(&str, &str)]) -> (
|
||||
/// Converts a `(name, source)` slice into a `HashMap` suitable for
|
||||
/// [`GameState::with_scripts`].
|
||||
fn scripts_from(pairs: &[(&str, &str)]) -> HashMap<String, String> {
|
||||
pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
|
||||
pairs
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns an `ObjectDef` at `(x, y)` bound to the named script.
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
use color::Rgba8;
|
||||
use crate::archetype::Archetype;
|
||||
use crate::board::tests::{crate_at, open_board, stamp, wall_at};
|
||||
use crate::board::tests::{add_floor, crate_at, open_board, stamp, wall_at};
|
||||
use crate::game::GameState;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::object_def::ObjectDef;
|
||||
use crate::utils::Direction;
|
||||
use color::Rgba8;
|
||||
|
||||
#[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.
|
||||
// The cell a crate is pushed off of becomes transparent and glyph_at shows the
|
||||
// floor on the layer beneath. After the push the player is at (1,0); check the
|
||||
// cell the player vacated (0,0) — it must reveal the floor glyph, not 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 },
|
||||
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];
|
||||
add_floor(&mut board, floor_glyph); // floor below; terrain becomes layer 1
|
||||
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.get(1, 2, 0).1, Archetype::Crate);
|
||||
assert_eq!(b.get(1, 1, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.glyph_at(0, 0), floor_glyph);
|
||||
}
|
||||
|
||||
@@ -35,8 +45,8 @@ fn player_pushes_single_crate() {
|
||||
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(0, 1, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.get(0, 2, 0).1, Archetype::Crate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -48,8 +58,8 @@ fn push_blocked_by_wall_moves_nothing() {
|
||||
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::Wall);
|
||||
assert_eq!(b.get(0, 1, 0).1, Archetype::Crate);
|
||||
assert_eq!(b.get(0, 2, 0).1, Archetype::Wall);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -61,7 +71,7 @@ fn push_blocked_by_edge_moves_nothing() {
|
||||
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(0, 1, 0).1, Archetype::Crate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -73,9 +83,9 @@ fn cascade_pushes_two_crates() {
|
||||
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);
|
||||
assert_eq!(b.get(0, 1, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.get(0, 2, 0).1, Archetype::Crate);
|
||||
assert_eq!(b.get(0, 3, 0).1, Archetype::Crate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -88,8 +98,8 @@ fn cascade_blocked_by_wall_moves_nothing() {
|
||||
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);
|
||||
assert_eq!(b.get(0, 1, 0).1, Archetype::Crate);
|
||||
assert_eq!(b.get(0, 2, 0).1, Archetype::Crate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -114,8 +124,8 @@ fn hcrate_pushes_east_but_not_north() {
|
||||
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);
|
||||
assert_eq!(b.get(0, 1, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.get(0, 2, 0).1, Archetype::HCrate);
|
||||
drop(b);
|
||||
|
||||
// Pushing north into an HCrate: blocked, nothing moves.
|
||||
@@ -125,7 +135,7 @@ fn hcrate_pushes_east_but_not_north() {
|
||||
game.try_move(Direction::North);
|
||||
let b = game.board();
|
||||
assert_eq!((b.player.x, b.player.y), (0, 3));
|
||||
assert_eq!(b.get(0, 2).1, Archetype::HCrate);
|
||||
assert_eq!(b.get(0, 0, 2).1, Archetype::HCrate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -137,8 +147,8 @@ fn vcrate_pushes_north_but_not_east() {
|
||||
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);
|
||||
assert_eq!(b.get(0, 0, 2).1, Archetype::Empty);
|
||||
assert_eq!(b.get(0, 0, 1).1, Archetype::VCrate);
|
||||
drop(b);
|
||||
|
||||
// Pushing east into a VCrate: blocked, nothing moves.
|
||||
@@ -148,5 +158,5 @@ fn vcrate_pushes_north_but_not_east() {
|
||||
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::VCrate);
|
||||
assert_eq!(b.get(0, 1, 0).1, Archetype::VCrate);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::time::Duration;
|
||||
use super::{board_with_object, log_texts, scripted_object, scripts_from};
|
||||
use crate::board::tests::open_board;
|
||||
use crate::game::{GameState, ScrollLine};
|
||||
use crate::utils::Direction;
|
||||
use super::{board_with_object, scripted_object, log_texts, scripts_from};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn init_runs_only_on_run_init_not_at_construction() {
|
||||
@@ -44,10 +44,7 @@ fn missing_hooks_and_no_script_are_noops() {
|
||||
assert!(game.log.is_empty());
|
||||
|
||||
// Script defines neither init nor tick: also a no-op.
|
||||
let (board, scripts) = board_with_object(
|
||||
Some("e"),
|
||||
&[("e", "fn other() { log(\"x\"); }")],
|
||||
);
|
||||
let (board, scripts) = board_with_object(Some("e"), &[("e", "fn other() { log(\"x\"); }")]);
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
game.run_init();
|
||||
game.tick(Duration::from_millis(33));
|
||||
@@ -69,13 +66,11 @@ fn compile_and_unknown_script_errors_are_logged() {
|
||||
|
||||
#[test]
|
||||
fn script_reads_board_through_view() {
|
||||
let board = open_board(
|
||||
5,
|
||||
3,
|
||||
(3, 1),
|
||||
vec![scripted_object(2, 1, "r")],
|
||||
let board = open_board(5, 3, (3, 1), vec![scripted_object(2, 1, "r")]);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("r", "fn init() { log(Board.player_x.to_string()); }")]),
|
||||
);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("r", "fn init() { log(Board.player_x.to_string()); }")]));
|
||||
game.run_init();
|
||||
assert_eq!(log_texts(&game), vec!["3"]);
|
||||
}
|
||||
@@ -90,10 +85,13 @@ fn commands_are_routed_to_their_own_source_object() {
|
||||
(0, 0),
|
||||
vec![scripted_object(0, 1, "e"), scripted_object(4, 1, "w")],
|
||||
);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[
|
||||
("e", "fn init() { move(East); }"),
|
||||
("w", "fn init() { move(West); }"),
|
||||
]));
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[
|
||||
("e", "fn init() { move(East); }"),
|
||||
("w", "fn init() { move(West); }"),
|
||||
]),
|
||||
);
|
||||
game.run_init();
|
||||
let b = game.board();
|
||||
assert_eq!(b.objects[&1].x, 1); // moved east from 0
|
||||
@@ -112,7 +110,9 @@ fn start_map_greeter_runs_init() {
|
||||
let mut game = GameState::from_world(world);
|
||||
game.run_init();
|
||||
assert!(
|
||||
log_texts(&game).iter().any(|t| t.contains("hello from object")),
|
||||
log_texts(&game)
|
||||
.iter()
|
||||
.any(|t| t.contains("hello from object")),
|
||||
"greeter init should log a greeting"
|
||||
);
|
||||
assert!(
|
||||
@@ -139,7 +139,12 @@ fn set_tag_adds_and_removes_via_my_id() {
|
||||
&[("t2", r#"fn init() { set_tag(Me.id, "active", false); }"#)],
|
||||
);
|
||||
// Seed the tag before construction.
|
||||
board2.objects.get_mut(&1).unwrap().tags.insert("active".to_string());
|
||||
board2
|
||||
.objects
|
||||
.get_mut(&1)
|
||||
.unwrap()
|
||||
.tags
|
||||
.insert("active".to_string());
|
||||
let mut game2 = GameState::with_scripts(board2, scripts2);
|
||||
game2.run_init();
|
||||
assert!(!game2.board().objects[&1].tags.contains("active"));
|
||||
@@ -172,37 +177,39 @@ fn objects_with_tag_returns_matching_ids() {
|
||||
// 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]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[
|
||||
("q", r#"fn init() {
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[
|
||||
(
|
||||
"q",
|
||||
r#"fn init() {
|
||||
let infos = Board.tagged("enemy");
|
||||
log(infos.len().to_string());
|
||||
log(infos[0].id.to_string());
|
||||
}"#),
|
||||
("none", ""),
|
||||
]));
|
||||
}"#,
|
||||
),
|
||||
("none", ""),
|
||||
]),
|
||||
);
|
||||
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
|
||||
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, scripts) = board_with_object(
|
||||
Some("n"),
|
||||
&[("n", r#"fn init() { log(Me.name); }"#)],
|
||||
);
|
||||
let (mut board, scripts) =
|
||||
board_with_object(Some("n"), &[("n", r#"fn init() { log(Me.name); }"#)]);
|
||||
board.objects.get_mut(&1).unwrap().name = Some("beacon".to_string());
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
game.run_init();
|
||||
assert_eq!(log_texts(&game), vec!["beacon"]);
|
||||
|
||||
// An unnamed object should get an empty string.
|
||||
let (board2, scripts2) = board_with_object(
|
||||
Some("n"),
|
||||
&[("n", r#"fn init() { log(Me.name); }"#)],
|
||||
);
|
||||
let (board2, scripts2) =
|
||||
board_with_object(Some("n"), &[("n", r#"fn init() { log(Me.name); }"#)]);
|
||||
let mut game2 = GameState::with_scripts(board2, scripts2);
|
||||
game2.run_init();
|
||||
assert_eq!(log_texts(&game2), vec![""]);
|
||||
@@ -216,18 +223,24 @@ fn object_id_for_name_finds_by_name() {
|
||||
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]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[
|
||||
("q", r#"fn init() {
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[
|
||||
(
|
||||
"q",
|
||||
r#"fn init() {
|
||||
log(Board.named("target").id.to_string());
|
||||
let miss = Board.named("missing");
|
||||
log(if miss == () { "not_found" } else { miss.id.to_string() });
|
||||
}"#),
|
||||
("none", ""),
|
||||
]));
|
||||
}"#,
|
||||
),
|
||||
("none", ""),
|
||||
]),
|
||||
);
|
||||
game.run_init();
|
||||
let texts = log_texts(&game);
|
||||
assert_eq!(texts[0], "2"); // obj2 is id 2
|
||||
assert_eq!(texts[1], "not_found"); // Board.named returns () when no match
|
||||
assert_eq!(texts[0], "2"); // obj2 is id 2
|
||||
assert_eq!(texts[1], "not_found"); // Board.named returns () when no match
|
||||
}
|
||||
|
||||
// Ensure try_move from the player side also triggers scripted bump
|
||||
@@ -236,7 +249,10 @@ 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")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("b", "fn bump(id) { log(`bumped by ${id}`); }")]));
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("b", "fn bump(id) { log(`bumped by ${id}`); }")]),
|
||||
);
|
||||
game.run_init();
|
||||
game.try_move(Direction::East);
|
||||
assert_eq!((game.board().player.x, game.board().player.y), (0, 0)); // blocked
|
||||
@@ -251,22 +267,36 @@ fn scroll_opens_on_player_bump() {
|
||||
// player bumps it and a tick resolves the queued action, active_scroll is set
|
||||
// with the correct ScrollLine variants.
|
||||
let board = open_board(3, 1, (0, 0), vec![scripted_object(1, 0, "s")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("s", r#"fn bump(id) { scroll(["Hello world", ["eat", "Eat it"]]); }"#)]));
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[(
|
||||
"s",
|
||||
r#"fn bump(id) { scroll(["Hello world", ["eat", "Eat it"]]); }"#,
|
||||
)]),
|
||||
);
|
||||
game.run_init();
|
||||
game.try_move(Direction::East);
|
||||
game.tick(Duration::from_millis(16));
|
||||
|
||||
let scroll = game.active_scroll.as_ref().expect("scroll should be active after bump");
|
||||
let scroll = game
|
||||
.active_scroll
|
||||
.as_ref()
|
||||
.expect("scroll should be active after bump");
|
||||
assert_eq!(scroll.lines.len(), 2);
|
||||
assert!(matches!(&scroll.lines[0], ScrollLine::Text(t) if t == "Hello world"));
|
||||
assert!(matches!(&scroll.lines[1], ScrollLine::Choice { choice, display }
|
||||
if choice == "eat" && display == "Eat it"));
|
||||
assert!(
|
||||
matches!(&scroll.lines[1], ScrollLine::Choice { choice, display }
|
||||
if choice == "eat" && display == "Eat it")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_scroll_without_choice_clears_it() {
|
||||
let board = open_board(3, 1, (0, 0), vec![scripted_object(1, 0, "s")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("s", r#"fn bump(id) { scroll(["Hello"]); }"#)]));
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("s", r#"fn bump(id) { scroll(["Hello"]); }"#)]),
|
||||
);
|
||||
game.run_init();
|
||||
game.try_move(Direction::East);
|
||||
game.tick(Duration::from_millis(16));
|
||||
@@ -282,10 +312,16 @@ fn handle_scroll_with_choice_dispatches_send_to_source() {
|
||||
// Setting choice on the scroll before a tick fires send() on the source object;
|
||||
// fn eat() logging "eaten" confirms the dispatch arrived.
|
||||
let board = open_board(3, 1, (0, 0), vec![scripted_object(1, 0, "s")]);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("s", r#"
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[(
|
||||
"s",
|
||||
r#"
|
||||
fn bump(id) { scroll(["Muffin?", ["eat", "Eat it"]]); }
|
||||
fn eat() { log("eaten"); }
|
||||
"#)]));
|
||||
"#,
|
||||
)]),
|
||||
);
|
||||
game.run_init();
|
||||
game.try_move(Direction::East);
|
||||
game.tick(Duration::from_millis(16));
|
||||
@@ -295,5 +331,8 @@ fn handle_scroll_with_choice_dispatches_send_to_source() {
|
||||
game.active_scroll.as_mut().unwrap().choice = Some("eat".to_string());
|
||||
game.tick(Duration::from_millis(16));
|
||||
assert!(game.active_scroll.is_none());
|
||||
assert!(log_texts(&game).iter().any(|t| t == "eaten"), "eat() should have logged");
|
||||
assert!(
|
||||
log_texts(&game).iter().any(|t| t == "eaten"),
|
||||
"eat() should have logged"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user