> ## 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 matchmaking skill for AI agents

> Get players into matches with Summer matchmaking: queues, teams, accept prompts, join progress and failures. A Summer Engine skill for AI agents.

`summer-matchmaking` 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. Get players into matches with Summer matchmaking: queues, teams, accept prompts, join progress and failures.

| | |
| - | - |
| Status | Preview: not yet exercised in-engine by the Summer team |
| Domains | Multiplayer, Ux |
| 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 matchmaking, a lobby, a play button, quick play, ranked or casual modes in a multiplayer Summer game
* Declaring queues, player counts, teams (2v2, 5v5), rated queues or a ready check in summer.build.json
* Showing search progress, an Accept/Decline prompt, cancel search or join errors to players
* The authority needs to know which team or queue each player is on

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

A **queue** is a game mode players search in: "casual 2–8", "duel 1v1",
"ranked 2v2". You declare queues in `summer.build.json`; Summer matchmaking
groups searching players, reserves a server for them, and every player's
`Summer.client.join(SummerJoinTarget.queue(...))` completes in the same World.
The authority then reads each player's team from the verified Session.

There is no lobby object, invite code or server browser. Friends who want to
play together use a party (`summer-parties`), which enters a queue as one group.

Where it works:

| Piece | Local Play | Hosted |
| - | - | - |
| Joining a queue, progress, failures | Yes: players are placed at once | Yes |
| Teams (`get_participant(session).team`) | Yes: round-robin in roster order | Yes: balanced by rating, parties kept together |
| Accept/Decline prompt | No: no proposal happens | Yes, when the queue declares `acceptance` |
| Rating-based grouping | No | Yes |

This skill extends the Courtyard game from `multiplayer-project` and
`multiplayer-state`. Its client already joins `Game.QUEUE` and sends a
`join` Command; its authority publishes `match_doc`.

## Steps

### 1. Declare the queues

Each queue in `runtime.queues` of `summer.build.json` names the
WorldDefinition its matches run. A casual mode and a 1v1 rated duel:

```json theme={null}
"queues": [
  { "name": "courtyard", "minPlayers": 2, "maxPlayers": 8, "worldDefinition": "courtyard", "policy": "fifo", "transport": "enet" },
  {
    "name": "duel", "minPlayers": 2, "maxPlayers": 2, "worldDefinition": "courtyard",
    "policy": "rating", "ratingSystemRef": "elo_v1",
    "teams": { "count": 2, "size": 1 },
    "acceptance": { "mode": "all", "ttl": "20s" },
    "searchTtl": { "min": "30s", "default": "2m", "max": "5m" },
    "transport": "enet"
  }
]
```

The fields Summer accepts, and their rules:

| Field | Meaning | Rules |
| - | - | - |
| `name` | The id clients join | Unique; ASCII letters, digits, `_ . : -` |
| `minPlayers` / `maxPlayers` | Players per match | `minPlayers` ≥ 1, `maxPlayers` ≥ `minPlayers`. Measured support is up to 16 per match |
| `worldDefinition` | The WorldDefinition's `definition_id` | Required; every WorldDefinition must be used by a queue |
| `policy` | `fifo` (first come) or `rating` (group by skill) | `rating` needs `ratingSystemRef`; `fifo` must not declare one |
| `ratingSystemRef` | `elo_v1` (Summer rates two sides) or `authority_v1` (your authority reports rating moves) | See `summer-leaderboards` |
| `teams` | `{count, size}`: fixed sides, such as 2v2 = `{2, 2}` | `count` 2..64, `size` 1..16, `minPlayers` = `maxPlayers` = count × size; `elo_v1` needs exactly 2 teams |
| `acceptance` | `{"mode": "all", "ttl": "20s"}` asks every player to accept; `{"mode": "none"}` skips it | `ttl` required with `all`, forbidden with `none` |
| `searchTtl` | `{min, default, max}` search lifetime | Go durations (`30s`, `2m`); min ≤ default ≤ max |
| `regions` | Regions this queue may use | Omit to allow all |
| `transport` | `enet` (UDP, default) or `ws` (WebSocket) | |

Don't add `lobby` fields or `backfill`: Summer rejects both when you publish.
Teams and acceptance need `runtime.interfaces.multiplayer` 5 or higher; the
project skill's `summer.build.json` declares 6.

### 2. Search, with progress the player can see

Add a status label and two hidden buttons to the client scene,
`client/main.tscn` (position them in the editor as you like):

```
[node name="Hud" type="CanvasLayer" parent="."]

[node name="Status" type="Label" parent="Hud"]

[node name="Accept" type="Button" parent="Hud"]
visible = false
text = "Accept"

[node name="Decline" type="Button" parent="Hud"]
visible = false
text = "Decline"

[connection signal="pressed" from="Hud/Accept" to="." method="_on_accept_pressed"]
[connection signal="pressed" from="Hud/Decline" to="." method="_on_decline_pressed"]
```

