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"));
}
// ----------------------------------------------------------------