Skip to content

Leaderboard update part 2: stat columns#4465

Open
jsshap wants to merge 1 commit into
openfrontio:mainfrom
jsshap:feature/leaderboard-new-items
Open

Leaderboard update part 2: stat columns#4465
jsshap wants to merge 1 commit into
openfrontio:mainfrom
jsshap:feature/leaderboard-new-items

Conversation

@jsshap

@jsshap jsshap commented Jul 1, 2026

Copy link
Copy Markdown

Before opening a PR: discuss new features on Discord first, and file bugs or small improvements as issues. You must be assigned to an approved issue - unsolicited PRs will be auto-closed.

Add approved & assigned issue number here:

Resolves #4074

Description:

Leaderboard update part 2. Adds the new leaderboard stat items on a branch based directly on main.

This adds rolling 60 second Gold/min tracking, sends it through packed player updates, and displays Gold/min, current Troops, and Cities in the in-game leaderboard.

The configurable column chooser is split out into #4453.

Leaderboard

Testing: npx vitest tests/PackedPlayerUpdates.test.ts tests/PlayerUpdateDiff.test.ts tests/GameUpdateUtils.test.ts tests/client/view/GameView.test.ts --run; npx tsc --noEmit; targeted ESLint/Prettier

Please complete the following:

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

Please put your Discord username so you can be contacted if a bug or regression is found:

jsshap

@jsshap jsshap requested a review from a team as a code owner July 1, 2026 00:29
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a goldPerMinute income stat computed server-side using bucketed gold-income accumulation. The stat is threaded through packed player update buffers (now 5 fields instead of 4), the PlayerUpdate diff/merge contract, client-side PlayerState/PlayerView, and displayed with troops and cities in the leaderboard UI. addGold gains a countAsIncome flag so donations don't count as income.

Changes

Gold Per Minute Income Stat

Layer / File(s) Summary
Server-side gold income bucket tracking
src/core/game/PlayerImpl.ts, src/core/game/Game.ts, src/core/game/GameImpl.ts
addGold gains a countAsIncome flag; new bucket constants and per-player accumulator fields track gold income over time buckets and compute a per-minute rate exposed via goldPerMinute(); donateGold excludes recipient income.
PlayerUpdate contract and diff/merge handling
src/core/game/GameUpdates.ts, src/core/game/GameUpdateUtils.ts, tests/GameUpdateUtils.test.ts
PlayerUpdate gains optional goldPerMinute; it's excluded from per-tick diffs but merged in applyStateUpdate; tests cover churn-ignoring and merge behavior.
Client view and renderer state
src/client/render/types/Renderer.ts, src/client/view/GameView.ts, src/client/view/PlayerView.ts, tests/client/..., tests/util/viewStubs.ts
PlayerState adds goldPerMinute; GameView parses packed updates as 5-tuples instead of 4-tuples; PlayerView gains a goldPerMinute() accessor; test fixtures/stubs updated accordingly.
Leaderboard sorting and rendering
src/client/hud/layers/Leaderboard.ts, resources/lang/en.json
New SortKey union and Entry fields for goldPerMinute, troops, cities; sort switch, grid columns, header controls, and row cells updated to show these stats; new translation strings added.
Packed update and diff round-trip tests
tests/PackedPlayerUpdates.test.ts, tests/PlayerUpdateDiff.test.ts
Tests updated to validate 5-number packed records, goldPerMinute expiry over time, and that donated gold is excluded from recipient income.

Sequence Diagram(s)

sequenceDiagram
  participant Player
  participant PlayerImpl
  participant GameUpdateViewData
  participant GameView
  participant Leaderboard

  Player->>PlayerImpl: addGold(amount, tile, countAsIncome=true)
  PlayerImpl->>PlayerImpl: recordGoldIncome into time buckets
  GameUpdateViewData->>PlayerImpl: toUpdate() / toFullUpdate()
  PlayerImpl-->>GameUpdateViewData: goldPerMinute() computed from buckets
  GameUpdateViewData->>GameView: packedPlayerUpdates [smallID, tilesOwned, gold, goldPerMinute, troops]
  GameView->>GameView: update PlayerState.goldPerMinute
  Leaderboard->>GameView: read goldPerMinute via PlayerView
  Leaderboard-->>Player: render Gold/min column, sortable
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

  • openfrontio/OpenFrontIO#3967: Extends the same applyStateUpdate/PlayerUpdate diff-merge pipeline this PR builds upon by adding goldPerMinute handling.
  • openfrontio/OpenFrontIO#4244: Introduces the packed packedPlayerUpdates channel this PR extends from a 4-field to a 5-field record layout.

Suggested labels

UI/UX, Feature

