Store each player's leaderboard totals (wins, accumulated points and the
latest display name) on player_rating alongside the Elo rating, updated
in the same transaction as the match, so the board is a single indexed
table query instead of aggregating every participation on each request.
GET /api/leaderboard is now page-number paginated (page/limit) and
returns the page, page count and total, plus the caller's own rank and
page when authenticated; focus=me returns the caller's page directly.
The migration adds the columns, backfills them from the match history
(inserting rows for players from pre-Elo matches at the initial rating)
and replaces the index. backfill_elo rebuilds the aggregates too.
The web page opens on the page containing the signed-in player, marks
their row and adds a numbered footer pager.
New tavolo-scopa package with a pure rules engine and the platform
adapters for two-player scopa and four-player fixed-partnership scopa a
quattro: three cards each and four on the table, capture by rank or sum,
sweep for scope and score carte/denara/settebello/primiera.
The optional asso pigliatutto creation option turns either variant into
scopa d'assi (an ace sweeps the whole table and scores no scopa), reusing
scopone's rule semantics.
Wire both engines into the app and the Docker install, add the scopa
table view to the SPA (duo and four-seat layouts, deck count,
captured/scope stats, hand-summary and final screens, player names in the
two-player summary), and cover the rules, the platform contract, the
REST/WebSocket flows, match persistence and Elo.
Five-player briscola with a descending calling auction and a secret
partner. Starting with the player after the dealer, each player bids a
lower rank (A > 3 > K > H > J > 7 > 6 > 5 > 4 > 2) or passes; once the
standing bid is the two the auction continues on points (62, 64, ...).
The last player standing must bid, and the winner names any card of the
won rank: its holder is the secret partner, or the caller plays alone
when it is one of their own cards, and its suit becomes the trump. The
calling side must reach 61 (or the raised target); a 60-60 deal with the
plain target is a patta and is replayed. A match is a single deal, so
the caller and partner (or the lone caller) against the other players
map onto the platform's two-team result, Elo and match history.
The rules live in tavolo.briscola.chiamata with the
BriscolaChiamataEngine adapter registered alongside the other briscola
variants; the partner's role and the side totals stay hidden until the
called card is played. The web client gets a five-seat table, the
auction panel with legal bids and point raises, the call picker, the
reveal bands and a chiamata-aware final summary.
New tavolo-briscola distribution implementing briscola on the platform:
- pure rules (state.py, engine.py): 3-card deals, a face-up trump that
is the last card drawn, trump-first trick resolution (A > 3 > K > H >
J > 7 > 6 > 5 > 4 > 2), 120 points per deal, 60-60 patta re-deals, a
configurable target score (default 61), deal-end acknowledgements and
turn auto-play;
- BriscolaEngine (briscola, 2 players) and BriscolaQuattroEngine
(briscola_a_quattro, 4 players in fixed partnerships) adapters;
- registered in tavolo-app, packaged in the Docker image, documented.
Platform fixes surfaced by a two-player lobby:
- the websocket state and snapshot payloads now carry seats_open (they
were only in the REST lobby payload), so lobbies with player counts
other than four render correctly; seats_open is clamped at zero;
- the web client logs unparseable server messages instead of silently
dropping them.
Web client: briscola table for both variants (stock, face-up trump,
trick, last-trick banner), deal summary modal and game-over overlay.
Tests: tavolo-briscola rules/adapter suites, app integration tests
(lobby, websocket play, turn timeout, deal-end acks, persistence), a
platform seats_open test and frontend model parsing tests.
Move the game-independent machinery (lobby, live-game store, websocket,
deadline scheduler, match history, leaderboards) into a new
tavolo-platform distribution behind a GameEngine contract, the scopone
scientifico rules plus a platform adapter into tavolo-scopone, and keep
only the composition root in tavolo-app. The three distributions share
the tavolo namespace (PEP 420, kaya-style monorepo).
Match history becomes fully generic: Match carries the engine's result
JSON and MatchPlayer points/details instead of scopone-shaped team
columns (migration 3 backfills existing rows). Lobby creation takes an
opaque per-game options object and websocket actions dispatch to the
session's engine.
Tests: platform suite runs against a DummyEngine toy game, scopone
keeps the rules tests plus new adapter tests, server/tests covers the
wired stack end to end (194 tests, was 143).
pwo.async_test runs every test in a fresh event loop and TortoiseMixin
builds one TortoiseContext per loop. Dropping the previous context when
the loop changed orphaned its aiosqlite connections; their non-daemon
worker threads then blocked threading._shutdown forever, so the suite
printed OK but the interpreter never exited.
Add TortoiseMixin.aclose() and a tests.helpers.async_test wrapper that
closes the context in a finally on the test's own loop, and switch the
test modules over to it.
Each player's rating starts at 1500 and updates transactionally with
every finished match: a team's rating is the mean of its two members
and the standard K=32 formula decides the zero-sum delta applied to
both members of a team. Ratings are per game type in a new
player_rating table; match_player records each match's elo_delta.
- GET /api/leaderboard exposes elo and sorts by it
- GET /api/me/matches includes per-player elo deltas
- new GET /api/me/ratings returns the caller's rating per game type
- frontend: Elo column on the leaderboard, per-match delta in the
history page, current rating in the lobby
- python -m tavolo.backfill_elo recomputes all ratings from the
recorded match history (one-off backfill for existing matches)
A game-type registry (server/src/tavolo/games.py) is now the single
source of truth for the games the platform can host; only scopone
scientifico is registered so far. GET /api/game-types exposes it for the
lobby's new game dropdown, and POST /api/games accepts a validated
game_type (default scopone_scientifico) which is carried on the live
GameState and onto each finished Match row (new indexed column,
migration 1_20260916235833_update), so statistics can be scoped per
game: /api/me/matches and /api/leaderboard take an optional game_type
filter and every serialized match includes its game_type.
Game states serialized before this change still load with the default
game type.
The platform now hosts multiple card games, with scopone scientifico as
the first one. Rename the brand wherever it is not a game rule:
- move the Python package to server/src/tavolo and update imports
- rename the Postgres database/user, OIDC issuer path, client id and
Redis key prefixes to tavolo (clean break: existing pgdata volumes and
live games are not migrated)
- rename the Cargo package to tavolo-web and set the page title to Tavolo
- update docs and the Docker image path to woggioni/tavolo
The scopa game term (clearing the table) in the engine, state and web UI
is intentionally left untouched.
Repo is now a monorepo:
- server/: the kaya backend, unchanged in behaviour, plus:
- GET /api/me for SPA session detection
- last_move recorded on every play and broadcast in the game state, so
clients can show who played which card the moment they play it
- legal_moves per hand card for the player on turn (rules stay
server-side)
- static catch-all route serving the compiled SPA with index.html
fallback; Tortoise context now bound only for /api/* requests
- configurable OIDC post-login/logout redirects for dev against trunk
- web/: Sycamore 0.9 + WASM frontend (trunk): login via the OIDC flow,
lobby (create match / join by code), live game page over websocket with
card images (CC0 woodcut napoletane deck), capture picker, move banner,
game-over overlay, match history and leaderboard pages
- server/Dockerfile gains a rust+trunk stage building web/dist; the single
app image serves the SPA; compose builds from the repo root with
overridable ports/OIDC env
Verified end-to-end against the compose stack: four OIDC logins, game
creation, three joins by code, websocket play with broadcasts to all
players and out-of-turn rejection. 51 backend tests, mypy and cargo tests
all green.