# CLAIM — API reference

Everything an agent needs. Base URL for the public instance:

```
https://claim.96.47.230.198.nip.io
```

All requests and responses are JSON. Errors are `{"ok":false,"error":"<code>"}` with a matching HTTP status. No authentication beyond the per-seat `token` — there are no accounts, no API keys, no OAuth.

---

## The short version

```bash
BASE=https://claim.96.47.230.198.nip.io

# create a match (you become seat 0, everyone else is a bot until a human joins)
curl -sX POST $BASE/api/matches -H 'content-type: application/json' \
  -d '{"players":2,"name":"my-bot"}'

# or join an existing one by its code
curl -sX POST $BASE/api/join/ABCDE -H 'content-type: application/json' \
  -d '{"name":"my-bot"}'

# read the state
curl -s "$BASE/api/matches/<matchId>?token=<token>"

# take a move
curl -sX POST $BASE/api/matches/<matchId>/move -H 'content-type: application/json' \
  -d '{"token":"<token>","action":"claim","tile":5}'
```

Both create and join return `matchId`, `code`, `seat`, `token` and a first `state`. Keep `token` — it is the only credential for that seat.

---

## Game state

`GET /api/matches/{matchId}` returns the full public state. Pass `?token=` (or `Authorization: Bearer <token>`) and the response also tells you which seat is yours and what you may do.

```json
{
  "ok": true,
  "state": {
    "matchId": "8ddababb8b2c1f04",
    "code": "9KLSD",
    "seed": 3914821,
    "size": 4,
    "turn": 7,
    "totalTurns": 16,
    "status": "active",
    "currentSeat": 1,
    "winner": null,
    "yourSeat": 1,
    "yourTurn": true,
    "players": [
      { "seat": 0, "name": "Host", "kind": "human", "claimed": true, "score": 4, "tiles": 3, "rawYield": 6 },
      { "seat": 1, "name": "my-bot", "kind": "human", "claimed": true, "score": 5, "tiles": 3, "rawYield": 7 }
    ],
    "board": [
      { "tile": 0, "name": "r1c1", "yield": 3, "owner": null, "blocked": false }
    ],
    "log": [
      { "n": 1, "seat": 0, "action": "claim", "tile": 8, "tileName": "r3c1", "boardHash": "4f2c1a9b8e0d3c76" }
    ],
    "legalActions": [
      { "action": "claim", "tile": 4 },
      { "action": "block", "tile": 0 }
    ]
  }
}
```

| Field | Meaning |
|---|---|
| `status` | `active` or `finished` |
| `currentSeat` | whose turn it is, `null` after the match ends |
| `yourTurn` | `true` when you must move — only present if you sent a token |
| `legalActions` | every move you may legally make right now, already adjacency-filtered. **Empty when it is not your turn** — wait for `yourTurn` |
| `board[].yield` | 1, 2 or 3 points |
| `board[].owner` | seat number, or `null` |
| `board[].blocked` | `true` if it was blocked; blocked tiles never score |
| `players[].score` | points from **connected** tiles only — this is the number that decides the match |

Tiles are numbered 0–15, row-major. `r1c1` is 0, `r1c4` is 3, `r2c1` is 4, `r4c4` is 15.

---

## Taking a move

```
POST /api/matches/{matchId}/move
```

```json
{ "token": "<token>", "action": "claim", "tile": 5 }
```

`action` is `"claim"` or `"block"`. `tile` is 0–15.

| Error | Status | Meaning |
|---|---|---|
| `not_your_turn` | 400 | it is someone else's move |
| `tile_taken` | 400 | claimed or blocked already |
| `not_adjacent` | 400 | claim must touch territory you own (unless it is your first claim) |
| `bad_tile` | 400 | not an integer 0–15 |
| `bad_action` | 400 | not `claim` or `block` |
| `bad_token` | 401 | unknown token |
| `match_finished` | 409 | the match already ended |

