This commit is contained in:
2026-06-21 22:35:35 -05:00
parent 87979ac610
commit a67f3fa8cf
2 changed files with 105 additions and 95 deletions
+101 -91
View File
@@ -995,110 +995,120 @@ pub(crate) mod tests {
} }
#[test] #[test]
fn apply_swap_swaps_two_terrain_cells() { fn apply_shift_out_of_bounds_rejects_immediately() {
// A crate and a wall trade places in one batch (read-all then write-all). // apply_shift validates all cells upfront; any out-of-bounds cell causes immediate failure.
let mut board = open_board(3, 1, (1, 0), vec![]);
crate_at(&mut board, 0, 0);
wall_at(&mut board, 2, 0);
let errs = board.apply_swap(&[(0, 0, 2, 0), (2, 0, 0, 0)]);
assert!(errs.is_empty());
assert_eq!(board.get(0, 0, 0).1, Archetype::Wall);
assert_eq!(board.get(0, 2, 0).1, Archetype::Crate);
}
#[test]
fn apply_swap_cycle_propagates_empty() {
// The spec example: move a->b and b->c with a empty ⇒ a empty, b empty,
// c holds what b held (the empty written into b doesn't block b->c).
let mut board = open_board(4, 1, (3, 0), vec![]);
// a = (0,0) empty, b = (1,0) crate, c = (2,0) wall.
crate_at(&mut board, 1, 0);
wall_at(&mut board, 2, 0);
let errs = board.apply_swap(&[(0, 0, 1, 0), (1, 0, 2, 0)]);
assert!(errs.is_empty());
assert_eq!(board.get(0, 0, 0).1, Archetype::Empty);
assert_eq!(board.get(0, 1, 0).1, Archetype::Empty);
assert_eq!(board.get(0, 2, 0).1, Archetype::Crate);
}
#[test]
fn apply_swap_empty_onto_object_removes_it() {
// Moving an empty source onto an object despawns the object.
let mut board = open_board(3, 1, (2, 0), vec![ObjectDef::new(1, 0)]);
let errs = board.apply_swap(&[(0, 0, 1, 0)]);
assert!(errs.is_empty());
assert!(board.solid_object_id_at(1, 0).is_none());
assert!(board.objects.is_empty());
}
#[test]
fn apply_swap_terrain_onto_terrain_removes_displaced() {
// Moving a crate onto a wall clears the source and removes the wall.
let mut board = open_board(3, 1, (2, 0), vec![]); let mut board = open_board(3, 1, (2, 0), vec![]);
crate_at(&mut board, 0, 0); crate_at(&mut board, 0, 0);
wall_at(&mut board, 1, 0); let errs = board.apply_shift(&[(0, 0), (9, 0)]);
let errs = board.apply_swap(&[(0, 0, 1, 0)]);
assert!(errs.is_empty());
assert_eq!(board.get(0, 0, 0).1, Archetype::Empty);
assert_eq!(board.get(0, 1, 0).1, Archetype::Crate);
}
#[test]
fn apply_swap_moves_object_and_player() {
// An object and the player relocate (swap places) in one batch.
let mut board = open_board(3, 1, (0, 0), vec![ObjectDef::new(2, 0)]);
let errs = board.apply_swap(&[(0, 0, 2, 0), (2, 0, 0, 0)]);
assert!(errs.is_empty());
assert_eq!((board.player.x, board.player.y), (2, 0));
assert_eq!((board.objects[&1].x, board.objects[&1].y), (0, 0));
}
#[test]
fn apply_swap_out_of_bounds_skips_and_logs() {
let mut board = open_board(3, 1, (2, 0), vec![]);
crate_at(&mut board, 0, 0);
let errs = board.apply_swap(&[(0, 0, 9, 0)]);
assert_eq!(errs.len(), 1); assert_eq!(errs.len(), 1);
assert_eq!(board.get(0, 0, 0).1, Archetype::Crate); // unchanged assert_eq!(board.get(0, 0, 0).1, Archetype::Crate); // unchanged
} }
#[test] #[test]
fn apply_swap_will_not_overwrite_player() { fn apply_shift_wall_stops_cascade_but_empty_limits_it() {
// A crate moved onto the (non-relocating) player is rejected and logged. // A non-pushable Wall is immobile. Backward cascade from the wall traces
let mut board = open_board(3, 1, (1, 0), vec![]); // 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), vec![]);
crate_at(&mut board, 0, 0); crate_at(&mut board, 0, 0);
let errs = board.apply_swap(&[(0, 0, 1, 0)]); // (1,0) stays empty
assert_eq!(errs.len(), 1); wall_at(&mut board, 2, 0);
assert_eq!((board.player.x, board.player.y), (1, 0)); // player kept its cell crate_at(&mut board, 3, 0);
assert_eq!(board.get(0, 0, 0).1, Archetype::Empty); // source still vacated 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)
// 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
// Result: Crate(0,0)→(1,0), empty→no-op, Wall stays at (2,0),
// Crate(3,0)→(4,0), Crate(4,0)→(0,0) wrap
let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0), (3, 0), (4, 0)]);
assert_eq!(board.get(0, 0, 0).1, Archetype::Crate); // wrapped from (4,0)
assert_eq!(board.get(0, 1, 0).1, Archetype::Crate); // moved from (0,0)
assert_eq!(board.get(0, 2, 0).1, Archetype::Wall); // blocked, immobile
assert_eq!(board.get(0, 3, 0).1, Archetype::Empty); // cleared, crate moved
assert_eq!(board.get(0, 4, 0).1, Archetype::Crate); // moved from (3,0)
} }
#[test] #[test]
fn apply_swap_grab_onto_player_is_refused_like_any_solid() { fn apply_shift_hcrate_moves_horizontally_vcrate_stays() {
// A grab gem swapped onto the player is no longer special: it is refused and // In a horizontal (same-row) adjacent cycle:
// logged (the player wins its cell), and left at its source. Grab fires only // - HCrate (Pushable::Horizontal) moves freely (hmove, not blocked).
// when the player walks onto a grab thing, not when one is swapped in. // - VCrate (Pushable::Vertical) is immobile (Vertical && hmove).
let mut gem = ObjectDef::new(0, 0); // Both can share the same pattern without blocking each other's neighbors.
gem.grab = true;
gem.pushable = true; // Subcase A: HCrate moves
let mut board = open_board(2, 1, (1, 0), vec![gem]); {
let errs = board.apply_swap(&[(0, 0, 1, 0)]); let mut board = open_board(4, 1, (3, 0), vec![]);
assert_eq!(errs.len(), 1); // refusal logged stamp(&mut board, 0, 0, Archetype::HCrate);
assert_eq!((board.player.x, board.player.y), (1, 0)); // player kept its cell crate_at(&mut board, 1, 0);
assert_eq!((board.objects[&1].x, board.objects[&1].y), (0, 0)); // gem stayed // (2,0) empty
let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0)]);
assert_eq!(board.get(0, 0, 0).1, Archetype::Empty); // HCrate moved out
assert_eq!(board.get(0, 1, 0).1, Archetype::HCrate); // moved from (0,0)
assert_eq!(board.get(0, 2, 0).1, Archetype::Crate); // moved from (1,0)
}
// Subcase B: VCrate stays; Crate behind it still moves
{
let mut board = open_board(4, 1, (3, 0), vec![]);
stamp(&mut board, 0, 0, Archetype::VCrate);
crate_at(&mut board, 1, 0);
// (2,0) empty
// VCrate (idx 0): target (1,0), origin (0,0), hmove=true.
// Pushable::Vertical && hmove=true → immobile
// Backward: prev=idx 2, solids[2]=None → stop. blocked={0}.
// Crate at (1,0) is NOT in blocked, so it moves.
let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0)]);
assert_eq!(board.get(0, 0, 0).1, Archetype::VCrate); // immobile
assert_eq!(board.get(0, 1, 0).1, Archetype::Empty); // crate moved out
assert_eq!(board.get(0, 2, 0).1, Archetype::Crate); // moved from (1,0)
}
} }
#[test] #[test]
fn apply_swap_overlap_after_refused_player_write_deletes_the_other_solid() { fn apply_shift_vcrate_moves_vertically_hcrate_stays() {
// object (0,0)→player (1,0) [refused, stays at (0,0)] while a crate (2,0)→(0,0) // In a vertical (same-column) adjacent cycle:
// moves into the object's cell. The sweep keeps the object and deletes the // - VCrate (Pushable::Vertical) moves freely (vmove, not blocked).
// crate; one error logs the refusal, one logs the overlap. // - HCrate (Pushable::Horizontal) is immobile (Horizontal && vmove).
let mut board = open_board(3, 1, (1, 0), vec![ObjectDef::new(0, 0)]);
crate_at(&mut board, 2, 0); // Subcase A: VCrate moves
let errs = board.apply_swap(&[(0, 0, 1, 0), (2, 0, 0, 0)]); {
assert_eq!(errs.len(), 2); // the refusal and the overlap were logged let mut board = open_board(1, 4, (0, 3), vec![]);
assert_eq!((board.objects[&1].x, board.objects[&1].y), (0, 0)); // object kept stamp(&mut board, 0, 0, Archetype::VCrate);
assert_eq!(board.get(0, 0, 0).1, Archetype::Empty); // crate deleted stamp(&mut board, 0, 1, Archetype::Crate);
assert_eq!((board.player.x, board.player.y), (1, 0)); // player untouched // (0,2) empty
let _errs = board.apply_shift(&[(0, 0), (0, 1), (0, 2)]);
assert_eq!(board.get(0, 0, 0).1, Archetype::Empty); // VCrate moved out
assert_eq!(board.get(0, 0, 1).1, Archetype::VCrate); // moved from (0,0)
assert_eq!(board.get(0, 0, 2).1, Archetype::Crate); // moved from (0,1)
} }
// Subcase B: HCrate stays; Crate behind it still moves
{
let mut board = open_board(1, 4, (0, 3), vec![]);
stamp(&mut board, 0, 0, Archetype::HCrate);
stamp(&mut board, 0, 1, Archetype::Crate);
// (0,2) empty
// HCrate (idx 0): target (0,1), origin (0,0), vmove=true.
// Pushable::Horizontal && vmove=true → immobile
// Backward: prev=idx 2, solids[2]=None → stop. blocked={0}.
// Crate at (0,1) is NOT in blocked, so it moves.
let _errs = board.apply_shift(&[(0, 0), (0, 1), (0, 2)]);
assert_eq!(board.get(0, 0, 0).1, Archetype::HCrate); // immobile
assert_eq!(board.get(0, 0, 1).1, Archetype::Empty); // crate moved out
assert_eq!(board.get(0, 0, 2).1, Archetype::Crate); // moved from (0,1)
}
}
} }
+4 -4
View File
@@ -582,12 +582,12 @@ mod tests {
} }
#[test] #[test]
fn script_swap_moves_crates_simultaneously() { fn script_shift_rotates_crates() {
// Swap the crate at (2,0) with the empty cell at (3,0): one ends up empty, // Rotate the crate at (2,0) with the empty cell at (3,0) in a two-cell cycle:
// the other holds the crate, applied in a single batch. // crate moves to (3,0), empty moves back to (2,0).
let mut game = game_with_object_script( let mut game = game_with_object_script(
5, 5,
"fn tick(dt) { if Queue.length() == 0 { swap([[2, 0, 3, 0]]); } }", "fn tick(dt) { if Queue.length() == 0 { shift([[2, 0], [3, 0]]); } }",
); );
game.tick(Duration::from_millis(16)); game.tick(Duration::from_millis(16));
let b = game.board(); let b = game.board();