wip
This commit is contained in:
+64
-146
@@ -96,6 +96,15 @@ impl Board {
|
||||
&mut self.layers[z].cells[y * w + x]
|
||||
}
|
||||
|
||||
/// Replace the solid (if any) at `(x, y)` with `Empty`
|
||||
pub fn clear_solid(&mut self, x: usize, y: usize) {
|
||||
if self.in_bounds((x as i32, y as i32)) {
|
||||
if let Some(z) = self.solid_cell_layer(x, y) {
|
||||
*self.get_mut(z, x, y) = (Archetype::Empty.default_glyph(), Archetype::Empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the glyph to display at `(x, y)`, honoring layer draw order.
|
||||
///
|
||||
/// The player is always drawn on top (it is not part of the layer stack yet).
|
||||
@@ -524,165 +533,74 @@ impl Board {
|
||||
self.add_object(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a batch of one-way solid moves **simultaneously**, returning any
|
||||
/// nonfatal error lines (out-of-bounds entries / a write blocked by the player).
|
||||
///
|
||||
/// Each tuple is `(src_x, src_y, dst_x, dst_y)`: the solid occupant of `(src_x,
|
||||
/// src_y)` — the player, a solid object, or a terrain crate/wall — moves to
|
||||
/// `(dst_x, dst_y)`. A source with no solid moves an "empty", which **removes**
|
||||
/// whatever solid was at the destination. Every source is read before any
|
||||
/// destination is written, so cyclic permutations and two-cell swaps resolve
|
||||
/// correctly (e.g. `[a→b],[b→a]` swaps `a` and `b`).
|
||||
///
|
||||
/// Displacement rules: a destination's prior solid that isn't itself being moved
|
||||
/// is removed (terrain cleared; a scripted object despawned via
|
||||
/// [`remove_object`](Board::remove_object)). The **player is never destroyed** —
|
||||
/// a write that would overwrite the player without relocating it is skipped and
|
||||
/// logged (the player wins its cell, per the one-solid-per-cell invariant).
|
||||
///
|
||||
/// A solid refused onto the player is left at its source cell, which another
|
||||
/// entry may also target; a final sweep over the swapped cells resolves any
|
||||
/// such **overlap** — it keeps one solid, deletes the rest (never the player),
|
||||
/// and logs an error per deletion.
|
||||
pub fn apply_swap(&mut self, pairs: &[(i32, i32, i32, i32)]) -> Vec<LogLine> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
// 1. Validate: keep only entries whose source and destination are in bounds.
|
||||
let mut valid: Vec<((i32, i32), (i32, i32))> = Vec::new();
|
||||
for &(sx, sy, dx, dy) in pairs {
|
||||
if !self.in_bounds((sx, sy)) || !self.in_bounds((dx, dy)) {
|
||||
errors.push(LogLine::error(format!(
|
||||
"swap: out-of-bounds entry ({sx},{sy})->({dx},{dy})"
|
||||
)));
|
||||
continue;
|
||||
}
|
||||
valid.push(((sx, sy), (dx, dy)));
|
||||
|
||||
/// Shifts a set of cells, given as `(x, y)` coordinates. Backs the script
|
||||
/// `shift()` fn. Returns any errors as [`LogLine`]s for the caller to log.
|
||||
pub fn apply_shift(&mut self, cells: &[(i32, i32)]) -> Vec<LogLine> {
|
||||
// Validate all the cells are in bounds, error if not:
|
||||
if cells.iter().any(|&c| !self.in_bounds(c)) {
|
||||
return vec![LogLine::error("Called shift() with a cell out of bounds")]
|
||||
}
|
||||
|
||||
// 2. Snapshot the solid at each unique source (read phase). Reading every
|
||||
// source before any write is what lets cycles/swaps resolve. `None` is an
|
||||
// empty source (the old `SolidSnapshot::Empty`).
|
||||
let mut snapshots: HashMap<(i32, i32), Option<Solid>> = HashMap::new();
|
||||
for &(src, _) in &valid {
|
||||
snapshots.entry(src).or_insert_with(|| {
|
||||
self.solid_at(src.0 as usize, src.1 as usize)
|
||||
});
|
||||
}
|
||||
// Get all the Solids at these cells:
|
||||
let solids: Vec<_> = cells.iter().map(|&c| self.solid_at(c.0 as usize, c.1 as usize)).collect();
|
||||
|
||||
// 3. Compute the final occupant of every affected cell. A source that is not
|
||||
// anyone's destination is vacated (None); each entry writes its source's
|
||||
// snapshot into its destination (a later entry wins a repeated destination).
|
||||
let dsts: HashSet<(i32, i32)> = valid.iter().map(|&(_, d)| d).collect();
|
||||
let mut final_state: HashMap<(i32, i32), Option<Solid>> = HashMap::new();
|
||||
for &(src, _) in &valid {
|
||||
if !dsts.contains(&src) {
|
||||
final_state.insert(src, None);
|
||||
}
|
||||
}
|
||||
for &(src, dst) in &valid {
|
||||
final_state.insert(dst, snapshots[&src]);
|
||||
}
|
||||
// Tell whether these are normally pushable in this direction. This doesn't count whether
|
||||
// their target zone will be empty, just whether they would be willing to move that way at
|
||||
// all. The direction comes in because of hcrates / vcrates: if their target cell is adjacent,
|
||||
// then we'll check directions
|
||||
//let mut pushable: Vec<bool> = Vec::with_capacity(solids.len());
|
||||
|
||||
// The player's final cell: where a Player snapshot is installed, else its
|
||||
// current cell (it stays put). Used to protect the player from being
|
||||
// overwritten — computed up front so it's stable across the write loop.
|
||||
let player_final = final_state
|
||||
.iter()
|
||||
.find(|(_, s)| s.is_some_and(|s| s.player()))
|
||||
.map(|(&c, _)| c)
|
||||
.unwrap_or((self.player.x, self.player.y));
|
||||
let mut immobile = HashSet::new();
|
||||
for (curr_idx, curr) in solids.iter().enumerate() {
|
||||
let origin = cells[curr_idx];
|
||||
let target = cells[(curr_idx + 1) % cells.len()];
|
||||
|
||||
// 4a. Clear each affected cell's current occupant, despawning any object that
|
||||
// doesn't survive (isn't reused in final_state). The player and surviving
|
||||
// objects keep their entity and are repositioned by the install step.
|
||||
let survivors: HashSet<ObjectId> = final_state
|
||||
.values()
|
||||
.filter_map(|s| s.and_then(|s| s.object_id()))
|
||||
.collect();
|
||||
let affected: HashSet<(i32, i32)> = snapshots
|
||||
.keys()
|
||||
.copied()
|
||||
.chain(final_state.keys().copied())
|
||||
.collect();
|
||||
for &(cx, cy) in &affected {
|
||||
let (ux, uy) = (cx as usize, cy as usize);
|
||||
match self.solid_at(ux, uy) {
|
||||
Some(s) if s.object_id().is_some_and(|id| !survivors.contains(&id)) => {
|
||||
self.remove_object(s.object_id().unwrap());
|
||||
}
|
||||
Some(s) if s.archetype().is_some() => {
|
||||
if let Some(z) = self.solid_cell_layer(ux, uy) {
|
||||
*self.get_mut(z, ux, uy) = (Glyph::transparent(), Archetype::Empty);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
// Whether these represent a single-cell h or v move.
|
||||
let hmove = target.1 == origin.1 && (target.0 - origin.0).abs() == 1;
|
||||
let vmove = target.0 == origin.0 && (target.1 - origin.1).abs() == 1;
|
||||
|
||||
// We are never allowed to push a thing that won't push
|
||||
// We won't push a horizontal-only thing vertically
|
||||
// We won't push a vertical-only thing horizontally
|
||||
let pushable = curr.as_ref().map_or(Pushable::Any, |c| c.pushable());
|
||||
if pushable == Pushable::No ||
|
||||
pushable == Pushable::Vertical && hmove ||
|
||||
pushable == Pushable::Horizontal && vmove {
|
||||
immobile.insert(curr_idx);
|
||||
}
|
||||
}
|
||||
|
||||
// 4b. Install each cell's computed occupant.
|
||||
for (&(cx, cy), snap) in &final_state {
|
||||
let (ux, uy) = (cx as usize, cy as usize);
|
||||
// Empty cells were already cleared in phase 4a.
|
||||
let Some(solid) = snap else { continue };
|
||||
// The player wins its cell: never overwrite player_final with anything
|
||||
// other than the player itself. A refused solid stays at its source
|
||||
// cell; the overlap sweep below cleans up if that cell is now also
|
||||
// someone else's destination.
|
||||
if (cx, cy) == player_final && !solid.player() {
|
||||
errors.push(LogLine::error(format!(
|
||||
"swap: cannot overwrite the player at ({cx},{cy})"
|
||||
)));
|
||||
continue;
|
||||
}
|
||||
solid.place(self, ux, uy);
|
||||
}
|
||||
|
||||
// 5. Overlap sweep: a solid refused onto the player (above) stays at its
|
||||
// source cell, which may now also be a destination another entry wrote
|
||||
// into. For any swapped cell holding more than one solid, keep a single
|
||||
// occupant and delete the rest (never the player). Logs an error per
|
||||
// deletion.
|
||||
for &(cx, cy) in &affected {
|
||||
let (ux, uy) = (cx as usize, cy as usize);
|
||||
let player_here = self.player.x == cx && self.player.y == cy;
|
||||
// Solid objects on this cell; keep the first, delete the rest.
|
||||
let objs: Vec<ObjectId> = self
|
||||
.objects
|
||||
.iter()
|
||||
.filter(|(_, o)| o.x == ux && o.y == uy && o.solid)
|
||||
.map(|(&id, _)| id)
|
||||
.collect();
|
||||
let has_terrain = self.solid_cell_layer(ux, uy).is_some();
|
||||
|
||||
// How many solids share the cell (player + solid objects + solid terrain).
|
||||
let total = player_here as usize + objs.len() + has_terrain as usize;
|
||||
if total <= 1 {
|
||||
continue;
|
||||
}
|
||||
errors.push(LogLine::error(format!(
|
||||
"swap: {total} solids overlap at ({cx},{cy}); deleting extras"
|
||||
)));
|
||||
|
||||
// Pick the survivor (priority: player > first object > terrain) and
|
||||
// delete every other solid. The player is only ever a survivor, so it
|
||||
// is never deleted. `keep_obj` is the object we keep, if any.
|
||||
let keep_obj = (!player_here).then(|| objs.first().copied()).flatten();
|
||||
for id in &objs {
|
||||
if Some(*id) != keep_obj {
|
||||
self.remove_object(*id);
|
||||
// Trace back from each immobile until we find an empty:
|
||||
let mut blocked = HashSet::new();
|
||||
for curr_idx in immobile {
|
||||
let mut prev_idx = curr_idx;
|
||||
loop {
|
||||
if solids[prev_idx].is_some() && !blocked.contains(&prev_idx) {
|
||||
blocked.insert(prev_idx);
|
||||
prev_idx = (prev_idx + cells.len() - 1) % cells.len();
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Clear terrain unless it's the sole survivor (no player, no kept object).
|
||||
if has_terrain
|
||||
&& (player_here || keep_obj.is_some())
|
||||
&& let Some(z) = self.solid_cell_layer(ux, uy)
|
||||
{
|
||||
*self.get_mut(z, ux, uy) = (Glyph::transparent(), Archetype::Empty);
|
||||
}
|
||||
|
||||
// Clear all the cells so everything can be placed:
|
||||
for (curr_idx, curr) in cells.iter().enumerate() {
|
||||
if !blocked.contains(&curr_idx) {
|
||||
self.clear_solid(curr.0 as usize, curr.1 as usize);
|
||||
}
|
||||
}
|
||||
|
||||
errors
|
||||
// Now, move anything that we've decided is not blocked:
|
||||
for (curr_idx, curr) in solids.iter().enumerate() {
|
||||
if let Some(solid) = curr && !blocked.contains(&curr_idx) {
|
||||
let target = cells[(curr_idx + 1) % cells.len()];
|
||||
solid.place(self, target.0 as usize, target.1 as usize);
|
||||
}
|
||||
}
|
||||
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Returns the index of the layer whose terrain cell at `(x, y)` is non-`Empty`
|
||||
|
||||
Reference in New Issue
Block a user