> ## Documentation Index
> Fetch the complete documentation index at: https://docs.summerengine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Summer leaderboards skill for AI agents

> Leaderboards in a Summer game: queue ratings from matches, and Game rankings your authority scores. A Summer Engine skill for AI agents.

`summer-leaderboards` is a skill from the [Summer skill library](/mcp/skills): a guide your AI agent reads when the task matches, so it builds the good version on the first try. Leaderboards in a Summer game: queue ratings from matches, and Game rankings your authority scores.

| | |
| - | - |
| Status | Preview: not yet exercised in-engine by the Summer team |
| Domains | Multiplayer, UI |
| Version | `1.0.0` |
| Engine version | `>=4.6` |
| Recommended set | No, installed by `--all` or by name |

## When your agent uses it

* The user wants a leaderboard, high scores, ranks, MMR, Elo or a 'top 50' screen in a Summer game
* A rated queue should show each player's rating, standing or percentile
* The authority should write a score, such as total coins or best time, to a Game-wide ranking
* A co-op mode should rate players against the content with authority\_v1 rating deltas

## Install

`summer setup` installs every skill for your agent. To add only this one:

```bash theme={null}
npx -y summer-engine@latest skills install summer-leaderboards --agent claude-code
```

Swap `claude-code` for your agent, such as `codex` or `cursor`. See [Skills](/mcp/skills#quick-start) for every option.

## The skill

## Overview

Summer keeps two kinds of score board. Pick by where the number comes from:

| Board | The number is | Declared as | Written by | Read with |
| - | - | - | - | - |
| **Queue rating** | A skill rating (Elo, MMR) that moves with every match in a rated queue | A queue with `"policy": "rating"` | Summer, when the authority concludes a match | `Summer.client.ratings`, `Summer.authority.ratings` |
| **Game ranking** | Any score your game defines: total coins, best time, levels cleared | `runtime.rankings` | Your authority, with compare-and-set | `Summer.client.rankings`, `Summer.authority.rankings` |

Players only ever read their **own** rating and score, plus the public top 50,
which shows display names, never account ids. Nobody can write a score from a
client.

Where it works: leaderboards, standings and rankings are read through the
Summer app that hosts the player, so they work in **hosted** games only.
Under Local Play they fail with a typed code (step 5); the authority's
`ratings.load` returns a deterministic test value. Build the screens so they
degrade gracefully, and check the real numbers on a hosted staging build.

This skill extends the Courtyard game: its authority concludes matches as in
`summer-match-results`.

## Steps

### 1. Declare the boards

In `summer.build.json`, a rated queue gets a rating, and `runtime.rankings`
declares Game rankings:

```json theme={null}
"queues": [
  {
    "name": "duel",
    "minPlayers": 2,
    "maxPlayers": 2,
    "worldDefinition": "courtyard",
    "policy": "rating",
    "ratingSystemRef": "elo_v1",
    "teams": { "count": 2, "size": 1 },
    "transport": "enet"
  }
],
"rankings": [
  { "name": "coins", "initialRating": 0 }
]
```

* `ratingSystemRef`: `elo_v1` lets Summer compute rating moves from win, loss
  and draw. `authority_v1` makes your authority report each player's move
  (step 4). Optional `initialRating` is a newcomer's rating (default 1200).
* A ranking `name` is lower-case letters and digits separated by single `.`,
  `_` or `-`, at most 64 characters; at most 32 rankings. `initialRating`
  defaults to 0.
* To make a rated queue and a ranking one board, give the queue
  `"rankingRef": "<ranking name>"` and drop the queue's `initialRating`. A
  queue and a ranking may not share a name without `rankingRef`.

### 2. Show a queue's top 50 and the player's standing

On a client, after Summer is initialized (no match needed):

```gdscript theme={null}
## Fills a leaderboard screen for a rated queue. Hosted games only.
func show_queue_board(queue: StringName) -> void:
	var board := Summer.client.ratings.leaderboard(queue)
	var read: SummerResult = await board.get_result_or_completed_signal()
	if not read.ok:
		show_board_unavailable(read)
		return
	for entry: SummerLeaderboardEntry in board.entries:
		add_board_row(entry.rank, entry.display_name, entry.value, entry.you)
	var standing := Summer.client.ratings.standing(queue)
	if (await standing.get_result_or_completed_signal()).ok:
		if standing.rank > 0:
			show_place("#%d of %d" % [standing.rank, standing.players])
		elif standing.rated:
			show_place("Top %.1f%%" % (100.0 - standing.percentile))
		else:
			show_place("Play a rated match to get a rank")
```

* `entries` holds up to 50 rows, best first; `you` marks the player's own row.
* `standing.rank` is set only inside the top 1000; otherwise show the
  percentile. Before the first rated match `rated` is false and `value` is the
  queue's `initialRating`.
* `Summer.client.ratings.get_current(queue)` returns the last known
  `SummerRatingSnapshot` (`value`, `games`, `version`) without a network read,
  or `null`. `refresh(queue)` reads it again; `rating_changed(queue)` fires
  when it moves.
* One read of each kind is in flight at a time; calling again while one is
  pending returns the same operation.

After a match, `Summer.client.ratings.rating_committed` delivers the
player's own before-and-after rating (see `summer-match-results`).

### 3. Write a Game ranking from the authority

Only the authority writes rankings, by the player's stable id, with
**compare-and-set**: read the score and its version, write the new absolute
value against that version, and read again if someone else wrote first. In
`authority/main.gd`, after the match concluded:

```gdscript theme={null}
## Adds this match's coins to each player's Game ranking "coins".
## Compare-and-set: read the version, write against it, re-read on conflict.
func _record_coins() -> void:
	for session_id: String in wallets:
		var player: SummerPlayer = sessions[session_id].player
		await _add_score(&"coins", player.user_id, int(wallets[session_id]))

func _add_score(ranking: StringName, player_id: String, points: int) -> void:
	for attempt in 3:
		var before := Summer.authority.rankings.get_score(ranking, player_id)
		var read: SummerResult = await before.get_result_or_completed_signal()
		if not read.ok:
			return   # hosted only: Local Play has no ranking service
		var write := Summer.authority.rankings.set_score(ranking, player_id, before.rating.version, before.rating.value + points, before.rating.games + 1)
		var wrote: SummerResult = await write.get_result_or_completed_signal()
		if wrote.ok or wrote.code != &"ranking_version_conflict":
			return
		# Someone else wrote first: read again and recompute.
```

`sessions` maps each `session_id` to the `SummerSession` the authority keeps
in `_on_session_joined` (`summer-match-results`, step 1). The engine never
retries a write. After a failure other than a conflict, read the score before
deciding to write again: the first write may have committed.

Clients read their own ranking with `Summer.client.rankings.get_score("coins")`,
`standing("coins")` (full rank and player count; rank 0 before the first
score) and `leaderboard("coins")` (top 50, zero-score players included). These
work in a standalone game too, before any match.

### 4. Own a co-op queue's rating (`authority_v1`)

Elo rates two sides against each other. A co-op mode, where a group plays
against the content, declares `"ratingSystemRef": "authority_v1"` and reports
every player's move when the match concludes:

```gdscript theme={null}
## A co-op run against content rated `content_rating`; `cleared` says whether
## the group made it. Moves each player's rating like Elo against the content.
func conclude_run(players: Array[SummerSession], content_rating: int, cleared: bool) -> void:
	var outcomes: Dictionary[SummerSession, SummerAuthorityMatch.MatchOutcome] = {}
	var deltas: Dictionary[SummerSession, int] = {}
	for session in players:
		var read := Summer.authority.ratings.load(session)
		if not (await read.get_result_or_completed_signal()).ok:
			return   # rating_unavailable: this queue keeps no rating
		var expected := 1.0 / (1.0 + pow(10.0, (content_rating - read.rating.value) / 400.0))
		deltas[session] = roundi(32 * ((1.0 if cleared else 0.0) - expected))
		outcomes[session] = SummerAuthorityMatch.MATCH_OUTCOME_WIN if cleared else SummerAuthorityMatch.MATCH_OUTCOME_LOSS
	var concluded: SummerResult = await Summer.authority.match.conclude(outcomes, "run_complete", deltas).get_result_or_completed_signal()
```

* Each delta is an `int` within ±1000, one for **every** participant.
  Deltas on a non-`authority_v1` queue, or a missing one, fail the
  conclusion with `invalid_request` and commit nothing; conclude again with
  corrected arguments.
* `Summer.authority.ratings.load(session)` is for the authority's own logic.
  Never show one player's rating to other players; the leaderboard is the
  public view.
* `Summer.authority.ratings.leaderboard("dungeon")` reads the same top 50,
  unmarked, for an in-World trophy wall.

### 5. Degrade gracefully where boards aren't available

```gdscript theme={null}
func show_board_unavailable(result: SummerResult) -> void:
	match String(result.code):
		"rating_unavailable":
			show_place("This mode has no ranking")
		_:
			show_place("Leaderboard unavailable right now" if result.retryable else "Leaderboards appear in the Summer app")
```

Under Local Play these calls fail without a Summer account behind them:

| Call | Under Local Play |
| - | - |
| `Summer.client.ratings.leaderboard`, `standing`, `refresh` | Fails with `unavailable` (retryable) |
| `Summer.client.ratings.get_current` | `null` |
| `Summer.client.rankings.get_score`, `standing`, `leaderboard` | Fails with `unavailable` (not retryable) |
| `Summer.authority.rankings.get_score`, `set_score` | Fails with `service_error` |
| `Summer.authority.ratings.load` | Works: a deterministic test rating (system `deterministic-v1`) |
| `Summer.authority.ratings.leaderboard` | Works: an empty list |
| `Summer.authority.match.conclude` | Works: a deterministic settlement with test rating changes |

Hide or grey out leaderboard buttons when a read fails; never block the game
on them.

## Checklist

* [ ] Every board is declared: rated queues with `ratingSystemRef`, rankings under `runtime.rankings`.
* [ ] The leaderboard screen marks the player's own row and shows rank or percentile.
* [ ] Ranking writes happen only on the authority, by `player.user_id`, with compare-and-set and a conflict re-read.
* [ ] `authority_v1` queues conclude with a delta for every participant; other queues send none.
* [ ] Every read handles failure, and the game stays playable without boards.
* [ ] Real numbers checked on a hosted staging build.

## Common mistakes

| Don't | Do | Why |
| - | - | - |
| Write scores from the client | `Summer.authority.rankings.set_score` | Clients can only read their own score; the authority is trusted |
| `set_score` without the version you read | Read with `get_score`, pass `before.rating.version` | It is compare-and-set; a stale version fails `ranking_version_conflict` |
| Blindly retry a failed write | Read again, then decide | The first write may have committed; the engine doesn't replay it |
| Send rating deltas on an `elo_v1` or `fifo` queue | Deltas only on `authority_v1`, one per participant | Otherwise the conclusion fails `invalid_request` |
| Show other players' ratings from `authority.ratings.load` | Show the leaderboard | Ratings are private to each player |
| Name a ranking like a queue without `rankingRef` | Distinct names, or `rankingRef` to share one board | Summer rejects the manifest |
| Treat a Local Play failure as a bug | Expect `unavailable` or `service_error` locally; test boards hosted | Boards are read through the hosting Summer app |

## See also

* `summer-match-results` — concluding matches, which moves ratings
* `summer-matchmaking` — declaring rated queues and teams
* `summer-player-data` — saving per-player progress that isn't a ranking

## From a shell

No Summer MCP tools in this session? Run each one as a command; when `summer --version` is missing or below 4.0, use `npx -y summer-engine@latest` in place of `summer`. Fields are flags in kebab-case (`--scene-path res://main.tscn`) or one `--input '<json>'`, and `summer ops describe <op>` prints the exact input. The `summer-cli` skill has the rest.

| In this skill | Command |
| - | - |
| `summer_get_diagnostics` | `summer engine get-diagnostics` |
| `summer_get_script_errors` | `summer engine get-script-errors` |
| `summer_play` | `summer engine play` |
| `summer_read_file` | `summer engine read-file` |
| `summer_replace_text` | `summer engine replace-text` |
| `summer_stop` | `summer engine stop` |
| `summer_write_file` | `summer engine write-file` |

## Related skills

* [`multiplayer`](/skills/multiplayer): Start here for Summer multiplayer: how authority, clients, Worlds and Sessions fit, and what comes next.
* [`summer-matchmaking`](/skills/summer-matchmaking): Get players into matches with Summer matchmaking: queues, teams, accept prompts, join progress and failures.
* [`summer-match-results`](/skills/summer-match-results): End a Summer match: report each player's win, loss or draw, show results on clients, then close the World.
* [`summer-player-data`](/skills/summer-player-data): Save a player's progress so it follows them to every match: authority load and commit, autosave, safe retries.

***

Source: [`library/skills/summer-leaderboards/SKILL.md`](https://github.com/SummerEngine/summer/blob/main/library/skills/summer-leaderboards/SKILL.md), rendered from `summer-engine@4.3.0` on npm.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.