grid.step() turn manager does not check entity occupancy for movement collision #415

Open
opened 2026-07-28 00:18:27 +00:00 by john · 0 comments
Owner

Found while building the mcrogueface-007 demo body node (robot_sim_stack, SIM_SPEC_v2.md §13.6), which wanted Trigger.BLOCKED to be reachable as "a genuine actuation failure... a collision with another moving entity."

Traced the turn manager (PyGridData::py_step, src/PyGridDataMethods.cpp:1000-1040) and the per-behavior executors (src/EntityBehavior.cpp): for WAYPOINT/PATH/PATROL/LOOP, BehaviorResult::BLOCKED is produced only when isCellWalkable() fails (static grid.walkable_plane, EntityBehavior.cpp:44) or TCODPath::compute() finds no route. BehaviorResult::MOVED never checks whether the target cell is currently occupied by another entity — grid->spatial_hash.queryCell(...) is consulted only after a BLOCKED result already happened for a terrain reason, purely to name a blocker for the callback payload, not to decide BLOCKED in the first place.

Net effect: two entities can walk through/onto one another with no collision, no BLOCKED trigger, and no signal at all. This is a plausible gap for anyone building a turn-based game on grid.step() and expecting entities to be mutually solid.

Not a blocker for mcrogueface-007 — that demo produces a legible BLOCKED via an unreachable target (walled-off pocket) instead, which satisfies the same "deterministic, fully explained, reconstructible" requirement without needing this. Filing so the gap is tracked rather than silently rediscovered later.

Possible fix shape (not designed in detail here): in the BehaviorResult::MOVED case in PyGridData::py_step, check grid->spatial_hash.queryCell(target_cell) for a live entity before committing the move, and re-route that case through the existing BLOCKED path (including the blocker lookup that already exists) instead of unconditionally moving.

Found while building the mcrogueface-007 demo body node (robot_sim_stack, SIM_SPEC_v2.md §13.6), which wanted `Trigger.BLOCKED` to be reachable as "a genuine actuation failure... a collision with another moving entity." Traced the turn manager (`PyGridData::py_step`, `src/PyGridDataMethods.cpp:1000-1040`) and the per-behavior executors (`src/EntityBehavior.cpp`): for WAYPOINT/PATH/PATROL/LOOP, `BehaviorResult::BLOCKED` is produced only when `isCellWalkable()` fails (static `grid.walkable_plane`, `EntityBehavior.cpp:44`) or `TCODPath::compute()` finds no route. `BehaviorResult::MOVED` never checks whether the target cell is currently occupied by another entity — `grid->spatial_hash.queryCell(...)` is consulted only *after* a BLOCKED result already happened for a terrain reason, purely to name a blocker for the callback payload, not to decide BLOCKED in the first place. Net effect: two entities can walk through/onto one another with no collision, no BLOCKED trigger, and no signal at all. This is a plausible gap for anyone building a turn-based game on `grid.step()` and expecting entities to be mutually solid. Not a blocker for mcrogueface-007 — that demo produces a legible BLOCKED via an unreachable target (walled-off pocket) instead, which satisfies the same "deterministic, fully explained, reconstructible" requirement without needing this. Filing so the gap is tracked rather than silently rediscovered later. Possible fix shape (not designed in detail here): in the `BehaviorResult::MOVED` case in `PyGridData::py_step`, check `grid->spatial_hash.queryCell(target_cell)` for a live entity before committing the move, and re-route that case through the existing BLOCKED path (including the blocker lookup that already exists) instead of unconditionally moving.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
john/McRogueFace#415
No description provided.