CI / Build and push docker image (push) Successful in 1m42s
Add an opt-in bell to the game HUD: the first click asks for the browser's notification permission, and the choice is remembered per device. A play by any other seat then raises a desktop notification with the player's name, the card and the next player, while the page is in the background and the viewer is actually seated in the game. Detect the newest play across the game families: scopone and scopa report it in last_move, while briscola and briscola chiamata only expose the trick, which mid-trick or once completed ends with the card just played. The first snapshot after every (re)connection is a resync baseline, so reconnects and the initial load never replay old moves; so are the viewer's own plays and the server's auto-plays. Deliver through the service worker registration (installed PWAs on Android reject page-context notifications), falling back to a page notification when no worker is active, under a per-game tag that replaces the previous move. sw.js now focuses the game window when a notification is clicked.
152 lines
6.8 KiB
Markdown
152 lines
6.8 KiB
Markdown
# 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](https://github.com/woggioni/kaya)
|
||
framework, OIDC login, live games in Redis, match statistics in Postgres.
|
||
A monorepo of five distributions sharing the `tavolo` namespace: the
|
||
game-independent `tavolo-platform`, the `tavolo-scopone`,
|
||
`tavolo-scopa` and `tavolo-briscola` games, and the `tavolo-app`
|
||
composition root wiring them together. See
|
||
[server/README.md](server/README.md).
|
||
- **`web/`** — frontend: Rust + [Sycamore](https://sycamore.dev) compiled to
|
||
WebAssembly, built with [Trunk](https://trunkrs.dev) and styled with
|
||
[Bootstrap](https://getbootstrap.com) 5.3 (MIT, vendored under
|
||
`web/vendor/` so builds work offline) plus the custom felt theme in
|
||
`web/theme.css`. Also an installable PWA — see [PWA](#pwa). Card images
|
||
are the CC0 *woodcut napoletane* deck traced from a 1902 Naples print
|
||
([SONDLecT/woodcut-napoletane](https://github.com/SONDLecT/woodcut-napoletane)).
|
||
|
||
## Run the whole stack
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
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 a caching-free service worker — it deliberately caches nothing, since
|
||
live state always comes from the server and hashed asset names change every
|
||
build — that also handles clicks on move notifications. 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.
|
||
|
||
## Move notifications
|
||
|
||
Players can opt into desktop notifications for the other players' card
|
||
plays with the bell in the game HUD (the first click asks for the browser
|
||
permission; the preference is stored per device and applies to every
|
||
game). Notifications only appear while the game tab is in the background
|
||
or its window is unfocused — never while the player is watching the table
|
||
— and only for games the viewer is seated in, never for their own plays.
|
||
They are delivered through the service worker (so installed PWAs get them
|
||
too) under a per-game tag, which replaces the previous move instead of
|
||
stacking one notification per play; clicking one focuses the game tab.
|
||
The game page must stay open to receive them, since that is where the
|
||
live-play websocket lives.
|
||
|
||
## Development
|
||
|
||
Backend (from `server/`):
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
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:
|
||
|
||
```sh
|
||
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.
|