Report the overnight fleet news everywhere a day rolls over - #172
Conversation
increaseDay() returns {"evicted": bool, "report": [str]} and its docstring
says the player has to be told rather than left to notice their roster
changed. Three of the six places a day rolls over read only "evicted" and
dropped "report"; the voyage leg bound neither.
- a captained voyage leg (docks) bound nothing at all, so a five-day
voyage could end with the player evicted and no line of text saying so
- a fishing trip long enough to cross 8am read only "evicted"
- FishE's hourly tick, the one place guaranteed to run whatever the
player did, read only "evicted"
The three that worked were three hand-rolled copies of the same loop. All
six now go through dayReportLines()/appendDayReport(), added next to the
producer in timeService so reading one half of the contract and dropping
the other stops being the easy thing to write. Docks._reportTheDay, a
fourth copy, is gone.
appendDayReport takes the separator so FishE's loop keeps its wider gap -
there the day's news is appended to text another subsystem wrote, which
is not true of the location sites.
The voyage's day now rolls before its dialogue is built, so the news
lands on the same screen as the leg that caused it. The fishing trip
collects its lines and folds them into the trip report, since that report
is written after the loop and would otherwise overwrite them.
Tests cover all six sites plus the helper; verified they fail if the
report is dropped again.
Closes #151
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dmccoystephenson
left a comment
There was a problem hiding this comment.
Self-review: read the full diff against #151 and traced each of the six rollover sites. No correctness problems found. Notes below record the two places where the fix is not a straight swap (and so is the most likely thing a later reader breaks), one deliberate behaviour change worth calling out explicitly, and one scope decision.
The thing I'd want a reviewer to check hardest is the ordering change on the voyage leg — it is the only site where this PR moves an existing call rather than just reading more of its return value.
Worth stating plainly, since the diff doesn't show it: all 791 pre-existing tests passed before this fix. Nothing covered the report at any of the six sites, only the eviction flag — which is exactly how three sites drifted. So I checked the new tests against a simulated re-introduction of the bug (dayReportLines stubbed to drop the report) and confirmed the nine expected tests fail and nothing else does. Without that step these tests would assert current behaviour rather than the fix.
| # same screen as the leg that caused it; dropping it is how a | ||
| # player could sail a five-day voyage and come home evicted with | ||
| # nothing on screen having said so. | ||
| dayLines = dayReportLines(self.timeService.increaseDay()) |
There was a problem hiding this comment.
This is the one site where the fix reorders existing calls rather than just reading more of a return value, so it deserves the closest look.
increaseDay() used to run after showDialogue, so the day's news could not have gone on that screen even if it had been bound. Rolling the day first is what lets it land on the same screen as the leg that caused it, instead of a screen later or not at all.
What I checked: nothing between the old and new position reads the clock or the player's money, so moving it changes only when the text appears, not what the leg resolves to. adventures.advanceLeg is still called before the roll, and adventures.isOver(voyage) is driven by legs sailed rather than by timeService.day, so the loop's termination is unaffected — test_a_voyage_sails_every_leg_costs_days_and_comes_home still asserts one day per leg.
The "\n".join([outcome] + notes) if notes else outcome conditional was also dropped: joining a one-item list returns the item, so the ternary was always equivalent to the join.
| # as it happens, because that report is written further down and would | ||
| # overwrite anything put on the prompt now. A trip runs 1-10 hours, so | ||
| # it crosses at most one 8am boundary and these lines can't stack up. | ||
| dayLines = [] |
There was a problem hiding this comment.
The one site that can't use appendDayReport directly, flagged so it isn't "simplified" into a call that silently loses the report.
The trip's own summary is assigned with = further down (self.currentPrompt.text = "You caught %d ..."), not appended — so anything written to the prompt inside this loop is overwritten a few lines later. The lines have to be collected and appended after that assignment, which is why this reads dayLines += here and a plain loop at the end rather than the shared helper.
The previous evicted = True flag collapsed repeats implicitly; a list doesn't. That's safe because hours = random.randint(1, 10), so a trip crosses at most one 8am boundary — but it is the assumption to re-check if the hour range ever widens.
| return lines | ||
|
|
||
|
|
||
| def appendDayReport(prompt, summary, separator=" "): |
There was a problem hiding this comment.
separator is a parameter used by exactly one caller, which normally isn't worth the knob — so, the reasoning:
#151 flagged that fishE.py appended with two spaces where the other five sites use one, reading like an inconsistency to be normalised away. It isn't. In a location the line continues a sentence that method just wrote ("You sleep until the next morning. The fleet landed..."), whereas in FishE's game loop it is appended to whatever a different subsystem already put on the screen, alongside the milestone and unlock announcements that use the same wider gap.
So rather than flattening both to one space (which would visually merge two unrelated messages) or leaving five copies of a magic " ", the difference is now a named parameter with the reason attached. Both spacings are asserted — test_appendDayReport_separator_defaults_to_one_space and ..._honours_a_wider_separator.
| } | ||
|
|
||
|
|
||
| def dayReportLines(summary): |
There was a problem hiding this comment.
Two scope notes on where this landed.
Why timeService and not a UI module: these are module functions on the same file as the increaseDay() whose return value they interpret. That adjacency is the actual fix — the bug was six callers each deciding how to read a two-key dict, and half of them reading one key. timeService.py already imports housing, so EVICTION_MESSAGE costs no new dependency. They take a duck-typed prompt (anything with .text) rather than importing Prompt, so this stays free of front-end coupling.
Why .get("report", []) rather than requiring the key: existing tests mock increaseTime as {"evicted": False} with no report key at all (e.g. test_fish_mentions_eviction_when_a_day_rolls_over_mid_trip). Being strict here would have failed those tests for reasons unrelated to what they assert, and would make the helper hostile to any caller that hasn't been updated yet. test_dayReportLines_tolerates_a_summary_with_no_report_key pins that tolerance so it isn't tightened by accident.
Summary
TimeService.increaseDay()returns{"evicted": bool, "report": [str]}, and its own docstring states the contract: "a pirate crew can come home a man short, the player has to be told rather than left to notice their roster changed."README.md:75makes the same promise. Three of the six places a day rolls over honoured it; three read onlyevictedand droppedreport.Fixed — the three that dropped it:
docks.py) bound nothing fromincreaseDay(). Each leg runs wages, rent, crew walkouts and possible eviction, so a player could sail a five-day voyage and come home homeless without a single line of text saying so.evicted, so a crew member who didn't come back went unmentioned until the player happened to open Manage Fleet.FishE's hourly tick — the one place guaranteed to run whatever the player did — read onlyevicted.What changed
The three sites that did work were three hand-rolled copies of the same loop (
home.pyandtavern.pywere character-identical toDocks._reportTheDay's body). All six now go through two functions added next to the producer intimeService.py:dayReportLines(summary)— the display lines: the fleet's takings, then the eviction notice. Lives besideincreaseDayso reading one half of the contract and dropping the other stops being the easy thing to write. Tolerates a summary with noreportkey, so callers (and existing tests) that only ever produced an eviction flag don't raise.appendDayReport(prompt, summary, separator=" ")— appends those lines to a prompt.Docks._reportTheDay— a fourth copy, then a one-line passthrough — is gone; its single call site calls the shared helper directly.Two call sites needed more than a swap:
"\n".join([outcome] + notes) if notes else outcomeconditional was redundant (joining a one-item list returns the item) and collapsed into the unconditional join.separatorexists becauseFishE's loop deliberately uses a wider gap — there the day's news is appended to text a different subsystem already wrote, which isn't true of the location sites. This also resolves the spacing inconsistency the issue flagged, by making it an explicit parameter with a stated reason rather than an accident.Verification
The existing 791 tests all passed before this fix, which is why the bug survived — no test covered the reporting at any of the six sites except the eviction flag. So the new tests were checked against a simulated regression: with
dayReportLinesstubbed to drop the report, exactly these fail, and nothing else does.All six sites are now guarded: the export site's existing eviction test was extended to assert the fleet report too, so a future per-site drift fails the build instead of going quiet.
Test plan
python3 -m compileall -q src testsSDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy python3 -m pytest --verbose -vv --cov=src --cov-report=term-missing --cov-report=xml:cov.xml— 803 passed, 97% total coverageshowDialoguecall — both already front-end agnostic, so console, pygame, web and Pyodide all get it through the same path with no per-UI work.Player/Stats/TimeServicefield added, renamed or retyped, so noschemas/*.jsonchange applies.README.md:75already promised this overnight report — the fix makes that promise true at all six sites instead of three, so no wording change was needed.PLANNING.mdunaffected.black/autoflakeperformat.sh;housingbecame an unused import indocks.py,tavern.pyandfishE.pyand was removed. Only the ten files in this diff are touched.Closes #151
drafted by Claude on behalf of Daniel Stephenson