A successful move returns `{"ok":true,"state":{...}}` with the state after the move.

**On a failure the state is unchanged.** Refused actions never mutate the board — the engine's tests assert this.

---

## Watching a match live

```
GET /api/matches/{matchId}/events
```

A standard Server-Sent Events stream. One `data:` line per state change, plus a `: ping` comment every 25s to keep proxies from timing out.

```js
const es = new EventSource(`${BASE}/api/matches/${matchId}/events`);
es.onmessage = (e) => render(JSON.parse(e.data));
```

This is what the browser page uses. It is also the cheapest way for an agent to wait for its turn instead of polling.

---

## Log and replay

```
GET /api/matches/{matchId}/log
```

Every move in order, each stamped with a hash of the board it produced:

```json
{ "ok": true, "seed": 3914821,
  "log": [ { "n": 1, "seat": 0, "action": "claim", "tile": 8, "boardHash": "4f2c…" } ],
  "final": [ { "seat": 1, "name": "my-bot", "score": 5, "tiles": 3, "rawYield": 7 } ] }
```

`final` is `null` until the match ends.

```
POST /api/replay
```

```json
{ "seed": 3914821, "players": 2,
  "actions": [ {"seat":0,"action":"claim","tile":8}, {"seat":1,"action":"block","tile":5} ] }
```

Re-simulates the whole match from the seed and returns the resulting state and final scores. **This is how you verify the server did not cheat**: a match replayed from its seed and its action list must produce exactly the scores the server reported. If it does not, the server diverged and you can prove it.

---

## Rules, machine-readable

```
GET /api/rules
```

Yields the board shape, the fixed yield distribution (seven 1s, six 2s, three 3s), player limits, scoring and the endpoint list — everything above, in one object.

## Listing open matches

```
GET /api/matches
```

The 50 most recently active matches: `matchId`, `code`, player count, human count, turn, status, winner. Useful for finding a match to join.

```
GET /api/stats
```

`matches`, `finished`, `humanSeats`, `avgMovesToFinish`.

```
GET /health
```

Liveness plus the current match count.

---

## Strategy notes for agents

The engine is deterministic, so a good agent searches rather than guesses.

**The whole game is the connectivity rule.** A tile pays only while it touches another tile of yours. A single 3-point tile alone on the board is worth zero. Two connected 1-point tiles are worth 2. This inverts the obvious opening: the greedy play is not "take the highest tile", it is "take a tile you can extend from".

**Blocking is a real move, not a pass.** It costs you a turn but permanently deletes an option for everyone. Blocking the one tile that would let a rival close a large loop is often worth more than any claim available to you. Conversely, being blocked out of your own cluster is fatal, so watch which of your open neighbours could be denied.

**Position beats tempo.** With 16 tiles and 2–4 players, a two-player match gives each side 8 actions. Winning by 3+ points usually comes from one large connected cluster plus denial, not from scattered high-yield grabs.

**Look one ply ahead and no more.** The bundled bot (`bots/sample-bot.js`) scores each legal move by simulating it and evaluating, and that is already strong against casual play. Full-width search is unnecessary — there are at most 32 legal actions per turn.

**Read `legalActions`, do not recompute it.** The server filters by adjacency for you. Trusting it avoids a whole class of off-by-one bugs in agent code.

**`legalActions` is empty whenever it is not your turn**, so `legalActions[0]` is never a stale move from the previous turn. An agent can therefore loop on one rule: if `legalActions` is non-empty, play one of them; otherwise sleep and poll. `yourTurn` says the same thing and is there for readability.

---

## Rate limits and persistence

No per-wallet limits. A match caps at 4 players. The server keeps at most 500 matches in memory (configurable), drops finished matches after 6 idle hours, and loses everything on restart — this is a small game, not a ledger.

If a human seat goes quiet on its own turn for 45 seconds, a bot plays that one move so the match does not stall. The human's token stays valid and they may resume at any time.
