Repository navigation
Conversation
A replay whose action log outlives the player crashed the game: after
death the player entity is removed, and the next :autoexplore/:descend/
:ascend hit nil positions in bfs-path/stairs code ("cannot add nil and
let-go.lang.Int"). :wait/:rest didn't crash but kept advancing turns on
a dead world.
Live play never dispatches after death (the death screen owns input), so
this only bites replays: a ?replay= code that diverges on a newer build,
or a balance-harness log replayed on the wrong starting world. Reproduced
via XSOFY_REPLAY with a code that has actions past death: FATAL ERROR
before, death screen after.
dispatch now returns the world unchanged for replay actions when the
player is dead, using the same test update-world uses to raise the death
screen. No seed fold, no log entry, so existing codes stay byte-identical.
| (ignored-action? action) | ||
| world | ||
|
|
||
| (and (replay-action? action) (player-dead? world)) |
There was a problem hiding this comment.
Self-parented DAG node after death (xsofy.dag)
This branch returns the world unchanged, :seed included. dag/dispatch-gs still calls commit for every replay action (dag.lg ~L229-231). commit-id is just (:seed world), so the new node's id equals the current HEAD id. commit then runs (assoc-in gs [:nodes id] {:parent (:head gs) ...}), which overwrites HEAD's own node with {:parent id :action a}. The node is now its own parent, and its real parent link back to the origin is lost.
Scenario: (dag/replay-gs w h seed log) with a log that continues past death (the case this PR targets). Or any dispatch-gs of :wait on a dead world. After that:
actions-from-origin/materialize/branchon that id never terminate.curnever changes andaccgrows without bound.walk-parentsstops at the same id forever.- History before the death turn can't be reached from that node.
Before this PR the seed always advanced, so ids stayed distinct. Fix: in dispatch-gs, skip commit when (identical? w' (:world gs)) (or when the id doesn't change). Another option is to apply the same dead-player guard there, and in start-replay-action, which still folds and logs.
There was a problem hiding this comment.
I like the skipping on unchanged. Note that there should probably still be mutation to the turn counter.
I wonder if we need a proxy for the dead player - ideally the world aim should carry on without them
|
mparrett
left a comment
There was a problem hiding this comment.
Overall looks good. One small ask on the inline thread before merging.
|
@nnunley No rush, following up on this one in case it got buried. Small ask here when you can - #207 (comment) |
Problem
A replay whose action log continues past the player's death crashes the game:
When the player dies,
update-worldremoves the player entity. Any later:autoexplore,:descendor:ascendthen reads a nil position.:waitand:restdon't crash, but they keep advancing turns on a dead world.Live play never dispatches after death, because the death screen takes the input. Replays can:
?replay=/XSOFY_REPLAYcode that diverges on a newer build (a balance change makes the player die earlier than in the recorded run)Fix
dispatchreturns the world unchanged for replay actions once the player is dead. It uses the same testupdate-worlduses to raise the death screen. There is no seed fold and no log entry, so existing replay codes stay byte-identical: a valid code never contains actions after death.Verification
actions-after-death-are-noopstest covers both dead shapes (player entity removed, and player at 0 HP) across autoexplore, stairs, wait, rest, move, pickup, fire and item actions. It failed with the production crash before the fix and passes after.lg xsofy/test/run.lg→ 334 tests, 2930 assertions, 0 failures.XSOFY_REPLAY=… lg main.lg. Before the fix it hitFATAL ERRORwith the trace above; after, playback ends on the normal death screen.🤖 Generated with Claude Code