Files
tavolo/server/packages/tavolo-platform/README.md
T
woggioni 5a73601ddf Split backend into tavolo-platform, tavolo-scopone and tavolo-app packages
Move the game-independent machinery (lobby, live-game store, websocket,
deadline scheduler, match history, leaderboards) into a new
tavolo-platform distribution behind a GameEngine contract, the scopone
scientifico rules plus a platform adapter into tavolo-scopone, and keep
only the composition root in tavolo-app. The three distributions share
the tavolo namespace (PEP 420, kaya-style monorepo).

Match history becomes fully generic: Match carries the engine's result
JSON and MatchPlayer points/details instead of scopone-shaped team
columns (migration 3 backfills existing rows). Lobby creation takes an
opaque per-game options object and websocket actions dispatch to the
session's engine.

Tests: platform suite runs against a DummyEngine toy game, scopone
keeps the rules tests plus new adapter tests, server/tests covers the
wired stack end to end (194 tests, was 143).
2026-09-19 07:28:58 +00:00

3.2 KiB

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.pyGameRegistry: game id → engine lookup, single source of truth for which games exist.
  • mixin.pyPlatform (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.pyDeadlineScheduler: 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.pyMatch (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.pysave_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.