v0.13.1 concession management and record, clients update, version report
test / test (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-09-29 02:09:45 -04:00
1 parent f1ac4cbbe7
commit 6e8d80c788
30 files changed
+704 -81

No files matched your search

+60 -8
View File
@@ -95,17 +95,23 @@ Authorization: Bearer <token>
### Health Check
`GET /health`
Returns server and storage status.
Returns server and storage status, and the build the server runs.
**Response (200):**
```json
{
"status": "healthy",
"time": 1699123456,
"storage": "ok"
"storage": "ok",
"version": "v0.14.0-3-g1a2b3c4"
}
```
`version` is `git describe` of the source the binary was built from (`make`
sets it), or `dev` for a plain `go build`. Compare it after a deployment: a
client newer than the server gets 404 `NOT_FOUND` for endpoints the server
predates.
Storage states:
- `"disabled"` - No database configured (`-dsn` or `CHESS_DSN`)
- `"ok"` - Database operational with auth enabled
@@ -225,6 +231,11 @@ carries its SAN, derived on read from the stored position before it.
}
```
`concession` (present only when there was one) is the game's first
resignation or agreed draw, in stored-result form (`"result": "black_wins"`);
see [Continuing after a resignation or agreed
draw](#continuing-after-a-resignation-or-agreed-draw).
`result` is omitted while a game is ongoing. Persisted terminal values are
`white_wins`, `black_wins`, `draw`, and `stalemate`. `pgnResult` is the PGN
token for the same outcome (`1-0`, `0-1`, `1/2-1/2`, or `*` while ongoing),
@@ -235,7 +246,7 @@ rules core cannot notate, which the server logs. A claimed player's `name` is
their username when the claim was recorded; it survives later renames and
account deletion, and is omitted for anonymous and computer players.
Undo stays available after a result (a finished game can be rewound and
Undo stays available after most results (a finished game can be rewound and
played on), so a stored game is never final. Responses carry a strong `ETag`
and `Cache-Control: private, no-cache`: send `If-None-Match` to get 304 while
nothing changed.
@@ -366,6 +377,7 @@ A live game (`GET /games/{gameId}` and every game response) carries:
| `termination` | How a finished game ended: `checkmate`, `resignation`, `stalemate`, `insufficient_material`, `threefold_repetition`, `fifty_move_rule`, or `agreement`; omitted while unfinished |
| `drawOffer` | `w` or `b`: that side's draw offer awaits an answer; omitted when none |
| `drawOutcome` | Only on responses to `POST .../draw`: `offered`, `accepted`, or `declined` |
| `concession` | The game's first resignation or agreed draw: `{"result": "black wins", "termination": "resignation", "ply": 24}`, `ply` being the moves played when it was made; kept when an undo continues play |
**Automatic draws.** After every move the game is drawn, without a claim, as
on most online servers, when:
@@ -421,16 +433,53 @@ Other errors are 400: `GAME_OVER` for a finished game, `INVALID_REQUEST` for
an ambiguous or computer side, a missing offer, or a game that is not
ongoing. Both return the game.
#### Continuing after a resignation or agreed draw
A resignation or agreed draw is a *concession*: a result the players chose.
- **Both sides human:** it is final. `undo` returns 400 `GAME_OVER`, for a
hot-seat game too.
- **Against the computer:** the claimant may `undo` it and play on. The live
`state` and `termination` (and the stored `result` and `termination`)
follow the game; `concession` keeps the first one, in memory and in the
database (`concession_*` columns), through later undos, moves, and results.
History reports it, and the full PGN notes it in the final comment, such as
`{ White resigned at ply 24; play continued. }`.
### Claims and control
Anyone holding the game ID may move for, undo, reconfigure, or unload an
unclaimed side; a claim is what protects a player. Moves, resignation, and
draws act for one side and need that side's claim (or none). The requests
below affect both sides, so:
| Request | Allowed when |
|---|---|
| undo, configure players | no side is claimed by another user: the caller holds every claim, or nothing is claimed |
| delete (unload) | the caller holds a claim, or nothing is claimed |
Otherwise they return 403 `UNAUTHORIZED`. Two players who each claimed a
side therefore cannot take back each other's moves or swap the opponent for
the computer: takebacks between players are not supported. Send the bearer
token (optional authentication) with these requests.
### Undo Moves
`POST /games/{gameId}/undo`
Reverts moves from history. Undo stays available after a result, including a
resignation or an agreed draw: it rewinds the result and any draw offer.
```json
{"count": 2}
```
Reverts `count` plies (default 1). Allowed after a result: it rewinds the
result and clears draw offers. Refused while the computer is thinking, after
a resignation or agreed draw between two humans (400 `GAME_OVER`), and by
another user's claim (403, see [Claims and control](#claims-and-control)).
### Configure Players
`PUT /games/{gameId}/players`
Changes player configuration mid-game.
Changes player configuration mid-game; subject to [Claims and
control](#claims-and-control).
### Get Board
`GET /games/{gameId}/board`
@@ -441,7 +490,8 @@ Returns ASCII board visualization.
`DELETE /games/{gameId}`
Unloads the live game from memory. Its persisted game and move history remain
available through the history endpoint. Returns 204 on success.
available through the history endpoint. Returns 204 on success, 403 to a
caller holding none of the game's claims.
## Error Format
```json
@@ -453,7 +503,9 @@ available through the history endpoint. Returns 204 on success.
```
Error codes:
- `GAME_NOT_FOUND` - Invalid game ID
- `GAME_NOT_FOUND` - No live game with that ID
- `NOT_FOUND` - No such endpoint (for example a server older than the client)
- `UNAUTHORIZED` - A side or the game is claimed by another user
- `INVALID_MOVE` - Illegal chess move
- `NOT_HUMAN_TURN` - Wrong player type for turn
- `GAME_OVER` - Game already ended
+8 -2
View File
@@ -237,7 +237,7 @@ Commands encapsulate operations with type, arguments, and optional user ID for a
### Player Configuration
Players identified by UUID (authenticated users) or generated IDs (anonymous), configured with type (human/computer), skill level, and search time.
### Storage Schema (version 2)
### Storage Schema (version 3)
```sql
users (
user_id uuid PRIMARY KEY,
@@ -271,6 +271,10 @@ games (
-- insufficient_material, threefold_repetition,
-- fifty_move_rule, agreement
end_time_utc timestamptz, -- set exactly when result is set
concession_result text, -- first resignation (win) or agreed draw,
concession_termination text, -- kept when an undo continues play
concession_ply integer, -- against the computer; all four set
concession_time_utc timestamptz -- together or all NULL
)
moves (
@@ -286,7 +290,9 @@ moves (
`schema_version` records the applied version; version 2 added
`termination`, constrained to be set exactly with `result` and to fit it
(wins by checkmate or resignation, draws by a draw rule or agreement). Indexes cover session expiry,
(wins by checkmate or resignation, draws by a draw rule or agreement).
Version 3 added the `concession_*` columns: a rewind clears `result` and
`termination` but keeps them, and the first concession is never overwritten. Indexes cover session expiry,
each claim column ordered by `(start_time_utc DESC, game_id DESC)` for the game
listing, and the start time of unclaimed games for the anonymous-game purge.
Claims and player IDs deliberately have no foreign key to `users`: history
+10 -3
View File
@@ -119,14 +119,18 @@ chess > computer
```
#### `undo` / `u`
Undo one or more moves.
Undo one or more moves. Refused when another user claimed a side (players
cannot take back each other's moves) and after a resignation or agreed draw
between two humans. Against the computer a resignation can be undone; the
client notes that it stays on record.
```
chess > undo # Undo last move
chess > undo 3 # Undo last 3 moves
```
#### `show` / `h`
Display board and game state with colored pieces.
Display board and game state with colored pieces, the result, and any
pending draw offer.
```
chess > show
```
@@ -145,7 +149,10 @@ chess > delete <gameId> # Unload specific live game
```
#### `poll` / `p`
Long-poll for game updates (waits up to 30 seconds).
Long-poll for game updates (waits up to 30 seconds). Returns on a move, an
undo, a result (a resignation or an accepted draw), or a draw offer, and
prints the outcome or offer. In a two-player game, `poll` is how you learn
that the opponent moved, offered a draw, or resigned.
```
chess > poll
```
+16 -4
View File
@@ -62,13 +62,25 @@ The tables:
| `chess.schema_version` | One row: the applied migration |
| `chess.users` | Accounts: lowercase username and email, Argon2id password hash |
| `chess.sessions` | One active session per user; deleting it signs the user out |
| `chess.games` | Players, claims (`*_claimed_by`), name snapshots, `result`, `termination`, times |
| `chess.games` | Players, claims (`*_claimed_by`), name snapshots, `result`, `termination`, `concession_*`, times |
| `chess.moves` | Ply `move_number` 1..n, UCI move, FEN after the move |
`result` is `white_wins`, `black_wins`, `stalemate`, or `draw`, and
`termination` says how: `checkmate`, `resignation`, `stalemate`,
`insufficient_material`, `threefold_repetition`, `fifty_move_rule`, or
`agreement`. Both are empty while a game is unfinished.
`agreement`. Both are empty while a game is unfinished, and an undo empties
them again. `concession_result`, `concession_termination`, and
`concession_ply` keep the first resignation or agreed draw even when an undo
against the computer continued play:
```sql
SELECT game_id, concession_result, concession_ply,
coalesce(result, 'unfinished') AS now
FROM chess.games
WHERE concession_result IS NOT NULL
AND (result IS DISTINCT FROM concession_result
OR termination IS DISTINCT FROM concession_termination); -- continued after conceding
```
`\set g '<game-id>'` stores a game ID for the `:'g'` references below.
@@ -276,7 +288,7 @@ su -m chess -c '/home/chess/bin/chess-server db delete -confirm -dsn "postgres:/
su -m chess -c '/home/chess/bin/chess-server db init -dsn "postgres:///chess?host=/tmp"'
```
`db init` prints `Database schema ready (version 2)`. In split mode the
`db init` prints `Database schema ready (version 3)`. In split mode the
`chess` role owns no tables and cannot drop or create them; run both commands
as `postgres` acting as the owner role instead, e.g. on Linux:
@@ -304,6 +316,6 @@ Verify any of them:
```sql
\dt chess.* -- games, moves, schema_version, sessions, users
SELECT version FROM chess.schema_version; -- 2
SELECT version FROM chess.schema_version; -- 3
\drds -- chess: search_path and timeouts in database chess
```
+4 -2
View File
@@ -137,12 +137,14 @@ systemctl status chessd
journalctl -u chessd -f # server log
sudo chess-db user add -username <name> # accounts; see database.md
sudo chess-db verify # check stored games; run after upgrades
curl -s http://127.0.0.1:8080/health # "storage":"ok"
curl -s http://127.0.0.1:8080/health # "storage":"ok", "version" = the build
systemctl list-timers chess-backup.timer # with ENABLE_BACKUP=yes
```
**Upgrade:** build, then rerun `setup.sh` with the new `CHESS_BINARY`.
Schema migrations run before the service starts.
Schema migrations run before the service starts. `/health` reports the
running build as `version` (`git describe` of the source); check it matches,
and upgrade the server before or with a web client served from elsewhere.
**Rollback:** until you delete it, the previous binary is kept:
+6 -1
View File
@@ -302,10 +302,15 @@ Restore the previous web client files on the host as well.
```sh
jail# service chessd status
jail# fetch -qo - http://127.0.0.1:8080/health # "storage":"ok"
jail# fetch -qo - http://127.0.0.1:8080/health # "storage":"ok", "version" = the build
jail# sockstat -4 -6 -l | grep postgres # no TCP listener
jail# sockstat -4 -c | grep ':8080' # peer = nginx address
jail# su -m chess -c 'psql -X -d chess -Atc "SHOW search_path"' # chess
jail# su -m chess -c 'psql -X -d postgres -c "select 1"' # rejected by pg_hba
host# curl -s https://<site>/chess/health # through nginx
```
`version` in `/health` is `git describe` of the checkout the binary was built
from. After an upgrade it must match the new build; a web client copied to a
static site ahead of the server gets 404 `NOT_FOUND` (shown as "Game Not
Found" by older clients) for endpoints the running server predates.
+2 -1
View File
@@ -41,7 +41,8 @@ into phases that each ship and test on their own.
| D3 | Where SAN/PGN is produced | **Decided: server-side Go** (R1), derived on read from the initial FEN and the UCI line: no schema change, one implementation for web and CLI, no third-party script under the CSP. |
| D4 | Player names | **Done.** `white_name`/`black_name` snapshot the claimant's username in the claim transaction and survive renames and deletions; exposed as `players.*.name`. |
| D5 | Draw rules | **Done.** Draws apply automatically after every move, as on most online servers: dead material, threefold repetition, and the fifty-move rule (FIDE's claimable draws, and so also its 75-move and fivefold rules). Draw offers (a computer answers from its evaluation) and resignation exist; the stored `termination` says how a game ended. |
| D6 | Undo after a result | **Decided: keep allowed**, so a player can step back from a finished game and play a line again. Consequence: a finished game's stored history, result, and end time are rewritten by undo; replay shows the line as it stands now. |
| D6 | Undo after a result | **Decided: keep allowed**, so a player can step back from a finished game and play a line again. Consequence: a finished game's stored history, result, and end time are rewritten by undo; replay shows the line as it stands now. **Refined (v3 schema):** a resignation or agreed draw between two humans is final, and undo, player changes, and unload respect other users' claims (no takebacks between players). Against the computer a concession can be undone, but the first one stays on record (`concession_*`) and in history and PGN. |
| D8 | Human-vs-human in the web client | **Open.** The web client plays only against the computer; two-player games run through the API and terminal client (`join`, `poll`). A web mode would need join-by-link, a long-poll for the opponent's moves, and an offer indicator on the Draw button (accept by pressing it). |
| D7 | List pagination | **Done.** Opaque keyset `cursor` on `(start_time_utc, game_id)`; `offset` kept for existing clients and exclusive with `cursor`. |
## Phase R1 — Notation Core (server) — done