Skip to content

Wrap and scroll text in the pygame front-end, and show location/goal in its status block - #121

Merged
dmccoystephenson merged 1 commit into
mainfrom
fix/pygame-text-wrapping-and-status-parity
Jul 25, 2026
Merged

Wrap and scroll text in the pygame front-end, and show location/goal in its status block#121
dmccoystephenson merged 1 commit into
mainfrom
fix/pygame-text-wrapping-and-status-parity

Conversation

@dmccoystephenson

Copy link
Copy Markdown
Member

Summary

Two pygame front-end defects, both about text the other two front-ends render correctly for free.

  • Wrap and scroll text (Pygame front-end renders multi-line and long text as a single unwrapped line #118). pygame.font.Font.render() draws a single row and renders "\n" as a glyph, so the pygame front-end was clipping or mangling anything that wasn't short and single-line. Measured on pygame 2.6.1 / SDL 2.28.4 at the default 800x600 window: a 25-line stats block rendered as one (4288, 17) surface, and the prompt with the goal announcement appended came to 983px wide. _wrapText now splits on newlines and word-wraps to the window (breaking a single over-wide word by character rather than clipping it), and it is applied to showDialogue, the prompt row of _draw_game_screen, promptForText (prompt and typed answer) and timedKeyPress. showDialogue scrolls with UP/DOWN when the block is taller than the window — the real stats screen wraps to ~30 lines against 21 visible — and any other key still continues, so nothing else changes.
  • Status block parity (Pygame status panel omits the location and goal rows the console and web front-ends show #119). FishE.play sets currentLocationName and goalProgress before every render and the console and web front-ends both show them; pygame showed neither. Both now appear (empty still hides the entry, same as the other two), and money is formatted $%.2f instead of printing a raw float such as $20.010000000000002.

Why the status block is now grouped rows

Issue #119 proposed one row per stat, in the console's order. That doesn't fit: adding two rows to a one-per-row column, plus a prompt that can wrap, pushes the option list off the bottom edge. The docks menu (7 options) already overflowed the default window by 54px before this change:

options start: 402.0  end: 654.0  window height: 600

So the header instead groups stats a few per row — the same compact layout the web front-end's flex header already uses — and the vertical layout moved into _gameScreenLayout, which sizes the option rows against the space actually left over (never below the font's own line height). The busiest screen the game builds (3 header rows, a 2-line prompt, 7 options) now ends at 510px with both instruction lines on screen, and short menus keep their original 36px rows.

Test plan

  • python3 -m compileall -q src tests
  • python3 -m pytest -q --cov=src --cov-report=term-missing --cov-report=xml:cov.xml — 368 passed; src/ui/pygameUserInterface.py coverage 67% → 92%
  • black + autoflake per format.sh (limited to the two files this PR touches — the formatter also rewrites several unrelated files on this repo, left alone here)
  • Front-ends touched: pygame only. BaseUserInterface and its abstract primitives are unchanged, so the console and web front-ends are untouched; this PR brings pygame up to what those two already did.
  • Rendered the game screen, the stats dialogue and a scrolled dialogue headless (SDL_VIDEODRIVER=dummy) and checked the images: all 7 docks options visible, prompt wrapped to 2 lines, stats block readable, scroll hint reading UP/DOWN to scroll (8-28 of 40) - any other key to continue.

Closes #118
Closes #119

pygame's Font.render draws a single row and renders "\n" as a glyph, so
multi-line and window-width text was unreadable or clipped in the pygame
front-end. Wrap every text block before drawing it, scroll a dialogue that
is taller than the window, and add the location/goal rows the console and web
front-ends already show.

The status block groups stats a few per row (like the web header) and the
option rows are sized against the space left over, because a one-per-row
column plus a wrapped prompt pushed the option list off the bottom of the
default window - the docks menu already overflowed it by 54px before this
change.

Closes #118
Closes #119

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

Copy link
Copy Markdown
Member Author

Self-review of the pygame wrap/scroll and status-parity change. No blocking findings; three things I checked deliberately and left as they are, with file/line references below (posted as one review body — the inline-anchoring API call isn't available to this session).

Verified beyond the test suite

  • Rendered the game screen, the stats dialogue and a scrolled dialogue headless and looked at the images: all 7 docks options on screen, prompt wrapped to 2 lines, the stats block readable as lines (it was one 4288px-wide row before), scroll hint correct.
  • Option rows are deliberately not wrapped. Measured every option string the menus can generate (housing tiers, investment property types, boat tiers): the widest is [1] Buy a Fisherman's Rowhouse ($4000) at 320px against 656px of available width in the default window, so wrapping them would be dead code.
  • No save-file surface is involved: Player/Stats/TimeService and the *JsonReaderWriter/schemas/*.json pair are untouched, so there is no schema drift to fix here.
  • Front-end parity: BaseUserInterface and its abstract primitives are unchanged, and nothing was added that the console and web front-ends don't already do — this PR is pygame catching up to them, so there is no new gap in the other two.

src/ui/pygameUserInterface.py:499 (the timedKeyPress wrap)

This re-wraps the message on every frame of the loop (same in promptForText, and in _draw_game_screen which showOptions calls per frame). Left uncached on purpose: the work is a handful of font.size() calls per word, the loop is already capped at 60 FPS by Clock().tick(60), and caching would need invalidating on both the text and a window resize. Worth revisiting only if a profile says otherwise. Worth noting for timedKeyPress specifically, since it backs the fishing minigame: the reaction time is measured as wall-clock between startTime and the keypress, so per-frame layout work doesn't bias the reading.

src/ui/pygameUserInterface.py:243 (the instructionsY clamp)

Deliberate tradeoff in an extreme case: if a menu ever offers so many options that they can't fit even at the font's own line height, the option rows win and the two static help lines get overlapped rather than pushed off the bottom edge. Losing "Use UP/DOWN arrows..." is recoverable; losing an option the player has to pick is not. The busiest screen the game actually builds (3 header rows, 2-line prompt, 7 options) ends at 510px in the 600px window, well clear of this.

src/ui/pygameUserInterface.py:100 (_splitLongWord)

This character-by-character search is O(n^2) in the length of a single unbreakable word. It only runs for a word that is itself wider than the window, which no game string is today — it's here so that a player-entered business name typed as one long run of characters still renders instead of vanishing off the edge.

@dmccoystephenson
dmccoystephenson merged commit 55c8fef into main Jul 25, 2026
1 check passed
@dmccoystephenson
dmccoystephenson deleted the fix/pygame-text-wrapping-and-status-parity branch July 25, 2026 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant