saumwacht-godot/tools/luneraria/net_rounds.py
Martin Stickelbroeck c15533831a M4: Spiel läuft vollständig über die autoritative Sitzung; M4 pausiert
Das echte Spiel umging die Sitzung bisher bei Bereitschaft, Startaufstellung
und Rückgängig, ließ Bots den Gast-Sitz planen und den Gast selbst simulieren.
Jetzt rechnet nur der Host; Gäste zeigen den Kampf aus Momentaufnahmen.
Dazu Protokollversion, Wiedereinstieg über den Anzeigenamen, verbindliches
Bereit, Entwurfs-Privatsphäre und Bot-Planung einmal pro Runde.

Geprüft: test_lune_net 43/43, net_rounds.py 12/12 (5 Runden, Wiedereinstieg,
gleiche Prüfsummen), Regeln 91/91, M2 73/73, verify_m1 38/38, verify_m2 14/14.
Offen und eingefroren: OpenGL-Modus, nativer Windows-11-Test, Demo-Neubau.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:00:08 +02:00

91 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Mehr-Runden-Loopback mit zwei bzw. drei Godot-Prozessen (Host, Gast, wiederkehrender Gast).
Ablauf: Der Host spielt --rounds Runden. Gast A spielt mit und trennt die Verbindung im
Kampf der Runde --drop-round. Danach startet Gast B mit demselben Anzeigenamen und muss
denselben Sitz zurückerhalten. Nach jeder Abrechnung vergleicht das Skript die
Prüfsumme des öffentlichen Zustands von Host und Gast.
Das ist ein lokaler Test über 127.0.0.1, keine Internetpartie auf sechs Rechnern.
Aufruf: python3 tools/luneraria/net_rounds.py [--rounds 5] [--drop-round 2] [--godot godot]
"""
import argparse
import json
import re
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
LOG_DIR = ROOT / "docs" / "luneraria" / "logs" / "m4"
ROUND_RE = re.compile(r"NET_ROUND role=(\w+) round=(\d+) phase=(\d+) sum=(\S+)")
COMBAT_RE = re.compile(r"NET_CLIENT_COMBAT round=(\d+) frames=(\d+) local_steps=(-?\d+)")
SETUP_RE = re.compile(r"NET_SETUP seat=(\d+)")
def launch(godot, port, *extra):
cmd = [godot, "--headless", "--path", str(ROOT), "res://tools/luneraria/net_pair.tscn", "--",
f"--port={port}", *extra]
return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--godot", default="godot")
parser.add_argument("--rounds", type=int, default=5)
parser.add_argument("--drop-round", type=int, default=2)
parser.add_argument("--port", type=int, default=24581)
args = parser.parse_args()
LOG_DIR.mkdir(parents=True, exist_ok=True)
host = launch(args.godot, args.port, "--role=host", f"--rounds={args.rounds}")
time.sleep(2.0)
guest_a = launch(args.godot, args.port, "--role=client", "--name=Gast", f"--rounds={args.rounds}",
f"--drop-round={args.drop_round}")
out_a, _ = guest_a.communicate(timeout=300)
guest_b = launch(args.godot, args.port, "--role=client", "--name=Gast", f"--rounds={args.rounds}")
out_b, _ = guest_b.communicate(timeout=300)
out_host, _ = host.communicate(timeout=300)
for name, text in (("host", out_host), ("gast-a", out_a), ("gast-b", out_b)):
(LOG_DIR / f"net-rounds-{name}.log").write_text(text)
def sums(text, role):
return {int(m.group(2)): m.group(4) for m in ROUND_RE.finditer(text) if m.group(1) == role}
host_sums = sums(out_host, "host")
guest_sums = {**sums(out_a, "client"), **sums(out_b, "client")}
compared = sorted(set(host_sums) & set(guest_sums))
mismatches = [r for r in compared if host_sums[r] != guest_sums[r]]
combats = [{"round": int(m.group(1)), "frames": int(m.group(2)), "local_steps": int(m.group(3))}
for text in (out_a, out_b) for m in COMBAT_RE.finditer(text)]
seats_a = [int(s) for s in SETUP_RE.findall(out_a)]
seats_b = [int(s) for s in SETUP_RE.findall(out_b)]
checks = {
"host_ok": host.returncode == 0,
"guest_a_ok": guest_a.returncode == 0 and "NET_CLIENT_DROP" in out_a,
"guest_b_ok": guest_b.returncode == 0 and "NET_CLIENT_DONE" in out_b,
"spoof_rejected": "ACCEPTED-BAD" not in out_a + out_b and "NET_SPOOF REJECTED" in out_a,
"rounds_compared": len(compared) >= args.rounds - 1,
"checksums_equal": not mismatches and bool(compared),
"client_never_simulates": bool(combats) and all(c["local_steps"] == 0 for c in combats),
"client_receives_frames": bool(combats) and all(c["frames"] > 0 for c in combats),
"rejoin_same_seat": bool(seats_a) and bool(seats_b) and seats_b[0] == seats_a[0],
"bot_took_over": "Slot wird vom Bot übernommen" in out_host,
"rejoin_announced": "kehrt zurück auf" in out_host,
"no_script_errors": all("SCRIPT ERROR" not in t for t in (out_host, out_a, out_b)),
}
report = {"rounds": args.rounds, "drop_round": args.drop_round, "compared_rounds": compared,
"mismatches": mismatches, "host_sums": host_sums, "guest_sums": guest_sums,
"client_combats": combats, "seats": {"gast_a": seats_a, "gast_b": seats_b}, "checks": checks}
(LOG_DIR / "net-rounds.json").write_text(json.dumps(report, indent=2, ensure_ascii=False))
failed = [k for k, v in checks.items() if not v]
print(json.dumps(checks, indent=2))
print("NET_ROUNDS_%s: %d/%d" % ("PASS" if not failed else "FAIL", len(checks) - len(failed), len(checks)))
return 0 if not failed else 1
if __name__ == "__main__":
sys.exit(main())