Skip to content

feat: add the Hall of Deaths - #23

Merged
and3rn3t merged 3 commits into
mainfrom
feature/hall-of-deaths
Sep 19, 2026
Merged

and3rn3t merged 3 commits into
mainfrom
feature/hall-of-deaths

Conversation

@and3rn3t

Copy link
Copy Markdown
Owner

Description

W3 from the family server roadmap, and the
first feature built on the event bus from #22.

Every death now gets a one-line obituary. It is announced in game to everyone
online and kept on a new dashboard page with a leaderboard.

Here lies Jonah, who met Zombie and did not come to an arrangement.
Silas lost an argument with foliage and did not take it well.
Jonah went out with a bang, as they had always quietly hoped to.
Silas tested the lava. The lava tested back, and won.
Jonah ran out of air, and then out of options.

This is deliberately a complete vertical slice: an event arrives, something
visible happens in the game, and a record survives for later. Anything else that
reacts to play follows the same shape, so it doubles as the worked example for
the rest of the roadmap.

api/epitaphs.py

Classifies a vanilla death cause into one of eighteen categories and writes a
line for it, with several lines per category so the same death does not read the
same way twice in an evening.

Matching order matters and is tested. was blown up by Creeper is an explosion
rather than a mob kill, and walked into a cactus while trying to escape Creeper
is a cactus death even though it names a mob. Where a culprit is named it is
extracted and used, with the weapon dropped, because the lines read better
naming the culprit alone.

Selection is seeded by player, cause and timestamp, so a given death always
produces the same epitaph while different deaths vary.

api/hall_of_deaths.py

Subscribes to death events, writes the epitaph, persists the record, then
announces it. Persisting comes first on purpose: announcing needs the game
server to be reachable, and a death is worth keeping even when the announcement
cannot be delivered. Records carry announced so the difference is visible.

Elsewhere

  • New page at /deaths with recent obituaries, summary tiles and the leaderboard.
  • GET /api/deaths and GET /api/deaths/leaderboard, both requiring
    players.view, with matching OpenAPI paths and schema.
  • config/deaths.conf.example. In-game announcements can be turned off while
    keeping the dashboard; colour and retention are configurable.
  • docs/HALL_OF_DEATHS.md, linked from the docs index.

Notes for review

A smoke test caught a real flaw in the leaderboard. It claimed "mostly
drowning" for a player whose four deaths were four different categories, all
tied at one. A favourite only means something once it has happened more than
once, so entries now carry favourite_cause_count, ties resolve deterministically
by count then name rather than by dict insertion order, and the dashboard shows
the player's latest death instead when there is no real pattern.

A player typing a fake death message in chat does not create an entry. The
event bus matches chat before any other pattern, so <Silas> Jonah was slain by Zombie stays chat. There is a test for exactly that, since it is the first thing
either child will try.

Epitaph writing is behind a small interface, and the default writer is
offline.
It costs nothing, returns instantly and needs no API key, which is the
right default for a server running on a Pi in a family's house. A
language-model-backed writer implements write() and drops in. The docs note
that handlers run synchronously on the follower thread, so a network call belongs
on a queue rather than inline; that, plus key handling and the kid-safety
guardrails, is W1 rather than this PR.

Announcement text is JSON-encoded, not interpolated, so an epitaph containing
a quote cannot break the tellraw or inject extra components. Overlong lines are
truncated deliberately, because the server would otherwise truncate them silently.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactor (code change that neither fixes a bug nor adds a feature)
  • Documentation (changes to docs only)
  • Other (please describe):

Checklist

  • I have performed a self-review of my code
  • I have added tests that prove my fix or feature works
  • I have updated documentation as needed
  • Lint and tests pass locally
  • I have not introduced any security vulnerabilities

Verification run locally:

Check Before After
make test-api 368 478
make test-web 150 168
Coverage 52.4% 56.1%

make lint passes, docker compose config validates, Black is clean, and the
production web build succeeds.

Related Issues

🤖 Generated with Claude Code

W3 from the family server roadmap, and the first feature built on the event bus.

Every death now gets a one-line obituary. It is announced in game to everyone
online and kept on a new dashboard page with a leaderboard.

This is deliberately a complete vertical slice: an event arrives, something
visible happens in the game, and a record survives for later. Anything else that
reacts to play follows the same shape, so it doubles as the worked example for
the rest of the roadmap.

api/epitaphs.py

Classifies a vanilla death cause into one of eighteen categories and writes a
line for it. Several lines per category, so the same death does not read the
same way twice in an evening.

Matching order matters and is tested: "was blown up by Creeper" is an explosion
rather than a mob kill, and "walked into a cactus while trying to escape
Creeper" is a cactus death even though it names a mob. Where the message names a
culprit it is extracted and used, with the weapon dropped, because the lines
read better naming the culprit alone.

