update
This commit is contained in:
99
README.md
99
README.md
@@ -148,7 +148,20 @@ 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
|
der Bot-Logik. Ohne dieses Limit würde ein abwesender oder getrennter
|
||||||
Mitspieler den ganzen Tisch dauerhaft blockieren.
|
Mitspieler den ganzen Tisch dauerhaft blockieren.
|
||||||
|
|
||||||
Wer keine Würfel mehr hat, scheidet aus. Der Letzte gewinnt.
|
**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 solo ein **Nochmal** mit denselben Bots. Im Netz führt
|
||||||
|
der Weg zurück ins Menü.
|
||||||
|
|
||||||
|
Wer keine Würfel mehr hat, scheidet aus und schaut zu. Der Letzte gewinnt.
|
||||||
|
|
||||||
## Münzen
|
## Münzen
|
||||||
|
|
||||||
@@ -156,48 +169,77 @@ Münzen ersetzen die frühere Booster-Phase. Die kostete jede Runde eine eigene
|
|||||||
Wartezeit und riss den Ablauf auseinander.
|
Wartezeit und riss den Ablauf auseinander.
|
||||||
|
|
||||||
* Jeder startet mit **einer** Münze.
|
* Jeder startet mit **einer** Münze.
|
||||||
* Wer danach einen Würfel abgibt, **bekommt eine Münze**. Das ist der einzige
|
* 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.
|
Nachschub, also bekommt der etwas zurück, der gerade hinten liegt.
|
||||||
* Geworfen wird **jederzeit während der Ansagen**, nicht nur am eigenen Zug.
|
* Geworfen wird **nur am eigenen Zug, vor der Ansage**. So ist der Zug eine
|
||||||
* Mehr als **drei** Münzen passen nicht in den Vorrat.
|
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
|
* 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.
|
*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
|
### Der Wurf
|
||||||
|
|
||||||
Beim Ausspielen wird die Münze geworfen und landet in der Mitte.
|
Beim Werfen entscheidet die Seite.
|
||||||
|
|
||||||
* **Zahl** – die Grundwirkung tritt ein.
|
* **Zahl** – die Grundwirkung tritt ein.
|
||||||
* **Wappen** – die Wirkung fällt stärker aus. Die Chance ist die Hälfte.
|
* **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,
|
Die Grundwirkung kommt immer. Kein Zug ist verschenkt. Der Ausgang wird beim
|
||||||
wann geworfen wird, bleibt beim Spieler. Der Wurf selbst ist reine Zugabe.
|
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.
|
||||||
|
|
||||||
Der Ausgang wird beim Host ausgewürfelt und mit dem Zustand verschickt. Die
|
### Die sechs Münzen
|
||||||
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.
|
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
|
**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 -
|
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
|
das ist seine Grundwirkung, und Wappen macht daraus nur eine 6.
|
||||||
werden sonst nur Beträge und Reichweiten.
|
|
||||||
|
|
||||||
| Münze | Art | Zahl | Wappen |
|
### Anzeige
|
||||||
|---|---|---|---|
|
|
||||||
| 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
|
* Oben rechts steht der **Gebotsverlauf** der Runde. Bluffs liest man daran,
|
||||||
Verstärkung sind der Kern; die Tabelle ist die aktuelle Füllung.
|
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.
|
||||||
|
* Klänge (Würfel, Münze, Becher, Gewinn) werden beim Start berechnet. Es gibt
|
||||||
|
keine Tondateien, so wie es auch keine Bilddateien gibt.
|
||||||
|
* Die Regeln stehen im Menü unter **Regeln**.
|
||||||
|
|
||||||
|
### Bots
|
||||||
|
|
||||||
|
Drei Stufen, im Menü wählbar: **leicht** ficht voreilig an und bietet
|
||||||
|
riskant, **schwer** bluffet öfter und zweifelt nur, wenn es sicher ist.
|
||||||
|
|
||||||
|
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
|
## Aufbau
|
||||||
|
|
||||||
@@ -210,6 +252,7 @@ Verstärkung sind der Kern; die Tabelle ist die aktuelle Füllung.
|
|||||||
| `scripts/die.gd` | Physik-Würfel, Ablesen der Augenzahl aus der Ruhelage |
|
| `scripts/die.gd` | Physik-Würfel, Ablesen der Augenzahl aus der Ruhelage |
|
||||||
| `scripts/ai.gd` | Bot-Entscheidungen (Binomialabschätzung für Gebot/Anfechtung) |
|
| `scripts/ai.gd` | Bot-Entscheidungen (Binomialabschätzung für Gebot/Anfechtung) |
|
||||||
| `scripts/hud.gd` | Spiel-UI |
|
| `scripts/hud.gd` | Spiel-UI |
|
||||||
|
| `scripts/klang.gd` | Autoload: berechnete Klänge, keine Dateien |
|
||||||
| `scripts/menu.gd` | Startmenü und Lobby |
|
| `scripts/menu.gd` | Startmenü und Lobby |
|
||||||
|
|
||||||
Die gesamte Spiellogik und die Würfelphysik laufen beim Host. Clients erhalten
|
Die gesamte Spiellogik und die Würfelphysik laufen beim Host. Clients erhalten
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ config/features=PackedStringArray("4.7", "Mobile")
|
|||||||
|
|
||||||
BoosterDB="*res://scripts/boosters.gd"
|
BoosterDB="*res://scripts/boosters.gd"
|
||||||
Net="*res://scripts/net.gd"
|
Net="*res://scripts/net.gd"
|
||||||
|
Klang="*res://scripts/klang.gd"
|
||||||
|
|
||||||
[display]
|
[display]
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,43 @@ static func _own(values: Array, face: int) -> int:
|
|||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
## Wahrscheinlichkeit, dass ein Gebot (count, face) genau stimmt.
|
||||||
|
static func exact_chance(values: Array, unknown: int, count: int, face: int) -> float:
|
||||||
|
var k := count - _own(values, face)
|
||||||
|
if k < 0 or k > unknown:
|
||||||
|
return 0.0
|
||||||
|
return _pmf(unknown, k, 1.0 / 6.0)
|
||||||
|
|
||||||
|
|
||||||
## Wahrscheinlichkeit, dass ein Gebot (count, face) hält.
|
## Wahrscheinlichkeit, dass ein Gebot (count, face) hält.
|
||||||
static func chance(values: Array, unknown: int, count: int, face: int) -> float:
|
static func chance(values: Array, unknown: int, count: int, face: int) -> float:
|
||||||
return tail(unknown, count - _own(values, face), 1.0 / 6.0)
|
return tail(unknown, count - _own(values, face), 1.0 / 6.0)
|
||||||
|
|
||||||
|
|
||||||
|
## 0 = leicht, 1 = normal, 2 = schwer. Das Menü setzt es vor dem Start.
|
||||||
|
static var stufe := 1
|
||||||
|
|
||||||
|
## Je Stufe vier Regler:
|
||||||
|
## bluff - wie oft er blufft, statt ein glaubwürdiges Gebot anzufechten.
|
||||||
|
## Ohne das wäre er ein reiner Rechner, und den liest man.
|
||||||
|
## genau - ab welcher Trefferchance er "Genau" ruft.
|
||||||
|
## zweifel - unter welcher Haltechance er anficht. Hoch heißt: voreilig.
|
||||||
|
## sicher - welche Chance ein eigenes Gebot mindestens haben muss.
|
||||||
|
const STUFEN := [
|
||||||
|
{"bluff": 0.15, "genau": 0.35, "zweifel": 0.40, "sicher": 0.30},
|
||||||
|
{"bluff": 0.40, "genau": 0.22, "zweifel": 0.30, "sicher": 0.42},
|
||||||
|
{"bluff": 0.50, "genau": 0.18, "zweifel": 0.24, "sicher": 0.50},
|
||||||
|
]
|
||||||
|
## Wie oft er ein sicheres Gebot einen Schritt höher setzt, als nötig wäre.
|
||||||
|
const FRECH := 0.12
|
||||||
|
|
||||||
|
|
||||||
|
static func _regler() -> Dictionary:
|
||||||
|
return STUFEN[clampi(stufe, 0, STUFEN.size() - 1)]
|
||||||
|
|
||||||
|
|
||||||
static func choose_action(values: Array, total_dice: int, bid: Dictionary,
|
static func choose_action(values: Array, total_dice: int, bid: Dictionary,
|
||||||
min_raise: int, must_bid: bool, rng: RandomNumberGenerator) -> Dictionary:
|
must_bid: bool, rng: RandomNumberGenerator) -> Dictionary:
|
||||||
var known: Array = []
|
var known: Array = []
|
||||||
for v in values:
|
for v in values:
|
||||||
if v > 0:
|
if v > 0:
|
||||||
@@ -47,26 +77,40 @@ static func choose_action(values: Array, total_dice: int, bid: Dictionary,
|
|||||||
var op := _opening(known, unknown, rng)
|
var op := _opening(known, unknown, rng)
|
||||||
return {"type": "bid", "count": op.count, "face": op.face}
|
return {"type": "bid", "count": op.count, "face": op.face}
|
||||||
|
|
||||||
|
var r := _regler()
|
||||||
var hold := chance(known, unknown, bid.count, bid.face)
|
var hold := chance(known, unknown, bid.count, bid.face)
|
||||||
var raise_ = _best_raise(known, unknown, total_dice, bid, min_raise)
|
var raise_ = _best_raise(known, unknown, total_dice, bid, float(r.sicher))
|
||||||
|
var low := lowest_raise(bid, total_dice)
|
||||||
|
|
||||||
if not must_bid:
|
if not must_bid:
|
||||||
var doubt := 0.30 + rng.randf_range(-0.07, 0.07)
|
# Liegt die Ansage vermutlich haarscharf, lohnt "Genau" mehr als die
|
||||||
|
# Anfechtung: der Ansager verliert, und man bekommt selbst einen dazu.
|
||||||
|
if exact_chance(known, unknown, bid.count, bid.face) > float(r.genau) and hold < 0.6:
|
||||||
|
return {"type": "exact"}
|
||||||
|
var doubt := float(r.zweifel) + rng.randf_range(-0.07, 0.07)
|
||||||
if hold < doubt and (raise_.is_empty() or raise_.p < 0.32):
|
if hold < doubt and (raise_.is_empty() or raise_.p < 0.32):
|
||||||
return {"type": "challenge"}
|
return {"type": "challenge"}
|
||||||
if raise_.is_empty():
|
if raise_.is_empty():
|
||||||
if must_bid:
|
# Kein sicheres Gebot mehr. Anfechten wäre ehrlich - aber wer immer
|
||||||
# Notgebot: minimal erhöhen, auch wenn unwahrscheinlich
|
# ehrlich ist, wird gelesen. Also ab und zu bluffen, solange es noch
|
||||||
var c: int = int(bid.count) + maxi(1, min_raise)
|
# ein gültiges Gebot gibt.
|
||||||
if c <= total_dice:
|
if not low.is_empty() and (must_bid or rng.randf() < float(r.bluff)):
|
||||||
return {"type": "bid", "count": c, "face": bid.face}
|
return {"type": "bid", "count": low.count, "face": low.face}
|
||||||
if min_raise <= 1 and int(bid.face) < 6:
|
|
||||||
return {"type": "bid", "count": int(bid.count), "face": int(bid.face) + 1}
|
|
||||||
# Es gibt kein gültiges Gebot mehr - dann bleibt nur die Anfechtung.
|
|
||||||
return {"type": "challenge"}
|
return {"type": "challenge"}
|
||||||
|
if rng.randf() < FRECH and int(raise_.count) + 1 <= total_dice:
|
||||||
|
return {"type": "bid", "count": int(raise_.count) + 1, "face": raise_.face}
|
||||||
return {"type": "bid", "count": raise_.count, "face": raise_.face}
|
return {"type": "bid", "count": raise_.count, "face": raise_.face}
|
||||||
|
|
||||||
|
|
||||||
|
## Das kleinste Gebot, das noch über dem liegenden steht ({} = keins mehr).
|
||||||
|
static func lowest_raise(bid: Dictionary, total: int) -> Dictionary:
|
||||||
|
if int(bid.face) < 6:
|
||||||
|
return {"count": int(bid.count), "face": int(bid.face) + 1}
|
||||||
|
if int(bid.count) + 1 <= total:
|
||||||
|
return {"count": int(bid.count) + 1, "face": 1}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
static func _opening(known: Array, unknown: int, rng: RandomNumberGenerator) -> Dictionary:
|
static func _opening(known: Array, unknown: int, rng: RandomNumberGenerator) -> Dictionary:
|
||||||
var best_face := rng.randi_range(1, 6)
|
var best_face := rng.randi_range(1, 6)
|
||||||
var best := -1
|
var best := -1
|
||||||
@@ -79,14 +123,15 @@ static func _opening(known: Array, unknown: int, rng: RandomNumberGenerator) ->
|
|||||||
return {"count": count, "face": best_face}
|
return {"count": count, "face": best_face}
|
||||||
|
|
||||||
|
|
||||||
static func _best_raise(known: Array, unknown: int, total: int, bid: Dictionary, min_raise: int) -> Dictionary:
|
static func _best_raise(known: Array, unknown: int, total: int, bid: Dictionary,
|
||||||
|
sicher: float) -> Dictionary:
|
||||||
var best := {}
|
var best := {}
|
||||||
for face in range(1, 7):
|
for face in range(1, 7):
|
||||||
for count in range(1, total + 1):
|
for count in range(1, total + 1):
|
||||||
if not _is_higher(count, face, bid, min_raise):
|
if not _is_higher(count, face, bid):
|
||||||
continue
|
continue
|
||||||
var p := chance(known, unknown, count, face)
|
var p := chance(known, unknown, count, face)
|
||||||
if p < 0.42:
|
if p < sicher:
|
||||||
continue
|
continue
|
||||||
if best.is_empty() or count < best.count or (count == best.count and p > best.p):
|
if best.is_empty() or count < best.count or (count == best.count and p > best.p):
|
||||||
best = {"count": count, "face": face, "p": p}
|
best = {"count": count, "face": face, "p": p}
|
||||||
@@ -94,22 +139,16 @@ static func _best_raise(known: Array, unknown: int, total: int, bid: Dictionary,
|
|||||||
return best
|
return best
|
||||||
|
|
||||||
|
|
||||||
static func _is_higher(count: int, face: int, bid: Dictionary, min_raise: int) -> bool:
|
static func _is_higher(count: int, face: int, bid: Dictionary) -> bool:
|
||||||
if count >= bid.count + min_raise:
|
if count > bid.count:
|
||||||
return true
|
return true
|
||||||
if min_raise > 1:
|
|
||||||
return false
|
|
||||||
return count == bid.count and face > bid.face
|
return count == bid.count and face > bid.face
|
||||||
|
|
||||||
|
|
||||||
## Welcher eigene Würfel soll dran glauben? Für Münzen mit Zielwahl.
|
## Welcher eigene Würfel soll umgedreht werden? Einer, der die Augenzahl
|
||||||
static func target_for(id: String, values: Array) -> int:
|
## noch nicht zeigt - sonst wäre die Münze verschenkt.
|
||||||
if values.is_empty():
|
static func target_for(values: Array, face: int) -> int:
|
||||||
return 0
|
|
||||||
# Der schwächste Würfel: den neu zu werfen oder zur 6 zu machen bringt
|
|
||||||
# am meisten.
|
|
||||||
var idx := 0
|
|
||||||
for i in values.size():
|
for i in values.size():
|
||||||
if values[i] < values[idx]:
|
if values[i] != face:
|
||||||
idx = i
|
return i
|
||||||
return idx
|
return 0
|
||||||
|
|||||||
@@ -1,91 +1,66 @@
|
|||||||
extends Node
|
extends Node
|
||||||
## Katalog aller Münzen.
|
## Katalog aller Münzen.
|
||||||
##
|
##
|
||||||
## Münzen gibt es nicht rundenweise zur Auswahl. Wer einen Würfel verliert,
|
## Sechs Stück, jede mit einem klaren Zweck: entweder sie gibt Wissen, oder sie
|
||||||
## bekommt eine. Gespielt wird jederzeit während der Ansagen - deshalb wirkt
|
## macht Druck, oder sie hilft der eigenen Hand nach. Keine würfelt an den
|
||||||
## jede Münze sofort und braucht keine eigene Phase mehr.
|
## Gegnern herum - das Lesen der anderen bleibt der Kern des Spiels.
|
||||||
##
|
##
|
||||||
## Beim Ausspielen wird die Münze geworfen. Wappen oben heißt verstärkt. Die
|
## Geworfen wird nur am eigenen Zug, vor der Ansage. Die Münze landet im Topf
|
||||||
## Grundwirkung kommt immer, der Wurf legt nur eine Schippe drauf. Deshalb ist
|
## in der Mitte. Wer das Aufdecken gewinnt, nimmt den Topf. Damit kostet jede
|
||||||
## kein Zug verschenkt, und die Entscheidung bleibt beim Spieler.
|
## Münze etwas, und das Aufdecken hat einen Einsatz.
|
||||||
|
##
|
||||||
|
## Beim Werfen entscheidet die Seite: Wappen verstärkt, Zahl lässt es bei der
|
||||||
|
## Grundwirkung. Die Grundwirkung kommt immer - kein Zug ist verschenkt.
|
||||||
|
|
||||||
enum Kind { BUFF, DEBUFF }
|
enum Kind { BUFF, DEBUFF }
|
||||||
|
|
||||||
## Mehr als das passt nicht auf die Hand; sonst hortet man bis zum Schluss.
|
## Mehr als das passt nicht in den Vorrat; sonst hortet man bis zum Schluss.
|
||||||
const HANDLIMIT := 3
|
const HANDLIMIT := 4
|
||||||
|
## Mehr liegt nie im Topf. Die älteste Münze fällt vom Tisch, sonst wächst ein
|
||||||
|
## Haufen, den keiner mehr abholen kann.
|
||||||
|
const TOPF_MAX := 6
|
||||||
|
|
||||||
## Eine Münze hat zwei Seiten, also die Hälfte.
|
## Eine Münze hat zwei Seiten, also die Hälfte.
|
||||||
const WAPPEN_CHANCE := 0.5
|
const WAPPEN_CHANCE := 0.5
|
||||||
|
|
||||||
## Die Würfelzahl wird bewusst nie verstärkt. Sie ist die Kernressource -
|
## `target`: fragt nach einem eigenen Würfel. `gebot`: geht nur, wenn eine
|
||||||
## ein Würfel mehr aus Glück würde die Partie kippen.
|
## Ansage auf dem Tisch liegt.
|
||||||
const ALL := [
|
const ALL := [
|
||||||
{
|
{
|
||||||
"id": "reroll_all", "kind": Kind.BUFF, "target": false,
|
"id": "peek", "kind": Kind.BUFF, "target": false, "gebot": false,
|
||||||
"name": "Neuwurf",
|
|
||||||
"desc": "Wirf alle deine Würfel noch einmal neu.",
|
|
||||||
"stark": "Danach wird einer davon zur 6.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "reroll_one", "kind": Kind.BUFF, "target": true,
|
|
||||||
"name": "Feinschliff",
|
|
||||||
"desc": "Wirf einen selbst gewählten Würfel neu.",
|
|
||||||
"stark": "Ein zweiter, zufälliger Würfel kommt mit.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "set_six", "kind": Kind.BUFF, "target": true,
|
|
||||||
"name": "Glücksgriff",
|
|
||||||
"desc": "Ein gewählter Würfel wird zur 6.",
|
|
||||||
"stark": "Ein zweiter, zufälliger Würfel wird auch zur 6.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "gain_die", "kind": Kind.BUFF, "target": false,
|
|
||||||
"name": "Nachschub",
|
|
||||||
"desc": "Du bekommst einen Würfel dazu.",
|
|
||||||
"stark": "Der neue Würfel zeigt sofort eine 6.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cancel", "kind": Kind.BUFF, "target": false,
|
|
||||||
"name": "Widerruf",
|
|
||||||
"desc": "Hebt Nebel, Störfeuer und Maulkorb auf, die auf dir liegen.",
|
|
||||||
"stark": "Die Münze bleibt dir erhalten.",
|
|
||||||
"behalten": true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "peek", "kind": Kind.BUFF, "target": false,
|
|
||||||
"name": "Spähen",
|
"name": "Spähen",
|
||||||
"desc": "Von jedem Gegner wird dir ein zufälliger Würfel aufgedeckt.",
|
"desc": "Von jedem Gegner wird dir ein zufälliger Würfel aufgedeckt.",
|
||||||
"stark": "Es sind zwei Würfel je Gegner.",
|
"stark": "Es sind zwei Würfel je Gegner.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "bid_up", "kind": Kind.DEBUFF, "target": false,
|
"id": "count", "kind": Kind.BUFF, "target": false, "gebot": true,
|
||||||
|
"name": "Zählen",
|
||||||
|
"desc": "Du erfährst ungefähr, wie oft die angesagte Augenzahl am Tisch liegt.",
|
||||||
|
"stark": "Du erfährst die genaue Zahl.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "set_face", "kind": Kind.BUFF, "target": true, "gebot": true,
|
||||||
|
"name": "Falschspiel",
|
||||||
|
"desc": "Ein gewählter Würfel zeigt die angesagte Augenzahl.",
|
||||||
|
"stark": "Ein zweiter, zufälliger Würfel auch.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gain_die", "kind": Kind.BUFF, "target": false, "gebot": false,
|
||||||
|
"name": "Nachschub",
|
||||||
|
"desc": "Du bekommst einen Würfel dazu.",
|
||||||
|
"stark": "Der neue Würfel zeigt sofort eine 6.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bid_up", "kind": Kind.DEBUFF, "target": false, "gebot": true,
|
||||||
"name": "Erhöhen",
|
"name": "Erhöhen",
|
||||||
"desc": "Die laufende Ansage steigt um 1.",
|
"desc": "Die laufende Ansage steigt um 1. Der Ansager muss dafür geradestehen.",
|
||||||
"stark": "Sie steigt um 2.",
|
"stark": "Sie steigt um 2.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "enemy_lower", "kind": Kind.DEBUFF, "target": false,
|
"id": "gag", "kind": Kind.DEBUFF, "target": false, "gebot": false,
|
||||||
"name": "Absenken",
|
|
||||||
"desc": "Bei jedem Gegner sinkt ein zufälliger Würfel um 1 (1 bleibt 1).",
|
|
||||||
"stark": "Er sinkt um 2.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "fog", "kind": Kind.DEBUFF, "target": false,
|
|
||||||
"name": "Nebel",
|
|
||||||
"desc": "Jeder Gegner verliert die Sicht auf einen seiner eigenen Würfel.",
|
|
||||||
"stark": "Es sind zwei Würfel.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "raise_min", "kind": Kind.DEBUFF, "target": false,
|
|
||||||
"name": "Störfeuer",
|
|
||||||
"desc": "Alle Gegner müssen diese Runde um mindestens 2 erhöhen.",
|
|
||||||
"stark": "Es müssen mindestens 3 sein.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "gag", "kind": Kind.DEBUFF, "target": false,
|
|
||||||
"name": "Maulkorb",
|
"name": "Maulkorb",
|
||||||
"desc": "Wer gerade dran ist, muss bieten und darf nicht anfechten.",
|
"desc": "Der nächste Spieler muss diese Runde bieten und darf nicht anfechten.",
|
||||||
"stark": "Alle Gegner müssen bieten.",
|
"stark": "Das gilt für alle Gegner.",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -101,9 +76,8 @@ func needs_target(id: String) -> bool:
|
|||||||
return get_booster(id).get("target", false)
|
return get_booster(id).get("target", false)
|
||||||
|
|
||||||
|
|
||||||
## Münzen, die bei Wappen auf der Hand bleiben, statt in die Mitte zu wandern.
|
func needs_bid(id: String) -> bool:
|
||||||
func keeps_on_wappen(id: String) -> bool:
|
return get_booster(id).get("gebot", false)
|
||||||
return get_booster(id).get("behalten", false)
|
|
||||||
|
|
||||||
|
|
||||||
## Eine zufällige Münze, für den, der gerade einen Würfel verloren hat.
|
## Eine zufällige Münze, für den, der gerade einen Würfel verloren hat.
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ const PIPS := {
|
|||||||
# damit ein Würfel nur ein einziges Renderobjekt ist.
|
# damit ein Würfel nur ein einziges Renderobjekt ist.
|
||||||
static var _mesh: ArrayMesh
|
static var _mesh: ArrayMesh
|
||||||
static var _mat: StandardMaterial3D
|
static var _mat: StandardMaterial3D
|
||||||
|
static var _gold: StandardMaterial3D ## fürs Zählen beim Aufdecken
|
||||||
|
var _body: MeshInstance3D
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -125,10 +127,22 @@ func _build_visuals() -> void:
|
|||||||
# liegt der Glanzpunkt genau auf der Fläche und hellt die Augen auf.
|
# liegt der Glanzpunkt genau auf der Fläche und hellt die Augen auf.
|
||||||
_mat.roughness = 0.9
|
_mat.roughness = 0.9
|
||||||
_mat.metallic_specular = 0.0
|
_mat.metallic_specular = 0.0
|
||||||
var body := MeshInstance3D.new()
|
if _gold == null:
|
||||||
body.mesh = _mesh
|
# Vertexfarben bleiben, werden nur gold getönt - die Augen bleiben dunkel.
|
||||||
body.material_override = _mat
|
_gold = _mat.duplicate()
|
||||||
add_child(body)
|
_gold.albedo_color = Color(1.0, 0.82, 0.35)
|
||||||
|
_gold.emission_enabled = true
|
||||||
|
_gold.emission = Color(0.9, 0.65, 0.2)
|
||||||
|
_gold.emission_energy_multiplier = 0.5
|
||||||
|
_body = MeshInstance3D.new()
|
||||||
|
_body.mesh = _mesh
|
||||||
|
_body.material_override = _mat
|
||||||
|
add_child(_body)
|
||||||
|
|
||||||
|
|
||||||
|
## Beim Zählen leuchtet jeder passende Würfel kurz auf.
|
||||||
|
func hervorheben(on: bool) -> void:
|
||||||
|
_body.material_override = _gold if on else _mat
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ Zustand
|
# ------------------------------------------------------------ Zustand
|
||||||
|
|||||||
388
scripts/game.gd
388
scripts/game.gd
@@ -3,7 +3,11 @@ extends Node3D
|
|||||||
|
|
||||||
const START_DICE := 5
|
const START_DICE := 5
|
||||||
const BID_TIMEOUT := 30.0
|
const BID_TIMEOUT := 30.0
|
||||||
const REVEAL_PAUSE := 5.0 ## Anzeigedauer des Rundenergebnisses
|
## Aufdecken in drei Etappen: Becher hoch, zählen, Urteil. So baut sich die
|
||||||
|
## Spannung auf, statt dass alles auf einmal dasteht.
|
||||||
|
const ZEIT_BECHER := 1.3
|
||||||
|
const ZEIT_ZAEHLEN := 2.2 ## reicht, um bis zu 15 passende Würfel aufleuchten zu lassen
|
||||||
|
const ZEIT_URTEIL := 2.8
|
||||||
const WURF_ZEIT := 1.1 ## wie lange die geworfene Münze zu sehen ist,
|
const WURF_ZEIT := 1.1 ## wie lange die geworfene Münze zu sehen ist,
|
||||||
## bevor ihre Wirkung einsetzt
|
## bevor ihre Wirkung einsetzt
|
||||||
|
|
||||||
@@ -20,14 +24,16 @@ var my_values: Array = [] ## Client-Sicht (-1 = vernebelt)
|
|||||||
var bid: Dictionary = {}
|
var bid: Dictionary = {}
|
||||||
var turn := 0
|
var turn := 0
|
||||||
var starter := 0
|
var starter := 0
|
||||||
var fogged: Dictionary = {} ## seat -> Array[int] verdeckter Würfel
|
|
||||||
var peeked: Dictionary = {} ## viewer -> {target_seat: Array[[idx, value]]}
|
var peeked: Dictionary = {} ## viewer -> {target_seat: Array[[idx, value]]}
|
||||||
var min_raise: Dictionary = {} ## seat -> int
|
var gezaehlt: Dictionary = {} ## seat -> {face: [von, bis]}, durch Zählen
|
||||||
|
var verlauf: Array = [] ## alle Ansagen dieser Runde, {seat, count, face, erhoeht}
|
||||||
var must_bid: Dictionary = {} ## seat -> bool
|
var must_bid: Dictionary = {} ## seat -> bool
|
||||||
var hand: Dictionary = {} ## seat -> Array[String], Münzen beim Spieler
|
var hand: Dictionary = {} ## seat -> Array[String], Münzen beim Spieler
|
||||||
var mitte: Dictionary = {} ## seat -> zuletzt in die Mitte gelegte Münze
|
var topf: Array = [] ## geworfene Münzen, {seat, id, stark, nr}; bleibt liegen, bis ihn einer nimmt
|
||||||
var _muenze_laeuft := false ## eine Münze wird gerade aufgelöst
|
var _muenze_laeuft := false ## eine Münze wird gerade aufgelöst
|
||||||
var _wurf_nr := 0 ## zählt die Würfe, damit das HUD neue erkennt
|
var _wurf_nr := 0 ## zählt die Würfe übers Spiel, damit das HUD neue erkennt
|
||||||
|
var _klang_phase := "" ## letzte Phase, für die ein Klang lief
|
||||||
|
var _gezaehlt_gezeigt := false ## Hervorhebung beim Aufdecken lief schon
|
||||||
var last_bids: Dictionary = {} ## seat -> letzte Ansage dieser Runde
|
var last_bids: Dictionary = {} ## seat -> letzte Ansage dieser Runde
|
||||||
var log_lines: Array = []
|
var log_lines: Array = []
|
||||||
var phase := "lobby"
|
var phase := "lobby"
|
||||||
@@ -48,6 +54,7 @@ func _ready() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func setup(cfg: Dictionary) -> void:
|
func setup(cfg: Dictionary) -> void:
|
||||||
|
DiceAI.stufe = int(cfg.get("stufe", 1))
|
||||||
seats.clear()
|
seats.clear()
|
||||||
var peers: Array = cfg.peers
|
var peers: Array = cfg.peers
|
||||||
for p in peers:
|
for p in peers:
|
||||||
@@ -76,6 +83,8 @@ func setup(cfg: Dictionary) -> void:
|
|||||||
add_child(hud)
|
add_child(hud)
|
||||||
hud.bid_pressed.connect(_on_bid_pressed)
|
hud.bid_pressed.connect(_on_bid_pressed)
|
||||||
hud.challenge_pressed.connect(_on_challenge_pressed)
|
hud.challenge_pressed.connect(_on_challenge_pressed)
|
||||||
|
hud.exact_pressed.connect(_on_exact_pressed)
|
||||||
|
hud.rematch_pressed.connect(_on_rematch)
|
||||||
hud.booster_chosen.connect(_on_booster_chosen)
|
hud.booster_chosen.connect(_on_booster_chosen)
|
||||||
hud.quit_pressed.connect(_on_quit)
|
hud.quit_pressed.connect(_on_quit)
|
||||||
hud.pause_changed.connect(_on_pause_changed)
|
hud.pause_changed.connect(_on_pause_changed)
|
||||||
@@ -200,13 +209,11 @@ func _phase_roll() -> void:
|
|||||||
deadline = 0.0
|
deadline = 0.0
|
||||||
reveal_total = 0
|
reveal_total = 0
|
||||||
bid = {}
|
bid = {}
|
||||||
fogged.clear()
|
|
||||||
last_bids.clear()
|
last_bids.clear()
|
||||||
peeked.clear()
|
peeked.clear()
|
||||||
min_raise.clear()
|
gezaehlt.clear()
|
||||||
|
verlauf.clear()
|
||||||
must_bid.clear()
|
must_bid.clear()
|
||||||
mitte.clear()
|
|
||||||
_wurf_nr = 0
|
|
||||||
banner = ""
|
banner = ""
|
||||||
_log("--- Neue Runde ---")
|
_log("--- Neue Runde ---")
|
||||||
|
|
||||||
@@ -275,14 +282,16 @@ func _zieh_muenze(seat: int) -> void:
|
|||||||
_log("%s bekommt %s." % [seats[seat].name, BoosterDB.get_booster(id).name])
|
_log("%s bekommt %s." % [seats[seat].name, BoosterDB.get_booster(id).name])
|
||||||
|
|
||||||
|
|
||||||
## Ausspielen heißt: Münze in die Mitte legen und werfen. Wappen verstärkt,
|
## Ausspielen heißt: Münze in den Topf werfen. Geht nur am eigenen Zug, bevor
|
||||||
## Zahl lässt es bei der Grundwirkung. Beides hilft - verschenkt ist nie ein Zug.
|
## man ansagt - so ist der Zug eine Entscheidung und nichts kommt sich in die
|
||||||
|
## Quere. Wappen verstärkt, Zahl lässt es bei der Grundwirkung.
|
||||||
func _do_play(seat: int, id: String, target: int) -> void:
|
func _do_play(seat: int, id: String, target: int) -> void:
|
||||||
if seat < 0 or phase != "bid" or not seats[seat].alive:
|
if seat < 0 or phase != "bid" or seat != turn or _answered.has(seat):
|
||||||
return
|
return
|
||||||
# Zwei Münzen gleichzeitig würden sich beim Würfeln in die Quere kommen.
|
|
||||||
if _muenze_laeuft:
|
if _muenze_laeuft:
|
||||||
return
|
return
|
||||||
|
if BoosterDB.needs_bid(id) and bid.is_empty():
|
||||||
|
return
|
||||||
var muenzen: Array = hand.get(seat, [])
|
var muenzen: Array = hand.get(seat, [])
|
||||||
if not muenzen.has(id):
|
if not muenzen.has(id):
|
||||||
return
|
return
|
||||||
@@ -292,7 +301,10 @@ func _do_play(seat: int, id: String, target: int) -> void:
|
|||||||
|
|
||||||
var stark := rng.randf() < BoosterDB.WAPPEN_CHANCE
|
var stark := rng.randf() < BoosterDB.WAPPEN_CHANCE
|
||||||
_wurf_nr += 1
|
_wurf_nr += 1
|
||||||
mitte[seat] = {"id": id, "stark": stark, "nr": _wurf_nr}
|
topf.append({"seat": seat, "id": id, "stark": stark, "nr": _wurf_nr})
|
||||||
|
while topf.size() > BoosterDB.TOPF_MAX:
|
||||||
|
var weg: Dictionary = topf.pop_front()
|
||||||
|
_log("%s fällt vom Tisch - der Topf ist voll." % BoosterDB.get_booster(String(weg.id)).name)
|
||||||
_log("%s wirft %s - %s." % [seats[seat].name,
|
_log("%s wirft %s - %s." % [seats[seat].name,
|
||||||
BoosterDB.get_booster(id).name, "Wappen" if stark else "Zahl"])
|
BoosterDB.get_booster(id).name, "Wappen" if stark else "Zahl"])
|
||||||
|
|
||||||
@@ -305,12 +317,6 @@ func _do_play(seat: int, id: String, target: int) -> void:
|
|||||||
_muenze_laeuft = false
|
_muenze_laeuft = false
|
||||||
if not _alive():
|
if not _alive():
|
||||||
return
|
return
|
||||||
# Der Widerruf hat keine eigene Verstärkung; bei Wappen bleibt er liegen.
|
|
||||||
if stark and BoosterDB.keeps_on_wappen(id):
|
|
||||||
var zurueck: Array = hand.get(seat, [])
|
|
||||||
if zurueck.size() < BoosterDB.HANDLIMIT:
|
|
||||||
zurueck.append(id)
|
|
||||||
hand[seat] = zurueck
|
|
||||||
_snapshot_all()
|
_snapshot_all()
|
||||||
_push_state()
|
_push_state()
|
||||||
|
|
||||||
@@ -319,15 +325,21 @@ func _wirkung(seat: int, id: String, target: int, stark: bool) -> void:
|
|||||||
var vals: Array = host_values[seat]
|
var vals: Array = host_values[seat]
|
||||||
var ziel: int = clampi(target, 0, maxi(0, vals.size() - 1))
|
var ziel: int = clampi(target, 0, maxi(0, vals.size() - 1))
|
||||||
match id:
|
match id:
|
||||||
"reroll_all":
|
"peek":
|
||||||
await _neu_werfen(seat, range(vals.size()))
|
var info := {}
|
||||||
if stark and not host_values[seat].is_empty():
|
for o in _alive_seats():
|
||||||
_set_die(seat, rng.randi_range(0, host_values[seat].size() - 1), 6)
|
if o != seat:
|
||||||
"reroll_one":
|
info[o] = _zufalls_paare(o, 2 if stark else 1)
|
||||||
await _neu_werfen(seat, _ziel_und_zufall(seat, ziel, stark))
|
peeked[seat] = info
|
||||||
"set_six":
|
"count":
|
||||||
|
# Zahl: ein Fenster von drei, in dem die Wahrheit irgendwo liegt.
|
||||||
|
# Genau wüsste man sonst, ob die Ansage hält - kein Bluff mehr.
|
||||||
|
var n := _count_face(int(bid.face))
|
||||||
|
var von: int = n if stark else maxi(0, n - rng.randi_range(0, 2))
|
||||||
|
gezaehlt[seat] = {int(bid.face): [von, von if stark else von + 2]}
|
||||||
|
"set_face":
|
||||||
for k: int in _ziel_und_zufall(seat, ziel, stark):
|
for k: int in _ziel_und_zufall(seat, ziel, stark):
|
||||||
_set_die(seat, k, 6)
|
_set_die(seat, k, int(bid.face))
|
||||||
"gain_die":
|
"gain_die":
|
||||||
seats[seat].count += 1
|
seats[seat].count += 1
|
||||||
table.set_dice_count(seat, seats[seat].count)
|
table.set_dice_count(seat, seats[seat].count)
|
||||||
@@ -336,46 +348,26 @@ func _wirkung(seat: int, id: String, target: int, stark: bool) -> void:
|
|||||||
await _neu_werfen(seat, [neu])
|
await _neu_werfen(seat, [neu])
|
||||||
if stark:
|
if stark:
|
||||||
_set_die(seat, neu, 6)
|
_set_die(seat, neu, 6)
|
||||||
"cancel":
|
|
||||||
fogged.erase(seat)
|
|
||||||
min_raise.erase(seat)
|
|
||||||
must_bid.erase(seat)
|
|
||||||
"peek":
|
|
||||||
var info := {}
|
|
||||||
for o in _alive_seats():
|
|
||||||
if o != seat:
|
|
||||||
info[o] = _zufalls_paare(o, 2 if stark else 1)
|
|
||||||
peeked[seat] = info
|
|
||||||
"bid_up":
|
"bid_up":
|
||||||
if not bid.is_empty():
|
bid.count = int(bid.count) + (2 if stark else 1)
|
||||||
bid.count = int(bid.count) + (2 if stark else 1)
|
last_bids[int(bid.seat)] = {"count": bid.count, "face": bid.face}
|
||||||
last_bids[int(bid.seat)] = {"count": bid.count, "face": bid.face}
|
verlauf.append({"seat": int(bid.seat), "count": bid.count, "face": bid.face, "erhoeht": true})
|
||||||
"enemy_lower":
|
|
||||||
var minus := 2 if stark else 1
|
|
||||||
for o in _alive_seats():
|
|
||||||
if o != seat:
|
|
||||||
for k: int in _zufalls_idx(o, 1):
|
|
||||||
_set_die(o, k, maxi(1, int(host_values[o][k]) - minus))
|
|
||||||
"fog":
|
|
||||||
for o in _alive_seats():
|
|
||||||
if o == seat:
|
|
||||||
continue
|
|
||||||
var liste: Array = fogged.get(o, [])
|
|
||||||
for k: int in _zufalls_idx(o, 2 if stark else 1):
|
|
||||||
if not liste.has(k):
|
|
||||||
liste.append(k)
|
|
||||||
fogged[o] = liste
|
|
||||||
"raise_min":
|
|
||||||
for o in _alive_seats():
|
|
||||||
if o != seat:
|
|
||||||
min_raise[o] = 3 if stark else 2
|
|
||||||
"gag":
|
"gag":
|
||||||
if stark:
|
if stark:
|
||||||
for o in _alive_seats():
|
for o in _alive_seats():
|
||||||
if o != seat:
|
if o != seat:
|
||||||
must_bid[o] = true
|
must_bid[o] = true
|
||||||
else:
|
else:
|
||||||
must_bid[turn] = true
|
must_bid[_next_alive(seat)] = true
|
||||||
|
|
||||||
|
|
||||||
|
func _count_face(face: int) -> int:
|
||||||
|
var n := 0
|
||||||
|
for i in _alive_seats():
|
||||||
|
for v in host_values[i]:
|
||||||
|
if v == face:
|
||||||
|
n += 1
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
## Bis zu `n` verschiedene Würfelplätze eines Sitzes, zufällig gezogen.
|
## Bis zu `n` verschiedene Würfelplätze eines Sitzes, zufällig gezogen.
|
||||||
@@ -453,12 +445,15 @@ func _phase_bid() -> Dictionary:
|
|||||||
_push_state()
|
_push_state()
|
||||||
if seats[turn].ai:
|
if seats[turn].ai:
|
||||||
await _sleep(1.1)
|
await _sleep(1.1)
|
||||||
# Bots werfen ihre Münzen, bevor sie ansagen.
|
# Erst planen, dann passend dazu eine Münze werfen. Die kann die
|
||||||
await _bot_muenze(turn)
|
# Lage ändern - also danach noch einmal entscheiden.
|
||||||
|
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
||||||
|
must_bid.get(turn, false), rng)
|
||||||
|
if await _bot_muenze(turn, a):
|
||||||
|
a = DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
||||||
|
must_bid.get(turn, false), rng)
|
||||||
if not _alive():
|
if not _alive():
|
||||||
return {}
|
return {}
|
||||||
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
|
||||||
min_raise.get(turn, 1), must_bid.get(turn, false), rng)
|
|
||||||
var res := _apply_action(turn, a)
|
var res := _apply_action(turn, a)
|
||||||
if not res.is_empty():
|
if not res.is_empty():
|
||||||
return res
|
return res
|
||||||
@@ -471,7 +466,7 @@ func _phase_bid() -> Dictionary:
|
|||||||
else:
|
else:
|
||||||
# Zeit abgelaufen oder getrennt: automatisch entscheiden
|
# Zeit abgelaufen oder getrennt: automatisch entscheiden
|
||||||
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
||||||
min_raise.get(turn, 1), must_bid.get(turn, false), rng)
|
must_bid.get(turn, false), rng)
|
||||||
_log("%s wird automatisch gespielt." % seats[turn].name)
|
_log("%s wird automatisch gespielt." % seats[turn].name)
|
||||||
var res := _apply_action(turn, a)
|
var res := _apply_action(turn, a)
|
||||||
if not res.is_empty():
|
if not res.is_empty():
|
||||||
@@ -480,24 +475,66 @@ func _phase_bid() -> Dictionary:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
## Ein Bot wirft manchmal eine Münze, statt sie bis zum Ausscheiden zu horten.
|
## Ein Bot wirft eine Münze, wenn sie zu seinem Vorhaben passt. `a` ist die
|
||||||
func _bot_muenze(seat: int) -> void:
|
## geplante Handlung. Gibt zurück, ob geworfen wurde.
|
||||||
|
func _bot_muenze(seat: int, a: Dictionary) -> bool:
|
||||||
var muenzen: Array = hand.get(seat, [])
|
var muenzen: Array = hand.get(seat, [])
|
||||||
if muenzen.is_empty():
|
if muenzen.is_empty():
|
||||||
return
|
return false
|
||||||
# Voller Vorrat: raus damit. Sonst nur ab und zu, sonst ist alles sofort weg.
|
var vals: Array = host_values[seat]
|
||||||
var lust := 1.0 if muenzen.size() >= BoosterDB.HANDLIMIT else 0.35
|
var unknown: int = _total_dice() - vals.size()
|
||||||
if rng.randf() > lust:
|
var will_bieten: bool = a.type == "bid"
|
||||||
return
|
# Wie glaubwürdig ist das, was der Bot vorhat bzw. was auf dem Tisch liegt?
|
||||||
var id: String = muenzen[rng.randi_range(0, muenzen.size() - 1)]
|
var eigenes: float = DiceAI.chance(vals, unknown, int(a.count), int(a.face)) if will_bieten else 0.0
|
||||||
if id == "bid_up" and bid.is_empty():
|
var liegt: float = DiceAI.chance(vals, unknown, int(bid.count), int(bid.face)) if not bid.is_empty() else 0.0
|
||||||
return
|
|
||||||
await _do_play(seat, id, DiceAI.target_for(id, host_values[seat]))
|
var wahl := ""
|
||||||
|
var ziel := 0
|
||||||
|
# Hinten liegen: Nachschub ist der einzige Weg zurück.
|
||||||
|
if muenzen.has("gain_die") and vals.size() <= _kleinste_hand():
|
||||||
|
wahl = "gain_die"
|
||||||
|
# Vor dem Anfechten lieber noch nachsehen, wenn es knapp ist.
|
||||||
|
elif not will_bieten and liegt > 0.2 and liegt < 0.55 and muenzen.has("count"):
|
||||||
|
wahl = "count"
|
||||||
|
elif not will_bieten and liegt > 0.2 and liegt < 0.55 and muenzen.has("peek"):
|
||||||
|
wahl = "peek"
|
||||||
|
# Zudrücken: die Ansage des Gegners höher schrauben, dann anfechten.
|
||||||
|
elif not will_bieten and muenzen.has("bid_up"):
|
||||||
|
wahl = "bid_up"
|
||||||
|
# Ein Bluff steht an: dem Nächsten den Mund verbieten.
|
||||||
|
elif will_bieten and eigenes < 0.35 and muenzen.has("gag"):
|
||||||
|
wahl = "gag"
|
||||||
|
# Auf derselben Augenzahl erhöhen: einen Würfel dazu drehen.
|
||||||
|
elif will_bieten and not bid.is_empty() and int(a.face) == int(bid.face) \
|
||||||
|
and muenzen.has("set_face") and vals.count(int(bid.face)) < vals.size():
|
||||||
|
wahl = "set_face"
|
||||||
|
ziel = DiceAI.target_for(vals, int(bid.face))
|
||||||
|
# Voller Vorrat: irgendwas Spielbares raus, sonst wird nie geworfen.
|
||||||
|
elif muenzen.size() >= BoosterDB.HANDLIMIT:
|
||||||
|
for id: String in muenzen:
|
||||||
|
if not BoosterDB.needs_bid(id) or not bid.is_empty():
|
||||||
|
wahl = id
|
||||||
|
break
|
||||||
|
if wahl == "":
|
||||||
|
return false
|
||||||
|
await _do_play(seat, wahl, ziel)
|
||||||
await _sleep(0.6)
|
await _sleep(0.6)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Die kleinste Würfelzahl am Tisch - wer da liegt, ist hinten.
|
||||||
|
func _kleinste_hand() -> int:
|
||||||
|
var k := 99
|
||||||
|
for i in _alive_seats():
|
||||||
|
k = mini(k, seats[i].count)
|
||||||
|
return k
|
||||||
|
|
||||||
|
|
||||||
## Gibt {} zurück wenn das Spiel weitergeht, sonst das Anfechtungs-Ergebnis.
|
## Gibt {} zurück wenn das Spiel weitergeht, sonst das Anfechtungs-Ergebnis.
|
||||||
func _apply_action(seat: int, a: Dictionary) -> Dictionary:
|
func _apply_action(seat: int, a: Dictionary) -> Dictionary:
|
||||||
|
if a.type == "exact":
|
||||||
|
_log("%s ruft: Genau!" % seats[seat].name)
|
||||||
|
return {"challenger": seat, "bid": bid.duplicate(), "exact": true}
|
||||||
if a.type == "bid" and not _valid_bid(seat, int(a.count), int(a.face)):
|
if a.type == "bid" and not _valid_bid(seat, int(a.count), int(a.face)):
|
||||||
# Wer einen Maulkorb hat, darf sich nicht über ein ungültiges Gebot in
|
# Wer einen Maulkorb hat, darf sich nicht über ein ungültiges Gebot in
|
||||||
# die Anfechtung retten - dann wird auf das kleinste gültige korrigiert.
|
# die Anfechtung retten - dann wird auf das kleinste gültige korrigiert.
|
||||||
@@ -513,22 +550,17 @@ func _apply_action(seat: int, a: Dictionary) -> Dictionary:
|
|||||||
return {"challenger": seat, "bid": bid.duplicate()}
|
return {"challenger": seat, "bid": bid.duplicate()}
|
||||||
bid = {"seat": seat, "count": int(a.count), "face": int(a.face)}
|
bid = {"seat": seat, "count": int(a.count), "face": int(a.face)}
|
||||||
last_bids[seat] = {"count": bid.count, "face": bid.face}
|
last_bids[seat] = {"count": bid.count, "face": bid.face}
|
||||||
|
verlauf.append({"seat": seat, "count": bid.count, "face": bid.face, "erhoeht": false})
|
||||||
must_bid.erase(seat)
|
must_bid.erase(seat)
|
||||||
_log("%s sagt %d x %d an." % [seats[seat].name, bid.count, bid.face])
|
_log("%s sagt %d x %d an." % [seats[seat].name, bid.count, bid.face])
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
## Kleinstes Gebot, das dieser Platz noch ansagen darf ({} = gar keins mehr).
|
## Kleinstes Gebot, das dieser Platz noch ansagen darf ({} = gar keins mehr).
|
||||||
func _lowest_legal_bid(seat: int) -> Dictionary:
|
func _lowest_legal_bid(_seat: int) -> Dictionary:
|
||||||
if bid.is_empty():
|
if bid.is_empty():
|
||||||
return {"count": 1, "face": 1}
|
return {"count": 1, "face": 1}
|
||||||
var mr: int = min_raise.get(seat, 1)
|
return DiceAI.lowest_raise(bid, _total_dice())
|
||||||
if mr == 1 and int(bid.face) < 6:
|
|
||||||
return {"count": int(bid.count), "face": int(bid.face) + 1}
|
|
||||||
var c: int = int(bid.count) + mr
|
|
||||||
if c <= _total_dice():
|
|
||||||
return {"count": c, "face": 1}
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
func _valid_bid(seat: int, count: int, face: int) -> bool:
|
func _valid_bid(seat: int, count: int, face: int) -> bool:
|
||||||
@@ -536,11 +568,8 @@ func _valid_bid(seat: int, count: int, face: int) -> bool:
|
|||||||
return false
|
return false
|
||||||
if bid.is_empty():
|
if bid.is_empty():
|
||||||
return true
|
return true
|
||||||
var mr: int = min_raise.get(seat, 1)
|
if count > bid.count:
|
||||||
if count >= bid.count + mr:
|
|
||||||
return true
|
return true
|
||||||
if mr > 1:
|
|
||||||
return false
|
|
||||||
return count == bid.count and face > bid.face
|
return count == bid.count and face > bid.face
|
||||||
|
|
||||||
|
|
||||||
@@ -548,19 +577,13 @@ func _phase_reveal(result: Dictionary) -> void:
|
|||||||
phase = "reveal"
|
phase = "reveal"
|
||||||
deadline = 0.0
|
deadline = 0.0
|
||||||
var b: Dictionary = result.bid
|
var b: Dictionary = result.bid
|
||||||
var challenger: int = result.challenger
|
var rufer: int = result.challenger
|
||||||
var total := 0
|
var genau: bool = result.get("exact", false)
|
||||||
for i in _alive_seats():
|
var total := _count_face(int(b.face))
|
||||||
for v in host_values[i]:
|
|
||||||
if v == b.face:
|
|
||||||
total += 1
|
|
||||||
|
|
||||||
reveal_total = total
|
|
||||||
var loser := challenger if total >= b.count else int(b.seat)
|
|
||||||
_log("%d x %d angesagt - tatsächlich %d. %s verliert einen Würfel." % [
|
|
||||||
b.count, b.face, total, seats[loser].name])
|
|
||||||
banner = "%s verliert" % seats[loser].name
|
|
||||||
|
|
||||||
|
# 1. Becher hoch. Noch kein Urteil - erst schauen.
|
||||||
|
reveal_total = -1
|
||||||
|
banner = "Genau?" if genau else "Aufdecken"
|
||||||
for i in _alive_seats():
|
for i in _alive_seats():
|
||||||
table.arrange_and_show(i, host_values[i], rng)
|
table.arrange_and_show(i, host_values[i], rng)
|
||||||
table.set_cup_down(i, false)
|
table.set_cup_down(i, false)
|
||||||
@@ -572,20 +595,64 @@ func _phase_reveal(result: Dictionary) -> void:
|
|||||||
if _peer_live(peer):
|
if _peer_live(peer):
|
||||||
_reveal.rpc_id(peer, values, banner)
|
_reveal.rpc_id(peer, values, banner)
|
||||||
_push_state()
|
_push_state()
|
||||||
await _sleep(REVEAL_PAUSE)
|
await _sleep(ZEIT_BECHER)
|
||||||
if not _alive():
|
if not _alive():
|
||||||
return
|
return
|
||||||
|
|
||||||
seats[loser].count -= 1
|
# 2. Zählen. Die Ansage in der Mitte färbt sich jetzt ein.
|
||||||
if seats[loser].count <= 0:
|
reveal_total = total
|
||||||
seats[loser].alive = false
|
banner = "%d gefunden, %d angesagt" % [total, b.count]
|
||||||
seats[loser].count = 0
|
_push_state()
|
||||||
host_values[loser] = []
|
await _sleep(ZEIT_ZAEHLEN)
|
||||||
table.set_dice_count(loser, 0)
|
if not _alive():
|
||||||
_log("%s scheidet aus." % seats[loser].name)
|
return
|
||||||
|
|
||||||
|
# 3. Urteil. Bei "Genau" trifft es alle anderen - oder nur den Rufer.
|
||||||
|
var verlierer: Array = []
|
||||||
|
var winner: int
|
||||||
|
var bonus := -1
|
||||||
|
if genau and total == int(b.count):
|
||||||
|
# Fester Schwung von zwei, egal wie viele am Tisch sitzen: der Ansager
|
||||||
|
# verliert einen, der Rufer bekommt einen. Mehr als eine Anfechtung
|
||||||
|
# bringt, weniger als "alle anderen" - das kippte bei vielen Spielern.
|
||||||
|
verlierer = [int(b.seat)]
|
||||||
|
winner = rufer
|
||||||
|
bonus = rufer
|
||||||
|
banner = "Genau! %s verliert, %s bekommt einen Würfel" % [
|
||||||
|
seats[int(b.seat)].name, seats[rufer].name]
|
||||||
|
elif genau:
|
||||||
|
verlierer = [rufer]
|
||||||
|
winner = int(b.seat)
|
||||||
|
banner = "Daneben - %s verliert" % seats[rufer].name
|
||||||
else:
|
else:
|
||||||
_zieh_muenze(loser)
|
var loser := rufer if total >= int(b.count) else int(b.seat)
|
||||||
starter = loser if seats[loser].alive else _next_alive(loser)
|
verlierer = [loser]
|
||||||
|
winner = int(b.seat) if loser == rufer else rufer
|
||||||
|
banner = "%s verliert" % seats[loser].name
|
||||||
|
_log("%d x %d angesagt - tatsächlich %d. %s" % [b.count, b.face, total, banner])
|
||||||
|
_topf_an(winner)
|
||||||
|
_push_state()
|
||||||
|
await _sleep(ZEIT_URTEIL)
|
||||||
|
if not _alive():
|
||||||
|
return
|
||||||
|
|
||||||
|
for l: int in verlierer:
|
||||||
|
seats[l].count -= 1
|
||||||
|
if seats[l].count <= 0:
|
||||||
|
seats[l].alive = false
|
||||||
|
seats[l].count = 0
|
||||||
|
host_values[l] = []
|
||||||
|
table.set_dice_count(l, 0)
|
||||||
|
_log("%s scheidet aus." % seats[l].name)
|
||||||
|
else:
|
||||||
|
_zieh_muenze(l)
|
||||||
|
if bonus >= 0 and seats[bonus].alive:
|
||||||
|
seats[bonus].count += 1
|
||||||
|
host_values[bonus].append(rng.randi_range(1, 6))
|
||||||
|
table.set_dice_count(bonus, seats[bonus].count)
|
||||||
|
# Nach einem Treffer beginnt der Rufer, sonst der Verlierer.
|
||||||
|
var naechster: int = winner if genau and total == int(b.count) else verlierer[0]
|
||||||
|
starter = naechster if seats[naechster].alive else _next_alive(naechster)
|
||||||
# Nur wer noch spielt, bekommt den Becher wieder. Auf einer leeren Zone
|
# Nur wer noch spielt, bekommt den Becher wieder. Auf einer leeren Zone
|
||||||
# sähe er aus, als lägen dort noch Würfel.
|
# sähe er aus, als lägen dort noch Würfel.
|
||||||
for i in seats.size():
|
for i in seats.size():
|
||||||
@@ -595,6 +662,22 @@ func _phase_reveal(result: Dictionary) -> void:
|
|||||||
_push_state()
|
_push_state()
|
||||||
|
|
||||||
|
|
||||||
|
## 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 - so ist
|
||||||
|
## ein voller Vorrat kein Grund, den Topf zu verschenken.
|
||||||
|
func _topf_an(winner: int) -> void:
|
||||||
|
if topf.is_empty():
|
||||||
|
return
|
||||||
|
var h: Array = hand.get(winner, [])
|
||||||
|
var genommen := 0
|
||||||
|
while not topf.is_empty() and h.size() < BoosterDB.HANDLIMIT:
|
||||||
|
h.append(String(topf.pop_front().id))
|
||||||
|
genommen += 1
|
||||||
|
hand[winner] = h
|
||||||
|
_log("%s nimmt %d Münze(n) aus dem Topf, %d bleiben." % [
|
||||||
|
seats[winner].name, genommen, topf.size()])
|
||||||
|
|
||||||
|
|
||||||
func _all_values() -> Array:
|
func _all_values() -> Array:
|
||||||
var out: Array = []
|
var out: Array = []
|
||||||
for i in seats.size():
|
for i in seats.size():
|
||||||
@@ -715,15 +798,9 @@ func _push_state() -> void:
|
|||||||
_state.rpc_id(peer, _state_for(i))
|
_state.rpc_id(peer, _state_for(i))
|
||||||
|
|
||||||
|
|
||||||
## Welche Münze liegt von wem in der Mitte? Beim Würfeln steht noch nichts
|
## Was liegt im Topf? Der bleibt auch übers Würfeln hinweg liegen.
|
||||||
## an, danach ist es offen.
|
|
||||||
func _round_boosters() -> Array:
|
func _round_boosters() -> Array:
|
||||||
var out: Array = []
|
return topf.duplicate(true)
|
||||||
if phase == "roll":
|
|
||||||
return out
|
|
||||||
for i in seats.size():
|
|
||||||
out.append(mitte.get(i, {}))
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
## Was jeder Platz in dieser Runde zuletzt angesagt hat.
|
## Was jeder Platz in dieser Runde zuletzt angesagt hat.
|
||||||
@@ -739,25 +816,22 @@ func _state_for(seat: int) -> Dictionary:
|
|||||||
if phase == "roll":
|
if phase == "roll":
|
||||||
for _k in seats[seat].count: # während des Wurfs steht der Wert noch nicht fest
|
for _k in seats[seat].count: # während des Wurfs steht der Wert noch nicht fest
|
||||||
vis.append(-1)
|
vis.append(-1)
|
||||||
elif phase == "reveal":
|
|
||||||
vis = host_values[seat].duplicate() # aufgedeckt wird ohne Nebel
|
|
||||||
else:
|
else:
|
||||||
var fg: Array = fogged.get(seat, [])
|
vis = host_values[seat].duplicate()
|
||||||
for k in host_values[seat].size():
|
|
||||||
vis.append(-1 if fg.has(k) else host_values[seat][k])
|
|
||||||
var pub: Array = []
|
var pub: Array = []
|
||||||
for s in seats:
|
for s in seats:
|
||||||
pub.append({"name": s.name, "count": s.count, "alive": s.alive, "ai": s.ai})
|
pub.append({"name": s.name, "count": s.count, "alive": s.alive, "ai": s.ai})
|
||||||
return {
|
return {
|
||||||
"phase": phase, "turn": turn, "bid": bid, "seats": pub, "values": vis,
|
"phase": phase, "turn": turn, "bid": bid, "seats": pub, "values": vis,
|
||||||
"banner": banner,
|
"banner": banner,
|
||||||
"peek": peeked.get(seat, {}), "min_raise": min_raise.get(seat, 1),
|
"peek": peeked.get(seat, {}), "zaehlung": gezaehlt.get(seat, {}),
|
||||||
"must_bid": must_bid.get(seat, false), "total": _total_dice(),
|
"must_bid": must_bid.get(seat, false), "total": _total_dice(),
|
||||||
"time_left": 0.0 if deadline <= 0.0 else maxf(0.0, deadline - _now()),
|
"time_left": 0.0 if deadline <= 0.0 else maxf(0.0, deadline - _now()),
|
||||||
"open": _all_values() if phase == "reveal" else [],
|
"open": _all_values() if phase == "reveal" else [],
|
||||||
"boosts": _round_boosters(),
|
"boosts": _round_boosters(),
|
||||||
"actual": reveal_total if phase == "reveal" else 0,
|
"actual": reveal_total if phase == "reveal" else 0,
|
||||||
"bids": _seat_bids(),
|
"bids": _seat_bids(),
|
||||||
|
"verlauf": verlauf.duplicate(true),
|
||||||
"hand": (hand.get(seat, []) as Array).duplicate(),
|
"hand": (hand.get(seat, []) as Array).duplicate(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -791,7 +865,23 @@ func _apply_state(st: Dictionary) -> void:
|
|||||||
table.set_cup_down(i, seats[i].alive)
|
table.set_cup_down(i, seats[i].alive)
|
||||||
table.set_dice_visible(local_seat, true)
|
table.set_dice_visible(local_seat, true)
|
||||||
_refresh_labels()
|
_refresh_labels()
|
||||||
_apply_fog_to_table()
|
table.set_topf(st.get("boosts", []))
|
||||||
|
if phase == "reveal" and int(st.get("actual", -1)) >= 0 and not _gezaehlt_gezeigt:
|
||||||
|
_gezaehlt_gezeigt = true
|
||||||
|
if not bid.is_empty():
|
||||||
|
table.zaehlen_zeigen(int(bid.face), st.get("open", []))
|
||||||
|
elif phase != "reveal" and _gezaehlt_gezeigt:
|
||||||
|
_gezaehlt_gezeigt = false
|
||||||
|
table.hervorhebung_weg()
|
||||||
|
if phase != _klang_phase:
|
||||||
|
_klang_phase = phase
|
||||||
|
match phase:
|
||||||
|
"roll":
|
||||||
|
Klang.wuerfel()
|
||||||
|
"reveal":
|
||||||
|
Klang.becher()
|
||||||
|
"over":
|
||||||
|
Klang.chime()
|
||||||
var bids: Array = st.get("bids", [])
|
var bids: Array = st.get("bids", [])
|
||||||
for i in mini(seats.size(), bids.size()):
|
for i in mini(seats.size(), bids.size()):
|
||||||
var b: Dictionary = bids[i]
|
var b: Dictionary = bids[i]
|
||||||
@@ -804,15 +894,6 @@ func _apply_state(st: Dictionary) -> void:
|
|||||||
|
|
||||||
# ---------------------------------------------------------------- Aktionen
|
# ---------------------------------------------------------------- Aktionen
|
||||||
|
|
||||||
## Der Nebel-Debuff muss auch den Würfel auf dem Tisch verdecken.
|
|
||||||
func _apply_fog_to_table() -> void:
|
|
||||||
if phase == "roll" or local_seat < 0 or local_seat >= table.dice.size():
|
|
||||||
return
|
|
||||||
var dl: Array = table.dice_of(local_seat)
|
|
||||||
for i in mini(dl.size(), my_values.size()):
|
|
||||||
dl[i].visible = my_values[i] >= 0
|
|
||||||
|
|
||||||
|
|
||||||
func _on_bid_pressed(count: int, face: int) -> void:
|
func _on_bid_pressed(count: int, face: int) -> void:
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
_do_bid(local_seat, count, face)
|
_do_bid(local_seat, count, face)
|
||||||
@@ -827,6 +908,13 @@ func _on_challenge_pressed() -> void:
|
|||||||
_req_challenge.rpc_id(1)
|
_req_challenge.rpc_id(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_exact_pressed() -> void:
|
||||||
|
if multiplayer.is_server():
|
||||||
|
_do_exact(local_seat)
|
||||||
|
else:
|
||||||
|
_req_exact.rpc_id(1)
|
||||||
|
|
||||||
|
|
||||||
func _on_booster_chosen(id: String, target: int) -> void:
|
func _on_booster_chosen(id: String, target: int) -> void:
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
_do_play(local_seat, id, target)
|
_do_play(local_seat, id, target)
|
||||||
@@ -849,6 +937,19 @@ func _on_pause_changed(on: bool) -> void:
|
|||||||
_push_state()
|
_push_state()
|
||||||
|
|
||||||
|
|
||||||
|
## Noch eine Solopartie mit denselben Bots. Der Wunsch wandert über Net ins
|
||||||
|
## neu geladene Menü, das dann sofort startet.
|
||||||
|
func _on_rematch() -> void:
|
||||||
|
if _quitting:
|
||||||
|
return
|
||||||
|
var bots := 0
|
||||||
|
for s in seats:
|
||||||
|
if s.ai:
|
||||||
|
bots += 1
|
||||||
|
Net.rematch = {"name": Net.player_name, "ai": bots, "stufe": DiceAI.stufe}
|
||||||
|
_on_quit()
|
||||||
|
|
||||||
|
|
||||||
func _on_quit() -> void:
|
func _on_quit() -> void:
|
||||||
if _quitting:
|
if _quitting:
|
||||||
return
|
return
|
||||||
@@ -871,6 +972,11 @@ func _req_challenge() -> void:
|
|||||||
_do_challenge(_seat_of(multiplayer.get_remote_sender_id()))
|
_do_challenge(_seat_of(multiplayer.get_remote_sender_id()))
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _req_exact() -> void:
|
||||||
|
_do_exact(_seat_of(multiplayer.get_remote_sender_id()))
|
||||||
|
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _req_boost(id: String, target: int) -> void:
|
func _req_boost(id: String, target: int) -> void:
|
||||||
_do_play(_seat_of(multiplayer.get_remote_sender_id()), id, target)
|
_do_play(_seat_of(multiplayer.get_remote_sender_id()), id, target)
|
||||||
@@ -904,3 +1010,15 @@ func _do_challenge(seat: int) -> void:
|
|||||||
return
|
return
|
||||||
_answered[seat] = _apply_action(seat, {"type": "challenge"})
|
_answered[seat] = _apply_action(seat, {"type": "challenge"})
|
||||||
|
|
||||||
|
|
||||||
|
## "Genau" unterliegt denselben Regeln wie die Anfechtung: nur am Zug, nur
|
||||||
|
## mit Ansage, und der Maulkorb sperrt es genauso.
|
||||||
|
func _do_exact(seat: int) -> void:
|
||||||
|
if seat < 0 or phase != "bid" or seat != turn or _answered.has(seat):
|
||||||
|
return
|
||||||
|
if bid.is_empty():
|
||||||
|
return
|
||||||
|
if must_bid.get(seat, false) and not _lowest_legal_bid(seat).is_empty():
|
||||||
|
return
|
||||||
|
_answered[seat] = _apply_action(seat, {"type": "exact"})
|
||||||
|
|
||||||
|
|||||||
231
scripts/hud.gd
231
scripts/hud.gd
@@ -3,6 +3,8 @@ extends CanvasLayer
|
|||||||
|
|
||||||
signal bid_pressed(count: int, face: int)
|
signal bid_pressed(count: int, face: int)
|
||||||
signal challenge_pressed
|
signal challenge_pressed
|
||||||
|
signal exact_pressed
|
||||||
|
signal rematch_pressed
|
||||||
signal booster_chosen(id: String, target: int)
|
signal booster_chosen(id: String, target: int)
|
||||||
signal quit_pressed
|
signal quit_pressed
|
||||||
signal pause_changed(paused: bool)
|
signal pause_changed(paused: bool)
|
||||||
@@ -33,17 +35,22 @@ var _count_min := 1
|
|||||||
var _count_max := 30
|
var _count_max := 30
|
||||||
var _bid_count := 0 ## aktuelles Gebot, 0 = noch keins
|
var _bid_count := 0 ## aktuelles Gebot, 0 = noch keins
|
||||||
var _bid_face := 0
|
var _bid_face := 0
|
||||||
var _min_raise := 1
|
|
||||||
var _actual := 0 ## beim Aufdecken: tatsächlich gefundene Würfel
|
var _actual := 0 ## beim Aufdecken: tatsächlich gefundene Würfel
|
||||||
var face_buttons: Array[Button] = []
|
var face_buttons: Array[Button] = []
|
||||||
var bid_btn: Button
|
var bid_btn: Button
|
||||||
var challenge_btn: Button
|
var challenge_btn: Button
|
||||||
|
var exact_btn: Button
|
||||||
|
var verlauf_box: VBoxContainer
|
||||||
|
var _verlauf_sig := "" ## Abbild des Gebotsverlaufs, gegen unnötigen Neuaufbau
|
||||||
var action_panel: PanelContainer
|
var action_panel: PanelContainer
|
||||||
|
|
||||||
var hand_panel: PanelContainer
|
var hand_panel: PanelContainer
|
||||||
var hand_box: HBoxContainer
|
var hand_box: HBoxContainer
|
||||||
var boost_slot: CenterContainer
|
var boost_slot: CenterContainer
|
||||||
var pause_slot: Control
|
var pause_slot: Control
|
||||||
|
var over_slot: Control
|
||||||
|
var over_title: Label
|
||||||
|
var over_again: Button
|
||||||
var pause_hint: Label
|
var pause_hint: Label
|
||||||
var boost_panel: PanelContainer
|
var boost_panel: PanelContainer
|
||||||
var boost_cards: HBoxContainer
|
var boost_cards: HBoxContainer
|
||||||
@@ -102,34 +109,17 @@ class MuenzIcon extends Control:
|
|||||||
|
|
||||||
func _zeichen(m: Vector2, s: float) -> void:
|
func _zeichen(m: Vector2, s: float) -> void:
|
||||||
match id:
|
match id:
|
||||||
"reroll_all":
|
"peek":
|
||||||
_wuerfel(m + Vector2(-s * 0.16, -s * 0.06), s * 0.42, 5)
|
_auge(m, s)
|
||||||
_wuerfel(m + Vector2(s * 0.18, s * 0.10), s * 0.42, 2)
|
"count":
|
||||||
"reroll_one":
|
_striche(m, s)
|
||||||
_wuerfel(m + Vector2(0, s * 0.10), s * 0.38, 3)
|
"set_face":
|
||||||
_bogen(m + Vector2(0, s * 0.10), s * 0.34)
|
_schiefer_wuerfel(m, s * 0.50)
|
||||||
"set_six":
|
|
||||||
_wuerfel(m, s * 0.54, 6)
|
|
||||||
"gain_die":
|
"gain_die":
|
||||||
_wuerfel(m + Vector2(-s * 0.10, 0), s * 0.46, 4)
|
_wuerfel(m + Vector2(-s * 0.10, 0), s * 0.46, 4)
|
||||||
_plus(m + Vector2(s * 0.26, -s * 0.20), s * 0.20)
|
_plus(m + Vector2(s * 0.26, -s * 0.20), s * 0.20)
|
||||||
"cancel":
|
|
||||||
draw_arc(m, s * 0.30, 0, TAU, 32, farbe, s * 0.08)
|
|
||||||
draw_line(m + Vector2(-s * 0.21, -s * 0.21), m + Vector2(s * 0.21, s * 0.21),
|
|
||||||
farbe, s * 0.08)
|
|
||||||
"peek":
|
|
||||||
_auge(m, s)
|
|
||||||
"bid_up":
|
"bid_up":
|
||||||
_pfeil(m, s * 0.52, true)
|
_pfeil(m, s * 0.52, true)
|
||||||
"raise_min":
|
|
||||||
_pfeil(m + Vector2(-s * 0.14, 0), s * 0.44, true)
|
|
||||||
_pfeil(m + Vector2(s * 0.14, 0), s * 0.44, true)
|
|
||||||
"enemy_lower":
|
|
||||||
_pfeil(m, s * 0.52, false)
|
|
||||||
"fog":
|
|
||||||
draw_circle(m + Vector2(-s * 0.16, s * 0.06), s * 0.16, farbe)
|
|
||||||
draw_circle(m + Vector2(s * 0.16, s * 0.06), s * 0.16, farbe)
|
|
||||||
draw_circle(m + Vector2(0, -s * 0.06), s * 0.20, farbe)
|
|
||||||
"gag":
|
"gag":
|
||||||
var r := Rect2(m - Vector2(s * 0.28, s * 0.16), Vector2(s * 0.56, s * 0.32))
|
var r := Rect2(m - Vector2(s * 0.28, s * 0.16), Vector2(s * 0.56, s * 0.32))
|
||||||
draw_rect(r, farbe, false, s * 0.07)
|
draw_rect(r, farbe, false, s * 0.07)
|
||||||
@@ -138,6 +128,23 @@ class MuenzIcon extends Control:
|
|||||||
_:
|
_:
|
||||||
draw_circle(m, s * 0.26, farbe)
|
draw_circle(m, s * 0.26, farbe)
|
||||||
|
|
||||||
|
## Vier Striche und einer quer - eine Strichliste, das Zeichen fürs Zählen.
|
||||||
|
func _striche(mitte: Vector2, s: float) -> void:
|
||||||
|
var w := maxf(1.5, s * 0.07)
|
||||||
|
for i in 4:
|
||||||
|
var x: float = mitte.x + (float(i) - 1.5) * s * 0.16
|
||||||
|
draw_line(Vector2(x, mitte.y - s * 0.26), Vector2(x, mitte.y + s * 0.26), farbe, w)
|
||||||
|
draw_line(mitte + Vector2(-s * 0.32, s * 0.20), mitte + Vector2(s * 0.32, -s * 0.20), farbe, w)
|
||||||
|
|
||||||
|
## Ein gekippter Würfel mit einem Auge - der wird gerade umgedreht.
|
||||||
|
func _schiefer_wuerfel(mitte: Vector2, kante: float) -> void:
|
||||||
|
var h := kante * 0.5
|
||||||
|
var ecken := PackedVector2Array()
|
||||||
|
for e: Vector2 in [Vector2(-h, -h), Vector2(h, -h), Vector2(h, h), Vector2(-h, h)]:
|
||||||
|
ecken.append(mitte + e.rotated(0.35))
|
||||||
|
draw_colored_polygon(ecken, farbe)
|
||||||
|
draw_circle(mitte, kante * 0.11, FLAECHE)
|
||||||
|
|
||||||
func _wuerfel(mitte: Vector2, kante: float, augen: int) -> void:
|
func _wuerfel(mitte: Vector2, kante: float, augen: int) -> void:
|
||||||
var r := Rect2(mitte - Vector2(kante, kante) * 0.5, Vector2(kante, kante))
|
var r := Rect2(mitte - Vector2(kante, kante) * 0.5, Vector2(kante, kante))
|
||||||
draw_rect(r, farbe, true)
|
draw_rect(r, farbe, true)
|
||||||
@@ -146,13 +153,6 @@ class MuenzIcon extends Control:
|
|||||||
for p: Vector2 in DieIcon.PIPS[augen]:
|
for p: Vector2 in DieIcon.PIPS[augen]:
|
||||||
draw_circle(mitte + p * weg, punkt, FLAECHE)
|
draw_circle(mitte + p * weg, punkt, FLAECHE)
|
||||||
|
|
||||||
## Offener Kreisbogen mit Spitze - das Zeichen fürs Neuwerfen.
|
|
||||||
func _bogen(mitte: Vector2, r: float) -> void:
|
|
||||||
draw_arc(mitte, r, PI * 0.85, TAU * 0.92, 28, farbe, maxf(1.5, r * 0.20))
|
|
||||||
var spitze := mitte + Vector2(cos(TAU * 0.92), sin(TAU * 0.92)) * r
|
|
||||||
draw_colored_polygon([spitze + Vector2(-r * 0.30, -r * 0.10),
|
|
||||||
spitze + Vector2(r * 0.24, -r * 0.16), spitze + Vector2(r * 0.02, r * 0.28)], farbe)
|
|
||||||
|
|
||||||
func _plus(mitte: Vector2, r: float) -> void:
|
func _plus(mitte: Vector2, r: float) -> void:
|
||||||
draw_rect(Rect2(mitte - Vector2(r, r * 0.28), Vector2(r * 2, r * 0.56)), farbe, true)
|
draw_rect(Rect2(mitte - Vector2(r, r * 0.28), Vector2(r * 2, r * 0.56)), farbe, true)
|
||||||
draw_rect(Rect2(mitte - Vector2(r * 0.28, r), Vector2(r * 0.56, r * 2)), farbe, true)
|
draw_rect(Rect2(mitte - Vector2(r * 0.28, r), Vector2(r * 0.56, r * 2)), farbe, true)
|
||||||
@@ -304,9 +304,17 @@ func _build() -> void:
|
|||||||
_place(pl, 1, 0, 1, 0, -16, 16, -16, 16)
|
_place(pl, 1, 0, 1, 0, -16, 16, -16, 16)
|
||||||
pl.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
pl.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||||
root.add_child(pl)
|
root.add_child(pl)
|
||||||
|
var rechts := VBoxContainer.new()
|
||||||
|
rechts.add_theme_constant_override("separation", 10)
|
||||||
|
pl.add_child(rechts)
|
||||||
players_box = VBoxContainer.new()
|
players_box = VBoxContainer.new()
|
||||||
players_box.add_theme_constant_override("separation", 6)
|
players_box.add_theme_constant_override("separation", 6)
|
||||||
pl.add_child(players_box)
|
rechts.add_child(players_box)
|
||||||
|
# Der Gebotsverlauf der Runde. Bluffs liest man daran, wer wie schnell
|
||||||
|
# hochgegangen ist - nicht am letzten Gebot allein.
|
||||||
|
verlauf_box = VBoxContainer.new()
|
||||||
|
verlauf_box.add_theme_constant_override("separation", 2)
|
||||||
|
rechts.add_child(verlauf_box)
|
||||||
|
|
||||||
# --- Aktive Booster oben links, Breite richtet sich nach dem Inhalt
|
# --- Aktive Booster oben links, Breite richtet sich nach dem Inhalt
|
||||||
var bo := _panel()
|
var bo := _panel()
|
||||||
@@ -421,6 +429,13 @@ func _build() -> void:
|
|||||||
challenge_btn.pressed.connect(_on_challenge)
|
challenge_btn.pressed.connect(_on_challenge)
|
||||||
av.add_child(challenge_btn)
|
av.add_child(challenge_btn)
|
||||||
|
|
||||||
|
exact_btn = Button.new()
|
||||||
|
exact_btn.text = "Genau"
|
||||||
|
exact_btn.tooltip_text = "Die Ansage trifft exakt. Stimmt es, verlieren alle anderen einen Würfel."
|
||||||
|
exact_btn.custom_minimum_size = Vector2(96, 42)
|
||||||
|
exact_btn.pressed.connect(_on_exact)
|
||||||
|
av.add_child(exact_btn)
|
||||||
|
|
||||||
timer_bid = _label("", 18, DIM)
|
timer_bid = _label("", 18, DIM)
|
||||||
timer_bid.custom_minimum_size = Vector2(TIMER_W, 0)
|
timer_bid.custom_minimum_size = Vector2(TIMER_W, 0)
|
||||||
timer_bid.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
timer_bid.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
@@ -534,6 +549,34 @@ func _build() -> void:
|
|||||||
leave.pressed.connect(_on_quit)
|
leave.pressed.connect(_on_quit)
|
||||||
pv.add_child(leave)
|
pv.add_child(leave)
|
||||||
|
|
||||||
|
# --- Spielende: wer gewonnen hat, und wie es weitergeht
|
||||||
|
over_slot = Control.new()
|
||||||
|
over_slot.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
over_slot.visible = false
|
||||||
|
root.add_child(over_slot)
|
||||||
|
var oc := CenterContainer.new()
|
||||||
|
oc.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
over_slot.add_child(oc)
|
||||||
|
var op := _panel(Color(0.05, 0.06, 0.09, 0.98))
|
||||||
|
oc.add_child(op)
|
||||||
|
var ov := VBoxContainer.new()
|
||||||
|
ov.custom_minimum_size = Vector2(300, 0)
|
||||||
|
ov.add_theme_constant_override("separation", 10)
|
||||||
|
op.add_child(ov)
|
||||||
|
over_title = _label("", 26, ACCENT)
|
||||||
|
over_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
ov.add_child(over_title)
|
||||||
|
over_again = Button.new()
|
||||||
|
over_again.text = "Nochmal"
|
||||||
|
over_again.custom_minimum_size = Vector2(0, 42)
|
||||||
|
over_again.pressed.connect(func() -> void: rematch_pressed.emit())
|
||||||
|
ov.add_child(over_again)
|
||||||
|
var menue := Button.new()
|
||||||
|
menue.text = "Zum Menü"
|
||||||
|
menue.custom_minimum_size = Vector2(0, 42)
|
||||||
|
menue.pressed.connect(_on_quit)
|
||||||
|
ov.add_child(menue)
|
||||||
|
|
||||||
|
|
||||||
func _on_quit() -> void:
|
func _on_quit() -> void:
|
||||||
quit_pressed.emit()
|
quit_pressed.emit()
|
||||||
@@ -542,10 +585,8 @@ func _on_quit() -> void:
|
|||||||
## Ein Gebot muss höher sein als das vorige. Bei gleicher Anzahl geht das nur
|
## Ein Gebot muss höher sein als das vorige. Bei gleicher Anzahl geht das nur
|
||||||
## über eine höhere Augenzahl - die kleineren werden deshalb ausgeblendet.
|
## über eine höhere Augenzahl - die kleineren werden deshalb ausgeblendet.
|
||||||
func _face_allowed(f: int) -> bool:
|
func _face_allowed(f: int) -> bool:
|
||||||
if _bid_count <= 0 or _count >= _bid_count + _min_raise:
|
if _bid_count <= 0 or _count > _bid_count:
|
||||||
return true
|
return true
|
||||||
if _min_raise > 1:
|
|
||||||
return false
|
|
||||||
return _count == _bid_count and f > _bid_face
|
return _count == _bid_count and f > _bid_face
|
||||||
|
|
||||||
|
|
||||||
@@ -568,12 +609,12 @@ func _lowest_bid() -> void:
|
|||||||
if _bid_count <= 0:
|
if _bid_count <= 0:
|
||||||
_set_count(1)
|
_set_count(1)
|
||||||
_on_face(1)
|
_on_face(1)
|
||||||
elif _min_raise == 1 and _bid_face < 6:
|
elif _bid_face < 6:
|
||||||
# Gleiche Anzahl reicht, wenn die Augenzahl steigt.
|
# Gleiche Anzahl reicht, wenn die Augenzahl steigt.
|
||||||
_set_count(_bid_count)
|
_set_count(_bid_count)
|
||||||
_on_face(_bid_face + 1)
|
_on_face(_bid_face + 1)
|
||||||
else:
|
else:
|
||||||
_set_count(_bid_count + _min_raise)
|
_set_count(_bid_count + 1)
|
||||||
_on_face(1)
|
_on_face(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -597,6 +638,10 @@ func _on_challenge() -> void:
|
|||||||
challenge_pressed.emit()
|
challenge_pressed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_exact() -> void:
|
||||||
|
exact_pressed.emit()
|
||||||
|
|
||||||
|
|
||||||
func _unhandled_key_input(event: InputEvent) -> void:
|
func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
if event is InputEventKey and event.pressed and not event.echo \
|
if event is InputEventKey and event.pressed and not event.echo \
|
||||||
and event.keycode == KEY_ESCAPE:
|
and event.keycode == KEY_ESCAPE:
|
||||||
@@ -672,7 +717,7 @@ func update_state(st: Dictionary, my_seat: int) -> void:
|
|||||||
# Beim Aufdecken bleibt die Ansage stehen, wie sie war: die angesagten
|
# Beim Aufdecken bleibt die Ansage stehen, wie sie war: die angesagten
|
||||||
# Würfel normal, die fehlenden davon rot. Gab es mehr als angesagt,
|
# Würfel normal, die fehlenden davon rot. Gab es mehr als angesagt,
|
||||||
# kommen die überzähligen grün dahinter.
|
# kommen die überzähligen grün dahinter.
|
||||||
var faerben: bool = st.phase == "reveal"
|
var faerben: bool = st.phase == "reveal" and _actual >= 0
|
||||||
var angesagt := int(st.bid.count)
|
var angesagt := int(st.bid.count)
|
||||||
var gezeigt := maxi(angesagt, _actual) if faerben else angesagt
|
var gezeigt := maxi(angesagt, _actual) if faerben else angesagt
|
||||||
var px := _fit_dice(gezeigt)
|
var px := _fit_dice(gezeigt)
|
||||||
@@ -686,10 +731,17 @@ func update_state(st: Dictionary, my_seat: int) -> void:
|
|||||||
bid_row.add_child(DieIcon.new(int(st.bid.face), px, bg))
|
bid_row.add_child(DieIcon.new(int(st.bid.face), px, bg))
|
||||||
|
|
||||||
_zeichne_hand(st)
|
_zeichne_hand(st)
|
||||||
|
|
||||||
_zeichne_mitte(st, my_seat)
|
_zeichne_mitte(st, my_seat)
|
||||||
|
_zeichne_verlauf(st, my_seat)
|
||||||
|
|
||||||
banner_label.text = st.banner
|
# Am Ende steht der Sieger im Endbildschirm; das Banner darüber wäre doppelt.
|
||||||
|
banner_label.text = "" if st.phase == "over" else st.banner
|
||||||
|
if st.phase == "over" and not over_slot.visible:
|
||||||
|
over_title.text = String(st.banner)
|
||||||
|
# Nochmal gibt es nur solo. Im Netz müsste der Server den Raum neu
|
||||||
|
# aufsetzen; bis dahin geht es zurück ins Menü.
|
||||||
|
over_again.visible = Net.room_code == Net.SOLO
|
||||||
|
over_slot.visible = true
|
||||||
|
|
||||||
var my_turn: bool = st.phase == "bid" and st.turn == my_seat and st.seats[my_seat].alive
|
var my_turn: bool = st.phase == "bid" and st.turn == my_seat and st.seats[my_seat].alive
|
||||||
var kam_dran := my_turn and not _my_turn
|
var kam_dran := my_turn and not _my_turn
|
||||||
@@ -708,19 +760,15 @@ func update_state(st: Dictionary, my_seat: int) -> void:
|
|||||||
action_panel.modulate = Color(1, 1, 1, 1.0 if my_turn else 0.45)
|
action_panel.modulate = Color(1, 1, 1, 1.0 if my_turn else 0.45)
|
||||||
bid_btn.disabled = not my_turn
|
bid_btn.disabled = not my_turn
|
||||||
_count_max = maxi(1, int(st.total))
|
_count_max = maxi(1, int(st.total))
|
||||||
_min_raise = maxi(1, int(st.min_raise))
|
|
||||||
_bid_count = 0 if st.bid.is_empty() else int(st.bid.count)
|
_bid_count = 0 if st.bid.is_empty() else int(st.bid.count)
|
||||||
_bid_face = 0 if st.bid.is_empty() else int(st.bid.face)
|
_bid_face = 0 if st.bid.is_empty() else int(st.bid.face)
|
||||||
# Ein Maulkorb sperrt das Anfechten - ausser es gäbe gar kein gültiges
|
# Ein Maulkorb sperrt das Anfechten - ausser es gäbe gar kein gültiges
|
||||||
# Gebot mehr, dann bliebe sonst kein einziger Knopf übrig.
|
# Gebot mehr, dann bliebe sonst kein einziger Knopf übrig.
|
||||||
var gebot_moeglich: bool = _bid_count + _min_raise <= _count_max \
|
var gebot_moeglich: bool = _bid_count + 1 <= _count_max or _bid_face < 6
|
||||||
or (_min_raise == 1 and _bid_face < 6)
|
|
||||||
challenge_btn.disabled = not my_turn or st.bid.is_empty() \
|
challenge_btn.disabled = not my_turn or st.bid.is_empty() \
|
||||||
or (st.must_bid and gebot_moeglich)
|
or (st.must_bid and gebot_moeglich)
|
||||||
var low := 1
|
exact_btn.disabled = challenge_btn.disabled
|
||||||
if _bid_count > 0:
|
_count_min = clampi(maxi(1, _bid_count), 1, _count_max)
|
||||||
low = _bid_count + (_min_raise if _min_raise > 1 else 0)
|
|
||||||
_count_min = clampi(low, 1, _count_max)
|
|
||||||
_set_count(_count)
|
_set_count(_count)
|
||||||
if kam_dran:
|
if kam_dran:
|
||||||
_lowest_bid()
|
_lowest_bid()
|
||||||
@@ -803,10 +851,12 @@ func _zeichne_hand(st: Dictionary) -> void:
|
|||||||
# stünden nur klickbar im Weg.
|
# stünden nur klickbar im Weg.
|
||||||
var dabei: bool = _my_seat < st.seats.size() and st.seats[_my_seat].alive
|
var dabei: bool = _my_seat < st.seats.size() and st.seats[_my_seat].alive
|
||||||
hand_panel.visible = dabei and not muenzen.is_empty()
|
hand_panel.visible = dabei and not muenzen.is_empty()
|
||||||
var spielbar: bool = st.phase == "bid"
|
# Geworfen wird nur am eigenen Zug. Was eine Ansage braucht, wartet auf eine.
|
||||||
|
var spielbar: bool = st.phase == "bid" and int(st.turn) == _my_seat
|
||||||
|
var gebot: bool = not st.bid.is_empty()
|
||||||
# Jeder Zustand kommt mehrmals an. Ein Neuaufbau würde die Münze unter dem
|
# Jeder Zustand kommt mehrmals an. Ein Neuaufbau würde die Münze unter dem
|
||||||
# Zeiger wegreissen und die Erklärung mitnehmen.
|
# Zeiger wegreissen und die Erklärung mitnehmen.
|
||||||
var sig := "%s|%s" % [muenzen, spielbar]
|
var sig := "%s|%s|%s" % [muenzen, spielbar, gebot]
|
||||||
if sig == _hand_sig:
|
if sig == _hand_sig:
|
||||||
return
|
return
|
||||||
_hand_sig = sig
|
_hand_sig = sig
|
||||||
@@ -819,7 +869,10 @@ func _zeichne_hand(st: Dictionary) -> void:
|
|||||||
|
|
||||||
var btn := Button.new()
|
var btn := Button.new()
|
||||||
btn.custom_minimum_size = Vector2(104, 120)
|
btn.custom_minimum_size = Vector2(104, 120)
|
||||||
btn.disabled = not spielbar
|
btn.disabled = not spielbar or (BoosterDB.needs_bid(id) and not gebot)
|
||||||
|
# Der gesperrte Knopf dunkelt nur seinen Rand ab; Münze und Name darauf
|
||||||
|
# blieben sonst so hell wie bei einer werfbaren.
|
||||||
|
btn.modulate = Color(1, 1, 1, 1.0 if not btn.disabled else 0.4)
|
||||||
# Ein gesperrter Knopf meldet keine Maus mehr; die Erklärung soll aber
|
# Ein gesperrter Knopf meldet keine Maus mehr; die Erklärung soll aber
|
||||||
# auch dann kommen, wenn gerade nicht geworfen werden darf.
|
# auch dann kommen, wenn gerade nicht geworfen werden darf.
|
||||||
btn.mouse_filter = Control.MOUSE_FILTER_STOP
|
btn.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
@@ -845,35 +898,32 @@ func _zeichne_hand(st: Dictionary) -> void:
|
|||||||
v.add_child(name_l)
|
v.add_child(name_l)
|
||||||
|
|
||||||
|
|
||||||
## Was liegt in der Mitte, plus die Wirkungen, die gerade auf einem liegen.
|
## Der Topf: was diese Runde geworfen wurde und wem es gehört, wenn er
|
||||||
|
## das Aufdecken gewinnt. Darunter, was gerade auf einem selbst liegt.
|
||||||
## Auch hier gilt: nur neu bauen, wenn sich etwas geändert hat.
|
## Auch hier gilt: nur neu bauen, wenn sich etwas geändert hat.
|
||||||
func _zeichne_mitte(st: Dictionary, my_seat: int) -> void:
|
func _zeichne_mitte(st: Dictionary, my_seat: int) -> void:
|
||||||
var boosts: Array = st.get("boosts", [])
|
var boosts: Array = st.get("boosts", [])
|
||||||
# Der neueste Wurf muss bei jedem Zustand geprüft werden, auch ohne Neubau.
|
# Der neueste Wurf muss bei jedem Zustand geprüft werden, auch ohne Neubau.
|
||||||
var neu := {}
|
var neu := {}
|
||||||
for i in mini(boosts.size(), st.seats.size()):
|
for m in boosts:
|
||||||
var m: Dictionary = boosts[i]
|
if int(m.get("nr", 0)) > int(neu.get("nr", 0)):
|
||||||
if not m.is_empty() and int(m.get("nr", 0)) > int(neu.get("nr", 0)):
|
neu = m
|
||||||
neu = {"nr": m.get("nr", 0), "id": m.get("id", ""),
|
|
||||||
"stark": m.get("stark", false), "seat": i}
|
|
||||||
_pruefe_wurf(neu, st)
|
_pruefe_wurf(neu, st)
|
||||||
|
|
||||||
var sig := "%s|%s|%s" % [boosts, st.min_raise, st.must_bid]
|
var zaehlung: Dictionary = st.get("zaehlung", {})
|
||||||
|
var sig := "%s|%s|%s" % [boosts, zaehlung, st.must_bid]
|
||||||
if sig == _mitte_sig:
|
if sig == _mitte_sig:
|
||||||
return
|
return
|
||||||
_mitte_sig = sig
|
_mitte_sig = sig
|
||||||
_clear(boosts_box)
|
_clear(boosts_box)
|
||||||
|
|
||||||
var any := false
|
boosts_box.add_child(_label("Topf" if not boosts.is_empty() else "Topf: leer", 13, DIM))
|
||||||
for i in mini(boosts.size(), st.seats.size()):
|
for m in boosts:
|
||||||
var m: Dictionary = boosts[i]
|
|
||||||
if m.is_empty():
|
|
||||||
continue
|
|
||||||
var id := String(m.get("id", ""))
|
var id := String(m.get("id", ""))
|
||||||
var b: Dictionary = BoosterDB.get_booster(id)
|
var b: Dictionary = BoosterDB.get_booster(id)
|
||||||
if b.is_empty():
|
if b.is_empty():
|
||||||
continue
|
continue
|
||||||
any = true
|
var wer := int(m.get("seat", 0))
|
||||||
var line := HBoxContainer.new()
|
var line := HBoxContainer.new()
|
||||||
line.add_theme_constant_override("separation", 5)
|
line.add_theme_constant_override("separation", 5)
|
||||||
# Der Container muss die Maus annehmen, sonst gibt es kein Überfahren.
|
# Der Container muss die Maus annehmen, sonst gibt es kein Überfahren.
|
||||||
@@ -881,19 +931,55 @@ func _zeichne_mitte(st: Dictionary, my_seat: int) -> void:
|
|||||||
boosts_box.add_child(line)
|
boosts_box.add_child(line)
|
||||||
_info_an(line, id, true)
|
_info_an(line, id, true)
|
||||||
line.add_child(MuenzIcon.new(id, _kind_col(b), 22.0))
|
line.add_child(MuenzIcon.new(id, _kind_col(b), 22.0))
|
||||||
line.add_child(_label(_short(String(st.seats[i].name)) + ":", 14,
|
|
||||||
ACCENT if i == my_seat else Color(0.78, 0.80, 0.86)))
|
|
||||||
line.add_child(_label(String(b.get("name", "")), 14, _kind_col(b)))
|
line.add_child(_label(String(b.get("name", "")), 14, _kind_col(b)))
|
||||||
if bool(m.get("stark", false)):
|
if bool(m.get("stark", false)):
|
||||||
line.add_child(_label("· Wappen", 14, ACCENT))
|
line.add_child(_label("· Wappen", 14, ACCENT))
|
||||||
if int(st.min_raise) > 1:
|
if wer < st.seats.size():
|
||||||
boosts_box.add_child(_label("Störfeuer: mind. %d erhöhen" % st.min_raise, 14, ACCENT))
|
line.add_child(_label("von " + _short(String(st.seats[wer].name)), 13,
|
||||||
any = true
|
ACCENT if wer == my_seat else DIM))
|
||||||
|
# Ergebnis vom Zählen: so oft liegt die Augenzahl am Tisch.
|
||||||
|
for f in zaehlung:
|
||||||
|
var line := HBoxContainer.new()
|
||||||
|
line.add_theme_constant_override("separation", 5)
|
||||||
|
boosts_box.add_child(line)
|
||||||
|
var von := int(zaehlung[f][0])
|
||||||
|
var bis := int(zaehlung[f][1])
|
||||||
|
var text := "Am Tisch: %d ×" % von if von == bis else "Am Tisch: %d–%d ×" % [von, bis]
|
||||||
|
line.add_child(_label(text, 14, ACCENT))
|
||||||
|
line.add_child(DieIcon.new(int(f), 18.0))
|
||||||
if st.must_bid:
|
if st.must_bid:
|
||||||
boosts_box.add_child(_label("Maulkorb: du musst bieten", 14, ACCENT))
|
boosts_box.add_child(_label("Maulkorb: du musst bieten", 14, ACCENT))
|
||||||
any = true
|
|
||||||
if not any:
|
|
||||||
boosts_box.add_child(_label("Keine Münze in der Mitte", 14, DIM))
|
## Die letzten Ansagen der Runde, die jüngste unten und hervorgehoben.
|
||||||
|
func _zeichne_verlauf(st: Dictionary, my_seat: int) -> void:
|
||||||
|
var verlauf: Array = st.get("verlauf", [])
|
||||||
|
var sig := str(verlauf)
|
||||||
|
if sig == _verlauf_sig:
|
||||||
|
return
|
||||||
|
_verlauf_sig = sig
|
||||||
|
_clear(verlauf_box)
|
||||||
|
if verlauf.is_empty():
|
||||||
|
return
|
||||||
|
verlauf_box.add_child(_label("Verlauf", 13, DIM))
|
||||||
|
var ab := maxi(0, verlauf.size() - 6)
|
||||||
|
for i in range(ab, verlauf.size()):
|
||||||
|
var e: Dictionary = verlauf[i]
|
||||||
|
var wer := int(e.get("seat", 0))
|
||||||
|
if wer >= st.seats.size():
|
||||||
|
continue
|
||||||
|
var letzte: bool = i == verlauf.size() - 1
|
||||||
|
var col := ACCENT if letzte else (Color(0.85, 0.87, 0.92) if wer != my_seat else Color(0.95, 0.85, 0.55))
|
||||||
|
var line := HBoxContainer.new()
|
||||||
|
line.add_theme_constant_override("separation", 5)
|
||||||
|
verlauf_box.add_child(line)
|
||||||
|
var nm := _label(_short(String(st.seats[wer].name)), 13, col)
|
||||||
|
nm.custom_minimum_size = Vector2(64, 0)
|
||||||
|
line.add_child(nm)
|
||||||
|
line.add_child(_label("%d ×" % int(e.get("count", 0)), 13, col))
|
||||||
|
line.add_child(DieIcon.new(int(e.get("face", 1)), 16.0))
|
||||||
|
if bool(e.get("erhoeht", false)):
|
||||||
|
line.add_child(_label("erhöht", 12, DIM))
|
||||||
|
|
||||||
|
|
||||||
## Erklärung zu einer Münze, sobald die Maus daraufliegt. `unter` legt sie
|
## Erklärung zu einer Münze, sobald die Maus daraufliegt. `unter` legt sie
|
||||||
@@ -953,6 +1039,8 @@ func _pruefe_wurf(neu: Dictionary, st: Dictionary) -> void:
|
|||||||
return
|
return
|
||||||
_letzter_wurf = nr
|
_letzter_wurf = nr
|
||||||
var seat := int(neu.get("seat", 0))
|
var seat := int(neu.get("seat", 0))
|
||||||
|
if seat >= st.seats.size():
|
||||||
|
return
|
||||||
_zeig_wurf(String(neu.get("id", "")), bool(neu.get("stark", false)),
|
_zeig_wurf(String(neu.get("id", "")), bool(neu.get("stark", false)),
|
||||||
_short(String(st.seats[seat].name)))
|
_short(String(st.seats[seat].name)))
|
||||||
|
|
||||||
@@ -971,6 +1059,7 @@ func _zeig_wurf(id: String, stark: bool, wer: String) -> void:
|
|||||||
wurf_label.text = "%s wirft\n%s" % [wer, b.name]
|
wurf_label.text = "%s wirft\n%s" % [wer, b.name]
|
||||||
wurf_label.add_theme_color_override("font_color", DIM)
|
wurf_label.add_theme_color_override("font_color", DIM)
|
||||||
wurf_slot.visible = true
|
wurf_slot.visible = true
|
||||||
|
Klang.muenze()
|
||||||
if _wurf_tween != null and _wurf_tween.is_valid():
|
if _wurf_tween != null and _wurf_tween.is_valid():
|
||||||
_wurf_tween.kill()
|
_wurf_tween.kill()
|
||||||
_wurf_tween = create_tween()
|
_wurf_tween = create_tween()
|
||||||
|
|||||||
85
scripts/klang.gd
Normal file
85
scripts/klang.gd
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
extends Node
|
||||||
|
## Klänge ohne Dateien. Die kurzen Wellen werden beim Start berechnet - so wie
|
||||||
|
## Würfel und Münzen gemalt statt geladen werden. Auf dem Spielserver ohne
|
||||||
|
## Tongerät spielt das ins Leere, stört aber nicht.
|
||||||
|
|
||||||
|
const RATE := 22050
|
||||||
|
|
||||||
|
var _wuerfel: AudioStreamWAV
|
||||||
|
var _muenze: AudioStreamWAV
|
||||||
|
var _becher: AudioStreamWAV
|
||||||
|
var _chime: AudioStreamWAV
|
||||||
|
var _rng := RandomNumberGenerator.new()
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_rng.randomize()
|
||||||
|
# Klack: Rauschen, das sofort wieder weg ist.
|
||||||
|
_wuerfel = _wav(0.07, func(t: float) -> float:
|
||||||
|
return _rausch() * exp(-t * 60.0))
|
||||||
|
# Klirren: drei unharmonische Töne, die langsam ausklingen.
|
||||||
|
_muenze = _wav(0.35, func(t: float) -> float:
|
||||||
|
return (sin(TAU * 2600.0 * t) + 0.5 * sin(TAU * 4100.0 * t)
|
||||||
|
+ 0.3 * sin(TAU * 6300.0 * t)) / 1.8 * exp(-t * 9.0))
|
||||||
|
# Dumpfer Schlag: tiefer Ton plus kurzes Rauschen.
|
||||||
|
_becher = _wav(0.14, func(t: float) -> float:
|
||||||
|
return sin(TAU * 110.0 * t) * exp(-t * 22.0) + _rausch() * 0.3 * exp(-t * 80.0))
|
||||||
|
# Zwei Töne aufwärts - für den Gewinn.
|
||||||
|
_chime = _wav(0.55, func(t: float) -> float:
|
||||||
|
var a := sin(TAU * 880.0 * t) * exp(-t * 4.0)
|
||||||
|
var b := 0.0
|
||||||
|
if t > 0.14:
|
||||||
|
b = sin(TAU * 1320.0 * (t - 0.14)) * exp(-(t - 0.14) * 4.0)
|
||||||
|
return (a + b) * 0.5)
|
||||||
|
|
||||||
|
|
||||||
|
func _rausch() -> float:
|
||||||
|
return _rng.randf_range(-1.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _wav(sek: float, f: Callable) -> AudioStreamWAV:
|
||||||
|
var n := int(RATE * sek)
|
||||||
|
var data := PackedByteArray()
|
||||||
|
data.resize(n * 2)
|
||||||
|
for i in n:
|
||||||
|
var v: float = clampf(f.call(float(i) / RATE), -1.0, 1.0)
|
||||||
|
data.encode_s16(i * 2, int(v * 32000.0))
|
||||||
|
var w := AudioStreamWAV.new()
|
||||||
|
w.format = AudioStreamWAV.FORMAT_16_BITS
|
||||||
|
w.mix_rate = RATE
|
||||||
|
w.data = data
|
||||||
|
return w
|
||||||
|
|
||||||
|
|
||||||
|
func _spiel(w: AudioStreamWAV, db: float, pitch := 1.0) -> void:
|
||||||
|
var p := AudioStreamPlayer.new()
|
||||||
|
p.stream = w
|
||||||
|
p.volume_db = db
|
||||||
|
p.pitch_scale = pitch
|
||||||
|
add_child(p)
|
||||||
|
p.finished.connect(p.queue_free)
|
||||||
|
p.play()
|
||||||
|
|
||||||
|
|
||||||
|
## Mehrere Würfel fallen: ein paar Klacker kurz hintereinander, verschieden hoch.
|
||||||
|
func wuerfel(anzahl := 5) -> void:
|
||||||
|
for i in mini(anzahl, 8):
|
||||||
|
await get_tree().create_timer(_rng.randf_range(0.0, 0.35)).timeout
|
||||||
|
_spiel(_wuerfel, -8.0, _rng.randf_range(0.8, 1.3))
|
||||||
|
|
||||||
|
|
||||||
|
## Ein kurzer heller Klick - je gezähltem Würfel einer.
|
||||||
|
func tick() -> void:
|
||||||
|
_spiel(_wuerfel, -14.0, 2.2)
|
||||||
|
|
||||||
|
|
||||||
|
func muenze() -> void:
|
||||||
|
_spiel(_muenze, -6.0, _rng.randf_range(0.95, 1.05))
|
||||||
|
|
||||||
|
|
||||||
|
func becher() -> void:
|
||||||
|
_spiel(_becher, -3.0)
|
||||||
|
|
||||||
|
|
||||||
|
func chime() -> void:
|
||||||
|
_spiel(_chime, -8.0)
|
||||||
@@ -30,6 +30,12 @@ func _ready() -> void:
|
|||||||
menu = preload("res://scripts/menu.gd").new()
|
menu = preload("res://scripts/menu.gd").new()
|
||||||
add_child(menu)
|
add_child(menu)
|
||||||
|
|
||||||
|
# "Nochmal" nach einer Solopartie: das Menü wird übersprungen.
|
||||||
|
if not Net.rematch.is_empty():
|
||||||
|
var r: Dictionary = Net.rematch
|
||||||
|
Net.rematch = {}
|
||||||
|
Net.start_singleplayer(String(r.name), int(r.ai), int(r.stufe))
|
||||||
|
|
||||||
|
|
||||||
## Das Spiel startet im Vollbild; F11 schaltet um.
|
## Das Spiel startet im Vollbild; F11 schaltet um.
|
||||||
func _unhandled_key_input(event: InputEvent) -> void:
|
func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ var current := "main"
|
|||||||
|
|
||||||
# Einzelspieler
|
# Einzelspieler
|
||||||
var solo_bots: NumberRow
|
var solo_bots: NumberRow
|
||||||
|
var solo_stufe: OptionButton
|
||||||
|
var lobby_stufe: OptionButton
|
||||||
|
|
||||||
# Mehrspieler
|
# Mehrspieler
|
||||||
var name_edit: LineEdit
|
var name_edit: LineEdit
|
||||||
@@ -37,6 +39,7 @@ func _ready() -> void:
|
|||||||
screens["multi"] = _screen(root, _build_multi())
|
screens["multi"] = _screen(root, _build_multi())
|
||||||
screens["join"] = _screen(root, _build_join())
|
screens["join"] = _screen(root, _build_join())
|
||||||
screens["lobby"] = _screen(root, _build_lobby())
|
screens["lobby"] = _screen(root, _build_lobby())
|
||||||
|
screens["regeln"] = _screen(root, _build_regeln())
|
||||||
|
|
||||||
Net.lobby_changed.connect(_refresh)
|
Net.lobby_changed.connect(_refresh)
|
||||||
Net.connection_failed.connect(_on_failed)
|
Net.connection_failed.connect(_on_failed)
|
||||||
@@ -85,6 +88,17 @@ func _text(t: String, size: int, col := Color.WHITE) -> Label:
|
|||||||
return l
|
return l
|
||||||
|
|
||||||
|
|
||||||
|
## Leicht, normal, schwer - stellt Bluff-Quote, Genau-Schwelle und Vorsicht
|
||||||
|
## der Bots ein.
|
||||||
|
func _stufen_wahl() -> OptionButton:
|
||||||
|
var o := OptionButton.new()
|
||||||
|
for name in ["leicht", "normal", "schwer"]:
|
||||||
|
o.add_item(name)
|
||||||
|
o.selected = 1
|
||||||
|
o.add_theme_font_size_override("font_size", 16)
|
||||||
|
return o
|
||||||
|
|
||||||
|
|
||||||
func _row(caption: String, ctrl: Control) -> HBoxContainer:
|
func _row(caption: String, ctrl: Control) -> HBoxContainer:
|
||||||
var h := HBoxContainer.new()
|
var h := HBoxContainer.new()
|
||||||
var l := Label.new()
|
var l := Label.new()
|
||||||
@@ -158,10 +172,43 @@ func _build_main() -> Control:
|
|||||||
_title(box)
|
_title(box)
|
||||||
_button("Einzelspieler", box, _show.bind("single"))
|
_button("Einzelspieler", box, _show.bind("single"))
|
||||||
_button("Mehrspieler", box, _show.bind("multi"))
|
_button("Mehrspieler", box, _show.bind("multi"))
|
||||||
|
_button("Regeln", box, _show.bind("regeln"))
|
||||||
_button("Beenden", box, func() -> void: get_tree().quit())
|
_button("Beenden", box, func() -> void: get_tree().quit())
|
||||||
return box
|
return box
|
||||||
|
|
||||||
|
|
||||||
|
## Die Regeln auf einer Seite. Wer die Münzen nicht kennt, wüsste sonst im
|
||||||
|
## Spiel nur, was der Tooltip verrät.
|
||||||
|
func _build_regeln() -> Control:
|
||||||
|
var box := _box(680)
|
||||||
|
box.add_child(_text("Regeln", 26, Color(1.0, 0.82, 0.32)))
|
||||||
|
var absaetze := [
|
||||||
|
["Würfeln", "Jeder hat fünf Würfel unter dem Becher. Nur die eigenen sieht man."],
|
||||||
|
["Ansagen", "Reihum sagt man, wie viele Würfel einer Augenzahl auf dem ganzen Tisch liegen - zum Beispiel „vier Dreien\". Wer dran ist, muss höher gehen: mehr Würfel, oder gleich viele mit höherer Augenzahl. Oder er zweifelt."],
|
||||||
|
["Anfechten", "Stimmt die Ansage nicht, verliert der Ansager einen Würfel. Stimmt sie, verliert der Zweifler einen. Mehr als angesagt zählt als richtig."],
|
||||||
|
["Genau", "Wer glaubt, die Ansage trifft exakt, ruft „Genau\". Trifft es, verliert der Ansager einen Würfel und der Rufer bekommt einen dazu. Daneben verliert der Rufer einen."],
|
||||||
|
["Münzen", "Jeder startet mit einer Münze; wer einen Würfel verliert, bekommt eine. Geworfen wird nur am eigenen Zug, vor der Ansage. Wappen verstärkt die Wirkung, Zahl lässt es bei der Grundwirkung - die kommt immer."],
|
||||||
|
["Topf", "Geworfene Münzen liegen in der Mitte. Wer das Aufdecken gewinnt, nimmt sie. Mehr als vier passen nicht in den Vorrat; der Rest bleibt liegen. Im Topf liegen höchstens sechs - die älteste fällt vom Tisch."],
|
||||||
|
["Ende", "Wer keine Würfel mehr hat, schaut zu. Der Letzte gewinnt."],
|
||||||
|
]
|
||||||
|
for a: Array in absaetze:
|
||||||
|
var h := HBoxContainer.new()
|
||||||
|
h.add_theme_constant_override("separation", 12)
|
||||||
|
var k := _text(String(a[0]), 15, Color(1.0, 0.82, 0.32))
|
||||||
|
k.custom_minimum_size = Vector2(110, 0)
|
||||||
|
k.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
k.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||||
|
h.add_child(k)
|
||||||
|
var t := _text(String(a[1]), 15, Color(0.85, 0.87, 0.92))
|
||||||
|
t.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
|
t.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
t.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
h.add_child(t)
|
||||||
|
box.add_child(h)
|
||||||
|
_button("Zurück", box, _show.bind("main"))
|
||||||
|
return box
|
||||||
|
|
||||||
|
|
||||||
func _build_single() -> Control:
|
func _build_single() -> Control:
|
||||||
var box := _box(400)
|
var box := _box(400)
|
||||||
_title(box)
|
_title(box)
|
||||||
@@ -169,6 +216,8 @@ func _build_single() -> Control:
|
|||||||
box.add_child(_text("Bots", 15))
|
box.add_child(_text("Bots", 15))
|
||||||
solo_bots = NumberRow.new(1, BOT_NAMES_MAX, 3)
|
solo_bots = NumberRow.new(1, BOT_NAMES_MAX, 3)
|
||||||
box.add_child(solo_bots)
|
box.add_child(solo_bots)
|
||||||
|
solo_stufe = _stufen_wahl()
|
||||||
|
box.add_child(_row("Bots spielen", solo_stufe))
|
||||||
box.add_child(HSeparator.new())
|
box.add_child(HSeparator.new())
|
||||||
var row := HBoxContainer.new()
|
var row := HBoxContainer.new()
|
||||||
row.add_theme_constant_override("separation", 8)
|
row.add_theme_constant_override("separation", 8)
|
||||||
@@ -244,6 +293,8 @@ func _build_lobby() -> Control:
|
|||||||
box.add_child(_text("Bots", 15))
|
box.add_child(_text("Bots", 15))
|
||||||
lobby_bots = NumberRow.new(0, BOT_NAMES_MAX, 1)
|
lobby_bots = NumberRow.new(0, BOT_NAMES_MAX, 1)
|
||||||
box.add_child(lobby_bots)
|
box.add_child(lobby_bots)
|
||||||
|
lobby_stufe = _stufen_wahl()
|
||||||
|
box.add_child(_row("Bots spielen", lobby_stufe))
|
||||||
|
|
||||||
var row := HBoxContainer.new()
|
var row := HBoxContainer.new()
|
||||||
row.add_theme_constant_override("separation", 8)
|
row.add_theme_constant_override("separation", 8)
|
||||||
@@ -257,7 +308,7 @@ func _build_lobby() -> Control:
|
|||||||
# ------------------------------------------------------------ Aktionen
|
# ------------------------------------------------------------ Aktionen
|
||||||
|
|
||||||
func _on_solo_start() -> void:
|
func _on_solo_start() -> void:
|
||||||
Net.start_singleplayer("Spieler", solo_bots.value)
|
Net.start_singleplayer("Spieler", solo_bots.value, solo_stufe.selected)
|
||||||
|
|
||||||
|
|
||||||
## Ein leeres Namensfeld ergäbe einen namenlosen Platz am Tisch.
|
## Ein leeres Namensfeld ergäbe einen namenlosen Platz am Tisch.
|
||||||
@@ -287,7 +338,7 @@ func _on_start() -> void:
|
|||||||
if humans + ai < 2:
|
if humans + ai < 2:
|
||||||
_sagen("Mindestens 2 Spieler nötig (Mitspieler oder Bots).")
|
_sagen("Mindestens 2 Spieler nötig (Mitspieler oder Bots).")
|
||||||
return
|
return
|
||||||
Net.request_start(ai)
|
Net.request_start(ai, lobby_stufe.selected)
|
||||||
|
|
||||||
|
|
||||||
func _on_failed(msg: String) -> void:
|
func _on_failed(msg: String) -> void:
|
||||||
@@ -314,6 +365,7 @@ func _refresh() -> void:
|
|||||||
code_label.text = Net.room_code
|
code_label.text = Net.room_code
|
||||||
start_btn.disabled = not Net.is_owner
|
start_btn.disabled = not Net.is_owner
|
||||||
lobby_bots.anpassen(Net.MAX_HUMANS - Net.lobby.size(), Net.is_owner)
|
lobby_bots.anpassen(Net.MAX_HUMANS - Net.lobby.size(), Net.is_owner)
|
||||||
|
lobby_stufe.disabled = not Net.is_owner
|
||||||
|
|
||||||
var names: Array = []
|
var names: Array = []
|
||||||
for id: int in Net.lobby:
|
for id: int in Net.lobby:
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ var owner_id := 0
|
|||||||
var is_owner := false
|
var is_owner := false
|
||||||
var room_code := ""
|
var room_code := ""
|
||||||
var in_game := false
|
var in_game := false
|
||||||
|
## Wunsch für eine neue Solopartie; das frisch geladene Menü löst ihn ein.
|
||||||
|
var rematch: Dictionary = {}
|
||||||
|
|
||||||
# ── Server ───────────────────────────────────────────────────────────────────
|
# ── Server ───────────────────────────────────────────────────────────────────
|
||||||
var dedicated := false
|
var dedicated := false
|
||||||
@@ -228,7 +230,7 @@ func _push_lobby(code: String, data: Dictionary, besitzer: int) -> void:
|
|||||||
# ── Start ────────────────────────────────────────────────────────────────────
|
# ── Start ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
## Einzelspieler: kein Netzwerk, nur der lokale Spieler und Bots.
|
## Einzelspieler: kein Netzwerk, nur der lokale Spieler und Bots.
|
||||||
func start_singleplayer(pname: String, ai_count: int) -> void:
|
func start_singleplayer(pname: String, ai_count: int, stufe := 1) -> void:
|
||||||
player_name = pname
|
player_name = pname
|
||||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||||
room_code = SOLO
|
room_code = SOLO
|
||||||
@@ -237,16 +239,17 @@ func start_singleplayer(pname: String, ai_count: int) -> void:
|
|||||||
is_owner = true
|
is_owner = true
|
||||||
in_game = true
|
in_game = true
|
||||||
lobby_changed.emit()
|
lobby_changed.emit()
|
||||||
game_started.emit(SOLO, {"peers": [1], "names": lobby.duplicate(), "ai": ai_count})
|
game_started.emit(SOLO, {"peers": [1], "names": lobby.duplicate(), "ai": ai_count,
|
||||||
|
"stufe": stufe})
|
||||||
|
|
||||||
|
|
||||||
## Der Host bittet den Server um den Start.
|
## Der Host bittet den Server um den Start.
|
||||||
func request_start(ai_count: int) -> void:
|
func request_start(ai_count: int, stufe := 1) -> void:
|
||||||
_req_start.rpc_id(1, ai_count)
|
_req_start.rpc_id(1, ai_count, stufe)
|
||||||
|
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _req_start(ai_count: int) -> void:
|
func _req_start(ai_count: int, stufe: int) -> void:
|
||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
return
|
return
|
||||||
var id := multiplayer.get_remote_sender_id()
|
var id := multiplayer.get_remote_sender_id()
|
||||||
@@ -259,7 +262,8 @@ func _req_start(ai_count: int) -> void:
|
|||||||
if order.size() + ai_count < 2:
|
if order.size() + ai_count < 2:
|
||||||
return
|
return
|
||||||
raum.in_game = true
|
raum.in_game = true
|
||||||
var cfg := {"peers": order, "names": raum.lobby.duplicate(), "ai": ai_count}
|
var cfg := {"peers": order, "names": raum.lobby.duplicate(), "ai": ai_count,
|
||||||
|
"stufe": clampi(stufe, 0, 2)}
|
||||||
_start_zustellen(code, cfg)
|
_start_zustellen(code, cfg)
|
||||||
game_started.emit(code, cfg)
|
game_started.emit(code, cfg)
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ var bid_icons: Array[Sprite3D] = []
|
|||||||
var _bid_anchor: Array[Vector3] = []
|
var _bid_anchor: Array[Vector3] = []
|
||||||
var plates: Array[Sprite3D] = []
|
var plates: Array[Sprite3D] = []
|
||||||
var rims: Array[MeshInstance3D] = [] ## Rand der Zone, leuchtet beim Zug
|
var rims: Array[MeshInstance3D] = [] ## Rand der Zone, leuchtet beim Zug
|
||||||
|
var topf_muenzen: Dictionary = {} ## Wurfnummer -> Münze in der Tischmitte
|
||||||
|
static var _muenz_mesh: CylinderMesh
|
||||||
|
static var _muenz_mat: StandardMaterial3D
|
||||||
var dice: Array = [] ## seat -> Array[Die]
|
var dice: Array = [] ## seat -> Array[Die]
|
||||||
|
|
||||||
var camera: Camera3D
|
var camera: Camera3D
|
||||||
@@ -472,6 +475,85 @@ func freeze_dice(seat: int) -> void:
|
|||||||
d.set_kinematic(true)
|
d.set_kinematic(true)
|
||||||
|
|
||||||
|
|
||||||
|
## Der Topf liegt als Häufchen Messing in der Tischmitte. Jede Münze trägt
|
||||||
|
## ihre Wurfnummer; was nicht mehr in der Liste steht, ist beim Gewinner.
|
||||||
|
## Lage und Drehung kommen aus der Nummer, damit Host und Clients dasselbe
|
||||||
|
## Häufchen sehen.
|
||||||
|
func set_topf(liste: Array) -> void:
|
||||||
|
var bleibt := {}
|
||||||
|
for m in liste:
|
||||||
|
var nr := int(m.get("nr", 0))
|
||||||
|
bleibt[nr] = true
|
||||||
|
if topf_muenzen.has(nr):
|
||||||
|
continue
|
||||||
|
var r := RandomNumberGenerator.new()
|
||||||
|
r.seed = nr * 7919
|
||||||
|
var ang := r.randf() * TAU
|
||||||
|
var rad := r.randf_range(0.30, 0.55)
|
||||||
|
var ziel := Vector3(cos(ang) * rad, TABLE_H + 0.006, sin(ang) * rad)
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
mi.mesh = _muenz_form()
|
||||||
|
mi.material_override = _muenz_stoff()
|
||||||
|
mi.position = ziel + Vector3(0, 0.35, 0)
|
||||||
|
mi.rotation.y = r.randf() * TAU
|
||||||
|
add_child(mi)
|
||||||
|
topf_muenzen[nr] = mi
|
||||||
|
# Fällt aus der Hand auf den Tisch.
|
||||||
|
create_tween().tween_property(mi, "position", ziel, 0.35) \
|
||||||
|
.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||||
|
for nr in topf_muenzen.keys():
|
||||||
|
if not bleibt.has(nr):
|
||||||
|
topf_muenzen[nr].queue_free()
|
||||||
|
topf_muenzen.erase(nr)
|
||||||
|
|
||||||
|
|
||||||
|
static func _muenz_form() -> CylinderMesh:
|
||||||
|
if _muenz_mesh == null:
|
||||||
|
_muenz_mesh = CylinderMesh.new()
|
||||||
|
# Aus Kamerahöhe muss eine Münze fast so gross sein wie ein Würfel,
|
||||||
|
# sonst ist sie ein Punkt.
|
||||||
|
_muenz_mesh.top_radius = 0.11
|
||||||
|
_muenz_mesh.bottom_radius = 0.11
|
||||||
|
_muenz_mesh.height = 0.014
|
||||||
|
_muenz_mesh.radial_segments = 24
|
||||||
|
return _muenz_mesh
|
||||||
|
|
||||||
|
|
||||||
|
static func _muenz_stoff() -> StandardMaterial3D:
|
||||||
|
if _muenz_mat == null:
|
||||||
|
_muenz_mat = StandardMaterial3D.new()
|
||||||
|
_muenz_mat.albedo_color = Color(0.80, 0.62, 0.26)
|
||||||
|
_muenz_mat.metallic = 0.85
|
||||||
|
_muenz_mat.roughness = 0.35
|
||||||
|
_muenz_mat.emission_enabled = true
|
||||||
|
_muenz_mat.emission = Color(0.55, 0.40, 0.12)
|
||||||
|
_muenz_mat.emission_energy_multiplier = 0.35
|
||||||
|
return _muenz_mat
|
||||||
|
|
||||||
|
|
||||||
|
## Beim Zählen leuchten die passenden Würfel nacheinander auf, mit einem
|
||||||
|
## Tick je Würfel. So sieht man, woher die Zahl im Banner kommt.
|
||||||
|
func zaehlen_zeigen(face: int, werte: Array) -> void:
|
||||||
|
var k := 0
|
||||||
|
for seat in mini(werte.size(), dice.size()):
|
||||||
|
var dl: Array = dice[seat]
|
||||||
|
for i in mini(dl.size(), werte[seat].size()):
|
||||||
|
if int(werte[seat][i]) != face:
|
||||||
|
continue
|
||||||
|
var d = dl[i]
|
||||||
|
get_tree().create_timer(0.12 * k).timeout.connect(func() -> void:
|
||||||
|
if is_instance_valid(d):
|
||||||
|
d.hervorheben(true)
|
||||||
|
Klang.tick())
|
||||||
|
k += 1
|
||||||
|
|
||||||
|
|
||||||
|
func hervorhebung_weg() -> void:
|
||||||
|
for dl in dice:
|
||||||
|
for d in dl:
|
||||||
|
d.hervorheben(false)
|
||||||
|
|
||||||
|
|
||||||
## Der Platz, der dran ist, bekommt einen leuchtenden Rand. In der dunklen
|
## Der Platz, der dran ist, bekommt einen leuchtenden Rand. In der dunklen
|
||||||
## Szene ist das von jeder Ecke aus zu sehen - ein farbiger Name allein nicht.
|
## Szene ist das von jeder Ecke aus zu sehen - ein farbiger Name allein nicht.
|
||||||
func set_active_seat(seat: int) -> void:
|
func set_active_seat(seat: int) -> void:
|
||||||
|
|||||||
81
test_cli.gd
Normal file
81
test_cli.gd
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
extends Node
|
||||||
|
const ORT := "/tmp/claude-1000/-home-arbeit-projects-pirate-dice/00de2cf5-7327-4fce-b543-2b4db6d3b775/scratchpad"
|
||||||
|
var game: Node
|
||||||
|
var rolle := ""
|
||||||
|
var runden := 0
|
||||||
|
var wuerfe := 0
|
||||||
|
var genau := 0
|
||||||
|
var _letzte_phase := ""
|
||||||
|
var _rng := RandomNumberGenerator.new()
|
||||||
|
func _ready() -> void:
|
||||||
|
Engine.time_scale = 8.0
|
||||||
|
_rng.randomize()
|
||||||
|
rolle = OS.get_cmdline_user_args()[0]
|
||||||
|
add_child(preload("res://scenes/main.tscn").instantiate())
|
||||||
|
await get_tree().create_timer(0.3).timeout
|
||||||
|
_lauf()
|
||||||
|
func _find(n: Node, w: String) -> Node:
|
||||||
|
if n.name == w: return n
|
||||||
|
for c in n.get_children():
|
||||||
|
var r := _find(c, w)
|
||||||
|
if r != null: return r
|
||||||
|
return null
|
||||||
|
func _warte(cond: Callable, sek: float) -> bool:
|
||||||
|
var t := 0.0
|
||||||
|
while t < sek:
|
||||||
|
if cond.call(): return true
|
||||||
|
await get_tree().create_timer(0.1).timeout
|
||||||
|
t += 0.1
|
||||||
|
return false
|
||||||
|
func _lauf() -> void:
|
||||||
|
if rolle == "host":
|
||||||
|
Net.create_room("Host")
|
||||||
|
await _warte(func(): return Net.room_code != "", 40.0)
|
||||||
|
var f := FileAccess.open(ORT + "/code.txt", FileAccess.WRITE)
|
||||||
|
f.store_string(Net.room_code); f.close()
|
||||||
|
await _warte(func(): return Net.lobby.size() >= 2, 80.0)
|
||||||
|
Net.request_start(1, 1)
|
||||||
|
else:
|
||||||
|
await _warte(func(): return FileAccess.file_exists(ORT + "/code.txt"), 80.0)
|
||||||
|
await get_tree().create_timer(0.5).timeout
|
||||||
|
Net.join_room(FileAccess.get_file_as_string(ORT + "/code.txt").strip_edges(), "Gast")
|
||||||
|
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 120.0)
|
||||||
|
if game == null:
|
||||||
|
print("%s: kein Spiel" % rolle); get_tree().quit(1); return
|
||||||
|
var t := 0.0
|
||||||
|
var gewartet := 0.0
|
||||||
|
while t < 4400.0 and is_instance_valid(game) and game.phase != "over":
|
||||||
|
await get_tree().create_timer(0.2).timeout; t += 0.2
|
||||||
|
if not is_instance_valid(game): break
|
||||||
|
if game.phase != _letzte_phase:
|
||||||
|
_letzte_phase = game.phase
|
||||||
|
if game.phase == "roll": runden += 1
|
||||||
|
var me: int = game.local_seat
|
||||||
|
if game.phase != "bid" or game.turn != me or not game.seats[me].alive:
|
||||||
|
gewartet = 0.0; continue
|
||||||
|
gewartet += 0.2
|
||||||
|
if gewartet < 0.6: continue
|
||||||
|
var hand: Array = game.hand.get(me, [])
|
||||||
|
if not hand.is_empty() and (not BoosterDB.needs_bid(hand[0]) or not game.bid.is_empty()) and _rng.randf() < 0.7:
|
||||||
|
game._on_booster_chosen(hand[0], 0)
|
||||||
|
wuerfe += 1
|
||||||
|
await get_tree().create_timer(2.5).timeout
|
||||||
|
if not is_instance_valid(game) or game.turn != me or game.phase != "bid": continue
|
||||||
|
var total := 0
|
||||||
|
for s in game.seats: total += int(s.count)
|
||||||
|
var a: Dictionary = DiceAI.choose_action(game.my_values, total, game.bid, false, _rng)
|
||||||
|
if a.type == "challenge" and not game.bid.is_empty() and _rng.randf() < 0.3:
|
||||||
|
a = {"type": "exact"}
|
||||||
|
match a.type:
|
||||||
|
"bid": game._on_bid_pressed(int(a.count), int(a.face))
|
||||||
|
"challenge": game._on_challenge_pressed()
|
||||||
|
"exact":
|
||||||
|
game._on_exact_pressed(); genau += 1
|
||||||
|
gewartet = -2.0
|
||||||
|
if is_instance_valid(game):
|
||||||
|
print("%s: Ende %s '%s' Platz=%d Runden=%d Wuerfe=%d Genau=%d" % [rolle, game.phase, game.banner, game.local_seat, runden, wuerfe, genau])
|
||||||
|
for i in game.seats.size():
|
||||||
|
print("%s: Platz %d %s count=%d alive=%s" % [rolle, i, game.seats[i].name, game.seats[i].count, game.seats[i].alive])
|
||||||
|
else:
|
||||||
|
print("%s: Spiel weg, Runden=%d" % [rolle, runden])
|
||||||
|
get_tree().quit()
|
||||||
6
test_cli.tscn
Normal file
6
test_cli.tscn
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://test_cli.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="Cli" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
32
test_srv.gd
Normal file
32
test_srv.gd
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
extends Node
|
||||||
|
const ORT := "/tmp/claude-1000/-home-arbeit-projects-pirate-dice/00de2cf5-7327-4fce-b543-2b4db6d3b775/scratchpad"
|
||||||
|
var game: Node
|
||||||
|
func _ready() -> void:
|
||||||
|
Engine.time_scale = 8.0
|
||||||
|
add_child(preload("res://scenes/main.tscn").instantiate())
|
||||||
|
_lauf()
|
||||||
|
func _find(n: Node, w: String) -> Node:
|
||||||
|
if n.name == w: return n
|
||||||
|
for c in n.get_children():
|
||||||
|
var r := _find(c, w)
|
||||||
|
if r != null: return r
|
||||||
|
return null
|
||||||
|
func _lauf() -> void:
|
||||||
|
var t := 0.0
|
||||||
|
var runden := 0
|
||||||
|
var letzte := ""
|
||||||
|
while t < 4500.0:
|
||||||
|
await get_tree().create_timer(0.5).timeout; t += 0.5
|
||||||
|
if game == null:
|
||||||
|
game = _find(get_tree().root, "Game"); continue
|
||||||
|
if not is_instance_valid(game) or game.phase == "over": break
|
||||||
|
if game.phase != letzte:
|
||||||
|
letzte = game.phase
|
||||||
|
if letzte == "roll": runden += 1
|
||||||
|
if is_instance_valid(game):
|
||||||
|
print("SERVER: Ende %s '%s' nach %.0fs, Runden=%d" % [game.phase, game.banner, t, runden])
|
||||||
|
for i in game.seats.size():
|
||||||
|
print("SERVER: Platz %d %s count=%d alive=%s" % [i, game.seats[i].name, game.seats[i].count, game.seats[i].alive])
|
||||||
|
# Den Clients Zeit lassen, das Ende zu sehen und selbst zu gehen.
|
||||||
|
await get_tree().create_timer(80.0).timeout
|
||||||
|
get_tree().quit()
|
||||||
6
test_srv.tscn
Normal file
6
test_srv.tscn
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://test_srv.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="Srv" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
Reference in New Issue
Block a user