Partnership games go to 21: scopone scientifico (the tournament target)
and scopa a quattro, while two-player scopa keeps its usual 11. The scopa
package resolves the default from the player count and exposes a per
variant options_schema default, so the lobby creation form shows the
right value without frontend changes.
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.
A player who accidentally closes the game page can now get back to it
from the lobby.
Backend:
- GameStore.list_ongoing() returns live unfinished sessions (newest
first), skipping expired keys and finished matches;
- GET /api/me/games lists the games the caller is seated in, with the
game name/description, players, open seats and your seat (no join
code);
- store, platform route and app integration tests.
Frontend:
- MyGame/MyGamesPage model plus api::my_games();
- the lobby shows a "Your games" card with phase, seats, players and a
Continue button linking to /game/<id>; hidden when empty or logged
out.
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.
Anonymous users and logged-in non-players can now open /game/<id> and
follow the match: they see the table, the score and each player's
captured cards and scope, but never a hand and never the join code.
Spectator actions are rejected with a forbidden error.
Backend (tavolo-platform / tavolo-scopone):
- /ws/games/{id} accepts anonymous and non-seated connections as
spectators; only unknown games are refused. The state payload carries
spectator: true and withholds join_code unless the viewer is seated.
- GET /api/games/{id} is public and returns the same personalized
(player) or read-only (spectator) view.
- GameEngine.view_for/game_over_view take Optional[str] (None = spectator)
and are documented to hide privileged information for non-seated
viewers.
Frontend (Sycamore):
- Parse the spectator flag and render a spectator table: card backs plus
captured/scope for every seat, no clickable hand, "Spectating" badge.
- A pre-start spectator sees a "game not started yet" waiting room with
the watch link and no join code.
- "Copy watch link" buttons in the waiting room and the game HUD.
- The hand-end summary carries no acknowledge button for spectators and
the game-over overlay hides "My matches".
Tests and docs updated.
GET /api/games/preview returns the lobby behind a code plus the game's
name and engine-echoed options. The lobby debounces the lookup as a
complete code is typed and shows the game, seated players, open seats
and option badges rendered from the game's schema, disabling Join when
the lobby is full or the match has already started.
An ace played when no equal rank is on the table sweeps the whole
table; the sweep never scores a scopa, and an ace already on the table
still forces the mandatory equal-rank capture. The rule is off by
default, selectable per match via the schema-driven creation form, and
announced in the game view.
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.
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.