Selection is seeded by player, cause and timestamp, so a given death always
produces the same epitaph while different deaths vary.

Writing sits behind a small interface. The default writer runs offline, costs
nothing, returns instantly and needs no API key, which is the right default for
a server running on a Pi in a family's house. A writer backed by a language
model implements write() and drops in; the docs note that handlers run on the
follower thread, so a network call belongs on a queue rather than inline.

api/hall_of_deaths.py

Subscribes to death events, writes the epitaph, persists the record, then
announces it. Persisting comes first on purpose: announcing needs the game
server to be reachable, and a death is worth keeping even when the announcement
cannot be delivered. Records carry `announced` so the difference is visible.

Announcement text is JSON-encoded rather than interpolated, so an epitaph
containing a quote cannot break the tellraw or inject extra components, and
overlong lines are truncated deliberately rather than silently by the server.

Leaderboard entries carry favourite_cause_count alongside favourite_cause. A
smoke test showed the leaderboard claiming "mostly drowning" for a player whose
four deaths were four different categories tied at one. A favourite only means
something once it has happened more than once, so the count is exposed, ties
resolve deterministically by count then name, and the dashboard falls back to
the player's latest death when there is no real pattern.

Elsewhere

- New page at /deaths with recent obituaries, summary tiles and the leaderboard.
- GET /api/deaths and GET /api/deaths/leaderboard, both requiring players.view,
  with matching OpenAPI paths and schema.
- getDeaths() and getDeathsLeaderboard() in web/src/services/api.js.
- config/deaths.conf.example: in-game announcements can be turned off while
  keeping the dashboard, and the colour and retention are configurable.
- docs/HALL_OF_DEATHS.md, linked from the docs index.

A player typing a fake death message in chat does not create an entry, because
the event bus matches chat before any other pattern. There is a test for exactly
that, since it is the first thing either child will try.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 19, 2026 14:08
@github-actions github-actions Bot added documentation Documentation additions or updates tests labels Sep 19, 2026
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

Comment thread tests/api/test_epitaphs.py Fixed
Comment thread api/epitaphs.py Fixed
Comment thread api/hall_of_deaths.py Fixed
Comment thread api/hall_of_deaths.py Fixed
Comment thread tests/api/test_hall_of_deaths.py Fixed
Five new alerts, all legitimate:

- Unused imports: Death in tests/api/test_hall_of_deaths.py and
  dataclasses.field in api/hall_of_deaths.py.
- An empty except in the retention-days config parse now states why: a typo
  should leave the default in place rather than stop the server starting.
- The EpitaphWriter protocol method used a bare ellipsis, which reads as an
  ineffectual statement. A docstring says the same thing.
- Two asserts constructed or called into objects inline. Asserts vanish under
  -O, so the work is hoisted out of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved moderate issues affect persistence, event processing, culprit extraction, API documentation, and dashboard behavior.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 5 Medium severity · 1 Low severity

Open (6)
What changed in this PR

Adds the Hall of Deaths vertical slice: death classification, epitaphs, persistence, announcements, APIs, and a dashboard leaderboard.

Changes:

  • Added configurable epitaph generation, storage, announcements, and leaderboard logic.
  • Added secured REST/OpenAPI endpoints and the /deaths React dashboard.
  • Added event-bus integration, tests, configuration, and documentation.
File Reviewed changes and findings
web/​src/​services/​api.js Death API client methods.
web/​src/​services/​__tests__/​api.test.js API client tests.
web/​src/​pages/​HallOfDeaths.jsx Dashboard UI. moderate, 2 votes: Error state also renders the empty-hall message. moderate, 3 votes: Earlier filter requests can overwrite newer results. nit, 2 votes: Filter input lacks an accessible name.
web/​src/​pages/​__tests__/​HallOfDeaths.test.jsx Dashboard tests.
web/​src/​components/​Layout.jsx Navigation entry.
web/​src/​App.jsx Lazy-loaded /deaths route.
tests/​api/​test_hall_of_deaths.py Hall, API, and integration tests.
tests/​api/​test_epitaphs.py Epitaph classification tests.
docs/​INDEX.md Documentation index link.
docs/​HALL_OF_DEATHS.md Feature documentation.
config/​deaths.conf.example Death feature configuration example.
CHANGELOG.md Release notes. nit, 1 vote: Duplicate ### Added heading in the Unreleased section.
api/​server.py REST endpoints and event-bus wiring. moderate, 1 vote: Synchronous RCON announcement delivery can block event processing; enqueue or otherwise bound delivery.
api/​openapi.yaml Death API schemas and paths. moderate, 1 vote: Leaderboard schema omits the implemented causes map.
api/​hall_of_deaths.py Persistence, announcements, statistics, and leaderboard. moderate, 3 votes: Successful announcements do not update the persisted announced value. moderate, 2 votes: Synchronous RCON work can block the follower thread.
api/​epitaphs.py Death classification and epitaph generation. moderate, 2 votes: a skull from ... is captured as the culprit phrase. moderate, 1 vote: Generic was killed by magic phrases are treated as named attackers.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread api/epitaphs.py
Comment thread api/hall_of_deaths.py Outdated
Comment thread api/hall_of_deaths.py Outdated
Comment thread web/src/pages/HallOfDeaths.jsx Outdated
Comment thread web/src/pages/HallOfDeaths.jsx Outdated
Comment thread web/src/pages/HallOfDeaths.jsx
@and3rn3t and3rn3t self-assigned this Sep 19, 2026
All six Copilot findings. Each was verified against the code; all six were real,
and the first two were the serious ones.

