The backend README, how to run and test it, what is easy to get wrong with the game loop and the shared data formats, and the deployment note. The web app was played against it in Chrome: a full game with a spectator, a reload, a restart mid-game, the replay and a game lost on time. |
||
|---|---|---|
| .. | ||
| .mvn/wrapper | ||
| scripts | ||
| src | ||
| .gitignore | ||
| Dockerfile | ||
| mvnw | ||
| mvnw.cmd | ||
| pom.xml | ||
| README.md | ||
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.
./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:
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:
{ "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.