# playgd.party bot API

Connect your own Guandan bot to the site and play many hands against our bots (the same `adb:v18` the site serves,
or DanZero / the rule bots), scored the way we score ourselves. One WebSocket, JSON text frames, one decision at a
time. The example client `examples/bot_client.py` (also served at https://playgd.party/examples/bot_client.py) is
a complete working client in ~100 lines of Python; replace its `choose(state)` with your bot.

## Getting a key

1. Create an account on https://playgd.party/account (a username and a password; no email is asked for, so
   there is no password reset by mail - if you lose the password, ask the site owner).
2. Open **API keys** (https://playgd.party/keys), give the key a label and click *Create key*.
3. Copy the key: it is shown once and only a hash of it is stored. Up to 3 active keys per account; revoke on the
   same page.

```
pip install websockets
export GD_BOT_KEY=gd_xxxxxxxx_...
python bot_client.py --hands 20                      # random legal moves vs adb:v18, duplicate
python bot_client.py --opponent danzero:0.0 --partner adb:v18 --hands 200 --name mybot
```

## The connection

`wss://playgd.party/ws/bot`. Every frame is a JSON object with a `"type"`. The server sends
`{"type":"ping"}` every 25 s (the site is behind Cloudflare, which drops idle sockets): answer `{"type":"pong"}` or
ignore it; you may send `{"type":"ping"}` yourself and get a `pong`.

### 1. `hello` (client, the first frame, within 20 s)

```json
{"type":"hello","key":"gd_...","name":"mybot","opponent":"adb:v18","partner":"self","hands":200,"duplicate":true,"seed":12345}
```

| field | meaning |
|---|---|
| `key` | your API key (never put it in the URL) |
| `name` | what your bot is called in the game log (1-24 chars) |
| `opponent` | the bot in the two opposing seats: `adb:v18` (default `ours` = the site's default bot), `adb:v17`, ..., `danzero:0.0`, `ai1`, `ai3`, `greedy`. `GET /api/agents` lists them; searched entries (`adb:v13`) are not available here |
| `partner` | `self` (default): you play both seats of your team, each seat gets its own `decide` with only its own cards. Or an agent spec: that bot is your partner |
| `hands` | 1..2000 (even when `duplicate`) |
| `duplicate` | default `true`: each deal is played twice, your team in seats 0+2 then in seats 1+3 |
| `seed` | optional; the same seed and settings give the same deals |

### 2. `ready` (server)

```json
{"type":"ready","session":"3fa2c1b9","hands":200,"opponent":"adb:v18","partner":"self","duplicate":true,"seed":12345,
 "hands_left_today":4800,"limits":{...},"convention":"..."}
```
`hands` may be less than asked when your daily quota is nearly used up.

### 3. `decide` (server) / `play` (client)

```json
{"type":"decide","hand":0,"seat":0,"req":17,"state":{
  "seat":0,"hand":["S2","S5","H7","HT","HJ","HJ","CK","DA","SB","HR"],"level":"2","wild":"H2","team_ranks":["2","2"],
  "cards_left":[10,12,9,11],"finished":[],
  "to_beat":{"seat":3,"kind":"Pair","rank":"9","cards":["C9","D9"]},
  "history":[{"seat":2,"kind":"Single","rank":"5","cards":["H5"]},{"seat":3,"kind":"PASS","rank":"","cards":[]}, ...],
  "legal":[{"index":0,"kind":"PASS","rank":"","cards":[]},{"index":1,"kind":"Pair","rank":"J","cards":["HJ","HJ"]}, ...]}}
```
```json
{"type":"play","req":17,"index":1}
```
`state` is exactly what a human in that seat sees: your own hand, the level and the wild card, cards left per seat,
the seats already out (in finishing order), the play to beat and who made it (`null` when you lead), this hand's
history from the first move, and your legal moves. Answer with the `index` of one legal move and the same `req`.
Forced moves (one legal action) are played for you. Before the first decide of a hand nothing is sent; the hand
begins when it is your turn.

**Notation.** A card is suit + rank: suits `S H C D`, ranks `2 3 4 5 6 7 8 9 T J Q K A`; the jokers are `SB`
(black) and `HR` (red). The wild card is the heart of the level rank (`H2` at level 2). Move kinds: `Single`,
`Pair`, `Trips`, `ThreeWithTwo` (full house), `Straight` (five), `ThreePair` (three consecutive pairs), `TwoTrips`
(two consecutive trips), `Bomb` (4-10 of a kind), `StraightFlush`, `FourKings` (all four jokers) and `PASS`; `rank`
is the kind's defining rank. Seats 0+2 and 1+3 are the two teams; play goes 0 -> 1 -> 2 -> 3.

### 4. `hand_end` (server)

```json
{"type":"hand_end","hand":0,"deal":0,"your_seats":[0,2],"score":2,"scores":[2,-2],"finish_order":[0,1,2,3],
 "n_actions":87,"timeouts":0,"running":{"hands":1,"mean":2.0}}
```
`score` is YOUR team's result for the hand: +3 when your team finishes 1st and 2nd, +2 for 1st and 3rd, +1 for
1st and 4th, and the negatives when the other team wins.

### 5. `done` (server), then the close frame

```json
{"type":"done","reason":"complete","summary":{"hands":200,"mean":0.31,"se":0.17,"win_rate":0.55,
 "paired":{"n":100,"mean":0.62,"se":0.25},"timeouts":0,"strikes":0,"elapsed_s":812.4,"hands_per_min":14.8}}
```
`mean` is the mean score per hand; with `duplicate`, `paired` is the mean of (hand as seats 0+2) + (same deal as
seats 1+3) over the deal pairs, with its standard error - the number we quote when we compare bots.

### `error` (server)

```json
{"type":"error","code":"bad_index","message":"index must be an integer in 0..14","strikes":1,"fatal":false}
```
Codes: `bad_key`, `disabled` (the API is switched off), `bad_hello`, `bad_agent`, `busy`, `quota`, `handshake_timeout`,
`bad_json`, `bad_frame`, `unexpected_type`, `stale_req`, `bad_index`, `timeout`, `frame_too_large`, `flood`,
`server_error`. A fatal error is followed by a close frame whose code says why: 1000 done, 4001 bad key, 4002 bad
hello, 4003 busy, 4004 too many strikes, 4005 too many timeouts, 4006 handshake timeout, 4007 frame too large /
flood, 4008 daily quota, 4009 server error, 4010 API switched off.

## The game convention

Every hand is a fresh deal at level 2 with no tribute (the same convention as our evaluation scripts, so your
numbers compare with ours): 27 cards each, 108-card deck, seats 0 and 2 against 1 and 3, the server's bots play
their argmax move (no sampling). Nothing carries from one hand to the next. With `duplicate`, the two hands of a
deal use the same cards; your team's seats are swapped, so the paired score removes most of the luck of the deal.

## Limits (defaults; the `ready` frame reports the server's)

| limit | default |
|---|---|
| outstanding decisions per connection | 1 |
| time per move | 30 s, then legal move 0 is played for you and counted as a timeout |
| timeouts per session | 5, then the session closes (4005) |
| client decisions per second | 5 |
| hands per session | 2,000 |
| hands per account per day (UTC) | 5,000 |
| concurrent sessions per account | 2 |
| concurrent sessions on the server | 4 |
| hello / idle timeout | 20 s |
| frame size | 64 KB |
| malformed or out-of-range replies | 3, then the session closes (4004) |
| active keys per account | 3 |

## What is logged

Every finished API hand is stored in the site's game log: the deal, every move, the result, the names of the bots at
the table, your bot's `name`, your account id and the key's label. Never the key itself. Human games on the site
are stored the same way (with the names shown at the table). Ask the site owner to delete your account and its
records.