In `client/main.gd`, replace the join in `_ready()`, from `var join :=`
through the `if not joined.ok:` block, with a call to `find_match`:

```gdscript theme={null}
	var joined := await find_match(Game.QUEUE)
	if not joined.ok:
		return
```

and add the matchmaking functions:

```gdscript theme={null}
var proposal: SummerMatchmakingProposal   # a found match waiting for Accept/Decline

## Searches `queue` until the player is in a match, showing progress.
func find_match(queue: StringName) -> SummerResult:
	var join := Summer.client.join(SummerJoinTarget.queue(queue))
	join.progress_changed.connect(_on_progress)
	join.acceptance_required.connect(_on_match_found)
	var joined: SummerResult = await join.get_result_or_completed_signal()
	proposal = null
	$Hud/Accept.hide()
	$Hud/Decline.hide()
	show_status("" if joined.ok else join_failure_text(joined))
	return joined

## Stages carry no tickets or addresses, so they are safe to show.
func _on_progress(progress: SummerJoinProgress) -> void:
	match progress.stage:
		SummerJoinProgress.STAGE_RESOLVING:
			show_status("Searching for players… %ds" % (progress.elapsed_msec / 1000))
		SummerJoinProgress.STAGE_RESERVED, SummerJoinProgress.STAGE_PROPOSED:
			show_status("Match found")
		SummerJoinProgress.STAGE_LAUNCHING, SummerJoinProgress.STAGE_CONNECTING, SummerJoinProgress.STAGE_ADMITTING:
			show_status("Joining the match…")
		SummerJoinProgress.STAGE_RECONNECTING:
			show_status("Reconnecting…")

func show_status(text: String) -> void:
	$Hud/Status.text = text
```

### 3. Accept or decline a found match

With `"acceptance": {"mode": "all"}`, Summer reserves a server first, then asks
every player. The match starts only when all accept before the deadline. The
engine never decides for the player: connect `acceptance_required`, or the
join waits until the proposal expires.

```gdscript theme={null}
## Summer reserved a server for these players. The match starts only when
## everyone accepts before the deadline.
func _on_match_found(found: SummerMatchmakingProposal) -> void:
	proposal = found
	var seconds_left := maxi(0, (found.accept_by_unix_msec - int(Time.get_unix_time_from_system() * 1000.0)) / 1000)
	show_status("Match found! Accept within %ds" % seconds_left)
	$Hud/Accept.show()
	$Hud/Decline.show()

func _on_accept_pressed() -> void: _decide(true)
func _on_decline_pressed() -> void: _decide(false)

func _decide(accept: bool) -> void:
	if proposal == null or proposal.is_decided():
		return
	var decided: SummerResult = proposal.accept() if accept else proposal.decline()
	if not decided.ok:
		show_status(join_failure_text(decided))
	$Hud/Accept.hide()
	$Hud/Decline.hide()
```

After an accept, `find_match` keeps waiting until everyone accepted. A
decline ends it with `proposal_declined`. A game that always accepts (one
"Play" button) connects `acceptance_required` straight to `proposal.accept()`.

### 4. Tell players what went wrong

Every failure is a `SummerResult` with a stable `code` and `retryable`:

```gdscript theme={null}
## Turns a typed failure into words for the player. `retryable` says whether
## offering "Try again" makes sense.
func join_failure_text(result: SummerResult) -> String:
	match String(result.code):
		"proposal_declined":
			return "You declined the match."
		"proposal_expired":
			return "The match wasn't accepted in time."
		"queue_not_found", "queue_closed":
			return "This mode isn't available right now."
		"capacity_exhausted", "world_full", "world_start_timeout", "region_unavailable":
			return "Servers are busy. Try again in a moment."
		"rate_limited":
			return "Too many tries. Wait a moment."
		"cancelled":
			return ""
	return "Couldn't join. Try again." if result.retryable else "Couldn't join: %s" % result.message
```

Other codes: `proposal_stale` and `proposal_already_decided` (a second
decision), `world_waking` (a persistent World is restoring), `qos_stale`,
`platform_error`, and `unavailable` (Local Play hosts a different queue).

### 5. Cancel the search, or leave

`Summer.client.leave()` cancels a search in flight, or leaves the current
match. Await it before quitting the game, so the platform knows the player is
gone and doesn't hold a seat for them:

```gdscript theme={null}
## Stops searching, or leaves the current match. Await it before quitting.
func leave_match() -> void:
	var left: SummerResult = await Summer.client.leave().get_result_or_completed_signal()
	if not left.ok:
		push_warning("leave: %s" % left.code)
```

