mancala/apps/backend
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
..
.mvn/wrapper feat(backend): replace the Node backend with the Quarkus backend 2026-10-16 11:47:40 +03:00
scripts feat(backend): replace the Node backend with the Quarkus backend 2026-10-16 11:47:40 +03:00
src feat(backend): save how the bot played 2026-10-16 14:19:56 +03:00
.gitignore feat(backend): replace the Node backend with the Quarkus backend 2026-10-16 11:47:40 +03:00
Dockerfile feat(backend): replace the Node backend with the Quarkus backend 2026-10-16 11:47:40 +03:00
mvnw feat(backend): replace the Node backend with the Quarkus backend 2026-10-16 11:47:40 +03:00
mvnw.cmd feat(backend): replace the Node backend with the Quarkus backend 2026-10-16 11:47:40 +03:00
pom.xml feat(backend): choose bot moves by strength 2026-10-16 14:18:00 +03:00
README.md docs: describe the game bot 2026-10-16 14:27:49 +03:00

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; 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:

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

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:

./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.