Skip to content

feat(livestats): per-player killedBy + deathPosition + winner for live standings#4593

Merged
evanpelle merged 4 commits into
openfrontio:mainfrom
Zixer1:feat/ofm-live-standings-fields
Jul 16, 2026
Merged

feat(livestats): per-player killedBy + deathPosition + winner for live standings#4593
evanpelle merged 4 commits into
openfrontio:mainfrom
Zixer1:feat/ofm-live-standings-fields

Conversation

@Zixer1

@Zixer1 Zixer1 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Description:

Enriches the admin-bot live-stats snapshot (/api/adminbot/game/:id/stats) with the per-player fields a semi-live standings board needs, so it can score kills + placement live instead of waiting for the post-game record.

Per player in liveStats.players[]:

  • killedBy — the eliminator's clientID (null while alive / non-client killer). Invert → live kill points.
  • deathPosition — finishing place at elimination = non-bot players still standing + 1 (frozen at "last of the then-standing"). Null while alive → live placement.

The full roster is reported (dead players are retained in the client view), plus top-level winner (the decided winner's clientID, else null).

Deterministic / consensus-safe: killedBy (stamped at recordKill) and deathPosition (stamped in PlayerExecution) are pure sim values, so in-sync clients agree on the majority vote; winner is added server-side in GameServer.liveStats() from the winner vote (like the existing publicID/username/connected enrichment).

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory

…e standings

The admin-bot live-stats snapshot already carries tilesOwned + publicID, but not
who eliminated a player, where they finished, or whether the game is decided, so
a semi-live standings board can't score kills or placement off it and must wait
for the post-game record. Surface those live.

All deterministic sim values (so they survive the per-turn majority consensus),
and the full roster is reported (dead players are retained in the client view):
- PlayerImpl: killedBy (eliminator clientID) stamped at recordKill; deathPosition
  (finishing place = non-bot players still standing + 1) stamped when the player
  hits zero tiles in PlayerExecution. Idempotent, first write wins.
- Threaded through the Player interface, the PlayerUpdate diff/apply, PlayerView,
  and the live-stats wire schema.
- GameServer.liveStats() adds the decided winner's clientID (server-side, from the
  winner vote) alongside the existing publicID/username/connected enrichment.

Tests: diff + apply cover the new fields; fixtures updated; full suite green.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 425e7c46-0689-4257-99af-246c3cea8198

📥 Commits

Reviewing files that changed from the base of the PR and between 7b922d8 and 61f5ca9.

📒 Files selected for processing (3)
  • src/core/Schemas.ts
  • src/core/game/PlayerImpl.ts
  • src/server/GameServer.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/core/game/PlayerImpl.ts
  • src/server/GameServer.ts
  • src/core/Schemas.ts

Walkthrough

Elimination metadata now records killer identity and finishing position, propagates through player updates and renderer state, and appears in live-stat snapshots. The server live-stats response also reports the player winner client ID when available.

Changes

Elimination live standings

