diff --git a/kiln-core/src/action.rs b/kiln-core/src/action.rs index 8042d0e..558b1e0 100644 --- a/kiln-core/src/action.rs +++ b/kiln-core/src/action.rs @@ -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(()) diff --git a/kiln-core/src/board.rs b/kiln-core/src/board.rs index 6f65c29..e3646d2 100644 --- a/kiln-core/src/board.rs +++ b/kiln-core/src/board.rs @@ -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 { - &self.grid[y * self.width + x] + pub fn get>(&self, p: P) -> &Option { + 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 { + pub fn get_mut>(&mut self, p: P) -> &mut Option { + 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>(&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>(&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>(&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>(&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>(&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 { + pub fn sensor_ids_at>(&self, p: P) -> Vec { + 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>(&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) -> Result<(), String> { + pub fn place>(&mut self, p: P, spec: Option) -> 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, P2: Into>(&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>(&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>(&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>(&self, p: P) -> bool { + matches!(self.get(p), Some(Tile::Object(_))) } } diff --git a/kiln-core/src/game.rs b/kiln-core/src/game.rs index 6c0395f..c0dfdb3 100644 --- a/kiln-core/src/game.rs +++ b/kiln-core/src/game.rs @@ -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); diff --git a/kiln-core/src/utils.rs b/kiln-core/src/utils.rs index a9fff18..af65552 100644 --- a/kiln-core/src/utils.rs +++ b/kiln-core/src/utils.rs @@ -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")