# Online games: the free-play service that is live

Updated 2026-10-26. This is the implemented protocol. [Token wagering](multiplayer.md)
is a separate proposed protocol: no deposits, burns, prizes or payouts run through these APIs.

## Games and modes

| Rules key | Local cabinet | Online cabinet | Core strategy |
| --- | --- | --- | --- |
| `gravity` | Gravity Four | Gravity Four Online | Drop discs; connect four, including diagonals |
| `reversi` | Orbit Reversi | Orbit Reversi Online | Bracket discs, flip lines, secure corners |
| `boxes` | Neon Dots | Neon Dots Online | Close boxes, keep your turn, exploit chains |
| `five` | Fivefold | Fivefold Online | Place stones on a 9×9 grid; build an unbroken five |
| `boxing` | Practice vs bot inside the online cabinet | Neon Boxing | Real-time footwork, punches, blocks, slips and stamina |

Local games share a screen and need no permissions or network. Online games run across devices.
Public matchmaking takes the oldest compatible waiting room. Private invites reserve the open
seat for whoever has the unguessable invite link. Both participants explicitly press Ready.
These are real human seats. No bot fills an empty online room.

**Boxing has a separate real-time clock and reconnect policy:** see [the boxing protocol and reference rules](boxing.md). The turn-clock rules below apply to board games.

Queues and invites expire in 10 minutes; ready checks expire in 90 seconds. Each turn lasts
60 seconds, including extra moves when a box is captured or Reversi automatically skips an
opponent with no legal move. A match lasts at most 30 minutes. Timers continue through tab changes,
disconnects and server restarts. A turn timeout or resignation loses; pre-start cancellation has
no winner; a rules upgrade or total-duration expiry voids the match. No funds are involved.

## Sessions, recovery and privacy

The platform generates a 256-bit guest secret in browser storage and posts it to
`POST /api/multiplayer/session` as `{ "token": "64 lowercase hexadecimal characters" }`.
The response contains a pseudonymous `player` ID. Only a hash of the secret is stored on the server.
Subsequent requests use `X-Player-Token`; the secret is never put in a link or given to an opponent.
All writes require the platform's exact Origin. The separate uploaded-games origin cannot call them.

One player can occupy one active room. Retrying a room request resumes that room instead of
creating a duplicate. Guest sessions are not wallet authentication or proof that two browsers
belong to different people. Clearing browser storage loses access; different devices have different
guest identities. There is no cross-device sign-in or wallet stake authorization in this service.

Snapshots and replays require the participant's session. `Your matches` on the homepage and the
online lobby recover active games and recent receipts. Reconnecting reads a saved snapshot;
it never sends the last move again. History/replays persist for 30 days, with cleanup when the
service is used. Active rooms remain until terminal. Limits include 200 active rooms, 60 room
requests/hour/player, 90 actions/minute/player and 12 new guest sessions/hour/sending-address hash.

## HTTP contract

These routes are enabled by the `multiplayer` feature. JSON bodies are limited to 1 KiB.
Use the parent platform's session, not an iframe-held secret.

| Request | Body / response |
| --- | --- |
| `GET /api/multiplayer/catalog` | Rule build hash, game definitions, clocks and `wagers: false` |
| `POST /api/multiplayer/rooms` | `{game, mode: "public"}` to queue; `"private"` to invite; `"join", room` to accept an invite |
| `GET /api/multiplayer/rooms/{id}` | Participant snapshot; safe after a lost response or reconnect |
| `POST /api/multiplayer/rooms/{id}` | `{kind: "ready"}`, `{kind: "leave"}`, or `{kind: "move", revision, action}` |
| `GET /api/multiplayer/rooms/{id}/replay` | Original position, ordered legal inputs, pinned rules and saved result |
| `GET /api/multiplayer/history` | Last 20 rooms and last 20 score attempts for this guest |

A snapshot includes `id`, `game`, `rules`, `mode`, `seat` (0 or 1), `players`, `ready` (bitmask),
`status`, monotonic `revision`, authoritative `state`, legal action indices, `winner`, `reason`,
`deadline`, `serverTime`, creation/start/end timestamps and `free: true`.
Statuses are `waiting`, `ready`, `playing`, `finished`, `cancelled` and `void`.
Winner is seat 0/1, -1 for a draw, or null for cancellation/void.

