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

# Multiplayer testing skill for AI agents

> Test a Summer multiplayer game with Local Play: several players, bad network, bots, a passing run. A Summer Engine skill for AI agents.

`multiplayer-testing` 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. Test a Summer multiplayer game with Local Play: several players, bad network, bots, a passing run.

| | |
| - | - |
| Status | Stable |
| Domains | Multiplayer, Playtest, Verification |
| Version | `1.0.0` |
| Engine version | `>=4.6` |
| Recommended set | No, installed by `--all` or by name |

## When your agent uses it

* Running or testing a Summer multiplayer game locally with several players (Local Play)
* Testing multiplayer under latency, jitter or packet loss, or with bots
* A multiplayer run says ok but nothing networked works, or deciding whether a multiplayer change is done

## Install

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

```bash theme={null}
npx -y summer-engine@latest skills install multiplayer-testing --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

Step 4 of the multiplayer sequence (`multiplayer` explains the model).

**Local Play** runs a Summer multiplayer game on one machine. It starts the
World's authority scene headless and N clients, and each client joins through
the game's own `Summer.client.join`. No account, server or extra tools are
needed. It can also make the network bad on purpose, which is where
multiplayer bugs show.

## 1. Start Local Play

Pick whichever the session has.

**Summer MCP tools** (they set project settings, then play):

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

**Editor:** **Debug > Local Multiplayer**, pick the player count, press Play.
The client windows tile side by side.

**Terminal** (`<summer>` is the Summer editor executable). Import once after
adding scripts so their classes register:

```sh theme={null}
<summer> --headless --path . --import
<summer> --path . --summer-local-play 2 --summer-local-play-timeout 30
```

| Setting | Terminal flag | Meaning |
| - | - | - |
| `summer/local_play/players` | `--summer-local-play <n>` | Players to start. `-1` uses the queue's `minPlayers`; `0` turns Local Play off |
| `summer/local_play/queue` | `--summer-local-play-queue <name>` | Which `summer.build.json` queue to stand in for (default: the first) |
| — | `--summer-local-play-headless` | Run the clients without windows too |
| — | `--summer-local-play-timeout <s>` | End the session after that many seconds |
| — | `--summer-local-play-smoke` | Headless clients; exit 0 once every client joined and stayed 3 s |
| — | `--summer-local-play-spectators <n>` | Also start spectator clients |
| — | `--summer-local-play-web` | Run the clients in a browser (the game's Web export) |

## 2. Make the network bad

Loopback has no latency, so it hides every timing bug. Always run once with
these settings; they apply to every client:

```
summer_project_setting key="summer/local_play/network/round_trip_msec" value=100
summer_project_setting key="summer/local_play/network/jitter_msec" value=10
summer_project_setting key="summer/local_play/network/loss_percent" value=1
```

Or put them in `project.godot` while testing:

```ini theme={null}
[summer]

local_play/network/round_trip_msec=100
local_play/network/jitter_msec=10
local_play/network/loss_percent=1
```

Ranges: round trip 0–2000 ms, jitter 0–1000 ms, loss 0–50 %. Each client
prints `NETWORK_EMULATION round_trip_ms=100 ...` when it applies. Try 200 ms
too: that is a player on another continent.

## 3. Read the run

Every output line is prefixed by its process: `[authority]`, `[player-1]`,
`[player-2]`. The last line is the summary:

```text theme={null}
SUMMER_LOCAL_PLAY_RESULT {"ok":true,"players":2,"network":{...},"processes":[{"label":"authority","script_errors":0,...},{"label":"player-1","joined":true,"script_errors":0,...}],...}
```

A run passes only when **all** of these hold:

* [ ] `"ok":true`, and every process has `"script_errors":0`.
* [ ] The game's own ready lines appear: the authority's, and one per client after its `join` Command (`COURTYARD_AUTHORITY_READY`, `COURTYARD_CLIENT_JOINED` in the Courtyard example). Print one in your game too.
* [ ] No `[SummerNetworkSpawner] cannot attach` warning on any process.
* [ ] No `SCRIPT ERROR` lines.

`"ok":true` alone only proves that clients got seats. If the composition is
invalid, the Spawner logs `cannot attach` as a warning and nothing networked
works, yet every client still counts as joined. The smoke flag
(`--summer-local-play-smoke`) proves the same thing and no more.

With MCP, `summer_get_diagnostics` reports runtime errors from the debugger.
Read it after every play; `summer_get_console` alone misses them.

## 4. Drive the game with bots

To test gameplay without hands, give the game a bot mode. Arguments after
`--` reach every process, and `{client}` becomes 1, 2, ... for players and 0
for the authority:

```sh theme={null}
<summer> --path . --summer-local-play 2 --summer-local-play-headless --summer-local-play-timeout 20 -- --bot={client}
```

Put the bot in `client/bot.gd`. It presses the same input actions a player
does, so it tests the real controls:

```gdscript theme={null}
extends Node
## Test bot for Local Play, enabled with `-- --bot={client}`. It presses the
## real input actions. Bot 1 walks to the first coin and collects it; every
## bot prints what it sees, for the test run to check.

