Files
tavolo/server/packages/tavolo-platform
woggioni a3566f2083
CI / Build and push docker image (push) Successful in 1m22s
Preview a match by join code before joining
GET /api/games/preview returns the lobby behind a code plus the game's
name and engine-echoed options. The lobby debounces the lookup as a
complete code is typed and shows the game, seated players, open seats
and option badges rendered from the game's schema, disabling Join when
the lobby is full or the match has already started.
2026-09-24 09:03:31 +08:00
..

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 opaque state blob), Seat, Deadline (kind + due time + revalidation token) and the MatchResult/PlayerResult outcome 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) and PlatformMixin, 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}) and stats (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) and InMemoryGameStore (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 through engine.fire_deadline under the per-game lock. Engines revalidate the token, so stale or duplicate deliveries are harmless.
  • models.py — Match (game_type, timestamps, game-specific JSON result), MatchPlayer (seat, team, won, points, Elo delta, JSON details) and PlayerRating (Elo per (user_sub, game_type)).
  • stats.py — save_match_result (engine MatchResult → Postgres, transactionally, with Elo) and apply_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.