diff --git a/Assets/Tests/EditMode/Simulation/NoFloatInSimulationTests.cs b/Assets/Tests/EditMode/Simulation/NoFloatInSimulationTests.cs index 407dfa8..97e415b 100644 --- a/Assets/Tests/EditMode/Simulation/NoFloatInSimulationTests.cs +++ b/Assets/Tests/EditMode/Simulation/NoFloatInSimulationTests.cs @@ -12,9 +12,13 @@ namespace Nova.Simulation.Tests /// Determinism source guard (EditMode lane). Sprint hard rule 3 and /// docs/tech/SimulationCore.md section 9: the authoritative simulation is /// Q16.16 fixed point, so no IEEE-754 arithmetic may enter - /// Assets/_Project/Scripts/Simulation/** or Core/**. A single float - /// multiply that rounds differently on two CPUs desynchronises lockstep, - /// and no unit test catches it — the sources have to be checked directly. + /// Assets/_Project/Scripts/Simulation/**, Core/**, AI/** or AI.Data/**. + /// The AI roots share the contract: SkirmishAiSystem is registered in the + /// canonical tick order and its commands travel byte-identical to the + /// network path, so a float there desynchronises lockstep like any other. + /// A single float multiply that rounds differently on two CPUs + /// desynchronises lockstep, and no unit test catches it — the sources + /// have to be checked directly. /// /// The scan strips comments and string literals first, so documentation /// that merely mentions floats never trips it. @@ -33,13 +37,13 @@ public sealed class NoFloatInSimulationTests /// /// Scanned roots, relative to Assets/_Project/Scripts. /// - private static readonly string[] ScannedRoots = { "Core", "Simulation" }; + private static readonly string[] ScannedRoots = { "AI", "AI.Data", "Core", "Simulation" }; /// /// Repo-relative paths (forward slashes, below Assets/_Project/Scripts) /// allowed to contain IEEE-754 tokens, each with the reason it exists. - /// Nothing else in Core/** or Simulation/** may name float, double, - /// decimal or Mathf in code. + /// Nothing else in AI/**, AI.Data/**, Core/** or Simulation/** may + /// name float, double, decimal or Mathf in code. /// private static readonly Dictionary FloatWhitelist = new Dictionary(StringComparer.Ordinal) { @@ -72,7 +76,7 @@ public sealed class NoFloatInSimulationTests /// /// Zero-tolerance tokens: no whitelist at all outside the declaring /// file. A FromFloat CALL is the exact defect hard rule 3 forbids, and - /// UnityEngine must not be reachable from rank 0/1 at all. + /// UnityEngine must not be reachable from any scanned root at all. /// private static readonly Regex ForbiddenEverywhere = new Regex( @"SimFixed\s*\.\s*FromFloat|(?(); foreach (ScannedFile file in EnumerateSources()) seen.Add(file.RelativePath); - Assert.That(seen.Count, Is.GreaterThan(50), "expected the full Core+Simulation source set"); + Assert.That(seen.Count, Is.GreaterThan(50), "expected the full AI+AI.Data+Core+Simulation source set"); Assert.That(seen, Contains.Item("Core/SimFixed.cs")); Assert.That(seen, Contains.Item("Simulation/SimulationKernel.cs")); Assert.That(seen, Contains.Item("Simulation/Economy/EconomySystem.cs")); + Assert.That(seen, Contains.Item("AI/SkirmishAiSystem.cs")); + Assert.That(seen, Contains.Item("AI.Data/AiProfile.cs")); } // ---------------------------------------------------------------- diff --git a/Assets/Tests/PlayMode/MainMenuTests.cs b/Assets/Tests/PlayMode/MainMenuTests.cs index c957e7a..37780c5 100644 --- a/Assets/Tests/PlayMode/MainMenuTests.cs +++ b/Assets/Tests/PlayMode/MainMenuTests.cs @@ -237,6 +237,18 @@ public IEnumerator NewGame_StartsTheMatchHidesTheOverlayAndWakesTheCockpit() "— it must not keep playing over the match"); } + /// + /// The direct connection keeps its sprint-13 contract — the code is + /// masked, the masks are validated, and Abbrechen never starts + /// gameplay — but it is REACHED through the lobby: since sprint 14 + /// (D-092) "Netzpartie" opens the lobby entry view and the network + /// panel sits one step further in, behind "Direktverbindung …" + /// (MainMenuController.BuildMainButtons and BuildLobbyEntry). This + /// test still pinned the pre-lobby path and was the suite's one red + /// bar because of it (#110): the detour is the deliberate, documented + /// product behaviour, so the expectation had to follow the code, not + /// the other way round. Everything past the entry view is unchanged. + /// [UnityTest] public IEnumerator NetworkPanel_ValidatesMasksAndCancelsWithoutStartingGameplay() { @@ -250,7 +262,21 @@ public IEnumerator NetworkPanel_ValidatesMasksAndCancelsWithoutStartingGameplay( yield return null; Assert.AreEqual(DisplayStyle.None, root.Q("menu-main").style.display.value); - Assert.AreNotEqual(DisplayStyle.None, root.Q("menu-network").style.display.value); + Assert.AreNotEqual(DisplayStyle.None, root.Q("menu-lobby").style.display.value, + "'Netzpartie' opens the LOBBY panel, not the direct connection (sprint 14, D-092)"); + Assert.AreNotEqual(DisplayStyle.None, root.Q("lobby-entry").style.display.value, + "the lobby opens on its entry view — create, join, or the direct connection"); + Assert.AreEqual(DisplayStyle.None, root.Q("menu-network").style.display.value, + "the direct connection stays closed until the player picks it from the entry view"); + + Submit(FindButton(root, "Direktverbindung …")); + yield return null; + + Assert.AreEqual(DisplayStyle.None, root.Q("menu-lobby").style.display.value, + "the lobby gives way to the direct connection it led to"); + Assert.AreNotEqual(DisplayStyle.None, root.Q("menu-network").style.display.value, + "'Direktverbindung …' opens the unchanged sprint-13 network panel"); + TextField host = FindTextField(root, "Serveradresse"); TextField port = FindTextField(root, "Port"); TextField code = FindTextField(root, "Match-Code"); @@ -283,6 +309,9 @@ public IEnumerator NetworkPanel_ValidatesMasksAndCancelsWithoutStartingGameplay( yield return null; Assert.AreNotEqual(DisplayStyle.None, root.Q("menu-main").style.display.value); Assert.AreEqual(DisplayStyle.None, root.Q("menu-network").style.display.value); + Assert.AreEqual(DisplayStyle.None, root.Q("menu-lobby").style.display.value, + "Abbrechen on the direct connection returns to the MAIN panel, not one step back " + + "into the lobby (CancelNetworkJoin → ShowNetworkPanel(false))"); Assert.AreEqual(NetworkJoinPhase.Idle, bootstrap.JoinStatus.Phase); } diff --git a/Assets/_Project/Scripts/AI/CombatStrength.cs b/Assets/_Project/Scripts/AI/CombatStrength.cs index 1dbe20e..4b5671c 100644 --- a/Assets/_Project/Scripts/AI/CombatStrength.cs +++ b/Assets/_Project/Scripts/AI/CombatStrength.cs @@ -21,12 +21,12 @@ namespace Nova.AI /// not 963. That is deterministic on every machine, which is the only /// property the netcode cares about. /// - /// NOTHING ENFORCES THAT AUTOMATICALLY HERE. NoFloatInSimulationTests - /// scans Scripts/Core and Scripts/Simulation — not - /// Scripts/AI — so a float under this directory would pass CI today. - /// The determinism rule covers it, the guard does not; keeping the two in - /// step is a question for the owners of the EditMode mirror, since the test - /// exists twice. + /// THE GUARD NOW WATCHES THIS DIRECTORY TOO. NoFloatInSimulationTests + /// scans Scripts/AI and Scripts/AI.Data alongside + /// Scripts/Core and Scripts/Simulation — in both lanes, the + /// EditMode mirror and the .NET SimRunner suite the CI actually runs — so + /// a float under this directory fails the chain exactly like one in the + /// kernel would. /// /// /// diff --git a/CHANGELOG.md b/CHANGELOG.md index 87d58e3..5291b5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -310,6 +310,26 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de bei 2 AE/Tick, bis eine gespielte Balance-Kalibrierung belastbare Werte gibt ### Behoben +- **Der Determinismus-Wächter sieht jetzt auch den KI-Strang (#74).** `NoFloatInSimulationTests` + scannte `Scripts/Core` und `Scripts/Simulation`, nicht aber `Scripts/AI` und + `Scripts/AI.Data` — dabei ist `SkirmishAiSystem` in der kanonischen + Tickreihenfolge registriert und seine Befehle laufen byte-gleich zum + Netzwerkpfad. Ein `float` dort hätte den Lockstep genauso gebrochen wie einer + im Kernel, nur hätte ihn kein Test gefangen. Beide Wächterkopien scannen jetzt + alle vier Wurzeln (97 statt 86 Dateien); der Bestand ist sauber, es gab nichts + zu beheben. Neue Existenz-Pins verhindern, dass der Scan eines Tages still + über ein leeres Verzeichnis grün läuft. Die Lücke hatte der externe + Beitragende selbst im Quelltext vermerkt — der Vermerk ist nachgezogen +- **Der eine rote PlayMode-Test war eine überholte Erwartung (#110).** + `MainMenuTests.NetworkPanel_ValidatesMasksAndCancelsWithoutStartingGameplay` + schlug auf unberührtem `main` fehl und pinnte den Sprint-13-Pfad: seit + Sprint 14 (D-092) öffnet „Netzpartie" die **Lobby**, und das Netzwerk-Panel + liegt einen Schritt dahinter hinter „Direktverbindung …". Der Test geht jetzt + den echten Weg und behauptet dabei **mehr** als vorher — dass das + Netzwerk-Panel bis zur Wahl geschlossen bleibt und dass „Abbrechen" ins + Hauptmenü zurückführt, nicht in die Lobby. Kein Produktivcode geändert. + Offen bleibt der eigentliche Befund des Issues: die Unity-Tests laufen in + keiner CI, und das ist eine Inhaberentscheidung - **Ruckler in der Bauphase: das Baubereich-Overlay hat den Hauptthread gebremst.** Das 21.4-Overlay fragte die beiden Zonen-Reads (`IsInsideBuildInfluence`, `HasMinimumBuildingSpacing`) **pro Texel** ab — jeder Texel scannte das diff --git a/tools/Nova.SimRunner.Tests/NoFloatInSimulationTests.cs b/tools/Nova.SimRunner.Tests/NoFloatInSimulationTests.cs index f2dad9a..27f7e28 100644 --- a/tools/Nova.SimRunner.Tests/NoFloatInSimulationTests.cs +++ b/tools/Nova.SimRunner.Tests/NoFloatInSimulationTests.cs @@ -11,9 +11,13 @@ namespace Nova.SimRunner.Tests /// Determinism source guard (.NET lane). Sprint hard rule 3 and /// docs/tech/SimulationCore.md section 9: the authoritative simulation is /// Q16.16 fixed point, so no IEEE-754 arithmetic may enter - /// Assets/_Project/Scripts/Simulation/** or Core/**. A single float - /// multiply that rounds differently on two CPUs desynchronises lockstep, - /// and no unit test catches it — the sources have to be checked directly. + /// Assets/_Project/Scripts/Simulation/**, Core/**, AI/** or AI.Data/**. + /// The AI roots share the contract: SkirmishAiSystem is registered in the + /// canonical tick order and its commands travel byte-identical to the + /// network path, so a float there desynchronises lockstep like any other. + /// A single float multiply that rounds differently on two CPUs + /// desynchronises lockstep, and no unit test catches it — the sources + /// have to be checked directly. /// /// The scan strips comments and string literals first, so documentation /// that merely mentions floats never trips it. @@ -32,13 +36,13 @@ public sealed class NoFloatInSimulationTests /// /// Scanned roots, relative to Assets/_Project/Scripts. /// - private static readonly string[] ScannedRoots = { "Core", "Simulation" }; + private static readonly string[] ScannedRoots = { "AI", "AI.Data", "Core", "Simulation" }; /// /// Repo-relative paths (forward slashes, below Assets/_Project/Scripts) /// allowed to contain IEEE-754 tokens, each with the reason it exists. - /// Nothing else in Core/** or Simulation/** may name float, double, - /// decimal or Mathf in code. + /// Nothing else in AI/**, AI.Data/**, Core/** or Simulation/** may + /// name float, double, decimal or Mathf in code. /// private static readonly Dictionary FloatWhitelist = new Dictionary(StringComparer.Ordinal) { @@ -71,7 +75,7 @@ public sealed class NoFloatInSimulationTests /// /// Zero-tolerance tokens: no whitelist at all outside the declaring /// file. A FromFloat CALL is the exact defect hard rule 3 forbids, and - /// UnityEngine must not be reachable from rank 0/1 at all. + /// UnityEngine must not be reachable from any scanned root at all. /// private static readonly Regex ForbiddenEverywhere = new Regex( @"SimFixed\s*\.\s*FromFloat|(?(); foreach (ScannedFile file in EnumerateSources()) seen.Add(file.RelativePath); - Assert.That(seen.Count, Is.GreaterThan(50), "expected the full Core+Simulation source set"); + Assert.That(seen.Count, Is.GreaterThan(50), "expected the full AI+AI.Data+Core+Simulation source set"); Assert.That(seen, Contains.Item("Core/SimFixed.cs")); Assert.That(seen, Contains.Item("Simulation/SimulationKernel.cs")); Assert.That(seen, Contains.Item("Simulation/Economy/EconomySystem.cs")); + Assert.That(seen, Contains.Item("AI/SkirmishAiSystem.cs")); + Assert.That(seen, Contains.Item("AI.Data/AiProfile.cs")); } // ----------------------------------------------------------------