From 788912fc720c8ed81a8494365803f27210d40fc9 Mon Sep 17 00:00:00 2001 From: Daniel McCoy Stephenson Date: Wed, 26 Aug 2026 01:30:29 -0600 Subject: [PATCH 1/2] Report the run's own score on the obituary screen formatObituaryLines() read name, level, length, ticksSurvived and causeOfDeath off the obituary record but never score, even though SaveManager.recordRun() stores it and the function's own docstring lists it. The only score anywhere on the end-of-run screen was the chronicle's "Highest score ever", which is the lifetime maximum from lifetimeStats - so a player was shown their all-time best at the exact moment a run was summarised, with no way to see what the run just played scored, including when that run was the one that set the best. The narrative line now carries the run's score, read with a default of 0 so obituaries written before score was recorded still render. Both UIs render through this one formatter, so the graphical and text interfaces pick the change up together. README's Scoring section says where the run's score is reported once the run is over. Closes #136 Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 3 +++ src/progression/obituary.py | 12 ++++++++++-- tests/progression/test_obituary.py | 23 +++++++++++++++++++++++ tests/test_ophidian_run_lifecycle.py | 22 ++++++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b754aa6..cfc5a8d 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,9 @@ as long as the multiplier lasts. Note that the score resets at the start of each level, alongside the board. +When a run ends, the score it finished on is reported in the obituary that both UIs +show, alongside the all-time best in the chronicle printed beneath it. + ## Controls Key | Action ------------ | ------------- diff --git a/src/progression/obituary.py b/src/progression/obituary.py index f16cdb1..6cff880 100644 --- a/src/progression/obituary.py +++ b/src/progression/obituary.py @@ -41,13 +41,21 @@ def formatObituaryLines(obituary): level = obituary.get("level", 1) length = obituary.get("length", 0) ticks = obituary.get("ticksSurvived", 0) + # obituaries written before score was recorded have no "score" key + score = obituary.get("score", 0) cause = causeOfDeathPhrase(obituary.get("causeOfDeath")) return [ "== Obituary ==", "The ophidian {name} lived to level {level}, reaching a length of " - "{length}, surviving {ticks} ticks, before {cause}.".format( - name=name, level=level, length=length, ticks=ticks, cause=cause + "{length} and a score of {score}, surviving {ticks} ticks, before " + "{cause}.".format( + name=name, + level=level, + length=length, + score=score, + ticks=ticks, + cause=cause, ), ] diff --git a/tests/progression/test_obituary.py b/tests/progression/test_obituary.py index 8837376..0eec9b0 100644 --- a/tests/progression/test_obituary.py +++ b/tests/progression/test_obituary.py @@ -61,6 +61,7 @@ def test_format_obituary_lines_interpolates_fields_for_collision(): assert "Noodle" in narrative assert "level 3" in narrative assert "length of 12" in narrative + assert "score of 789" in narrative assert "surviving 456 ticks" in narrative assert "colliding with itself" in narrative @@ -82,10 +83,23 @@ def test_format_obituary_lines_falls_back_when_name_missing(): assert "Unnamed Ophidian" in lines[1] +def test_format_obituary_lines_reports_the_score_of_the_run_that_just_ended(): + lines = formatObituaryLines(sampleObituary(score=1234)) + assert "score of 1234" in lines[1] + + +def test_format_obituary_lines_defaults_score_for_records_predating_it(): + stale = sampleObituary() + del stale["score"] + lines = formatObituaryLines(stale) + assert "score of 0" in lines[1] + + def test_format_obituary_lines_handles_empty_dict(): lines = formatObituaryLines({}) assert lines[0] == "== Obituary ==" assert "Unnamed Ophidian" in lines[1] + assert "score of 0" in lines[1] assert "unknown causes" in lines[1] @@ -115,3 +129,12 @@ def test_format_obituary_screen_combines_both_sections_with_separator(): joined = "\n".join(lines) assert "Noodle" in joined assert "Total runs: 7" in joined + + +def test_format_obituary_screen_distinguishes_run_score_from_lifetime_best(): + lines = formatObituaryScreen( + sampleObituary(score=789), sampleLifetimeStats(highestScore=999) + ) + joined = "\n".join(lines) + assert "score of 789" in joined + assert "Highest score ever: 999" in joined diff --git a/tests/test_ophidian_run_lifecycle.py b/tests/test_ophidian_run_lifecycle.py index d75101f..23466d2 100644 --- a/tests/test_ophidian_run_lifecycle.py +++ b/tests/test_ophidian_run_lifecycle.py @@ -1,5 +1,6 @@ import time +from progression.obituary import formatObituaryScreen from textui.textrenderer import TextRenderer from ophidian import Ophidian @@ -186,6 +187,27 @@ def test_restart_records_the_run_before_reinitializing_the_board(tmp_path, monke assert len(game.snakeParts) == 1 +def test_ending_a_run_shows_that_run_s_score_on_the_obituary_screen( + tmp_path, monkeypatch +): + # regression test: the screen reported the lifetime best and never the + # score of the run just played (see issue #136). The lifetime best is + # seeded above the run's score so the run's number can't be satisfied by + # the chronicle line. + game = _makeGame(monkeypatch, tmp_path) + game.saveManager.data["lifetimeStats"]["highestScore"] = 9999 + game.score = 4242 + + game.recordCurrentRun("collision") + lines = formatObituaryScreen( + game.lastObituary, game.saveManager.data["lifetimeStats"] + ) + + joined = "\n".join(lines) + assert "score of 4242" in joined + assert "Highest score ever: 9999" in joined + + def test_restart_still_reinitializes_and_signals_restart(tmp_path, monkeypatch): game = _makeGame(monkeypatch, tmp_path) calls = [] From 0b803a85b8d8013afaaad14177e90fd5b12a7b38 Mon Sep 17 00:00:00 2001 From: Daniel McCoy Stephenson Date: Wed, 26 Aug 2026 01:32:18 -0600 Subject: [PATCH 2/2] Move the obituary-score test out of the restart cluster The new end-to-end test was inserted between two restart tests, splitting a group that otherwise reads top to bottom. It belongs after them, since it is about what an ended run reports rather than about restarting one. Co-Authored-By: Claude Opus 5 (1M context) --- tests/test_ophidian_run_lifecycle.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/test_ophidian_run_lifecycle.py b/tests/test_ophidian_run_lifecycle.py index 23466d2..867d920 100644 --- a/tests/test_ophidian_run_lifecycle.py +++ b/tests/test_ophidian_run_lifecycle.py @@ -187,6 +187,19 @@ def test_restart_records_the_run_before_reinitializing_the_board(tmp_path, monke assert len(game.snakeParts) == 1 +def test_restart_still_reinitializes_and_signals_restart(tmp_path, monkeypatch): + game = _makeGame(monkeypatch, tmp_path) + calls = [] + monkeypatch.setattr( + game, "checkForLevelProgressAndReinitialize", lambda: calls.append("reinit") + ) + + result = game.handleKeyDownEvent("r") + + assert calls == ["reinit"] + assert result == "restart" + + def test_ending_a_run_shows_that_run_s_score_on_the_obituary_screen( tmp_path, monkeypatch ): @@ -206,16 +219,3 @@ def test_ending_a_run_shows_that_run_s_score_on_the_obituary_screen( joined = "\n".join(lines) assert "score of 4242" in joined assert "Highest score ever: 9999" in joined - - -def test_restart_still_reinitializes_and_signals_restart(tmp_path, monkeypatch): - game = _makeGame(monkeypatch, tmp_path) - calls = [] - monkeypatch.setattr( - game, "checkForLevelProgressAndReinitialize", lambda: calls.append("reinit") - ) - - result = game.handleKeyDownEvent("r") - - assert calls == ["reinit"] - assert result == "restart"