Fix direction lock with the tick limit off; record the run on restart - #116
Conversation
Self-reviewBoth fixes are correct and each has a regression test confirmed to fail against (Posted as a single comment rather than anchored inline review comments — the review API was not reachable from this session. File and line references below are against the head commit.)
|
|
Follow-up for the This comment was drafted during a Gardener session (Stephenson-Software/gardener). |
Two run-lifecycle correctness bugs in the game loop.
Summary
#112 — turning off the tick-speed limit locked the snake's direction.
self.tick += 1andself.changedDirectionThisTick = Falselived inside theif self.config.limitTickSpeed:block next totime.sleep(), in bothrunTextUIandrunPygameUI.handleKeyDownEventlatcheschangedDirectionThisTicktoTrueon every accepted turn and nothing else ever clears it (initialize()doesn't either), so pressinglmeant the snake ignored every direction key after its first turn — for the rest of the process, across restarts. It also frozeself.tick, so runs recorded a staleticksSurvivedandtotalTicksSurvivedstopped accumulating (which gates thefrostcosmetic unlock at 300 ticks).Both loops now call a shared
endOfTick()that gates only the sleep onlimitTickSpeed. One loop iteration is exactly onemoveEntitycall either way, so the counter and the latch advance once per iteration. The latch itself is kept — it is what stops the player turning twice between movement steps and reversing into themselves behind the reversal guard's back.#113 — restarting with
rdiscarded the run. Therhandler went straight tocheckForLevelProgressAndReinitialize(), so the run's currency, obituary and lifetime stats were silently thrown away, unlike the collision and quit paths which both callrecordCurrentRun. A player who restarts rather than dies could never accumulate currency for the shop.Both UI branches now call a new
restartRun()that records the run as cause-of-death"restart"and then reinitializes. Recording lives inrestartRun()rather than insidecheckForLevelProgressAndReinitializebecause the collision path calls that method too and has already recorded by then."restart"is added toCAUSE_OF_DEATH_PHRASES("a deliberate restart") so the obituary line reads as narrative instead of falling through to the raw code.Gameplay logic stays out of both UI layers:
endOfTick()andrestartRun()live onOphidianand are called identically from each loop, and no renderer internals are touched.Test plan
python3 -m pytest— 150 passed (138 before, 12 new)python3 -m compileall srccleanmain'ssrc/(stashed the source change, re-ran: 11 failed)tests/test_ophidian_run_lifecycle.py:endOfTickskips the sleep but still advances tick/latch with the limit off; still sleepstickSpeedwith it on; a second direction change is accepted after a tick with the limit off;runTextUIreaches the bookkeeping with the limit off;rrecords the run, banks its currency, and records before the board resets (so length isn't logged as 1)tests/rendering/test_pygame_run_loop.py: same loop-level regression against the real headless pygame looptests/rendering/test_pygame_keydown_events.py:K_rrecords the runtests/progression/test_obituary.py:"restart"phrase and its narrative lineblack/autoflakeclean on the new and changed files (not run repo-wide:src/ophidian.pyis not black-formatted atmain, so./format.shwould bury this diff in unrelated reformatting)Notes
README.mdneeds no change —ris still documented as "restart" and still restarts; it just no longer loses the run's bookkeeping on the way.Closes #112
Closes #113
This PR description was drafted during a Gardener session (Stephenson-Software/gardener).