new layer model

This commit is contained in:
2026-07-10 23:16:28 -05:00
parent 325d2c27dd
commit ad95a9cd8d
36 changed files with 1040 additions and 897 deletions
+170 -213
View File
@@ -1,12 +1,33 @@
use crate::archetype::Archetype;
use crate::floor::Floor;
use crate::glyph::Glyph;
use crate::layer::Layer;
use crate::log::LogLine;
use crate::object_def::ObjectDef;
use crate::utils::Direction;
use crate::utils::{Behavior, ObjectId, PlayerPos, PortalDef, Pushable, RegistryValue, Solid};
use std::collections::{BTreeMap, HashMap, HashSet};
/// A non-solid `(glyph, archetype)` placed at a board coordinate, **outside** the
/// main grid, drawn only when the grid cell at `(x, y)` is empty.
///
/// Decorations exist so a single file format can serve both world files and save
/// files: at authoring time every cell holds at most one thing, but during play a
/// runtime solid can end up sharing a cell with a non-solid that was already there.
/// The non-solid is recorded here (the grid keeps the solid). The editor does not
/// place decorations; they are a save/runtime concern. A decoration's archetype is
/// always non-solid (a solid one is rejected at load).
#[derive(Clone)]
pub struct Decoration {
/// Column (0-indexed).
pub x: usize,
/// Row (0-indexed).
pub y: usize,
/// The decoration's visual.
pub glyph: Glyph,
/// The decoration's (non-solid) archetype.
pub archetype: Archetype,
}
/// The complete state of one game board (a single room or screen).
///
/// `Board` is the central data structure of the engine, equivalent to a
@@ -37,12 +58,18 @@ pub struct Board {
pub width: usize,
/// Height of the board in cells.
pub height: usize,
/// Ordered draw stack of [`Layer`]s, bottom (index 0) to top. Each layer holds
/// a row-major grid of `(Glyph, Archetype)` cells; a transparent cell lets the
/// layer beneath show through. Drawing ([`Board::glyph_at`]) walks the stack
/// top-down; solidity ([`Board::solid_at`]) scans every layer. Access a single
/// cell with [`Board::get`]/[`Board::get_mut`] by `(z, x, y)`.
pub(crate) layers: Vec<Layer>,
/// The single row-major grid of `(Glyph, Archetype)` cells (`width * height`),
/// holding every solid and most non-solids. A transparent cell (glyph tile 0)
/// draws nothing, revealing a [`decoration`](Board::decorations) or the
/// [`floor`](Board::floor) beneath. Access a cell with [`Board::get`]/
/// [`Board::get_mut`] by `(x, y)`.
pub(crate) grid: Vec<(Glyph, Archetype)>,
/// The board's cosmetic floor (blank / one fixed glyph / a biome), drawn beneath
/// everything. Replaces the old dedicated floor layer.
pub(crate) floor: Floor,
/// Non-solid things placed off the main grid, drawn only where the grid cell is
/// empty (see [`Decoration`]). Normally empty; populated by save files.
pub(crate) decorations: Vec<Decoration>,
/// Current player position on this board. See [`PlayerPos`] for caveats
/// about its future. Game-global player *stats* live in [`crate::player::Player`].
pub player: PlayerPos,
@@ -75,98 +102,91 @@ pub struct Board {
}
impl Board {
/// Number of draw layers on this board (≥ 1 for a loaded board).
pub fn layer_count(&self) -> usize {
self.layers.len()
}
/// Return a list of all `ObjectId`s currently on the board.
pub fn all_ids(&self) -> Vec<ObjectId> {
self.objects.keys().cloned().collect()
}
/// Returns a reference to the cell at `(x, y)` on layer `z`.
/// Returns a reference to the `(Glyph, Archetype)` cell at `(x, y)`.
///
/// The cell is a `(Glyph, Archetype)` tuple. Panics if `z`, `x`, or `y` are
/// out of bounds.
pub fn get(&self, z: usize, x: usize, y: usize) -> &(Glyph, Archetype) {
&self.layers[z].cells[y * self.width + x]
/// Panics if `x` or `y` are out of bounds.
pub fn get(&self, x: usize, y: usize) -> &(Glyph, Archetype) {
&self.grid[y * self.width + x]
}
/// Returns a mutable reference to the cell at `(x, y)` on layer `z`.
/// Returns a mutable reference to the cell at `(x, y)`.
///
/// Panics if `z`, `x`, or `y` are out of bounds.
pub fn get_mut(&mut self, z: usize, x: usize, y: usize) -> &mut (Glyph, Archetype) {
/// Panics if `x` or `y` are out of bounds.
pub fn get_mut(&mut self, x: usize, y: usize) -> &mut (Glyph, Archetype) {
let w = self.width;
&mut self.layers[z].cells[y * w + x]
&mut self.grid[y * w + x]
}
/// Replace the solid (if any) at `(x, y)` with `Empty`
/// Replace the solid terrain (if any) at `(x, y)` with a transparent `Empty`
/// cell, revealing the floor beneath.
pub fn clear_solid(&mut self, x: usize, y: usize) {
if self.in_bounds((x as i64, y as i64))
&& let Some(z) = self.solid_cell_layer(x, y) {
*self.get_mut(z, x, y) = (Archetype::Empty.default_glyph(), Archetype::Empty)
}
if self.in_bounds((x as i64, y as i64)) && self.get(x, y).1.behavior().solid {
*self.get_mut(x, y) = (Glyph::transparent(), Archetype::Empty);
}
}
/// Returns the glyph to display at `(x, y)`, honoring layer draw order.
/// Returns the glyph to display at `(x, y)`.
///
/// The player is always drawn on top (it is not part of the layer stack yet).
/// Otherwise the layers are walked **top-down**; the first thing that draws on
/// a layer wins:
/// With a single grid the draw order is a fixed precedence (no layer walk):
///
/// 1. an object on that layer (a solid object always; otherwise a non-solid
/// object whose glyph is not transparent, i.e. `tile != 0`),
/// 2. a portal on that layer,
/// 3. the layer's terrain cell — a solid always draws, and a non-solid draws
/// only when not transparent (`tile != 0`).
/// 1. the player (drawn on top; not part of the grid yet);
/// 2. an object at the cell — a solid object always, otherwise the first
/// non-transparent non-solid object (`tile != 0`, so invisible objects exist);
/// 3. the grid cell `(glyph, arch)` — a solid always draws, a non-solid only when
/// visible (`tile != 0`);
/// 4. a portal at the cell (portals sit on a transparent grid cell);
/// 5. a [`decoration`](Board::decorations) at the cell (reached only because the
/// grid cell was empty);
/// 6. the [`floor`](Board::floor) glyph, if any;
/// 7. the canonical black `Empty` glyph.
///
/// If no layer contributes anything, the canonical black `Empty` glyph is used.
/// Panics if out of bounds.
pub fn glyph_at(&self, x: usize, y: usize) -> Glyph {
// The player is rendered above the whole stack (see the `Player` notes).
// The player is rendered above everything (see the `Player` notes).
if self.player.x == x as i64 && self.player.y == y as i64 {
return Glyph::player();
}
for z in (0..self.layers.len()).rev() {
// Objects on this layer: a solid object always draws; otherwise the
// first non-transparent non-solid object (lets invisible objects exist).
let mut nonsolid: Option<Glyph> = None;
for o in self
.objects
.values()
.filter(|o| o.x == x && o.y == y && o.z == z)
{
if o.solid {
return o.glyph;
}
if nonsolid.is_none() && o.glyph.tile != 0 {
nonsolid = Some(o.glyph);
}
// Objects: a solid object always draws; otherwise the first non-transparent
// non-solid object (lets invisible trigger objects exist).
let mut nonsolid: Option<Glyph> = None;
for o in self.objects.values().filter(|o| o.x == x && o.y == y) {
if o.solid {
return o.glyph;
}
if let Some(g) = nonsolid {
return g;
}
// A portal on this layer draws above its (transparent) terrain cell.
if self
.portals
.iter()
.any(|p| p.x == x && p.y == y && p.z == z)
{
return PortalDef::default_glyph();
}
// The terrain cell: a solid always draws; a non-solid only if visible.
let (glyph, arch) = self.get(z, x, y);
if arch.behavior().solid || glyph.tile != 0 {
return *glyph;
if nonsolid.is_none() && o.glyph.tile != 0 {
nonsolid = Some(o.glyph);
}
}
if let Some(g) = nonsolid {
return g;
}
// Nothing on any layer: the canonical black empty cell.
Archetype::Empty.default_glyph()
// The grid cell: a solid always draws; a non-solid only if visible.
let (glyph, arch) = self.get(x, y);
if arch.behavior().solid || glyph.tile != 0 {
return *glyph;
}
// A portal sits on its (transparent) grid cell.
if self.portals.iter().any(|p| p.x == x && p.y == y) {
return PortalDef::default_glyph();
}
// The grid cell was empty: a decoration may show here.
if let Some(d) = self.decorations.iter().find(|d| d.x == x && d.y == y) {
return d.glyph;
}
// Then the floor, else the canonical black empty cell.
self.floor
.glyph_at(x, y, self.width)
.unwrap_or_else(|| Archetype::Empty.default_glyph())
}
/// Returns `true` if `(x, y)` is a valid cell coordinate on this board.
@@ -218,20 +238,14 @@ impl Board {
};
return Some(Solid::object_at(x, y, id, behavior));
}
// Otherwise some layer's terrain archetype may be solid (e.g. a wall).
if let Some(z) = self.solid_cell_layer(x, y) {
let (glyph, arch) = *self.get(z, x, y);
return Some(Solid::terrain_at(x, y, z, glyph, arch));
// Otherwise the grid cell's terrain archetype may be solid (e.g. a wall).
let (glyph, arch) = *self.get(x, y);
if arch.behavior().solid {
return Some(Solid::terrain_at(x, y, glyph, arch));
}
None
}
/// Returns the index of the layer whose terrain cell at `(x, y)` is solid, if
/// any. By the one-solid-per-cell invariant there is at most one such layer.
fn solid_cell_layer(&self, x: usize, y: usize) -> Option<usize> {
(0..self.layers.len()).find(|&z| self.get(z, x, y).1.behavior().solid)
}
/// Returns `true` if a mover can enter `(x, y)` — i.e. no solid occupies it.
///
/// Convenience inverse of [`solid_at`](Board::solid_at).
@@ -381,10 +395,9 @@ impl Board {
/// Moves the single solid occupant of `(x, y)` one step by `(dx, dy)`.
///
/// A solid object is relocated (keeping its layer); otherwise the solid
/// terrain archetype (a crate) is moved within its own layer, leaving a
/// transparent cell behind so the layer beneath (e.g. floor) shows through.
/// The caller guarantees the destination is already clear.
/// A solid object is relocated; otherwise the solid terrain archetype (a crate)
/// is moved, leaving a transparent cell behind so the floor shows through. The
/// caller guarantees the destination is already clear.
fn shift_solid(&mut self, x: usize, y: usize, dx: i64, dy: i64) {
let (tx, ty) = ((x as i64 + dx) as usize, (y as i64 + dy) as usize);
let Some(solid) = self.solid_at(x, y) else {
@@ -393,8 +406,8 @@ impl Board {
// A terrain cell leaves a transparent cell behind (revealing any floor); the
// player and objects carry no grid cell, so there is nothing to vacate. `place`
// captured the glyph/arch, so clearing the source first is safe.
if let Some(z) = self.solid_cell_layer(x, y) {
*self.get_mut(z, x, y) = (Glyph::transparent(), Archetype::Empty);
if self.get(x, y).1.behavior().solid {
*self.get_mut(x, y) = (Glyph::transparent(), Archetype::Empty);
}
solid.place(self, tx, ty);
}
@@ -465,23 +478,19 @@ impl Board {
/// drawing tools cannot place, remove, or alter a floor):
///
/// - **Terrain** (`arch != Empty`, always solid today): removes any solid object
/// already in the cell, then writes `(glyph, arch)` into the cell's terrain.
/// - **Erase** (`arch == Empty`): removes the cell's terrain *and* every object in
/// it, leaving the floor (a visible `Empty` cell on a lower layer) in place.
/// already in the cell, then writes `(glyph, arch)` into the grid cell.
/// - **Erase** (`arch == Empty`): removes the grid cell's terrain *and* every
/// object in it, leaving the floor beneath in place.
///
/// Terrain is written to the cell's existing terrain layer (the single non-`Empty`
/// archetype across layers, if any) or else the top layer; a vacated terrain cell
/// becomes a transparent `Empty` so a lower floor shows through. Panics if `(x, y)`
/// is out of bounds.
/// A vacated grid cell becomes a transparent `Empty` so the floor shows through.
/// Panics if `(x, y)` is out of bounds.
pub fn place_archetype(&mut self, x: usize, y: usize, arch: Archetype, glyph: Glyph) {
if arch == Archetype::Empty {
// Erase: drop every object in the cell and clear its terrain (keep floor).
// Erase: drop every object in the cell and clear its grid cell (keep floor).
for id in self.object_ids_at(x, y) {
self.objects.remove(&id);
}
if let Some(z) = self.terrain_layer_at(x, y) {
*self.get_mut(z, x, y) = (Glyph::transparent(), Archetype::Empty);
}
*self.get_mut(x, y) = (Glyph::transparent(), Archetype::Empty);
return;
}
@@ -489,10 +498,7 @@ impl Board {
if let Some(id) = self.solid_object_id_at(x, y) {
self.objects.remove(&id);
}
// Reuse the existing terrain layer if the cell already has terrain, else the
// top layer (so the new wall draws above any floor on a lower layer).
let z = self.terrain_layer_at(x, y).unwrap_or(self.layers.len() - 1);
*self.get_mut(z, x, y) = (glyph, arch);
*self.get_mut(x, y) = (glyph, arch);
}
/// Replaces every terrain cell whose archetype is script-backed (e.g. a
@@ -509,26 +515,23 @@ impl Board {
/// already loaded as objects are untouched, so it is safe to call more than once.
pub fn expand_builtin_archetypes(&mut self) {
use crate::builtin_scripts::builtin_tag;
// Collect first: the loop below mutates both layers and the object map.
let mut found: Vec<(usize, usize, usize, Glyph, Archetype)> = Vec::new();
for z in 0..self.layers.len() {
for y in 0..self.height {
for x in 0..self.width {
let (glyph, arch) = *self.get(z, x, y);
if matches!(arch, Archetype::Builtin(_, _)) {
found.push((z, x, y, glyph, arch));
}
// Collect first: the loop below mutates both the grid and the object map.
let mut found: Vec<(usize, usize, Glyph, Archetype)> = Vec::new();
for y in 0..self.height {
for x in 0..self.width {
let (glyph, arch) = *self.get(x, y);
if matches!(arch, Archetype::Builtin(_, _)) {
found.push((x, y, glyph, arch));
}
}
}
for (z, x, y, glyph, arch) in found {
// Vacate the terrain cell (revealing any floor beneath), then spawn the
for (x, y, glyph, arch) in found {
// Vacate the grid cell (revealing any floor beneath), then spawn the
// object — mirroring `resolve_entry`'s object template.
*self.get_mut(z, x, y) = (Glyph::transparent(), Archetype::Empty);
*self.get_mut(x, y) = (Glyph::transparent(), Archetype::Empty);
let Archetype::Builtin(b, _alias) = arch else { continue };
let beh = b.behavior();
let mut obj = ObjectDef::new(x, y);
obj.z = z;
obj.glyph = glyph;
obj.solid = beh.solid;
obj.opaque = beh.opaque;
@@ -617,13 +620,6 @@ impl Board {
vec![]
}
/// Returns the index of the layer whose terrain cell at `(x, y)` is non-`Empty`
/// (the cell's single terrain archetype, if any). By the one-solid-per-cell
/// invariant there is at most one such layer.
fn terrain_layer_at(&self, x: usize, y: usize) -> Option<usize> {
(0..self.layers.len()).find(|&z| self.get(z, x, y).1 != Archetype::Empty)
}
/// Clear the queues of all objects on this board: called when entering a board, objects
/// don't retain their state across board visits (they get initialized again, but can
/// store things in the board registry)
@@ -638,20 +634,20 @@ impl Board {
pub(crate) mod tests {
use super::Board;
use crate::archetype::{Archetype, Builtin};
use crate::floor::Floor;
use crate::glyph::Glyph;
use crate::layer::Layer;
use crate::object_def::ObjectDef;
use crate::utils::Direction;
use crate::utils::{ObjectId, PlayerPos};
use color::Rgba8;
use std::collections::{BTreeMap, HashMap};
/// Builds an all-empty `w×h` single-layer board with the given player position
/// and objects (all on layer 0). Assigns sequential ids (1..=n) to objects.
/// Builds an all-empty `w×h` board with the given player position and objects.
/// Assigns sequential ids (1..=n) to objects.
///
/// The single layer is fully transparent, so terrain stamped via [`crate_at`]
/// etc. always lands on the board's top layer. Use [`add_floor`] to slip a
/// visible floor layer underneath.
/// The grid is fully transparent (a blank floor), so terrain stamped via
/// [`crate_at`] etc. lands on the single grid. Use [`add_floor`] to give the
/// board a visible fixed floor underneath.
pub(crate) fn open_board(
w: usize,
h: usize,
@@ -669,9 +665,9 @@ pub(crate) mod tests {
name: "test".into(),
width: w,
height: h,
layers: vec![Layer {
cells: vec![(Glyph::transparent(), Archetype::Empty); w * h],
}],
grid: vec![(Glyph::transparent(), Archetype::Empty); w * h],
floor: Floor::Blank,
decorations: Vec::new(),
player: PlayerPos {
x: player.0,
y: player.1,
@@ -685,42 +681,25 @@ pub(crate) mod tests {
}
}
/// Inserts a visible floor layer (filled with `glyph`) below everything,
/// bumping existing terrain and objects up one layer.
/// Gives the board a uniform fixed floor glyph (the single-grid replacement for
/// the old separate floor layer).
pub(crate) fn add_floor(board: &mut Board, glyph: Glyph) {
let count = board.width * board.height;
board.layers.insert(
0,
Layer {
cells: vec![(glyph, Archetype::Empty); count],
},
);
for o in board.objects.values_mut() {
o.z += 1;
}
board.floor = Floor::Fixed(glyph);
}
/// The index of the board's top (terrain) layer, where stamps are written.
fn top(board: &Board) -> usize {
board.layers.len() - 1
}
/// Stamps a crate cell onto the board's top layer.
/// Stamps a crate cell onto the grid.
pub(crate) fn crate_at(board: &mut Board, x: usize, y: usize) {
let z = top(board);
*board.get_mut(z, x, y) = (Archetype::Crate.default_glyph(), Archetype::Crate);
*board.get_mut(x, y) = (Archetype::Crate.default_glyph(), Archetype::Crate);
}
/// Stamps a wall cell onto the board's top layer.
/// Stamps a wall cell onto the grid.
pub(crate) fn wall_at(board: &mut Board, x: usize, y: usize) {
let z = top(board);
*board.get_mut(z, x, y) = (Archetype::Wall.default_glyph(), Archetype::Wall);
*board.get_mut(x, y) = (Archetype::Wall.default_glyph(), Archetype::Wall);
}
/// Stamps an arbitrary archetype cell onto the board's top layer.
/// Stamps an arbitrary archetype cell onto the grid.
pub(crate) fn stamp(board: &mut Board, x: usize, y: usize, arch: Archetype) {
let z = top(board);
*board.get_mut(z, x, y) = (arch.default_glyph(), arch);
*board.get_mut(x, y) = (arch.default_glyph(), arch);
}
#[test]
@@ -790,8 +769,8 @@ pub(crate) mod tests {
let mut board = open_board(3, 1, (0, 0), vec![]);
crate_at(&mut board, 1, 0);
assert!(board.can_push(1, 0, Direction::East));
assert_eq!(board.get(0, 1, 0).1, Archetype::Crate); // no mutation
assert_eq!(board.get(0, 2, 0).1, Archetype::Empty);
assert_eq!(board.get(1, 0).1, Archetype::Crate); // no mutation
assert_eq!(board.get(2, 0).1, Archetype::Empty);
let mut board = open_board(3, 1, (0, 0), vec![]);
crate_at(&mut board, 1, 0);
@@ -808,7 +787,7 @@ pub(crate) mod tests {
// Crate with open space ahead: shiftable.
crate_at(&mut board, 0, 0);
assert!(board.can_shift(0, 0, Direction::East));
assert_eq!(board.get(0, 0, 0).1, Archetype::Crate); // read-only
assert_eq!(board.get(0, 0).1, Archetype::Crate); // read-only
// Crate with another pushable crate ahead: still shiftable (unlike can_push,
// which would follow the chain to the wall and fail).
@@ -854,8 +833,8 @@ pub(crate) mod tests {
crate_at(&mut board, 1, 0);
assert!(board.can_push(1, 0, Direction::East));
board.push(1, 0, Direction::East);
assert_eq!(board.get(0, 1, 0).1, Archetype::Empty);
assert_eq!(board.get(0, 2, 0).1, Archetype::Crate);
assert_eq!(board.get(1, 0).1, Archetype::Empty);
assert_eq!(board.get(2, 0).1, Archetype::Crate);
assert_eq!((board.player.x, board.player.y), (3, 0));
}
@@ -867,7 +846,7 @@ pub(crate) mod tests {
wall_at(&mut board, 3, 0);
assert!(!board.can_push(1, 0, Direction::East));
board.push(1, 0, Direction::East); // no-op
assert_eq!(board.get(0, 1, 0).1, Archetype::Crate);
assert_eq!(board.get(1, 0).1, Archetype::Crate);
assert_eq!((board.player.x, board.player.y), (2, 0));
}
@@ -890,7 +869,7 @@ pub(crate) mod tests {
a: 255,
},
};
// Floor on a lower layer, a wall on the top (terrain) layer at (0,0).
// A fixed floor attribute, a wall on the grid at (0,0).
add_floor(&mut board, floor_glyph);
wall_at(&mut board, 0, 0);
// The wall (solid) draws over the floor; the empty cell reveals the floor.
@@ -898,29 +877,9 @@ pub(crate) mod tests {
assert_eq!(board.glyph_at(1, 0), floor_glyph);
}
#[test]
fn glyph_at_draws_higher_layer_over_lower() {
// A non-solid object on an upper layer renders above a wall on a lower one.
let mut board = open_board(2, 1, (1, 0), vec![]);
wall_at(&mut board, 0, 0); // wall on layer 0
// Add an upper layer holding a visible, non-solid object at (0,0).
board.layers.push(Layer {
cells: vec![(Glyph::transparent(), Archetype::Empty); 2],
});
let mut obj = ObjectDef::new(0, 0);
obj.z = 1;
obj.solid = false;
obj.glyph = Glyph {
tile: '*' as u32,
..Glyph::transparent()
};
board.add_object(obj);
assert_eq!(board.glyph_at(0, 0).tile, '*' as u32);
}
#[test]
fn place_wall_keeps_floor_and_removes_solid_object() {
// Floor on layer 0, terrain layer on top; a solid object sits at (1,0).
// A fixed floor attribute; a solid object sits on the grid at (1,0).
let mut board = open_board(3, 1, (2, 0), vec![ObjectDef::new(1, 0)]);
let floor = Glyph {
tile: '.' as u32,
@@ -931,9 +890,8 @@ pub(crate) mod tests {
let wall = Archetype::Wall.default_glyph();
board.place_archetype(1, 0, Archetype::Wall, wall);
// The wall landed on the terrain (top) layer; the floor below is untouched.
assert_eq!(board.get(1, 1, 0), &(wall, Archetype::Wall));
assert_eq!(board.get(0, 1, 0).0, floor);
// The wall landed on the grid; the floor attribute is untouched.
assert_eq!(board.get(1, 0), &(wall, Archetype::Wall));
// The solid object that was there is gone.
assert!(board.object_ids_at(1, 0).is_empty());
assert_eq!(board.glyph_at(1, 0), wall);
@@ -941,17 +899,17 @@ pub(crate) mod tests {
#[test]
fn place_wall_overwrites_existing_terrain_in_place() {
// A crate already occupies the top layer at (1,0).
// A crate already occupies the grid at (1,0).
let mut board = open_board(3, 1, (2, 0), vec![]);
crate_at(&mut board, 1, 0);
let wall = Archetype::Wall.default_glyph();
board.place_archetype(1, 0, Archetype::Wall, wall);
assert_eq!(board.get(0, 1, 0), &(wall, Archetype::Wall));
assert_eq!(board.get(1, 0), &(wall, Archetype::Wall));
}
#[test]
fn erase_removes_terrain_and_objects_but_keeps_floor() {
// Floor, a wall on the terrain layer, and a (non-solid) object all at (1,0).
// A fixed floor attribute, a wall on the grid, and a (non-solid) object at (1,0).
let mut obj = ObjectDef::new(1, 0);
obj.solid = false;
let mut board = open_board(3, 1, (2, 0), vec![obj]);
@@ -964,13 +922,12 @@ pub(crate) mod tests {
board.place_archetype(1, 0, Archetype::Empty, Glyph::transparent());
// Terrain cleared to transparent Empty; object removed; floor still there.
// Grid cell cleared to transparent Empty; object removed; floor still there.
assert_eq!(
board.get(1, 1, 0),
board.get(1, 0),
&(Glyph::transparent(), Archetype::Empty)
);
assert!(board.object_ids_at(1, 0).is_empty());
assert_eq!(board.get(0, 1, 0).0, floor);
assert_eq!(board.glyph_at(1, 0), floor);
}
@@ -990,7 +947,7 @@ pub(crate) mod tests {
board.expand_builtin_archetypes();
// The terrain cell is vacated and a scripted object takes its place.
assert_eq!(board.get(0, 0, 0).1, Archetype::Empty);
assert_eq!(board.get(0, 0).1, Archetype::Empty);
let obj = board.objects.values().next().expect("spinner object");
assert_eq!((obj.x, obj.y), (0, 0));
assert!(obj.solid);
@@ -1019,7 +976,7 @@ pub(crate) mod tests {
crate_at(&mut board, 0, 0);
let errs = board.apply_shift(&[(0, 0), (9, 0)]);
assert_eq!(errs.len(), 1);
assert_eq!(board.get(0, 0, 0).1, Archetype::Crate); // unchanged
assert_eq!(board.get(0, 0).1, Archetype::Crate); // unchanged
}
#[test]
@@ -1043,11 +1000,11 @@ pub(crate) mod tests {
// 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)
assert_eq!(board.get(0, 0).1, Archetype::Crate); // wrapped from (4,0)
assert_eq!(board.get(1, 0).1, Archetype::Crate); // moved from (0,0)
assert_eq!(board.get(2, 0).1, Archetype::Wall); // blocked, immobile
assert_eq!(board.get(3, 0).1, Archetype::Empty); // cleared, crate moved
assert_eq!(board.get(4, 0).1, Archetype::Crate); // moved from (3,0)
}
#[test]
@@ -1066,9 +1023,9 @@ pub(crate) mod tests {
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)
assert_eq!(board.get(0, 0).1, Archetype::Empty); // HCrate moved out
assert_eq!(board.get(1, 0).1, Archetype::HCrate); // moved from (0,0)
assert_eq!(board.get(2, 0).1, Archetype::Crate); // moved from (1,0)
}
// Subcase B: VCrate stays; Crate behind it still moves
@@ -1084,9 +1041,9 @@ pub(crate) mod tests {
// 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)
assert_eq!(board.get(0, 0).1, Archetype::VCrate); // immobile
assert_eq!(board.get(1, 0).1, Archetype::Empty); // crate moved out
assert_eq!(board.get(2, 0).1, Archetype::Crate); // moved from (1,0)
}
}
@@ -1105,9 +1062,9 @@ pub(crate) mod tests {
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)
assert_eq!(board.get(0, 0).1, Archetype::Empty); // VCrate moved out
assert_eq!(board.get(0, 1).1, Archetype::VCrate); // moved from (0,0)
assert_eq!(board.get(0, 2).1, Archetype::Crate); // moved from (0,1)
}
// Subcase B: HCrate stays; Crate behind it still moves
@@ -1123,9 +1080,9 @@ pub(crate) mod tests {
// 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)
assert_eq!(board.get(0, 0).1, Archetype::HCrate); // immobile
assert_eq!(board.get(0, 1).1, Archetype::Empty); // crate moved out
assert_eq!(board.get(0, 2).1, Archetype::Crate); // moved from (0,1)
}
}