wip 6 fixed tests

This commit is contained in:
2026-08-11 23:01:12 -05:00
parent 456b3448eb
commit 0f045c39a3
9 changed files with 77 additions and 227 deletions
+3 -74
View File
@@ -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
View File
@@ -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;
+3 -3
View File
@@ -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);
+25 -27
View File
@@ -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');
+1 -2
View File
@@ -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 -2
View File
@@ -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;
+15 -31
View File
@@ -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);
+29 -30
View File
@@ -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(&[
-57
View File
@@ -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));
}
}