Skip to content

feat(ai): Goal-Katalog fuer die Skirmish-KI, erste Regel DefendHome - #96

Open
arn-c0de wants to merge 5 commits into
VibecodingGermany:mainfrom
arn-c0de:feat/ai-goal-system-r8
Open

feat(ai): Goal-Katalog fuer die Skirmish-KI, erste Regel DefendHome#96
arn-c0de wants to merge 5 commits into
VibecodingGermany:mainfrom
arn-c0de:feat/ai-goal-system-r8

Conversation

@arn-c0de

Copy link
Copy Markdown
Collaborator

Was & Warum

Die Skirmish-KI benennt jetzt, was eine Einheit vorhat (GoalKind statt namenloser if-Kette), und die Form bekommt ihre erste Regel: DefendHome — wer noch im Sammelring steht, bricht ab und marschiert zum eigenen HQ, wenn dort ein sichtbarer bewaffneter Gegner innerhalb von defendHomeCells (10, 0 = aus) steht. Vorher hing eine angekommene Einheit allein an der D-087-Auto-Acquisition (6–7 Zellen), während der Sammelpunkt zwölf Zellen vom HQ liegt — ein Angreifer an der Basis war ausserhalb jeder Reichweite. AiBehaviorId: r7r8.

Das ist die Grundlage, auf der die künftigen KI-Verhaltensweisen aufsetzen — ich lege gerade die Baselines dafür an; aktueller Stand und geplante Ausbaustufen sind im Labor einsehbar: https://github.com/arn-c0de/Nova.AiLab/blob/main/ROADMAP.md

Im laufenden Spiel gesehen: nein — im Labor gebaut und gemessen. Ersatznachweise: die Umbenennung ist verhaltensneutral (kanonische Partie unbewegt, Laborartefakte byte-identisch), 0xE002DD893916967B), 722/722 grün.

Restrisiko:

  • Die Wirkungszahlen im CHANGELOG (96 % → 60 %, 3.213 → 6.490 Ticks) sind am Stand vor den zwei Review-Korrekturen im letzten Commit gemessen. Grössenordnung, kein Nachweis — wird neu gemessen.
  • Unterhalb der Squad-Schwelle läuft der Armeeschritt nicht, dort verteidigt niemand. Bekannt, absichtlich nicht in diesem PR: das wäre eine Verhaltensausweitung mit eigener Messpflicht.
  • Die Regel verschiebt die Niederlage der Legion, sie wendet sie nicht ab.

Checkliste

  • dotnet test tools/Nova.SimRunner.Tests lokal grün
  • Zeile unter [Unreleased] in CHANGELOG.md
  • D-ID im DecisionLog — keine echte Entscheidung; die Goal-Priorität liegt bewusst in C# statt im Profil, damit ProfileHash sich nicht bewegt
  • Bei Simulationsänderung: keine Determinismus-Baseline im selben PR geändert

Externe Beiträge

  • I agree to the Contributor License Agreement

arn-c0de and others added 5 commits August 10, 2026 20:52
Der Armeeschritt entschied in einer if-Kette, in der keine Verzweigung
einen Namen hatte. Er waehlt jetzt je Einheit und Kadenz EIN Goal aus
einer festen Prioritaetsliste — Retreat, Attack, Hold, Advance — und
wendet dessen Wirkung aus einer Tabelle an.

ES IST KEINE VERHALTENSAENDERUNG, und das ist der ganze Zweck des
Schritts: dieselben vier Bedingungen in derselben Reihenfolge, dieselben
Befehle. Der Nachweis ist kein Test, sondern eine Zahl — die kanonische
Partie entscheidet auf demselben Tick mit demselben Endzustand
(CanonicalAiOutcomeTests), und ein Laborlauf ist byte-identisch bis auf
die gemessene Laufzeit. Deshalb kein AiBehaviorId.Revision-Bump.

KEIN PROFILFELD, obwohl der Plan Prioritaeten im Profil vorsah. Ein
angehaengtes Feld bewegt AiProfile.ProfileHash und damit den
Verhaltensbezeichner, der in jedes Laborartefakt gedruckt wird — der
Schritt waere dann nicht byte-identisch gewesen, und die Byte-Gleichheit
ist genau das, was ihn auswertbar macht. Eine Aus-Stellung bringt jedes
Modul mit, das eine REGEL bekommt, im PR, der ihm die Regel gibt.

Ein Goal ist KEIN Zustand: es wird je Kadenz neu abgeleitet und nirgends
gespeichert. Die KI bleibt eine reine Funktion des committeten Zustands,
es entsteht kein Sidecar-Block, kein neues System, keine Aenderung der
Tick-Reihenfolge.

Zwei Klauseln, die beim Modularisieren leicht verlorengehen und beide
tragend sind:

- Retreat tritt fuer eine Einheit zurueck, die ANGEKOMMEN ist. Heimkommen
  beendet den Rueckzug; MS-1-Einheiten heilen nie, eine Regel, die sie
  bis zur Genesung festhaelt, staut die Verwundeten zuhause und fuellt
  nie wieder eine Welle.
