Add scopa: classic two-player and scopa a quattro
New tavolo-scopa package with a pure rules engine and the platform adapters for two-player scopa and four-player fixed-partnership scopa a quattro: three cards each and four on the table, capture by rank or sum, sweep for scope and score carte/denara/settebello/primiera. The optional asso pigliatutto creation option turns either variant into scopa d'assi (an ace sweeps the whole table and scores no scopa), reusing scopone's rule semantics. Wire both engines into the app and the Docker install, add the scopa table view to the SPA (duo and four-seat layouts, deck count, captured/scope stats, hand-summary and final screens, player names in the two-player summary), and cover the rules, the platform contract, the REST/WebSocket flows, match persistence and Elo.
This commit is contained in:
+58
-13
@@ -1,10 +1,11 @@
|
||||
# tavolo (backend)
|
||||
|
||||
The backend for a multiplayer card-game platform, built on the
|
||||
[kaya](../kaya) framework. It is a monorepo of four distributions
|
||||
[kaya](../kaya) framework. It is a monorepo of five distributions
|
||||
sharing the `tavolo` namespace (see
|
||||
[`packages/tavolo-platform/README.md`](packages/tavolo-platform/README.md),
|
||||
[`packages/tavolo-scopone/README.md`](packages/tavolo-scopone/README.md)
|
||||
[`packages/tavolo-scopone/README.md`](packages/tavolo-scopone/README.md),
|
||||
[`packages/tavolo-scopa/README.md`](packages/tavolo-scopa/README.md)
|
||||
and
|
||||
[`packages/tavolo-briscola/README.md`](packages/tavolo-briscola/README.md)):
|
||||
|
||||
@@ -15,6 +16,10 @@ and
|
||||
- **`tavolo-scopone`** (`packages/tavolo-scopone/`) — scopone
|
||||
scientifico, the four-player, fixed-partnership variant of the classic
|
||||
Italian card game: pure rules plus the platform adapter.
|
||||
- **`tavolo-scopa`** (`packages/tavolo-scopa/`) — scopa, the classic
|
||||
Italian fishing game, in its two-player (`scopa`) and four-player
|
||||
fixed-partnership (`scopa_a_quattro`) variants, with the optional asso
|
||||
pigliatutto rule (scopa d'assi): pure rules plus the platform adapters.
|
||||
- **`tavolo-briscola`** (`packages/tavolo-briscola/`) — briscola, the
|
||||
Italian trick-taking game, in its classic two-player (`briscola`),
|
||||
four-player partnership (`briscola_a_quattro`) and five-player
|
||||
@@ -236,9 +241,11 @@ aggregate with `max`, not `sum`.
|
||||
scoped per game), start/finish timestamps, and the game-specific outcome
|
||||
as reported by the engine (`result` JSON — for scopone: both teams'
|
||||
final scores, winner, target score, hands played and the per-hand
|
||||
audit; for briscola: the final scores, winner, target score, deals
|
||||
played and the per-deal audit; for briscola chiamata: the calling side's
|
||||
points against the defenders', the declared target, the called card and
|
||||
audit; for scopa: both sides' final scores, winner, target score, the
|
||||
asso pigliatutto flag, hands played and the per-hand audit; for
|
||||
briscola: the final scores, winner, target score, deals played and the
|
||||
per-deal audit; for briscola chiamata: the calling side's points
|
||||
against the defenders', the declared target, the called card and
|
||||
whether the caller played alone).
|
||||
- `match_player` — one row per participant: the OIDC `sub`, display name,
|
||||
seat, game-defined team label (nullable), whether they won, the points
|
||||
@@ -278,7 +285,7 @@ All endpoints except `/api/health`, `/api/docs`, `/api/openapi.json`,
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/game-types` | The games the platform can host (for the creation dropdown), each with its `options_schema` |
|
||||
| `POST` | `/api/games` | Create a lobby game. Optional body `{"game_type": "scopone_scientifico", "options": {"target_score": 11, "napola": true, "asso_pigliatutto": false}}`; briscola games use `{"game_type": "briscola"}` or `{"game_type": "briscola_a_quattro"}` with an optional `{"target_score": 61}`, and briscola chiamata uses `{"game_type": "briscola_chiamata"}` with no options. Returns the lobby (`{id, join_code, players, seats_open, ...}`) |
|
||||
| `POST` | `/api/games` | Create a lobby game. Optional body `{"game_type": "scopone_scientifico", "options": {"target_score": 11, "napola": true, "asso_pigliatutto": false}}`; scopa games use `{"game_type": "scopa"}` or `{"game_type": "scopa_a_quattro"}` with an optional `{"target_score": 11, "asso_pigliatutto": false}` (the asso pigliatutto rule makes it scopa d'assi); briscola games use `{"game_type": "briscola"}` or `{"game_type": "briscola_a_quattro"}` with an optional `{"target_score": 61}`, and briscola chiamata uses `{"game_type": "briscola_chiamata"}` with no options. 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}` | 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 |
|
||||
@@ -312,7 +319,7 @@ 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. Spectators may only send `state`/`sync`: any other
|
||||
action is answered with an `error` whose `code` is `forbidden`. For
|
||||
scopone scientifico:
|
||||
scopone scientifico and scopa (both variants):
|
||||
|
||||
```json
|
||||
{"action": "play", "card": "07D", "capture": ["02D", "05C"]}
|
||||
@@ -384,11 +391,11 @@ for the called-card choice (a random suit of the won rank).
|
||||
When a hand finishes but the match continues, the game enters the
|
||||
`hand_end` phase instead of dealing immediately: the state carries the
|
||||
scoring breakdown of the hand just played (`last_hand` with an `award`
|
||||
map naming the team that won each scopone category; `last_deal` with the
|
||||
deal's points and running totals for briscola), the `acknowledged` seats
|
||||
and a `hand_end_deadline`. The frontend renders this as a screen every
|
||||
player must dismiss. A play attempted in this phase is rejected with an
|
||||
`illegal_move` error.
|
||||
map naming the side that won each scopone/scopa category; `last_deal`
|
||||
with the deal's points and running totals for briscola), the
|
||||
`acknowledged` seats and a `hand_end_deadline`. The frontend renders this
|
||||
as a screen every player must dismiss. A play attempted in this phase is
|
||||
rejected with an `illegal_move` error.
|
||||
|
||||
## Rules implemented
|
||||
|
||||
@@ -417,6 +424,36 @@ player must dismiss. A play attempted in this phase is rejected with an
|
||||
configurable per game via the `target_score` creation option) with a
|
||||
clear lead; a tie at or above the target is broken by another hand.
|
||||
|
||||
### Scopa
|
||||
|
||||
- 40-card Italian deck; three cards per player and four cards face up on
|
||||
the table at the start of every hand. When every hand is empty the
|
||||
dealer deals three more cards to each player (no new table cards) until
|
||||
the deck is exhausted: two players play six rounds per hand, four
|
||||
players three.
|
||||
- A card captures a **single card of equal rank** (mandatory when present)
|
||||
or a **combination of table cards whose ranks sum to its own**; when any
|
||||
capture exists it must be taken. Remaining table cards at the end of the
|
||||
hand go to the player who made the last capture.
|
||||
- Emptying the table scores a *scopa* (+1), except on the last play of the
|
||||
hand (a sweep on the last play of a round still scores when the deck has
|
||||
cards left).
|
||||
- Hand points: `carte`, `denara`, `settebello` and `primiera` (same values
|
||||
and all-four-suits requirement as scopone), plus one point per scopa.
|
||||
Ties award nothing. There is no napola.
|
||||
- Optional *asso pigliatutto* rule (per-game `asso_pigliatutto` creation
|
||||
option, default off; scopa d'assi): an ace played when no equal rank is
|
||||
on the table sweeps every card on the table — or captures itself when
|
||||
the table is empty. The capture never scores a scopa, but it counts as
|
||||
the last capture, and the mandatory equal-rank capture still applies
|
||||
when another ace is on the table.
|
||||
- The match ends when a side reaches the target score (default 11,
|
||||
configurable per game via the `target_score` creation option) with a
|
||||
clear lead; a tie at or above the target is broken by another hand.
|
||||
- `scopa` is the classic two-player game; `scopa_a_quattro` seats four
|
||||
players in two fixed partnerships (seats 0/2 vs 1/3, partners
|
||||
opposite).
|
||||
|
||||
### Briscola
|
||||
|
||||
- 40-card Italian deck, three cards per player. The card turned up after
|
||||
@@ -470,16 +507,18 @@ player must dismiss. A play attempted in this phase is rejected with an
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
.venv/bin/pip install -e ./packages/tavolo-platform -e ./packages/tavolo-scopone -e ./packages/tavolo-briscola -e './packages/tavolo-app[dev]'
|
||||
.venv/bin/pip install -e ./packages/tavolo-platform -e ./packages/tavolo-scopone -e ./packages/tavolo-scopa -e ./packages/tavolo-briscola -e './packages/tavolo-app[dev]'
|
||||
# app integration suite:
|
||||
.venv/bin/python -m unittest discover -s tests -t .
|
||||
# per-package suites (each package is self-contained):
|
||||
.venv/bin/python -m unittest discover -s packages/tavolo-platform/tests
|
||||
.venv/bin/python -m unittest discover -s packages/tavolo-scopone/tests
|
||||
.venv/bin/python -m unittest discover -s packages/tavolo-scopa/tests
|
||||
.venv/bin/python -m unittest discover -s packages/tavolo-briscola/tests
|
||||
# type checks (see packages/tavolo-app/README.md for the app modules):
|
||||
.venv/bin/python -m mypy -p tavolo.platform
|
||||
.venv/bin/python -m mypy -p tavolo.scopone
|
||||
.venv/bin/python -m mypy -p tavolo.scopa
|
||||
.venv/bin/python -m mypy -p tavolo.briscola
|
||||
```
|
||||
|
||||
@@ -538,6 +577,12 @@ packages/tavolo-scopone/ # scopone scientifico game (own pyproject + tests)
|
||||
├── engine.py # pure scopone scientifico rules
|
||||
├── errors.py # scopone-specific rule violations
|
||||
└── plugin.py # ScoponeEngine: the GameEngine adapter
|
||||
packages/tavolo-scopa/ # scopa game (own pyproject + tests)
|
||||
src/tavolo/scopa/
|
||||
├── state.py # ScopaState / PlayerState / Card / Move, JSON (de)serialization
|
||||
├── engine.py # pure scopa rules (two- and four-player)
|
||||
├── errors.py # scopa-specific rule violations
|
||||
└── plugin.py # ScopaEngine + ScopaQuattroEngine adapters
|
||||
packages/tavolo-briscola/ # briscola game (own pyproject + tests)
|
||||
src/tavolo/briscola/
|
||||
├── state.py # BriscolaState / PlayerState / Card / Trick, JSON (de)serialization
|
||||
|
||||
Reference in New Issue
Block a user