Start forfra: server-autoritativ spilmotor over WebSocket
Den tidligere version (Angular-SPA, REST og polling) er gemt på branchen legacy. Denne version samler alle regler i en ren Python-spilmotor (fupogfakta/engine.py), holder rummene i hukommelsen i én proces og sender hele spiltilstanden pr. rolle over én WebSocket. Vært-login sker via Authentik forward-auth i NPMplus. Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# Protokol: server ↔ klient
|
||||
|
||||
Al spilkommunikation går over én WebSocket pr. skærm. Serveren er autoritativ: klienten sender **handlinger** og modtager **hele tilstanden** (et snapshot tilpasset modtagerens rolle) efter hver ændring. Klienten gemmer intet spil-state ud over sin `token` og tegner altid fra seneste snapshot.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Sti | Hvem | Hvad |
|
||||
| --- | --- | --- |
|
||||
| `GET /` | spillere/publikum | Join-side (`?code=ABCD` udfylder koden) |
|
||||
| `GET /host/` | vært (Authentik) | Opret nyt spil |
|
||||
| `POST /host/new` | vært | Opretter rum, redirect til `/host/<CODE>/` |
|
||||
| `GET /host/<CODE>/` | vært | Storskærm |
|
||||
| `GET /qr/<CODE>.svg` | alle | QR-kode til `https://<host>/?code=<CODE>` |
|
||||
| `WS /ws/play/<CODE>/` | spillere/publikum | |
|
||||
| `WS /ws/host/<CODE>/` | vært (Authentik) | |
|
||||
|
||||
Koder er 4 store bogstaver (`BCDFGHJKLMNPQRSTVWXZ`), fx `KLMT`.
|
||||
|
||||
## Klient → server
|
||||
|
||||
Alle beskeder er JSON `{"action": "...", ...}`.
|
||||
|
||||
**Spiller/publikum (`/ws/play/`)**
|
||||
|
||||
| action | felter | hvornår |
|
||||
| --- | --- | --- |
|
||||
| `join` | `name` (1-12 tegn) | første gang. Svar: `joined`. Er der 8 spillere, eller er spillet i gang, bliver man publikum. |
|
||||
| `resume` | `token` | ved genforbindelse (token fra `localStorage["wpp:"+code]`). Ukendt token → `error` `unknown_token`, klienten viser join-formularen igen. |
|
||||
| `start` | | VIP (første spiller) starter fra lobby |
|
||||
| `choose_category` | `category_id` | kun den valgte spiller i fasen `category` |
|
||||
| `submit_lie` | `text` (1-40 tegn) | fase `lie` |
|
||||
| `lie_for_me` | | fase `lie`; svar: `suggestion` |
|
||||
| `guess` | `answer_id` | fase `guess`; spillere og publikum, én gang |
|
||||
| `like` | `answer_id` | fase `guess` (efter eget gæt) og `reveal`; slår til/fra; ikke eget svar |
|
||||
| `play_again` | | VIP i fasen `final` |
|
||||
| `ping` | | svar `pong` |
|
||||
|
||||
**Vært (`/ws/host/`)**: `start`, `skip` (afslut nuværende fase nu), `play_again`, `kick` (`player_id`), `ping`.
|
||||
|
||||
## Server → klient
|
||||
|
||||
| type | felter |
|
||||
| --- | --- |
|
||||
| `state` | `state`: snapshot (se nedenfor) |
|
||||
| `joined` | `token`, `player_id`, `role` (`player`/`audience`) |
|
||||
| `suggestion` | `text` |
|
||||
| `error` | `code`, `message` (dansk, kan vises direkte) |
|
||||
| `pong` | |
|
||||
|
||||
Fejlkoder: `unknown_room`, `unknown_token`, `name_taken`, `name_invalid`, `not_allowed`, `wrong_phase`, `lie_is_truth`, `lie_invalid`, `already_done`, `answer_invalid`, `too_few_players`.
|
||||
|
||||
## Snapshot
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"code": "KLMT",
|
||||
"phase": "lobby", // lobby | category | lie | guess | reveal | scores | final
|
||||
"server_time": 1790000000000,// ms epoch; klienten udregner sin forskydning
|
||||
"deadline": 1790000060000, // ms epoch eller null
|
||||
"join_url": "https://party.weircon.dk/?code=KLMT",
|
||||
"min_players": 2, "max_players": 8,
|
||||
"round": {"number": 1, "name": "Runde 1", "multiplier": 1,
|
||||
"question_number": 2, "questions_in_round": 3}, // null i lobby/final
|
||||
"players": [
|
||||
{"id": "p1", "name": "Asger", "color": "#ff4d6d", "score": 1500,
|
||||
"connected": true, "vip": true, "done": false} // done = har løjet/gættet i nuværende fase
|
||||
],
|
||||
"audience_count": 0,
|
||||
"category": {"chooser_id": "p2", "options": [{"id": 3, "name": "Dyreriget"}]}, // kun i fase category
|
||||
"question": {"category": "Dyreriget", "prompt": "En flok _____ kaldes en parlament."}, // lie/guess/reveal
|
||||
"answers": [{"id": "a1", "text": "ugler"}], // guess + reveal; spillerens egen løgn er udeladt i guess
|
||||
"reveal": { // kun i fase reveal
|
||||
"steps": [ // løgne først (kun dem der blev valgt), sandheden sidst
|
||||
{"answer_id": "a3", "text": "krager", "is_truth": false, "house": false,
|
||||
"authors": ["p2"], "guessed_by": ["p1", "p3"], "audience_votes": 2, "likes": 1,
|
||||
"points": [{"player_id": "p2", "delta": 1000}]},
|
||||
{"answer_id": "a1", "text": "ugler", "is_truth": true, "house": false,
|
||||
"authors": [], "guessed_by": ["p4"], "audience_votes": 1, "likes": 0,
|
||||
"points": [{"player_id": "p4", "delta": 1000}]}
|
||||
],
|
||||
"step_ms": 4000 // værtsskærmen viser ét trin pr. step_ms
|
||||
},
|
||||
"deltas": [{"player_id": "p1", "delta": 1500}], // fase scores: point i den netop afsluttede runde
|
||||
"final": { // kun i fase final
|
||||
"ranking": [{"player_id": "p1", "score": 9000, "place": 1}],
|
||||
"awards": [{"title": "Største løgner", "player_id": "p2", "detail": "narrede 7 gange"}]
|
||||
},
|
||||
"me": { // kun til spiller/publikum
|
||||
"id": "p1", "role": "player", "vip": true,
|
||||
"lie": "krager", // egen løgn i nuværende spørgsmål eller null
|
||||
"guess": "a3", // eget gæt eller null
|
||||
"likes": ["a2"],
|
||||
"is_chooser": false,
|
||||
"last_delta": 1500 // point fra seneste spørgsmål (reveal/scores)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Værten får samme snapshot uden `me`, men med `"host": true`.
|
||||
|
||||
## Spillets gang (Fibbage-klassisk)
|
||||
|
||||
| Runde | Spørgsmål | Point: rigtigt gæt / pr. narret spiller |
|
||||
| --- | --- | --- |
|
||||
| Runde 1 | 3 | 1000 / 500 |
|
||||
| Runde 2 | 3 | 2000 / 1000 |
|
||||
| Finale | 1 | 3000 / 1500 |
|
||||
|
||||
For hvert spørgsmål: `category` (15 s; skiftende spiller vælger blandt 4; i finalen springes fasen over) → `lie` (60 s, slutter når alle har løjet) → `guess` (30 s, slutter når alle spillere har gættet) → `reveal` (automatisk: antal trin × `step_ms` + 3 s; værten kan `skip`). Efter sidste spørgsmål i en runde: `scores` (10 s). Efter finalen: `final`.
|
||||
|
||||
- Løgn lig sandheden (eller næsten lig) afvises med `lie_is_truth`. Ens løgne slås sammen, og alle forfattere får point.
|
||||
- Spillere, der ikke løj i tide, får ingen løgn; huset fylder op med lokkesvar, så der er mindst `max(4, spillere+1)` svar.
|
||||
- Man kan ikke vælge sin egen løgn.
|
||||
- Like: +100 til forfatteren pr. like (uden multiplikator).
|
||||
- Publikum: forfatteren får `round(500 × multiplikator × andel af publikum narret)`.
|
||||
- Spil igen: samme spillere, point nulstilles.
|
||||
Reference in New Issue
Block a user