diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index a014738..11190c8 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -23,7 +23,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: gitea.woggioni.net/woggioni/scopa + images: gitea.woggioni.net/woggioni/tavolo flavor: latest=false tags: | type=match,pattern=release/(.*),group=1 diff --git a/README.md b/README.md index 154714b..e6d6ca0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# scopa +# tavolo -Multiplayer **scopone scientifico** — the four-player, fixed-partnership -Italian card game — as a web application: +A platform for multiplayer card games. The first game is **scopone +scientifico** — the four-player, fixed-partnership Italian card game: - **`server/`** — backend: Python + [kaya](https://github.com/woggioni/kaya) framework, OIDC login, live games in Redis, match statistics in Postgres. @@ -22,7 +22,7 @@ Postgres, Redis, a mock OIDC provider (test users `alice`, `bob`, `carol`, 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: +`http://mockoauth:8180/tavolo`, add a host entry once: ```sh echo "127.0.0.1 mockoauth" | sudo tee -a /etc/hosts @@ -71,7 +71,7 @@ run the backend with: ```sh 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 + .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` diff --git a/docker-compose.yml b/docker-compose.yml index e830ee6..4f3aaf5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,15 +2,15 @@ services: postgres: image: postgres:18-alpine environment: - POSTGRES_DB: scopa - POSTGRES_USER: scopa - POSTGRES_PASSWORD: scopa + POSTGRES_DB: tavolo + POSTGRES_USER: tavolo + POSTGRES_PASSWORD: tavolo ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql healthcheck: - test: ["CMD-SHELL", "pg_isready -U scopa"] + test: ["CMD-SHELL", "pg_isready -U tavolo"] interval: 5s timeout: 3s retries: 10 @@ -21,7 +21,7 @@ services: # The mock does not validate clients, so any client id/secret works. # # It listens on 8180 both inside and outside the network so the OIDC - # issuer URL (http://mockoauth:8180/scopa) is identical for + # issuer URL (http://mockoauth:8180/tavolo) is identical for # container-to-container calls and for browser redirects (via the # /etc/hosts entry documented in the README). mockoauth: @@ -43,7 +43,7 @@ services: - mockoauth entrypoint: > /bin/sh -c " - until curl -sf http://mockoauth:8180/scopa/.well-known/openid-configuration > /dev/null; do + until curl -sf http://mockoauth:8180/tavolo/.well-known/openid-configuration > /dev/null; do echo 'waiting for mockoauth...'; sleep 2; done " @@ -71,12 +71,12 @@ services: working_dir: /app command: ["aerich", "upgrade"] environment: - DATABASE_URL: postgres://scopa:scopa@postgres:5432/scopa + DATABASE_URL: postgres://tavolo:tavolo@postgres:5432/tavolo depends_on: postgres: condition: service_healthy - scopa: + tavolo: build: context: . dockerfile: server/Dockerfile @@ -90,13 +90,13 @@ services: redis: condition: service_healthy environment: - DATABASE_URL: postgres://scopa:scopa@postgres:5432/scopa + DATABASE_URL: postgres://tavolo:tavolo@postgres:5432/tavolo # By default the app and browsers reach the mock IdP under the same # name (see README /etc/hosts note); override OIDC_ISSUER and # OIDC_REDIRECT_URI to use a real provider or a different host port. - OIDC_ISSUER: ${OIDC_ISSUER:-http://mockoauth:8180/scopa} + OIDC_ISSUER: ${OIDC_ISSUER:-http://mockoauth:8180/tavolo} # The mock OIDC server does not validate clients: any id/secret works. - OIDC_CLIENT_ID: scopa + OIDC_CLIENT_ID: tavolo OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET:-dev-secret} OIDC_REDIRECT_URI: ${OIDC_REDIRECT_URI:-http://localhost:8080/auth/callback} REDIS_URL: redis://redis:6379/0 diff --git a/server/.env.example b/server/.env.example index f9a5bf6..f98fd9d 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,15 +1,15 @@ # Postgres POSTGRES_HOST=localhost POSTGRES_PORT=5432 -POSTGRES_DB=scopa -POSTGRES_USER=scopa -POSTGRES_PASSWORD=scopa -DATABASE_URL=postgres://scopa:scopa@localhost:5432/scopa +POSTGRES_DB=tavolo +POSTGRES_USER=tavolo +POSTGRES_PASSWORD=tavolo +DATABASE_URL=postgres://tavolo:tavolo@localhost:5432/tavolo # OIDC (mock-oauth2-server in dev; it does not validate clients, so any # client id/secret works. For a real IdP like Keycloak, use its values here.) -OIDC_ISSUER=http://localhost:8180/scopa -OIDC_CLIENT_ID=scopa +OIDC_ISSUER=http://localhost:8180/tavolo +OIDC_CLIENT_ID=tavolo OIDC_CLIENT_SECRET=dev-secret OIDC_REDIRECT_URI=http://localhost:8080/auth/callback diff --git a/server/Dockerfile b/server/Dockerfile index 636d98a..be2bd3b 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -# Multi-stage build for the scopa stack (kaya backend + Sycamore/WASM +# Multi-stage build for the tavolo stack (kaya backend + Sycamore/WASM # frontend). The Docker build context is the REPOSITORY ROOT (see # docker-compose.yml) so this single image assembles both parts: # @@ -86,4 +86,4 @@ EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \ CMD wget -q -O- http://127.0.0.1:8080/api/health || exit 1 -CMD ["granian", "scopa.app:app"] +CMD ["granian", "tavolo.app:app"] diff --git a/server/README.md b/server/README.md index 21d13d8..e58cc27 100644 --- a/server/README.md +++ b/server/README.md @@ -1,8 +1,9 @@ -# scopa +# tavolo -A multiplayer backend for **scopone scientifico** (the four-player, -fixed-partnership variant of the classic Italian card game), built on the -[kaya](../kaya) framework. +The backend for a multiplayer card-game platform, built on the +[kaya](../kaya) framework. The first game implemented is **scopone +scientifico**, the four-player, fixed-partnership variant of the classic +Italian card game. Players authenticate with the configured **OIDC** provider. Live game state is kept in **Redis** (with real-time play over WebSocket), and completed @@ -24,7 +25,7 @@ serves the Sycamore/WASM frontend (built from `../web/` by the Docker image), so the UI is available at that address. Because the browser and the app both talk to the OIDC issuer at -`http://mockoauth:8180/scopa`, add a host entry once: +`http://mockoauth:8180/tavolo`, add a host entry once: ```sh echo "127.0.0.1 mockoauth" | sudo tee -a /etc/hosts @@ -43,10 +44,10 @@ All configuration comes from environment variables (see `.env.example`): | Variable | Default | Description | |---|---|---| -| `DATABASE_URL` | `postgres://scopa:scopa@localhost:5432/scopa` | Postgres DSN for match statistics | +| `DATABASE_URL` | `postgres://tavolo:tavolo@localhost:5432/tavolo` | Postgres DSN for match statistics | | `REDIS_URL` | unset | Redis DSN for sessions + live games. Unset falls back to in-memory stores | -| `OIDC_ISSUER` | `http://localhost:8180/scopa` | OIDC issuer URL | -| `OIDC_CLIENT_ID` | `scopa` | OIDC client id | +| `OIDC_ISSUER` | `http://localhost:8180/tavolo` | OIDC issuer URL | +| `OIDC_CLIENT_ID` | `tavolo` | OIDC client id | | `OIDC_CLIENT_SECRET` | unset | OIDC client secret | | `OIDC_REDIRECT_URI` | `http://localhost:8080/auth/callback` | Login callback URL | | `GAME_TTL_SECONDS` | `86400` | Sliding TTL of a live game in Redis | @@ -58,13 +59,13 @@ All configuration comes from environment variables (see `.env.example`): ### Redis (live games) -- `scopa:game:` — the whole match as JSON: players (seat 0/2 = team A, +- `tavolo:game:` — the whole match as JSON: players (seat 0/2 = team A, 1/3 = team B), hands, table, captured piles, scope, current turn, dealer, scores, phase (`lobby` → `playing` → `finished`). Sliding TTL (`GAME_TTL_SECONDS`). -- `scopa:code:` — the 6-character join code → game id index. -- `scopa:game::lock` — a short-lived lock serializing every mutation. -- `scopa:game::events` — a pub/sub channel carrying "state changed" +- `tavolo:code:` — the 6-character join code → game id index. +- `tavolo:game::lock` — a short-lived lock serializing every mutation. +- `tavolo:game::events` — a pub/sub channel carrying "state changed" signals; every open WebSocket reloads the state and pushes the personalized view to its player. @@ -178,11 +179,11 @@ for Redis, a fake OIDC user patched onto the mixins, and `httpx` / The Postgres schema is owned by aerich migrations in `migrations/`. The `db-migrate` compose service runs `aerich upgrade` before the app starts. -To add a migration after changing `src/scopa/models.py`: +To add a migration after changing `src/tavolo/models.py`: ```sh -DATABASE_URL=postgres://scopa:scopa@localhost:5432/scopa .venv/bin/aerich migrate -DATABASE_URL=postgres://scopa:scopa@localhost:5432/scopa .venv/bin/aerich upgrade +DATABASE_URL=postgres://tavolo:tavolo@localhost:5432/tavolo .venv/bin/aerich migrate +DATABASE_URL=postgres://tavolo:tavolo@localhost:5432/tavolo .venv/bin/aerich upgrade ``` (`aerich init-db` produces sqlite-flavored DDL when pointed at sqlite; @@ -194,7 +195,7 @@ baseline.) Everything lives under `server/`: ``` -src/scopa/ +src/tavolo/ ├── app.py # composition root: session/OIDC/Tortoise/OpenAPI mixins ├── config.py # env -> frozen Settings ├── auth.py # auth helpers (HTTP + WebSocket) diff --git a/server/conftest.py b/server/conftest.py index 7a064ff..a85c7bd 100644 --- a/server/conftest.py +++ b/server/conftest.py @@ -3,14 +3,14 @@ Tests run against an in-memory sqlite database (overriding ``DATABASE_URL``) so they need no running Postgres, and with ``REDIS_URL`` unset so sessions and live games use their in-memory stores. The environment is set before -:mod:`scopa.app` is imported by the test modules. +:mod:`tavolo.app` is imported by the test modules. """ from __future__ import annotations import os os.environ.setdefault("DATABASE_URL", "sqlite://:memory:") -os.environ.setdefault("OIDC_ISSUER", "http://localhost:8180/scopa") -os.environ.setdefault("OIDC_CLIENT_ID", "scopa") +os.environ.setdefault("OIDC_ISSUER", "http://localhost:8180/tavolo") +os.environ.setdefault("OIDC_CLIENT_ID", "tavolo") os.environ.setdefault("OIDC_REDIRECT_URI", "http://localhost:8080/auth/callback") os.environ.pop("REDIS_URL", None) diff --git a/server/dev/mockoauth/Dockerfile b/server/dev/mockoauth/Dockerfile index 3b8b9be..fa1b9ea 100644 --- a/server/dev/mockoauth/Dockerfile +++ b/server/dev/mockoauth/Dockerfile @@ -1,4 +1,4 @@ -# Dev OIDC provider: navikt/mock-oauth2-server with the scopa test +# Dev OIDC provider: navikt/mock-oauth2-server with the tavolo test # configuration (four ready-made players) baked in. The config is # COPYed instead of bind-mounted so this also works against containerized # (e.g. rootless/DinD) docker daemons that cannot see the host workspace. diff --git a/server/dev/mockoauth/config.json b/server/dev/mockoauth/config.json index c0141b4..c9908d3 100644 --- a/server/dev/mockoauth/config.json +++ b/server/dev/mockoauth/config.json @@ -2,7 +2,7 @@ "interactiveLogin": true, "tokenCallbacks": [ { - "issuerId": "scopa", + "issuerId": "tavolo", "tokenExpiry": 3600, "requestMappings": [ { diff --git a/server/pyproject.toml b/server/pyproject.toml index 998a1b1..b2845ca 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -3,9 +3,9 @@ requires = ["setuptools>=68"] build-backend = "setuptools.build_meta" [project] -name = "scopa" +name = "tavolo" version = "0.1.0" -description = "Scopone scientifico multiplayer backend built on the kaya framework" +description = "Multiplayer card-game platform backend built on the kaya framework" readme = "README.md" requires-python = ">=3.10" dependencies = [ @@ -37,7 +37,7 @@ namespaces = false # Database migrations (aerich). See the Migrations section in README.md. [tool.aerich] -tortoise_orm = "scopa.aerich_config.TORTOISE_ORM" +tortoise_orm = "tavolo.aerich_config.TORTOISE_ORM" location = "./migrations" [tool.mypy] @@ -49,13 +49,13 @@ plugins = [] # runtime; without the (unavailable here) tortoise mypy plugin the stubs # only declare the relation field. These are real attributes, not bugs. [[tool.mypy.overrides]] -module = "scopa.models" +module = "tavolo.models" disable_error_code = ["attr-defined"] [[tool.mypy.overrides]] -module = "scopa.routes.*" +module = "tavolo.routes.*" disable_error_code = ["attr-defined"] [[tool.mypy.overrides]] -module = "scopa.game.*" +module = "tavolo.game.*" disable_error_code = ["attr-defined"] diff --git a/server/requirements.txt b/server/requirements.txt index 4094efd..bf6b4ab 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -8,7 +8,7 @@ --extra-index-url https://pypi.org/simple aerich==0.10.1 - # via scopa (pyproject.toml) + # via tavolo (pyproject.toml) aiosqlite==0.22.1 # via tortoise-orm anyio==4.15.1 @@ -19,7 +19,7 @@ anyio==4.15.1 asyncclick==8.4.2.1 # via aerich asyncpg==0.31.0 - # via scopa (pyproject.toml) + # via tavolo (pyproject.toml) certifi==2026.7.22 # via # httpcore @@ -35,7 +35,7 @@ dictdiffer==0.10.0 granian==2.8.3 # via # kaya-rsgi - # scopa (pyproject.toml) + # tavolo (pyproject.toml) h11==0.16.0 # via httpcore httpcore==1.0.9 @@ -43,7 +43,7 @@ httpcore==1.0.9 httpx==0.28.1 # via # kaya-oidc - # scopa (pyproject.toml) + # tavolo (pyproject.toml) idna==3.19 # via # anyio @@ -56,42 +56,42 @@ kaya-core==0.0.3 # kaya-openapi # kaya-rsgi # kaya-session - # scopa (pyproject.toml) + # tavolo (pyproject.toml) kaya-oidc==0.0.3 - # via scopa (pyproject.toml) + # via tavolo (pyproject.toml) kaya-openapi==0.0.3 - # via scopa (pyproject.toml) + # via tavolo (pyproject.toml) kaya-rsgi==0.0.3 - # via scopa (pyproject.toml) + # via tavolo (pyproject.toml) kaya-session==0.0.3 # via # kaya-oidc # kaya-session-redis - # scopa (pyproject.toml) + # tavolo (pyproject.toml) kaya-session-redis==0.0.3 - # via scopa (pyproject.toml) + # via tavolo (pyproject.toml) pwo==0.1.2 # via # kaya-core # kaya-rsgi # kaya-session - # scopa (pyproject.toml) + # tavolo (pyproject.toml) pycparser==3.0 # via cffi pyjwt[crypto]==2.14.0 # via # kaya-oidc - # scopa (pyproject.toml) + # tavolo (pyproject.toml) pypika-tortoise==0.6.5 # via tortoise-orm redis==8.1.0 # via # kaya-session-redis - # scopa (pyproject.toml) + # tavolo (pyproject.toml) tortoise-orm==1.1.8 # via # aerich - # scopa (pyproject.toml) + # tavolo (pyproject.toml) typing-extensions==4.16.0 # via # anyio diff --git a/server/src/scopa/__init__.py b/server/src/tavolo/__init__.py similarity index 100% rename from server/src/scopa/__init__.py rename to server/src/tavolo/__init__.py diff --git a/server/src/scopa/aerich_config.py b/server/src/tavolo/aerich_config.py similarity index 72% rename from server/src/scopa/aerich_config.py rename to server/src/tavolo/aerich_config.py index 9979eb7..3845776 100644 --- a/server/src/scopa/aerich_config.py +++ b/server/src/tavolo/aerich_config.py @@ -1,8 +1,8 @@ """Tortoise ORM configuration consumed by the aerich CLI. -Kept separate from :mod:`scopa.app` so ``aerich`` can import it without +Kept separate from :mod:`tavolo.app` so ``aerich`` can import it without assembling the whole application (mixins, routes). The database URL comes -from the same :class:`~scopa.config.Settings` the app uses, so the CLI +from the same :class:`~tavolo.config.Settings` the app uses, so the CLI and the app always point at the same database. ``aerich.models`` is required alongside the app models: it provides the @@ -16,7 +16,7 @@ TORTOISE_ORM = { "connections": {"default": settings.database_url}, "apps": { "models": { - "models": ["scopa.models", "aerich.models"], + "models": ["tavolo.models", "aerich.models"], "default_connection": "default", } }, diff --git a/server/src/scopa/app.py b/server/src/tavolo/app.py similarity index 94% rename from server/src/scopa/app.py rename to server/src/tavolo/app.py index bfa9398..154e691 100644 --- a/server/src/scopa/app.py +++ b/server/src/tavolo/app.py @@ -6,7 +6,7 @@ Assembles the :class:`~kaya.core.KayaApp` with four mixins: :class:`~kaya.session.redis.RedisSessionStore` when ``REDIS_URL`` is set, otherwise an in-memory store — e.g. for tests) - :class:`~kaya.oidc.OIDCMixin` (OIDC login) -- :class:`~scopa.tortoise_mixin.TortoiseMixin` (Postgres match statistics; +- :class:`~tavolo.tortoise_mixin.TortoiseMixin` (Postgres match statistics; skipped for ``/api/health`` and the OpenAPI documentation endpoints) - :class:`~kaya.openapi.OpenAPIMixin` (serves the OpenAPI document at ``/api/openapi.json`` and a Swagger UI at ``/api/docs``) @@ -57,15 +57,15 @@ oidc_mixin = OIDCMixin( session=session_mixin, ) openapi_mixin = OpenAPIMixin( - title="scopa", - version=_pkg_version("scopa"), + title="tavolo", + version=_pkg_version("tavolo"), description="Scopone scientifico multiplayer API", spec_path="/api/openapi.json", docs_path="/api/docs", ) tortoise_mixin = TortoiseMixin( database_url=settings.database_url, - models_modules=["scopa.models"], + models_modules=["tavolo.models"], skip_paths=frozenset({"/api/health", "/api/docs", "/api/openapi.json"}), ) diff --git a/server/src/scopa/auth.py b/server/src/tavolo/auth.py similarity index 96% rename from server/src/scopa/auth.py rename to server/src/tavolo/auth.py index 72fff96..85479c8 100644 --- a/server/src/scopa/auth.py +++ b/server/src/tavolo/auth.py @@ -1,6 +1,6 @@ """Authentication helpers on top of the kaya-oidc mixin. -Scopa has no application roles: every authenticated user may create and +Tavolo has no application roles: every authenticated user may create and join games. Authorization beyond login is game membership, checked against the live game state in Redis. """ diff --git a/server/src/scopa/config.py b/server/src/tavolo/config.py similarity index 92% rename from server/src/scopa/config.py rename to server/src/tavolo/config.py index 7ae8e36..921d616 100644 --- a/server/src/scopa/config.py +++ b/server/src/tavolo/config.py @@ -1,4 +1,4 @@ -"""Environment-driven configuration for the scopa application. +"""Environment-driven configuration for the tavolo application. Mirrors kaya's own pattern: read ``os.environ`` directly into a plain dataclass. No pydantic-settings, no settings module. @@ -50,9 +50,9 @@ class Settings: @staticmethod def from_env() -> "Settings": return Settings( - database_url=_env("DATABASE_URL", "postgres://scopa:scopa@localhost:5432/scopa"), - oidc_issuer=_env("OIDC_ISSUER", "http://localhost:8180/scopa"), - oidc_client_id=_env("OIDC_CLIENT_ID", "scopa"), + database_url=_env("DATABASE_URL", "postgres://tavolo:tavolo@localhost:5432/tavolo"), + oidc_issuer=_env("OIDC_ISSUER", "http://localhost:8180/tavolo"), + oidc_client_id=_env("OIDC_CLIENT_ID", "tavolo"), oidc_client_secret=os.environ.get("OIDC_CLIENT_SECRET"), oidc_redirect_uri=_env("OIDC_REDIRECT_URI", "http://localhost:8080/auth/callback"), oidc_post_login_redirect=_env("OIDC_POST_LOGIN_REDIRECT", "/"), diff --git a/server/src/scopa/game/__init__.py b/server/src/tavolo/game/__init__.py similarity index 100% rename from server/src/scopa/game/__init__.py rename to server/src/tavolo/game/__init__.py diff --git a/server/src/scopa/game/engine.py b/server/src/tavolo/game/engine.py similarity index 98% rename from server/src/scopa/game/engine.py rename to server/src/tavolo/game/engine.py index 3e2aaeb..edfe29d 100644 --- a/server/src/scopa/game/engine.py +++ b/server/src/tavolo/game/engine.py @@ -1,8 +1,8 @@ """Pure rules engine for scopone scientifico. Every function here is deterministic and I/O-free: it mutates (or reads) -:class:`~scopa.game.state.GameState` and raises -:class:`~scopa.game.errors.GameError` subclasses on rule violations. This +:class:`~tavolo.game.state.GameState` and raises +:class:`~tavolo.game.errors.GameError` subclasses on rule violations. This makes the whole rule set unit-testable without Redis, Postgres or HTTP. Rules implemented @@ -210,7 +210,7 @@ def play( ``capture_codes`` selects which table cards to capture; it must be a legal capture (see :func:`legal_captures`) when one exists and empty - otherwise. Raises a :class:`~scopa.game.errors.GameError` subclass on + otherwise. Raises a :class:`~tavolo.game.errors.GameError` subclass on any violation. """ if state.phase == PHASE_FINISHED: @@ -450,7 +450,7 @@ def state_for_player(state: GameState, sub: str) -> Dict[str, object]: """Serialize ``state`` hiding other players' hands. Hands are reduced to a count, except for the requesting player's own - hand. Raises :class:`~scopa.game.errors.GameNotFound`-style access via + hand. Raises :class:`~tavolo.game.errors.GameNotFound`-style access via the caller; this function assumes ``sub`` may or may not be seated and simply omits the hand for non-seated viewers. """ diff --git a/server/src/scopa/game/errors.py b/server/src/tavolo/game/errors.py similarity index 100% rename from server/src/scopa/game/errors.py rename to server/src/tavolo/game/errors.py diff --git a/server/src/scopa/game/state.py b/server/src/tavolo/game/state.py similarity index 99% rename from server/src/scopa/game/state.py rename to server/src/tavolo/game/state.py index bc67988..50d96cd 100644 --- a/server/src/scopa/game/state.py +++ b/server/src/tavolo/game/state.py @@ -1,7 +1,7 @@ """In-memory representation of a scopone scientifico game. The whole mutable game lives in :class:`GameState`, which is serialized to -and from plain JSON for storage in Redis (see :mod:`scopa.store`). Keeping +and from plain JSON for storage in Redis (see :mod:`tavolo.store`). Keeping the representation JSON-native means the store needs no custom codecs and the state is inspectable with ``redis-cli``. diff --git a/server/src/scopa/http.py b/server/src/tavolo/http.py similarity index 100% rename from server/src/scopa/http.py rename to server/src/tavolo/http.py diff --git a/server/src/scopa/models.py b/server/src/tavolo/models.py similarity index 95% rename from server/src/scopa/models.py rename to server/src/tavolo/models.py index 5085c11..d628a8b 100644 --- a/server/src/scopa/models.py +++ b/server/src/tavolo/models.py @@ -1,6 +1,6 @@ """Tortoise ORM models: match statistics persisted in Postgres. -Live game state lives in Redis (see :mod:`scopa.store`); only completed +Live game state lives in Redis (see :mod:`tavolo.store`); only completed matches are written here. The two tables answer the question "every match a player took part in, with the final score": diff --git a/server/src/scopa/openapi.py b/server/src/tavolo/openapi.py similarity index 100% rename from server/src/scopa/openapi.py rename to server/src/tavolo/openapi.py diff --git a/server/src/scopa/pagination.py b/server/src/tavolo/pagination.py similarity index 100% rename from server/src/scopa/pagination.py rename to server/src/tavolo/pagination.py diff --git a/server/src/scopa/routes/__init__.py b/server/src/tavolo/routes/__init__.py similarity index 100% rename from server/src/scopa/routes/__init__.py rename to server/src/tavolo/routes/__init__.py diff --git a/server/src/scopa/routes/games.py b/server/src/tavolo/routes/games.py similarity index 98% rename from server/src/scopa/routes/games.py rename to server/src/tavolo/routes/games.py index 0a7de88..e01ed04 100644 --- a/server/src/scopa/routes/games.py +++ b/server/src/tavolo/routes/games.py @@ -3,7 +3,7 @@ A game starts as a lobby: the creator is seated first and shares the six-character ``join_code``. When the fourth player joins, the engine deals the first hand and the match begins. Live play then happens over the -``/ws/games/{id}`` websocket (see :mod:`scopa.ws`); these endpoints cover +``/ws/games/{id}`` websocket (see :mod:`tavolo.ws`); these endpoints cover creation, joining and snapshotting state. """ from __future__ import annotations diff --git a/server/src/scopa/routes/health.py b/server/src/tavolo/routes/health.py similarity index 100% rename from server/src/scopa/routes/health.py rename to server/src/tavolo/routes/health.py diff --git a/server/src/scopa/routes/me.py b/server/src/tavolo/routes/me.py similarity index 100% rename from server/src/scopa/routes/me.py rename to server/src/tavolo/routes/me.py diff --git a/server/src/scopa/routes/static.py b/server/src/tavolo/routes/static.py similarity index 100% rename from server/src/scopa/routes/static.py rename to server/src/tavolo/routes/static.py diff --git a/server/src/scopa/routes/stats.py b/server/src/tavolo/routes/stats.py similarity index 98% rename from server/src/scopa/routes/stats.py rename to server/src/tavolo/routes/stats.py index f88025e..842d55d 100644 --- a/server/src/scopa/routes/stats.py +++ b/server/src/tavolo/routes/stats.py @@ -1,6 +1,6 @@ """Player statistics endpoints, served from Postgres. -Every finished match is persisted by :func:`scopa.stats.save_match_result`. +Every finished match is persisted by :func:`tavolo.stats.save_match_result`. These endpoints expose a player's own match history and a global leaderboard aggregated from the same two tables. """ diff --git a/server/src/scopa/stats.py b/server/src/tavolo/stats.py similarity index 100% rename from server/src/scopa/stats.py rename to server/src/tavolo/stats.py diff --git a/server/src/scopa/store.py b/server/src/tavolo/store.py similarity index 96% rename from server/src/scopa/store.py rename to server/src/tavolo/store.py index e94ad86..74de27c 100644 --- a/server/src/scopa/store.py +++ b/server/src/tavolo/store.py @@ -1,10 +1,10 @@ """Persistence for live games. Game state is small, mutable and short-lived, which makes Redis a natural -fit: the whole match is a single JSON value under ``scopa:game:`` with +fit: the whole match is a single JSON value under ``tavolo:game:`` with a sliding TTL, and a join-code index maps the short code a player shares to that id. Completed matches are copied to Postgres (see -:mod:`scopa.models`); Redis keeps serving the finished state until it +:mod:`tavolo.models`); Redis keeps serving the finished state until it expires. Two implementations satisfy the same interface: @@ -31,9 +31,9 @@ from redis.asyncio import Redis from .game.state import GameState -GAME_KEY_PREFIX = "scopa:game:" -CODE_KEY_PREFIX = "scopa:code:" -CHANNEL_PREFIX = "scopa:game:" +GAME_KEY_PREFIX = "tavolo:game:" +CODE_KEY_PREFIX = "tavolo:code:" +CHANNEL_PREFIX = "tavolo:game:" # Sentinel pushed into in-memory subscriber queues to signal a change. _BUMP = b"update" diff --git a/server/src/scopa/tortoise_mixin.py b/server/src/tavolo/tortoise_mixin.py similarity index 100% rename from server/src/scopa/tortoise_mixin.py rename to server/src/tavolo/tortoise_mixin.py diff --git a/server/src/scopa/ws.py b/server/src/tavolo/ws.py similarity index 99% rename from server/src/scopa/ws.py rename to server/src/tavolo/ws.py index d6aa2fc..69f4e7c 100644 --- a/server/src/scopa/ws.py +++ b/server/src/tavolo/ws.py @@ -20,7 +20,7 @@ Client -> server messages are JSON objects:: {"action": "state"} ``capture`` lists the table cards to take and must be a legal capture when -one exists (see :func:`scopa.game.engine.legal_captures`); it is omitted +one exists (see :func:`tavolo.game.engine.legal_captures`); it is omitted when the played card cannot capture. ``ack`` acknowledges the hand-end scoring summary; the next hand is dealt when all four players have acknowledged or the timeout fires. diff --git a/server/tests/__init__.py b/server/tests/__init__.py index da1935b..99fac83 100644 --- a/server/tests/__init__.py +++ b/server/tests/__init__.py @@ -1,7 +1,7 @@ """Test package init. Sets environment overrides BEFORE any test module imports -:mod:`scopa.app` (which evaluates :data:`scopa.config.settings` +:mod:`tavolo.app` (which evaluates :data:`tavolo.config.settings` at import time). Works under both ``python -m unittest discover`` and ``pytest``; conftest.py mirrors this for pytest-only collection. """ @@ -10,8 +10,8 @@ from __future__ import annotations import os os.environ.setdefault("DATABASE_URL", "sqlite://:memory:") -os.environ.setdefault("OIDC_ISSUER", "http://localhost:8180/scopa") -os.environ.setdefault("OIDC_CLIENT_ID", "scopa") +os.environ.setdefault("OIDC_ISSUER", "http://localhost:8180/tavolo") +os.environ.setdefault("OIDC_CLIENT_ID", "tavolo") os.environ.setdefault("OIDC_REDIRECT_URI", "http://localhost:8080/auth/callback") # Unset REDIS_URL: sessions and live games use the in-memory fallbacks. os.environ.pop("REDIS_URL", None) diff --git a/server/tests/helpers/oidc.py b/server/tests/helpers/oidc.py index 5b5c937..0bcb031 100644 --- a/server/tests/helpers/oidc.py +++ b/server/tests/helpers/oidc.py @@ -1,7 +1,7 @@ """Helpers for faking the OIDC authenticated user during tests. HTTP handlers funnel through ``oidc_mixin.get_user``; websocket handlers -through :func:`scopa.auth.get_ws_user`. Patching those two entry points +through :func:`tavolo.auth.get_ws_user`. Patching those two entry points lets route and websocket tests run entirely in-process with no IdP. """ from __future__ import annotations @@ -13,10 +13,10 @@ from typing import Iterator, Optional, Sequence from kaya.oidc import OIDCUser # Import the app first: it pulls in the route modules, which import -# ``scopa.auth`` themselves. Importing ``auth`` before ``app`` would hit a +# ``tavolo.auth`` themselves. Importing ``auth`` before ``app`` would hit a # partially initialized module (same constraint as reimpasto). -from scopa.app import oidc_mixin -from scopa import auth +from tavolo.app import oidc_mixin +from tavolo import auth def make_user(sub: str, name: Optional[str] = None) -> OIDCUser: diff --git a/server/tests/test_engine.py b/server/tests/test_engine.py index 7bf5a6e..85c5c27 100644 --- a/server/tests/test_engine.py +++ b/server/tests/test_engine.py @@ -4,15 +4,15 @@ from __future__ import annotations import random import unittest -from scopa.game import engine -from scopa.game.errors import ( +from tavolo.game import engine +from tavolo.game.errors import ( CardNotInHand, GameFinished, GameNotStarted, IllegalMove, NotYourTurn, ) -from scopa.game.state import ( +from tavolo.game.state import ( PHASE_FINISHED, PHASE_PLAYING, Card, diff --git a/server/tests/test_routes_games.py b/server/tests/test_routes_games.py index ae2f78a..08c380a 100644 --- a/server/tests/test_routes_games.py +++ b/server/tests/test_routes_games.py @@ -6,7 +6,7 @@ import unittest from httpx import ASGITransport, AsyncClient from pwo import async_test -from scopa.app import app +from tavolo.app import app from tests.helpers import oidc_user diff --git a/server/tests/test_routes_me.py b/server/tests/test_routes_me.py index 29530de..d0e39e3 100644 --- a/server/tests/test_routes_me.py +++ b/server/tests/test_routes_me.py @@ -10,8 +10,8 @@ from unittest import mock from httpx import ASGITransport, AsyncClient from pwo import async_test -from scopa.app import app -from scopa.config import settings +from tavolo.app import app +from tavolo.config import settings from tests.helpers import oidc_user @@ -45,7 +45,7 @@ class StaticRouteTest(unittest.TestCase): (cards / "07D.svg").write_text("") patched = dataclasses.replace(settings, static_dir=dist) - with mock.patch("scopa.routes.static.settings", patched): + with mock.patch("tavolo.routes.static.settings", patched): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client: index = await client.get("/") @@ -74,7 +74,7 @@ class StaticRouteTest(unittest.TestCase): @async_test async def test_missing_dist_returns_404(self) -> None: patched = dataclasses.replace(settings, static_dir="/nonexistent-dist") - with mock.patch("scopa.routes.static.settings", patched): + with mock.patch("tavolo.routes.static.settings", patched): transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://127.0.0.1") as client: response = await client.get("/") diff --git a/server/tests/test_stats.py b/server/tests/test_stats.py index 09209b1..af4b41f 100644 --- a/server/tests/test_stats.py +++ b/server/tests/test_stats.py @@ -8,11 +8,11 @@ from datetime import datetime, timezone from httpx import ASGITransport, AsyncClient from pwo import async_test -from scopa.app import app, tortoise_mixin -from scopa.game import engine -from scopa.game.state import GameState -from scopa.models import Match, MatchPlayer -from scopa.stats import save_match_result +from tavolo.app import app, tortoise_mixin +from tavolo.game import engine +from tavolo.game.state import GameState +from tavolo.models import Match, MatchPlayer +from tavolo.stats import save_match_result from tests.helpers import oidc_user @@ -37,7 +37,7 @@ def _finished_state() -> GameState: turn=0, table=[engine.parse_card("02C")], ) - from scopa.game.state import PlayerState, Card + from tavolo.game.state import PlayerState, Card state.players = [ PlayerState(sub="alice", name="alice", seat=0, hand=[Card.parse("02D")]), diff --git a/server/tests/test_store.py b/server/tests/test_store.py index 263b9fe..e586edf 100644 --- a/server/tests/test_store.py +++ b/server/tests/test_store.py @@ -6,8 +6,8 @@ import unittest from pwo import async_test -from scopa.game import engine -from scopa.store import InMemoryGameStore +from tavolo.game import engine +from tavolo.store import InMemoryGameStore class InMemoryGameStoreTest(unittest.TestCase): diff --git a/server/tests/test_websocket.py b/server/tests/test_websocket.py index 2178a58..cbd9819 100644 --- a/server/tests/test_websocket.py +++ b/server/tests/test_websocket.py @@ -9,9 +9,9 @@ from httpx_ws import WebSocketDisconnect, aconnect_ws from httpx_ws.transport import ASGIWebSocketTransport from pwo import async_test -from scopa.app import app, game_store -from scopa.game import engine -from scopa.game.state import Card, GameState, PlayerState +from tavolo.app import app, game_store +from tavolo.game import engine +from tavolo.game.state import Card, GameState, PlayerState from tests.helpers import make_user, oidc_user, ws_users PLAYERS = ("alice", "bob", "carol", "dave") diff --git a/web/Cargo.lock b/web/Cargo.lock index 91526f0..dc57df2 100644 --- a/web/Cargo.lock +++ b/web/Cargo.lock @@ -369,24 +369,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" -[[package]] -name = "scopa-web" -version = "0.1.0" -dependencies = [ - "console_error_panic_hook", - "futures", - "gloo-net", - "gloo-timers", - "js-sys", - "serde", - "serde_json", - "sycamore", - "sycamore-router", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "serde" version = "1.0.229" @@ -579,6 +561,24 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tavolo-web" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "futures", + "gloo-net", + "gloo-timers", + "js-sys", + "serde", + "serde_json", + "sycamore", + "sycamore-router", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/web/Cargo.toml b/web/Cargo.toml index 3e809a6..de8e142 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "scopa-web" +name = "tavolo-web" version = "0.1.0" edition = "2021" -description = "Sycamore/WASM frontend for the scopone scientifico backend" +description = "Sycamore/WASM frontend for the tavolo card-game platform" [dependencies] sycamore = "0.9" diff --git a/web/index.html b/web/index.html index c7a7b4d..3a01e97 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,7 @@ - Scopone scientifico + Tavolo diff --git a/web/src/api.rs b/web/src/api.rs index 9949de8..51bc80e 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -1,4 +1,4 @@ -//! REST client for the scopa backend. Same-origin requests carry the +//! REST client for the tavolo backend. Same-origin requests carry the //! session cookie automatically. use crate::model::*; use gloo_net::http::Request;