- Attack verlangt, nicht auf Rueckzug zu sein. Eine verwundete Einheit
  ausserhalb des Rings erfuellt sonst jede andere Haelfte des
  Marschtests, und der Rueckzug erreichte genau die Einheit nicht, fuer
  die es ihn gibt.

707/707 im Spiel.
Zwei optionale Naehte an der Skirmish-KI, die der ausgelieferte Pfad nie
fuellt — MatchRunner uebergibt keine von beiden, die Referenzen bleiben
null, und die Null-Pruefung ist der ganze Aufwand im Spiel.

IAiGoalObserver laesst mitlesen, welches Goal eine Einheit bekommen hat
UND mit welchen Zahlen die Bedingung entschieden hat: Leben gegen die
Rueckzugsschwelle, Abstand gegen die Sammeltoleranz, Abstand zum HQ
gegen den Ring. Jede Bedingung im Katalog ist ein ganzzahliger
Vergleich, also ist der Abstand zum naechsten Goal exakte Arithmetik und
keine Schaetzung. Es ist ein Rueckruf und kein Feld auf dem System: ein
Puffer "was ich zuletzt entschieden habe" waere genau das Gedaechtnis,
das der ganze Entwurf vermeidet, und das erste, was eine spaetere Regel
lesen wollte.

IAiGoalOverride erlaubt es, einem Goal von aussen vorzugreifen — als
EINGABE der Entscheidung, wie das Profil, nicht als gespeicherter
Zustand. Die Maske ersetzt die Wahl und nie die Wirkung: ein
erzwungenes Goal erzeugt genau die Befehle, die dieses Goal immer
erzeugt, damit kein Werkzeug ein Verhalten vorfuehren kann, fuer das die
KI keinen Code hat.

Wozu ueberhaupt: bisher war der einzige Weg, das WARUM einer Einheit zu
zeigen, die Regeln neben der Aufzeichnung nachzubauen und das Ergebnis
"hergeleitet" zu nennen. Ein Diagnosewerkzeug, das eine zweite, leicht
abweichende Fassung der Regeln zeigt, ist schlimmer als eines, das
nichts zeigt.

WaveStrengthGate bekommt eine IsReady-Ueberladung, die die Schwelle
herausgibt. Als Ueberladung und nicht als zweite Rechnung beim Aufrufer,
damit der Vergleichsoperator weiterhin genau einmal existiert.

Beide Naehte sind byte-identisch nachgewiesen: ein Lauf mit Beobachter
und ein Lauf mit leerer Maske erreichen denselben Endzustand auf
demselben Tick wie einer ohne. Dazu die Gegenprobe, dass eine Maske, die
etwas benennt, die Partie tatsaechlich veraendert — sonst waere die
erste Zusicherung leer.

715/715 im Spiel (707 plus acht neue). Keine der vier Baseline-Dateien
angefasst.
Neues Goal DefendHome (r7 -> r8): wer noch im Sammelring steht und
nicht zurueckzieht, marschiert zur statischen HQ-Zelle und zielt auf
den naechsten sichtbaren bewaffneten Gegner. Wer draussen ist,
marschiert weiter -- die r3-Regel "Einheiten draussen werden nie
zurueckgerufen" bleibt, die Welle wird unterbrochen, nicht freigegeben.

Der Defekt ist so alt wie der Sammelpunkt: eine angekommene Einheit
bekommt absichtlich keinen Befehl und haengt allein an der D-087-Auto-
Acquisition, die 6 bzw. 7 Zellen reicht -- der Sammelpunkt liegt 12 vom
HQ. Gemessen: das Legions-HQ nimmt ueber 766 Ticks 327 Treffer, waehrend
die eigenen Einheiten im Median 13 Zellen entfernt unter Hold stehen und
keine einzige angreift.

Profilfeld defendHomeCells (ausgeliefert 10, 0 = aus). Auf 0 beidseitig
ergibt die kanonische Partie bitgenau r7: Tick 3213, 0xE002DD893916967B.
Der Bezeichner bewegt beide Haelften -- die Revision, weil Entscheidungen
sich aendern, und ProfileHash, weil die Regel mit ihrer Aus-Stellung
ausgeliefert wird.

Wehrlosigkeit im Beschussfenster 96 % -> 60 %, Partie 3213 -> 6490 Ticks,
eigene Verluste 18 -> 60: die Regel verschiebt die Niederlage, sie wendet
sie nicht ab. 31 Beurteilungen kosten 25 Befehle auf eine einzige
Zielzelle -- das ist die Korrektur gegenueber DefendBase (Journal V002,
+23 % Intents an einem beweglichen Ziel). Bei bedrohter Basis steht keine
Einheit im Ring mehr unter Hold.