To offer "Play online" only when it can work, read
`Summer.client.matchmaking.availability.hosted_sessions_available` (and
`hosted_unavailable_reason`) before showing the button.

### 6. Read teams and the queue on the authority

The authority learns each player's team from the verified Session. In
`authority/main.gd`, give `match_doc` a `"teams": {}` entry and add this to
`_join` just before `request.accept(...)`:

```gdscript theme={null}
	# The team matchmaking seated this player on: 0..count-1, or -1 without teams.
	var participant := Summer.authority.match.get_participant(session)
	var team := participant.team if participant != null else -1
	match_doc.teams[session.player.display_name] = team
	match_group.reset(Game.pack(match_doc))
```

Players learn teams only from what the authority publishes, here the shared
match document. `Summer.get_world().queue` on the authority names the queue
this match came from, so one authority scene can serve several modes.

### 7. Test it with Local Play

Local Play stands in for one queue at a time: the first one declared, or the
one you name. In the editor and with `summer_play`, name it with the
`summer/local_play/queue` project setting:

```
summer_project_setting key="summer/local_play/queue" value="duel"
summer_project_setting key="summer/local_play/players" value=2
summer_play
summer_get_diagnostics
summer_stop
```

A terminal run ignores that setting; pass the queue instead:
`<summer> --path . --summer-local-play 2 --summer-local-play-queue duel --summer-local-play-smoke`.

* The client must join the queue Local Play stands in for. Another queue
  fails with `unavailable`: "Local Play is hosting queue 'courtyard'. Start
  Local Play with queue 'duel' to test that queue."
* Players are placed at once: progress goes straight to `STAGE_CONNECTED`,
  with no search and no Accept/Decline.
* A team queue seats players round-robin in roster order (player-1 on team 0,
  player-2 on team 1, and so on), whatever order they connect in.
* `Summer.client.matchmaking.availability` reports hosted sessions as
  available.

Test the accept prompt and rating-based grouping on a hosted staging build.

## Checklist

* [ ] Every queue names a WorldDefinition; teams queues have `minPlayers` = `maxPlayers` = count × size.
* [ ] The client shows search progress and a readable message for each failure code.
* [ ] Queues with `acceptance` show Accept/Decline with the deadline; nothing auto-accepts unless the game means to.
* [ ] Cancel and quit both await `Summer.client.leave()`.
* [ ] The authority reads `get_participant(session).team` and publishes teams to players.
* [ ] Local Play with `summer/local_play/queue` set runs each queue you declared.

## Common mistakes

| Don't | Do | Why |
| - | - | - |
| Build a lobby, invite codes or join-by-IP | Queues, plus parties for friends | Summer has no lobby object; invite, follow, portal and resume joins return `unsupported_placement` |
| Ignore `acceptance_required` on a queue with `acceptance` | Show Accept/Decline, or connect it to `proposal.accept()` | The engine never accepts for the player; the join waits until `proposal_expired` |
| `teams` with `minPlayers` ≠ count × size | Equal sizes: 2v2 is 4 and 4 | Summer rejects the queue when you publish |
| Read the team on the client from matchmaking | Publish it from the authority | Clients only learn teams through the game's own state |
| Join `courtyard` while Local Play stands in for `duel` | Set `summer/local_play/queue` to the queue the client joins | Local Play hosts one queue per run; the other fails `unavailable` |
| Quit right after "Cancel" | Await `Summer.client.leave()` first | The search may keep holding a ticket for the player |
| Show raw `result.message` for known codes | Map codes to your own words | Codes are stable; messages are diagnostics |

## See also

* `multiplayer` — how Summer multiplayer fits together
* `summer-match-results` — ending a match and reporting who won
* `summer-leaderboards` — ratings and leaderboards for rated queues
* `summer-parties` — friends entering a queue together
* `multiplayer-testing` — Local Play options and bots

## 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_project_setting` | `summer engine project-setting` |
| `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.
* [`multiplayer-project`](/skills/multiplayer-project): Set up a Summer multiplayer project: entry scenes, World and queue files, joining, a first Local Play.
* [`multiplayer-testing`](/skills/multiplayer-testing): Test a Summer multiplayer game with Local Play: several players, bad network, bots, a passing run.
* [`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-leaderboards`](/skills/summer-leaderboards): Leaderboards in a Summer game: queue ratings from matches, and Game rankings your authority scores.
* [`summer-parties`](/skills/summer-parties): Let a Summer party play together: the leader presses Play, members follow into the same match. Hosted only.

***

Source: [`library/skills/summer-matchmaking/SKILL.md`](https://github.com/SummerEngine/summer/blob/main/library/skills/summer-matchmaking/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.