mancala/apps/backend/README.md
Halit Aksoy f7a76c861e docs: describe the game bot
The bot in the backend README with the calibration command, AGENTS.md notes on
bot ids and test tags, and the open work in TODO.md.
2026-10-16 14:27:49 +03:00

124 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Mancala Backend
The game server: matchmaking, live games over a WebSocket, saved games and a small HTTP API. Java 25 and Quarkus 3.40,
built with Maven. The design and the reasons for it are in `docs/specs/quarkus-backend`.
## Getting started
You need a JDK 25 (e.g. Temurin); Maven comes with the wrapper. Postgres, Valkey and Keycloak run with Docker Compose,
see the README in the repo root.
```sh
./mvnw quarkus:dev # http://localhost:5005; a debugger can attach on 5006
./mvnw test # needs Docker: Postgres and Valkey run with Quarkus Dev Services
./mvnw package # target/quarkus-app/quarkus-run.jar
java -jar target/quarkus-app/quarkus-run.jar
```
The Docker image is built from the repo root: `docker build -f apps/backend/Dockerfile .`. It listens on port 6000.
## Code
| Package | What |
|---|---|
| `engine` | The game rules, a port of `packages/mancala.js`; `Position`, a compact board for searching moves |
| `bot` | The bot: levels, strength, the search and `BotPlayer`, which plays its turns in live games |
| `model` | The JSON sent to clients and stored, and the shared `ObjectMapper` |
| `loop` | `GameLoop`, the single thread that owns all live game state |
| `auth` | Keycloak token verification |
| `rtmt` | The WebSocket protocol (`RtmtServer`) and the Quarkus endpoint |
| `game` | Games, matchmaking, the clock, live previews, snapshots and saving finished games |
| `history` | Saved games in Postgres, migrations, the lists for the API |
| `live` | Valkey: live game snapshots, games waiting to be saved, the cache |
| `http` | The HTTP API |
| `Backend` | Wiring and startup |
All live state (games, matchmaking, sockets) is only touched on the game loop, like on the Node backend's event loop.
Blocking work never runs there: Postgres writes run on the recorder's thread, Valkey writes on their own ordered
thread, token checks on virtual threads, HTTP requests on Quarkus' workers (which read live games through
`GameLoop.call`).
The clients use the TypeScript engine, the server the Java one; they must play exactly alike.
`EngineParityTest` replays games recorded with the TypeScript engine. After changing the rules in both, record them
again:
```sh
yarn --cwd ../../packages/mancala.js build && node scripts/record-engine-games.js
```
## Authentication
Clients log in with Keycloak (see `apps/keycloak`), either as a guest or with an account, and send the access token
over the WebSocket right after connecting:
```json
{ "channel": "auth", "message": { "token": "<Keycloak access token>" } }
```
The server answers on `on_auth` with the user (`{ id, name, isGuest }`) or on `on_auth_error` and closes the socket
with code 4001. Other messages are ignored until the socket is authenticated, and connections that do not
authenticate within 5 seconds are closed. Send `auth` again with the new token after refreshing it.
User ids of Keycloak users are the token subject prefixed with `kc:`.
| Variable | Default | Purpose |
|---|---|---|
| `KEYCLOAK_ISSUER` | `http://localhost:8080/realms/mancala` | Token issuer; must match the `iss` claim exactly |
| `KEYCLOAK_JWKS_URL` | issuer + `/protocol/openid-connect/certs` | Where to fetch signing keys, if the backend reaches Keycloak at a different address |
| `KEYCLOAK_CLIENT_IDS` | `mancala-web` | Comma-separated clients whose tokens are accepted |
| `LEGACY_USERKEY_AUTH` | `true` | Also accept old clients that connect with `?userKey=<uuid>` and the `/register` endpoint. Turn off once no released client uses it, because a userKey is not a secret the server can check |
| `PORT` | `5005` (`6000` in the image) | HTTP and WebSocket port |
## Saved games and live games
Finished games are saved in Postgres; live games are copied to Valkey after every change and restored when the
backend starts, so a restart or deploy does not end them. The design is in `docs/specs/game-history`.
| Variable | Default | Purpose |
|---|---|---|
| `DATABASE_JDBC_URL` | `jdbc:postgresql://localhost:5433/mancala` | Saved games; migrations run on startup |
| `DATABASE_USERNAME`, `DATABASE_PASSWORD` | `mancala`, `mancala` | |
| `VALKEY_URL` | `redis://localhost:6379` | Live game state and short-lived caches |
The backend keeps running when either is down: games are still played, finished games are saved once Postgres is
back, and lists are served without the missing data. The tables, the `schema_migrations` table and the Valkey keys
are the Node backend's, so the two can replace each other in either direction.
## Game clock
Each player has a clock for the whole game; only the clock of the player whose turn it is runs, also while they are
offline. A move that arrives after the mover's time has run out is refused, and `GameClockScheduler` ends games whose
running clock reaches zero once a second, so the opponent wins on time even if nobody is connected. Time while the
backend is down is not charged. The design is in `docs/specs/game-clock`.
| Variable | Default | Purpose |
|---|---|---|
| `GAME_CLOCK_MS` | `300000` | Time each player has for a game; a short value such as `40000` helps when testing by hand |
## Bot
Players can play against a bot at four levels (`new_bot_game` with `{ level }`); the lobby offers it after 10 seconds
without an opponent. Each level is a range of strengths, and the bot draws one per game, so games at the same level
differ. It plays as `bot:<level>` through the same game logic as players, with the clock, saving and spectators, and
waits for the previous move's animation and a thinking time before moving. Its moves are worked out with alpha-beta
search on `Position` on a thread pool of their own, never on the game loop. The design is in `docs/specs/game-bot`.
| Variable | Default | Purpose |
|---|---|---|
| `BOT_SEARCH_THREADS` | half the processors | Threads working out the bot's moves |
After changing the search or the strength table, check that every level still beats the one below:
```sh
./mvnw test -Dtest=BotTournamentTest -Dgroups=calibration -DexcludedGroups=none
```
| Route | Auth | Response |
|---|---|---|
| `GET /game/:id` | – | A live game, or `{ status: "finished", record }` |
| `GET /games/popular?limit=5` | – | `{ kind: "live" \| "finished", games }` |
| `GET /games/me/active` | Bearer token | `{ game }` or `{ game: null }` |
| `GET /games/me?limit=3` | Bearer token | `{ games }`, newest first |
Requests are logged to `access.log`, and a move that fails writes the game to `crashes/`, both in the working
directory.