Skip to content

Release the front-end wherever the game ends, and let the browser see the ended screen - #175

Merged
dmccoystephenson merged 2 commits into
mainfrom
fix/central-cleanup-on-game-end
Aug 9, 2026
Merged

Release the front-end wherever the game ends, and let the browser see the ended screen#175
dmccoystephenson merged 2 commits into
mainfrom
fix/central-cleanup-on-game-end

Conversation

@dmccoystephenson

@dmccoystephenson dmccoystephenson commented Aug 9, 2026

Copy link
Copy Markdown
Member

Summary

  • cleanup() is now called once, from a finally in FishE.play(), rather than from the Pyodide entry point alone. Every way a run can finish — retiring, quitting, an unhandled error — is covered, for all four front-ends. It stays a no-op for the console.
  • exit(0) in the save-file menu's "Quit" branch has been replaced by clearing running. Ending the interpreter from inside __init__ meant no front-end was ever released; __init__ now returns early and play() does nothing but clean up.
  • The server-backed web front-end held the browser's ended screen for as long as it took to publish it and no longer: the socket was closed before the next 300 ms poll could fetch it, so a player who had just retired was shown "Lost connection to the game". The server is now kept open until that screen has actually been sent (typically one poll interval), with a two-second timeout for when nothing is listening at all.
  • The page stops polling once the ended screen is rendered, instead of failing against a finished process for the rest of the tab's life.
  • web/pyodide_main.py's SystemExit handling and its own cleanup() call have been dropped as redundant.

Test plan

  • python3 -m compileall -q src tests web examples
  • python3 -m pytest --cov=src --cov-report=term-missing --cov-report=xml:cov.xml — 815 passed, src/ui/webUserInterface.py at 97%
  • Front-ends checked: the change is in shared code (FishE.play), so console, pygame, UIType.WEB and UIType.PYODIDE all take the same path. Console and pygame cleanup() were read and are unaffected beyond now being called (pygame's pygame.quit() is idempotent, and its own QUIT handlers still call it). The delivery wait is skipped when no server is running, which is how the Pyodide subclass inherits it.
  • Nine tests added: play() cleans up on a normal end and on an exception; "Quit" from the save-file menu ends the run without SystemExit and claims no slot; the ended screen is published with or without a server; the server is held open until that screen is fetched and gives up when nothing fetches it; delivery bookkeeping ignores a late overlapping poll; the client's poll loop stops on ended.
  • Documentation checked against the implementation: README.md, PLANNING.md and the three schemas/*.json files make no claim touched by this change (no Player/Stats/TimeService field was added, renamed or retyped).

Closes #153

This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).

dmccoystephenson and others added 2 commits August 9, 2026 20:13
cleanup() had exactly one production caller, on the Pyodide path, so a run
that finished under any other front-end released nothing: the pygame window
closed without pygame.quit(), and the server-backed web front-end exited
without publishing its ended screen - the tab kept polling a dead process and
told a player who had just retired that the connection was lost.

play() now calls it once, in a finally, so every ending (retiring, quitting,
an unhandled error) goes through the same path. "Quit" in the save-file menu
clears running instead of calling exit(0), which used to kill the interpreter
from inside __init__ before any front-end could be released; the Pyodide entry
point's SystemExit handling and its own cleanup() call go with it.

The ended screen also has to survive being published: the browser only
discovers it on its next poll, and cleanup() closed the socket immediately
after. The server is now held open until the page has actually collected that
screen, or a short timeout expires when nothing is listening, and the client
stops polling once it renders it.

Closes #153

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every other internal helper on WebUserInterface (_present, _awaitInput,
_header) carries the underscore; only get_state/submit_input/
record_state_delivered are public, because the request handler calls them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dmccoystephenson

Copy link
Copy Markdown
Member Author

Self-review

The full diff was read against this repo's conventions (front-end parity, the schema/reader-writer contract, test coverage of new public methods, doc drift). One finding was acted on before this comment was posted; the rest are notes on deliberate choices, and nothing is left outstanding.

Acted on

  • src/ui/webUserInterface.py:250 — the ended-screen wait was originally added as awaitScreenDelivery, which reads as part of this class's public surface. Every other internal helper here carries the underscore (_present, _awaitInput, _header); only get_state, submit_input and the new record_state_delivered are public, because the request handler calls them. It has been renamed to _awaitScreenDelivery (commit 27829c9).

Reviewed and judged correct

  • src/fishE.py:296 — the loop body was extracted to _runGameLoop() rather than re-indented under a try:, so the diff shows what changed instead of fifty moved lines. The behaviour is identical, and play() remains the only entry point anything calls.
  • src/fishE.py:67 — the early return leaves self.locations and self.currentLocation unset when "Quit" is chosen before a slot is. That is reachable only through _runGameLoop(), which never runs with running cleared, and building five locations for a game nobody will play would be the stranger choice. get_save_path() raising "No save slot selected" is what makes stopping here mandatory rather than optional.
  • src/ui/pygameUserInterface.py:196 and its four siblings — a window-close during the game now reaches cleanup() twice: once from the QUIT handler, then again through play()'s finally as SystemExit unwinds. pygame.quit() is documented as safe to call on an uninitialised pygame, so this is a no-op rather than a fault, and the handlers have to stay because a QUIT during __init__'s save-file menu never reaches play() at all.
  • src/ui/webUserInterface.py:160 — delivery is recorded after _send() returns rather than inside get_state(). Recording it at snapshot time would let cleanup() close the server while the response for that very snapshot was still being written. A client that disconnects mid-write leaves the version unrecorded, which correctly reads as "not delivered".
  • src/ui/webUserInterface.py:246max() rather than assignment, because two polls can overlap and the older one can finish last; a stale write would otherwise un-deliver the ended screen and cost the full timeout.
  • src/ui/webUserInterface.py:359 — the two-second timeout is only ever paid when nothing is polling (a closed tab, or the game driven by something other than a browser). With a page open the wait ends within one 300 ms poll. makeWebUI in the tests passes a short timeout for the same reason, so no test sits on the production default.
  • web/pyodide_main.py — the SystemExit branch was removed rather than kept as a safety net, because the only thing that raised it was the exit(0) this PR deletes. The end-to-end test that covers quitting from a Worker (tests/ui/test_pyodideUserInterface.py:369) still passes, now getting its ended screen from play() instead.

Parity, schemas and docs

  • No front-end gap: the fix lives in FishE.play(), which is shared, and the one front-end-specific part (holding the socket open) sits in WebUserInterface where PyodideUserInterface inherits it as a no-op — it has no server, so cleanup() returns straight after publishing the screen.
  • No Player, Stats or TimeService field was touched, so schemas/player.json, schemas/stats.json and schemas/timeService.json need no change.
  • README.md and PLANNING.md were re-read against the implementation; neither describes what happens when a run ends, so nothing there became inaccurate.

This review was performed during a Gardener session (https://github.com/Stephenson-Software/gardener).

@dmccoystephenson
dmccoystephenson merged commit 8fa43ea into main Aug 9, 2026
1 check passed
@dmccoystephenson
dmccoystephenson deleted the fix/central-cleanup-on-game-end branch August 9, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The server-backed web front-end shows "lost connection" instead of the ended screen when the game finishes

1 participant