extends Node # CI regression driver (task 3.6): a headless server plus two headless # --test-bot clients (AIShipController, not a human) playing a real match, # for a longer/unattended CI smoke pass. Not part of tests/test_runner.tscn # — needs real ENet peers and real physics, same reason as # networked_match_smoke.gd. Run: # # godot --headless --path Game res://tests/networked_match_ci.tscn -- --role=host # godot --headless --path Game res://tests/networked_match_ci.tscn -- --role=client-bot --test-bot # (run the client-bot line twice, for two bots — --test-bot itself is # read by networked_match.gd directly from the same command line) # # task 3.6's original acceptance text also names "p95/p99 prediction error" # and "snap count" — both Phase 4 concepts (client-side prediction and its # hard-snap threshold don't exist until then). Asserting on data that # doesn't exist yet would be fabricated, so this checks what's actually # meaningful at Phase 3: snapshot throughput, and cross-peer score # agreement — forced via a deterministic server-side goal (same # ball-into-the-goal trick used to verify task 2.4's goal-reset-ordering # fix), since two low-skill bots scoring naturally within a short CI run # isn't reliable enough to gate on. "Clean stderr" is the external # invocation's job (grep the captured output, same as every other smoke # test in this project) — a GDScript process can't observe its own # engine-level ERROR prints or another process's stderr. const PORT := 7820 const RUN_SECONDS := 8.0 var _role := "" var _players_joined := 0 func _ready() -> void: for arg in OS.get_cmdline_user_args(): if arg.begins_with("--role="): _role = arg.substr("--role=".length()) match _role: "host": var err := NetworkManager.host(PORT) if err != OK: print("SMOKE FAIL: host() failed: %s" % error_string(err)) get_tree().quit(1) return print("SMOKE: hosting on port %d, waiting for 2 players ..." % PORT) MatchNet.player_joined.connect(_on_host_player_joined) "client-bot": MatchNet.local_player_name = "CIBot" var err := NetworkManager.join("127.0.0.1", PORT) if err != OK: print("SMOKE FAIL: join() failed: %s" % error_string(err)) get_tree().quit(1) return print("SMOKE: joining as a test bot ...") MatchNet.welcomed.connect(_on_client_welcomed) _: print("SMOKE FAIL: missing or unrecognised --role= (expected host|client-bot)") get_tree().quit(1) return func _process(_delta: float) -> void: NetworkManager.poll() func _physics_process(_delta: float) -> void: NetworkManager.poll() func _on_host_player_joined(_peer_id: int, _name: String) -> void: _players_joined += 1 if _players_joined < 2: return MatchNet.player_joined.disconnect(_on_host_player_joined) print("SMOKE: host loading networked_match.tscn (2 players joined) ...") get_tree().change_scene_to_file.call_deferred("res://scenes/networked_match.tscn") var hooks := preload("res://tests/networked_match_test_hooks.gd").new() get_tree().root.add_child.call_deferred(hooks) hooks.run_ci_host_check.call_deferred(RUN_SECONDS) func _on_client_welcomed() -> void: MatchNet.welcomed.disconnect(_on_client_welcomed) print("SMOKE: client-bot loading networked_match.tscn ...") get_tree().change_scene_to_file.call_deferred("res://scenes/networked_match.tscn") var hooks := preload("res://tests/networked_match_test_hooks.gd").new() get_tree().root.add_child.call_deferred(hooks) hooks.run_ci_client_check.call_deferred(RUN_SECONDS)