diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ac279..ed056ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,20 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de gespulte Partie ohne dieses Etikett nichts wert ist. ### Geändert +- **Der Ausgangspin der kanonischen KI-Partie ist vom Identitätspin getrennt + (D-101).** `SkirmishAiTests` pinnte Kennung, Entscheidungstick und + Endzustands-Hash in einer Zusicherung. Die beiden Zahlen bewegen sich aber bei + jeder Änderung an der Simulation, in der die KI spielt — im ersten + Wirtschaftssprint riss das jedes Paket, ohne dass eine Zeile KI-Code berührt + war, und die im Test hinterlegte Prozedur schickte den falschen Strang ins + Verhaltensjournal. Entscheidungstick und Endzustand liegen jetzt in + `CanonicalAiOutcomeTests` beim Maintainer-Strang, die Kennung bleibt beim + Einheitenstrang. Die Diagnose bleibt erhalten: der neue Test liest die Kennung + mit und unterscheidet im Fehlertext zwischen KI- und Simulationsänderung. Neu + ist eine Zusicherung, die es vorher nicht gab — eine **unentschiedene** Partie + gilt als Defekt und nicht als verschobener Pin. Dazu bekommt + `tools/Nova.SimRunner.Tests/` erstmals eine Eigentümerzeile in der + Schreibhoheitstabelle: geteilt je Datei, fremde Testdateien nur nach Ansage. - **Angeschlagene KI-Einheiten drehen ab (Einheitenstrang, KI-Verhalten `r4`):** Wer die KI angriff, merkte nichts davon — angeschlagene Einheiten kämpften bis zum letzten Lebenspunkt. Eine Einheit unter 60 % Leben, in deren Nähe (8 diff --git a/docs/production/DecisionLog.md b/docs/production/DecisionLog.md index 8d76f21..4c4e8b5 100644 --- a/docs/production/DecisionLog.md +++ b/docs/production/DecisionLog.md @@ -1,6 +1,6 @@ # Decision Log -**Version:** 1.32.0 | **Status:** aktiv (laufend) | **Verantwortungsbereich:** Game Director / Lead Technical Director / Project Owner | **Sprint:** 16 +**Version:** 1.33.0 | **Status:** aktiv (laufend) | **Verantwortungsbereich:** Game Director / Lead Technical Director / Project Owner | **Sprint:** 16 ## Zweck @@ -2924,6 +2924,74 @@ Vertriebsweg und setzt sich später neben `install_hash`, ohne Tabellen, Sperrarten oder Bedienweg zu ändern. Q-041 bleibt bis zur Öffnung der Beta offen. +--- + +### D-101 | verbindlich | Sprint 16 (der Ausgangspin der kanonischen KI-Partie wird vom Identitätspin getrennt) + +**Status:** Inhaberentscheidung vom 2026-08-09, umgesetzt im selben Zug. + +**Kontext:** `SkirmishAiTests.AiBehaviorId_TracksWhatTheAiActuallyDoes` pinnte +drei Werte in einer Zusicherung: die KI-Kennung `AiBehaviorId.Value`, den +Entscheidungstick der kanonischen Partie und deren Endzustands-Hash. Die +Kopplung war beabsichtigt — der Kommentar begründet sie damit, dass die Kennung +geänderte Zahlen fängt und der Endzustand geänderte Regeln. + +Der erste Wirtschaftssprint hat gezeigt, dass die Kopplung an der falschen Naht +liegt: Entscheidungstick und Endzustand bewegen sich bei **jeder** Änderung an +der Simulation, in der die KI spielt. Paket 16.1 verschob den Endzustand, 16.2 +zusätzlich den Tick — beide fassen keine Zeile KI-Code an. Die im Test +hinterlegte Prozedur schickte den Netzstrang damit in Arns Verhaltensjournal, +also in das falsche Buch. Der Test liegt zudem in +`tools/Nova.SimRunner.Tests/`, einem Verzeichnis, das in der +Schreibhoheitstabelle gar keinen Eigentümer hatte. + +**Alternativen:** + +1. **Arn zieht den Pin nach jedem Merge des Netzstrangs nach.** Verworfen: bei + zehn Paketen in Sprint 16 sind das zehn Runden Ping-Pong über einen + Fork-PR-Weg, und jede blockiert einen fertigen Maintainer-PR an einer + Zahl, die niemand aus seinem Strang bewegt hat. +2. **Der Netzstrang aktualisiert die Zahlen selbst, der Test bleibt, wo er + ist.** Verworfen: dann schreibt ein Strang regelmäßig in die Testdatei des + anderen, ohne dass die Tabelle das deckt — genau die Unklarheit, die den + Fall erzeugt hat. Ausserdem bliebe die irreführende Prozedur im Kommentar + stehen. +3. **Den Ausgangspin ersatzlos streichen.** Verworfen: er ist der einzige Test, + der bemerkt, dass die KI die kanonische Partie überhaupt nicht mehr + entscheidet. Genau das ist in Paket 16.3 passiert (Entscheidungstick 0), und + ohne den Pin wäre es niemandem aufgefallen. +4. **ANGENOMMEN: der Pin wird geteilt.** Die Kennung bleibt beim + Einheitenstrang (`SkirmishAiTests.AiBehaviorId_TracksWhichAiThisIs`), + Entscheidungstick und Endzustand ziehen in `CanonicalAiOutcomeTests` um, + das der Netzstrang besitzt. + +**Entscheidung:** + +1. `CanonicalAiOutcomeTests` (neu, Netzstrang) pinnt Entscheidungstick und + Endzustands-Hash der kanonischen KI-Partie. +2. `SkirmishAiTests` behält die Kennung und deren Prozedur (Revision bumpen, + Journaleintrag) unverändert. +3. Der Ausgangstest **liest die Kennung mit** und macht die Unterscheidung im + Fehlertext: Ausgang bewegt **und** Kennung bewegt heisst KI-Änderung; + Ausgang bewegt und Kennung unverändert heisst Simulationsänderung. +4. **Unentschieden ist ein Defekt, kein verschobener Pin.** Der Ausgangstest + sichert das zuerst und getrennt zu, damit der Fehlertext beide Fälle + auseinanderhält. +5. `tools/Nova.SimRunner.Tests/` bekommt eine Eigentümerzeile: geteilt je + Datei, fremde Testdateien nur nach Ansage. Der KI-Harness (`AiHost`, + `BuildMatch`) wird Vertragsfläche und dafür `internal`. + +**Begründung:** Die Kennung beantwortet „welche KI ist das", der Ausgang +beantwortet „hat sich irgendetwas bewegt". Das sind zwei Fragen, und nur die +erste gehört dem Einheitenstrang. Die Diagnose, die die Kopplung leistete, geht +nicht verloren — sie wandert aus der Zusicherung in den Fehlertext, wo sie +ohnehin hingehört: gelesen wird sie erst, wenn der Test rot ist. + +**Konsequenzen:** Vier Deklarationen in `SkirmishAiTests` werden `internal`, +die Zusicherung dort schrumpft auf eine Zeile, der erklärende Kommentar +verweist auf die neue Datei. Der Einheitenstrang ist zu informieren (Issue #75). +Wer den Harness umbenennt, sagt es an. Keine Baseline-Datei ist berührt. + ## Offene Punkte - Alle Sprint-4-Review-Befunde (105, davon 9 kritisch): 7 entscheidungsbedürftige kritische Befunde sind durch D-043–D-052 entschieden. @@ -3004,6 +3072,7 @@ offen. | Version | Datum | Änderung | Autor | |---|---|---|---| +| 1.33.0 | 2026-08-09 | D-101 aufgenommen: der Ausgangspin der kanonischen KI-Partie (Entscheidungstick, Endzustand) wird vom Identitätspin getrennt und zieht in eine Maintainer-Datei; `tools/Nova.SimRunner.Tests/` bekommt erstmals eine Eigentümerzeile | Project Owner / Orchestrator | | 1.0.0 | 2026-07-21 | D-001 bis D-005 aus Sprint 0 protokolliert | Game Director | | 1.1.0 | 2026-07-21 | D-006 (Unity 6.3 LTS + URP bestätigt) aus Sprint-1-Validierung | Lead Technical Director | | 1.2.0 | 2026-07-21 | D-007 bis D-019: verbindliche Game-Design-Grundlagen (Q-001–Q-012, Q-016, Q-017) | Game Director | diff --git a/docs/production/hashkrieg/13-15_Parallelbetrieb.md b/docs/production/hashkrieg/13-15_Parallelbetrieb.md index 0f84430..d522823 100644 --- a/docs/production/hashkrieg/13-15_Parallelbetrieb.md +++ b/docs/production/hashkrieg/13-15_Parallelbetrieb.md @@ -1,6 +1,6 @@ # Parallelbetrieb Sprint 13–18 — zwei Stränge, eine Simulation -**Version:** 1.3.1 | **Status:** verbindlich ab Merge des Sprint-13.0-PR | **Verantwortungsbereich:** Maintainers und Strangverantwortliche | **Sprint:** 13–18 | **Gilt für:** [13](13_Sprint_Netzpartie.md), [13B](13B_Sprint_Einheitenverhalten.md), [14](14_Sprint_Lobby.md), [15](15_Sprint_Netzstabilitaet.md), [16](16_Sprint_Wirtschaft.md), [18](18_Sprint_Befehl_und_Auswahl.md) | **Leitsatz:** getrennte Ordner sind billig, getrennte Determinismus-Zustände nicht +**Version:** 1.4.0 | **Status:** verbindlich ab Merge des Sprint-13.0-PR | **Verantwortungsbereich:** Maintainers und Strangverantwortliche | **Sprint:** 13–18 | **Gilt für:** [13](13_Sprint_Netzpartie.md), [13B](13B_Sprint_Einheitenverhalten.md), [14](14_Sprint_Lobby.md), [15](15_Sprint_Netzstabilitaet.md), [16](16_Sprint_Wirtschaft.md), [18](18_Sprint_Befehl_und_Auswahl.md) | **Leitsatz:** getrennte Ordner sind billig, getrennte Determinismus-Zustände nicht ## Warum es dieses Dokument gibt @@ -85,6 +85,7 @@ Freiraum. | `Scripts/Simulation/Replays/`, `Snapshots/` | **niemand ohne D-ID** | Speicherformat und Fingerprint — Änderung ist eine Inhaberentscheidung | | `Scripts/Simulation/State/` — **Layout und Serialisierung** | **niemand ohne D-ID** | Feldbestand, Feldreihenfolge, `StateVersion`, Blockformat. Das ist der Teil, der Snapshots und Replays unlesbar macht | | `Scripts/Simulation/State/` — **Befehlsanwendung** (`UnitCommandStateView`) | **Netzstrang** | mit D-095 aus dem Frost gelöst: *was* ein bestehender `CommandKind` in den Zustand schreibt, ist Verhalten, nicht Format. **Kein neuer `CommandKind`** — das Register bleibt eingefroren | +| `tools/Nova.SimRunner.Tests/` | **geteilt — je Datei** | jeder Strang besitzt die Testdateien zu seinem Gegenstand. Änderungen an einer fremden Testdatei werden **vorher angesagt**. Zwei Pins sind namentlich zugeordnet: `SkirmishAiTests.AiBehaviorId_TracksWhichAiThisIs` gehört dem Einheitenstrang, `CanonicalAiOutcomeTests` dem Netzstrang (D-101) | | `CHANGELOG.md` | **serialisiert** | ein Eintrag pro PR, Konflikte löst der Mergende | | `docs/production/hashkrieg/` | **Maintainer** | Planungsstand; Befunde kommen per Mail oder Issue, nicht per PR | @@ -107,7 +108,7 @@ D-095 schreibt diese bereits gelebte Linie nur auf. ### Vertragsflächen in fremdem Besitz -Vier Flächen gehören einem Strang, werden aber vom anderen konsumiert. Dort gilt +Fünf Flächen gehören einem Strang, werden aber vom anderen konsumiert. Dort gilt zusätzlich: **Verhalten ändern ja, Vertrag ändern nur nach Absprache.** | Fläche | Eigentümer | Konsument | Was ohne Absprache nicht geht | @@ -115,6 +116,7 @@ zusätzlich: **Verhalten ändern ja, Vertrag ändern nur nach Absprache.** | `Pathfinding.CostField` | Einheitenstrang | `ConstructionSystem` (Platzierungsprüfung, Sprint 16) | Signatur oder Begehbarkeits-Semantik von `IsWalkable` ändern. Flow-Field-Erzeugung und Pathfinding-Interna sind frei | | `FogOfWarSystem.GetTeamView` | Netzstrang | `CombatSystem` (Zielerlaubnis) | Rückgabeform oder Sichtbarkeitsregel ändern, ohne den Einheitenstrang zu informieren | | `WeaponProfiles`-Slot `UnitRole.Unit` | Einheitenstrang | `ConstructionSystem` (Baustellen tragen heute diese Rolle) | Sprint 16 löst die Kopplung auf, indem die Baustelle `def.Role` statt `UnitRole.Unit` bekommt. Bis dahin gilt: der Fallback-Schaden von 15 ist **keine** Baustellenregel, sondern ein Nebeneffekt. Wer den Slot umwidmet, sagt es an | +| `SkirmishAiTests.AiHost` / `BuildMatch` | Einheitenstrang | `CanonicalAiOutcomeTests` (Netzstrang) | Umbenennen oder Signatur ändern, ohne es anzusagen — der Ausgangspin baut darauf auf. Der Inhalt des Harness ist frei | | `UnitState.AttackTarget` | Einheitenstrang (`CombatSystem`) | `UnitCommandStateView` (`Stop` löscht es, Sprint 16) | das Feld **löschen** darf der Netzstrang. Eine Regel, *wann automatisch neu erfasst wird* (D-087, Auto-Zielerfassung), gehört dem Einheitenstrang. Ein „Feuer einstellen" ist deshalb kein Netzstrang-Paket | ## Neue Systeme — wer die Tick-Reihenfolge setzt @@ -291,6 +293,7 @@ zusammen gespielt wurden, sind zwei Behauptungen. | Version | Datum | Änderung | Autor | |---|---|---|---| +| 1.4.0 | 2026-08-09 | **D-101:** `tools/Nova.SimRunner.Tests/` bekommt eine Eigentümerzeile — geteilt je Datei, fremde Testdateien nur nach Ansage. Der Ausgangspin der kanonischen KI-Partie ist vom Identitätspin getrennt: `CanonicalAiOutcomeTests` (Netzstrang) hält Entscheidungstick und Endzustand, `SkirmishAiTests` die Kennung. Der KI-Harness ist damit Vertragsfläche | Orchestrator | | 1.3.1 | 2026-08-09 | Zugangsmodell nachgezogen: der externe Beitragende erhält einen Collaborator-Eintrag auf der Stufe **Triage**, damit ihn Issues erreichen können. Der Code-Weg bleibt Fork-only, die Merge-Sperre unberührt | Orchestrator | | 1.3.0 | 2026-08-09 | **D-095:** Trennung von „Verhaltensraum" auf „Dateihoheit" umgestellt — Sprint 16 läuft parallel zu 13B statt dahinter. `Simulation/State/` in Layout (weiter eingefroren) und Befehlsanwendung (Eigentümer des jeweiligen Befehls) getrennt. Zwei Vertragsflächen ergänzt (`WeaponProfiles`-Slot `UnitRole.Unit`, `UnitState.AttackTarget`). Merge-Fenster auf einen Strang je Fenster verschärft. Abschnitte „Definitions-Hash" und „kanonische Startaufstellung an vier Stellen" ergänzt. Plattform-Abschnitt berichtigt: der Linux-Build existiert seit `e15f5e6`, die offene Bringschuld ist stattdessen ein `NovaBuildCommit`-Leser im Spiel | Orchestrator | | 1.2.2 | 2026-08-09 | Zwei Pfade nachgetragen, die der Einheitenstrang in der Praxis braucht und die die Tabelle nicht kannte: `tools/Nova.AiLab/` samt Tests (Messwerkzeug, kein Spielcode) und `Presentation/UI/DebugHud.cs` als ausdrückliche Ausnahme aus `Presentation/`. Beide Lücken lagen im Dokument, nicht im Verhalten des Beitragenden | Producer / Agent (Umsetzung) | diff --git a/tools/Nova.SimRunner.Tests/CanonicalAiOutcomeTests.cs b/tools/Nova.SimRunner.Tests/CanonicalAiOutcomeTests.cs new file mode 100644 index 0000000..262ead1 --- /dev/null +++ b/tools/Nova.SimRunner.Tests/CanonicalAiOutcomeTests.cs @@ -0,0 +1,76 @@ +using NUnit.Framework; +using Nova.AI.Data; + +namespace Nova.SimRunner.Tests +{ + /// + /// Pins the OUTCOME of the canonical AI match: the tick it is decided on + /// and the end-state hash. Owned by the maintainer strand (D-101). + /// + /// Why this is not in any more: these two + /// numbers move on ANY change to the simulation the AI plays in — economy, + /// construction, production, vision — not only on a change to the AI + /// itself. They sat in the AI suite until 2026-08-09, where every package + /// of Sprint 16 tripped them and the failure message sent the maintainer + /// strand to the AI behaviour journal, which is the wrong book. The + /// identifier pin — "which AI is this" — stayed where it belongs, in + /// . + /// + /// + /// The split does not lose the diagnosis, because this test reads the + /// identifier too. Read the two results together: + /// + /// + /// outcome moved, identifier movedthe AI + /// changed. The AI strand bumps AiBehaviorId.Revision, writes the + /// journal entry in tools/Nova.AiLab/reports/behavior-log.md, and + /// updates the numbers below in the same commit. + /// outcome moved, identifier unchangedthe + /// simulation under the AI changed. The strand that changed it updates the + /// numbers below and says so in its PR. No journal entry, no revision + /// bump — nothing about the AI moved. + /// outcome UNDECIDED (tick 0)not a moved + /// pin. The AI no longer finishes the match inside the budget, which means + /// something broke its loop. Fix the cause; do not update the + /// numbers. + /// + /// + /// This is NOT one of the four determinism baselines: those live in their + /// own files and force a behaviour PR and a baseline PR apart. This pin + /// belongs WITH the change that moved it and is updated in the same commit. + /// + /// + [TestFixture] + public sealed class CanonicalAiOutcomeTests + { + /// Decided tick of the canonical AI match, last moved by: Sprint 15 (r5). + private const uint PinnedDecidedTick = 2548u; + + /// End-state hash of the canonical AI match, last moved by: Sprint 15 (r5). + private const string PinnedEndState = "0x14472B2B943ED2BB"; + + [Test] + public void CanonicalAiMatch_DecidesOnThePinnedTick_WithThePinnedEndState() + { + SkirmishAiTests.AiHost host = SkirmishAiTests.BuildMatch(SkirmishAiTests.Seed); + uint decided = host.RunUntilDecided(SkirmishAiTests.EndToEndBudgetTicks); + ulong endState = host.Kernel.CalculateStateHash(); + + // Undecided is a defect, not a moved pin — it is asserted first and + // on its own so the failure says which of the two happened. + Assert.That(decided, Is.Not.Zero, + $"the AI did not decide the canonical match within {SkirmishAiTests.EndToEndBudgetTicks} ticks — " + + "its loop is broken, not merely moved. Fix the cause instead of updating the pin."); + + Assert.Multiple(() => + { + Assert.That(decided, Is.EqualTo(PinnedDecidedTick), + "the canonical AI match is decided on a different tick than the pinned one — " + + $"AI identifier is {AiBehaviorId.Value}; if that string is unchanged, the simulation moved, not the AI"); + Assert.That($"0x{endState:X16}", Is.EqualTo(PinnedEndState), + "the canonical AI match ends in a different state than the pinned one — " + + $"AI identifier is {AiBehaviorId.Value}; if that string is unchanged, the simulation moved, not the AI"); + }); + } + } +} diff --git a/tools/Nova.SimRunner.Tests/SkirmishAiTests.cs b/tools/Nova.SimRunner.Tests/SkirmishAiTests.cs index c2e375f..3357d57 100644 --- a/tools/Nova.SimRunner.Tests/SkirmishAiTests.cs +++ b/tools/Nova.SimRunner.Tests/SkirmishAiTests.cs @@ -40,7 +40,8 @@ namespace Nova.SimRunner.Tests [TestFixture] public sealed class SkirmishAiTests { - private const ulong Seed = 0xA17E57DE57UL; + /// Seed of the canonical AI match. Internal since D-101: CanonicalAiOutcomeTests pins its outcome. + internal const ulong Seed = 0xA17E57DE57UL; private const byte HumanSlot = 0; private const byte AiSlot = 1; private const ushort MapWidth = 128; @@ -53,13 +54,13 @@ public sealed class SkirmishAiTests /// at tick 2242, so 6.000 ticks is a ~2.7x margin — comfortably sane, /// and exact because the whole loop is deterministic. /// - private const int EndToEndBudgetTicks = 6000; + internal const int EndToEndBudgetTicks = 6000; // ---------------------------------------------------------------- // The AI host (mirror of MatchRunner's skirmish wiring) // ---------------------------------------------------------------- - private sealed class AiHost + internal sealed class AiHost { public SimulationKernel Kernel; public EntityManager Entities; @@ -250,7 +251,7 @@ private static void ApplyOpeningPosition(AiHost host) } } - private static AiHost BuildMatch(ulong seed, AiProfile? profile = null) + internal static AiHost BuildMatch(ulong seed, AiProfile? profile = null) { AiHost host = BuildAiHost(seed, profile); ApplyOpeningPosition(host); @@ -417,22 +418,29 @@ public void SkirmishAi_EndToEnd_IsByteDeterministicAcrossRuns() // ---------------------------------------------------------------- /// - /// Pins TOGETHER with what the AI actually - /// does. Either half alone is useless: the identifier's profile hash - /// catches changed numbers but never a changed rule, and the end state - /// catches a changed rule but does not know the identifier exists. + /// Pins — the identifier that says WHICH AI + /// this is. It carries the profile hash and the revision, so it catches + /// changed numbers, and the revision catches a changed rule that the + /// author declared. /// - /// WHEN THIS GOES RED — and only then read on, because the failure - /// message is the procedure: - /// - /// - /// Was the behaviour change intended? If not, fix the code. The - /// test just told you the AI plays differently than you thought. - /// If it was: bump AiBehaviorId.Revision, add its line to - /// the history in that file, write the journal entry in + /// WHEN THIS GOES RED: the AI's profile numbers or its revision moved. + /// Bump AiBehaviorId.Revision, add its line to the history in + /// that file, write the journal entry in /// tools/Nova.AiLab/reports/behavior-log.md — measured values, - /// better AND worse — and only then update the numbers below. - /// + /// better AND worse — and only then update the string below. + /// + /// + /// THE OTHER HALF LIVES ELSEWHERE (D-101, 2026-08-09). This pin used to + /// also assert the decided tick and the end-state hash of the canonical + /// AI match. Those two numbers move on ANY simulation change, not only + /// on an AI change — every package of Sprint 16 tripped them — and the + /// procedure above then pointed the wrong strand at the wrong journal. + /// They now live in CanonicalAiOutcomeTests, which the maintainer + /// strand owns. The diagnosis stays intact because that test reads this + /// identifier: outcome moved AND identifier moved means the AI changed; + /// outcome moved and identifier unchanged means the simulation under it + /// changed. + /// /// /// This is NOT one of the four determinism baselines and must not be /// treated as one: those live in their own files and separate a @@ -441,21 +449,10 @@ public void SkirmishAi_EndToEnd_IsByteDeterministicAcrossRuns() /// /// [Test] - public void AiBehaviorId_TracksWhatTheAiActuallyDoes() + public void AiBehaviorId_TracksWhichAiThisIs() { - AiHost host = BuildMatch(Seed); - uint decided = host.RunUntilDecided(EndToEndBudgetTicks); - ulong endState = host.Kernel.CalculateStateHash(); - - Assert.Multiple(() => - { - Assert.That(AiBehaviorId.Value, Is.EqualTo("r5.779A1B5B"), - "the AI identifier changed — bump the revision and write the journal entry"); - Assert.That(decided, Is.EqualTo(2548u), - "the AI decides the canonical match on a different tick than the pinned one"); - Assert.That($"0x{endState:X16}", Is.EqualTo("0x14472B2B943ED2BB"), - "same identifier, different end state: behaviour moved without the revision moving"); - }); + Assert.That(AiBehaviorId.Value, Is.EqualTo("r5.779A1B5B"), + "the AI identifier changed — bump the revision and write the journal entry"); } // ----------------------------------------------------------------