Layer / File(s) Summary
Record elimination metadata
src/core/StatsSchemas.ts, src/core/game/Stats.ts, src/core/game/StatsImpl.ts, src/core/execution/PlayerExecution.ts, src/core/game/GameImpl.ts
Stats record first-write-wins killer attribution and death positions for conquest and death handling.
Propagate player metadata
src/core/game/GameUpdates.ts, src/core/game/PlayerImpl.ts, src/core/game/GameUpdateUtils.ts, src/client/render/types/Renderer.ts, src/client/view/PlayerView.ts, src/core/Schemas.ts, src/client/controllers/LiveStatsController.ts
Player updates, renderer state, and live-stat snapshots carry killedBy and deathPosition.
Publish live-stat winner
src/server/GameServer.ts
liveStats() returns the winning player’s client ID or null.
Validate update and live-stat shapes
tests/LiveStandingsFields.test.ts, tests/GameUpdateUtils.test.ts, tests/server/LiveStats.test.ts, tests/client/render/frame/derive/*
Tests cover metadata recording, update propagation, default state fields, live-stat fields, and winner output.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GameImpl
  participant StatsImpl
  participant PlayerImpl
  participant LiveStatsController
  GameImpl->>StatsImpl: record killer and death position
  StatsImpl->>PlayerImpl: expose death stats in PlayerUpdate
  PlayerImpl->>LiveStatsController: provide player snapshot fields
  LiveStatsController->>LiveStatsController: emit enriched live stats
Loading

Possibly related PRs

Suggested labels: Backend, Feature

Poem

Kill marks bloom in stats,
Positions settle into place,
Updates carry the tale,
Live standings join the race,
A winner’s name appears.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding per-player live standings fields and winner data.
Description check ✅ Passed The description is on-topic and accurately describes the live-stats, per-player fields, winner, and related tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Zixer1 Zixer1 added this to the Backlog milestone Jul 13, 2026

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/server/LiveStats.test.ts (2)

58-67: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a non-null elimination metadata case.

This fixture only verifies killedBy: null and deathPosition: null. Add a snapshot with a real killer ID and positive death position, then assert consensus preserves both fields.

Based on the PR objective requiring live elimination attribution and placement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/server/LiveStats.test.ts` around lines 58 - 67, Extend the snapshot
fixture and its consensus assertions in the LiveStats tests to cover a player
with a non-null killedBy value and positive deathPosition. Verify the consensus
result preserves both elimination metadata fields, while retaining the existing
null-field coverage.

94-94: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test a player winner, not only null.

Both new expectations cover the undecided case. Add a winner vote such as ["player", "client01"] and assert that liveStats().winner returns that client ID; otherwise an implementation that always returns null would still pass these tests.

Based on the new server-side winner contract.

Also applies to: 157-157

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/server/LiveStats.test.ts` at line 94, Add a decided-winner case to the
LiveStats tests near the existing winner expectations: provide a winner vote
such as ["player", "client01"] and assert that liveStats().winner returns
"client01", while retaining the existing null/undecided coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/execution/PlayerExecution.ts`:
- Around line 72-79: Record death position immediately in
GameImpl.conquerPlayer() when a player is conquered, calculating the alive
non-bot player count while excluding the conquered player and assigning that
count plus one. Retain the existing fallback calculation in PlayerExecution’s
elimination branch for deaths not handled through conquest, avoiding duplicate
or conflicting updates.

In `@src/core/game/PlayerImpl.ts`:
- Around line 143-149: Update the _killedBy state and markKilledBy flow so an
explicitly recorded null remains a finalized first result and cannot be
overwritten by later calls. Distinguish the unset state from a stamped null,
using a separate flag or an internal undefined sentinel, while preserving the
existing first-write-wins behavior for both killer and death position.

---

Nitpick comments:
In `@tests/server/LiveStats.test.ts`:
- Around line 58-67: Extend the snapshot fixture and its consensus assertions in
the LiveStats tests to cover a player with a non-null killedBy value and
positive deathPosition. Verify the consensus result preserves both elimination
metadata fields, while retaining the existing null-field coverage.
- Line 94: Add a decided-winner case to the LiveStats tests near the existing
winner expectations: provide a winner vote such as ["player", "client01"] and
assert that liveStats().winner returns "client01", while retaining the existing
null/undecided coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c3c1d60f-f70e-40a1-b78a-8bf29d13201d

📥 Commits

Reviewing files that changed from the base of the PR and between e57939a and 168f4a4.

📒 Files selected for processing (15)
  • src/client/controllers/LiveStatsController.ts
  • src/client/render/types/Renderer.ts
  • src/client/view/PlayerView.ts
  • src/core/Schemas.ts
  • src/core/execution/PlayerExecution.ts
  • src/core/game/Game.ts
  • src/core/game/GameImpl.ts
  • src/core/game/GameUpdateUtils.ts
  • src/core/game/GameUpdates.ts
  • src/core/game/PlayerImpl.ts
  • src/server/GameServer.ts
  • tests/GameUpdateUtils.test.ts
  • tests/client/render/frame/derive/nuke-telegraphs.test.ts
  • tests/client/render/frame/derive/player-status.test.ts
  • tests/server/LiveStats.test.ts

Comment thread src/core/execution/PlayerExecution.ts Outdated
Comment thread src/core/game/PlayerImpl.ts Outdated
@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Jul 13, 2026
Addresses review:
- GameImpl.conquerPlayer stamps deathPosition (excluding the conquered from the
  alive count) so a game-ending tick still records the finishing place;
  PlayerExecution keeps the same stamp as a fallback for non-conquest deaths
  (setDeathPosition is first-write-wins).
- markKilledBy uses an explicit stamped flag, so a recorded null killer (a
  bot/nation elimination) can't be overwritten by a later call.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 13, 2026
Comment thread src/core/game/Game.ts Outdated
Comment on lines +554 to +559
// OFM live standings (see PlayerImpl): eliminator's clientID (null while alive
// or a non-client killer) + finishing position at elimination. Stamped once.
killedBy(): ClientID | null;
deathPosition(): number | null;
markKilledBy(clientID: ClientID | null): void;
setDeathPosition(position: number): void;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can this go in playerstats instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These need to be live, per-tick. PlayerStats/allPlayersStats only gets emitted on the game-end Win update (setWinner → GameUpdateType.Win) and read off winner.allPlayersStats, so it's not there until the game ends.
So instead we enrich LiveStatsController (same as the existing isAlive field that tracks dead players), which snapshots every ~10s off the live PlayerView.

Main reasons, is that the settle sometimes takes too long and delays our games, and we want to see live standings instead of waiting for game end.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i mean stored in stats here:

export const PlayerStatsSchema = z
.object({
attacks: AtLeastOneNumberSchema.optional(),
betrayals: BigIntStringSchema.optional(),
killedAt: BigIntStringSchema.optional(),
// Tiles owned at game end, for OFM standings (set on setWinner).
finalTiles: BigIntStringSchema.optional(),
// Humans this player eliminated (victim clientID + tick), for OFM kill scoring.
kills: z
.array(z.object({ victim: z.string(), tick: BigIntStringSchema }))
.optional(),
conquests: AtLeastOneNumberSchema.optional(),
boats: z.partialRecord(BoatUnitSchema, AtLeastOneNumberSchema).optional(),
bombs: z.partialRecord(BombUnitSchema, AtLeastOneNumberSchema).optional(),
gold: AtLeastOneNumberSchema.optional(),
units: z.partialRecord(OtherUnitSchema, AtLeastOneNumberSchema).optional(),
})
.optional();

and then this execution does this.mg.stats.player(...).betrayals etc. That way we don't bloat the Player interface.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@evanpelle Ah ok, should be good now

Move the OFM live-standings elimination fields off the Player interface and
into PlayerStats (set via mg.stats().recordKilledBy / recordDeathPosition),
per review feedback, so the Player interface isn't bloated with them.

They still surface on the live PlayerUpdate: toFullUpdate reads them back from
getPlayerStats() every tick, so the standings board stays live (not game-end
only). getPlayerStats doesn't create-on-read, so there's no per-tick bloat.

- StatsSchemas: add killedBy (nullable, null = non-client killer) + deathPosition
- Stats/StatsImpl: recordKilledBy + recordDeathPosition, both first-write-wins
- conquerPlayer + PlayerExecution stamp via mg.stats() instead of Player methods
- Remove Player.killedBy/deathPosition/markKilledBy/setDeathPosition + fields
- Add sim-level test for the stats->live-update path
@Zixer1

Zixer1 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Done. Moved both fields onto PlayerStats and set them via mg.stats().recordKilledBy() / recordDeathPosition() from conquerPlayer + PlayerExecution, and dropped them from the Player interface (no more killedBy/deathPosition/markKilledBy/setDeathPosition).

One thing I kept: these need to stay live on the PlayerUpdate every tick (the standings board is semi-live, not game-end only), so toFullUpdate reads them back through getPlayerStats(). That path doesn't create-on-read, so there's no per-tick allocation. the stats entry is only ever created at elimination (same as recordFinalTiles). Added a sim-level test covering the stats -> live-update path. Pushed.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
src/core/StatsSchemas.ts (1)

111-115: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add integer and positive validations to match downstream schema.

Make the schema for deathPosition strict to match downstream usage. PlayerLiveStatsSchema enforces an integer and positive number constraint (z.number().int().positive().nullable()), so aligning this schema ensures data consistency and prevents invalid fractional or negative placements from being persisted.

💡 Proposed refactor
     // first-write-wins. Surfaced live on the PlayerUpdate (not just at game end).
     killedBy: z.string().nullable().optional(),
-    deathPosition: z.number().optional(),
+    deathPosition: z.number().int().positive().optional(),
     // Tiles owned at game end, for OFM standings (set on setWinner).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/StatsSchemas.ts` around lines 111 - 115, Update deathPosition in the
relevant stats schema to enforce integer and positive-number validation,
matching PlayerLiveStatsSchema; preserve its optional behavior and existing
killedBy definition.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/core/StatsSchemas.ts`:
- Around line 111-115: Update deathPosition in the relevant stats schema to
enforce integer and positive-number validation, matching PlayerLiveStatsSchema;
preserve its optional behavior and existing killedBy definition.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78413c87-aae4-4528-84f7-2e8ae428a52c

📥 Commits

Reviewing files that changed from the base of the PR and between 52d5d39 and 7b922d8.

📒 Files selected for processing (7)
  • src/core/StatsSchemas.ts
  • src/core/execution/PlayerExecution.ts
  • src/core/game/GameImpl.ts
  • src/core/game/PlayerImpl.ts
  • src/core/game/Stats.ts
  • src/core/game/StatsImpl.ts
  • tests/LiveStandingsFields.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/core/game/GameImpl.ts
  • src/core/execution/PlayerExecution.ts

@github-project-automation github-project-automation Bot moved this from Development to Final Review in OpenFront Release Management Jul 16, 2026
@evanpelle evanpelle merged commit f6e1b8f into openfrontio:main Jul 16, 2026
10 checks passed
@github-project-automation github-project-automation Bot moved this from Final Review to Complete in OpenFront Release Management Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

2 participants