made grab simpler

This commit is contained in:
2026-06-21 19:15:43 -05:00
parent cbdaca4a90
commit 6c0d957ca2
4 changed files with 290 additions and 352 deletions
+22 -100
View File
@@ -2,7 +2,7 @@ use crate::action::Action;
use crate::board::Board;
use crate::log::LogLine;
use crate::script::ScriptHost;
use crate::utils::{Direction, ObjectId, Player, ScriptArg, Solid};
use crate::utils::{Direction, ObjectId, Player, ScriptArg};
use crate::world::World;
use std::cell::{Ref, RefMut};
use std::time::Duration;
@@ -210,9 +210,6 @@ impl GameState {
// below also borrows `self`.
let mut logs: Vec<LogLine> = Vec::new();
let mut bumps: Vec<(ObjectId, i64)> = Vec::new();
// Grab hooks to fire (player walked into / pushed a grab thing); fired after
// the board borrow drops, like bumps.
let mut grabs: Vec<ObjectId> = Vec::new();
// Net change to the player's gem count from AddGems actions; applied to
// `self` after the board borrow drops.
let mut gem_delta: i64 = 0;
@@ -225,13 +222,9 @@ impl GameState {
for ba in actions {
match ba.action {
Action::Move(dir) => {
let outcome = step_object(&mut board, ba.source, dir);
if let Some(bumped) = outcome.bumped {
if let Some(bumped) = step_object(&mut board, ba.source, dir) {
bumps.push((bumped, ba.source as i64));
}
if let Some(grabbed) = outcome.grabbed {
grabs.push(grabbed);
}
}
Action::SetTile(tile) => {
if let Some(obj) = board.objects.get_mut(&ba.source) {
@@ -311,23 +304,14 @@ impl GameState {
Action::Push { x, y, dir } => {
if !board.in_bounds((x, y)) {
logs.push(LogLine::error(format!("push({x},{y}): out of bounds")));
} else if let Some(grabbed) =
board.pushed_grab_into_player(x as usize, y as usize, dir)
{
// A grab thing shoved into the player is grabbed, not pushed.
grabs.push(grabbed);
} else {
board.push(x as usize, y as usize, dir);
}
}
// apply_swap reads all sources then writes all destinations, so a
// whole batch of moves resolves simultaneously (cycles included).
// A grab thing swapped onto the player is reported back for its
// grab() hook (fired in phase B with the other grabs).
Action::Swap(pairs) => {
let (errs, grabbed) = board.apply_swap(&pairs);
logs.extend(errs);
grabs.extend(grabbed);
logs.extend(board.apply_swap(&pairs));
}
// Accumulated and applied to `self.player_gems` after the borrow drops.
Action::AddGems(n) => gem_delta += n,
@@ -355,11 +339,6 @@ impl GameState {
for (bumped, bumper) in bumps {
self.scripts.run_bump(bumped, bumper);
}
// Fire grab() hooks (player walked into / pushed a grab thing). The hook
// typically adds a stat and calls die(); those actions resolve next pass.
for grabbed in grabs {
self.scripts.run_grab(grabbed);
}
for (target, fn_name, arg) in sends {
self.scripts.run_send(target, &fn_name, arg);
}
@@ -454,10 +433,10 @@ impl GameState {
grabbed = board.grab_object_at(nx, ny);
// A solid object in the way is bumped by the player (id -1) — but a
// grab thing fires grab() instead of bump(), so don't also bump it.
bumped = match board.solid_at(nx, ny) {
Some(Solid::Object(_)) if grabbed.is_none() => board.solid_object_id_at(nx, ny),
_ => None,
};
bumped = board
.solid_at(nx, ny)
.filter(|_| grabbed.is_none())
.and_then(|s| s.object_id());
if grabbed.is_some() || board.is_passable(nx, ny) || board.can_push(nx, ny, dir) {
// Don't push a grab thing aside — walk onto it. Otherwise shove any
// pushable chain out of the way (no-op when there's nothing to push).
@@ -494,61 +473,31 @@ impl GameState {
}
}
/// The side effects of a [`step_object`] call that the caller resolves after the
/// board borrow drops: a `bump`ed object and/or a `grab`bed object.
#[derive(Default)]
struct StepOutcome {
/// A solid object the mover collided with (target cell was occupied by it).
bumped: Option<ObjectId>,
/// A grab object the move shoved into the player (its `grab()` hook should fire).
grabbed: Option<ObjectId>,
}
/// Moves object `id` one cell in `dir` on `board`, returning the objects it bumped
/// and/or grabbed (see [`StepOutcome`]).
/// Moves object `id` one cell in `dir` on `board`, returning the object it bumped
/// (if any) for the caller to resolve after the board borrow drops.
///
/// The move itself proceeds only if the target is in bounds and either passable or a
/// pushable solid the object can shove out of the way (see [`Board::can_push`]). The
/// bump is recorded whenever a solid object occupies the target — whether it gets
/// pushed aside or blocks the move — since something tried to move into it. Walls and
/// crates carry no script, so only solid objects yield a bump.
///
/// If the push would shove a **grab** thing into the player (see
/// [`Board::pushed_grab_into_player`]), the push is skipped and the grab thing is
/// reported instead of sliding the player along.
fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> StepOutcome {
fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<ObjectId> {
let (dx, dy): (i32, i32) = dir.into();
let Some((ox, oy)) = board.objects.get(&id).map(|o| (o.x, o.y)) else {
return StepOutcome::default();
};
let (ox, oy) = board.objects.get(&id).map(|o| (o.x, o.y))?;
let target = (ox as i32 + dx, oy as i32 + dy);
if !board.in_bounds(target) {
return StepOutcome::default();
return None;
}
let (nx, ny) = (target.0 as usize, target.1 as usize);
// A push that would drive a grab thing into the player grabs it instead of
// sliding the player; skip the move entirely (the grab thing despawns itself).
if let Some(grabbed) = board.pushed_grab_into_player(nx, ny, dir) {
return StepOutcome {
bumped: None,
grabbed: Some(grabbed),
};
}
// Capture the bumped object before any push relocates it (its id is stable).
let bumped = match board.solid_at(nx, ny) {
Some(Solid::Object(_)) => board.solid_object_id_at(nx, ny),
_ => None,
};
let bumped = board.solid_at(nx, ny).and_then(|s| s.object_id());
if board.is_passable(nx, ny) || board.can_push(nx, ny, dir) {
board.push(nx, ny, dir); // shoves a crate/object out of the way; no-op otherwise
let obj = board.objects.get_mut(&id).expect("id checked above");
obj.x = nx;
obj.y = ny;
}
StepOutcome {
bumped,
grabbed: None,
}
bumped
}
#[cfg(test)]
@@ -580,9 +529,11 @@ mod tests {
}
#[test]
fn pushing_a_gem_into_the_player_grabs_it() {
// A non-solid script object at (0,0) pushes the gem at (1,0) east into the
// player at (2,0); the gem is grabbed rather than sliding the player.
fn pushing_a_gem_into_the_player_does_not_grab() {
// A non-solid script object at (0,0) pushes the gem at (1,0) east toward the
// player at (2,0). Grab now fires only on player movement, so the gem is an
// ordinary solid here: the chain can't move (the player is backed against
// the board edge), so nothing happens and the gem is not collected.
let mut sobj = ObjectDef::new(0, 0);
sobj.solid = false;
sobj.script_name = Some("s".to_string());
@@ -596,41 +547,12 @@ mod tests {
let mut game = GameState::with_scripts(board, scripts);
game.run_init();
// First tick: the push resolves into a grab (gem's grab() is queued).
// Second tick: the gem's add_gems()/die() resolve.
game.tick(Duration::from_millis(16));
game.tick(Duration::from_millis(16));
assert_eq!(game.player_gems, 1);
// The gem (the only non-script object) is gone; the player never moved.
assert!(!game.board().objects.values().any(|o| o.grab));
assert_eq!((game.board().player.x, game.board().player.y), (2, 0));
}
#[test]
fn swapping_a_gem_into_the_player_grabs_it() {
// A non-solid script object swaps the gem at (1,0) onto the player at (2,0).
// apply_swap reports the grab; the gem's grab() collects it and die()s.
let mut sobj = ObjectDef::new(0, 0);
sobj.solid = false;
sobj.script_name = Some("s".to_string());
let mut board = open_board(3, 1, (2, 0), vec![sobj]);
stamp(&mut board, 1, 0, Archetype::Gem);
board.expand_builtin_archetypes();
let scripts = HashMap::from([(
"s".to_string(),
"fn tick(dt) { if Queue.length() == 0 { swap([[1, 0, 2, 0]]); } }".to_string(),
)]);
let mut game = GameState::with_scripts(board, scripts);
game.run_init();
// First tick: the swap reports the grab (gem's grab() is queued).
// Second tick: the gem's add_gems()/die() resolve.
game.tick(Duration::from_millis(16));
game.tick(Duration::from_millis(16));
assert_eq!(game.player_gems, 1);
assert!(!game.board().objects.values().any(|o| o.grab));
// No grab: the gem is untouched and the player never moved.
assert_eq!(game.player_gems, 0);
assert!(game.board().objects.values().any(|o| o.grab));
assert_eq!((game.board().player.x, game.board().player.y), (2, 0));
}