mancala/AGENTS.md
Halit Aksoy a3fe708746 docs: describe rematches and series
The rematch spec is implemented; AGENTS.md lists it with notes on the
offers, the keyed game route and stale dev builds, TODO.md takes the
items left out.
2026-10-17 02:21:01 +03:00

11 KiB

AGENTS.md

Guidance for coding agents working in this repository. Keep it current: when you learn something that would have saved time, add it here.

Project

Online Mancala (the Turkish Mangala rules) with a web app, an Android/iOS app and a Java (Quarkus) backend.

Path What
packages/mancala.js Game engine used by the apps; rules are plug-ins in src/common/game_rules. The backend has a Java port
packages/core Code shared by the apps: models, WebSocket client (RTMTWS), channel names, history helpers, themes, PitAnimator, translations (en, tr)
apps/backend Java 25 + Quarkus: matchmaking, games, auth, saved games, HTTP API, the Java port of the engine
apps/frontend React web app, built with Parcel
apps/mobile React Native app
apps/keycloak Keycloak image with the realm, the login theme and a Java extension for guest login
apps/postgres Init script creating the keycloak and mancala databases
apps/compose.yaml Postgres, Valkey, Keycloak, backend, frontend
config/nginx.conf The production server's nginx config (Certbot managed)
docs/specs/<feature> Specs: requirements.md, design.md, tasks.md
TODO.md Open work, known bugs and deployment steps; read it before starting

Specs that exist: keycloak-auth (login, guests, tokens), game-history (saved games, live games, replay), game-clock (5 minutes per player, ending games on time), web-redesign (the web app's look: themes, board, pages), quarkus-backend (the backend's move from Node to Java and Quarkus), game-bot (playing against a bot at four levels), tutorial (rules page, lessons, hints in bot games) and rematch (rematches, alternating first moves, the series strip).

Running locally

yarn build:packages                       # repo root; builds mancala.js and core
cd apps && docker compose up -d --build postgres valkey keycloak   # needs apps/.env, see README.md
cd apps/backend && ./mvnw quarkus:dev     # http://localhost:5005, needs a JDK 25
cd apps/frontend && yarn dev              # http://localhost:1234 (must be 1234, Keycloak only allows that)

Postgres is on localhost:5433 (5432 is often taken), Valkey on 6379, Keycloak on 8080 (admin console /admin). The backend's and frontend's defaults point at these. The mobile app points at production unless useLocalServer is set in apps/mobile/src/const/config.ts.

Checks

What Command Notes
Engine cd packages/mancala.js && npx jest After changing the rules, change the Java port too and record the parity games again (apps/backend/README.md)
Core cd packages/core && npx jest
Backend cd apps/backend && ./mvnw test Needs a JDK 25 and Docker: Postgres and Valkey run with Quarkus Dev Services. The bot's calibration tournament is tagged calibration and skipped; see apps/backend/README.md
Keycloak extension cd apps/keycloak/extension && mvn test Needs Docker; starts Keycloak with the real realm file
Web app cd apps/frontend && yarn build There is no tsconfig.json, so nothing type-checks it; use a temporary one when needed (with "types": ["styled-jsx", "react", "react-dom"]). 1 type error (react-dom/client) is known
Mobile cd apps/mobile && npx tsc --noEmit -p . and npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output /tmp/x.bundle 11 type errors are known and older than recent work

There is no automated end-to-end suite. Changes to the game flow were checked by driving Chrome with playwright-core (installed outside the repo, e.g. npm install playwright-core@1.48.2 in a scratch folder; executablePath: /usr/bin/google-chrome) against the local stack. A live game is GET /game/:id on the backend, which helps to check what the server holds.

How we work

  • The user writes in Turkish; code, comments, commit messages and specs are in English.
  • Larger features start with a spec in docs/specs/<feature>/ (requirements with acceptance criteria, design with decisions and reasons, tasks as a checklist). Implement the tasks in order, one commit each, and tick them. Small fixes need no spec. When resuming a feature, read its spec and tasks.md first.
  • Work on feature branches; commit messages follow type(scope): summary with a short body.
  • Never add AI attribution (Co-Authored-By lines, "Generated with …") to commits or PRs.
  • Do not commit files the user changed but did not ask to commit; stage only your own changes (hunks if needed).
  • New dependencies get exact versions.

Things that are easy to get wrong

Packages and builds

  • The web and mobile apps use packages/core and packages/mancala.js through link: dependencies, i.e. their built dist. After changing a package, run yarn build in it (or yarn dev to rebuild on change), or the apps use stale code. Running dev servers do not notice a rebuilt package: restart Parcel after deleting apps/frontend/.parcel-cache. yarn build in apps/frontend writes to the dist that yarn dev serves; running it while the dev server runs can leave stale CSS or JS being served, so restart the dev server afterwards. The backend does not use the packages.
  • Backend and frontend Docker images are built from the repo root: docker build -f apps/backend/Dockerfile ..
  • The mobile app uses RTMTWS from core (its own src/rtmt copy is unused), so changing the core WebSocket client changes mobile too. Metro needs watchFolders and nodeModulesPaths (already in metro.config.js).

