woggioni d9cdba33a1 Add hand-end scoring summary screen with acknowledgement
After each hand of an unfinished match the game now pauses in a new
hand_end phase instead of dealing immediately:

- engine: hand_points gains an 'award' map (which team won each category),
  _end_hand stops at hand_end with a deadline, new acknowledge_hand deals
  the next hand once all four players have acked; plays are rejected while
  the summary is up
- state: acked seats, hand_end_deadline and hand_ack_timeout are persisted
  and exposed in the personalized view (also on the finished state, so the
  final hand is explained before the result)
- ws: new {"action": "ack"}; a per-hand timer force-deals the next hand
  after HAND_ACK_TIMEOUT_SECONDS (new env var, default 30s) so an away
  player cannot stall the match
- web: modal explaining each category in plain language with icons (card
  images for denara/settebello/primiera), team-coloured rows, running
  totals with progress bars, an 'Understood — next hand' button that turns
  into 'Waiting for …' plus an auto-continue countdown; the final screen
  shows the last hand's breakdown too

Verified in the browser against the compose stack: hand played to
completion, summary rendered (including a carte tie), ack from all four
players dealt the next hand live, and the auto-continue path fired when
nobody acked. 60 backend tests + mypy + cargo tests green.
2026-09-16 21:46:01 +08:00

scopa

Multiplayer scopone scientifico — the four-player, fixed-partnership Italian card game — as a web application:

  • server/ — backend: Python + kaya framework, OIDC login, live games in Redis, match statistics in Postgres. See server/README.md.
  • web/ — frontend: Rust + Sycamore compiled to WebAssembly, built with Trunk. 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/scopa, 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) 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.

Development

Backend (from server/):

cd server
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/pip install -e '.[dev]'
.venv/bin/python -m unittest discover -s tests -t .
.venv/bin/mypy src

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 scopa.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
4 MiB
Languages
Python 72.7%
Rust 21.3%
CSS 3.8%
Dockerfile 1.7%
Shell 0.4%
Other 0.1%