From e32b256237e41a1ddca74951a6ea12659934dd73 Mon Sep 17 00:00:00 2001 From: McDope Date: Thu, 27 Aug 2026 01:16:49 +0200 Subject: [PATCH 1/3] fix(hud): stop showing the kill count twice on the commit summary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The COMMIT SUMMARY screen rendered the same number in two adjacent rows under two names: "Bugs squashed" and, one line below it, the stats block's "Kills". Both resolve to `local.kills` for the level just cleared — `main.ts` passed `bugsSquashed: stats.kills`, and `statRows`' "Kills" reads `levelPlayerStats.kills`, which `buildPlayerFacingStats` is handed from the same variable. "Bugs squashed" is the older of the two: the summary was once just Lines refactored and Bugs squashed, which is what `EngineStats.kills`' doc comment still described. beta-9 switched the full stats block back on and it brought its own generic Kills row along, so the two ended up side by side. Neither author saw it, because the rows are built in different files and the screen is only assembled at runtime — it took playing a level to notice. Keeping "Kills" and dropping "Bugs squashed", per the request. That loses a bit of flavour on a screen called COMMIT SUMMARY, but "Kills" is the one that generalises: `statRows` is shared with both run-end screens, where the flavour name never appeared, so this also makes the three screens agree on what the row is called. `CommitSummaryInfo.bugsSquashed` goes with it — the single production caller was the only thing supplying it. `stats` stays optional; nothing in the app omits it today, but removing the affordance is a wider change than this needs. The regression test asserts the *count*, not just the absence of the old label: `texts.filter((t) => t === "Kills")` must be exactly 1, so re-introducing the duplicate under any other name still fails it. Verified it fails on the bug by reinstating the row and watching it go red, rather than trusting a green run. Co-Authored-By: Claude Opus 5 (1M context) --- src/engine/engine.ts | 3 ++- src/main.ts | 1 - src/ui/gameHud.test.ts | 21 +++++++++++++++------ src/ui/gameHud.ts | 12 ++++++++---- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/engine/engine.ts b/src/engine/engine.ts index 021819b..f008204 100644 --- a/src/engine/engine.ts +++ b/src/engine/engine.ts @@ -1014,7 +1014,8 @@ export interface EngineStats { * (and, within the current level, can fluctuate) until the exit is reached, * at which point it becomes the baseline the next level carries forward. */ score: number; - /** Enemies defeated this level ("bugs squashed" for the commit summary). */ + /** Enemies defeated this level. Surfaced to the player as the commit + * summary's and both run-end screens' "Kills" row, via `statRows`. */ kills: number; /** True once any cheat has fired this run — drives the HUD's persistent * "run not recorded" badge. See `PlayerState.cheatsUsed`. */ diff --git a/src/main.ts b/src/main.ts index 810e849..8a82082 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3674,7 +3674,6 @@ function launchLevel(path: string, parsed: ParsedFile, source: string | null, ca hud.showCommitSummary( { linesRefactored: parsed.linesOfCode, - bugsSquashed: stats.kills, stats: statsScreenInfo(stats.levelScoreBreakdown, stats.levelPlayerStats), }, () => void advanceToNextLevel(stats), diff --git a/src/ui/gameHud.test.ts b/src/ui/gameHud.test.ts index 2d12aa1..cc47cf1 100644 --- a/src/ui/gameHud.test.ts +++ b/src/ui/gameHud.test.ts @@ -166,24 +166,33 @@ describe("GameHud — overlay content per method", () => { }); it("showCommitSummary draws its stats and has no body lines", () => { - hud.showCommitSummary({ linesRefactored: 120, bugsSquashed: 3 }, vi.fn()); + hud.showCommitSummary({ linesRefactored: 120 }, vi.fn()); const texts = fillTextCalls(); expect(texts).toContain("COMMIT SUMMARY"); expect(texts).toContain("Lines refactored"); expect(texts).toContain("120"); - expect(texts).toContain("Bugs squashed"); - expect(texts).toContain("3"); expect(texts).toContain("Continue"); }); it("showCommitSummary also draws the curated stat rows when given stats", () => { - hud.showCommitSummary({ linesRefactored: 120, bugsSquashed: 3, stats: fakeStatsScreenInfo() }, vi.fn()); + hud.showCommitSummary({ linesRefactored: 120, stats: fakeStatsScreenInfo() }, vi.fn()); const texts = fillTextCalls(); expect(texts).toContain("Weapon accuracy"); expect(texts).toContain("Time survived"); expect(texts).toContain("Damage taken"); }); + it("shows the kill count exactly once, as \"Kills\"", () => { + // Regression: "Bugs squashed" used to sit directly above the stats block + // and render the same number as its "Kills" row, one line apart. The two + // rows were built in different files, which is why it survived review and + // was only caught by playing a level. + hud.showCommitSummary({ linesRefactored: 120, stats: fakeStatsScreenInfo() }, vi.fn()); + const texts = fillTextCalls(); + expect(texts).not.toContain("Bugs squashed"); + expect(texts.filter((t) => t === "Kills")).toHaveLength(1); + }); + it("showMultiplayerResults draws the given title/color and one row per player", () => { hud.showMultiplayerResults( "MULTIPLAYER: CAMPAIGN COMPLETE", @@ -652,7 +661,7 @@ describe("GameHud — no overlay squeezes its own text", () => { // "Path … · Map … · Lore … · Secrets … · Streaks …" needs 446px against // the 286px each side gets in a 620-wide box — the one case that was // squeezed even *with* `wide` set, so no fixed width could have fixed it. - hud.showCommitSummary({ linesRefactored: 1234, bugsSquashed: 567, stats: fakeStatsScreenInfo() }, vi.fn()); + hud.showCommitSummary({ linesRefactored: 1234, stats: fakeStatsScreenInfo() }, vi.fn()); expectNothingSqueezed(); }); @@ -692,7 +701,7 @@ describe("GameHud — no overlay squeezes its own text", () => { const screens: [string, () => void][] = [ ["kernel panic", () => hud.showKernelPanic(stats, vi.fn(), { cheated: true })], ["kernel panic + rollback", () => hud.showKernelPanic(undefined, vi.fn(), { rollback: { remaining: 2, onRollback: vi.fn() } })], - ["commit summary", () => hud.showCommitSummary({ linesRefactored: 1234, bugsSquashed: 567, stats }, vi.fn())], + ["commit summary", () => hud.showCommitSummary({ linesRefactored: 1234, stats }, vi.fn())], ["build successful", () => hud.showBuildSuccessful(stats, vi.fn())], ["level start", () => hud.showLevelStart({ campaign: "stage06_pipeline.py", levelName: "stage06_pipeline.py", roomCount: 12, enemyCount: 34, secretRoomCount: 2 }, vi.fn())], // The worst string in the repo by a distance: `main.ts`'s balance-mismatch diff --git a/src/ui/gameHud.ts b/src/ui/gameHud.ts index d12cc68..2f9fe3c 100644 --- a/src/ui/gameHud.ts +++ b/src/ui/gameHud.ts @@ -38,9 +38,14 @@ export interface StatsScreenInfo { /** Stats shown on the post-level commit summary. */ export interface CommitSummaryInfo { linesRefactored: number; - bugsSquashed: number; - /** This level's own curated stats/breakdown — omitted shows just the two - * fields above, same as before this existed. */ + /** This level's own curated stats/breakdown — omitted shows just the line + * count above, same as before this existed. + * + * The kill count lives in here (`statRows`' "Kills"), not as a field of its + * own. It used to be both: a "Bugs squashed" row sat directly above the + * stats block and rendered the identical number, because the two rows are + * built in different files and nobody saw them together until someone + * played a level. */ stats?: StatsScreenInfo; } @@ -178,7 +183,6 @@ export class GameHud { lines: [], stats: [ ["Lines refactored", String(info.linesRefactored)], - ["Bugs squashed", String(info.bugsSquashed)], ...(info.stats ? statRows(info.stats) : []), ], buttonLabel: "Continue", From f3b6c50d4b64cb3cfa79b7c3bcc6552e23b27a79 Mon Sep 17 00:00:00 2001 From: McDope Date: Thu, 27 Aug 2026 01:19:35 +0200 Subject: [PATCH 2/3] docs(hud): record why "Kills" won, not just that it deduplicated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit justified dropping "Bugs squashed" as losing "a bit of flavour". That undersold it, per the user: the flavour was making a claim. An enemy is a *function* — that is the whole code-to-level mapping — so killing one is not fixing a bug, and a row called "Bugs squashed" asserted a defect where there need not be one. The game also has a literal `Bug`: `placeTodoEncounter` spawns one beside a TODO/FIXME terminal, and it is rare — measured on the demo campaign the tech-debt mix is trap 4 / mine 3 / Bug 1 across all 17 levels. So the row named the one entity in the game that really is a bug, while counting everything except it. Comment only; no behaviour change. Kept as a followup rather than an amend since the branch is already pushed. Co-Authored-By: Claude Opus 5 (1M context) --- src/ui/gameHud.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ui/gameHud.ts b/src/ui/gameHud.ts index 2f9fe3c..846ef25 100644 --- a/src/ui/gameHud.ts +++ b/src/ui/gameHud.ts @@ -45,7 +45,16 @@ export interface CommitSummaryInfo { * own. It used to be both: a "Bugs squashed" row sat directly above the * stats block and rendered the identical number, because the two rows are * built in different files and nobody saw them together until someone - * played a level. */ + * played a level. + * + * **The duplication is why it was noticed; the name is why "Kills" won.** + * An enemy is a *function* — see the code-to-level mapping — so killing one + * is not fixing a bug, and calling it that claimed a defect where there need + * not be one. Worse, the game has a literal `Bug`: `placeTodoEncounter` + * (`map/generation/lore.ts`) spawns one beside a TODO/FIXME terminal, and it + * is rare — measured on the demo campaign the tech-debt mix is trap 4 / + * mine 3 / Bug 1 across all 17 levels. So the row named the one thing in the + * game that genuinely is a bug, while counting everything except it. */ stats?: StatsScreenInfo; } From 76e4927a82f6d36de425d616eccb5575cee23a27 Mon Sep 17 00:00:00 2001 From: McDope Date: Thu, 27 Aug 2026 01:20:35 +0200 Subject: [PATCH 3/3] =?UTF-8?q?docs(hud):=20lead=20with=20the=20real=20obj?= =?UTF-8?q?ection=20=E2=80=94=20it=20claims=20defects=20in=20the=20user's?= =?UTF-8?q?=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correcting my own previous commit, which led with an in-fiction argument (an enemy is a function, so killing one is not fixing a bug). That was the lesser half. The user's actual objection is about the *analysed source*, not the fiction: This game reads somebody's real codebase. "Bugs squashed: 47" tells them their file contained 47 defects. It did not. It contained enemies derived from cyclomatic complexity, and complexity is not defects — a file can be gnarly and correct, or trivial and broken. A tool that points at your code should not assert a defect count it has no basis for, and "Kills" describes what happened without claiming anything about the code. The literal-`Bug` observation stays, demoted to what it is: a second, smaller argument pointing the same way. Comment only; no behaviour change. Co-Authored-By: Claude Opus 5 (1M context) --- src/ui/gameHud.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/ui/gameHud.ts b/src/ui/gameHud.ts index 846ef25..547c415 100644 --- a/src/ui/gameHud.ts +++ b/src/ui/gameHud.ts @@ -48,13 +48,19 @@ export interface CommitSummaryInfo { * played a level. * * **The duplication is why it was noticed; the name is why "Kills" won.** - * An enemy is a *function* — see the code-to-level mapping — so killing one - * is not fixing a bug, and calling it that claimed a defect where there need - * not be one. Worse, the game has a literal `Bug`: `placeTodoEncounter` - * (`map/generation/lore.ts`) spawns one beside a TODO/FIXME terminal, and it - * is rare — measured on the demo campaign the tech-debt mix is trap 4 / - * mine 3 / Bug 1 across all 17 levels. So the row named the one thing in the - * game that genuinely is a bug, while counting everything except it. */ + * This game reads somebody's real source code, so "Bugs squashed: 47" is a + * claim that their file contained 47 defects. It did not — it contained + * enemies derived from cyclomatic complexity, and complexity is not defects. + * Never let a label assert something about the player's codebase that the + * generator has no basis for; "Kills" describes what actually happened and + * claims nothing. + * + * Secondary, but it points the same way: the game has a *literal* `Bug`. + * `placeTodoEncounter` (`map/generation/lore.ts`) spawns one beside a + * TODO/FIXME terminal, and it is rare — measured on the demo campaign the + * tech-debt mix is trap 4 / mine 3 / Bug 1 across all 17 levels. So the row + * named the one entity here that really is a bug, while counting everything + * except it. */ stats?: StatsScreenInfo; }