Skip to content

Cap each player's rows on the global list, scaled to the field - #71

Merged
IlianP merged 1 commit into
mainfrom
claude/board-generation-patterns-pv17tf
Sep 23, 2026
Merged

IlianP merged 1 commit into
mainfrom
claude/board-generation-patterns-pv17tf

Conversation

@IlianP

@IlianP IlianP commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Completes the consolidation idea from #63. player_rank fixed the sentence ("Platz 3 von 3 Spielern"), but the list still let one enthusiast fill it — measured on 8×8 hard: 83 entries from three names, one of them on 34 of the first 50 places.

The rule

top_scores now shows each player at most

per_player = ceil(p_limit / players in the slice), at least 1

of their best rows.

players per player (50 places)
1 50 (no cap)
3 17
10 5
25 2
50+ 1

A fixed cap is right for exactly one population size; one row per player would take 20 of 22 buckets down to 1–3 rows. Deriving the number keeps the list full while few play and converges on one row per player by itself.

What it does and doesn't touch

  • Display only. Nothing is deleted. submit_score's rank and player_rank still count every row, and the player's own local list keeps them all.
  • Same player key as player_rank: anonymous rows are each their own player, so they are never merged and never capped; case/whitespace variants of a name are one player.
  • The window counts its own players, so the 90-day tab isn't throttled by people who only played last year.
  • It says so. The server returns per_player and hidden; the client adds one muted line after the rows when the cap held something back („Höchstens 17 Einträge pro Spieler · 17 weitere ausgeblendet"), so a regular doesn't think entries were lost. New key score.capNote in all six packs.
  • An un-migrated server sends neither column and reads as "no cap".

Testing

Against a throwaway PostgreSQL 16:

  • new tests/sql/top-scores-cap.sql: the measured shape, name variants, anonymous rows, 60 players → one each, a lone player uncapped, the window's own cap, best rows shown, nothing deleted. Shown to fail with the cap removed (5 checks).
  • rank-order.sql, player-rank.sql, play-stats.sql still green.
  • The rollout itself: old (current live) schema with data → the workflow's BEGIN…ROLLBACK probe → --single-transaction apply → all rows intact, anon's execute grant intact, cap applied (2 players → 25 each), second run a no-op.

Client:

  • tests/logic/ 16/16 green; leaderboard-period.mjs extended for the metadata (including "absent ≠ 0").
  • new tests/browser/leaderboard-cap.mjs: note present/absent, after the rows, highlight and numbering untouched, fits 320 px in fr/ru/pt. Shown to fail with the note removed.
  • win-feedback, leaderboard-period, player-rank, leaderboard-retry, i18n-layout, board-size-hint green.

Rollout

The SQL change is applied automatically by SQL ausrollen on merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_019bNvb3PKt2h1XNZD5qGX7G


Generated by Claude Code

player_rank fixed the sentence under the list, but the list itself still let
one enthusiast fill it: measured on 8x8 hard, 34 of the first 50 places
belonged to one of three players, so a newcomer landed far down between one
person's repeats.

top_scores now shows each player at most ceil(p_limit / players) of their best
rows — 17 each for three players on 50 places, 5 for ten, one row each from 50
players up. A fixed cap is right for exactly one population size; one row per
player would take 20 of 22 buckets down to 1–3 rows. Deriving the number keeps
the list full while few play and converges on one row per player by itself.

It is display only: nothing is deleted, submit_score's rank and player_rank
still count every row, and the player's own local list keeps them all. The
player key is player_rank's (anonymous rows each their own player, so never
merged or capped), and the p_since window counts its own players.

The server also returns per_player and hidden; fetchTopScores lifts them onto
the list, and renderScoreList adds one muted line after the rows when the cap
held something back — otherwise a regular watches half their entries vanish
and assumes they were lost. An un-migrated server sends neither and reads as
"no cap".

Verified against a throwaway PostgreSQL 16: tests/sql/top-scores-cap.sql
(measured shape, name variants, anonymous rows, 60 players, a lone player, the
window's own cap, best rows shown, nothing deleted), shown to fail with the cap
removed; the three existing SQL suites; and the rollout itself — old schema,
the workflow's rollback probe, --single-transaction apply, rows intact, anon's
execute grant intact, second run a no-op. The browser test for the note was
also shown to fail with the note removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bNvb3PKt2h1XNZD5qGX7G
@IlianP
IlianP merged commit e6bd1dd into main Sep 23, 2026
1 check passed
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.

2 participants