CI / Build and push docker image (push) Successful in 3m0s
Anonymous users and logged-in non-players can now open /game/<id> and
follow the match: they see the table, the score and each player's
captured cards and scope, but never a hand and never the join code.
Spectator actions are rejected with a forbidden error.
Backend (tavolo-platform / tavolo-scopone):
- /ws/games/{id} accepts anonymous and non-seated connections as
spectators; only unknown games are refused. The state payload carries
spectator: true and withholds join_code unless the viewer is seated.
- GET /api/games/{id} is public and returns the same personalized
(player) or read-only (spectator) view.
- GameEngine.view_for/game_over_view take Optional[str] (None = spectator)
and are documented to hide privileged information for non-seated
viewers.
Frontend (Sycamore):
- Parse the spectator flag and render a spectator table: card backs plus
captured/scope for every seat, no clickable hand, "Spectating" badge.
- A pre-start spectator sees a "game not started yet" waiting room with
the watch link and no join code.
- "Copy watch link" buttons in the waiting room and the game HUD.
- The hand-end summary carries no acknowledge button for spectators and
the game-over overlay hides "My matches".
Tests and docs updated.
tavolo-platform
The game-independent half of tavolo: lobby, live play, match history and
leaderboards. Everything here works for any game that implements the
GameEngine contract; the package
itself ships no game.
Contents
engine.py— the platform↔game contract:GameEngine(ABC),GameSession(the platform-owned envelope with an opaquestateblob),Seat,Deadline(kind + due time + revalidation token) and theMatchResult/PlayerResultoutcome types.registry.py—GameRegistry: game id → engine lookup, single source of truth for which games exist.mixin.py—Platform(registry + store + scheduler + OIDC, the collaborators every endpoint needs) andPlatformMixin, the kaya mixin that registers all routes and the websocket endpoint on an app.routes/—health(GET /api/health),me(GET /api/me),games(lobby:GET /api/game-types,POST /api/games,POST /api/games/join,GET /api/games/{id}) andstats(GET /api/me/matches,GET /api/leaderboard,GET /api/me/ratings).ws.py— the live-play websocket (/ws/games/{id}): connection lifecycle, the message envelope and the publish/subscribe fan-out. Game-specific actions are dispatched to the session's engine.store.py— live-session persistence:RedisGameStore(production) andInMemoryGameStore(tests/dev) over a JSON envelope plus the engine's opaque state blob, with per-game locks, change signals and the shared deadline queue.deadlines.py—DeadlineScheduler: enqueue the deadline an engine declares after each mutation; a per-loop consumer fires due entries throughengine.fire_deadlineunder the per-game lock. Engines revalidate the token, so stale or duplicate deliveries are harmless.models.py—Match(game_type, timestamps, game-specific JSONresult),MatchPlayer(seat, team, won, points, Elo delta, JSONdetails) andPlayerRating(Elo per(user_sub, game_type)).stats.py—save_match_result(engineMatchResult→ Postgres, transactionally, with Elo) andapply_elo.elo.py— chess-style Elo math generalized to two-team matches.backfill_elo.py— rebuild every rating from the match history:python -m tavolo.platform.backfill_elo --database-url postgres://….auth.py,http.py,pagination.py,openapi.py,tortoise_mixin.py— OIDC helpers, JSON helpers, keyset pagination, shared OpenAPI fragments, the TortoiseORM lifecycle mixin.
Adding a game
Implement GameEngine (see engine.py for the full contract), register
it in a GameRegistry, and mount PlatformMixin(Platform(...)) on a
KayaApp — see the composition root in tavolo.app. The canonical
example is
tavolo-scopone.
Development (from server/)
.venv/bin/python -m unittest discover -s packages/tavolo-platform/tests
.venv/bin/python -m mypy -p tavolo.platform
Tests run fully in-process against a DummyEngine (a two-player toy game
in tests/helpers.py): in-memory sqlite, in-memory stores, a patched
OIDC user and httpx / httpx-ws ASGI transports. No test in this
package may import a real game.