This commit is contained in:
team3
2026-09-16 15:04:02 +02:00
parent 3be22351a3
commit b9d6b115c0
23 changed files with 795 additions and 425 deletions

View File

@@ -1,6 +1,7 @@
# 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
eine echte Mehrspieler-Partie mit Server, Host-Client und Gast-Client - der
@@ -11,6 +12,10 @@ Fenster.
## 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
haben (`/root/Main/Raeume/<CODE>/…`). Daran hängt jeder RPC. Die
Mehrspieler-Tests laden deshalb die echte Hauptszene als aktuelle Szene und

View File

@@ -1,5 +1,10 @@
extends Node
## 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

View File

@@ -1,29 +1,32 @@
extends "res://tests/basis.gd"
## Alle Plätze sind Bots, achtfacher Zeitraffer. Läuft die Partie fehlerfrei
## bis zum Ende? Stufe über "-- 0|1|2".
## Alle Plätze sind Bots, ohne Wartezeiten, drei Plätze mit je drei Würfeln:
## eine ganze Partie in Sekunden. Stufe über "-- 0|1|2".
var game: Node
func _ready() -> void:
Engine.time_scale = 12.0
add_child(preload("res://scenes/main.tscn").instantiate())
await get_tree().process_frame
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()
func _lauf() -> void:
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
# 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 gesehen := {}
var zaehler := {"Genau": 0, "Topf": 0, " wirft ": 0, "vom Tisch": 0}
var runden := 0
var letzte := ""
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
t += 0.2
if game.phase != letzte:
@@ -40,6 +43,20 @@ func _lauf() -> void:
for k in zaehler:
if k in z:
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("Münzen wurden geworfen (%d)" % zaehler[" wirft "], zaehler[" wirft "] > 0)
_pruef("Topf wurde vergeben (%d)" % zaehler["Topf"], zaehler["Topf"] > 0)

62
tests/genau.gd Normal file
View 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
View 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")

View File

@@ -1,7 +1,9 @@
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
## 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
@@ -20,6 +22,8 @@ func _menschen() -> void:
func _lauf() -> void:
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)
var me: int = game.local_seat
var hud: Node = game.hud
@@ -35,6 +39,20 @@ func _lauf() -> void:
await game._do_play(me, "count", 0)
_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
game.bid = {"seat": 1, "count": 1, "face": 4}
var n4: int = game._count_face(4)
@@ -57,19 +75,15 @@ func _lauf() -> void:
game.bid = {}
game.must_bid.clear()
# --- 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()
# --- Fangnetz: ein Würfel unter dem Tisch kommt zurück und kommt zur Ruhe
var d0 = game.table.dice_of(me)[0]
d0.freeze = false
d0.place(Transform3D(Basis(), Vector3(0, -50.0, 0)))
_pruef("Fangnetz holt den Würfel zurück", game.table.fange_verirrte(me) and d0.global_position.y > 0.0)
d0.place(Transform3D(Basis(), Vector3(0, -50.0, 0)))
await game._neu_werfen(me, [0])
_pruef("Würfeln endet trotz verirrtem Würfel, Wert gelesen",
d0.global_position.y > 0.5 and game.host_values[me][0] >= 1 and game.host_values[me][0] <= 6)
# --- Protokoll und Tipp-Schranke im HUD
game._push_state()
@@ -88,39 +102,4 @@ func _lauf() -> void:
hud._touch = false
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()

View File

@@ -16,7 +16,6 @@ var _code_datei := ""
func _ready() -> void:
Engine.time_scale = 8.0
_rng.randomize()
var args := OS.get_cmdline_user_args()
rolle = args[0]
@@ -33,7 +32,7 @@ func _lauf() -> void:
f.store_string(Net.room_code)
f.close()
await _warte(func(): return Net.lobby.size() >= 2, 80.0)
Net.request_start(1, 1)
Net.request_start(1, 1, 2)
else:
await _beitreten()
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 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
while t < 1400.0 and is_instance_valid(game) and game.phase != "over":
await get_tree().create_timer(0.1).timeout
t += 0.1
if not is_instance_valid(game):
break
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:
gewartet = 0.0
continue
gewartet += 0.2
if gewartet < 0.6:
gewartet += 0.1
if gewartet < 0.2:
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
await get_tree().create_timer(0.4).timeout
if not is_instance_valid(game) or game.turn != me or game.phase != "bid":
continue
var total := 0
@@ -88,11 +87,22 @@ func _lauf() -> void:
game._on_exact_pressed()
genau += 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],
is_instance_valid(game) and game.phase == "over")
if rolle == "join":
_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):
print("%s: '%s'" % [rolle, game.banner])
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")
## 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:
var alter_platz: int = game.local_seat
var alte_zuege := zuege
Net.leave()
get_tree().change_scene_to_file("res://scenes/main.tscn")
await get_tree().create_timer(1.5).timeout
var code := Net.room_code
multiplayer.multiplayer_peer.close()
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
await _beitreten()
Net.wieder_rein()
var ok := await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 60.0)
if not ok:
_pruef("join: nach Rückkehr kommt ein Spiel", false)

View File

