105 lines
5.3 KiB
Markdown
105 lines
5.3 KiB
Markdown
|
|
# 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` |
|
|||
|
|
| `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 |
|
|||
|
|
|
|||
|
|
| 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.
|