fix(grid): correct Dijkstra path order and layer-setter cache invalidation

Two real engine bugs, both surfaced by unrotting tests that had been silently
scored as passing for months, and both independently reproduced before fixing.

#375 -- DijkstraMap.path_from() returned the path REVERSED, and step_from()
inherited the error, returning a cell that is not adjacent to the query
position. libtcod builds its list as [pos, ..., root_adjacent] and
TCOD_dijkstra_path_walk() pops from the BACK, so the collecting loop produced
[root_adjacent, ..., pos]. On an open grid with root (5,5):

    path_from((9,5)) -> [(6,5),(7,5),(8,5),(9,5)]   # marched AWAY from the root
    step_from((9,5)) -> (6,5)                       # a three-cell teleport

That contradicted path_from's own docstring, AStarPath.origin/.destination,
and find_path()'s convention. step_from was the dangerous one: an entity
chasing a Dijkstra root would jump next to the root on its first step. The
path is now emitted origin->destination, excluding the origin and including
the root (found via descentStep, so multi-root lands on the right root), and
step_from returns its first element -- provably adjacent.

The behavior system was never affected: PathProvider (SEEK/FLEE) uses
descentStep, which was already correct. getPathFrom/stepFrom had no internal
C++ callers.

#376 -- ColorLayer/TileLayer .visible and .z_index setters never called
markDirty(), so they did not bump content_generation, which is the key the
#351 render early-out uses. Hiding a layer left the rendered image
byte-identical (sha256 unchanged) until some unrelated mutation happened to
dirty it; re-showing it did not restore it. z_index carried an explicit
"TODO: Trigger re-sort in parent grid" and had the same defect. Both setters
now invalidate on change. (Only observable after #351 stopped redrawing
unconditionally -- the same masking story as the entity set_position bug.)

Neither was caught because no test asserted path ORDER (test_astar checks only
length and wall-avoidance, both invariant under reversal), and because
issue_148_layer_dirty_flags.py -- the test literally named for this defect
class -- had rotted and was exiting 0 without running.

closes #375
closes #376
This commit is contained in:
John McCardle 2026-07-14 07:29:03 -04:00
commit 48eef0b095
3 changed files with 47 additions and 15 deletions

File diff suppressed because one or more lines are too long