diff --git a/TRAINING.md b/TRAINING.md index afb8e955..c3b4eced 100644 --- a/TRAINING.md +++ b/TRAINING.md @@ -139,6 +139,14 @@ appends to `training/eval_history.json` — the long-term progress record. Evaluate each new candidate against the previous promoted bot and a fixed early reference to see absolute progress over time. +The evaluator launches Godot with the explicit headless display driver, +Compatibility renderer, dummy audio driver, and a temporary writable log path +so evaluation is reproducible on machines where the default renderer or +`user://` log location is unavailable. A two-seed smoke check on 2026-09-01 +completed 10 paired episodes per seed for the current Stage 6 export versus +`hard.json` (both runs finished 3–5 with 2 draws); this is only a runtime +smoke and is not promotion evidence for the Stage 6 gate. + If a model was trained with the locomotion mask on (curriculum stages 1, 2, and 5 — see below), pass `--grounded-a`/`--grounded-b` for whichever side it's on. The eval otherwise runs `AIShipController` fully unmasked regardless of how a diff --git a/training/evaluate.py b/training/evaluate.py index 786fdbe4..4619bcd2 100644 --- a/training/evaluate.py +++ b/training/evaluate.py @@ -17,6 +17,7 @@ import json import os import pathlib import subprocess +import tempfile TRAINING_DIR = pathlib.Path(__file__).resolve().parent GAME_DIR = TRAINING_DIR.parent / "Game" @@ -37,6 +38,14 @@ def run_half( ) -> dict: cmd = [ godot_bin, + "--display-driver", + "headless", + "--rendering-method", + "gl_compatibility", + "--audio-driver", + "Dummy", + "--log-file", + str(pathlib.Path(tempfile.gettempdir()) / "cosmic-clash-evaluate-godot.log"), "--path", str(GAME_DIR), TRAINING_SCENE, diff --git a/training/test_evaluate.py b/training/test_evaluate.py index ae6736a0..76452acb 100644 --- a/training/test_evaluate.py +++ b/training/test_evaluate.py @@ -55,6 +55,14 @@ class EvaluatePairTests(unittest.TestCase): command = run_process.call_args.args[0] self.assertIn("--eval_team_size=2", command) + @patch("evaluate.subprocess.run") + def test_run_uses_portable_headless_renderer_and_writable_log(self, run_process) -> None: + run_process.return_value.stdout = 'EVAL_RESULT {"episodes": 2, "goals_a": 1, "goals_b": 0, "draws": 1}\n' + evaluate.run_half("godot", "a", "b", 2, 16, 9) + command = run_process.call_args.args[0] + for option in ("--display-driver", "headless", "--rendering-method", "gl_compatibility", "--audio-driver", "Dummy", "--log-file"): + self.assertIn(option, command) + @patch("evaluate.run_half") def test_2v2_evaluation_preserves_side_swap_and_team_size(self, run_half) -> None: run_half.side_effect = [