pointify board.rs

This commit is contained in:
2026-08-10 22:57:54 -05:00
parent a8444b25ad
commit 3b5bf21ac5
4 changed files with 156 additions and 217 deletions
+1 -10
View File
@@ -177,7 +177,7 @@ pub fn apply_teleport(board: &mut Board, target: i64, x: i64, y: i64) -> Result<
};
if let Some(from) = from {
// Check if we're blocked:
if (from.0 != x || from.1 != y) && board.get(x, y).is_none() {
if (from.0 != x || from.1 != y) && board.get((x, y)).is_none() {
// Not blocked, move it
board.move_cell(from.into(), (x, y).into());
}
@@ -188,15 +188,6 @@ pub fn apply_teleport(board: &mut Board, target: i64, x: i64, y: i64) -> Result<
}
}
pub fn apply_push(board: &mut Board, x: i64, y: i64, dir: Direction) -> Result<(), String> {
if !board.in_bounds((x, y)) {
Err(format!("push({x},{y}): out of bounds"))
} else {
board.push(x as usize, y as usize, dir);
Ok(())
}
}
pub fn apply_shift(board: &mut Board, cells: &[(i64, i64)]) -> Result<(), String> {
board.apply_shift(cells)?;
Ok(())
+148 -205
View File
@@ -86,23 +86,26 @@ impl Board {
/// Returns a reference to the `(Glyph, Archetype)` cell at `(x, y)`.
///
/// Panics if `x` or `y` are out of bounds.
pub fn get(&self, x: usize, y: usize) -> &Option<Tile> {
&self.grid[y * self.width + x]
pub fn get<P: Into<Point>>(&self, p: P) -> &Option<Tile> {
let p = p.into();
&self.grid[p.uy() * self.width + p.ux()]
}
/// Returns a mutable reference to the cell at `(x, y)`.
///
/// Panics if `x` or `y` are out of bounds.
pub fn get_mut(&mut self, x: usize, y: usize) -> &mut Option<Tile> {
pub fn get_mut<P: Into<Point>>(&mut self, p: P) -> &mut Option<Tile> {
let p = p.into();
let w = self.width;
&mut self.grid[y * w + x]
&mut self.grid[p.uy() * w + p.ux()]
}
/// Replace the solid terrain (if any) at `(x, y)` with a transparent `Empty`
/// cell, revealing the floor beneath.
pub fn clear_cell(&mut self, x: usize, y: usize) {
if self.in_bounds((x as i64, y as i64)) {
*self.get_mut(x, y) = None;
pub fn clear_cell<P: Into<Point>>(&mut self, p: P) {
let p = p.into();
if self.in_bounds(p) {
*self.get_mut(p) = None;
}
}
@@ -122,10 +125,11 @@ impl Board {
/// 7. the canonical black `Empty` glyph.
///
/// Panics if out of bounds.
pub fn glyph_at(&self, x: usize, y: usize) -> Glyph {
let grid_glyph = self.get(x, y).as_ref().map(Tile::glyph);
pub fn glyph_at<P: Into<Point>>(&self, p: P) -> Glyph {
let p = p.into();
let grid_glyph = self.get(p).as_ref().map(Tile::glyph);
let sensors = self.sensors.iter().filter(|&s| s.x == x && s.y == y);
let sensors = self.sensors.iter().filter(|&s| s.x == p.ux() && s.y == p.uy());
// Is there a sensor above the grid?
if let Some(above) = sensors.clone().find(|s| s.draw_layer == DrawLayer::Above && s.scripting.glyph.is_visible()) {
@@ -144,7 +148,7 @@ impl Board {
// Otherwise the floor, or the canonical black empty cell.
self.floor
.glyph_at(x, y, self.width)
.glyph_at(p.ux(), p.uy(), self.width)
.unwrap_or_else(|| Glyph::transparent())
}
@@ -152,17 +156,9 @@ impl Board {
///
/// Takes signed coords so callers can pass a raw `pos + delta` without first
/// checking for negatives.
pub fn in_bounds(&self, pos: (i64, i64)) -> bool {
let (x, y) = pos;
x >= 0 && y >= 0 && (x as usize) < self.width && (y as usize) < self.height
}
/// Returns `true` if a mover can enter `(x, y)` — i.e. no solid occupies it.
///
/// Convenience inverse of [`solid_at`](Board::solid_at).
/// Panics if `x` or `y` are out of bounds.
pub fn is_passable(&self, x: usize, y: usize) -> bool {
self.get(x, y).is_none()
pub fn in_bounds<P: Into<Point>>(&self, pos: P) -> bool {
let p = pos.into();
p.x >= 0 && p.y >= 0 && p.ux() < self.width && p.uy() < self.height
}
/// Returns `true` if cell `(x, y)` blocks line of sight (and light).
@@ -171,11 +167,12 @@ impl Board {
/// **or** any object on it is opaque. This is the input to lighting on
/// [`dark`](Board::dark) boards; see [`Board::lighting`].
/// Panics if `x` or `y` are out of bounds.
pub fn is_opaque_at(&self, x: usize, y: usize) -> bool {
if self.sensors.iter().any(|s| s.x == x && s.y == y && s.scripting.optics.opaque) {
pub fn is_opaque_at<P: Into<Point>>(&self, p: P) -> bool {
let p = p.into();
if self.sensors.iter().any(|s| s.x == p.ux() && s.y == p.uy() && s.scripting.optics.opaque) {
true
} else {
match self.get(x, y) {
match self.get(p) {
None => false,
Some(Tile::Player) => false,
Some(Tile::Object(def)) => {
@@ -203,9 +200,12 @@ impl Board {
let mut lighting = Lighting::new(w, h);
// One caster whose transparency is seeded once from the opaque cells;
// reused for the LOS pass and every light source (its FOV is cleared per cast).
let mut caster = FovCaster::new(w, h, |x, y| !self.is_opaque_at(x, y));
let mut caster = FovCaster::new(w, h, |x, y| !self.is_opaque_at((x, y)));
let (px, py) = self.player_pos();
let (px, py) = {
let p = self.player_pos();
(p.ux(), p.uy())
};
// (a) Player line of sight — unbounded (radius 0), pure geometry.
caster.cast(px, py, 0, |x, y| lighting.set_los(x, y));
@@ -229,7 +229,7 @@ impl Board {
// Glowing terrain (e.g. a `Torch` cell): color = the cell's glyph foreground.
for y in 0..h {
for x in 0..w {
if let Some(Tile::Object(obj)) = self.get(x, y) {
if let Some(Tile::Object(obj)) = self.get((x, y)) {
let radius = obj.scripting.optics.glow;
if radius > 0 {
add_source(&mut lighting, x, y, radius, color_to_rgb(obj.scripting.glyph.fg));
@@ -247,180 +247,121 @@ impl Board {
Some(lighting)
}
/// Whether the cell's single solid occupant (if any) can be pushed in `dir`.
///
/// This is whether the thing in this cell will _transmit_ a push impulse through
/// it. Empty cells and cells that can't be pushed in that direction break the
/// chain.
fn is_pushable(&self, x: usize, y: usize, dir: Direction) -> bool {
match self.get(x, y) {
Some(Tile::Player) => true,
Some(Tile::Object(obj)) => {
if let EnterResponse::Push(p) = obj.enter_response {
p.allows(dir)
} else {
false
}
}
_ => false,
}
}
// /// Whether the solid at `(x, y)` can be **shifted** one step in `dir`: it is
// /// itself a pushable solid *and* the next cell is either empty or holds another
// /// pushable solid.
// ///
// /// Unlike [`can_push`](Board::can_push) this inspects only the single cell
// /// ahead — it does **not** verify the whole chain ends in open space. It is the
// /// right test for a simultaneous rotation/shift (applied via
// /// [`apply_swap`](Board::apply_swap)), where a destination is occupied by
// /// another pushable that is itself moving the same frame. Returns `false` if
// /// `(x, y)` holds no pushable, or the cell ahead runs off the board.
// ///
// /// The **player** is always a blocker: a shift can't relocate the player, and
// /// `apply_swap` refuses to overwrite it, so a cell holding the player is never
// /// an acceptable shift destination.
// pub fn can_shift(&self, x: usize, y: usize, dir: Direction) -> bool {
// // The source must hold a solid pushable in `dir`.
// if !self.is_pushable(x, y, dir) {
// return false;
// }
// let (dx, dy): (i64, i64) = dir.into();
// let next = (x as i64 + dx, y as i64 + dy);
// if !self.in_bounds(next) {
// return false; // nothing to shift into off the board
// }
// let (nx, ny) = (next.0 as usize, next.1 as usize);
// // The player always blocks a shift: a shift can't relocate it and
// // `apply_swap` refuses to overwrite it. (The player reads as pushable, so it
// // must be excluded explicitly before the cell-ahead test below.)
// if self.get(nx, ny).as_ref().is_some_and(Tile::player) {
// return false;
// }
// // The cell ahead is acceptable if it is empty or another pushable solid.
// self.is_passable(nx, ny) || self.is_pushable(nx, ny, dir)
// }
/// Whether the chain of pushable solids starting at `(x, y)` can be shoved one
/// step in `dir` — i.e. the chain ends at a passable cell rather than the board
/// edge or a non-pushable solid.
///
/// Read-only (`&self`); pairs with [`push`](Board::push). Returns `false` when
/// `(x, y)` itself holds no pushable solid, so it doubles as the "is the cell
/// ahead shovable?" half of a "can I move here?" query.
pub fn can_push(&self, x: usize, y: usize, dir: Direction) -> bool {
let (dx, dy): (i64, i64) = dir.into();
let (mut cx, mut cy) = (x, y);
loop {
// This cell must hold a solid pushable in `dir` to advance the chain.
if !self.is_pushable(cx, cy, dir) {
return false;
}
let next = (cx as i64 + dx, cy as i64 + dy);
if !self.in_bounds(next) {
return false; // chain runs off the board
}
let (nx, ny) = (next.0 as usize, next.1 as usize);
if self.is_passable(nx, ny) {
return true; // open space at the end: the whole chain can move
}
// Next cell holds a solid too; continue (it must itself be pushable).
cx = nx;
cy = ny;
}
}
// /// Shoves the chain of pushable solids starting at `(x, y)` one step in `dir`,
// /// leaving `Empty` floor behind each moved cell.
// ///
// /// No-op when the chain can't move (it self-checks via [`can_push`](Board::can_push)),
// /// so it is safe to call unconditionally.
// /// Returns the cells the shoved solids moved **into** (each chain cell stepped
// /// one cell in `dir`), so the caller can fire `enter` on any non-solid object a
// /// pushed solid landed on. Empty when nothing moved.
// pub fn push(&mut self, x: usize, y: usize, dir: Direction) -> Vec<(usize, usize)> {
// if !self.can_push(x, y, dir) {
// return Vec::new();
// }
// let (dx, dy): (i64, i64) = dir.into();
// // can_push guaranteed the chain ends at an in-bounds passable cell, so
// // re-walk it (no bounds checks needed) and shift the far end first, which
// // keeps each destination cell vacated before its occupant arrives.
// let mut chain: Vec<(usize, usize)> = Vec::new();
// let (mut cx, mut cy) = (x, y);
// while !self.is_passable(cx, cy) {
// chain.push((cx, cy));
// cx = (cx as i64 + dx) as usize;
// cy = (cy as i64 + dy) as usize;
// }
// for &(px, py) in chain.iter().rev() {
// self.shift_solid(px, py, dx, dy);
// }
// // Each solid ended up one step along `dir`; those destination cells are
// // where an `enter` may need to fire.
// chain
// .iter()
// .map(|&(px, py)| ((px as i64 + dx) as usize, (py as i64 + dy) as usize))
// .collect()
// }
/// Whether the solid at `(x, y)` can be **shifted** one step in `dir`: it is
/// itself a pushable solid *and* the next cell is either empty or holds another
/// pushable solid.
///
/// Unlike [`can_push`](Board::can_push) this inspects only the single cell
/// ahead — it does **not** verify the whole chain ends in open space. It is the
/// right test for a simultaneous rotation/shift (applied via
/// [`apply_swap`](Board::apply_swap)), where a destination is occupied by
/// another pushable that is itself moving the same frame. Returns `false` if
/// `(x, y)` holds no pushable, or the cell ahead runs off the board.
///
/// The **player** is always a blocker: a shift can't relocate the player, and
/// `apply_swap` refuses to overwrite it, so a cell holding the player is never
/// an acceptable shift destination.
pub fn can_shift(&self, x: usize, y: usize, dir: Direction) -> bool {
// The source must hold a solid pushable in `dir`.
if !self.is_pushable(x, y, dir) {
return false;
}
let (dx, dy): (i64, i64) = dir.into();
let next = (x as i64 + dx, y as i64 + dy);
if !self.in_bounds(next) {
return false; // nothing to shift into off the board
}
let (nx, ny) = (next.0 as usize, next.1 as usize);
// The player always blocks a shift: a shift can't relocate it and
// `apply_swap` refuses to overwrite it. (The player reads as pushable, so it
// must be excluded explicitly before the cell-ahead test below.)
if self.get(nx, ny).as_ref().is_some_and(Tile::player) {
return false;
}
// The cell ahead is acceptable if it is empty or another pushable solid.
self.is_passable(nx, ny) || self.is_pushable(nx, ny, dir)
}
/// Shoves the chain of pushable solids starting at `(x, y)` one step in `dir`,
/// leaving `Empty` floor behind each moved cell.
///
/// No-op when the chain can't move (it self-checks via [`can_push`](Board::can_push)),
/// so it is safe to call unconditionally.
/// Returns the cells the shoved solids moved **into** (each chain cell stepped
/// one cell in `dir`), so the caller can fire `enter` on any non-solid object a
/// pushed solid landed on. Empty when nothing moved.
pub fn push(&mut self, x: usize, y: usize, dir: Direction) -> Vec<(usize, usize)> {
if !self.can_push(x, y, dir) {
return Vec::new();
}
let (dx, dy): (i64, i64) = dir.into();
// can_push guaranteed the chain ends at an in-bounds passable cell, so
// re-walk it (no bounds checks needed) and shift the far end first, which
// keeps each destination cell vacated before its occupant arrives.
let mut chain: Vec<(usize, usize)> = Vec::new();
let (mut cx, mut cy) = (x, y);
while !self.is_passable(cx, cy) {
chain.push((cx, cy));
cx = (cx as i64 + dx) as usize;
cy = (cy as i64 + dy) as usize;
}
for &(px, py) in chain.iter().rev() {
self.shift_solid(px, py, dx, dy);
}
// Each solid ended up one step along `dir`; those destination cells are
// where an `enter` may need to fire.
chain
.iter()
.map(|&(px, py)| ((px as i64 + dx) as usize, (py as i64 + dy) as usize))
.collect()
}
/// Tries to walk the object in x,y in the given direction. Pushes the cell in that direction
/// first. This won't move anything if:
/// - the cell x,y is empty; no-op
/// - x,y is out of bounds, no-op
/// - the target cell can't be pushed in that direction, either because it's `Block` or
/// something in its chain is
pub fn move_object(&mut self, x: usize, y: usize, dir: Direction) {
// Is this even a cell?
if self.in_bounds((x as i64, y as i64)) {
// Is there anything in this cell?
if self.get(x, y).is_some() {
let (tx, ty) = dir.from_point(x as i64, y as i64);
// Is the _target_ in bounds?
if self.in_bounds((tx, ty)) {
// Attempt to push the target out of the way
self.push(tx as usize, ty as usize, dir);
// Is the cell now empty?
if self.get(tx as usize, ty as usize).is_none() {
// Then finally, move the thing:
let thing = self.grid[x + y * self.width].take();
self.grid[tx as usize + ty as usize * self.width] = thing;
}
}
}
}
}
/// Moves the single solid occupant of `(x, y)` one step by `(dx, dy)`.
///
/// 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);
if self.get(x, y).is_none() {
return; // nothing to shift
}
*self.get_mut(tx, ty) = self.get_mut(x, y).take();
}
// /// Tries to walk the object in x,y in the given direction. Pushes the cell in that direction
// /// first. This won't move anything if:
// /// - the cell x,y is empty; no-op
// /// - x,y is out of bounds, no-op
// /// - the target cell can't be pushed in that direction, either because it's `Block` or
// /// something in its chain is
// pub fn move_object<P: Into<Point>>(&mut self, p: P, dir: Direction) {
// let p = p.into();
// // Is this even a cell?
// if self.in_bounds(p) {
// // Is there anything in this cell?
// if !self.is_empty(p) {
// let (tx, ty) = dir.from_point(p.x, p.y);
// // Is the _target_ in bounds?
// if self.in_bounds((tx, ty)) {
// // Attempt to push the target out of the way
// self.push(tx as usize, ty as usize, dir);
// // Is the cell now empty?
// if self.get(tx as usize, ty as usize).is_none() {
// // Then finally, move the thing:
// let thing = self.grid[x + y * self.width].take();
// self.grid[tx as usize + ty as usize * self.width] = thing;
// }
// }
// }
// }
// }
/// Returns the [`ObjectId`]s of the **non-solid** objects at `(x, y)`.
///
/// These are the targets of an `enter` hook when a solid relocates onto the
/// cell (terrain is always solid, so only objects can be non-solid). Mirrors
/// [`object_ids_at`](Board::object_ids_at) / [`solid_object_id_at`](Board::solid_object_id_at).
pub fn sensor_ids_at(&self, x: usize, y: usize) -> Vec<ObjectId> {
pub fn sensor_ids_at<P: Into<Point>>(&self, p: P) -> Vec<ObjectId> {
let p = p.into();
self.sensors
.iter()
.filter(|s| s.x == x && s.y == y)
.filter(|s| s.x == p.ux() && s.y == p.uy())
.map(|s| s.scripting.id)
.collect()
}
/// Find and return the portal at the given location
pub fn portal_at(&self, x: usize, y: usize) -> Option<&Portal> {
self.portals.iter().find(|&portal| portal.location() == (x, y))
pub fn portal_at<P: Into<Point>>(&self, p: P) -> Option<&Portal> {
let p = p.into();
self.portals.iter().find(|&portal| portal.location() == (p.ux(), p.uy()))
}
/// Editor primitive: stamps `arch` (with visual `glyph`) into the cell at
@@ -436,12 +377,12 @@ impl Board {
///
/// A vacated grid cell becomes a transparent `Empty` so the floor shows through.
/// Panics if `(x, y)` is out of bounds.
pub fn place(&mut self, x: usize, y: usize, spec: Option<TileSpec>) -> Result<(), String> {
pub fn place<P: Into<Point>>(&mut self, p: P, spec: Option<TileSpec>) -> Result<(), String> {
if let Some(spec) = spec {
let tile = spec.into_tile(&mut self.next_object_id)?;
*self.get_mut(x, y) = Some(tile);
*self.get_mut(p) = Some(tile);
} else {
*self.get_mut(x, y) = None;
*self.get_mut(p) = None;
}
Ok(())
}
@@ -457,7 +398,7 @@ impl Board {
}
// Get all the Solids at these cells:
let solids: Vec<_> = cells.iter().map(|&c| self.get_mut(c.0 as usize, c.1 as usize).take()).collect();
let solids: Vec<_> = cells.iter().map(|&c| self.get_mut(c).take()).collect();
// Find which ones are blockers
let mut immobile = HashSet::new();
@@ -491,12 +432,12 @@ impl Board {
// Not blocked, write it to target
let origin = cells[curr_idx];
let target = cells[(curr_idx + 1) % cells.len()];
*self.get_mut(target.0 as usize, target.1 as usize) = Some(solid);
*self.get_mut(target) = Some(solid);
moves.push((origin, target));
} else {
// it was blocked so just write it back where it was
let origin = cells[curr_idx];
*self.get_mut(origin.0 as usize, origin.1 as usize) = Some(solid);
*self.get_mut(origin) = Some(solid);
}
}
}
@@ -515,10 +456,10 @@ impl Board {
}
}
pub fn player_pos(&self) -> (usize, usize) {
pub fn player_pos(&self) -> Point {
self.grid.iter().enumerate().find_map(|(i, cell)| {
if matches!(cell, Some(Tile::Player)) {
Some((i % self.width, i / self.width))
Some((i % self.width, i / self.width).into())
} else { None }
}).expect("No player found!") // This should never happen, player presence is validated when building a board
}
@@ -622,26 +563,28 @@ impl Board {
/// Moves whatever is in `from` to `to`, leaving an empty cell behind. Silent no-op if either
/// `from` or `to` is out of bounds, or if they're the same cell.
pub fn move_cell(&mut self, from: Point, to: Point) {
if from != to && self.in_bounds(from.into()) && self.in_bounds(to.into()) {
let thing = self.get_mut(from.x as usize, from.y as usize).take();
self.grid[to.x as usize + to.y as usize * self.width] = thing;
pub fn move_cell<P1: Into<Point>, P2: Into<Point>>(&mut self, from: P1, to: P2) {
let from = from.into();
let to = to.into();
if from != to && self.in_bounds(from) && self.in_bounds(to) {
let thing = self.get_mut(from).take();
*self.get_mut(to) = thing;
}
}
/// Return whether the given point is empty
pub fn is_empty(&self, p: Point) -> bool {
self.get(p.x as usize, p.y as usize).is_none()
pub fn is_empty<P: Into<Point>>(&self, p: P) -> bool {
self.get(p).is_none()
}
/// Return whether the given point contains the player
pub fn is_player(&self, p: Point) -> bool {
matches!(self.get(p.x as usize, p.y as usize), Some(Tile::Player))
pub fn is_player<P: Into<Point>>(&self, p: P) -> bool {
matches!(self.get(p), Some(Tile::Player))
}
/// Return whether the given point contains an object
pub fn is_object(&self, p: Point) -> bool {
matches!(self.get(p.x as usize, p.y as usize), Some(Tile::Object(_)))
pub fn is_object<P: Into<Point>>(&self, p: P) -> bool {
matches!(self.get(p), Some(Tile::Object(_)))
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
use crate::action::{apply_push, apply_shift, apply_teleport, Action, BoardAction, SendArg};
use crate::action::{apply_shift, apply_teleport, Action, BoardAction, SendArg};
use crate::board::Board;
use crate::log::LogLine;
use crate::script::ScriptHost;
@@ -540,7 +540,7 @@ fn step_object(board: &mut Board, id: ObjectId, dir: Direction) {
if solid {
// This is a real object on the board, try and push it
board.move_object(loc.0, loc.1, dir);
board.move_object((loc.0, loc.1), dir);
} else {
// This is a sensor, we can just teleport it
board.move_sensor(id, dir);
+5
View File
@@ -159,6 +159,11 @@ impl Display for Point {
}
}
impl Point {
pub fn ux(&self) -> usize { self.x as usize }
pub fn uy(&self) -> usize { self.y as usize }
}
impl Registerable for Point {
fn register(engine: &mut Engine, _log_sink: LogSink) {
engine.register_type_with_name::<Point>("Point")