The creation options were rendered as a keyed list, so a field shared by two games (target_score) kept the previous game's widget when the game changed: after switching scopone -> briscola the input still showed 11 with scopone's min/max instead of the briscola default 61 (1/100 instead of 61/120), even though the submitted payload was already correct. Recreate the option inputs on every game switch instead, so each one is initialized from the new game's defaults and schema constraints.
tavolo
A platform for multiplayer card games. Two games are included: scopone scientifico — the four-player, fixed-partnership Italian card game — and briscola, in its classic two-player and four-player partnership (briscola a quattro) variants:
server/— backend: Python + kaya framework, OIDC login, live games in Redis, match statistics in Postgres. A monorepo of four distributions sharing thetavolonamespace: the game-independenttavolo-platform, thetavolo-scoponeandtavolo-briscolagames, and thetavolo-appcomposition root wiring them together. See server/README.md.web/— frontend: Rust + Sycamore compiled to WebAssembly, built with Trunk and styled with Bootstrap 5.3 (MIT, vendored underweb/vendor/so builds work offline) plus the custom felt theme inweb/theme.css. Card images are the CC0 woodcut napoletane deck traced from a 1902 Naples print (SONDLecT/woodcut-napoletane).
Run the whole stack
docker compose up --build
Postgres, Redis, a mock OIDC provider (test users alice, bob, carol,
dave), the database migrator and the app all come up together. The app —
frontend and API — listens on http://127.0.0.1:8080.
Because both the browser and the app talk to the OIDC issuer at
http://mockoauth:8180/tavolo, add a host entry once:
echo "127.0.0.1 mockoauth" | sudo tee -a /etc/hosts
Between hands
When a hand ends but the match is not decided, the game pauses on a
scoring summary screen: scopone players see how each category was won
(carte, denara, settebello, primiera, scope — plus napola and asso
pigliatutto when enabled), briscola players see the deal's points and any
patta; both see the running totals and must click "Understood" before
the next hand is dealt. If someone is away the next hand is dealt
automatically after HAND_ACK_TIMEOUT_SECONDS (default 30s). The
match-ending hand is explained on the final screen.
On your turn
Every turn shows a countdown (TURN_TIMEOUT_SECONDS, default 30s). If a
player does not move — disconnected or fallen asleep — the server plays a
random legal card for them (choosing among the legal captures when the
rules require a capture), so one absent player cannot stall the table.
Watching
Anyone who has the game URL (/game/<id>) can watch, logged in or not:
spectators see the table, the score and every player's captured cards
(and scope for scopone), but never a hand and never the join code. The
"Copy watch link" button in the waiting room and in the game HUD copies
the URL to share. A spectator who opens the URL before the match starts
sees a "game not started yet" waiting room.
Development
Backend (from server/):
cd server
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/pip install -e ./packages/tavolo-platform -e ./packages/tavolo-scopone -e ./packages/tavolo-briscola -e './packages/tavolo-app[dev]'
.venv/bin/python -m unittest discover -s tests -t .
.venv/bin/python -m unittest discover -s packages/tavolo-platform/tests
.venv/bin/python -m unittest discover -s packages/tavolo-scopone/tests
.venv/bin/python -m unittest discover -s packages/tavolo-briscola/tests
Frontend (from web/), with the backend running on :8080:
cd web
trunk serve # SPA on http://localhost:8000, /api /auth /ws proxied
Trunk proxies /api, /auth and /ws to 127.0.0.1:8080 (see
web/Trunk.toml). For the login redirect to land back on the dev server,
run the backend with:
OIDC_0_PROVIDER=default \
OIDC_0_ISSUER=http://localhost:8180/tavolo \
OIDC_0_CLIENT_ID=tavolo \
OIDC_0_REDIRECT_URI=http://localhost:8080/auth/default/callback \
OIDC_0_POST_LOGIN_REDIRECT=http://localhost:8000/ \
OIDC_0_POST_LOGOUT_REDIRECT=http://localhost:8000/ \
.venv/bin/granian --host 127.0.0.1 --port 8080 tavolo.app:app
Card images are committed under web/assets/cards/; web/fetch-cards.sh
re-downloads them if needed.