Announcing blocked the log follower thread

Event bus handlers run on the thread that follows the server log, and the
production announcer makes a network call: RCON with a five second connection
timeout, then a shell fallback with a thirty second one. An unreachable game
server could therefore stall the follower for up to thirty-five seconds per
death, holding up every other event behind it.

The docs shipped in this branch already said a network call belongs on a queue
rather than inline, and then the code did it inline anyway. Deaths are now
queued and processed on a worker thread, started by start_event_capture(). The
handler enqueues and returns. drain() waits for the queue and stop_worker()
gives queued deaths a chance to be written; without a worker the handler stays
synchronous, which is what the tests use.

`announced` was always false in storage

The record was written before the announcement was attempted, so the stored
value never reflected what happened, and the field was decorative in
/api/deaths. The announcement now runs first and the record is written once with
the real value.

Storing first and patching afterwards would have meant either rewriting the file
or appending a duplicate. Nothing is lost by waiting: the death is already in the
event log by then, and the announcer has a bounded timeout.

Epitaphs named the projectile instead of the mob

"was shot by a skull from Wither" recorded "a skull from Wither" as the culprit,
so the epitaph credited the skull. Indirect forms now resolve to what fired
them, and ordinary names are left alone.

Dashboard

- Typing in the player filter fired a request per keystroke with no ordering
  guard, so a slow response for "S" could overwrite the results for "Si". The
  filter is debounced through the existing useDebounce hook and stale responses
  are discarded by request id.
- A failed request left the list empty, which rendered "NOBODY HAS DIED YET"
  underneath the error and reported a backend failure as good news. The empty
  state now excludes the error case.
- The filter input had no accessible name once its placeholder disappeared. It
  has an aria-label.

Both dashboard fixes have tests that were confirmed to fail without them.

docs/HALL_OF_DEATHS.md described the old ordering and has been corrected, with
the reasoning for the worker and for announcing before storing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@and3rn3t

Copy link
Copy Markdown
Owner Author

All six Copilot findings are fixed in eb006e4. The five CodeQL alerts were already fixed in 000f365, before those threads were filed. Each was checked against the code first; all eleven were real.

The two that mattered:

Announcing blocked the log follower thread. Worse than the diff suggests. The production announcer goes through RCON with a five second connection timeout, then falls back to scripts/rcon-client.sh with a thirty second one, so an unreachable game server could stall the follower for up to thirty-five seconds per death with every later event queued behind it. docs/HALL_OF_DEATHS.md, shipped in this same branch, already said a network call belongs on a queue rather than inline. I wrote that and then did it inline anyway. Deaths are now drained by a worker thread.

announced was always false in storage. The record was written before the announcement was attempted, so the field was decorative and /api/deaths always said false. Announcing now happens first and the record is written once with the real value. I rejected updating the record in place, which the comment suggested, because rewriting a line in a JSONL file means rewriting the whole day file, which is racy against concurrent appends and needless SD-card wear on a Pi.

The other four: epitaphs credited the projectile rather than the mob for was shot by a skull from Wither; the player filter fired a request per keystroke with no ordering guard; a failed request rendered "NOBODY HAS DIED YET" beneath the error and reported an outage as good news; the filter input had no accessible name.

Both dashboard fixes have tests that I confirmed fail without them, rather than assuming they cover the bug.

Check Before After
API tests 478 495
Web tests 168 172
Coverage 56.1% 56.6%

make lint passes, Black and flake8 are clean, docker compose config validates.

🤖 Generated with Claude Code

@and3rn3t
and3rn3t merged commit 0d36203 into main Sep 19, 2026
17 checks passed
@and3rn3t
and3rn3t deleted the feature/hall-of-deaths branch September 19, 2026 14:22
@and3rn3t and3rn3t added the feature New feature request label Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Documentation additions or updates feature New feature request tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants