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>
91 lines
4.5 KiB
Python
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())
|