Add a web app manifest and an install-only service worker (no offline caching: live state always comes from the server and hashed asset names change every build), registered from the WASM bundle. The worker must live at the site root for its scope to cover the app, and Granian only serves /static and /assets, so tavolo/static.py answers /manifest.webmanifest and /sw.js from STATIC_DIR with the content types browsers require; index.html gains the manifest link, icons, theme-color and Apple standalone meta tags. Icons are committed under web/assets/icons/ with vector sources and a make-icons.sh regeneration script, mirroring the card art workflow.
6.0 KiB
tavolo
A platform for multiplayer card games. Four games are included: scopone scientifico — the four-player, fixed-partnership Italian card game — scopa, in its classic two-player and four-player partnership (scopa a quattro) variants, with the asso pigliatutto option turning either into scopa d'assi — briscola, in its classic two-player and four-player partnership (briscola a quattro) variants, and briscola chiamata — the five-player variant with a calling auction and a secret partner:
server/— backend: Python + kaya framework, OIDC login, live games in Redis, match statistics in Postgres. A monorepo of five distributions sharing thetavolonamespace: the game-independenttavolo-platform, thetavolo-scopone,tavolo-scopaandtavolo-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. Also an installable PWA — see PWA. 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 and scopa 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. Briscola
chiamata plays a single deal per match, so it goes straight from the last
trick to the final scoring screen; a 60–60 patta is replayed
immediately.
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.
Briscola chiamata arms the same countdown for its auction turns and for
the auction winner's called-card choice: an absent bidder passes, the
last player standing with no bid is made to bid the ace, and a timed-out
call names a random suit of the won rank.
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. In
briscola chiamata they also see no role labels and no side totals until
the called card is played, so the secret partner stays secret. 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.
PWA
The frontend is an installable PWA: web/manifest.webmanifest describes
the app (standalone display, felt theme colors, PNG icons), and web/sw.js
is an install-only service worker — it deliberately caches nothing, since
live state always comes from the server and hashed asset names change every
build. Browsers offer to install Tavolo over HTTPS (or on localhost in
development).
Both files must be served from the site root, or the worker's scope and the
manifest's start_url could not cover the app: Granian only serves
/static and /assets, so tavolo/static.py answers
/manifest.webmanifest and /sw.js (revalidated on every load) from the
same build directory as the shell. Icons are committed under
web/assets/icons/; web/make-icons.sh regenerates them with
rsvg-convert after editing the vector sources.
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-scopa -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-scopa/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. PWA icons are committed under
web/assets/icons/; web/make-icons.sh regenerates them from the SVG
sources.