Skip to content

Encounter stats page + /api/runs/encounter-stats endpoint (#335)#336

Merged
ptrlrd merged 1 commit into
mainfrom
feat/encounter-stats
May 22, 2026
Merged

Encounter stats page + /api/runs/encounter-stats endpoint (#335)#336
ptrlrd merged 1 commit into
mainfrom
feat/encounter-stats

Conversation

@ptrlrd
Copy link
Copy Markdown
Owner

@ptrlrd ptrlrd commented May 22, 2026

Closes #335.

New paginated per-encounter view of submitted runs at /leaderboards/encounters, with public API at /api/runs/encounter-stats for third-party tools.

Filters

  • Act — 1 / 2 / 3 (multi-select)
  • Type — monster / elite / boss (multi-select)
  • Players — All / Solo only / Multiplayer only

Columns

Top-level row: encounter name (linked to its detail page) · Act · room type · Runs (sample size) · Fatal count + % · Avg damage taken · Avg turns. Click any row to expand a per-character sub-table with the same columns scoped per character.

Pagination: 50 per page with prev/next + total count.

API

GET /api/runs/encounter-stats?act=1,2&room_type=elite,boss&multiplayer=only&page=1&limit=50

Returns {encounters: [{encounter_id, act, room_type, total, fatal, avg_damage, avg_turns, characters: [{character, total, fatal, avg_damage, avg_turns}]}], page, limit, total, has_next}.

Backend

get_encounter_stats() in runs_db_mongo.py walks each run's map_point_history (acts → rooms), unwinds twice, groups by (encounter_id, act, room_type, character) for the per-character breakdown, then collapses up so the page-limit applies to encounters rather than (encounter, character) pairs. Currently live-aggregates per request — likely OK for now, but worth materializing into stats_summary (alongside the global stats refresher) if traffic warrants. Left as a follow-up so this can ship.

Fatal detection: a room counts as fatal when its model_id matches the run's killed_by and win is falsy. PR #266's extractor refines this to the last matching room — the simpler heuristic here misattributes for runs that hit the same enemy type twice and died on the second instance (rare).

Nav + docs

  • "Encounters" added to the Stats nav dropdown between Stats and Scoring
  • contributing/API_REFERENCE.md updated with the new endpoint

Open follow-ups (not in this PR)

…ter-stats

Adds a paginated per-encounter view of submitted runs. Each row carries
total appearances, fatal count, average damage taken, and average turn
count, with an expandable per-character breakdown.

Backend:
- New aggregation get_encounter_stats() in runs_db_mongo.py walks each
  run's map_point_history, unwinds acts + rooms, groups by encounter id
  + character. Fatal detection uses the run's killed_by field (a
  reasonable approximation; PR #266's extractor handles the
  same-enemy-twice edge case more precisely).
- New endpoint GET /api/runs/encounter-stats with query params:
  act (comma-separated), room_type (comma-separated), multiplayer
  (only/exclude), page, limit (max 200, default 50).
- Currently live-aggregates per request. Worth materializing into
  stats_summary if traffic becomes meaningful — left for a follow-up.

Frontend:
- New page at /leaderboards/encounters with localized mirror at
  /[lang]/leaderboards/encounters.
- Filters: Act (1/2/3 multi-select), Type (monster/elite/boss
  multi-select), Players (any / solo only / multiplayer only).
- 50/page with prev/next pagination + total count.
- Click a row to expand the per-character sub-table.
- Encounter names resolved via /api/encounters with snake-case
  fallback for unknown ids.

Nav:
- 'Encounters' link added to the Stats dropdown between Stats and
  Scoring.

Docs:
- contributing/API_REFERENCE.md gets the new endpoint.
@ptrlrd ptrlrd merged commit 387157c into main May 22, 2026
5 checks passed
ptrlrd added a commit that referenced this pull request May 22, 2026
… script

The /api/runs/encounter-stats endpoint shipped in #336 returned zero rows
because the aggregation walks $map_point_history but that field was
never stored on the run doc — only denormalized fields (killed_by, deck,
relics, card_choices) were. The raw history lives on disk at
data/runs/<hash>.json (which the share-run page already reads).

Two changes:

1. submit_run now includes map_point_history in the inserted doc. Going
   forward, every new submission populates the field so the aggregation
   has data to walk.

2. tools/backfill_run_encounters_mongo.py reads every disk JSON whose
   corresponding Mongo doc is missing the field and $sets it. Idempotent
   — safe to re-run, processes ~5K docs/sec.

Operator run after deploy:

  ssh prod 'cd /var/www/spire-codex && docker compose -f docker-compose.prod.yml exec backend python3 -m tools.backfill_run_encounters_mongo'

(Beta box runs the same script against the beta compose file.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Encounters section on /leaderboards/stats — paginated browser w/ per-character breakdown

1 participant