Skip to content
Back to skills

Summer Leaderboards

ASecurity

Leaderboards in a Summer game: queue ratings from matches, and Game rankings your authority scores.

  • 77 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 16, 2026
ai-agentsrustgoshellgit

Works with

  • claude code
  • cursor
  • cli
  • mcp

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add SummerEngine/summer --skill summer-leaderboards --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Summer Leaderboards?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Summer Leaderboards
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/summerengine-summer-leaderboards-summer/badge)](https://www.skillsdirectory.com/skills/summerengine-summer-leaderboards-summer)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: summer-leaderboards
description: "Leaderboards in a Summer game: queue ratings from matches, and Game rankings your authority scores."
license: MIT
compatibility: [Cursor, Claude Code, Windsurf, Codex]
category: multiplayer-and-networking
user-invocable: true
allowed-tools: Read Grep Edit Write summer_read_file summer_replace_text summer_write_file summer_get_script_errors summer_play summer_stop summer_get_diagnostics
paths: ["summer.build.json", "client/**/*.gd", "authority/**/*.gd"]
---

# /summer-leaderboards — ratings, rankings and the top 50

## 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
"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
## 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
## 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
## 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
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

<!-- shell-commands: generated from the operation catalog by npm run generate:registry; do not edit by hand -->
## 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@4.3.0` 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` |
<!-- /shell-commands -->

Files in this skill

  • SKILL.md11.8 KB
  • resource.yaml897 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.