Am Player angesehen: Nova.AiLab-goal-base-defense-r8-20260810
https://youtu.be/pD9NXtTutmM
Im laufenden Spiel gesehen: nein.
IAiGoalObserver und IAiGoalOverride lagen in Nova.AI.Data. Nova.Gameplay
und Nova.AI.Tests referenzieren aber nur Nova.AI, und dass niemand die
beiden optionalen Konstruktorargumente uebergibt, hilft nicht: der
Compiler muss die Parametertypen fuer die Ueberladungsaufloesung laden.
Ergebnis war CS0012 in MatchRunner.cs und in den Unity-EditMode-Tests,
also eine rote Startkompilierung — und weil -executeMethod danach laeuft,
die Meldung "class 'DemoLauncher' could not be found". Der Demo-Start war
nie kaputt, die Assembly war es.

dotnet test hat das nicht gesehen und kann es nicht sehen: SimRunner und
Labor ziehen alle Quellordner in eine Assembly, die Grenzen gibt es nur
in Unity.

Die Naehte gehoeren ohnehin zur Vertragsflaeche der KI und nicht zur
Datenschicht: sie ziehen jetzt in Nova.AI um, die jeder referenziert, der
die KI baut. Damit braucht das ausgelieferte Spiel fuer den Konstruktor
keine Referenz auf die KI-Datenassembly. Die Worte, auf die sich drei
Oberflaechen einigen muessen — GoalKind, Profile, AiBehaviorId — bleiben
in Nova.AI.Data, wie es dort dokumentiert ist.

Die Alternative waere eine Zeile in Nova.Gameplay.asmdef gewesen. Das ist
fremdes Terrain und haette eine Rueckfrage gebraucht, nicht einen Commit.

Geprueft: dotnet test 721/721 gruen. In Unity noch nicht nachkompiliert.
Beide beim Pruefen der Begruendung gefunden, nicht des Codes. Keine fuegt
eine Zahl hinzu, also bewegt sich ProfileHash nicht und der Aus-Pfad
(defendHomeCells: 0) bleibt bitgenau r7.

1. Die statische Zielzelle allein traegt das Argument nicht. Die
Re-Issue-Unterdrueckung vergleicht den stehenden Befehl, und
MovementSystem loescht genau den bei der Ankunft (UnitState.Stop()). Ein
angekommener Verteidiger hatte damit nichts mehr zu vergleichen: die
HQ-Zelle ging jede Kadenz erneut raus. Gemessen ein Move-Intent pro
Kadenz fuer die Dauer der Belagerung, mit acht stehenden Einheiten, die
jedes Mal wieder in IsMoving kippten -- die V002-Form eine Nummer
kleiner. DefendHome verstummt jetzt selbst, sobald die Einheit daheim
steht; dieselbe Stille, die Hold am Sammelpunkt hat.

2. Die Regel fragte, ob die Einheit gerade zurueckzieht. Diese Klausel
konnte nur Verwundete treffen, die schon angekommen waren -- Laufende
nimmt Retreat eine Zeile frueher. Ankommen beendet den Rueckzug aber nach
der eigenen Regel der KI. Sie hat also nichts bewirkt ausser genau diese
Einheiten an Hold zu geben: zwoelf Zellen draussen stehend, auf einen
Verfolger zielend, den sie nicht erreichen, waehrend die Basis brennt,
zu der sie zurueckgelaufen waren.

Dazu zwei kleinere Sachen im selben Zug: CollectVisibleThreats laesst
eine Entity-Id, die das Wire-Format nicht ausdruecken kann, jetzt aus
(eine 0 gewaenne den Tie-Break auf die niedrigste Id und hiesse dann
stillschweigend "kein Angriffsbefehl"), und der Goal-Katalog zaehlt an
vier Stellen wieder richtig -- r8 hatte ein fuenftes Goal eingezogen und
"die vier Bedingungen" stehen lassen.

Zwei neue Tests, beide gegen den unkorrigierten Stand als rot
nachgewiesen: AnArrivedDefenderIsNotSentHomeAgain und
AWoundedUnitThatIsAlreadyHomeDefendsInsteadOfHolding. Der bestehende
AHeldDefenceDoesNotProduceAnOrderEveryCadence konnte Punkt 1 nicht sehen
-- alle Verteidiger teilen eine Zielzelle, die Wiederholung kostet also
genau einen gruppierten Intent und liegt unter seiner Schranke.

Der CHANGELOG-Eintrag sagt jetzt ausdruecklich, dass die Wirkungszahlen
(96 % auf 60 %, 3.213 auf 6.490 Ticks, 18 auf 60 Verluste) am
unkorrigierten DefendHome gemessen wurden und vor dem Merge neu zu messen
sind. Ebenso notiert, nicht behoben: unterhalb der Squad-Schwelle laeuft
der Armeeschritt gar nicht, also verteidigt niemand -- das ist eine
Verhaltensausweitung mit eigener Messpflicht und gehoert in einen eigenen
PR.

Geprueft: dotnet test 722/722 gruen. Im laufenden Spiel nicht gesehen.
@arn-c0de
arn-c0de requested a review from cubetribe as a code owner August 10, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant