update
This commit is contained in:
6
Makefile
6
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: dev server prod logs down test
|
.PHONY: dev server prod logs down test sim
|
||||||
|
|
||||||
GODOT ?= $(HOME)/bin/godot
|
GODOT ?= $(HOME)/bin/godot
|
||||||
|
|
||||||
@@ -16,6 +16,10 @@ dev:
|
|||||||
test:
|
test:
|
||||||
GODOT=$(GODOT) bash tests/run.sh
|
GODOT=$(GODOT) bash tests/run.sh
|
||||||
|
|
||||||
|
# Bot gegen Bot, je Stufe 12 Partien parallel (~3 min): Rundenzahl und Siegquoten.
|
||||||
|
sim:
|
||||||
|
@for s in 0 1 2; do $(GODOT) --headless --path . res://tests/sim.tscn -- $$s 12 2>/dev/null | grep "^SIM" & done; wait
|
||||||
|
|
||||||
# ── Produktion ───────────────────────────────────────────────────────────────
|
# ── Produktion ───────────────────────────────────────────────────────────────
|
||||||
# Auf dem Server ausführen, im Projektverzeichnis. Der Build holt Godot selbst.
|
# Auf dem Server ausführen, im Projektverzeichnis. Der Build holt Godot selbst.
|
||||||
prod:
|
prod:
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -41,7 +41,8 @@ Die Partie läuft nicht beim Host, sondern auf `dice.marha.de`:
|
|||||||
* **Wiedereinstieg.** Wer die Verbindung verliert, wird zum Bot. Tritt er mit
|
* **Wiedereinstieg.** Wer die Verbindung verliert, wird zum Bot. Tritt er mit
|
||||||
demselben Namen und Code wieder bei, bekommt er seinen Platz zurück: der
|
demselben Namen und Code wieder bei, bekommt er seinen Platz zurück: der
|
||||||
Server schickt ihm die laufende Aufstellung statt einer Lobby, Zustand und
|
Server schickt ihm die laufende Aufstellung statt einer Lobby, Zustand und
|
||||||
Würfellage kommen kurz danach nach.
|
Würfellage kommen kurz danach nach. Das Menü merkt sich den Abbruch und
|
||||||
|
bietet **Zurück in Raum XY** an - ein Klick, kein Code tippen.
|
||||||
* Der **Host** ist der erste Spieler im Raum. Geht er, rückt der nächste nach.
|
* Der **Host** ist der erste Spieler im Raum. Geht er, rückt der nächste nach.
|
||||||
Nur er stellt die Bots ein und startet.
|
Nur er stellt die Bots ein und startet.
|
||||||
* Der letzte macht das Licht aus: verlässt der letzte Spieler den Raum, wird er
|
* Der letzte macht das Licht aus: verlässt der letzte Spieler den Raum, wird er
|
||||||
@@ -162,8 +163,12 @@ 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
|
Beim Zählen leuchten die passenden Würfel nacheinander auf. So baut sich die
|
||||||
Spannung auf, statt dass alles auf einmal dasteht.
|
Spannung auf, statt dass alles auf einmal dasteht.
|
||||||
|
|
||||||
Nach dem Ende gibt es solo ein **Nochmal** mit denselben Bots. Im Netz führt
|
Nach dem Ende gibt es ein **Nochmal** mit derselben Runde - solo sofort, im
|
||||||
der Weg zurück ins Menü.
|
Netz für den Raumbesitzer; der Server setzt den Raum dann für alle neu auf.
|
||||||
|
|
||||||
|
Fällt ein Würfel je über die Bande oder durch den Filz, fängt ihn ein
|
||||||
|
Fangnetz und legt ihn zurück in seine Zone - sonst käme das Würfeln nie zur
|
||||||
|
Ruhe.
|
||||||
|
|
||||||
Wer keine Würfel mehr hat, scheidet aus und schaut zu. Der Letzte gewinnt.
|
Wer keine Würfel mehr hat, scheidet aus und schaut zu. Der Letzte gewinnt.
|
||||||
|
|
||||||
@@ -237,7 +242,7 @@ das ist seine Grundwirkung, und Wappen macht daraus nur eine 6.
|
|||||||
die Münzen zu seiner Zone.
|
die Münzen zu seiner Zone.
|
||||||
* Klänge (Würfel, Münze, Becher, Gewinn) werden beim Start berechnet. Es gibt
|
* Klänge (Würfel, Münze, Becher, Gewinn) werden beim Start berechnet. Es gibt
|
||||||
keine Tondateien, so wie es auch keine Bilddateien gibt.
|
keine Tondateien, so wie es auch keine Bilddateien gibt.
|
||||||
* Die Regeln stehen im Menü unter **Regeln**.
|
* Die Regeln stehen im Menü unter **Regeln**, inklusive aller sechs Münzen.
|
||||||
|
|
||||||
### Bots
|
### Bots
|
||||||
|
|
||||||
@@ -258,12 +263,14 @@ vermutlich haarscharf, rufen sie „Genau".
|
|||||||
| `scripts/net.gd` | Autoload: WebSocket-Verbindung, Räume, Lobby, Spielstart |
|
| `scripts/net.gd` | Autoload: WebSocket-Verbindung, Räume, Lobby, Spielstart |
|
||||||
| `scripts/boosters.gd` | Autoload: Münzkatalog mit Grund- und Wappenwirkung |
|
| `scripts/boosters.gd` | Autoload: Münzkatalog mit Grund- und Wappenwirkung |
|
||||||
| `scripts/game.gd` | Spiellogik (host-autoritativ), RPCs, Zustandsabgleich |
|
| `scripts/game.gd` | Spiellogik (host-autoritativ), RPCs, Zustandsabgleich |
|
||||||
|
| `scripts/muenzen.gd` | Münzen: ziehen, werfen, Wirkung, Bot-Absicht, Topf |
|
||||||
|
| `scripts/aufdecken.gd` | Aufdecken in Etappen, Genau, Topfvergabe |
|
||||||
| `scripts/table.gd` | Aufbau der 3D-Szene: Tisch, Würfelzonen, Becher, Kamera |
|
| `scripts/table.gd` | Aufbau der 3D-Szene: Tisch, Würfelzonen, Becher, Kamera |
|
||||||
| `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/klang.gd` | Autoload: berechnete Klänge, keine Dateien |
|
||||||
| `tests/` | Headless-Tests, `make test` - siehe `tests/README.md` |
|
| `tests/` | Headless-Tests, `make test` (~30 s); `make sim` misst die Bots - siehe `tests/README.md` |
|
||||||
| `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
|
||||||
|
|||||||
@@ -55,10 +55,14 @@ static var stufe := 1
|
|||||||
const STUFEN := [
|
const STUFEN := [
|
||||||
{"bluff": 0.15, "genau": 0.35, "zweifel": 0.40, "sicher": 0.30},
|
{"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.40, "genau": 0.22, "zweifel": 0.30, "sicher": 0.42},
|
||||||
{"bluff": 0.50, "genau": 0.18, "zweifel": 0.24, "sicher": 0.50},
|
# Gemessen: bei zweifel 0.24 / genau 0.18 dauerte eine Partie doppelt so
|
||||||
|
# lang wie auf normal - viele Züge je Runde, viele nullsummige Genau-Treffer.
|
||||||
|
{"bluff": 0.50, "genau": 0.26, "zweifel": 0.28, "sicher": 0.48},
|
||||||
]
|
]
|
||||||
## Wie oft er ein sicheres Gebot einen Schritt höher setzt, als nötig wäre.
|
## Wie oft er ein sicheres Gebot einen Schritt höher setzt, als nötig wäre.
|
||||||
const FRECH := 0.12
|
const FRECH := 0.12
|
||||||
|
## Wie oft er eine gute Genau-Gelegenheit auch wirklich nutzt.
|
||||||
|
const GENAU_LUST := 0.35
|
||||||
|
|
||||||
|
|
||||||
static func _regler() -> Dictionary:
|
static func _regler() -> Dictionary:
|
||||||
@@ -85,7 +89,10 @@ static func choose_action(values: Array, total_dice: int, bid: Dictionary,
|
|||||||
if not must_bid:
|
if not must_bid:
|
||||||
# Liegt die Ansage vermutlich haarscharf, lohnt "Genau" mehr als die
|
# Liegt die Ansage vermutlich haarscharf, lohnt "Genau" mehr als die
|
||||||
# Anfechtung: der Ansager verliert, und man bekommt selbst einen dazu.
|
# 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:
|
# Aber nur ab und zu: Genau nimmt keinen Würfel vom Tisch. Ein Bot,
|
||||||
|
# der es bei jeder Gelegenheit ruft, zieht die Partie ins Endlose.
|
||||||
|
if exact_chance(known, unknown, bid.count, bid.face) > float(r.genau) and hold < 0.6 \
|
||||||
|
and rng.randf() < GENAU_LUST:
|
||||||
return {"type": "exact"}
|
return {"type": "exact"}
|
||||||
var doubt := float(r.zweifel) + rng.randf_range(-0.07, 0.07)
|
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):
|
||||||
|
|||||||
104
scripts/aufdecken.gd
Normal file
104
scripts/aufdecken.gd
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Das Aufdecken in drei Etappen: Becher hoch, zählen, Urteil. Dazu Genau
|
||||||
|
## und die Vergabe des Topfs. Arbeitet auf dem Spiel `g`.
|
||||||
|
|
||||||
|
var g: Node
|
||||||
|
|
||||||
|
|
||||||
|
func _init(spiel: Node) -> void:
|
||||||
|
g = spiel
|
||||||
|
|
||||||
|
func _phase_reveal(result: Dictionary) -> void:
|
||||||
|
g.phase = "reveal"
|
||||||
|
g.deadline = 0.0
|
||||||
|
var b: Dictionary = result.bid
|
||||||
|
var rufer: int = result.challenger
|
||||||
|
var genau: bool = result.get("exact", false)
|
||||||
|
var total: int = g._count_face(int(b.face))
|
||||||
|
|
||||||
|
# 1. Becher hoch. Noch kein Urteil - erst schauen.
|
||||||
|
g.reveal_total = -1
|
||||||
|
g.banner = "Genau?" if genau else "Aufdecken"
|
||||||
|
for i in g._alive_seats():
|
||||||
|
g.table.arrange_and_show(i, g.host_values[i], g.rng)
|
||||||
|
g.table.set_cup_down(i, false)
|
||||||
|
# Gezielt an die Mitspieler am Tisch: ein Peer, der nur in der Lobby sitzt,
|
||||||
|
# hat keinen Spielknoten und würde das Paket ins Leere laufen lassen.
|
||||||
|
var values := _all_values()
|
||||||
|
for i in g.seats.size():
|
||||||
|
var peer: int = g.seats[i].peer
|
||||||
|
if g._peer_live(peer):
|
||||||
|
g._reveal.rpc_id(peer, values, g.banner)
|
||||||
|
g._push_state()
|
||||||
|
await g._sleep(g.ZEIT_BECHER)
|
||||||
|
if not g._alive():
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. Zählen. Die Ansage in der Mitte färbt sich jetzt ein.
|
||||||
|
g.reveal_total = total
|
||||||
|
g.banner = "%d gefunden, %d angesagt" % [total, b.count]
|
||||||
|
g._push_state()
|
||||||
|
await g._sleep(g.ZEIT_ZAEHLEN)
|
||||||
|
if not g._alive():
|
||||||
|
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
|
||||||
|
g.banner = "Genau! %s verliert, %s bekommt einen Würfel" % [
|
||||||
|
g.seats[int(b.seat)].name, g.seats[rufer].name]
|
||||||
|
elif genau:
|
||||||
|
verlierer = [rufer]
|
||||||
|
winner = int(b.seat)
|
||||||
|
g.banner = "Daneben - %s verliert" % g.seats[rufer].name
|
||||||
|
else:
|
||||||
|
var loser := rufer if total >= int(b.count) else int(b.seat)
|
||||||
|
verlierer = [loser]
|
||||||
|
winner = int(b.seat) if loser == rufer else rufer
|
||||||
|
g.banner = "%s verliert" % g.seats[loser].name
|
||||||
|
g._log("%d x %d angesagt - tatsächlich %d. %s" % [b.count, b.face, total, g.banner])
|
||||||
|
g._topf_an(winner)
|
||||||
|
g._push_state()
|
||||||
|
await g._sleep(g.ZEIT_URTEIL)
|
||||||
|
if not g._alive():
|
||||||
|
return
|
||||||
|
|
||||||
|
for l: int in verlierer:
|
||||||
|
g.seats[l].count -= 1
|
||||||
|
if g.seats[l].count <= 0:
|
||||||
|
g.seats[l].alive = false
|
||||||
|
g.seats[l].count = 0
|
||||||
|
g.host_values[l] = []
|
||||||
|
g.table.set_dice_count(l, 0)
|
||||||
|
g._log("%s scheidet aus." % g.seats[l].name)
|
||||||
|
else:
|
||||||
|
g._zieh_muenze(l)
|
||||||
|
if bonus >= 0 and g.seats[bonus].alive:
|
||||||
|
g.seats[bonus].count += 1
|
||||||
|
g.host_values[bonus].append(g.rng.randi_range(1, 6))
|
||||||
|
g.table.set_dice_count(bonus, g.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]
|
||||||
|
g.starter = naechster if g.seats[naechster].alive else g._next_alive(naechster)
|
||||||
|
# Nur wer noch spielt, bekommt den Becher wieder. Auf einer leeren Zone
|
||||||
|
# sähe er aus, als lägen dort noch Würfel.
|
||||||
|
for i in g.seats.size():
|
||||||
|
g.table.set_dice_visible(i, false)
|
||||||
|
g.table.set_cup_down(i, g.seats[i].alive)
|
||||||
|
g.banner = ""
|
||||||
|
g._push_state()
|
||||||
|
|
||||||
|
|
||||||
|
func _all_values() -> Array:
|
||||||
|
var out: Array = []
|
||||||
|
for i in g.seats.size():
|
||||||
|
out.append(g.host_values[i] if g.seats[i].alive else [])
|
||||||
|
return out
|
||||||
327
scripts/game.gd
327
scripts/game.gd
@@ -13,6 +13,11 @@ const WURF_ZEIT := 1.1 ## wie lange die geworfene Münze zu sehen ist,
|
|||||||
|
|
||||||
var table: GameTable
|
var table: GameTable
|
||||||
var hud: CanvasLayer
|
var hud: CanvasLayer
|
||||||
|
## Alle Wartezeiten des Ablaufs (Bot-Pausen, Aufdecken, Münzwurf) laufen
|
||||||
|
## durch `_sleep` und lassen sich hiermit stauchen. Im Spiel 1.0; die Tests
|
||||||
|
## setzen 0, sonst dauert eine Bot-Partie Minuten statt Sekunden. Kein
|
||||||
|
## class_name dafür: den Klassencache baut nur der Editor, headless fehlt er.
|
||||||
|
var tempo := 1.0
|
||||||
|
|
||||||
var rng := RandomNumberGenerator.new()
|
var rng := RandomNumberGenerator.new()
|
||||||
var seats: Array = [] ## [{peer, name, ai, count, alive}]
|
var seats: Array = [] ## [{peer, name, ai, count, alive}]
|
||||||
@@ -64,13 +69,14 @@ func setup(cfg: Dictionary) -> void:
|
|||||||
seats.append({"peer": int(s.peer), "name": String(s.name), "ai": bool(s.ai),
|
seats.append({"peer": int(s.peer), "name": String(s.name), "ai": bool(s.ai),
|
||||||
"count": int(s.count), "alive": bool(s.alive), "verwaist": false})
|
"count": int(s.count), "alive": bool(s.alive), "verwaist": false})
|
||||||
else:
|
else:
|
||||||
|
var dice := int(cfg.get("dice", START_DICE)) # Tests spielen mit weniger
|
||||||
var peers: Array = cfg.peers
|
var peers: Array = cfg.peers
|
||||||
for p in peers:
|
for p in peers:
|
||||||
seats.append({"peer": int(p), "name": String(cfg.names[p]), "ai": false,
|
seats.append({"peer": int(p), "name": String(cfg.names[p]), "ai": false,
|
||||||
"count": START_DICE, "alive": true, "verwaist": false})
|
"count": dice, "alive": true, "verwaist": false})
|
||||||
for i in int(cfg.ai):
|
for i in int(cfg.ai):
|
||||||
seats.append({"peer": 0, "name": "Bot %d" % (i + 1), "ai": true,
|
seats.append({"peer": 0, "name": "Bot %d" % (i + 1), "ai": true,
|
||||||
"count": START_DICE, "alive": true, "verwaist": false})
|
"count": dice, "alive": true, "verwaist": false})
|
||||||
|
|
||||||
# Der Spielserver spielt nicht mit und bekommt deshalb keinen Platz.
|
# Der Spielserver spielt nicht mit und bekommt deshalb keinen Platz.
|
||||||
var me := multiplayer.get_unique_id()
|
var me := multiplayer.get_unique_id()
|
||||||
@@ -299,6 +305,10 @@ func _await_settled(list: Array, timeout := 7.0) -> void:
|
|||||||
continue
|
continue
|
||||||
var ok := true
|
var ok := true
|
||||||
for s in list:
|
for s in list:
|
||||||
|
# Verirrte Würfel zurückholen, sonst wartet man hier vergebens.
|
||||||
|
if table.fange_verirrte(s):
|
||||||
|
ok = false
|
||||||
|
continue
|
||||||
for d in table.dice_of(s):
|
for d in table.dice_of(s):
|
||||||
if d.freeze:
|
if d.freeze:
|
||||||
continue
|
continue
|
||||||
@@ -311,136 +321,45 @@ func _await_settled(list: Array, timeout := 7.0) -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- Münzen
|
# ---------------------------------------------------------------- Münzen und Aufdecken
|
||||||
|
# Ausgelagert nach muenzen.gd und aufdecken.gd. Die Hüllen hier bleiben, weil
|
||||||
|
# Tests und Aufrufer den Weg über das Spiel kennen.
|
||||||
|
|
||||||
|
var muenzen := preload("res://scripts/muenzen.gd").new(self)
|
||||||
|
var aufdecken := preload("res://scripts/aufdecken.gd").new(self)
|
||||||
|
|
||||||
|
|
||||||
## Wer einen Würfel abgibt, bekommt eine Münze. Das ist der einzige Nachschub.
|
|
||||||
func _zieh_muenze(seat: int) -> void:
|
func _zieh_muenze(seat: int) -> void:
|
||||||
var muenzen: Array = hand.get(seat, [])
|
muenzen._zieh_muenze(seat)
|
||||||
if muenzen.size() >= BoosterDB.HANDLIMIT:
|
|
||||||
_log("%s hat schon drei Münzen und bekommt keine." % seats[seat].name)
|
|
||||||
return
|
|
||||||
var id := BoosterDB.zieh(rng)
|
|
||||||
muenzen.append(id)
|
|
||||||
hand[seat] = muenzen
|
|
||||||
_log("%s bekommt %s." % [seats[seat].name, BoosterDB.get_booster(id).name])
|
|
||||||
|
|
||||||
|
|
||||||
## Ausspielen heißt: Münze in den Topf werfen. Geht nur am eigenen Zug, bevor
|
|
||||||
## 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 seat != turn or _answered.has(seat):
|
await muenzen._do_play(seat, id, target)
|
||||||
return
|
|
||||||
if _muenze_laeuft:
|
|
||||||
return
|
|
||||||
if BoosterDB.needs_bid(id) and bid.is_empty():
|
|
||||||
return
|
|
||||||
var muenzen: Array = hand.get(seat, [])
|
|
||||||
if not muenzen.has(id):
|
|
||||||
return
|
|
||||||
muenzen.erase(id)
|
|
||||||
hand[seat] = muenzen
|
|
||||||
_muenze_laeuft = true
|
|
||||||
|
|
||||||
var stark := rng.randf() < BoosterDB.WAPPEN_CHANCE
|
|
||||||
_wurf_nr += 1
|
|
||||||
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,
|
|
||||||
BoosterDB.get_booster(id).name, "Wappen" if stark else "Zahl"])
|
|
||||||
|
|
||||||
# Erst die Münze zeigen, dann wirken lassen. Andersherum sähe man die
|
|
||||||
# Folge vor der Ursache.
|
|
||||||
_push_state()
|
|
||||||
await _sleep(WURF_ZEIT)
|
|
||||||
if _alive():
|
|
||||||
await _wirkung(seat, id, target, stark)
|
|
||||||
_muenze_laeuft = false
|
|
||||||
if not _alive():
|
|
||||||
return
|
|
||||||
_snapshot_all()
|
|
||||||
_push_state()
|
|
||||||
|
|
||||||
|
|
||||||
func _wirkung(seat: int, id: String, target: int, stark: bool) -> void:
|
func _wirkung(seat: int, id: String, target: int, stark: bool) -> void:
|
||||||
var vals: Array = host_values[seat]
|
await muenzen._wirkung(seat, id, target, stark)
|
||||||
var ziel: int = clampi(target, 0, maxi(0, vals.size() - 1))
|
|
||||||
match id:
|
|
||||||
"peek":
|
|
||||||
var info := {}
|
|
||||||
for o in _alive_seats():
|
|
||||||
if o != seat:
|
|
||||||
info[o] = _zufalls_paare(o, 2 if stark else 1)
|
|
||||||
peeked[seat] = info
|
|
||||||
"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):
|
|
||||||
_set_die(seat, k, int(bid.face))
|
|
||||||
"gain_die":
|
|
||||||
seats[seat].count += 1
|
|
||||||
table.set_dice_count(seat, seats[seat].count)
|
|
||||||
host_values[seat].append(1)
|
|
||||||
var neu: int = host_values[seat].size() - 1
|
|
||||||
await _neu_werfen(seat, [neu])
|
|
||||||
if stark:
|
|
||||||
_set_die(seat, neu, 6)
|
|
||||||
"bid_up":
|
|
||||||
bid.count = int(bid.count) + (2 if stark else 1)
|
|
||||||
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})
|
|
||||||
"gag":
|
|
||||||
if stark:
|
|
||||||
for o in _alive_seats():
|
|
||||||
if o != seat:
|
|
||||||
must_bid[o] = true
|
|
||||||
else:
|
|
||||||
must_bid[_next_alive(seat)] = true
|
|
||||||
|
|
||||||
|
|
||||||
func _count_face(face: int) -> int:
|
func _count_face(face: int) -> int:
|
||||||
var n := 0
|
return muenzen._count_face(face)
|
||||||
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.
|
func _bot_muenze(seat: int, a: Dictionary) -> bool:
|
||||||
func _zufalls_idx(seat: int, n: int) -> Array:
|
return await muenzen._bot_muenze(seat, a)
|
||||||
var frei: Array = range(host_values[seat].size())
|
|
||||||
var out: Array = []
|
|
||||||
while out.size() < n and not frei.is_empty():
|
|
||||||
out.append(frei.pop_at(rng.randi_range(0, frei.size() - 1)))
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
func _zufalls_paare(seat: int, n: int) -> Array:
|
func _topf_an(winner: int) -> void:
|
||||||
var out: Array = []
|
muenzen._topf_an(winner)
|
||||||
for k: int in _zufalls_idx(seat, n):
|
|
||||||
out.append([k, host_values[seat][k]])
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
## Der selbst gewählte Würfel, bei Wappen zusätzlich ein anderer zufälliger.
|
func _phase_reveal(result: Dictionary) -> void:
|
||||||
func _ziel_und_zufall(seat: int, ziel: int, stark: bool) -> Array:
|
await aufdecken._phase_reveal(result)
|
||||||
var out: Array = [ziel]
|
|
||||||
if not stark:
|
|
||||||
return out
|
func _all_values() -> Array:
|
||||||
var frei: Array = []
|
return aufdecken._all_values()
|
||||||
for k in host_values[seat].size():
|
|
||||||
if k != ziel:
|
|
||||||
frei.append(k)
|
|
||||||
if not frei.is_empty():
|
|
||||||
out.append(frei[rng.randi_range(0, frei.size() - 1)])
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
## Würfel neu werfen und die Augenzahl danach übernehmen.
|
## Würfel neu werfen und die Augenzahl danach übernehmen.
|
||||||
@@ -518,61 +437,6 @@ func _phase_bid() -> Dictionary:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
## Ein Bot wirft eine Münze, wenn sie zu seinem Vorhaben passt. `a` ist die
|
|
||||||
## geplante Handlung. Gibt zurück, ob geworfen wurde.
|
|
||||||
func _bot_muenze(seat: int, a: Dictionary) -> bool:
|
|
||||||
var muenzen: Array = hand.get(seat, [])
|
|
||||||
if muenzen.is_empty():
|
|
||||||
return false
|
|
||||||
var vals: Array = host_values[seat]
|
|
||||||
var unknown: int = _total_dice() - vals.size()
|
|
||||||
var will_bieten: bool = a.type == "bid"
|
|
||||||
# Wie glaubwürdig ist das, was der Bot vorhat bzw. was auf dem Tisch liegt?
|
|
||||||
var eigenes: float = DiceAI.chance(vals, unknown, int(a.count), int(a.face)) if will_bieten else 0.0
|
|
||||||
var liegt: float = DiceAI.chance(vals, unknown, int(bid.count), int(bid.face)) if not bid.is_empty() else 0.0
|
|
||||||
|
|
||||||
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)
|
|
||||||
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":
|
if a.type == "exact":
|
||||||
@@ -616,124 +480,11 @@ func _valid_bid(seat: int, count: int, face: int) -> bool:
|
|||||||
return count == bid.count and face > bid.face
|
return count == bid.count and face > bid.face
|
||||||
|
|
||||||
|
|
||||||
func _phase_reveal(result: Dictionary) -> void:
|
|
||||||
phase = "reveal"
|
|
||||||
deadline = 0.0
|
|
||||||
var b: Dictionary = result.bid
|
|
||||||
var rufer: int = result.challenger
|
|
||||||
var genau: bool = result.get("exact", false)
|
|
||||||
var total := _count_face(int(b.face))
|
|
||||||
|
|
||||||
# 1. Becher hoch. Noch kein Urteil - erst schauen.
|
|
||||||
reveal_total = -1
|
|
||||||
banner = "Genau?" if genau else "Aufdecken"
|
|
||||||
for i in _alive_seats():
|
|
||||||
table.arrange_and_show(i, host_values[i], rng)
|
|
||||||
table.set_cup_down(i, false)
|
|
||||||
# Gezielt an die Mitspieler am Tisch: ein Peer, der nur in der Lobby sitzt,
|
|
||||||
# hat keinen Spielknoten und würde das Paket ins Leere laufen lassen.
|
|
||||||
var values := _all_values()
|
|
||||||
for i in seats.size():
|
|
||||||
var peer: int = seats[i].peer
|
|
||||||
if _peer_live(peer):
|
|
||||||
_reveal.rpc_id(peer, values, banner)
|
|
||||||
_push_state()
|
|
||||||
await _sleep(ZEIT_BECHER)
|
|
||||||
if not _alive():
|
|
||||||
return
|
|
||||||
|
|
||||||
# 2. Zählen. Die Ansage in der Mitte färbt sich jetzt ein.
|
|
||||||
reveal_total = total
|
|
||||||
banner = "%d gefunden, %d angesagt" % [total, b.count]
|
|
||||||
_push_state()
|
|
||||||
await _sleep(ZEIT_ZAEHLEN)
|
|
||||||
if not _alive():
|
|
||||||
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:
|
|
||||||
var loser := rufer if total >= int(b.count) else int(b.seat)
|
|
||||||
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
|
|
||||||
# sähe er aus, als lägen dort noch Würfel.
|
|
||||||
for i in seats.size():
|
|
||||||
table.set_dice_visible(i, false)
|
|
||||||
table.set_cup_down(i, seats[i].alive)
|
|
||||||
banner = ""
|
|
||||||
_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
|
|
||||||
_topf_ziel = winner
|
|
||||||
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:
|
|
||||||
var out: Array = []
|
|
||||||
for i in seats.size():
|
|
||||||
out.append(host_values[i] if seats[i].alive else [])
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
## process_always = false: eine offene Pause hält den Timer wirklich an.
|
## process_always = false: eine offene Pause hält den Timer wirklich an.
|
||||||
func _sleep(sec: float) -> void:
|
func _sleep(sec: float) -> void:
|
||||||
if not _alive():
|
if not _alive():
|
||||||
return
|
return
|
||||||
await get_tree().create_timer(sec, false).timeout
|
await get_tree().create_timer(sec * tempo, false).timeout
|
||||||
|
|
||||||
|
|
||||||
func _wait_until(cond: Callable, timeout: float) -> bool:
|
func _wait_until(cond: Callable, timeout: float) -> bool:
|
||||||
@@ -983,11 +734,15 @@ func _on_pause_changed(on: bool) -> void:
|
|||||||
_push_state()
|
_push_state()
|
||||||
|
|
||||||
|
|
||||||
## Noch eine Solopartie mit denselben Bots. Der Wunsch wandert über Net ins
|
## Noch eine Partie mit denselben Bots. Solo wandert der Wunsch über Net ins
|
||||||
## neu geladene Menü, das dann sofort startet.
|
## neu geladene Menü, das dann sofort startet. Im Netz bittet der Raumbesitzer
|
||||||
|
## den Server, den Raum neu aufzusetzen.
|
||||||
func _on_rematch() -> void:
|
func _on_rematch() -> void:
|
||||||
if _quitting:
|
if _quitting:
|
||||||
return
|
return
|
||||||
|
if Net.room_code != Net.SOLO:
|
||||||
|
Net.request_rematch()
|
||||||
|
return
|
||||||
var bots := 0
|
var bots := 0
|
||||||
for s in seats:
|
for s in seats:
|
||||||
if s.ai:
|
if s.ai:
|
||||||
|
|||||||
@@ -748,9 +748,9 @@ func update_state(st: Dictionary, my_seat: int) -> void:
|
|||||||
banner_label.text = "" if st.phase == "over" else st.banner
|
banner_label.text = "" if st.phase == "over" else st.banner
|
||||||
if st.phase == "over" and not over_slot.visible:
|
if st.phase == "over" and not over_slot.visible:
|
||||||
over_title.text = String(st.banner)
|
over_title.text = String(st.banner)
|
||||||
# Nochmal gibt es nur solo. Im Netz müsste der Server den Raum neu
|
# Nochmal: solo immer, im Netz nur für den Raumbesitzer - der Server
|
||||||
# aufsetzen; bis dahin geht es zurück ins Menü.
|
# setzt den Raum dann für alle neu auf.
|
||||||
over_again.visible = Net.room_code == Net.SOLO
|
over_again.visible = Net.room_code == Net.SOLO or Net.is_owner
|
||||||
over_slot.visible = true
|
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
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
|||||||
|
|
||||||
func _raum_bauen(code: String, cfg: Dictionary) -> void:
|
func _raum_bauen(code: String, cfg: Dictionary) -> void:
|
||||||
if raeume.has_node(code):
|
if raeume.has_node(code):
|
||||||
return
|
# Ein Nochmal ersetzt die alte Partie; alles andere ist ein Doppel.
|
||||||
|
if not cfg.get("rematch", false):
|
||||||
|
return
|
||||||
|
_raum_abbauen(code)
|
||||||
if menu != null:
|
if menu != null:
|
||||||
menu.queue_free()
|
menu.queue_free()
|
||||||
menu = null
|
menu = null
|
||||||
@@ -103,9 +106,12 @@ func _wiedereinstieg(code: String, peer: int, pname: String) -> void:
|
|||||||
Net.rejoin_annehmen(code, peer, pname, cfg)
|
Net.rejoin_annehmen(code, peer, pname, cfg)
|
||||||
|
|
||||||
|
|
||||||
|
## Sofort aus dem Baum, nicht erst am Frameende: ein Nochmal baut den Raum
|
||||||
|
## unter demselben Namen gleich wieder auf.
|
||||||
func _raum_abbauen(code: String) -> void:
|
func _raum_abbauen(code: String) -> void:
|
||||||
var raum := raeume.get_node_or_null(NodePath(code))
|
var raum := raeume.get_node_or_null(NodePath(code))
|
||||||
if raum != null:
|
if raum != null:
|
||||||
|
raeume.remove_child(raum)
|
||||||
raum.queue_free()
|
raum.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ var current := "main"
|
|||||||
|
|
||||||
# Einzelspieler
|
# Einzelspieler
|
||||||
var solo_bots: NumberRow
|
var solo_bots: NumberRow
|
||||||
|
var zurueck_btn: Button ## "Zurück in Raum XY" nach einem Abbruch
|
||||||
var solo_stufe: OptionButton
|
var solo_stufe: OptionButton
|
||||||
var lobby_stufe: OptionButton
|
var lobby_stufe: OptionButton
|
||||||
|
|
||||||
@@ -62,6 +63,10 @@ func _show(which: String) -> void:
|
|||||||
for key: String in screens:
|
for key: String in screens:
|
||||||
screens[key].visible = key == which
|
screens[key].visible = key == which
|
||||||
_sagen("")
|
_sagen("")
|
||||||
|
if which == "main" and zurueck_btn != null:
|
||||||
|
zurueck_btn.visible = not Net.letzter_raum.is_empty()
|
||||||
|
if zurueck_btn.visible:
|
||||||
|
zurueck_btn.text = "Zurück in Raum %s" % Net.letzter_raum.code
|
||||||
if which == "lobby":
|
if which == "lobby":
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
@@ -88,6 +93,12 @@ func _text(t: String, size: int, col := Color.WHITE) -> Label:
|
|||||||
return l
|
return l
|
||||||
|
|
||||||
|
|
||||||
|
func _on_wieder_rein() -> void:
|
||||||
|
_show("join")
|
||||||
|
_sagen("Verbinde mit Raum %s …" % Net.letzter_raum.get("code", ""))
|
||||||
|
Net.wieder_rein()
|
||||||
|
|
||||||
|
|
||||||
## Leicht, normal, schwer - stellt Bluff-Quote, Genau-Schwelle und Vorsicht
|
## Leicht, normal, schwer - stellt Bluff-Quote, Genau-Schwelle und Vorsicht
|
||||||
## der Bots ein.
|
## der Bots ein.
|
||||||
func _stufen_wahl() -> OptionButton:
|
func _stufen_wahl() -> OptionButton:
|
||||||
@@ -170,6 +181,9 @@ func _title(box: VBoxContainer) -> void:
|
|||||||
func _build_main() -> Control:
|
func _build_main() -> Control:
|
||||||
var box := _box(360)
|
var box := _box(360)
|
||||||
_title(box)
|
_title(box)
|
||||||
|
# Nach einem Verbindungsabbruch: derselbe Name, derselbe Code, ein Klick.
|
||||||
|
zurueck_btn = _button("", box, _on_wieder_rein)
|
||||||
|
zurueck_btn.visible = false
|
||||||
_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("Regeln", box, _show.bind("regeln"))
|
||||||
@@ -191,6 +205,8 @@ func _build_regeln() -> Control:
|
|||||||
["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."],
|
["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."],
|
["Ende", "Wer keine Würfel mehr hat, schaut zu. Der Letzte gewinnt."],
|
||||||
]
|
]
|
||||||
|
for b in BoosterDB.ALL:
|
||||||
|
absaetze.append([String(b.name), "%s Wappen: %s" % [b.desc, b.stark]])
|
||||||
for a: Array in absaetze:
|
for a: Array in absaetze:
|
||||||
var h := HBoxContainer.new()
|
var h := HBoxContainer.new()
|
||||||
h.add_theme_constant_override("separation", 12)
|
h.add_theme_constant_override("separation", 12)
|
||||||
@@ -206,7 +222,13 @@ func _build_regeln() -> Control:
|
|||||||
h.add_child(t)
|
h.add_child(t)
|
||||||
box.add_child(h)
|
box.add_child(h)
|
||||||
_button("Zurück", box, _show.bind("main"))
|
_button("Zurück", box, _show.bind("main"))
|
||||||
return box
|
# Mit den Münzen passt die Seite nicht mehr auf 720 Zeilen: scrollen.
|
||||||
|
var sc := ScrollContainer.new()
|
||||||
|
sc.custom_minimum_size = Vector2(720, 560)
|
||||||
|
sc.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
box.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
sc.add_child(box)
|
||||||
|
return sc
|
||||||
|
|
||||||
|
|
||||||
func _build_single() -> Control:
|
func _build_single() -> Control:
|
||||||
|
|||||||
212
scripts/muenzen.gd
Normal file
212
scripts/muenzen.gd
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Münzen: ziehen, werfen, Wirkung, Bot-Absicht, Topf. Arbeitet auf dem Spiel
|
||||||
|
## `g` - Zustand und Ablauf bleiben dort, hier steht nur, was Münzen tun.
|
||||||
|
## Kein class_name: den Klassencache baut nur der Editor, headless fehlt er.
|
||||||
|
|
||||||
|
var g: Node
|
||||||
|
|
||||||
|
|
||||||
|
func _init(spiel: Node) -> void:
|
||||||
|
g = spiel
|
||||||
|
|
||||||
|
## Wer einen Würfel abgibt, bekommt eine Münze. Das ist der einzige Nachschub.
|
||||||
|
func _zieh_muenze(seat: int) -> void:
|
||||||
|
var muenzen: Array = g.hand.get(seat, [])
|
||||||
|
if muenzen.size() >= BoosterDB.HANDLIMIT:
|
||||||
|
g._log("%s hat schon drei Münzen und bekommt keine." % g.seats[seat].name)
|
||||||
|
return
|
||||||
|
var id: String = BoosterDB.zieh(g.rng)
|
||||||
|
muenzen.append(id)
|
||||||
|
g.hand[seat] = muenzen
|
||||||
|
g._log("%s bekommt %s." % [g.seats[seat].name, BoosterDB.get_booster(id).name])
|
||||||
|
|
||||||
|
|
||||||
|
## Ausspielen heißt: Münze in den Topf werfen. Geht nur am eigenen Zug, bevor
|
||||||
|
## 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:
|
||||||
|
if seat < 0 or g.phase != "bid" or seat != g.turn or g._answered.has(seat):
|
||||||
|
return
|
||||||
|
if g._muenze_laeuft:
|
||||||
|
return
|
||||||
|
if BoosterDB.needs_bid(id) and g.bid.is_empty():
|
||||||
|
return
|
||||||
|
var muenzen: Array = g.hand.get(seat, [])
|
||||||
|
if not muenzen.has(id):
|
||||||
|
return
|
||||||
|
muenzen.erase(id)
|
||||||
|
g.hand[seat] = muenzen
|
||||||
|
g._muenze_laeuft = true
|
||||||
|
|
||||||
|
var stark: bool = g.rng.randf() < BoosterDB.WAPPEN_CHANCE
|
||||||
|
g._wurf_nr += 1
|
||||||
|
g.topf.append({"seat": seat, "id": id, "stark": stark, "nr": g._wurf_nr})
|
||||||
|
while g.topf.size() > BoosterDB.TOPF_MAX:
|
||||||
|
var weg: Dictionary = g.topf.pop_front()
|
||||||
|
g._log("%s fällt vom Tisch - der Topf ist voll." % BoosterDB.get_booster(String(weg.id)).name)
|
||||||
|
g._log("%s wirft %s - %s." % [g.seats[seat].name,
|
||||||
|
BoosterDB.get_booster(id).name, "Wappen" if stark else "Zahl"])
|
||||||
|
|
||||||
|
# Erst die Münze zeigen, dann wirken lassen. Andersherum sähe man die
|
||||||
|
# Folge vor der Ursache.
|
||||||
|
g._push_state()
|
||||||
|
await g._sleep(g.WURF_ZEIT)
|
||||||
|
if g._alive():
|
||||||
|
await _wirkung(seat, id, target, stark)
|
||||||
|
g._muenze_laeuft = false
|
||||||
|
if not g._alive():
|
||||||
|
return
|
||||||
|
g._snapshot_all()
|
||||||
|
g._push_state()
|
||||||
|
|
||||||
|
|
||||||
|
func _wirkung(seat: int, id: String, target: int, stark: bool) -> void:
|
||||||
|
var vals: Array = g.host_values[seat]
|
||||||
|
var ziel: int = clampi(target, 0, maxi(0, vals.size() - 1))
|
||||||
|
match id:
|
||||||
|
"peek":
|
||||||
|
var info := {}
|
||||||
|
for o in g._alive_seats():
|
||||||
|
if o != seat:
|
||||||
|
info[o] = _zufalls_paare(o, 2 if stark else 1)
|
||||||
|
g.peeked[seat] = info
|
||||||
|
"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: int = _count_face(int(g.bid.face))
|
||||||
|
var von: int = n if stark else maxi(0, n - g.rng.randi_range(0, 2))
|
||||||
|
g.gezaehlt[seat] = {int(g.bid.face): [von, von if stark else von + 2]}
|
||||||
|
"set_face":
|
||||||
|
for k: int in _ziel_und_zufall(seat, ziel, stark):
|
||||||
|
g._set_die(seat, k, int(g.bid.face))
|
||||||
|
"gain_die":
|
||||||
|
g.seats[seat].count += 1
|
||||||
|
g.table.set_dice_count(seat, g.seats[seat].count)
|
||||||
|
g.host_values[seat].append(1)
|
||||||
|
var neu: int = g.host_values[seat].size() - 1
|
||||||
|
await g._neu_werfen(seat, [neu])
|
||||||
|
if stark:
|
||||||
|
g._set_die(seat, neu, 6)
|
||||||
|
"bid_up":
|
||||||
|
g.bid.count = int(g.bid.count) + (2 if stark else 1)
|
||||||
|
g.last_bids[int(g.bid.seat)] = {"count": g.bid.count, "face": g.bid.face}
|
||||||
|
g.verlauf.append({"seat": int(g.bid.seat), "count": g.bid.count, "face": g.bid.face, "erhoeht": true})
|
||||||
|
"gag":
|
||||||
|
if stark:
|
||||||
|
for o in g._alive_seats():
|
||||||
|
if o != seat:
|
||||||
|
g.must_bid[o] = true
|
||||||
|
else:
|
||||||
|
g.must_bid[g._next_alive(seat)] = true
|
||||||
|
|
||||||
|
|
||||||
|
func _count_face(face: int) -> int:
|
||||||
|
var n := 0
|
||||||
|
for i in g._alive_seats():
|
||||||
|
for v in g.host_values[i]:
|
||||||
|
if v == face:
|
||||||
|
n += 1
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
## Bis zu `n` verschiedene Würfelplätze eines Sitzes, zufällig gezogen.
|
||||||
|
func _zufalls_idx(seat: int, n: int) -> Array:
|
||||||
|
var frei: Array = range(g.host_values[seat].size())
|
||||||
|
var out: Array = []
|
||||||
|
while out.size() < n and not frei.is_empty():
|
||||||
|
out.append(frei.pop_at(g.rng.randi_range(0, frei.size() - 1)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func _zufalls_paare(seat: int, n: int) -> Array:
|
||||||
|
var out: Array = []
|
||||||
|
for k: int in _zufalls_idx(seat, n):
|
||||||
|
out.append([k, g.host_values[seat][k]])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Der selbst gewählte Würfel, bei Wappen zusätzlich ein anderer zufälliger.
|
||||||
|
func _ziel_und_zufall(seat: int, ziel: int, stark: bool) -> Array:
|
||||||
|
var out: Array = [ziel]
|
||||||
|
if not stark:
|
||||||
|
return out
|
||||||
|
var frei: Array = []
|
||||||
|
for k in g.host_values[seat].size():
|
||||||
|
if k != ziel:
|
||||||
|
frei.append(k)
|
||||||
|
if not frei.is_empty():
|
||||||
|
out.append(frei[g.rng.randi_range(0, frei.size() - 1)])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Ein Bot wirft eine Münze, wenn sie zu seinem Vorhaben passt. `a` ist die
|
||||||
|
## geplante Handlung. Gibt zurück, ob geworfen wurde.
|
||||||
|
func _bot_muenze(seat: int, a: Dictionary) -> bool:
|
||||||
|
var muenzen: Array = g.hand.get(seat, [])
|
||||||
|
if muenzen.is_empty():
|
||||||
|
return false
|
||||||
|
var vals: Array = g.host_values[seat]
|
||||||
|
var unknown: int = g._total_dice() - vals.size()
|
||||||
|
var will_bieten: bool = a.type == "bid"
|
||||||
|
# Wie glaubwürdig ist das, was der Bot vorhat bzw. was auf dem Tisch liegt?
|
||||||
|
var eigenes: float = DiceAI.chance(vals, unknown, int(a.count), int(a.face)) if will_bieten else 0.0
|
||||||
|
var liegt: float = DiceAI.chance(vals, unknown, int(g.bid.count), int(g.bid.face)) if not g.bid.is_empty() else 0.0
|
||||||
|
|
||||||
|
var wahl := ""
|
||||||
|
var ziel := 0
|
||||||
|
# Wirklich hinten liegen: Nachschub ist der Weg zurück. Bei Gleichstand
|
||||||
|
# nicht - sonst wirft ihn zu Beginn jeder, und der Tisch wird nie leerer.
|
||||||
|
if muenzen.has("gain_die") and vals.size() < _groesste_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 g.bid.is_empty() and int(a.face) == int(g.bid.face) \
|
||||||
|
and muenzen.has("set_face") and vals.count(int(g.bid.face)) < vals.size():
|
||||||
|
wahl = "set_face"
|
||||||
|
ziel = DiceAI.target_for(vals, int(g.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 g.bid.is_empty():
|
||||||
|
wahl = id
|
||||||
|
break
|
||||||
|
if wahl == "":
|
||||||
|
return false
|
||||||
|
await _do_play(seat, wahl, ziel)
|
||||||
|
await g._sleep(0.6)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Die grösste Würfelzahl am Tisch - wer weniger hat, liegt hinten.
|
||||||
|
func _groesste_hand() -> int:
|
||||||
|
var k := 0
|
||||||
|
for i in g._alive_seats():
|
||||||
|
k = maxi(k, g.seats[i].count)
|
||||||
|
return k
|
||||||
|
|
||||||
|
|
||||||
|
## 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 g.topf.is_empty():
|
||||||
|
return
|
||||||
|
var h: Array = g.hand.get(winner, [])
|
||||||
|
var genommen := 0
|
||||||
|
g._topf_ziel = winner
|
||||||
|
while not g.topf.is_empty() and h.size() < BoosterDB.HANDLIMIT:
|
||||||
|
h.append(String(g.topf.pop_front().id))
|
||||||
|
genommen += 1
|
||||||
|
g.hand[winner] = h
|
||||||
|
g._log("%s nimmt %d Münze(n) aus dem Topf, %d bleiben." % [
|
||||||
|
g.seats[winner].name, genommen, g.topf.size()])
|
||||||
@@ -35,6 +35,9 @@ 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.
|
## Wunsch für eine neue Solopartie; das frisch geladene Menü löst ihn ein.
|
||||||
var rematch: Dictionary = {}
|
var rematch: Dictionary = {}
|
||||||
|
## Raum und Name nach einem Verbindungsabbruch - für "Zurück in Raum XY".
|
||||||
|
## Ein bewusstes Verlassen löscht es.
|
||||||
|
var letzter_raum: Dictionary = {}
|
||||||
|
|
||||||
# ── Server ───────────────────────────────────────────────────────────────────
|
# ── Server ───────────────────────────────────────────────────────────────────
|
||||||
var dedicated := false
|
var dedicated := false
|
||||||
@@ -100,6 +103,7 @@ func serve(port: int) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
func leave() -> void:
|
func leave() -> void:
|
||||||
|
letzter_raum = {}
|
||||||
if multiplayer.multiplayer_peer != null:
|
if multiplayer.multiplayer_peer != null:
|
||||||
multiplayer.multiplayer_peer.close()
|
multiplayer.multiplayer_peer.close()
|
||||||
multiplayer.multiplayer_peer = null
|
multiplayer.multiplayer_peer = null
|
||||||
@@ -131,6 +135,8 @@ func _on_connect_fail() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _on_server_disconnected() -> void:
|
func _on_server_disconnected() -> void:
|
||||||
|
if in_game and room_code != "" and room_code != SOLO:
|
||||||
|
letzter_raum = {"code": room_code, "name": player_name}
|
||||||
multiplayer.multiplayer_peer = null
|
multiplayer.multiplayer_peer = null
|
||||||
lobby.clear()
|
lobby.clear()
|
||||||
owner_id = 0
|
owner_id = 0
|
||||||
@@ -252,7 +258,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, stufe := 1) -> void:
|
func start_singleplayer(pname: String, ai_count: int, stufe := 1, dice := 5) -> void:
|
||||||
player_name = pname
|
player_name = pname
|
||||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||||
room_code = SOLO
|
room_code = SOLO
|
||||||
@@ -262,16 +268,51 @@ func start_singleplayer(pname: String, ai_count: int, stufe := 1) -> void:
|
|||||||
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})
|
"stufe": stufe, "dice": dice})
|
||||||
|
|
||||||
|
|
||||||
## Der Host bittet den Server um den Start.
|
## Nach einem Abbruch mit demselben Namen zurück in die laufende Partie.
|
||||||
func request_start(ai_count: int, stufe := 1) -> void:
|
func wieder_rein() -> void:
|
||||||
_req_start.rpc_id(1, ai_count, stufe)
|
var r := letzter_raum
|
||||||
|
letzter_raum = {}
|
||||||
|
join_room(String(r.code), String(r.name))
|
||||||
|
|
||||||
|
|
||||||
|
## Der Raumbesitzer will nach dem Ende noch eine Partie mit derselben Runde.
|
||||||
|
func request_rematch() -> void:
|
||||||
|
_req_rematch.rpc_id(1)
|
||||||
|
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _req_start(ai_count: int, stufe: int) -> void:
|
func _req_rematch() -> void:
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
var id := multiplayer.get_remote_sender_id()
|
||||||
|
var code: String = peer_raum.get(id, "")
|
||||||
|
var raum: Dictionary = raeume.get(code, {})
|
||||||
|
if raum.is_empty() or not raum.in_game or id != raum.owner:
|
||||||
|
return
|
||||||
|
var order: Array = raum.lobby.keys()
|
||||||
|
order.sort()
|
||||||
|
if order.size() + int(raum.get("ai", 0)) < 2:
|
||||||
|
return
|
||||||
|
raum.bestaetigt = {}
|
||||||
|
var cfg := {"peers": order, "names": raum.lobby.duplicate(), "ai": int(raum.get("ai", 0)),
|
||||||
|
"stufe": int(raum.get("stufe", 1)), "dice": int(raum.get("dice", 5)), "rematch": true}
|
||||||
|
room_closed.emit(code) # alter Spielknoten weg, auf dem Server
|
||||||
|
_start_zustellen(code, cfg)
|
||||||
|
game_started.emit(code, cfg)
|
||||||
|
print("Raum %s spielt noch einmal." % code)
|
||||||
|
|
||||||
|
|
||||||
|
## Der Host bittet den Server um den Start.
|
||||||
|
## `dice` unter 5 nutzen nur die Tests, für kurze Partien.
|
||||||
|
func request_start(ai_count: int, stufe := 1, dice := 5) -> void:
|
||||||
|
_req_start.rpc_id(1, ai_count, stufe, dice)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _req_start(ai_count: int, stufe: int, dice: 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()
|
||||||
@@ -284,8 +325,11 @@ func _req_start(ai_count: int, stufe: int) -> void:
|
|||||||
if order.size() + ai_count < 2:
|
if order.size() + ai_count < 2:
|
||||||
return
|
return
|
||||||
raum.in_game = true
|
raum.in_game = true
|
||||||
|
raum.ai = ai_count
|
||||||
|
raum.stufe = clampi(stufe, 0, 2)
|
||||||
|
raum.dice = clampi(dice, 1, 5)
|
||||||
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)}
|
"stufe": clampi(stufe, 0, 2), "dice": clampi(dice, 1, 5)}
|
||||||
_start_zustellen(code, cfg)
|
_start_zustellen(code, cfg)
|
||||||
game_started.emit(code, cfg)
|
game_started.emit(code, cfg)
|
||||||
|
|
||||||
@@ -323,7 +367,7 @@ func _start_erhalten() -> void:
|
|||||||
@rpc("authority", "reliable")
|
@rpc("authority", "reliable")
|
||||||
func _begin(code: String, cfg: Dictionary) -> void:
|
func _begin(code: String, cfg: Dictionary) -> void:
|
||||||
_start_erhalten.rpc_id(1)
|
_start_erhalten.rpc_id(1)
|
||||||
if in_game and room_code == code:
|
if in_game and room_code == code and not cfg.get("rematch", false):
|
||||||
return # Nachzügler-Paket, die Partie läuft hier schon
|
return # Nachzügler-Paket, die Partie läuft hier schon
|
||||||
in_game = true
|
in_game = true
|
||||||
room_code = code
|
room_code = code
|
||||||
|
|||||||
@@ -442,6 +442,27 @@ func throw_dice(seat: int, rng: RandomNumberGenerator) -> void:
|
|||||||
arr[i].throw_from(zone.global_transform * off, rng)
|
arr[i].throw_from(zone.global_transform * off, rng)
|
||||||
|
|
||||||
|
|
||||||
|
## Fangnetz: ein Würfel, der über die Bande oder durch den Filz gefallen ist,
|
||||||
|
## fiele sonst ewig - und das Würfeln käme nie zur Ruhe. Er kommt zurück in
|
||||||
|
## seine Zone und fällt dort noch einmal. Gibt zurück, ob einer gefangen wurde.
|
||||||
|
func fange_verirrte(seat: int) -> bool:
|
||||||
|
var zone: Node3D = zones[seat]
|
||||||
|
var gefangen := false
|
||||||
|
for d in dice[seat]:
|
||||||
|
if d.global_position.y > TABLE_H - 0.3 and d.global_position.y < TABLE_H + 3.0:
|
||||||
|
continue
|
||||||
|
var ang := randf() * TAU
|
||||||
|
var off := Vector3(cos(ang), 0.0, sin(ang)) * ZONE_RAD * 0.3 + Vector3(0, 0.35, 0)
|
||||||
|
d.freeze = false
|
||||||
|
d.sleeping = false
|
||||||
|
d.linear_velocity = Vector3.ZERO
|
||||||
|
d.angular_velocity = Vector3.ZERO
|
||||||
|
var t := Transform3D(Basis(), zone.global_transform * off)
|
||||||
|
d.place(t)
|
||||||
|
gefangen = true
|
||||||
|
return gefangen
|
||||||
|
|
||||||
|
|
||||||
## Legt die Würfel ordentlich nebeneinander (für Clients/Reveal). Ab `von`
|
## Legt die Würfel ordentlich nebeneinander (für Clients/Reveal). Ab `von`
|
||||||
## bleiben die davorliegenden unangetastet.
|
## bleiben die davorliegenden unangetastet.
|
||||||
func _layout_idle(seat: int, von := 0) -> void:
|
func _layout_idle(seat: int, von := 0) -> void:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Tests
|
# Tests
|
||||||
|
|
||||||
make test
|
make test # alle Prüfungen, etwa 30 Sekunden
|
||||||
|
make sim # Bot gegen Bot, je Stufe 20 Partien: Runden und Siegquoten
|
||||||
|
|
||||||
Läuft alles headless: Logik, Nochmal, Bot-Partie (Stufe schwer, 8×), und
|
Läuft alles headless: Logik, Nochmal, Bot-Partie (Stufe schwer, 8×), und
|
||||||
eine echte Mehrspieler-Partie mit Server, Host-Client und Gast-Client - der
|
eine echte Mehrspieler-Partie mit Server, Host-Client und Gast-Client - der
|
||||||
@@ -11,6 +12,10 @@ Fenster.
|
|||||||
|
|
||||||
## Fallen
|
## Fallen
|
||||||
|
|
||||||
|
* **Kein `Engine.time_scale`.** Er staucht auch den Physikschritt; ab etwa 6×
|
||||||
|
tunneln die Würfel durch den Filz und fallen ewig. Schnell wird es über
|
||||||
|
`game.tempo = 0` - das streicht nur die Wartezeiten des Ablaufs.
|
||||||
|
|
||||||
* **Wurzelname.** Der Spielknoten muss auf Server und Client denselben Pfad
|
* **Wurzelname.** Der Spielknoten muss auf Server und Client denselben Pfad
|
||||||
haben (`/root/Main/Raeume/<CODE>/…`). Daran hängt jeder RPC. Die
|
haben (`/root/Main/Raeume/<CODE>/…`). Daran hängt jeder RPC. Die
|
||||||
Mehrspieler-Tests laden deshalb die echte Hauptszene als aktuelle Szene und
|
Mehrspieler-Tests laden deshalb die echte Hauptszene als aktuelle Szene und
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
extends Node
|
extends Node
|
||||||
## Gemeinsame Helfer für die Testszenen. Jede Testszene erbt hiervon.
|
## Gemeinsame Helfer für die Testszenen. Jede Testszene erbt hiervon.
|
||||||
|
##
|
||||||
|
## Kein Engine.time_scale: der staucht auch den Physikschritt. Ab etwa 6x
|
||||||
|
## tunneln die Würfel durch den Filz und fallen ewig - die Partie hängt im
|
||||||
|
## Würfeln. Schnell wird es über `game.tempo = 0`, das nur die Wartezeiten
|
||||||
|
## des Ablaufs streicht.
|
||||||
|
|
||||||
var fehler := 0
|
var fehler := 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
extends "res://tests/basis.gd"
|
extends "res://tests/basis.gd"
|
||||||
## Alle Plätze sind Bots, achtfacher Zeitraffer. Läuft die Partie fehlerfrei
|
## Alle Plätze sind Bots, ohne Wartezeiten, drei Plätze mit je drei Würfeln:
|
||||||
## bis zum Ende? Stufe über "-- 0|1|2".
|
## eine ganze Partie in Sekunden. Stufe über "-- 0|1|2".
|
||||||
|
|
||||||
var game: Node
|
var game: Node
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
Engine.time_scale = 12.0
|
|
||||||
add_child(preload("res://scenes/main.tscn").instantiate())
|
add_child(preload("res://scenes/main.tscn").instantiate())
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
var args := OS.get_cmdline_user_args()
|
var args := OS.get_cmdline_user_args()
|
||||||
Net.start_singleplayer("Tester", 3, int(args[0]) if args.size() > 0 else 1)
|
Net.start_singleplayer("Tester", 2, int(args[0]) if args.size() > 0 else 1, 3)
|
||||||
_lauf()
|
_lauf()
|
||||||
|
|
||||||
|
|
||||||
func _lauf() -> void:
|
func _lauf() -> void:
|
||||||
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 20.0)
|
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 20.0)
|
||||||
|
game.tempo = 0.0
|
||||||
game.seats[game.local_seat].ai = true
|
game.seats[game.local_seat].ai = true
|
||||||
|
# Volle Vorräte: so wirft jeder Bot sicher, und der Topf kommt in Bewegung.
|
||||||
|
for i in game.seats.size():
|
||||||
|
game.hand[i] = ["peek", "gag", "count", "bid_up"]
|
||||||
var t := 0.0
|
var t := 0.0
|
||||||
var gesehen := {}
|
var gesehen := {}
|
||||||
var zaehler := {"Genau": 0, "Topf": 0, " wirft ": 0, "vom Tisch": 0}
|
var zaehler := {"Genau": 0, "Topf": 0, " wirft ": 0, "vom Tisch": 0}
|
||||||
var runden := 0
|
var runden := 0
|
||||||
var letzte := ""
|
var letzte := ""
|
||||||
var naechste_meldung := 500.0
|
var naechste_meldung := 500.0
|
||||||
while game.phase != "over" and t < 6000.0:
|
while game.phase != "over" and t < 600.0:
|
||||||
await get_tree().create_timer(0.2).timeout
|
await get_tree().create_timer(0.2).timeout
|
||||||
t += 0.2
|
t += 0.2
|
||||||
if game.phase != letzte:
|
if game.phase != letzte:
|
||||||
@@ -40,6 +43,20 @@ func _lauf() -> void:
|
|||||||
for k in zaehler:
|
for k in zaehler:
|
||||||
if k in z:
|
if k in z:
|
||||||
zaehler[k] += 1
|
zaehler[k] += 1
|
||||||
|
if game.phase != "over":
|
||||||
|
# Wo hängt es? Alles ausgeben, was den Ablauf treibt.
|
||||||
|
print(" STAND phase=%s turn=%d bid=%s must_bid=%s muenze_laeuft=%s answered=%s" % [
|
||||||
|
game.phase, game.turn, game.bid, game.must_bid, game._muenze_laeuft, game._answered.keys()])
|
||||||
|
for i in game.seats.size():
|
||||||
|
print(" Platz %d %s count=%d alive=%s ai=%s hand=%s" % [i, game.seats[i].name,
|
||||||
|
game.seats[i].count, game.seats[i].alive, game.seats[i].ai, game.hand.get(i, [])])
|
||||||
|
for z in game.log_lines:
|
||||||
|
print(" LOG " + z)
|
||||||
|
print(" paused=%s stream=%s" % [get_tree().paused, game._stream])
|
||||||
|
for i in game.seats.size():
|
||||||
|
for d in game.table.dice_of(i):
|
||||||
|
print(" Wuerfel Platz %d: pos=%s sleeping=%s freeze=%s settled=%s flat=%.2f" % [
|
||||||
|
i, d.global_position, d.sleeping, d.freeze, d.is_settled(), d.flatness()])
|
||||||
_pruef("Partie endet (%s, Stufe %d, %d Runden, %.0fs Spielzeit)" % [game.banner, DiceAI.stufe, runden, t], game.phase == "over")
|
_pruef("Partie endet (%s, Stufe %d, %d Runden, %.0fs Spielzeit)" % [game.banner, DiceAI.stufe, runden, t], game.phase == "over")
|
||||||
_pruef("Münzen wurden geworfen (%d)" % zaehler[" wirft "], zaehler[" wirft "] > 0)
|
_pruef("Münzen wurden geworfen (%d)" % zaehler[" wirft "], zaehler[" wirft "] > 0)
|
||||||
_pruef("Topf wurde vergeben (%d)" % zaehler["Topf"], zaehler["Topf"] > 0)
|
_pruef("Topf wurde vergeben (%d)" % zaehler["Topf"], zaehler["Topf"] > 0)
|
||||||
|
|||||||
62
tests/genau.gd
Normal file
62
tests/genau.gd
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
extends "res://tests/basis.gd"
|
||||||
|
## Genau und Aufdecken in Etappen. Eigene Szene, damit der Ablauf innerhalb
|
||||||
|
## der 30 s Zugzeit bleibt - danach spielt der Host den Platz automatisch.
|
||||||
|
|
||||||
|
var game: Node
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
add_child(preload("res://scenes/main.tscn").instantiate())
|
||||||
|
await get_tree().process_frame
|
||||||
|
Net.start_singleplayer("Tester", 3, 1)
|
||||||
|
_lauf()
|
||||||
|
|
||||||
|
|
||||||
|
func _menschen() -> void:
|
||||||
|
for i in [1, 2, 3]:
|
||||||
|
game.seats[i].ai = false
|
||||||
|
|
||||||
|
|
||||||
|
func _lauf() -> void:
|
||||||
|
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 20.0)
|
||||||
|
game.tempo = 0.3
|
||||||
|
await _warte(func(): return game.phase == "bid", 40.0)
|
||||||
|
var me: int = game.local_seat
|
||||||
|
_menschen()
|
||||||
|
game.must_bid.clear()
|
||||||
|
# --- Genau-Treffer: Ansager verliert, Rufer bekommt einen
|
||||||
|
game.must_bid.clear()
|
||||||
|
var n3: int = game._count_face(3)
|
||||||
|
if n3 == 0:
|
||||||
|
game._set_die(me, 0, 3)
|
||||||
|
n3 = 1
|
||||||
|
var vorher: Array = []
|
||||||
|
for i in 4:
|
||||||
|
vorher.append(int(game.seats[i].count))
|
||||||
|
game._do_bid(me, n3, 3)
|
||||||
|
await _warte(func(): return game.turn == 1, 5.0)
|
||||||
|
game._do_exact(1)
|
||||||
|
await _warte(func(): return game.phase == "reveal", 5.0)
|
||||||
|
_pruef("Etappe 1: noch nicht gezählt", int(game.reveal_total) == -1)
|
||||||
|
await _warte(func(): return int(game.reveal_total) >= 0, 3.0)
|
||||||
|
_pruef("Etappe 2: gezählt", int(game.reveal_total) == n3)
|
||||||
|
await _warte(func(): return game.banner.begins_with("Genau!"), 3.0)
|
||||||
|
await _warte(func(): return game.phase != "reveal", 8.0)
|
||||||
|
_pruef("Genau: Ansager -1, Rufer +1, Rest gleich",
|
||||||
|
int(game.seats[me].count) == vorher[0] - 1 and int(game.seats[1].count) == vorher[1] + 1
|
||||||
|
and int(game.seats[2].count) == vorher[2] and int(game.seats[3].count) == vorher[3])
|
||||||
|
_pruef("Rufer beginnt", game.starter == 1)
|
||||||
|
|
||||||
|
# --- Genau daneben
|
||||||
|
# Erst wenn die neue Ansagerunde wirklich läuft: Platz 1 ist dran und
|
||||||
|
# noch unbeantwortet. Ein "turn == 1" allein gilt schon beim Würfeln.
|
||||||
|
await _warte(func(): return game.phase == "bid" and game.turn == 1 and game._answered.is_empty(), 30.0)
|
||||||
|
var n5: int = game._count_face(5)
|
||||||
|
game._do_bid(1, mini(n5 + 2, game._total_dice()), 5)
|
||||||
|
await _warte(func(): return game.turn == 2 and game._answered.is_empty(), 5.0)
|
||||||
|
var c2: int = game.seats[2].count
|
||||||
|
game._do_exact(2)
|
||||||
|
await _warte(func(): return game.banner.begins_with("Daneben"), 8.0)
|
||||||
|
await _warte(func(): return game.phase != "reveal", 8.0)
|
||||||
|
_pruef("Genau daneben: nur der Rufer verliert", int(game.seats[2].count) == c2 - 1)
|
||||||
|
_fertig()
|
||||||
6
tests/genau.tscn
Normal file
6
tests/genau.tscn
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/genau.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="Test" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
extends "res://tests/basis.gd"
|
extends "res://tests/basis.gd"
|
||||||
## Solopartie headless: Münzen, Genau, Topf, Zählen, Stufe, Tipp-Schranke.
|
## Solopartie headless: Münzen, Topf, Zählen, Stufe, Tipp-Schranke.
|
||||||
## Die Bots werden zu "Menschen" ohne Peer gemacht - dann wartet der Host
|
## Die Bots werden zu "Menschen" ohne Peer gemacht - dann wartet der Host
|
||||||
## auf unsere Eingaben, und der Ablauf ist steuerbar.
|
## auf unsere Eingaben, und der Ablauf ist steuerbar. Alles, was den eigenen
|
||||||
|
## Zug braucht, kommt zuerst: nach 30 s Spielzeit spielt der Host den Platz
|
||||||
|
## sonst automatisch. Genau und Aufdecken prüft tests/genau.gd.
|
||||||
|
|
||||||
var game: Node
|
var game: Node
|
||||||
|
|
||||||
@@ -20,6 +22,8 @@ func _menschen() -> void:
|
|||||||
|
|
||||||
func _lauf() -> void:
|
func _lauf() -> void:
|
||||||
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 20.0)
|
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 20.0)
|
||||||
|
# Nicht null: die Etappen beim Aufdecken sollen noch einzeln sichtbar sein.
|
||||||
|
game.tempo = 0.3
|
||||||
await _warte(func(): return game.phase == "bid", 40.0)
|
await _warte(func(): return game.phase == "bid", 40.0)
|
||||||
var me: int = game.local_seat
|
var me: int = game.local_seat
|
||||||
var hud: Node = game.hud
|
var hud: Node = game.hud
|
||||||
@@ -35,6 +39,20 @@ func _lauf() -> void:
|
|||||||
await game._do_play(me, "count", 0)
|
await game._do_play(me, "count", 0)
|
||||||
_pruef("Zählen ohne Ansage abgelehnt", game.hand[me].has("count"))
|
_pruef("Zählen ohne Ansage abgelehnt", game.hand[me].has("count"))
|
||||||
|
|
||||||
|
# --- Topf: Deckel und Übertrag
|
||||||
|
game.topf.clear()
|
||||||
|
game.hand[me] = ["gag", "gag", "gag", "gag", "gag", "gag", "gag"]
|
||||||
|
for i in 7:
|
||||||
|
await game._do_play(me, "gag", 0)
|
||||||
|
_pruef("Topf höchstens %d" % BoosterDB.TOPF_MAX, game.topf.size() == BoosterDB.TOPF_MAX)
|
||||||
|
_pruef("Rausfallen steht im Protokoll", game.log_lines.any(func(z): return "vom Tisch" in z))
|
||||||
|
game.hand[2] = ["peek", "peek"]
|
||||||
|
game._topf_an(2)
|
||||||
|
_pruef("Gewinner nimmt bis zum Limit, Rest bleibt",
|
||||||
|
game.hand[2].size() == BoosterDB.HANDLIMIT and game.topf.size() == BoosterDB.TOPF_MAX - 2)
|
||||||
|
game.topf.clear()
|
||||||
|
game.must_bid.clear()
|
||||||
|
|
||||||
# --- Wirkungen beider Seiten
|
# --- Wirkungen beider Seiten
|
||||||
game.bid = {"seat": 1, "count": 1, "face": 4}
|
game.bid = {"seat": 1, "count": 1, "face": 4}
|
||||||
var n4: int = game._count_face(4)
|
var n4: int = game._count_face(4)
|
||||||
@@ -57,19 +75,15 @@ func _lauf() -> void:
|
|||||||
game.bid = {}
|
game.bid = {}
|
||||||
game.must_bid.clear()
|
game.must_bid.clear()
|
||||||
|
|
||||||
# --- Topf: Deckel und Übertrag
|
# --- Fangnetz: ein Würfel unter dem Tisch kommt zurück und kommt zur Ruhe
|
||||||
game.topf.clear()
|
var d0 = game.table.dice_of(me)[0]
|
||||||
game.hand[me] = ["gag", "gag", "gag", "gag", "gag", "gag", "gag"]
|
d0.freeze = false
|
||||||
for i in 7:
|
d0.place(Transform3D(Basis(), Vector3(0, -50.0, 0)))
|
||||||
await game._do_play(me, "gag", 0)
|
_pruef("Fangnetz holt den Würfel zurück", game.table.fange_verirrte(me) and d0.global_position.y > 0.0)
|
||||||
_pruef("Topf höchstens %d" % BoosterDB.TOPF_MAX, game.topf.size() == BoosterDB.TOPF_MAX)
|
d0.place(Transform3D(Basis(), Vector3(0, -50.0, 0)))
|
||||||
_pruef("Rausfallen steht im Protokoll", game.log_lines.any(func(z): return "vom Tisch" in z))
|
await game._neu_werfen(me, [0])
|
||||||
game.hand[2] = ["peek", "peek"]
|
_pruef("Würfeln endet trotz verirrtem Würfel, Wert gelesen",
|
||||||
game._topf_an(2)
|
d0.global_position.y > 0.5 and game.host_values[me][0] >= 1 and game.host_values[me][0] <= 6)
|
||||||
_pruef("Gewinner nimmt bis zum Limit, Rest bleibt",
|
|
||||||
game.hand[2].size() == BoosterDB.HANDLIMIT and game.topf.size() == BoosterDB.TOPF_MAX - 2)
|
|
||||||
game.topf.clear()
|
|
||||||
game.must_bid.clear()
|
|
||||||
|
|
||||||
# --- Protokoll und Tipp-Schranke im HUD
|
# --- Protokoll und Tipp-Schranke im HUD
|
||||||
game._push_state()
|
game._push_state()
|
||||||
@@ -88,39 +102,4 @@ func _lauf() -> void:
|
|||||||
hud._touch = false
|
hud._touch = false
|
||||||
await get_tree().create_timer(2.5).timeout
|
await get_tree().create_timer(2.5).timeout
|
||||||
|
|
||||||
# --- Genau-Treffer: Ansager verliert, Rufer bekommt einen
|
|
||||||
game.must_bid.clear()
|
|
||||||
var n3: int = game._count_face(3)
|
|
||||||
if n3 == 0:
|
|
||||||
game._set_die(me, 0, 3)
|
|
||||||
n3 = 1
|
|
||||||
var vorher: Array = []
|
|
||||||
for i in 4:
|
|
||||||
vorher.append(int(game.seats[i].count))
|
|
||||||
game._do_bid(me, n3, 3)
|
|
||||||
await _warte(func(): return game.turn == 1, 5.0)
|
|
||||||
game._do_exact(1)
|
|
||||||
await _warte(func(): return game.phase == "reveal", 5.0)
|
|
||||||
_pruef("Etappe 1: noch nicht gezählt", int(game.reveal_total) == -1)
|
|
||||||
await _warte(func(): return int(game.reveal_total) >= 0, 3.0)
|
|
||||||
_pruef("Etappe 2: gezählt", int(game.reveal_total) == n3)
|
|
||||||
await _warte(func(): return game.banner.begins_with("Genau!"), 3.0)
|
|
||||||
await _warte(func(): return game.phase != "reveal", 8.0)
|
|
||||||
_pruef("Genau: Ansager -1, Rufer +1, Rest gleich",
|
|
||||||
int(game.seats[me].count) == vorher[0] - 1 and int(game.seats[1].count) == vorher[1] + 1
|
|
||||||
and int(game.seats[2].count) == vorher[2] and int(game.seats[3].count) == vorher[3])
|
|
||||||
_pruef("Rufer beginnt", game.starter == 1)
|
|
||||||
|
|
||||||
# --- Genau daneben
|
|
||||||
await _warte(func(): return game.phase == "bid", 20.0)
|
|
||||||
_menschen()
|
|
||||||
await _warte(func(): return game.turn == 1, 5.0)
|
|
||||||
var n5: int = game._count_face(5)
|
|
||||||
game._do_bid(1, mini(n5 + 2, game._total_dice()), 5)
|
|
||||||
await _warte(func(): return game.turn == 2, 5.0)
|
|
||||||
var c2: int = game.seats[2].count
|
|
||||||
game._do_exact(2)
|
|
||||||
await _warte(func(): return game.banner.begins_with("Daneben"), 8.0)
|
|
||||||
await _warte(func(): return game.phase != "reveal", 8.0)
|
|
||||||
_pruef("Genau daneben: nur der Rufer verliert", int(game.seats[2].count) == c2 - 1)
|
|
||||||
_fertig()
|
_fertig()
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ var _code_datei := ""
|
|||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
Engine.time_scale = 8.0
|
|
||||||
_rng.randomize()
|
_rng.randomize()
|
||||||
var args := OS.get_cmdline_user_args()
|
var args := OS.get_cmdline_user_args()
|
||||||
rolle = args[0]
|
rolle = args[0]
|
||||||
@@ -33,7 +32,7 @@ func _lauf() -> void:
|
|||||||
f.store_string(Net.room_code)
|
f.store_string(Net.room_code)
|
||||||
f.close()
|
f.close()
|
||||||
await _warte(func(): return Net.lobby.size() >= 2, 80.0)
|
await _warte(func(): return Net.lobby.size() >= 2, 80.0)
|
||||||
Net.request_start(1, 1)
|
Net.request_start(1, 1, 2)
|
||||||
else:
|
else:
|
||||||
await _beitreten()
|
await _beitreten()
|
||||||
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 120.0)
|
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 120.0)
|
||||||
@@ -46,9 +45,9 @@ func _lauf() -> void:
|
|||||||
|
|
||||||
var t := 0.0
|
var t := 0.0
|
||||||
var gewartet := 0.0
|
var gewartet := 0.0
|
||||||
while t < 4400.0 and is_instance_valid(game) and game.phase != "over":
|
while t < 1400.0 and is_instance_valid(game) and game.phase != "over":
|
||||||
await get_tree().create_timer(0.2).timeout
|
await get_tree().create_timer(0.1).timeout
|
||||||
t += 0.2
|
t += 0.1
|
||||||
if not is_instance_valid(game):
|
if not is_instance_valid(game):
|
||||||
break
|
break
|
||||||
if game.phase != _letzte_phase:
|
if game.phase != _letzte_phase:
|
||||||
@@ -63,14 +62,14 @@ func _lauf() -> void:
|
|||||||
if game.phase != "bid" or game.turn != me or not game.seats[me].alive:
|
if game.phase != "bid" or game.turn != me or not game.seats[me].alive:
|
||||||
gewartet = 0.0
|
gewartet = 0.0
|
||||||
continue
|
continue
|
||||||
gewartet += 0.2
|
gewartet += 0.1
|
||||||
if gewartet < 0.6:
|
if gewartet < 0.2:
|
||||||
continue
|
continue
|
||||||
var hand: Array = game.hand.get(me, [])
|
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:
|
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)
|
game._on_booster_chosen(hand[0], 0)
|
||||||
wuerfe += 1
|
wuerfe += 1
|
||||||
await get_tree().create_timer(2.5).timeout
|
await get_tree().create_timer(0.4).timeout
|
||||||
if not is_instance_valid(game) or game.turn != me or game.phase != "bid":
|
if not is_instance_valid(game) or game.turn != me or game.phase != "bid":
|
||||||
continue
|
continue
|
||||||
var total := 0
|
var total := 0
|
||||||
@@ -88,11 +87,22 @@ func _lauf() -> void:
|
|||||||
game._on_exact_pressed()
|
game._on_exact_pressed()
|
||||||
genau += 1
|
genau += 1
|
||||||
zuege += 1
|
zuege += 1
|
||||||
gewartet = -2.0
|
gewartet = -1.5
|
||||||
_pruef("%s: Partie endet über RPC (Runden=%d Züge=%d Würfe=%d Genau=%d)" % [rolle, runden, zuege, wuerfe, genau],
|
_pruef("%s: Partie endet über RPC (Runden=%d Züge=%d Würfe=%d Genau=%d)" % [rolle, runden, zuege, wuerfe, genau],
|
||||||
is_instance_valid(game) and game.phase == "over")
|
is_instance_valid(game) and game.phase == "over")
|
||||||
if rolle == "join":
|
if rolle == "join":
|
||||||
_pruef("join: Wiedereinstieg hat geklappt", wieder_da)
|
_pruef("join: Wiedereinstieg hat geklappt", wieder_da)
|
||||||
|
# Nochmal: der Besitzer bittet darum, beide bekommen eine frische Partie.
|
||||||
|
var alt: Node = game
|
||||||
|
await get_tree().create_timer(0.5).timeout
|
||||||
|
if Net.is_owner:
|
||||||
|
Net.request_rematch()
|
||||||
|
var neu := await _warte(func(): game = _find(get_tree().root, "Game"); return game != null and game != alt, 20.0)
|
||||||
|
var laeuft := false
|
||||||
|
if neu:
|
||||||
|
laeuft = await _warte(func(): return game.phase == "bid" or game.phase == "roll", 20.0)
|
||||||
|
_pruef("%s: Nochmal im Netz bringt eine neue Partie (Platz %d)" % [rolle, game.local_seat if neu else -1],
|
||||||
|
neu and laeuft and game.local_seat >= 0)
|
||||||
if is_instance_valid(game):
|
if is_instance_valid(game):
|
||||||
print("%s: '%s'" % [rolle, game.banner])
|
print("%s: '%s'" % [rolle, game.banner])
|
||||||
for i in game.seats.size():
|
for i in game.seats.size():
|
||||||
@@ -106,15 +116,19 @@ func _beitreten() -> void:
|
|||||||
Net.join_room(FileAccess.get_file_as_string(_code_datei).strip_edges(), "Gast")
|
Net.join_room(FileAccess.get_file_as_string(_code_datei).strip_edges(), "Gast")
|
||||||
|
|
||||||
|
|
||||||
## Verbindung kappen, frisches Menü, mit demselben Namen wieder beitreten.
|
## Verbindung reisst ab (kein bewusstes Verlassen), frisches Menü, dann der
|
||||||
|
## Weg des Knopfs "Zurück in Raum XY".
|
||||||
func _wiederkommen() -> void:
|
func _wiederkommen() -> void:
|
||||||
var alter_platz: int = game.local_seat
|
var alter_platz: int = game.local_seat
|
||||||
var alte_zuege := zuege
|
var alte_zuege := zuege
|
||||||
Net.leave()
|
var code := Net.room_code
|
||||||
get_tree().change_scene_to_file("res://scenes/main.tscn")
|
multiplayer.multiplayer_peer.close()
|
||||||
await get_tree().create_timer(1.5).timeout
|
if Net.in_game:
|
||||||
|
Net._on_server_disconnected() # der Verlust-Handler, wie bei echtem Abbruch
|
||||||
|
await get_tree().create_timer(1.0).timeout
|
||||||
|
_pruef("join: Raum nach Abbruch gemerkt", Net.letzter_raum.get("code", "") == code)
|
||||||
game = null
|
game = null
|
||||||
await _beitreten()
|
Net.wieder_rein()
|
||||||
var ok := await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 60.0)
|
var ok := await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 60.0)
|
||||||
if not ok:
|
if not ok:
|
||||||
_pruef("join: nach Rückkehr kommt ein Spiel", false)
|
_pruef("join: nach Rückkehr kommt ein Spiel", false)
|
||||||
|
|||||||
@@ -6,27 +6,34 @@ var game: Node
|
|||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
Engine.time_scale = 8.0
|
|
||||||
var t := 0.0
|
var t := 0.0
|
||||||
var runden := 0
|
var runden := 0
|
||||||
var letzte := ""
|
var letzte := ""
|
||||||
while t < 4500.0:
|
var zuletzt := "" # letzter gesehener Stand - die Clients gehen sofort,
|
||||||
await get_tree().create_timer(0.5).timeout
|
# dann räumt der Server den Raum ab, bevor wir gucken
|
||||||
t += 0.5
|
while t < 1500.0:
|
||||||
|
await get_tree().create_timer(0.2).timeout
|
||||||
|
t += 0.2
|
||||||
if game == null:
|
if game == null:
|
||||||
game = _find(get_tree().root, "Game")
|
game = _find(get_tree().root, "Game")
|
||||||
|
if game != null:
|
||||||
|
game.tempo = 0.0
|
||||||
continue
|
continue
|
||||||
if not is_instance_valid(game) or game.phase == "over":
|
if not is_instance_valid(game):
|
||||||
break
|
break
|
||||||
if game.phase != letzte:
|
if game.phase != letzte:
|
||||||
letzte = game.phase
|
letzte = game.phase
|
||||||
if letzte == "roll":
|
if letzte == "roll":
|
||||||
runden += 1
|
runden += 1
|
||||||
if is_instance_valid(game):
|
var staende: Array = game.seats.map(func(x): return "%s=%d" % [x.name, x.count])
|
||||||
print("SERVER: Ende %s '%s', Runden=%d" % [game.phase, game.banner, runden])
|
zuletzt = "%s '%s' Runden=%d %s" % [game.phase, game.banner, runden, staende]
|
||||||
for i in game.seats.size():
|
if game.phase == "over":
|
||||||
print("SERVER: Platz %d %s count=%d alive=%s" % [i, game.seats[i].name, game.seats[i].count, game.seats[i].alive])
|
break
|
||||||
var zurueck: bool = game.log_lines.any(func(z): return "ist zurück" in z)
|
print("SERVER: Ende %s" % zuletzt)
|
||||||
print("SERVER: Rückkehr im Protokoll=%s" % zurueck)
|
var alt: Node = game
|
||||||
await get_tree().create_timer(80.0).timeout
|
var neu := await _warte(func(): game = _find(get_tree().root, "Game"); return game != null and game != alt, 15.0)
|
||||||
|
if neu:
|
||||||
|
game.tempo = 0.0
|
||||||
|
print("SERVER: Nochmal-Partie aufgebaut=%s" % neu)
|
||||||
|
await get_tree().create_timer(15.0).timeout
|
||||||
get_tree().quit()
|
get_tree().quit()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ func _ready() -> void:
|
|||||||
await get_tree().create_timer(0.5).timeout
|
await get_tree().create_timer(0.5).timeout
|
||||||
Net.start_singleplayer("Tester", 2, 0)
|
Net.start_singleplayer("Tester", 2, 0)
|
||||||
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 20.0)
|
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 20.0)
|
||||||
|
game.tempo = 0.0
|
||||||
await _warte(func(): return game.phase == "bid", 40.0)
|
await _warte(func(): return game.phase == "bid", 40.0)
|
||||||
_pruef("Erste Partie läuft, Stufe leicht", DiceAI.stufe == 0 and game.seats.size() == 3)
|
_pruef("Erste Partie läuft, Stufe leicht", DiceAI.stufe == 0 and game.seats.size() == 3)
|
||||||
game._on_rematch()
|
game._on_rematch()
|
||||||
|
|||||||
87
tests/run.sh
87
tests/run.sh
@@ -1,51 +1,66 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Alle headless Tests. Aufruf: make test (oder: bash tests/run.sh)
|
# Alle headless Tests, parallel. Aufruf: make test (oder: bash tests/run.sh)
|
||||||
# Der Mehrspieler-Test braucht einen freien Port; 8000 ist auf Rechnern mit
|
# Ein Test fällt sofort, wenn ein Skriptfehler im Log auftaucht - nicht erst
|
||||||
# laufendem Entwicklungsserver oft belegt, deshalb 8765.
|
# nach dem Timeout. Der Mehrspieler-Test braucht einen freien Port; 8000 ist
|
||||||
|
# auf Rechnern mit laufendem Entwicklungsserver oft belegt, deshalb 8765.
|
||||||
set -u
|
set -u
|
||||||
GODOT="${GODOT:-$HOME/bin/godot}"
|
GODOT="${GODOT:-$HOME/bin/godot}"
|
||||||
PORT="${PORT:-8765}"
|
PORT="${PORT:-8765}"
|
||||||
|
# Ein Rest vom letzten Lauf oder ein fremder Dienst darf den Test nicht
|
||||||
|
# still auf einen falschen Server schicken: freien Port suchen.
|
||||||
|
while ss -ltn 2>/dev/null | grep -q ":$PORT "; do PORT=$((PORT + 1)); done
|
||||||
TMP="$(mktemp -d)"
|
TMP="$(mktemp -d)"
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
ergebnis=0
|
start=$(date +%s)
|
||||||
|
|
||||||
lauf() { # name, timeout, argumente...
|
declare -A PID
|
||||||
|
starte() { # name, timeout, argumente... -> Hintergrund
|
||||||
local name="$1"; local zeit="$2"; shift 2
|
local name="$1"; local zeit="$2"; shift 2
|
||||||
echo "=== $name"
|
timeout "$zeit" "$GODOT" --headless --path . "res://tests/${name%%:*}.tscn" -- "$@" > "$TMP/$name.log" 2>&1 &
|
||||||
timeout "$zeit" "$GODOT" --headless --path . "res://tests/$name.tscn" -- "$@" > "$TMP/$name.log" 2>&1
|
PID[$name]=$!
|
||||||
local rc=$?
|
|
||||||
grep -E "^(OK |FEHL| |TEST:)" "$TMP/$name.log"
|
|
||||||
local fehler; fehler=$(grep -c "SCRIPT ERROR" "$TMP/$name.log")
|
|
||||||
[ "$fehler" -gt 0 ] && { echo " $fehler Skriptfehler im Log: $TMP/$name.log"; rc=1; }
|
|
||||||
[ $rc -ne 0 ] && ergebnis=1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lauf logik 200
|
starte logik 90
|
||||||
lauf nochmal 120
|
starte genau 90
|
||||||
lauf bots 900 2
|
starte nochmal 60
|
||||||
|
starte bots 90 2
|
||||||
echo "=== mehrspieler"
|
|
||||||
rm -f "$TMP/code.txt"
|
rm -f "$TMP/code.txt"
|
||||||
timeout 700 "$GODOT" --headless --path . res://tests/mp_start.tscn -- server "$PORT" > "$TMP/srv.log" 2>&1 &
|
starte mp_start:srv 200 server "$PORT"
|
||||||
SRV=$!
|
|
||||||
sleep 3
|
|
||||||
PIRATE_DICE_SERVER="ws://127.0.0.1:$PORT" timeout 680 "$GODOT" --headless --path . res://tests/mp_start.tscn -- host "$TMP/code.txt" > "$TMP/host.log" 2>&1 &
|
|
||||||
H=$!
|
|
||||||
sleep 2
|
sleep 2
|
||||||
PIRATE_DICE_SERVER="ws://127.0.0.1:$PORT" timeout 680 "$GODOT" --headless --path . res://tests/mp_start.tscn -- join "$TMP/code.txt" > "$TMP/join.log" 2>&1 &
|
PIRATE_DICE_SERVER="ws://127.0.0.1:$PORT" starte mp_start:host 190 host "$TMP/code.txt"
|
||||||
J=$!
|
sleep 1
|
||||||
wait $H; rc_h=$?
|
PIRATE_DICE_SERVER="ws://127.0.0.1:$PORT" starte mp_start:join 190 join "$TMP/code.txt"
|
||||||
wait $J; rc_j=$?
|
|
||||||
# Der Server beendet sich nach dem Ende selbst. Ihn abzuschiessen würde
|
# Warten, aber bei Skriptfehlern sofort abbrechen.
|
||||||
# seine letzten Zeilen verlieren - Ausgabe in eine Datei ist gepuffert.
|
while :; do
|
||||||
for _ in $(seq 1 40); do kill -0 $SRV 2>/dev/null || break; sleep 1; done
|
offen=0
|
||||||
kill $SRV 2>/dev/null; wait $SRV 2>/dev/null
|
for name in "${!PID[@]}"; do
|
||||||
grep -E "^(OK |FEHL|SERVER:|TEST:)" "$TMP/host.log" "$TMP/join.log" "$TMP/srv.log" | sed 's|^.*/||'
|
pid=${PID[$name]}
|
||||||
pfad=$(cat "$TMP/host.log" "$TMP/join.log" "$TMP/srv.log" | grep -c "Failed to get path")
|
kill -0 "$pid" 2>/dev/null || continue
|
||||||
fehler=$(cat "$TMP/host.log" "$TMP/join.log" "$TMP/srv.log" | grep -c "SCRIPT ERROR")
|
if grep -q "SCRIPT ERROR" "$TMP/$name.log" 2>/dev/null; then
|
||||||
echo " RPC-Pfadfehler=$pfad Skriptfehler=$fehler (Logs: $TMP)"
|
kill "$pid" 2>/dev/null
|
||||||
[ $rc_h -ne 0 ] || [ $rc_j -ne 0 ] || [ "$pfad" -gt 0 ] || [ "$fehler" -gt 0 ] && ergebnis=1
|
continue
|
||||||
|
fi
|
||||||
|
offen=$((offen + 1))
|
||||||
|
done
|
||||||
|
[ "$offen" -eq 0 ] && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
wait 2>/dev/null
|
||||||
|
|
||||||
|
ergebnis=0
|
||||||
|
for name in logik genau nochmal bots mp_start:host mp_start:join mp_start:srv; do
|
||||||
|
log="$TMP/$name.log"
|
||||||
|
echo "=== $name"
|
||||||
|
grep -E "^(OK |FEHL|SERVER:| |TEST:)" "$log"
|
||||||
|
fehler=$(grep -c "SCRIPT ERROR" "$log")
|
||||||
|
[ "$fehler" -gt 0 ] && { echo " $fehler Skriptfehler:"; grep -A2 "SCRIPT ERROR" "$log" | head -6; ergebnis=1; }
|
||||||
|
if [ "$name" != "mp_start:srv" ] && ! grep -q "TEST: fertig, Fehler = 0" "$log"; then ergebnis=1; fi
|
||||||
|
done
|
||||||
|
pfad=$(cat "$TMP"/mp_start:*.log | grep -c "Failed to get path")
|
||||||
|
[ "$pfad" -gt 0 ] && { echo " RPC-Pfadfehler: $pfad"; ergebnis=1; }
|
||||||
|
|
||||||
echo
|
echo
|
||||||
[ $ergebnis -eq 0 ] && echo "ALLE TESTS GRÜN" || echo "TESTS ROT (Logs: $TMP)"
|
echo "Dauer: $(( $(date +%s) - start ))s Logs: $TMP"
|
||||||
|
[ $ergebnis -eq 0 ] && echo "ALLE TESTS GRÜN" || echo "TESTS ROT"
|
||||||
exit $ergebnis
|
exit $ergebnis
|
||||||
|
|||||||
71
tests/sim.gd
Normal file
71
tests/sim.gd
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
extends "res://tests/basis.gd"
|
||||||
|
## Bot gegen Bot, viele Partien hintereinander: Rundenzahl und Siegquoten je
|
||||||
|
## Platz. Kein Test, ein Messgerät für die Regler in ai.gd.
|
||||||
|
## Aufruf: make sim (oder einzeln: -- <stufe> <partien>)
|
||||||
|
|
||||||
|
var game: Node
|
||||||
|
var stufe := 1
|
||||||
|
var partien := 20
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var args := OS.get_cmdline_user_args()
|
||||||
|
if args.size() > 0:
|
||||||
|
stufe = int(args[0])
|
||||||
|
if args.size() > 1:
|
||||||
|
partien = int(args[1])
|
||||||
|
add_child(preload("res://scenes/main.tscn").instantiate())
|
||||||
|
await get_tree().process_frame
|
||||||
|
_lauf()
|
||||||
|
|
||||||
|
|
||||||
|
func _lauf() -> void:
|
||||||
|
var runden_summe := 0
|
||||||
|
var siege := {}
|
||||||
|
var genau := 0
|
||||||
|
var wuerfe := 0
|
||||||
|
var laengste := 0
|
||||||
|
for n in partien:
|
||||||
|
Net.start_singleplayer("Tester", 2, stufe, 3)
|
||||||
|
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null and game.phase != "lobby", 20.0)
|
||||||
|
game.tempo = 0.0
|
||||||
|
game.seats[game.local_seat].ai = true
|
||||||
|
var runden := 0
|
||||||
|
var letzte := ""
|
||||||
|
var gesehen := {}
|
||||||
|
var t := 0.0
|
||||||
|
while game.phase != "over" and t < 300.0:
|
||||||
|
await get_tree().create_timer(0.1).timeout
|
||||||
|
t += 0.1
|
||||||
|
if game.phase != letzte:
|
||||||
|
letzte = game.phase
|
||||||
|
if letzte == "roll":
|
||||||
|
runden += 1
|
||||||
|
for z in game.log_lines:
|
||||||
|
if gesehen.has(z):
|
||||||
|
continue
|
||||||
|
gesehen[z] = true
|
||||||
|
if "Genau" in z:
|
||||||
|
genau += 1
|
||||||
|
if " wirft " in z:
|
||||||
|
wuerfe += 1
|
||||||
|
runden_summe += runden
|
||||||
|
laengste = maxi(laengste, runden)
|
||||||
|
var sieger := ""
|
||||||
|
for s in game.seats:
|
||||||
|
if s.alive:
|
||||||
|
sieger = String(s.name)
|
||||||
|
siege[sieger] = int(siege.get(sieger, 0)) + 1
|
||||||
|
# Aufräumen für die nächste Partie: wie "Nochmal", nur ohne Menü.
|
||||||
|
game._quitting = true
|
||||||
|
Net.leave()
|
||||||
|
var raum: Node = game.get_parent().get_parent().get_parent()
|
||||||
|
raum.get_parent().remove_child(raum)
|
||||||
|
raum.queue_free()
|
||||||
|
game = null
|
||||||
|
await get_tree().process_frame
|
||||||
|
print("SIM Stufe %d: %d Partien, Ø %.1f Runden (längste %d), Ø %.1f Würfe, Ø %.1f Genau-Rufe" % [
|
||||||
|
stufe, partien, float(runden_summe) / partien, laengste, float(wuerfe) / partien, float(genau) / partien])
|
||||||
|
for k in siege:
|
||||||
|
print("SIM Stufe %d: %s gewinnt %d×" % [stufe, k, siege[k]])
|
||||||
|
get_tree().quit()
|
||||||
6
tests/sim.tscn
Normal file
6
tests/sim.tscn
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/sim.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="Test" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
Reference in New Issue
Block a user