const COIN := "0"

var number := 0
var main: Node
var elapsed := 0.0
var step := 0

func _process(delta: float) -> void:
	elapsed += delta
	var me := _own_player()
	if me == null:
		return
	if number == 1 and step == 0:
		var at: Array = main.match_doc.get("coins", {}).get(COIN, [])
		if at.is_empty():
			return
		var to_coin := Vector2(at[0] - me.global_position.x, at[2] - me.global_position.z)
		if to_coin.length() > 0.5:
			_steer(to_coin.normalized())
			return
		_steer(Vector2.ZERO)
		step = 1
		print("BOT_MOVED %s" % me.global_position)
		var collected: Dictionary = await main.collect(COIN)
		print("BOT_COLLECT %s" % JSON.stringify(collected))
	if elapsed > 6.0 and step < 2:
		step = 2
		for node in main.get_node("Entities").get_children():
			if node != me:
				print("BOT_SAW %s" % node.global_position)
		print("BOT_SCORES %s" % JSON.stringify(main.match_doc.get("scores", {})))

func _own_player() -> Node3D:
	for node in main.get_node("Entities").get_children():
		if node.get_script() == preload("res://client/player_owner.gd"):
			return node
	return null

func _steer(direction: Vector2) -> void:
	for action in [&"ui_left", &"ui_right", &"ui_up", &"ui_down"]:
		Input.action_release(action)
	if direction.x > 0.0: Input.action_press(&"ui_right", direction.x)
	if direction.x < 0.0: Input.action_press(&"ui_left", -direction.x)
	if direction.y > 0.0: Input.action_press(&"ui_down", direction.y)
	if direction.y < 0.0: Input.action_press(&"ui_up", -direction.y)
```

Add it from `client/main.gd` only when the argument is there, after the
player has joined:

```gdscript theme={null}
	# Local Play test bot: `-- --bot={client}` (see multiplayer-testing).
	for arg in OS.get_cmdline_user_args():
		if arg.begins_with("--bot="):
			var bot := preload("res://client/bot.gd").new()
			bot.number = int(arg.trim_prefix("--bot="))
			bot.main = self
			add_child(bot)
