woggioni abc55479e4
CI / Build and push docker image (push) Successful in 1m10s
Rename the playing-card CSS class away from Bootstrap's .card-img
Bootstrap 5.3 ships .card-img{width:100%} for images inside cards, which
collided with the similarly named class emitted by the card helpers:
every card image stretched to its container width, so the table cards
wrapped one per flex line and pushed the player's hand below the
viewport. Emit 'playing-card' instead and rename the six selectors in
style.css; callers keep passing only the size modifiers.
2026-09-22 08:36:14 +08:00
2026-09-18 19:18:04 +08:00

tavolo

A platform for multiplayer card games. The first game is scopone scientifico — the four-player, fixed-partnership Italian card game:

  • server/ — backend: Python + kaya framework, OIDC login, live games in Redis, match statistics in Postgres. A monorepo of three distributions sharing the tavolo namespace: the game-independent tavolo-platform, the scopone game tavolo-scopone, and the tavolo-app composition 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 under web/vendor/ so builds work offline) plus the custom felt theme in web/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: every player sees how each category was won (carte, denara, settebello, primiera, scope — plus napola when enabled) with 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 (randomizing among the legal captures when the rules require a capture), so one absent player cannot stall the table.

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-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

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_POST_LOGIN_REDIRECT=http://localhost:8000/ \
OIDC_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.

S
Description
Scopone scientifico multiplayer backend built on the kaya framework
Readme
6 MiB
Languages
Python 73.5%
Rust 23.5%
CSS 1.9%
Dockerfile 0.6%
JavaScript 0.2%
Other 0.3%