Theme files are cached under a path that only changes with Keycloak's
version, for 30 days, so returning players kept the grey stylesheet.
It is renamed to login.css, and theme files are now cached for an hour.
The header is now the logo, the page's actions, how to play, settings
and the account. The theme swatches move into a settings menu; the
account shows the player's initials and is on every page through an
AuthUserContext. On phones the home page's actions leave the header for
a row of two buttons. Checked menu items get a check at their end.
A script in the page's head reads the theme the web app saved in the
mancala_theme cookie, or follows the system's color scheme, and puts the
theme's class on the page. The stylesheet takes the redesign's colors,
font, header, cards, fields and pill buttons; the logo and the board on
the card are masks in the theme's colors.
Messages appear at the top centre below the header as an inverted chip
of the theme, in the app's font and without icons, and close on a click
instead of sliding in at the bottom right in Notyf's red.
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.
From the second game of a series, a strip shows the series score and a
box per game in the winner's colour, with the stores, who moved first
and how it ended; a finished game opens its replay in a new tab. The
game over card shows the series score too. Spectators see names.
The card asks for a rematch, shows a request from the opponent with
Rematch and No, waits with Cancel and says why a request ended. The game
page follows the next game, keyed by its id so it starts afresh. Play
again against the bot is a rematch. The score is two columns, each name
above its stones, the winner's stones stronger.
RematchOffers keeps an offer open for ten minutes after a game nobody
left. A request goes to the opponent; when both have asked the next game
of the series starts with the other player moving first. Requests end on
no, withdrawal, a minute without answer, going offline or looking for
another game. The bot accepts at once.
Every game starts a series; a game created in a series gets its seat to
move first. Clients get the series and the first seat with the game, the
end message the series with the ended game; snapshots keep the series.
The pit animator sends the same board model, changed, at every step, so
React skipped the lesson page's updates and only the first step showed.
Each update now gets a new model.
With hints on, the player's pits show whether their move gives an extra turn
or how many stones it takes, and a line under the board says what the last
move did and why. A Hints switch appears only in games against the bot.
The home page shows newcomers a banner to start the lessons; it goes away for
good after Not now, a lesson or a game. The lessons end with a practice game
against the Easy bot with hints on.
Six lessons, one per rule, on the real board with the TypeScript engine: a
task, the move animated, praise or an explanation and a retry. Completed
lessons are remembered on the device.
A How to play page with every rule and a before/after board computed by the
engine, a help button in the header, and the rules in a side panel over the
game page so a game can go on while a rule is looked up.
RULES defines every rule with an example and a lesson; getMoveOutcome works
out a move's effect with the engine, explainMove says what happened, and
describeMove now tells the kind of capture and how many stones it took.
The lobby offers the bot levels after 10 seconds without an opponent, the
home screen has Play with bot, the bot is shown with its level, and a new
game after a bot game is one at the same level.
The lobby offers the bot levels after 10 seconds without an opponent, the
home page has a Play with bot menu, and a game against the bot ends with
Play again at the same level.
new_bot_game starts a game against the bot at the chosen level and ends the
search for an opponent. BotPlayer works out the bot's moves on its own thread
pool and plays them on the game loop after the previous move's animation and a
thinking time. Bot games keep their settings across restarts.
Negamax with alpha-beta on Position scores every move; the strength sets the
depth, the softmax temperature and the chance of a random move. The levels
are ranges of strengths drawn per game. A calibration tournament checks that
each level beats the one below.
Position keeps the board as 14 stone counts and plays the Mangala rules on
it without allocating. Tested against MancalaGame with the recorded games and
random games.
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.
The Quarkus project moves to apps/backend and the Express sources go.
The image is built from the repo root as before, with Temurin 25, and
runs as a non-root user on port 6000. Compose passes the database as a
JDBC URL with the same MANCALA_DB_PASSWORD.
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.