RtmtServer ports the WebSocket protocol (auth handshake, sockets per
user, ping) and runs on the game loop; the websockets-next endpoint at /
only forwards socket events. The HTTP routes, CORS and the access log
match the Express app. Backend wires everything and restores live games
before handling socket events. End-to-end tests play games over real
sockets against Postgres and Valkey.
The Node backend's migrations, runner, queries, keys and JSON, so the
same database and Valkey serve either backend. Integration tests run
Postgres and Valkey with Quarkus Dev Services, including a game row the
Node backend wrote and an unreachable Valkey.
GameManager, matchmaking, the game clock, live game snapshots, saving
finished games and live previews, ported from the Node backend with its
tests. Snapshots keep the Node backend's format; a test restores one the
Node backend wrote. Valkey writes and saves run off the game loop.
Records for the JSON the clients read, leaving out only the fields
TypeScript leaves undefined. GameLoop is the single thread that owns live
state; ManualScheduler replaces Jest's fake timers in tests. Keycloak
tokens are verified with jose4j and the same checks as before.
Quarkus 3.40 on Java 25, built with the Maven wrapper. The engine is a
line-by-line port of packages/mancala.js; a parity test replays games
recorded with the TypeScript engine, including illegal moves, and
compares every board, turn, error and the game JSON.
Remove the sowing and animation bugs, which are fixed, and add the
open question of who gets the remaining stones when a move empties
the opponent's side.
PitAnimator guessed the played pit of a single stone move from the
first sowing step and skipped index 0, so playing one stone from
player 1's first pit showed it still holding the stone until the
animation ended. It now empties the move's own pit (moveIndex).
Mangala skips the opponent's store, but Board.move dropped a stone in
every pit and store. Moves with enough stones to pass it gave the
opponent a stone and ended one pit too early, so captures and extra
turns were decided on the wrong pit.
Board.getSowingPath now computes the pits a move sows into, and
Board.move and core's getSowingPath (the web board's move preview)
both use it. Game step indexes are now always within the board.
AGENTS.md notes on the themes, plain CSS for container queries and
driving the app in Chrome; TODO.md gets the mobile app and rematch as
open work; the spec is marked implemented.
The game in progress comes first with a large mini board; live,
popular and recent games are grids of cards with the score, a mini
board and a status pill. Live and popular games are shown together;
live games are hidden while nothing is being played.
A summary card with the result, date, duration, move count, players and
score; players and the move number beside the board; a controls card
with the transport buttons, speed, timeline and the shown move with its
tags; the landing pit of the shown move is ringed.
Players beside the board with avatar, online status, store count and
clock, the turn between them, a game-over card over the board, and a
move list with the newest move first and tags for extra turns,
captures and store gains. On phones the bottom player is shown below
the board.
Pits and stores are wells with scattered stones and count badges, the
viewer's store on the right. Below 600 px the board stands upright.
Playable pits preview their move on hover, animated pits are ringed and
the pit of the last move has a dashed ring.
Ceviz, Gece and Yeşim themes as CSS variables on <html>, Bricolage
Grotesque, shared styles, and one header for every page with theme
swatches, the account menu and the page's main action. The lobby,
privacy page and status panels use the new look.
GET /games/popular?kind=finished returns the most watched finished
games even when games are being played, so the web app can show live
and popular games together.
ThemeManager takes its list of themes, PitAnimator marks animated pits
with PitViewModel.highlight, getSowingPath lists the pits a move sows
into, and new texts in English and Turkish.
Backend README (GAME_CLOCK_MS), AGENTS.md notes, TODO items for the open points and the mobile release order; game-history R3.3 and R3.4 marked as replaced. Checked end to end in Chrome: moves, a backend restart, a timeout and the replay.
Both clocks next to the players, counting down in server time with the same running and low-time styles as the web app. The game screen now handles on_game_end, so a game that ends on time ends on the phone too and says why.
Both clocks sit next to the players' names and count down in server time; the running one is highlighted and turns red under 30 seconds. They replace the offline countdown, so core drops TURN_TIMEOUT_MS, INACTIVE_TIMEOUT_MS, offlineSince and its text. Long info texts wrap on phones.
Live game snapshots become version 2 with the clock and each move's time left. On restore the running clock goes on from the start of the turn, so downtime is not charged; version 1 snapshots get a new clock. Saved games keep the initial time (migration 002_clock) and each move's time left.
GameClockScheduler checks every live game once a second and ends those whose running clock has reached zero, whether or not anyone is connected. The clock replaces the offline turn timeout and the inactivity rule, so both are removed with the offline times they used.
Each player starts with GAME_CLOCK_MS (5 minutes). A move deducts the turn's time from the mover; a move that arrives after the time has run out is not played and the opponent wins on time. Every ending stops the clock, and clients get the clock and the server time with each game message.
GameClock and optional clock fields on live games, end messages, move records and finished games; getClockTimes, ClockSync, getClockAtMove and formatClock with tests.
Each player has 5 minutes; the server keeps the time and a scheduled service ends games whose running clock reaches zero. Replaces the offline turn and inactivity rules.
The mancala login theme extends keycloak.v2 with one stylesheet: the game's
app bar and logo, the board's colors, pit-shaped inputs, pill buttons and a
board with stones on top of the card. Dark colors follow the system color
scheme. The realm turns the theme on and offers English and Turkish.
The test container now loads the theme and checks that the login page uses
it in the requested language.
Requirements, design decisions and tasks for the Keycloak login, written
after the implementation; mobile login and turning off legacy userKey
login remain open.
- Backend README: DATABASE_URL, VALKEY_URL, the new routes and how the
backend behaves when Postgres or Valkey is down
- TODO: drop the fixed spectator and memory bugs, add game history on
mobile, a page with all games, single-instance limits, backups and the
new .env variables
- Spec marked as implemented, all tasks ticked
- Opening a finished game shows its final position, result, score, date
and duration, with Watch from start
- First / previous / play-pause / next / last, speed 1x, 2x, 4x, a
timeline, the move list and arrow keys and space
- Stepping forward animates the move like a live game; jumps show the
position directly
- ?move=n links to a move and follows the shown move
- Players see the board from their own side
- PitAnimator takes its step interval as a parameter
- Move list under the board with player, pit and what happened (extra
turn, capture, stones to the store, game over); collapsed on phones
- Selecting a move or using the arrow keys shows that position with a
banner and Back to live; pits are locked meanwhile and the banner says
when it is your turn
- Countdown while the opponent is offline on their turn
- on_game_end is handled, so games that end by timeout or inactivity end
on the page with the reason
- Banner for the user's game in progress with a Continue button
- Live games as cards with the real board, updated over WebSocket; the
most watched finished games when nothing is live
- The user's last 3 games, from their side of the board, with the result
as they would say it
- The lobby offers to continue the game in progress when a new one is
refused
- Spectators register once connected and identified, and again after a
reconnect, so they are counted
- The backend includes games that ended but are not saved yet in /game/:id
and the lists
Board components size themselves from a --board-unit CSS variable instead
of vw. By default it is 1vw, so the game page looks the same; with the new
width prop the same board renders small, e.g. in previews.
- GET /games/me/active and /games/me (Bearer token), /games/popular (live
games, or the most spectated finished ones), and /game/:id now also
returns saved records of finished games
- Lists are cached in Valkey for 10 seconds, cleared when a game is saved,
and empty instead of failing while the database is down
- Live game previews over WebSocket: subscribers get the top 5 on
subscribe and on change, at most once per second, without counting as
spectators; ids are public ids
- RTMT supports several connection listeners
- Games record start and move times, spectator peak and distinct count
- Games end as completed, left (the leaver loses), timeout (5 minutes
offline on your turn while the opponent is online) or inactive (both
offline for 30 minutes); everyone gets on_game_end
- Connection info carries offlineSince for the countdown
- A new game is rejected while the user has one in progress
- Live games are written to Valkey after every change and restored on
startup; finished games are saved to Postgres with public ids, and
saves that fail are kept and retried every 30 seconds
- Clients get a trimmed game without spectator ids
- Startup waits for Valkey, restores games, then accepts connections;
migrations are retried while Postgres is unreachable
- Valkey client that fails commands at once while Valkey is down instead
of queueing them, and logs connection errors once
- ValkeyLiveGameStore: live game snapshots and finished games waiting to
be saved, loaded with SCAN on startup
- ValkeyCache: JSON values with a TTL; errors count as cache misses
- Integration tests against a real Valkey, including an unreachable one
- Connection pool and a small migration runner (schema_migrations, one
transaction per migration, advisory lock)
- games and game_players tables with indexes for a user's games and for
popular games
- PostgresGameHistoryRepository: idempotent save, get, recent games of a
user, popular games
- Integration tests against a real Postgres via testcontainers; a custom
Jest environment gives tests Node's web globals, which Jest 27 leaves out
- GameRecord carries the number of distinct spectators
- Spectators are removed from games when they disconnect; the lookup
iterated an iterator with for...in and never found anything
- Finished and left games are removed from memory, not only from the
player mappings
- The connection info is sent once when a game starts
- PlayerSummary, MoveRecord, live and finished game summaries, GameRecord,
timeout constants, startedAt/moveTimes on Game and offlineSince on
UserConnectionInfo
- Channels for live game previews and for rejecting a new game while one
is in progress
- getInitialBoard, getBoardAfterMove, describeMove and createGameAtMove,
with tests against games played with the engine
- English and Turkish strings for the new screens
- postgres replaces keycloak-db and holds a keycloak and a mancala database,
each with its own user, created by apps/postgres/init
- valkey with an append-only file for live game state and caches
- backend gets DATABASE_URL and VALKEY_URL