Let anyone with the game URL watch as a read-only spectator
CI / Build and push docker image (push) Successful in 3m0s
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.
This commit is contained in:
+17
-11
@@ -262,7 +262,8 @@ existed):
|
||||
## REST API
|
||||
|
||||
All endpoints except `/api/health`, `/api/docs`, `/api/openapi.json`,
|
||||
`/api/game-types` and `/api/leaderboard` require authentication.
|
||||
`/api/game-types`, `/api/leaderboard` and the game snapshot
|
||||
(`GET /api/games/{id}`) require authentication.
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
@@ -270,32 +271,36 @@ All endpoints except `/api/health`, `/api/docs`, `/api/openapi.json`,
|
||||
| `POST` | `/api/games` | Create a lobby game. Optional body `{"game_type": "scopone_scientifico", "options": {"target_score": 11, "napola": true, "asso_pigliatutto": false}}`. Returns the lobby (`{id, join_code, players, seats_open, ...}`) |
|
||||
| `POST` | `/api/games/join` | Join with `{"code": "ABC123"}`. The last player triggers the start and gets the game view back |
|
||||
| `GET` | `/api/games/preview?code=ABC123` | Pre-join summary of the lobby behind a code: game name/description, seated players, open seats and the engine-echoed options. `404` when the code is unknown |
|
||||
| `GET` | `/api/games/{id}` | Personalized snapshot (only your own hand is visible) |
|
||||
| `GET` | `/api/games/{id}` | Game snapshot. Seated players get the personalized view (own hand plus the join code); anyone else gets the read-only spectator view: table, scores, captured counts and scope, no hands and no join code |
|
||||
| `GET` | `/api/me/matches` | Cursor-paginated match history with the engine's `result` and per-player Elo deltas (`?limit=&cursor=&game_type=`) |
|
||||
| `GET` | `/api/me/ratings` | The caller's Elo rating per game type |
|
||||
| `GET` | `/api/leaderboard` | Elo rating, aggregated wins / matches / points per player, sorted by Elo (`?game_type=`) |
|
||||
|
||||
## WebSocket protocol
|
||||
|
||||
Connect to `/ws/games/{game_id}` with your session cookie. Only seated
|
||||
players are accepted; the upgrade is refused before the handshake
|
||||
completes with an HTTP status mirroring `GET /api/games/{id}`: `401`
|
||||
unauthenticated, `404` unknown game, `403` not seated. (Browsers report
|
||||
a refused handshake as an abnormal close — there is no close code to
|
||||
read.)
|
||||
Connect to `/ws/games/{game_id}`; the session cookie is optional. Anyone
|
||||
who knows the game id is accepted: seated players get their personalized
|
||||
view, everyone else — anonymous users and authenticated non-players — is
|
||||
a read-only **spectator**. Only an unknown game id is refused before the
|
||||
handshake completes, with `404`. (Browsers report a refused handshake as
|
||||
an abnormal close — there is no close code to read.)
|
||||
|
||||
Server → client messages are JSON objects with a `type`:
|
||||
|
||||
- `{"type": "state", "game": {...}}` — personalized game view. Your own
|
||||
hand is included; other players expose only their card counts.
|
||||
`your_turn: true` is present when it is your move.
|
||||
Spectators get no hand at all, `spectator: true` marks the view and
|
||||
`join_code` is omitted for them. `your_turn: true` is present when it
|
||||
is your move.
|
||||
- `{"type": "game_over", "scores": {"A": 11, "B": 7}, "winner": "A"}`
|
||||
- `{"type": "error", "code": "illegal_move", "message": "..."}`
|
||||
|
||||
Client → server messages are JSON objects with an `action`. The
|
||||
platform handles `state` (resend the snapshot) itself; every other action
|
||||
is dispatched to the session's game engine with the rest of the message
|
||||
as its payload. For scopone scientifico:
|
||||
as its payload. Spectators may only send `state`/`sync`: any other
|
||||
action is answered with an `error` whose `code` is `forbidden`. For
|
||||
scopone scientifico:
|
||||
|
||||
```json
|
||||
{"action": "play", "card": "07D", "capture": ["02D", "05C"]}
|
||||
@@ -314,7 +319,8 @@ as its payload. For scopone scientifico:
|
||||
after `HAND_ACK_TIMEOUT_SECONDS`.
|
||||
- `state` asks for a fresh snapshot.
|
||||
|
||||
After every accepted move the new state is broadcast to all four players.
|
||||
After every accepted move the new state is broadcast to every connected
|
||||
client, spectators included.
|
||||
|
||||
### Turn timeout
|
||||
|
||||
|
||||
Reference in New Issue
Block a user