Leaderboard and Ladder
MediumAI-assisted coding · AI-Assisted Feature Work
Leaderboard and Ladder
You've joined the team behind the ranked mode of a casual online game. The leaderboard library keeps each player's best score and ranks players. Players have reported ranking oddities, and two features are queued: an Elo ladder for head-to-head matches, and seasons.
Codebase
| File | Purpose |
|---|---|
leaderboard/app.py | run() entrypoint and the operation dispatcher |
leaderboard/service.py | LeaderboardService: validation and the public operations |
leaderboard/scoreboard.py | The score board: best score per player |
leaderboard/ratings.py | Elo ratings (read side done, updates are a stub) |
leaderboard/seasons.py | Season archive (stub) |
leaderboard/ranking.py | Turning an ordered board into ranks |
leaderboard/players.py | Lifetime player records (never reset) |
leaderboard/clock.py | Monotonic clock for timestamps |
leaderboard/models.py, config.py | Records and constants (K factors, season carry) |
tests/ | Visible tests (plain assert functions) |
How your code is run
The grader calls run(operations) from leaderboard/app.py and compares the list of results with the expected one: one result per operation, in order. Errors are strings starting with error:, never exceptions.
A board is a named leaderboard: "score" (default) and, from Part 2, "elo". Players with equal values on a board are *tied* and share a rank: the rank of a player is 1 plus the number of players with a strictly better value (so ranks run 1, 2, 2, 4).
| Operation | Returns |
|---|---|
["submit", player, score, ts] | the player's best score after this submission. Only a strictly higher score replaces the best (and then its time ts becomes the time of the best) |
["score", player] | best score, or null |
["rank", player] / ["rank", player, board] | rank on the board, or null if the player is not on it |
["top", n] / ["top", n, board] | up to n rows {"rank", "player", "score"} ("rating" instead of "score" on the elo board), best first |
["around", player, radius] / [..., board] | the player's row plus up to radius rows above and below, or null if not on the board. Error error: invalid radius unless an integer of at least 0 |
["profile", player] | {"first_seen": ts, "submissions": n, "matches": n} (lifetime, never reset), or null if the player has never done anything |
Score board order: higher score first, then the earlier time of the best score, then player name ascending (plain string comparison). The elo board orders by rating descending, then name ascending.
Validation, in this order, where relevant: error: invalid player (not a non-empty string), error: invalid score (integers of at least 0 only; booleans and floats are invalid), error: invalid timestamp (integer of at least 0), error: time went backwards (the clock is shared by every operation that takes a timestamp and never decreases; equal is fine). rank/around: player, then error: unknown board 'x'. top: error: invalid n (integer of at least 1), then board. Wrong argument count: error: bad arguments for 'name'; unknown name: error: unknown operation 'name'; not a non-empty list starting with a string: error: malformed operation.
Example:
[["submit", "amy", 900, 1], ["submit", "bob", 800, 2], ["submit", "cat", 800, 3], ["top", 3]]returns [900, 800, 800, [{"rank": 1, "player": "amy", "score": 900}, {"rank": 2, "player": "bob", "score": 800}, {"rank": 2, "player": "cat", "score": 800}]].
Part 1 — Rank oddities
Goal: Ranks and orderings on the score board must be consistent.
Players report: "I'm tied for second but the leaderboard says I'm fourth when I search for myself" is fine, but "the player after the tie shows a different rank in rank than on the top list", and "players with identical scores and times appear in a different order every deploy".
Rules
rankandtop(andaround) always agree, and both use the ranking rule above.- Players with the same score are ordered by the time of their best score (earlier first), then by name ascending.
- All other behaviour stays as described.
Example: scores amy 900, bob 800, cat 800, dan 700 give ranks 1, 2, 2, 4, and ["rank", "dan"] is 4.
Part 2 — Elo ladder (LB-61)
Goal: Rate players from head-to-head matches.
["match", a, b, result, ts] with result one of "a" (first player won), "b" (second player won) or "draw". It returns {"a": new_rating_a, "b": new_rating_b}. Players are created on their first match with rating 1000. ["rating", player] returns the rating, or null for a player who has never played a match. The "elo" board accepts rank, top, around.
Rules
- Let
E_a = 1 / (1 + 10 ** ((R_b - R_a) / 400))andE_b = 1 - E_a, withR_a,R_bthe ratings *before* the match. The scoreSof a player is 1 for a win, 0 for a loss, 0.5 for a draw. - New rating =
floor(R + K * (S - E) + 0.5)for each player (round to nearest, halves up). Each player has their own K:K_PROVISIONAL(32) while they have played fewer thanPROVISIONAL_GAMES(10) matches before this one, otherwiseK_ESTABLISHED(16). Both are inconfig.py. - Both players' number of matches goes up by one after every valid match, including draws. A player's
matchesinprofilecounts them too. - Errors, in order:
error: invalid player(a, then b),error: same player,error: invalid result, then the timestamp errors. Nothing changes on an error. - Matches never touch the score board, and submissions never touch ratings.
Example: two fresh players: ["match", "ann", "bea", "a", 10] gives {"a": 1016, "b": 984}. Then ["match", "ann", "cy", "a", 20] gives {"a": 1031, "b": 985}.
Part 3 — Seasons (LB-88)
Goal: Reset the ladder periodically and keep the past available.
| Operation | Returns |
|---|---|
["end_season", ts] | the number of the season that just ended (the first one is 1). Timestamp errors as usual |
["current_season"] | the number of the running season (starts at 1) |
["season_top", season, n] / [..., board] | rows like top, from the finished season. Errors in order: error: unknown season 's' (not a finished season; the argument is printed as given), error: invalid n, error: unknown board 'x' |
["season_rank", season, player] / [..., board] | the player's rank in the finished season, or null. Errors: unknown season, invalid player, unknown board |
["player_history", player] | list, one entry per finished season (ascending) in which the player was on at least one board: {"season", "score", "score_rank", "rating", "rating_rank"} with null for a board they were not on |
Rules
end_seasonfreezes the current score board and elo board exactly as they are, including ranks and ties. Later play never changes a finished season.- Then the score board is emptied. Ratings are kept but pulled towards the start:
new = floor(1000 + (R - 1000) * SEASON_CARRY + 0.5)(SEASON_CARRYis 0.5 inconfig.py, halves round up), and every player's match count restarts at 0, so everyone is provisional again. profileis lifetime data and is not reset.- A season may be empty.
end_seasontakes part in the clock: itstsmust not be earlier than the last timestamp seen. - Everything from Parts 1 and 2 behaves as before inside a new season.
Example: scores amy 900, bob 700, cat 900; ["end_season", 4] is 1; then ["top", 3] is [] and ["season_top", 1, 3] lists amy and cat at rank 1 and bob at rank 3.
Notes
clock.py,players.pyandapp.pyhave been in production for a while; so has the read side ofratings.py.- You may use the AI assistant freely. Later parts must not break earlier ones; the visible tests are a starting point, not the full suite.
- Completing all three parts is a strong result.
Example 1
- Input:
- Scores: amy 900, bob 800, cat 800, dan 700; ["rank", "dan"] and ["top", 4]
- Output:
- 4, and ranks [1, 2, 2, 4]
- Explanation
- Bob and cat tie for second, so the next player is fourth, not third.
Example 2
- Input:
- ["match", "ann", "bea", "a", 10], then ["match", "ann", "cy", "a", 20]
- Output:
- {"a": 1016, "b": 984}, then {"a": 1031, "b": 985}
- Explanation
- Equal ratings move by 16 each (K=32). In the second match ann is the slight favourite, so she gains less than 16.
Example 3
- Input:
- Scores amy 900, bob 700, cat 900; ["end_season", 4]; ["top", 3]; ["season_top", 1, 3]
- Output:
- 1, [], and the frozen rows amy 900 (rank 1), cat 900 (rank 1), bob 700 (rank 3)
- Explanation
- Ending the season returns its number, wipes the live score board, and keeps the finished board available.
Guidelines
- Python 3 standard library only
- Scores are non-negative integers; timestamps are integer seconds and never decrease across all operations
- Ranks use standard competition ranking: tied players share a rank and the next rank is skipped (1, 2, 2, 4)
- Ratings are integers; fractions round half up (not Python's round)
- Every operation returns a value; errors are "error: ..." strings, never exceptions