324 lines
17 KiB
Markdown
324 lines
17 KiB
Markdown
# 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.
|