Files
pirate-dice/README.md
2026-09-16 18:35:59 +02:00

324 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Liar's Dice 3D
Würfel-Bluffspiel in Godot 4.7 mit echter 3D-Physik, Mehrspieler über Raum-Codes,
KI-Bots und Münzen als Buffs und Debuffs.
## Starten
godot --path . # oder das Projekt im Godot-Editor öffnen
Das Startmenü hat drei Punkte:
* **Einzelspieler** – nur die Bot-Anzahl (1–7) einstellen und starten. Es wird
kein Netzwerk geöffnet.
* **Mehrspieler** – nur den Namen eintragen, dann **Hosten** oder
**Beitreten**. Hosten führt direkt in die Lobby; dort steht der Code.
Beitreten fragt zuerst nach dem Code. Niemand braucht eine IP oder eine
Portfreigabe.
* **Lobby** – zeigt den Code, wer schon da ist und die Bot-Anzahl. Nur der
Host stellt die Bots ein und startet, sobald mindestens 2 Teilnehmer
zusammen sind (Menschen und Bots zusammengezählt, maximal 8).
* **Beenden** – schließt das Spiel.
## Mehrspieler: wie es zusammenhängt
Die Partie läuft nicht beim Host, sondern auf `dice.marha.de`:
Spieler ──wss://443──> ein Serverprozess ──> Raum MQG4 (SubViewport + World3D)
└──> Raum 4YPV (SubViewport + World3D)
* **Ein Prozess, ein Port.** Der Server trägt alle Räume gleichzeitig und
spricht WebSocket. Traefik reicht das auf 443 durch, es muss kein Port
freigegeben werden.
* **Ein Raum** ist ein Knoten unter `/root/Main/Raeume/<CODE>`. Darin ein
SubViewport mit `own_world_3d` – nur so hat jede Partie ihren eigenen
Physikraum, sonst lägen alle Würfel im selben.
* Die Knotennamen sind auf Server und Client gleich. Daran hängt das Ziel
jedes RPC, deshalb dürfen sie nicht auseinanderlaufen.
* Broadcasts (`rpc`) würden alle Räume treffen. Deshalb geht alles gezielt per
`rpc_id` an die Peers des eigenen Raums.
* Der Server sitzt nicht mit am Tisch: `local_seat` bleibt −1, kein HUD.
* **Wiedereinstieg.** Wer die Verbindung verliert, wird zum Bot. Tritt er mit
demselben Namen und Code wieder bei, bekommt er seinen Platz zurück: der
Server schickt ihm die laufende Aufstellung statt einer Lobby, Zustand und
Würfellage kommen kurz danach nach. Das Menü merkt sich den Abbruch und
bietet **Zurück in Raum XY** an - ein Klick, kein Code tippen.
* Der **Host** ist der erste Spieler im Raum. Geht er, rückt der nächste nach.
Nur er stellt die Bots ein und startet.
* Der letzte macht das Licht aus: verlässt der letzte Spieler den Raum, wird er
samt Code sofort abgeräumt.
Gemessen mit sechs gleichzeitigen Partien zu acht Plätzen: **3,7 MB je Raum**,
im Leerlauf unter 1 % eines Kerns, kurze Spitzen beim Würfelwurf.
## Lokal entwickeln
make server # Spielserver auf :8000, trägt alle Räume
make dev # Spiel, zeigt auf den lokalen Server
Ohne `PIRATE_DICE_SERVER` zeigt das Spiel auf `wss://dice.marha.de`.
## Auf dem Server
git pull
make prod # baut und startet, holt Godot selbst
make logs
make down
Mehr ist nicht nötig. Der Docker-Build lädt Godot 4.7.2 herunter (Version steht
im `Dockerfile`), importiert das Projekt einmal vor und startet den Server.
`restart: unless-stopped` bringt ihn nach einem Neustart von selbst wieder hoch.
Es muss kein Port freigegeben werden: alles läuft als WebSocket über Traefik
auf 443.
Das Spiel startet im Vollbild, **F11** schaltet um. **Esc** öffnet ein Menü mit
*Fortsetzen* und *Verlassen*; im Einzelspieler hält es das Spiel samt Zeitlimit
wirklich an, im Mehrspieler läuft die Runde weiter – sonst würde ein geöffnetes
Menü alle anderen am Tisch blockieren.
Die Bildaufteilung: Tisch mittig, darauf die letzte Wette als so viele Würfel
wie angesagt; aktive Booster oben links, Würfel aller Spieler oben rechts
(Name mit den Würfeln darunter) – beide Panels nur so breit wie ihr Inhalt –,
und die eigene Ansage als schmale Leiste unten in der Mitte.
Augenzahlen werden überall als gezeichnete Würfel dargestellt (`DieIcon` in
`hud.gd`), unbekannte Werte als Würfel mit Fragezeichen. Beim Aufdecken bleibt die Wette in der
Tischmitte stehen, wie sie angesagt war: fehlende Würfel werden rot. Gab es
mehr als angesagt, kommen die überzähligen grün dahinter. Am Tisch trägt jeder Becher ein Schild mit Namen und der letzten Ansage dieses
Spielers; Unterlage und Würfelbilder dafür werden beim Start einmal als Textur
gezeichnet. Im Panel oben rechts
stehen deshalb deine eigenen Würfel offen, gegnerische verdeckt – bis auf die
per *Spähen* aufgedeckten und beim Aufdecken am Rundenende.
Jeder Platz hat eine runde Würfelzone mit umlaufender Bande. Die runde Form
ist nicht nur Optik: in den Ecken der früheren rechteckigen Zone verkanteten
sich Würfel, im Kreis liegt kein Wurf mehr schief (Messung: 2,0 % schiefe
Würfel und 1,3 % Nachhelfer vorher, 0,4 % und 0 % nachher). Beim Aufdecken
legen sich die Würfel als Bogen auf einen Kreis um die Zonenmitte, damit sie
sich nicht überdecken und das Namensschild frei bleibt.
Der Tisch wächst mit der Spielerzahl, damit die Würfelzonen auch bei acht
Plätzen nicht aneinanderstoßen. Die Kamera schaut mit 15° Neigung fast senkrecht
von oben darauf; der eigene Platz liegt unten im Bild. `_frame_camera` in
`table.gd` rechnet den Abstand so aus, dass der Tisch genau das freie Band
zwischen den HUD-Feldern ausfüllt (`HUD_TOP`/`HUD_BOTTOM`) – bei jeder
Spielerzahl und jedem Fensterformat, auch nach dem Ändern der Fenstergröße.
## Physik und Bildrate
Zwei Engine-Vorgaben sind bewusst gesetzt, beide in `project.godot`:
* **`3d/physics_engine="Jolt Physics"`** – GodotPhysics3D hat Kontakt-Toleranzen,
die auf meterhohe Objekte ausgelegt sind (`contact_max_separation` 0.05 bei
0.075 Würfel-Halbkante). Würfel dieser Größe sanken dadurch in die Tischplatte
ein und rollten endlos weiter, sodass jeder Wurf in den Settle-Timeout lief.
Mit Jolt liegt die Wurfphase bei rund 0,9 s statt 7 s.
* **`renderer/rendering_method="mobile"`** – auf integrierten GPUs kostet
Forward+ hier mehr als das Doppelte: 22 statt 47 fps bei 30 Würfeln.
Ein liegender Würfel wird mit `FREEZE_MODE_STATIC` eingefroren, nicht
kinematisch. Ein kinematischer Körper meldet seine Lage weiter an den Knoten
zurück: setzt ein Booster wie *Glücksgriff* die Augenzahl über
`snap_to_value()`, überschreibt der Physikserver die neue Lage im nächsten
Schritt wieder mit der alten, und der Würfel zeigt den falschen Wert.
`Die.place()` zieht die Lage zusätzlich sofort im Physikserver nach.
Ein Würfel ist ein einziges Renderobjekt: Körper und Augen stecken in einem
gemeinsamen Mesh mit Vertexfarben, statt 21 Augen als eigene Meshes zu hängen.
Weil Kamera und Deckenlicht beide fast senkrecht stehen, liegt der Glanzpunkt
genau auf der Würfeloberseite. Mit glänzendem Material und starkem Spot klippte
die weiße Fläche bei 255 und riss die schwarzen Augen mit hoch – gemessen lag
kein einziger Punkt-Pixel unter Luminanz 150. Deshalb ist das Würfelmaterial
matt (`roughness` 0.9, `metallic_specular` 0.0) und der Spot schwächer
zugunsten von mehr Umgebungslicht; der Kontrast Auge zu Körper liegt damit bei
rund 11:1.
## Regeln
Klassisches Liar's Dice ohne Joker. Jeder startet mit 5 Würfeln und sieht nur
die eigenen.
1. **Würfeln** – alle Würfel fallen physikalisch in den eigenen Becher.
2. **Ansagen** – reihum wird ein Gebot "Anzahl x Augenzahl" über alle Würfel
am Tisch gemacht. Jedes Gebot muss höher sein: mehr Würfel, oder gleich
viele bei höherer Augenzahl.
3. **Anfechten** – statt zu erhöhen kann man das Vorgebot anzweifeln. Alle
Becher werden gehoben und gezählt: Stimmt das Gebot, verliert der
Anfechter einen Würfel, sonst der Bieter.
Für die Ansage hat man **30 Sekunden**; die Restzeit steht neben „Deine Ansage"
und wird unter zehn Sekunden rot. Läuft die Zeit ab, entscheidet der Host mit
der Bot-Logik. Ohne dieses Limit würde ein abwesender oder getrennter
Mitspieler den ganzen Tisch dauerhaft blockieren.
**Genau:** Wer glaubt, die Ansage trifft exakt, ruft „Genau" statt
anzufechten. Trifft es, verliert der Ansager einen Würfel, der Rufer bekommt
einen dazu und nimmt den Topf. Daneben verliert der Rufer einen Würfel. Der
Schwung ist damit immer zwei - doppelt so viel wie eine Anfechtung, aber
unabhängig von der Spielerzahl.
Aufgedeckt wird in drei Etappen: Becher hoch, dann zählen, dann das Urteil.
Beim Zählen leuchten die passenden Würfel nacheinander auf. So baut sich die
Spannung auf, statt dass alles auf einmal dasteht.
Nach dem Ende gibt es ein **Nochmal** mit derselben Runde - solo sofort, im
Netz für den Raumbesitzer; der Server setzt den Raum dann für alle neu auf.
Fällt ein Würfel je über die Bande oder durch den Filz, fängt ihn ein
Fangnetz und legt ihn zurück in seine Zone - sonst käme das Würfeln nie zur
Ruhe.
Wer keine Würfel mehr hat, scheidet aus und schaut zu. Der Letzte gewinnt.
## Kampagne
Im Menü unter **Kampagne** liegt eine Seekarte mit sechs Kapitänen, von
*Roter Jack* (leicht, 900) bis *Admiral Krähe* (schwer, 1360). Ein Klick auf
einen Hafen startet die Partie: der Kapitän spielt auf seiner Stufe, seine
Mannschaft eine Stufe darunter.
* Du hast eine **Wertung** (Elo, Start 1000) und daraus einen **Rang**:
Schiffsjunge, Matrose, Maat, Steuermann, Kapitän, Admiral.
* **Geschlagen** ist ein Kapitän, wenn er vor dir ausscheidet - oder du
gewinnst. Er verschwindet dann für zwei Partien auf See und **kommt
stärker zurück**: mehr Wertung, eine Stufe höher, ab „schwer" mehr
Mannschaft.
* Verlierst du, bleibt er, und deine Wertung sinkt.
* Jeder Kapitän hat ein **Temperament** - Bluffer, Zauderin, Genau-Rufer,
Rechner, Eiskalt, Draufgänger - dieselben vier Regler wie die Stufen, nur
je Kopf anders. Auf dem Hafen steht dazu deine **Siegchance** nach Elo.
* Nach einem Sieg wählst du **Beute**: eine Münze deiner Wahl, die du in die
nächste Partie mitnimmst.
* Die Karte meldet, wer zurück ist und wie stark. **Ziel** ist der Rang
Admiral; danach bleibt die See offen.
* Der Stand liegt in `user://kampagne.json`, samt Name. „Neu anfangen" löscht ihn.
## Münzen
Münzen ersetzen die frühere Booster-Phase. Die kostete jede Runde eine eigene
Wartezeit und riss den Ablauf auseinander.
* Jeder startet mit **einer** Münze.
* Wer einen Würfel abgibt, **bekommt eine Münze**. Das ist der einzige frische
Nachschub, also bekommt der etwas zurück, der gerade hinten liegt.
* Geworfen wird **nur am eigenen Zug, vor der Ansage**. So ist der Zug eine
Entscheidung, und zwei Würfe kommen sich nie in die Quere.
* Mehr als **vier** Münzen passen nicht in den Vorrat.
* Der Vorrat liegt offen unten links; ein Klick wirft die Münze sofort. Nur
*Falschspiel* fragt noch, welcher Würfel gemeint ist.
### Der Topf
Eine geworfene Münze landet als Messingstück in der Tischmitte. **Wer das
Aufdecken gewinnt, nimmt aus dem Topf, so viel in den Vorrat passt.** Der
Rest bleibt liegen und wächst in die nächste Runde. Mehr als sechs liegen nie
dort; die älteste fällt vom Tisch, sonst entstünde ein Haufen, den keiner
mehr abholt. Damit kostet jede Münze
etwas: wer wirft und dann verliert, schenkt sie dem Gegner. Und das Aufdecken
hat einen Einsatz, der über den einen Würfel hinausgeht.
### Der Wurf
Beim Werfen entscheidet die Seite.
* **Zahl** – die Grundwirkung tritt ein.
* **Wappen** – die Wirkung fällt stärker aus. Die Chance ist die Hälfte.
Die Grundwirkung kommt immer. Kein Zug ist verschenkt. Der Ausgang wird beim
Host ausgewürfelt und mit dem Zustand verschickt; die Drehung im HUD ist nur
die Ansicht dazu. Würfelphysik wäre hier nicht synchron zu bekommen.
### Die sechs Münzen
Jede Münze tut genau eins: Wissen geben, Druck machen oder der eigenen Hand
nachhelfen. **Keine würfelt an den Gegnern herum.** Nebel, Absenken und
Neuwurf sind deshalb raus - sie nahmen dem Lesen der anderen den Sinn.
| Münze | Art | Zahl | Wappen | Braucht Ansage |
|---|---|---|---|---|
| Spähen | Wissen | Von jedem Gegner wird ein zufälliger Würfel aufgedeckt | Zwei je Gegner | nein |
| Zählen | Wissen | Ein Fenster von drei, in dem die Zahl der angesagten Augenzahl liegt | Die genaue Zahl | ja |
| Falschspiel | Hand | Ein gewählter Würfel zeigt die angesagte Augenzahl | Ein zweiter, zufälliger auch | ja |
| Nachschub | Hand | Du bekommst einen Würfel dazu | Der neue zeigt sofort eine 6 | nein |
| Erhöhen | Druck | Die laufende Ansage steigt um 1; der Ansager steht dafür gerade | Um 2 | ja |
| Maulkorb | Druck | Der nächste Spieler muss diese Runde bieten, darf nicht anfechten | Alle Gegner | nein |
**Der Wurf verstärkt die Würfelzahl nie.** Sie ist die Kernressource; ein
Würfel mehr aus Glück würde die Partie kippen. *Nachschub* gibt einen Würfel -
das ist seine Grundwirkung, und Wappen macht daraus nur eine 6.
**Bonuswürfel gehen nur an den, der hinten liegt.** Nachschub kann werfen,
wer höchstens die halbe Startzahl hat; den Genau-Bonus bekommt nur ein Rufer
mit weniger Würfeln als der Ansager. Sonst kämen Würfel zurück, und der Tisch
würde nie leerer - `make sim` zeigte davor einzelne Partien mit über 50
Runden.
### Anzeige
* Oben rechts steht der **Gebotsverlauf** der Runde. Bluffs liest man daran,
wer wie schnell hochgegangen ist - nicht am letzten Gebot allein.
* Wer dran ist, hat einen leuchtenden Zonenrand, eine hinterlegte Zeile in
der Spielerliste und eine Zeile über der Ansageleiste.
* Über einer Münze erscheint sofort ihre Erklärung. Sie bleibt, solange die
Maus darauf liegt - auch wenn die Runde weiterläuft. Auf dem Handy öffnet
der erste Tipp die Erklärung, der zweite wirft.
* Unten rechts läuft das **Protokoll**: wer den Topf nimmt, was vom Tisch
fällt, wer geht und wer zurückkommt.
* Eine geworfene Münze fällt drehend in die Tischmitte. Wappen-Münzen tragen
einen geprägten Ring und leuchten stärker. Nimmt jemand den Topf, fliegen
die Münzen zu seiner Zone.
* Klänge (Würfel, Münze, Becher, Gewinn) werden beim Start berechnet. Es gibt
keine Tondateien, so wie es auch keine Bilddateien gibt.
* Kein Bild, kein Mesh aus einer Datei. Der Look ist **Low-Poly wie Knete**:
flache, pastellige Farben, runde Kanten, weiche Schatten, heller Himmel.
**Stoffe** (`stoffe.gd`) sind nur Farbe und Rauheit - was Holz von Filz
trennt, ist die Form. **Formen** (`formen.gd`) entstehen aus Profilen, die
um die Achse gedreht werden; Fasen sind Viertelkreise im Profil. Becher =
Kegelstumpf mit runden Kanten + Messingrand + Band. Der Plan steht in
`assets.md`.
* Auf dem Handy nur Querformat - ein Tisch von oben braucht Breite.
* Die Regeln stehen im Menü unter **Regeln**, inklusive aller sechs Münzen.
### Bots
Drei Stufen, im Menü wählbar: **leicht** ficht voreilig an und bietet
riskant, **schwer** bluffet öfter und zweifelt nur, wenn es sicher ist.
`make sim` misst sie: bei drei Plätzen mit je drei Würfeln dauert eine Partie
im Schnitt 6 (leicht) bis 14 (schwer) Runden; einzelne laufen bis 30. Zwölf
Partien je Stufe streuen stark - für echtes Abstimmen fünfzig nehmen.
Bots rechnen mit der Binomialverteilung, ob eine Ansage hält. Aber sie
bluffen auch: fehlt ein sicheres Gebot, setzen sie in 40 % der Fälle das
kleinste gültige, statt anzufechten. Und sie werfen Münzen mit Absicht -
Nachschub, wenn sie hinten liegen; Zählen oder Spähen, wenn es knapp ist;
Erhöhen vor dem Anfechten; Maulkorb vor dem eigenen Bluff. Liegt die Ansage
vermutlich haarscharf, rufen sie „Genau".
## Aufbau
| Datei | Inhalt |
|---|---|
| `scripts/net.gd` | Autoload: WebSocket-Verbindung, Räume, Lobby, Spielstart |
| `scripts/boosters.gd` | Autoload: Münzkatalog mit Grund- und Wappenwirkung |
| `scripts/game.gd` | Spiellogik (host-autoritativ), RPCs, Zustandsabgleich |
| `scripts/muenzen.gd` | Münzen: ziehen, werfen, Wirkung, Bot-Absicht, Topf |
| `scripts/aufdecken.gd` | Aufdecken in Etappen, Genau, Topfvergabe |
| `scripts/table.gd` | Aufbau der 3D-Szene: Tisch, Würfelzonen, Becher, Kamera |
| `scripts/die.gd` | Physik-Würfel, Ablesen der Augenzahl aus der Ruhelage |
| `scripts/ai.gd` | Bot-Entscheidungen (Binomialabschätzung für Gebot/Anfechtung) |
| `scripts/hud.gd` | Spiel-UI |
| `scripts/klang.gd` | Autoload: berechnete Klänge, keine Dateien |
| `scripts/kampagne.gd` | Autoload: Seekarte, Kapitäne, Wertung, Speicherstand |
| `scripts/stoffe.gd` | Flache Farben der Knete-Palette, ein Material je Stoff |
| `scripts/formen.gd` | Gedrehte Profile mit Fasen: Scheiben, Becher, Laterne |
| `scripts/icons.gd` | Gezeichnete Münz- und Würfelbilder fürs HUD |
| `scripts/hud_panels.gd` | Vorrat, Topf, Erklärung, Verlauf, Protokoll |
| `tests/` | Headless-Tests, `make test` (~30 s); `make sim` misst die Bots - siehe `tests/README.md` |
| `scripts/menu.gd` | Startmenü und Lobby |
Die gesamte Spiellogik und die Würfelphysik laufen beim Host. Clients erhalten
nur ihre eigenen Würfel als Transform-Stream, gegnerische Werte erst beim
Aufdecken – Mitlesen fremder Würfel ist damit nicht möglich.