@@ -6,27 +6,34 @@ var game: Node
func _ready() -> void:
Engine.time_scale = 8.0
var t := 0.0
var runden := 0
var letzte := ""
while t < 4500.0:
await get_tree().create_timer(0.5).timeout
t += 0.5
var zuletzt := "" # letzter gesehener Stand - die Clients gehen sofort,
# dann räumt der Server den Raum ab, bevor wir gucken
while t < 1500.0:
await get_tree().create_timer(0.2).timeout
t += 0.2
if game == null:
game = _find(get_tree().root, "Game")
if game != null:
game.tempo = 0.0
continue
if not is_instance_valid(game) or game.phase == "over":
if not is_instance_valid(game):
break
if game.phase != letzte:
letzte = game.phase
if letzte == "roll":
runden += 1
if is_instance_valid(game):
print("SERVER: Ende %s '%s', Runden=%d" % [game.phase, game.banner, 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])
var zurueck: bool = game.log_lines.any(func(z): return "ist zurück" in z)
print("SERVER: Rückkehr im Protokoll=%s" % zurueck)
await get_tree().create_timer(80.0).timeout
var staende: Array = game.seats.map(func(x): return "%s=%d" % [x.name, x.count])
zuletzt = "%s '%s' Runden=%d %s" % [game.phase, game.banner, runden, staende]
if game.phase == "over":
break
print("SERVER: Ende %s" % zuletzt)
var alt: Node = game
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()

View File

@@ -9,6 +9,7 @@ func _ready() -> void:
await get_tree().create_timer(0.5).timeout
Net.start_singleplayer("Tester", 2, 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)
_pruef("Erste Partie läuft, Stufe leicht", DiceAI.stufe == 0 and game.seats.size() == 3)
game._on_rematch()

View File

@@ -1,51 +1,66 @@
#!/usr/bin/env bash
# Alle headless Tests. Aufruf: make test (oder: bash tests/run.sh)
# Der Mehrspieler-Test braucht einen freien Port; 8000 ist auf Rechnern mit
# laufendem Entwicklungsserver oft belegt, deshalb 8765.
# Alle headless Tests, parallel. Aufruf: make test (oder: bash tests/run.sh)
# Ein Test fällt sofort, wenn ein Skriptfehler im Log auftaucht - nicht erst
# nach dem Timeout. Der Mehrspieler-Test braucht einen freien Port; 8000 ist
# auf Rechnern mit laufendem Entwicklungsserver oft belegt, deshalb 8765.
set -u
GODOT="${GODOT:-$HOME/bin/godot}"
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)"
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
echo "=== $name"
timeout "$zeit" "$GODOT" --headless --path . "res://tests/$name.tscn" -- "$@" > "$TMP/$name.log" 2>&1
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
timeout "$zeit" "$GODOT" --headless --path . "res://tests/${name%%:*}.tscn" -- "$@" > "$TMP/$name.log" 2>&1 &
PID[$name]=$!
}
lauf logik 200
lauf nochmal 120
lauf bots 900 2
echo "=== mehrspieler"
starte logik 90
starte genau 90
starte nochmal 60
starte bots 90 2
rm -f "$TMP/code.txt"
timeout 700 "$GODOT" --headless --path . res://tests/mp_start.tscn -- server "$PORT" > "$TMP/srv.log" 2>&1 &
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=$!
starte mp_start:srv 200 server "$PORT"
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 &
J=$!
wait $H; rc_h=$?
wait $J; rc_j=$?
# Der Server beendet sich nach dem Ende selbst. Ihn abzuschiessen würde
# seine letzten Zeilen verlieren - Ausgabe in eine Datei ist gepuffert.
for _ in $(seq 1 40); do kill -0 $SRV 2>/dev/null || break; sleep 1; done
kill $SRV 2>/dev/null; wait $SRV 2>/dev/null
grep -E "^(OK |FEHL|SERVER:|TEST:)" "$TMP/host.log" "$TMP/join.log" "$TMP/srv.log" | sed 's|^.*/||'
pfad=$(cat "$TMP/host.log" "$TMP/join.log" "$TMP/srv.log" | grep -c "Failed to get path")
fehler=$(cat "$TMP/host.log" "$TMP/join.log" "$TMP/srv.log" | grep -c "SCRIPT ERROR")
echo " RPC-Pfadfehler=$pfad Skriptfehler=$fehler (Logs: $TMP)"
[ $rc_h -ne 0 ] || [ $rc_j -ne 0 ] || [ "$pfad" -gt 0 ] || [ "$fehler" -gt 0 ] && ergebnis=1
PIRATE_DICE_SERVER="ws://127.0.0.1:$PORT" starte mp_start:host 190 host "$TMP/code.txt"
sleep 1
PIRATE_DICE_SERVER="ws://127.0.0.1:$PORT" starte mp_start:join 190 join "$TMP/code.txt"
# Warten, aber bei Skriptfehlern sofort abbrechen.
while :; do
offen=0
for name in "${!PID[@]}"; do
pid=${PID[$name]}
kill -0 "$pid" 2>/dev/null || continue
if grep -q "SCRIPT ERROR" "$TMP/$name.log" 2>/dev/null; then
kill "$pid" 2>/dev/null
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
[ $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

71
tests/sim.gd Normal file
View 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
View 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")