Backend

  • The game rules exist twice: packages/mancala.js for the clients and app.playmancala.engine in the backend. Change both; EngineParityTest replays games recorded with the TypeScript engine and fails if they differ.
  • All live state (games, matchmaking, sockets, previews) belongs to the game loop thread (GameLoop). Touch it only in tasks running there; never block the loop on Postgres, Valkey or HTTP. Other threads use GameLoop.call.
  • The JSON must stay what the Node backend sent and stored: TypeScript leaves out undefined but keeps null, so only optional fields get @JsonInclude(NON_NULL) (e.g. result: null stays). Valkey snapshots and keys, the tables and schema_migrations are shared with the Node backend's format; tests check data the Node backend wrote.
  • User ids: Keycloak users are kc:<sub>; legacy mobile users are their userKey, which is a secret. Anything saved or listed publicly goes through PublicIds.publicUserId; clients get toClientGame(), never spectator ids.
  • Live games: memory is the source of truth, Valkey is a write-through copy restored on startup; the backend assumes a single instance. Finished games are written to Postgres once, with retry when it is down.
  • Postgres or Valkey being down must not stop games; reads fall back, writes are retried.
  • The game clock is stored as each player's time at the start of the turn plus turnStartedAt; compute the time left with GameClocks.getClockTimes, never store a ticking value. GAME_CLOCK_MS=40000 makes games short for manual tests.
  • Rtmt.listenOnClientConnectionChange adds a listener (several are allowed).
  • The bot plays as bot:<level> (BotLevel.isBotId) in many games at once: GameStore does not map it to a game, nothing is sent to it, it is always online and PublicIds keeps its id. The bot's search must never run on the game loop; BotPlayer runs it on its own pool and plays the move back on the loop.
  • Rematch offers (RematchOffers) hook into GameManager like BotPlayer (onGameEnded, onPlayerSearching) and live in memory only. A game's series travels with the live game (BackendGame.series, snapshots); the seat that moved first is derived from the history (firstSeat()), not stored.
  • -Dtest=GameManagerTest skips its @Nested classes; use -Dtest='GameManagerTest*'.
  • -Dgroups=... runs only the tests with that tag; run the untagged tests separately too.
  • Unit tests build the logic with fakes (FakeRtmt, in-memory stores, ManualScheduler instead of timers and direct executors); only storage, HTTP and WebSocket tests are @QuarkusTests. FakeTokenVerifier replaces Keycloak in all @QuarkusTests.
  • Quarkus dev mode's debugger is moved to port 5006 (pom.xml), because 5005 is the backend's port.
  • The Maven wrapper checks the .zip it downloads; without unzip it fetches a .tar.gz and fails the check (the Dockerfile installs unzip).

Web app

  • styled-jsx only scopes JSX written directly in a component's render; elements built in helper functions or map callbacks need .parent :global(.child) rules.
  • Pages are full-height flex columns: a block with overflow: hidden needs flex-shrink: 0 or it gets squeezed.
  • Colours come from CSS variables of the theme class on <html> (styles/app.css); the web app's themes (theme/themes.ts) are passed to core's ThemeManager, which the mobile app uses with its own default themes.
  • Styles that need container queries (@container, cqw), such as the board's, are plain .css files imported by the component; styled-jsx 5.0 does not handle them. BoardView fills its container and stands upright below 600 px.
  • PitAnimator sends the same BoardViewModel object, changed, at every animation step. Put a new object (or a new id in its own state, as GamePage does) into React state, or React skips the update and only the first step shows.
  • The rules, lessons and move explanations live in packages/core/src/tutorial; their boards after a move come from the engine (getMoveOutcome). After a rule change, run core's tests: they fail if a lesson no longer works, and the rule texts in en.ts/tr.ts may need updating.
  • The game route renders GamePage with key={gameId}, so following a rematch to the new game's id remounts it.
  • Short class names collide across the global CSS files (e.g. .st is a stone on the board); check before adding one.
  • When a script drives the app in Chrome, wait for the header (.hdr) and a few seconds after the first load before navigating; navigating during the guest login leaves an invalid token. Open the lobby with the "New Game" button, not by URL, or the matchmaking message goes out before the WebSocket is open.
  • The WebSocket client emits open after authentication; anything the server must know about a connection (spectating a game, live game previews) is sent on open, because the server forgets it when a user goes offline.

Keycloak

  • The realm file is imported only when the realm does not exist; changes to it must also be made in a running Keycloak (or recreate the database).
  • Listing some requiredActions in the realm file replaces Keycloak's whole default list, so the file contains all of them.
  • The guest authenticator uses an internal Keycloak SPI: keep the version in extension/pom.xml and Dockerfile in sync and run mvn test when upgrading.
  • Keycloak sets Secure cookies; browsers send them to http://localhost, simple HTTP clients do not.
  • The login theme (apps/keycloak/themes/mancala) is only CSS on top of keycloak.v2. Keycloak in start mode caches themes and keeps gzipped resources in data/tmp/kc-gzip-cache across restarts; see apps/keycloak/README.md for reloading it in a running container.

Mobile

  • apps/mobile/release_android.sh contains signing passwords (see TODO.md); do not copy them anywhere.