218 lines
11 KiB
Markdown
218 lines
11 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.
|
||
* 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.
|
||
|
||
Wer keine Würfel mehr hat, scheidet aus. Der Letzte gewinnt.
|
||
|
||
## 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 danach einen Würfel abgibt, **bekommt eine Münze**. Das ist der einzige
|
||
Nachschub, also bekommt der etwas zurück, der gerade hinten liegt.
|
||
* Geworfen wird **jederzeit während der Ansagen**, nicht nur am eigenen Zug.
|
||
* Mehr als **drei** Münzen passen nicht in den Vorrat.
|
||
* Der Vorrat liegt offen unten links; ein Klick wirft die Münze sofort. Nur
|
||
*Feinschliff* und *Glücksgriff* fragen noch, welcher Würfel gemeint ist.
|
||
|
||
### Der Wurf
|
||
|
||
Beim Ausspielen wird die Münze geworfen und landet in der Mitte.
|
||
|
||
* **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, und die Entscheidung,
|
||
wann geworfen wird, bleibt beim Spieler. Der Wurf selbst ist reine Zugabe.
|
||
|
||
Der Ausgang wird beim Host ausgewürfelt und mit dem Zustand verschickt. Die
|
||
Drehung im HUD ist nur die Ansicht dazu - sie endet auf der Seite, die schon
|
||
feststeht. Würfelphysik wäre hier nicht synchron zu bekommen.
|
||
|
||
**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. Verstärkt
|
||
werden sonst nur Beträge und Reichweiten.
|
||
|
||
| Münze | Art | Zahl | Wappen |
|
||
|---|---|---|---|
|
||
| Neuwurf | Buff | Alle eigenen Würfel neu werfen | Danach wird einer davon zur 6 |
|
||
| Feinschliff | Buff | Einen gewählten eigenen Würfel neu werfen | Ein zweiter, zufälliger kommt mit |
|
||
| Glücksgriff | Buff | Ein gewählter Würfel wird zur 6 | Ein zweiter, zufälliger wird auch zur 6 |
|
||
| Nachschub | Buff | Du bekommst einen Würfel dazu | Der neue Würfel zeigt sofort eine 6 |
|
||
| Widerruf | Buff | Hebt Nebel, Störfeuer und Maulkorb auf, die auf dir liegen | Die Münze bleibt dir erhalten |
|
||
| Spähen | Buff | Von jedem Gegner wird ein zufälliger Würfel aufgedeckt | Es sind zwei je Gegner |
|
||
| Erhöhen | Debuff | Die laufende Ansage steigt um 1 | Sie steigt um 2 |
|
||
| Absenken | Debuff | Bei jedem Gegner sinkt ein zufälliger Würfel um 1 | Er sinkt um 2 |
|
||
| Nebel | Debuff | Jeder Gegner sieht einen eigenen Würfel nicht mehr | Es sind zwei Würfel |
|
||
| Störfeuer | Debuff | Gegner müssen diese Runde um mindestens 2 erhöhen | Mindestens 3 |
|
||
| Maulkorb | Debuff | Wer gerade dran ist, muss bieten und darf nicht anfechten | Alle Gegner müssen bieten |
|
||
|
||
Welche Wirkungen es am Ende gibt, steht noch nicht fest. Der Wurf und die
|
||
Verstärkung sind der Kern; die Tabelle ist die aktuelle Füllung.
|
||
|
||
## 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/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/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.
|