```

Then check the lines it prints. The Courtyard bot walks player 1 to a coin
and collects it; player 2 watches:

* [ ] `[player-1] BOT_MOVED` and `[player-2] BOT_SAW` report the same position (the observer is up to 100 ms behind while moving, then equal).
* [ ] `[player-1] BOT_COLLECT {"ok":true...}` and both players print the new score.

Keep bot code in `client/`: files under `test/` or other debug folders are
left out of the players' pack, so the shipped game could not load them.

Headless clients have no real window. Guard calls that need one (mouse
capture, window size) with `DisplayServer.get_name() != "headless"`.

## 5. What Local Play cannot show

* **Hosted-only services** answer with an error code locally: parties,
  friends, World chat, the store, client leaderboards and analytics. Their
  skills list the codes. Make the UI hide or explain them; never crash on them.
* **Matchmaking's accept prompt.** Local Play seats players directly: no
  proposal, one World per queue.
* **A player leaving for good.** A client that quits or calls
  `Summer.client.leave()` only disconnects. `session_disconnected` fires and
  their seat is held. `session_left` arrives when the World ends; hosted, it
  arrives when Summer ends the Session.
* **Which scene the hosted server starts.** Local Play starts the authority
  scene by name. Summer starts the exported `server.pck` with no scene, so a
  server preset without the `summer_authority` feature still passes here and
  then runs the title screen hosted, where every join fails with
  `world_start_timeout`. Run the bundle's `server.pck` with no scene
  (`multiplayer-publish`, step 4d).
* **A wrong Game ID.** Local Play has no hosted identity, so
  `Summer.initialize("some-id")` passes here and fails hosted with
  `game_identity_mismatch`. Call `Summer.initialize()` with no ID; only a
  real online join after publishing proves it.
* **Real devices, real accounts, real distances.** Publish to staging for that
  (`multiplayer-publish`).

Before a version reaches players, run `release-readiness`: it covers these
gaps on the `.zip` you upload.

## Checklist for any multiplayer change

* [ ] Ran Local Play with at least 2 players, and once with 100 ms / 10 ms / 1 %.
* [ ] Every process: no `SCRIPT ERROR`, no `cannot attach`, `script_errors` 0.
* [ ] The game's own ready lines appear for the authority and every client.
* [ ] Your own player responds instantly and never snaps while moving normally.
* [ ] Other players move smoothly and end where their owners are.
* [ ] Shared state matches on every client; private state reaches only its owner.
* [ ] A refused Command changes nothing and shows its reason.

## Common mistakes

| Don't | Do | Why |
| - | - | - |
| Test only on loopback | Add round trip, jitter and loss | Latency bugs are invisible at 0 ms |
| Read `"ok":true` as success | Also check your ready lines, `cannot attach` and `script_errors` | A broken composition still joins |
| Read only `summer_get_console` | `summer_get_diagnostics` after each play | Runtime errors live in the debugger |
| Put bot or test code under `test/` and load it from the game | Keep the bot in `client/`, enabled by `--bot` | Debug folders are left out of the players' pack |
| Wait for `session_left` in a local test | Watch `session_disconnected` | Local Play ends Sessions only when the World ends |
| Skip the import pass after adding scripts | `<summer> --headless --path . --import` first | New classes are unknown until imported |

## Next

`multiplayer-publish`: export the summer.games bundle and upload it as a Build.

## 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_console` | `summer engine get-console` |
| `summer_get_diagnostics` | `summer engine get-diagnostics` |
| `summer_play` | `summer engine play` |
| `summer_project_setting` | `summer engine project-setting` |
| `summer_screenshot` | `summer engine screenshot` |
| `summer_stop` | `summer engine stop` |

## 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-movement`](/skills/multiplayer-movement): Movement in Summer multiplayer: owners move instantly, others glide, the authority checks each pose.
* [`multiplayer-state`](/skills/multiplayer-state): Authority-owned state in Summer multiplayer: shared and private State groups, Commands, Events.
* [`multiplayer-publish`](/skills/multiplayer-publish): Ship a hosted Summer multiplayer game: the bundle, server preset, pre-upload checks, a new Build.
* [`release-readiness`](/skills/release-readiness): Before releasing a Summer game version, prove players can play it: server, online join, progress, files.
* [`summer-friends`](/skills/summer-friends): Summer friends in a game: a consented friends list, profile and chat screens, direct messages. Hosted only.
* [`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-matchmaking`](/skills/summer-matchmaking): Get players into matches with Summer matchmaking: queues, teams, accept prompts, join progress and failures.
* [`summer-parties`](/skills/summer-parties): Let a Summer party play together: the leader presses Play, members follow into the same match. Hosted only.
* [`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.
* [`summer-store`](/skills/summer-store): Sell Summer items for Sparks in a multiplayer game: shop, checkout, owned items, authority entitlement checks.
* [`summer-world-chat`](/skills/summer-world-chat): Add match chat to a Summer game: one World channel shared by everyone in the match. Hosted only.

***

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