Send exactly the displayed revision. Wrong-turn, occupied, malformed or stale moves are rejected.
The server checks and commits a move, clock change and replay event in one SQLite transaction.
After a lost move response, GET the snapshot: blindly repeating a POST could refer to another turn.
Board clients use serial polling (about 900 ms, slower in background tabs), back off on failure and
ignore older revisions. Client pause never pauses an online board match. Boxing switches from the same lobby/ready flow to an authenticated WebSocket; a network disconnect pauses it for its published grace period.

Actions are integers: Gravity Four uses column 0–6; Reversi and Fivefold use row-major cell indices;
Neon Dots uses the 20 horizontal edges first, then 20 vertical edges, each in row-major order.
The [reference browser rules](../js/duel-rules.js) provide `initial`, `legal`, `play`, `flips`
and `boxEdges`. `play` mutates only after validating a legal integer action. Full match replays
can be inspected and exported from results; the reference rules can reconstruct every board.

## Asynchronous score challengers

At the start of a single-score run, the parent records its round ID, speed/input mode and build:
`GET /api/multiplayer/challenge-rules?game=chop` returns `game`, `build`, `direction` and
`verified: false`. After a precommitted entry completes, the parent automatically sends:

```json
{
  "game": "chop",
  "build": "the exact build returned at run start",
  "round": "32 lowercase hexadecimal characters",
  "score": 68,
  "mode": "normal:standard"
}
```

`POST /api/multiplayer/challenges` now refuses new post-score submissions (409). Historical
`GET /api/multiplayer/challenges/{id}` receipts remain readable.

Choose **Free score match** before starting. `POST /api/multiplayer/score-entries` accepts
`game`, `build`, `mode` and a fresh 32-hex `round`, with no score/stake. It reserves the latest
compatible playing/completed entry from another guest. `POST /api/multiplayer/score-entries/{id}`
then accepts that exact `round`, `build`, `mode` and one finite nonnegative `score`, or
`{round, action: "forfeit"}`. A terminal result cannot be replaced. `GET` on that path returns the
private receipt; opponent scores stay hidden until the caller's attempt ends. The browser sends
completed results automatically and retries the same result after connection loss. Abandonment or
24-hour expiry forfeits an unfinished entry. One playing entry per guest, 30/hour and 10,000
unexpired entries are allowed. Old builds, speed and input modes never pair. Decibel Drag uses
lower-is-better; other single scores use higher-is-better. No video uploads through these routes.

The earlier JSON example is a completed-result payload, not permission to enter after playing.
Wager quotes in the UI are example tokens only: no funding/escrow endpoint exists.

Native single-score games and approved SDK games participate. Independent web builds must add
SDK lifecycle/score reporting first. SDK community scores currently use higher-is-better. Two-seat
games use their actual head-to-head result rather than a synthetic single-score comparison.

These are **reported scores**. Runs may use different random layouts, modified clients can lie,
and one human can create multiple guests. Do not attach token prizes or treat them as a verifier.

## Build another competitive game

Keep simulation separate from rendering. The shipped example consists of:

- `arcade/duel_rules.py`: bounded, deterministic rules, independent of requests and clocks.
- `public/js/duel-rules.js`: practice and preview mirror, also importable in Node tests.
- `arcade/multiplayer.py`: atomic reservations, ready checks, input validation, board clocks, replay and receipts. Boxing adds `arcade/boxing_service.py` and the deterministic `arcade/boxing.py` adapter.
- `public/js/multiplayer.js`: native accessible controls, snapshot recovery and recorded canvas view.
- `tests/test_multiplayer.py` and `tests/test_duel_rules.cjs`: concurrent pairing, failure handling
  and 120 full seeded games comparing Python authority with JavaScript practice.

For a native competitive adapter, define initial state, bounded legal actions, deterministic
transitions, turn/pass rules, draws and terminal outcomes. Add matching browser and server rules,
register local/online metadata and share previews, then run cross-language replay tests and two
independent clients. The platform owner reviews and deploys server adapters; uploaded code cannot
install arbitrary backend logic. This is not yet a generic `arcade.match` SDK extension.

An SDK creator can ship free single-score challenges immediately by implementing `score` and
`gameOver` correctly. For verified asynchronous wagers later, use a fixed seed and pinned build,
commit a run before play, validate every bounded input server-side, define anti-abuse and eligibility,
and use reviewed wallet sessions plus escrow with the proposed 5% combined-pool burn. A client
`gameOver` message, local video or free match receipt cannot authorize settlement.
