wip 6 fixed tests
This commit is contained in:
+3
-74
@@ -501,12 +501,11 @@ impl Board {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::assert_matches;
|
||||
use super::Board;
|
||||
use crate::builtin::Builtin;
|
||||
use crate::floor::Floor;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::utils::{Direction, ObjectId, Point};
|
||||
use crate::utils::ObjectId;
|
||||
use color::Rgba8;
|
||||
use std::collections::HashMap;
|
||||
use crate::object_def::ObjectDef;
|
||||
@@ -703,76 +702,6 @@ pub(crate) mod tests {
|
||||
assert!(!board.in_bounds((0, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_shift_only_checks_the_cell_ahead() {
|
||||
// Source must be pushable.
|
||||
let mut board = open_board(4, 1, (3, 0));
|
||||
assert!(!board.can_shift(0, 0, Direction::East)); // empty source
|
||||
|
||||
// Crate with open space ahead: shiftable.
|
||||
crate_at(&mut board, 0, 0);
|
||||
assert!(board.can_shift(0, 0, Direction::East));
|
||||
assert_matches!(board.get((0, 0)), Some(Tile::Object(_)));
|
||||
|
||||
// Crate with another pushable crate ahead: still shiftable (unlike can_push,
|
||||
// which would follow the chain to the wall and fail).
|
||||
let mut board = open_board(4, 1, (3, 0));
|
||||
crate_at(&mut board, 0, 0);
|
||||
crate_at(&mut board, 1, 0);
|
||||
wall_at(&mut board, 2, 0);
|
||||
assert!(board.can_shift(0, 0, Direction::East));
|
||||
assert!(!board.can_push(0, 0, Direction::East));
|
||||
|
||||
// Crate with a non-pushable wall ahead: not shiftable.
|
||||
let mut board = open_board(3, 1, (2, 0));
|
||||
crate_at(&mut board, 0, 0);
|
||||
wall_at(&mut board, 1, 0);
|
||||
assert!(!board.can_shift(0, 0, Direction::East));
|
||||
|
||||
// Crate at the board edge facing off-board: not shiftable.
|
||||
let mut board = open_board(2, 1, (0, 0));
|
||||
crate_at(&mut board, 1, 0);
|
||||
assert!(!board.can_shift(1, 0, Direction::East));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_shift_treats_the_player_as_a_blocker() {
|
||||
// The player is always a blocker for a shift — even a grab gem may not shift
|
||||
// onto it (grab now fires only on player movement, not on being shifted in).
|
||||
let mut board = open_board(2, 1, (1, 0));
|
||||
gem_at(&mut board, 0, 0);
|
||||
assert!(!board.can_shift(0, 0, Direction::East));
|
||||
|
||||
// A plain crate likewise may not shift onto the player.
|
||||
let mut board = open_board(2, 1, (1, 0));
|
||||
crate_at(&mut board, 0, 0);
|
||||
assert!(!board.can_shift(0, 0, Direction::East));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_into_player_pushes_player() {
|
||||
// Crate shoved east into the player slides the player along into open space.
|
||||
let mut board = open_board(4, 1, (2, 0));
|
||||
crate_at(&mut board, 1, 0);
|
||||
assert!(board.can_push(1, 0, Direction::East));
|
||||
board.push(1, 0, Direction::East);
|
||||
assert!(board.get((1, 0)).is_none());
|
||||
assert!(is_builtin(&board, 2, 0,"crate"));
|
||||
assert_eq!(board.player_pos(), Point { x: 3, y: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_into_player_blocked_by_wall() {
|
||||
// Player backed against a wall: push has nowhere to go, nothing moves.
|
||||
let mut board = open_board(4, 1, (2, 0));
|
||||
crate_at(&mut board, 1, 0);
|
||||
wall_at(&mut board, 3, 0);
|
||||
assert!(!board.can_push(1, 0, Direction::East));
|
||||
board.push(1, 0, Direction::East); // no-op
|
||||
assert!(is_builtin(&board, 1, 0, "crate"));
|
||||
assert_eq!(board.player_pos(), Point { x: 2, y: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyph_at_uses_floor_for_empty_and_grid_for_solid() {
|
||||
// Player parked at (2,0) so it doesn't overlap either asserted cell.
|
||||
@@ -812,7 +741,7 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn apply_shift_wall_stops_cascade_but_empty_limits_it() {
|
||||
// A non-pushable Wall is immobile. Backward cascade from the wall traces
|
||||
// A wall is immobile (`mobile = false`). Backward cascade from the wall traces
|
||||
// through preceding solids until it hits empty, marking those as blocked.
|
||||
// Solids on the *other* side of the empty (outside the blocked region) still move.
|
||||
let mut board = open_board(6, 1, (5, 0));
|
||||
@@ -823,7 +752,7 @@ pub(crate) mod tests {
|
||||
crate_at(&mut board, 4, 0);
|
||||
|
||||
// Cycle: idx 0 → idx 1 → idx 2 → idx 3 → idx 4 → idx 0 (wrap)
|
||||
// immobile = {2} (Wall, Pushable::No)
|
||||
// immobile = {2} (the wall: Tile::shiftable() is false)
|
||||
// Backward trace from idx 2: solids[2]=Some → blocked.insert(2), prev=1
|
||||
// solids[1]=None (empty) → break
|
||||
// blocked = {2}; Crate at (0,0) is NOT blocked
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::utils::Pushable;
|
||||
use color::Rgba8;
|
||||
use lazy_static::lazy_static;
|
||||
use crate::keys::KeyType;
|
||||
|
||||
@@ -483,9 +483,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn walking_onto_a_gem_grabs_it() {
|
||||
// A gem builtin object at (1,0), running scripts/gem.rhai. Its
|
||||
// EnterResponse::Grab means the player's step fires grab() rather than being
|
||||
// blocked. The player starts at (0,0).
|
||||
// A gem builtin object at (1,0), running scripts/gem.rhai. Its bump hook
|
||||
// banks the gem and calls die(), so the cell clears and the player's step
|
||||
// completes instead of being blocked. The player starts at (0,0).
|
||||
let mut board = open_board(3, 1, (0, 0));
|
||||
gem_at(&mut board, 1, 0);
|
||||
let mut game = GameState::new(board);
|
||||
|
||||
@@ -2,12 +2,11 @@ use super::{log_texts, scripts_from};
|
||||
use crate::board::tests::{crate_at, is_builtin, object_at, open_board, wall_at};
|
||||
use crate::game::GameState;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::tile::EnterResponse;
|
||||
use crate::utils::{ObjectId, Point};
|
||||
use std::time::Duration;
|
||||
|
||||
/// The cell object `id` currently occupies.
|
||||
fn loc(game: &GameState, id: ObjectId) -> (usize, usize) {
|
||||
fn loc(game: &GameState, id: ObjectId) -> Point {
|
||||
game.board()
|
||||
.get_hookable(id)
|
||||
.expect("object still on the board")
|
||||
@@ -22,13 +21,12 @@ fn glyph(game: &GameState, id: ObjectId) -> Glyph {
|
||||
.glyph()
|
||||
}
|
||||
|
||||
/// Builds a game with one `Block` object at `(x, y)` running `src`, plus the player
|
||||
/// Builds a game with one immobile object at `(x, y)` running `src`, plus the player
|
||||
/// parked at `player`, and runs `init()`.
|
||||
///
|
||||
/// The mover is `EnterResponse::Block` — an ordinary solid that blocks others and
|
||||
/// cannot be shoved, which is what a self-propelled scripted object should be. Its
|
||||
/// `enter_response` describes how it answers *others* entering its cell and should
|
||||
/// not constrain its own `move()`.
|
||||
/// The mover is `mobile = false`: nothing else may relocate it (a `shift` skips it,
|
||||
/// and it has no `bump` hook to shove itself aside). That flag constrains only what
|
||||
/// *others* can do to it — it never restricts the object's own `move()`.
|
||||
fn game_with_mover(
|
||||
w: usize,
|
||||
h: usize,
|
||||
@@ -37,7 +35,7 @@ fn game_with_mover(
|
||||
src: &str,
|
||||
) -> (GameState, ObjectId) {
|
||||
let mut board = open_board(w, h, player);
|
||||
let id = object_at(&mut board, at.0, at.1, "m", EnterResponse::Block);
|
||||
let id = object_at(&mut board, at.0, at.1, "m", false);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("m", src)]));
|
||||
game.run_init();
|
||||
(game, id)
|
||||
@@ -47,23 +45,23 @@ fn game_with_mover(
|
||||
fn move_command_relocates_the_source_object() {
|
||||
let (game, id) = game_with_mover(5, 3, (0, 0), (2, 1), "fn init(me) { move(East); }");
|
||||
// East increments x by one; the object started at (2, 1).
|
||||
assert_eq!(loc(&game, id), (3, 1));
|
||||
assert_eq!(loc(&game, id), Point { x: 3, y: 1 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_into_a_wall_or_edge_is_a_noop() {
|
||||
// Object at the west edge moving west: out of bounds, ignored.
|
||||
let (game, id) = game_with_mover(5, 3, (0, 0), (0, 1), "fn init(me) { move(West); }");
|
||||
assert_eq!(loc(&game, id), (0, 1));
|
||||
assert_eq!(loc(&game, id), Point { x: 0, y: 1 });
|
||||
|
||||
// Object facing a wall: blocked, also ignored.
|
||||
let mut board = open_board(5, 3, (0, 0));
|
||||
let id = object_at(&mut board, 1, 1, "m", EnterResponse::Block);
|
||||
let id = object_at(&mut board, 1, 1, "m", false);
|
||||
wall_at(&mut board, 2, 1);
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init(me) { move(East); }")]));
|
||||
game.run_init();
|
||||
assert_eq!(loc(&game, id), (1, 1));
|
||||
assert_eq!(loc(&game, id), Point { x: 1, y: 1 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -76,13 +74,13 @@ fn set_tile_command_changes_the_source_glyph() {
|
||||
fn object_pushes_crate_on_init() {
|
||||
// A scripted object moving east into a crate shoves it (the step_object path).
|
||||
let mut board = open_board(4, 1, (0, 0));
|
||||
let id = object_at(&mut board, 1, 0, "m", EnterResponse::Block);
|
||||
let id = object_at(&mut board, 1, 0, "m", false);
|
||||
crate_at(&mut board, 2, 0);
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init(me) { move(East); }")]));
|
||||
game.run_init();
|
||||
|
||||
assert_eq!(loc(&game, id), (2, 0));
|
||||
assert_eq!(loc(&game, id), Point { x: 2, y: 0 });
|
||||
let b = game.board();
|
||||
assert!(is_builtin(&b, 3, 0, "crate")); // crate shoved one east
|
||||
assert!(b.get((1, 0)).is_none()); // the object's old cell is vacated
|
||||
@@ -92,18 +90,18 @@ fn object_pushes_crate_on_init() {
|
||||
fn object_push_into_player() {
|
||||
// A scripted object moving into the player pushes the player when there's room.
|
||||
let (game, id) = game_with_mover(5, 1, (2, 0), (1, 0), "fn init(me) { move(East); }");
|
||||
assert_eq!(loc(&game, id), (2, 0));
|
||||
assert_eq!(loc(&game, id), Point { x: 2, y: 0 });
|
||||
assert_eq!(game.board().player_pos(), Point { x: 3, y: 0 });
|
||||
|
||||
// With a wall behind the player, the object is blocked and nothing moves.
|
||||
let mut board = open_board(4, 1, (2, 0));
|
||||
let id = object_at(&mut board, 1, 0, "m", EnterResponse::Block);
|
||||
let id = object_at(&mut board, 1, 0, "m", false);
|
||||
wall_at(&mut board, 3, 0);
|
||||
let mut game =
|
||||
GameState::with_scripts(board, scripts_from(&[("m", "fn init(me) { move(East); }")]));
|
||||
game.run_init();
|
||||
|
||||
assert_eq!(loc(&game, id), (1, 0));
|
||||
assert_eq!(loc(&game, id), Point { x: 1, y: 0 });
|
||||
assert_eq!(game.board().player_pos(), Point { x: 2, y: 0 });
|
||||
}
|
||||
|
||||
@@ -118,16 +116,16 @@ fn move_cost_rate_limits_repeated_moves() {
|
||||
(1, 0),
|
||||
"fn init(me) { move(East); move(East); }",
|
||||
);
|
||||
assert_eq!(loc(&game, id).0, 2); // first move applied (1 -> 2)
|
||||
assert_eq!(loc(&game, id).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!(loc(&game, id).0, 2);
|
||||
assert_eq!(loc(&game, id).x, 2);
|
||||
|
||||
// Crossing the 250 ms mark releases the queued second move.
|
||||
game.tick(Duration::from_millis(100));
|
||||
assert_eq!(loc(&game, id).0, 3);
|
||||
assert_eq!(loc(&game, id).x, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -135,7 +133,7 @@ fn inline_delay_paces_subsequent_moves() {
|
||||
// move() always appends a Delay(250 ms) to the queue, so a second queued move
|
||||
// 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));
|
||||
let id = object_at(&mut board, 1, 1, "m", EnterResponse::Block);
|
||||
let id = object_at(&mut board, 1, 1, "m", false);
|
||||
wall_at(&mut board, 2, 1);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
@@ -143,16 +141,16 @@ fn inline_delay_paces_subsequent_moves() {
|
||||
);
|
||||
game.run_init();
|
||||
// First (eastward) move is blocked by the wall: object hasn't moved.
|
||||
assert_eq!(loc(&game, id), (1, 1));
|
||||
assert_eq!(loc(&game, id), Point { x: 1, y: 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!(loc(&game, id), (1, 1));
|
||||
assert_eq!(loc(&game, id), Point { x: 1, y: 1 });
|
||||
|
||||
// Past 250 ms the queued South move resolves.
|
||||
game.tick(Duration::from_millis(100));
|
||||
assert_eq!(loc(&game, id), (1, 2));
|
||||
assert_eq!(loc(&game, id), Point { x: 1, y: 2 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -181,7 +179,7 @@ fn queue_clear_drops_pending_actions() {
|
||||
"fn init(me) { move(East); move(East); me.queue.clear(); }",
|
||||
);
|
||||
game.tick(Duration::from_millis(300));
|
||||
assert_eq!(loc(&game, id), (1, 0)); // never moved
|
||||
assert_eq!(loc(&game, id), Point { x: 1, y: 0 }); // never moved
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -190,7 +188,7 @@ fn blocked_reports_solid_and_clear() {
|
||||
|
||||
// Solid ahead (a wall): blocked() is true.
|
||||
let mut board = open_board(3, 1, (0, 0));
|
||||
let id = object_at(&mut board, 1, 0, "b", EnterResponse::Block);
|
||||
let id = object_at(&mut board, 1, 0, "b", false);
|
||||
wall_at(&mut board, 2, 0);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("b", src)]));
|
||||
game.run_init();
|
||||
@@ -198,7 +196,7 @@ fn blocked_reports_solid_and_clear() {
|
||||
|
||||
// Open ahead, nothing pending: blocked() is false.
|
||||
let mut board = open_board(3, 1, (0, 0));
|
||||
let id = object_at(&mut board, 1, 0, "b", EnterResponse::Block);
|
||||
let id = object_at(&mut board, 1, 0, "b", false);
|
||||
let mut game = GameState::with_scripts(board, scripts_from(&[("b", src)]));
|
||||
game.run_init();
|
||||
assert_eq!(glyph(&game, id).tile, 'N');
|
||||
|
||||
@@ -30,8 +30,7 @@ fn portal(
|
||||
target_name: &str,
|
||||
) -> Portal {
|
||||
Portal {
|
||||
x,
|
||||
y,
|
||||
location: (x, y).into(),
|
||||
name: name.to_string(),
|
||||
target_board: target_board.to_string(),
|
||||
target_name: target_name.to_string(),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
mod actions;
|
||||
// TODO(migration): map_file is not yet ported — it is blocked on the map files
|
||||
// themselves still being pre-BoardSpec (see todo.md #1).
|
||||
mod game_portals;
|
||||
// TODO(migration): the map_file suite predates BoardSpec and is not ported yet.
|
||||
// mod map_file;
|
||||
mod movement;
|
||||
mod scripting;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
//! Player push mechanics, driven through [`GameState::try_move`].
|
||||
//!
|
||||
//! Pushability now lives on the target's [`EnterResponse`]: `Push(Pushable::Any)`
|
||||
//! for a crate, `Push(Horizontal)`/`Push(Vertical)` for the axis-locked ones, and
|
||||
//! `Block` for a wall. `try_move` consults `Board::can_push` and, when the chain
|
||||
//! can move, pushes from the *player's* cell — the player is itself pushable, so it
|
||||
//! rides along at the head of the chain.
|
||||
//! Pushing is not a board primitive: [`GameState::resolve_move`] just fires the
|
||||
//! target's `bump` hook and then re-checks whether the cell it wanted has become
|
||||
//! free. A crate slides because `crate.rhai`'s `bump` pushes the cell ahead of it
|
||||
//! and then teleports itself into the space; an `hcrate`/`vcrate` returns early
|
||||
//! from `bump` on the axis it refuses; a wall has no script at all, so nothing
|
||||
//! ever clears its cell. The player is moved by the same recursion, so a chain
|
||||
//! shoved into it slides it along.
|
||||
|
||||
use crate::board::tests::{add_floor, builtin_at, crate_at, is_builtin, open_board, plain_object_at, wall_at};
|
||||
use crate::board::tests::{add_floor, builtin_at, crate_at, is_builtin, open_board, wall_at};
|
||||
use crate::game::GameState;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::tile::EnterResponse;
|
||||
use crate::utils::{Direction, Point, Pushable};
|
||||
use crate::utils::{Direction, Point};
|
||||
use color::Rgba8;
|
||||
|
||||
#[test]
|
||||
@@ -105,27 +106,9 @@ fn cascade_blocked_by_wall_moves_nothing() {
|
||||
assert!(is_builtin(&b, 2, 0, "crate"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_pushes_pushable_solid_object() {
|
||||
// A plain object whose EnterResponse is Push(Any) is shoved exactly like a
|
||||
// crate — the crate builtin has no privileged status, it just carries that
|
||||
// same enter response.
|
||||
let mut board = open_board(4, 1, (0, 0));
|
||||
let id = plain_object_at(&mut board, 1, 0, EnterResponse::Push(Pushable::Any));
|
||||
let mut game = GameState::new(board);
|
||||
game.try_move(Direction::East);
|
||||
|
||||
let b = game.board();
|
||||
assert_eq!(b.player_pos(), Point { x: 1, y: 0 });
|
||||
assert_eq!(
|
||||
b.get_hookable(id).expect("object still on the board").location(),
|
||||
(2, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hcrate_pushes_east_but_not_north() {
|
||||
// Pushing east: Push(Horizontal) allows it, so the HCrate slides.
|
||||
// Pushing east: hcrate.rhai accepts the axis, so the HCrate slides.
|
||||
let mut board = open_board(4, 1, (0, 0));
|
||||
builtin_at(&mut board, 1, 0, "hcrate");
|
||||
let mut game = GameState::new(board);
|
||||
@@ -136,8 +119,8 @@ fn hcrate_pushes_east_but_not_north() {
|
||||
assert!(is_builtin(&b, 2, 0, "hcrate"));
|
||||
}
|
||||
|
||||
// Pushing north into an HCrate: the direction isn't allowed, so can_push fails
|
||||
// and the move degrades to a bump — nothing moves.
|
||||
// Pushing north into an HCrate: its bump hook returns early on the vertical
|
||||
// axis, so its cell never clears and nothing moves.
|
||||
let mut board = open_board(1, 4, (0, 3));
|
||||
builtin_at(&mut board, 0, 2, "hcrate");
|
||||
let mut game = GameState::new(board);
|
||||
@@ -150,7 +133,7 @@ fn hcrate_pushes_east_but_not_north() {
|
||||
|
||||
#[test]
|
||||
fn vcrate_pushes_north_but_not_east() {
|
||||
// Pushing north: Push(Vertical) allows it, so the VCrate slides.
|
||||
// Pushing north: vcrate.rhai accepts the axis, so the VCrate slides.
|
||||
let mut board = open_board(1, 4, (0, 3));
|
||||
builtin_at(&mut board, 0, 2, "vcrate");
|
||||
let mut game = GameState::new(board);
|
||||
@@ -161,7 +144,8 @@ fn vcrate_pushes_north_but_not_east() {
|
||||
assert!(is_builtin(&b, 0, 1, "vcrate"));
|
||||
}
|
||||
|
||||
// Pushing east into a VCrate: blocked, nothing moves.
|
||||
// Pushing east into a VCrate: its bump hook refuses the horizontal axis,
|
||||
// so nothing moves.
|
||||
let mut board = open_board(4, 1, (0, 0));
|
||||
builtin_at(&mut board, 1, 0, "vcrate");
|
||||
let mut game = GameState::new(board);
|
||||
|
||||
@@ -2,12 +2,11 @@ use super::{log_texts, scripts_from};
|
||||
use crate::board::Board;
|
||||
use crate::board::tests::{object_at, open_board, plain_object_at, sensor_at};
|
||||
use crate::game::{GameState, ScrollLine};
|
||||
use crate::tile::EnterResponse;
|
||||
use crate::utils::{Direction, ObjectId, Point};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Builds a 2×1 board with a single `Block` object at (0,0) that optionally
|
||||
/// Builds a 2×1 board with a single immobile object at (0,0) that optionally
|
||||
/// references a script, the player parked at (1,0), and the `(name, source)`
|
||||
/// entries as a script map. Returns the object's id alongside them.
|
||||
///
|
||||
@@ -19,8 +18,8 @@ fn board_with_object(
|
||||
) -> (Board, HashMap<String, String>, ObjectId) {
|
||||
let mut board = open_board(2, 1, (1, 0));
|
||||
let id = match object_script {
|
||||
Some(name) => object_at(&mut board, 0, 0, name, EnterResponse::Block),
|
||||
None => plain_object_at(&mut board, 0, 0, EnterResponse::Block),
|
||||
Some(name) => object_at(&mut board, 0, 0, name, false),
|
||||
None => plain_object_at(&mut board, 0, 0, false),
|
||||
};
|
||||
(board, scripts_from(scripts), id)
|
||||
}
|
||||
@@ -35,7 +34,7 @@ fn has_tag(game: &GameState, id: ObjectId, tag: &str) -> bool {
|
||||
}
|
||||
|
||||
/// The cell object `id` currently occupies.
|
||||
fn loc(game: &GameState, id: ObjectId) -> (usize, usize) {
|
||||
fn loc(game: &GameState, id: ObjectId) -> Point {
|
||||
game.board()
|
||||
.get_hookable(id)
|
||||
.expect("object still on the board")
|
||||
@@ -113,7 +112,7 @@ fn compile_and_unknown_script_errors_are_logged() {
|
||||
#[test]
|
||||
fn script_reads_board_through_view() {
|
||||
let mut board = open_board(5, 3, (3, 1));
|
||||
object_at(&mut board, 2, 1, "r", EnterResponse::Block);
|
||||
object_at(&mut board, 2, 1, "r", false);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("r", "fn init(me) { log(Player.x.to_string()); }")]),
|
||||
@@ -127,8 +126,8 @@ 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 to its source queue).
|
||||
let mut board = open_board(5, 3, (0, 0));
|
||||
let east = object_at(&mut board, 0, 1, "e", EnterResponse::Block);
|
||||
let west = object_at(&mut board, 4, 1, "w", EnterResponse::Block);
|
||||
let east = object_at(&mut board, 0, 1, "e", false);
|
||||
let west = object_at(&mut board, 4, 1, "w", false);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[
|
||||
@@ -137,8 +136,8 @@ fn commands_are_routed_to_their_own_source_object() {
|
||||
]),
|
||||
);
|
||||
game.run_init();
|
||||
assert_eq!(loc(&game, east), (1, 1)); // moved east from 0
|
||||
assert_eq!(loc(&game, west), (3, 1)); // moved west from 4
|
||||
assert_eq!(loc(&game, east), Point { x: 1, y: 1 }); // moved east from 0
|
||||
assert_eq!(loc(&game, west), Point { x: 3, y: 1 }); // moved west from 4
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -150,7 +149,7 @@ fn start_map_greeter_runs_init() {
|
||||
// Keep the failure message short: a TomlError's Display embeds the whole file.
|
||||
let mut world = crate::world::load(path).unwrap_or_else(|e| {
|
||||
let first = e.to_string().lines().next().unwrap_or_default().to_string();
|
||||
panic!("load start.toml failed (see todo.md #1, maps are still pre-BoardSpec): {first}")
|
||||
panic!("load start.toml failed: {first}")
|
||||
});
|
||||
// Pin to the "start" board regardless of the world's current default entry point.
|
||||
world.start = "start".to_string();
|
||||
@@ -222,8 +221,8 @@ fn objects_with_tag_returns_matching_ids() {
|
||||
// A 5×1 board with two objects; one has tag "enemy". The script on the first
|
||||
// queries Board.tagged("enemy") and logs the count and the id it found.
|
||||
let mut board = open_board(5, 1, (4, 0));
|
||||
object_at(&mut board, 0, 0, "q", EnterResponse::Block);
|
||||
let enemy = object_at(&mut board, 1, 0, "none", EnterResponse::Block);
|
||||
object_at(&mut board, 0, 0, "q", false);
|
||||
let enemy = object_at(&mut board, 1, 0, "none", false);
|
||||
board
|
||||
.scripting_mut(enemy)
|
||||
.unwrap()
|
||||
@@ -274,8 +273,8 @@ fn object_id_for_name_finds_by_name() {
|
||||
// A board with two objects; one is named. The querying object uses Board.named
|
||||
// to find it and logs its id, then confirms a miss returns ().
|
||||
let mut board = open_board(5, 1, (4, 0));
|
||||
object_at(&mut board, 0, 0, "q", EnterResponse::Block);
|
||||
let target = object_at(&mut board, 1, 0, "none", EnterResponse::Block);
|
||||
object_at(&mut board, 0, 0, "q", false);
|
||||
let target = object_at(&mut board, 1, 0, "none", false);
|
||||
board.scripting_mut(target).unwrap().name = Some("target".to_string());
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
@@ -303,10 +302,10 @@ fn object_id_for_name_finds_by_name() {
|
||||
|
||||
#[test]
|
||||
fn player_bump_reports_the_direction_it_came_from() {
|
||||
// The player walks East into a Block object; the object is bumped from the West
|
||||
// The player walks East into a scripted solid; the object is bumped from the West
|
||||
// side (opposite the player's travel) and the player does not move onto it.
|
||||
let mut board = open_board(3, 1, (0, 0));
|
||||
object_at(&mut board, 1, 0, "b", EnterResponse::Block);
|
||||
object_at(&mut board, 1, 0, "b", false);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("b", "fn bump(me, dir) { log(`bumped from ${dir}`); }")]),
|
||||
@@ -322,7 +321,7 @@ fn player_bump_reports_the_direction_it_came_from() {
|
||||
fn bump_direction_supports_comparison_and_offset() {
|
||||
// A bump handler can compare the direction and read its dx/dy to locate the bumper.
|
||||
let mut board = open_board(3, 1, (0, 0));
|
||||
object_at(&mut board, 1, 0, "b", EnterResponse::Block);
|
||||
object_at(&mut board, 1, 0, "b", false);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[(
|
||||
@@ -341,10 +340,10 @@ fn bump_direction_supports_comparison_and_offset() {
|
||||
|
||||
#[test]
|
||||
fn scroll_opens_on_player_bump() {
|
||||
// A Block object's bump() calls scroll([text, [choice, display]]). The bump
|
||||
// A solid object's bump() calls scroll([text, [choice, display]]). The bump
|
||||
// resolves inside try_move, so active_scroll is set with the right variants.
|
||||
let mut board = open_board(3, 1, (0, 0));
|
||||
object_at(&mut board, 1, 0, "s", EnterResponse::Block);
|
||||
object_at(&mut board, 1, 0, "s", false);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[(
|
||||
@@ -370,7 +369,7 @@ fn scroll_opens_on_player_bump() {
|
||||
#[test]
|
||||
fn handle_scroll_without_choice_clears_it() {
|
||||
let mut board = open_board(3, 1, (0, 0));
|
||||
object_at(&mut board, 1, 0, "s", EnterResponse::Block);
|
||||
object_at(&mut board, 1, 0, "s", false);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[("s", r#"fn bump(me, dir) { scroll(["Hello"]); }"#)]),
|
||||
@@ -389,7 +388,7 @@ 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 mut board = open_board(3, 1, (0, 0));
|
||||
object_at(&mut board, 1, 0, "s", EnterResponse::Block);
|
||||
object_at(&mut board, 1, 0, "s", false);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[(
|
||||
@@ -424,8 +423,8 @@ fn a_later_object_sees_an_earlier_objects_move_this_tick() {
|
||||
// this tick, B observes it as blocked — under a collect-all-then-apply model B
|
||||
// would have seen (1,0) still empty.
|
||||
let mut board = open_board(3, 2, (2, 1));
|
||||
let a = object_at(&mut board, 0, 0, "a", EnterResponse::Block);
|
||||
object_at(&mut board, 1, 1, "b", EnterResponse::Block);
|
||||
let a = object_at(&mut board, 0, 0, "a", false);
|
||||
object_at(&mut board, 1, 1, "b", false);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[
|
||||
@@ -441,7 +440,7 @@ fn a_later_object_sees_an_earlier_objects_move_this_tick() {
|
||||
game.tick(Duration::from_millis(16));
|
||||
|
||||
// A moved onto (1,0), and B saw it there the same tick.
|
||||
assert_eq!(loc(&game, a), (1, 0));
|
||||
assert_eq!(loc(&game, a), Point { x: 1, y: 0 });
|
||||
assert_eq!(log_texts(&game), vec!["blocked"]);
|
||||
}
|
||||
|
||||
@@ -453,7 +452,7 @@ fn tick_is_gated_on_an_empty_queue() {
|
||||
// draining the engine skips re-running `tick`. With 100 ms frames the object steps
|
||||
// once (x=1) then waits ~0.25s before the next call fires.
|
||||
let mut board = open_board(6, 1, (5, 0));
|
||||
let id = object_at(&mut board, 0, 0, "m", EnterResponse::Block);
|
||||
let id = object_at(&mut board, 0, 0, "m", false);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
// Note: no `if me.queue.length == 0` guard — the engine provides it.
|
||||
@@ -466,12 +465,12 @@ fn tick_is_gated_on_an_empty_queue() {
|
||||
for _ in 0..3 {
|
||||
game.tick(Duration::from_millis(100));
|
||||
}
|
||||
assert_eq!(loc(&game, id).0, 1);
|
||||
assert_eq!(loc(&game, id).x, 1);
|
||||
|
||||
// Once the Delay fully drains the queue empties, so the next frame calls `tick`
|
||||
// again and the object takes its second step.
|
||||
game.tick(Duration::from_millis(100));
|
||||
assert_eq!(loc(&game, id).0, 2);
|
||||
assert_eq!(loc(&game, id).x, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -487,8 +486,8 @@ fn a_send_cycle_defers_instead_of_recursing() {
|
||||
// recurse until the stack blows. That this test *returns at all* is half the
|
||||
// assertion; the log growth is the other half.
|
||||
let mut board = open_board(3, 1, (2, 0));
|
||||
let a = object_at(&mut board, 0, 0, "a", EnterResponse::Block);
|
||||
let b = object_at(&mut board, 1, 0, "b", EnterResponse::Block);
|
||||
let a = object_at(&mut board, 0, 0, "a", false);
|
||||
let b = object_at(&mut board, 1, 0, "b", false);
|
||||
let mut game = GameState::with_scripts(
|
||||
board,
|
||||
scripts_from(&[
|
||||
|
||||
@@ -6,36 +6,6 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::log::LogLine;
|
||||
use crate::script::Registerable;
|
||||
|
||||
/// Which directions a solid may be pushed in.
|
||||
///
|
||||
/// Only meaningful for `solid` cells (a non-solid cell is passable, so nothing is
|
||||
/// ever pushed into it). `Crate` is [`Pushable::Any`]; the directional crates
|
||||
/// constrain pushes to one axis.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Pushable {
|
||||
/// Cannot be pushed.
|
||||
No,
|
||||
/// Pushable in any direction.
|
||||
Any,
|
||||
/// Pushable east/west only.
|
||||
Horizontal,
|
||||
/// Pushable north/south only.
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl Pushable {
|
||||
/// Whether a push in `dir` is allowed.
|
||||
pub fn allows(self, dir: Direction) -> bool {
|
||||
match self {
|
||||
Pushable::No => false,
|
||||
Pushable::Any => true,
|
||||
Pushable::Horizontal => matches!(dir, Direction::East | Direction::West),
|
||||
Pushable::Vertical => matches!(dir, Direction::North | Direction::South),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A stable, unique identifier for a board object.
|
||||
///
|
||||
/// Ids are handed out by [`Board::add_object`] from the per-board
|
||||
@@ -303,30 +273,3 @@ impl LogSink {
|
||||
std::mem::take(&mut self.0.borrow_mut())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Pushable;
|
||||
use crate::utils::Direction;
|
||||
|
||||
#[test]
|
||||
fn pushable_allows_only_its_axis() {
|
||||
for d in [
|
||||
Direction::North,
|
||||
Direction::South,
|
||||
Direction::East,
|
||||
Direction::West,
|
||||
] {
|
||||
assert!(!Pushable::No.allows(d));
|
||||
assert!(Pushable::Any.allows(d));
|
||||
}
|
||||
assert!(Pushable::Horizontal.allows(Direction::East));
|
||||
assert!(Pushable::Horizontal.allows(Direction::West));
|
||||
assert!(!Pushable::Horizontal.allows(Direction::North));
|
||||
assert!(!Pushable::Horizontal.allows(Direction::South));
|
||||
assert!(Pushable::Vertical.allows(Direction::North));
|
||||
assert!(Pushable::Vertical.allows(Direction::South));
|
||||
assert!(!Pushable::Vertical.allows(Direction::East));
|
||||
assert!(!Pushable::Vertical.allows(Direction::West));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user