Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions Assets/Tests/EditMode/Simulation/NoFloatInSimulationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// <para>
/// The scan strips comments and string literals first, so documentation
/// that merely mentions floats never trips it.
Expand All @@ -33,13 +37,13 @@ public sealed class NoFloatInSimulationTests
/// <summary>
/// Scanned roots, relative to Assets/_Project/Scripts.
/// </summary>
private static readonly string[] ScannedRoots = { "Core", "Simulation" };
private static readonly string[] ScannedRoots = { "AI", "AI.Data", "Core", "Simulation" };

/// <summary>
/// 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.
/// </summary>
private static readonly Dictionary<string, string> FloatWhitelist = new Dictionary<string, string>(StringComparer.Ordinal)
{
Expand Down Expand Up @@ -72,7 +76,7 @@ public sealed class NoFloatInSimulationTests
/// <summary>
/// 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.
/// </summary>
private static readonly Regex ForbiddenEverywhere = new Regex(
@"SimFixed\s*\.\s*FromFloat|(?<!\.)\bUnityEngine\b", RegexOptions.Compiled);
Expand Down Expand Up @@ -101,7 +105,7 @@ public void NoFloatTokensOutsideTheDocumentedWhitelist()
Assert.That(offenders, Is.Empty,
"IEEE-754 arithmetic in the authoritative simulation breaks cross-platform lockstep " +
"(sprint hard rule 3). Use SimFixed/SimTrig instead. If a file genuinely belongs at " +
"the presentation boundary, move it out of Core/Simulation — do not grow the " +
"the presentation boundary, move it out of the scanned roots — do not grow the " +
"whitelist without an explicit review.\n" + string.Join("\n", offenders));
}

Expand All @@ -120,7 +124,7 @@ public void NoFromFloatCallsAndNoEngineReferences()
}

Assert.That(offenders, Is.Empty,
"SimFixed.FromFloat must never be called inside Core/Simulation, and rank 0/1 " +
"SimFixed.FromFloat must never be called in any scanned root, and the scanned " +
"assemblies must not reference UnityEngine at all.\n" + string.Join("\n", offenders));
}

Expand Down Expand Up @@ -161,10 +165,12 @@ public void ScanActuallyReachesTheSimulationSources()
var seen = new List<string>();
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"));
}

// ----------------------------------------------------------------
Expand Down
31 changes: 30 additions & 1 deletion Assets/Tests/PlayMode/MainMenuTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,18 @@ public IEnumerator NewGame_StartsTheMatchHidesTheOverlayAndWakesTheCockpit()
"— it must not keep playing over the match");
}

/// <summary>
/// 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.
/// </summary>
[UnityTest]
public IEnumerator NetworkPanel_ValidatesMasksAndCancelsWithoutStartingGameplay()
{
Expand All @@ -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");
Expand Down Expand Up @@ -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);
}

Expand Down
12 changes: 6 additions & 6 deletions Assets/_Project/Scripts/AI/CombatStrength.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ namespace Nova.AI
/// not 963. That is deterministic on every machine, which is the only
/// property the netcode cares about.
/// <para>
/// NOTHING ENFORCES THAT AUTOMATICALLY HERE. <c>NoFloatInSimulationTests</c>
/// scans <c>Scripts/Core</c> and <c>Scripts/Simulation</c> — not
/// <c>Scripts/AI</c> — 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. <c>NoFloatInSimulationTests</c>
/// scans <c>Scripts/AI</c> and <c>Scripts/AI.Data</c> alongside
/// <c>Scripts/Core</c> and <c>Scripts/Simulation</c> — 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.
/// </para>
/// </para>
/// <para>
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 16 additions & 10 deletions tools/Nova.SimRunner.Tests/NoFloatInSimulationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// <para>
/// The scan strips comments and string literals first, so documentation
/// that merely mentions floats never trips it.
Expand All @@ -32,13 +36,13 @@ public sealed class NoFloatInSimulationTests
/// <summary>
/// Scanned roots, relative to Assets/_Project/Scripts.
/// </summary>
private static readonly string[] ScannedRoots = { "Core", "Simulation" };
private static readonly string[] ScannedRoots = { "AI", "AI.Data", "Core", "Simulation" };

/// <summary>
/// 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.
/// </summary>
private static readonly Dictionary<string, string> FloatWhitelist = new Dictionary<string, string>(StringComparer.Ordinal)
{
Expand Down Expand Up @@ -71,7 +75,7 @@ public sealed class NoFloatInSimulationTests
/// <summary>
/// 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.
/// </summary>
private static readonly Regex ForbiddenEverywhere = new Regex(
@"SimFixed\s*\.\s*FromFloat|(?<!\.)\bUnityEngine\b", RegexOptions.Compiled);
Expand Down Expand Up @@ -100,7 +104,7 @@ public void NoFloatTokensOutsideTheDocumentedWhitelist()
Assert.That(offenders, Is.Empty,
"IEEE-754 arithmetic in the authoritative simulation breaks cross-platform lockstep " +
"(sprint hard rule 3). Use SimFixed/SimTrig instead. If a file genuinely belongs at " +
"the presentation boundary, move it out of Core/Simulation — do not grow the " +
"the presentation boundary, move it out of the scanned roots — do not grow the " +
"whitelist without an explicit review.\n" + string.Join("\n", offenders));
}

Expand All @@ -119,7 +123,7 @@ public void NoFromFloatCallsAndNoEngineReferences()
}

Assert.That(offenders, Is.Empty,
"SimFixed.FromFloat must never be called inside Core/Simulation, and rank 0/1 " +
"SimFixed.FromFloat must never be called in any scanned root, and the scanned " +
"assemblies must not reference UnityEngine at all.\n" + string.Join("\n", offenders));
}

Expand Down Expand Up @@ -160,10 +164,12 @@ public void ScanActuallyReachesTheSimulationSources()
var seen = new List<string>();
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"));
}

// ----------------------------------------------------------------
Expand Down
Loading