Suggested reviewers

  • evanpelle

A player counts gold, tick by tick,
Buckets fill up, nice and slick.
Per minute rates now come to light,
Troops and cities join the fight.
Scoreboard glows with income true —
🪙💰 Gold per minute, just for you!

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR sends gold/min server-side, but the moving-average window is 60s, not the requested 120s. Update the income calculation to average over the last 120 seconds and confirm the update cadence matches the issue.
Out of Scope Changes check ⚠️ Warning The PR adds troop and city leaderboard columns plus supporting plumbing, which are not requested by #4074. Limit the change to the gold/min scoreboard column, or split extra stat columns into a separate PR or issue.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and matches the main leaderboard stat-column changes.
Description check ✅ Passed The description is on-topic and describes the leaderboard gold/min work.

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.

@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/client/hud/layers/Leaderboard.ts (1)

86-130: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache city count once, like maxTroops, instead of recomputing it repeatedly.

maxTroops is computed once per player up front and reused during sort and row-building. totalUnitLevels(UnitType.City) isn't cached the same way — it runs inside the sort comparator (once per comparison when sorted by cities) and again in the entry map for every player, every tick. Same idea, just not applied consistently.

♻️ Suggested fix: precompute cities alongside maxTroops
     interface PlayerViewTroopsCache {
       pv: PlayerView;
       maxTroops: number;
+      cities: number;
     }

     const compare = (a: number, b: number) =>
       this._sortOrder === "asc" ? a - b : b - a;

     const maxTroops = (p: PlayerView) => this.game!.config().maxTroops(p);

     const sorted: PlayerViewTroopsCache[] = this.game
       .playerViews()
       .filter((p) => p.isAlive())
-      .map((p) => ({ pv: p, maxTroops: maxTroops(p) }));
+      .map((p) => ({
+        pv: p,
+        maxTroops: maxTroops(p),
+        cities: p.totalUnitLevels(UnitType.City),
+      }));

     switch (this._sortKey) {
       ...
       case "cities":
-        sorted.sort((a, b) =>
-          compare(
-            a.pv.totalUnitLevels(UnitType.City),
-            b.pv.totalUnitLevels(UnitType.City),
-          ),
-        );
+        sorted.sort((a, b) => compare(a.cities, b.cities));
         break;

Then reuse playerCache.cities when building each Entry (line ~150), same as maxTroops is reused today.

Also applies to: 137-157, 171-190

🤖 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/client/hud/layers/Leaderboard.ts` around lines 86 - 130, Cache each
player’s city count once in the Leaderboard update flow, the same way
`maxTroops` is already cached in `PlayerViewTroopsCache`. Add a `cities` field
to the per-player cache built from `this.game.playerViews()` and compute it from
`totalUnitLevels(UnitType.City)` there, then reuse `playerCache.cities` both in
the `"cities"` branch of the sort and when building each leaderboard entry. Keep
the change localized around the `compare` helper, the `sorted` cache setup, and
the entry-building logic so `totalUnitLevels(UnitType.City)` is not recomputed
repeatedly.
🤖 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/client/hud/layers/Leaderboard.ts`:
- Around line 86-130: Cache each player’s city count once in the Leaderboard
update flow, the same way `maxTroops` is already cached in
`PlayerViewTroopsCache`. Add a `cities` field to the per-player cache built from
`this.game.playerViews()` and compute it from `totalUnitLevels(UnitType.City)`
there, then reuse `playerCache.cities` both in the `"cities"` branch of the sort
and when building each leaderboard entry. Keep the change localized around the
`compare` helper, the `sorted` cache setup, and the entry-building logic so
`totalUnitLevels(UnitType.City)` is not recomputed repeatedly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ab4091e-8319-4943-a69d-49f63cce1864

📥 Commits

Reviewing files that changed from the base of the PR and between 3833bfb and 8ccbd97.

📒 Files selected for processing (17)
  • resources/lang/en.json
  • src/client/hud/layers/Leaderboard.ts
  • src/client/render/types/Renderer.ts
  • src/client/view/GameView.ts
  • src/client/view/PlayerView.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
  • tests/GameUpdateUtils.test.ts
  • tests/PackedPlayerUpdates.test.ts
  • tests/PlayerUpdateDiff.test.ts
  • tests/client/render/frame/derive/nuke-telegraphs.test.ts
  • tests/client/render/frame/derive/player-status.test.ts
  • tests/client/view/GameView.test.ts
  • tests/util/viewStubs.ts

@jsshap

jsshap commented Jul 1, 2026

Copy link
Copy Markdown
Author

@ryanbarlow97

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

Add income (as gold per minute) to the scoreboard

1 participant