# U1 · Regelkern in C# Stand: 30. September 2026 · Zweig `luneraria/u1` · Referenz: Godot-Stand `luneraria/g3` (`1b540f2`, Regeln unverändert seit G2). **Ergebnis:** Der vollständige, engineunabhängige Regelkern läuft in C#. Er ist deterministisch und reproduzierbar und verhält sich in allen geprüften Fällen wie die Godot-Referenz: 402 von 402 Prüfungen, darunter 157 Golden-Vergleiche mit 51.221 Einzelwerten. **Noch offen:** der Lauf im Unity-Editor. Die Unity-Lizenz fehlt noch; bis dahin baut und testet der .NET-8-SDK den Kern mit Unitys Sprach- und API-Stand (C# 9, .NET Standard 2.1). ## Aufbau | Pfad | Inhalt | | --- | --- | | `Assets/Luneraria/Core/` | Assembly `Luneraria.Core`, `noEngineReferences: true` – keine Unity-Abhängigkeit | | `Core/Model/` | `Vec2` (Bodenvektor), `GdMath` (Skalarfunktionen), `OrderedMap` (Map mit fester Reihenfolge) | | `Core/Content/` | Tabellen (`ContentData.cs`, einmalig aus Godot erzeugt, seitdem gepflegte Quelle), Typen, `LuneBalance`, `LuneContent`, `LuneWaves`, `DataCatalog`, geplante Helden | | `Core/Simulation/` | `LuneMap`, `LuneUnit`, `LuneCombat`, `LuneMatch` mit Partiemodell, `LuneBot` | | `Assets/Luneraria/Tests/Core/` | Assembly `Luneraria.Core.Tests`: fünf Prüfsuiten, Golden-Dateien, Unity-Adapter (`Unity/CoreSuiteTests.cs`) | | `tools/dotnet/` | .NET-Projekte: Kern als `netstandard2.1`, Testlauf, Leistungsmessung | | `tools/godot-golden/` | Werkzeuge, die im Godot-Referenzprojekt laufen: Tabellenexport, Golden-Szenarien, Gleitkomma-Probe, Leistungsmessung | | `tools/port/gen_content.py` | einmalige Erzeugung von `ContentData.cs` aus dem Godot-Export | Unity dient später nur als Adapter und Präsentationsschicht (U2): Er treibt `LuneMatch` im festen Takt an und liest den Zustand. ## Portierter Umfang | Bereich | Umsetzung | Quelle in Godot | | --- | --- | --- | | Matchzustand, Phasen Planung/Kampf/Ende, sechs Plätze, zwei Teams | `LuneMatch`, `MatchModel.cs` | `lune_match.gd` | | Private Planung mit Entwurf, Rückgängig, atomare Übernahme bei Kampfbeginn | `Submit`, `Undo`, `BeginCombat`, `DraftValid` | `lune_match.gd` | | Gold, Käufe, Aufwertungen, Rückgabe (voll in derselben Planung, sonst 75 %) | `Apply` | `lune_match.gd` | | Formation: Reihen zu viert, Lane-Besitzer vorn, Rollenreihenfolge | `SpawnArmies`, `Sorted` | `lune_match.gd` | | Drei Lanes, Geometrie, Bottom so lang wie Top | `LuneMap` | `lune_map.gd` | | Zwei Laneturme je Lane, zwei Basistürme, Ancient, Sperren | `LuneMap.IsAttackable`, `LuneCombat` | `lune_map.gd`, `lune_combat.gd` | | Einheiten, Effekte, Schilde, Schaden über Zeit, Schadensmatrix | `LuneUnit` | `lune_unit.gd` | | Kampf: Ziele, Marsch, Trennung, Angriffe, Fähigkeiten, Tode, Ende | `LuneCombat` | `lune_combat.gd` | | Unbegrenzte Wellen mit steigender Stärke | `LuneWaves` | `lune_waves.gd` | | Helden: Stufen, Erfahrung, drei Itemplätze, Talente | `LuneContent.HeroDefinition`, `Apply` | `lune_content.gd`, `lune_match.gd` | | Normale Einheiten ohne Items | `item_buy` lehnt Truppen-IDs ab; `RosterEntry` hat kein Inventar | `lune_match.gd` | | Angriffschampions: Buchung n, Einsatz n+1, Kopfgeld, Verbrauch | `SpawnChampions`, `Settle` | `lune_match.gd` | | Bossgrotte: Expeditionen, ruhender Boss, Anspruch, Entsendung, Kopfgeld, dreirundige Sperre | `SpawnArmies`, `SettleBoss`, `LuneCombat.UpdateGrotto` | `lune_match.gd`, `lune_combat.gd` | | Bots über dieselbe Befehlsschnittstelle | `LuneBot` | `lune_bot.gd` | | Siegbedingung: nur Ancient-Zerstörung; zwei im selben Schritt = Unentschieden | `Settle` | `lune_match.gd` | Erhalten sind ausdrücklich: 3 gegen 3 auf drei Lanes, unbegrenzte Wellen, Partieende nur durch einen Ancient, zwei Laneturme je Lane und zwei Basistürme je Team, Heldenentwicklung, Angriffschampions, Bossgrotte mit Sperre. Die Architektur ist serverautoritativ: `LuneMatch` läuft nur beim Host; Clients senden `Command`-Absichten. Alte Saumwacht-Regeln (Arbeiter, Funken, Wellen der Kampagne) sind nicht portiert; ein Test prüft, dass der Kern keine solchen Typen enthält. ## Determinismus - **Fester Takt:** `SimStep` 0,05 s. Die Simulation liest weder Uhr noch Bildrate; Unity treibt sie mit einem Akkumulator an. - **Reihenfolgen:** Schleifen laufen nur über Listen in Spawnreihenfolge und über `OrderedMap` (Einfügereihenfolge, auch nach Entfernen stabil). `Dictionary` dient nur zum Nachschlagen; ein Codeaudit bestätigt, dass keine Regel darüber iteriert. - **IDs:** Einheiten-IDs zählen je Kampf von 1 hoch; UIDs sind `p{Platz}-{Folge}`, `h…`, `c…`, `boss-{Runde}`. Keine Objektidentität, keine Hashwerte. - **Sortierung:** Alle Vergleiche sind strenge Ordnungen auf eindeutigen Schlüsseln (Distanz, dann Einheiten-ID bzw. Rolle, dann Folge). Der Kern sortiert stabil per Einfügen. - **Zufall:** kommt in den Regeln nicht vor (in Godot wurde der Zufallsgenerator nur initialisiert, nie benutzt). - **Kein geteilter Zustand:** Zwei Partien im Wechsel liefern dasselbe wie einzeln. - **Physik und Threads:** nicht beteiligt. Nachweis: Die Suite `determinism` spielt dieselben Partien doppelt und verschachtelt. Alle Suiten liefen in drei getrennten Prozessen grün; .NET streut die String-Hashwerte je Prozess neu. ## Zahlen und Toleranzen Positionen sind Vektoren mit 32-Bit-Komponenten, alle anderen Größen 64 Bit, wie in der Referenz. `Vec2` rechnet wie Godots Vector2: einfache Einzelgenauigkeit ohne fusionierte Multiply-Add-Operationen. Die Probe `tools/godot-golden/float_probe.gd` zeigt 4.000 von 4.000 identischen Stichproben. Bitgleichheit ist **kein Ziel**. Verglichen wird fachlich: | Kategorie | Toleranz | Größte gemessene Abweichung (Mac, .NET 8) | | --- | --- | --- | | Ganzzahlen, Texte, Flags: Besitz, Gold, Käufe, Aufwertungen, Ziele (Einheiten-ID), Zustände, Gewinner, Befehlsergebnisse und Meldungen | exakt | – | | Lebenspunkte, Schilde, Gebäude-HP | 0,01 HP bzw. 10⁻⁹ relativ | 0 (5.679 Werte) | | Positionen, Lanefortschritt | 1 mm | 0 (3.420 Werte) | | Blickrichtung | 10⁻⁴ rad | 0 (1.140 Werte) | | Kampfzeit, Ereigniszeiten | 10⁻⁶ s | 0 (780 Werte) | | Effektstärken und -dauern | 10⁻⁶ | 0 (264 Werte) | Auf anderen Laufzeiten (Unity-Mono, IL2CPP, Windows x64, Linux) sind winzige Rundungsunterschiede möglich; dafür sind die Toleranzen da. Weil der Host autoritativ rechnet, müssen Clients die Simulation nicht selbst nachrechnen. **Nachtrag U1.5:** Unitys Mono rechnet float32-Zwischenwerte doppelt genau. Das änderte in einem Szenario eine Zielentscheidung, deshalb rundet `Vec2` seitdem jeden Zwischenwert explizit auf float32. Die .NET-Ergebnisse blieben byteidentisch; im Unity-Editor ist die Abweichung jetzt überall 0. Details: [U1.5-UNITY.md](U1.5-UNITY.md#float-befund). ## Bewusst andere technische Repräsentation | Godot | C# | Warum gleichwertig | | --- | --- | --- | | Definitionen, Spieler, Entwürfe, Helden, Buchungen als `Dictionary` | Klassen (`UnitDef`, `Player`, `Draft`, `HeroState`, `Booking`, `BossClaim`) | Standardwerte entsprechen den `get(key, default)`-Fallbacks (z. B. `SiegeFactor = 1`) | | leeres `{}` für „kein Held“ bzw. „kein Anspruch“ | `null` | – | | Befehle als `Dictionary` | `Command` mit Fabrikmethoden; fehlende Felder = `null` (z. B. Kauf ohne Lane = eigene Lane) | Nachrichten und Ablehnungsgründe identisch (Golden-Vergleich) | | `sort_custom` (Introsort, instabil) | stabile Einfügesortierung | bei strengen Ordnungen identisches Ergebnis | | GDScript-`int` (64 Bit) | `int` (32 Bit) | Werte liegen weit darunter; Monsterkopfgeld wird zusätzlich begrenzt | | Rückgängig über dieselbe `submit`-Funktion der Sitzung | `LuneMatch.Undo(slot)`; `Submit` kennt „undo“ nicht | wie in der Referenz, wo die Sitzung `undo()` direkt ruft | | `rng` in `LuneMatch` | entfällt | wurde nie benutzt | | Tabellen in `DataCatalog`/`LuneContent` | `ContentData.cs`, einmal erzeugt | Suite `content` vergleicht alles mit dem Godot-Export | ## Referenzfehler R1 (übernommen) `lune_bot.gd`, `_plan_boss`: Ein Bot, dessen Gedächtnis nur eine Champion-Buchung enthält, liest die fehlenden Felder `bar` und `sent`. GDScript bricht die Funktion dann ab. Folge: Ein Bossjäger-Bot, der vorher einen Champion gebucht hat, schickt nie eine Expedition in die Grotte. Beim Erzeugen der Golden-Referenzen trat der Fehler in vier Szenarien auf (`Invalid access to property or key 'bar'`). Der Port bildet das Verhalten bewusst nach (`LuneBot.PlanBoss`, Kommentar R1), damit die Bots fachlich gleich bleiben. **Empfehlung:** in einer späteren Balance- oder Bot-Phase korrigieren, mit neuer Golden-Referenz. ## Golden-Referenzen `Assets/Luneraria/Tests/Core/Golden/scenarios.json` beschreibt 19 Szenarien als Abfolge von Operationen. Operationen sind: Aufbau, Fixture wie Gold, Runde oder Gebäude-HP, Befehl, Bots, Kampfbeginn, Schritte, Kampfende, Zustandsaufnahme. `tools/godot-golden/golden_scenarios.gd` spielt sie mit den Godot-Regeln und schreibt `golden.json`; die Suite `golden` spielt sie in C# und vergleicht jeden Snapshot Feld für Feld. | Szenario | Deckt ab | | --- | --- | | `start_three_lanes` | normaler Drei-Lane-Start, Aufmarsch, Einheiten nach 10 und 30 s mit Zielen, Abrechnung | | `lane_outer_lost` | Lane ohne äußeren Turm | | `lane_both_lost` | Lane ohne beide Türme, Basis offen | | `base_tower_attack` | Angriff auf Basistürme bis zum Fall des Ancients | | `ancient_destruction` | Ancient-Zerstörung, Sieger, Partieende | | `high_wave_21`, `high_wave_137` | Wellen über 20 und über 100: Zusammensetzung, Werte, Kampf, Belohnungen | | `hero_levels` | Helden über neun Runden: Anwerben, Erfahrung, Stufen bis 6, Talente, Items | | `item_purchase` | Itemkauf, Plätze, Einzigartigkeit, Stufenbedingung, Rückgabe, Truppen ohne Items | | `attack_champion` | Buchung, Grenze, Rücknahme, Ankündigung, Einsatz, Kopfgeld, Verbrauch | | `boss_cycle` | Bossversuch erfolgreich, Anspruch, Lane-Wahl, Entsendung, Boss getötet mit Kopfgeld, Sperre in Runde 8 und 9, wieder offen in Runde 10, keine zweite Entsendung | | `boss_failure` | gescheiterter Bossversuch ohne Sperre, voller Boss beim nächsten Versuch | | `grotto_contested` | umkämpfte Grotte, ruhender Boss, Sieger erhält Anspruch | | `mirror_match` | exakte Symmetrie bei gleichen Bünden und Profilen | | `bot_match_1/2/3/11/28` | fünf vollständige Bot-Partien bis zum Ancient-Fall | Verglichen werden je Snapshot: Phase, Runde, Gewinner, Endnotiz; je Platz Gold, Entwurfsgold, Bereitschaft, Einnahmen, Ausgaben, Armee, Held, Buchungen und Rückgängig-Tiefe; Gebäude-HP; Bosszustand, Bosslog und Ankündigungen; die Abrechnung (Gewinne, Einkommen, verlorene Gebäude, Endgrund, Kills je Lane, Erfahrung); Kampfzustand und Grotte. Optional kommen hinzu: jede Einheit mit ID, Art, Team, Lane, Zone, Besitz, Leben, Schild, Position, Blickrichtung, Fortschritt, Ziel, Trefferzahl, Effekten und Schaden über Zeit; jedes Ereignis; die Welle. Dazu das Ergebnis und die Meldung jedes Befehls. Beim ersten Vergleich zeigte sich ein Fehler im Exporter, nicht im Port: `Array(typed_array)` liefert in Godot dieselbe Referenz. Frühe Snapshots enthielten deshalb schon den späteren Ancient-Fall. Behoben mit `duplicate()`. Bildnachweis: `docs/port/screenshots/u1/replay-*.png` zeigt die Karte mit den Godot-Positionen (Punkte) und den C#-Positionen (weiße Ringe). In allen drei Bildern stehen alle lebenden Einheiten an der Referenzposition (31/31, 35/35, 15/15). ## Tests | Suite | Prüfungen | Inhalt | | --- | ---: | --- | | `content` | 75 | alle Tabellen und die berechnete Karte gegen den Godot-Export; Geometrieabweichung 0 m | | `core` | 91 | Port von `test_lune_core.gd` (91 in Godot) | | `m2` | 73 | Port von `test_lune_m2.gd` (73 in Godot); die Saumwacht-Autoload-Prüfung ist durch „keine Altökonomie-Typen im Kern“ ersetzt | | `golden` | 157 | 19 Szenarien, 120 Snapshots, 51.221 Werte | | `determinism` | 6 | Wiederholung, Verschachtelung, IDs, fester Takt | | **gesamt** | **402** | alle bestanden, dreimal in getrennten Prozessen | ```bash sh tools/run_core_tests.sh ``` Mit `REGENERATE_GOLDEN=1` werden vorher die Godot-Referenzen neu erzeugt. Das braucht `../saumwacht-godot`; das Referenz-Repository bleibt unverändert. Protokoll: `docs/port/logs/u1/core-tests.json`. Seit U1.5 schreibt das Skript sein Protokoll nach `$LUNE_TEST_REPORT` (Standard `tools/out/`). Im Unity Test Runner laufen dieselben Suiten über `Luneraria.Core.Tests` (EditMode); Ergebnis in U1.5-UNITY.md. ## Leistung Gleiche Aufstellung wie die Godot-Benchmark-Probe: Runde 13, sechs Armeen am Versorgungslimit, 120 bewegliche Einheiten. Gemessen auf dem Mac (Apple M5). | | Godot 4.7.2 (GDScript) | C# (.NET 8, Release) | Faktor | | --- | ---: | ---: | ---: | | Simulationsschritt, Median aus 5 × 200 | 1,587 ms | 0,259 ms | 6,1 × | | Bot-Partien Seeds 1–3 (32 Runden) | 20,7 s | 1,17 s | 17,8 × | Rohdaten: `docs/port/logs/u1/sim-bench-{godot,csharp}.json`. - Unitys Editor läuft mit Mono. Die Messung dort steht in [U1.5-UNITY.md](U1.5-UNITY.md#leistung): 0,51 ms je Schritt im Release-Modus. - Der Faktor gegenüber Godot ist ein Anhaltspunkt, keine Zusage für den Player. ## Geplante Helden Lucy, Ghaliga, Siegfried und Feena stehen als `PlannedHeroes` im Datenmodell: - Volk, Rolle, Formationsreihe, Waffe, Aussehen, Ziel. - Fähigkeit und Ultimate mit Auslöser (automatisch bzw. Command-Menü) und Wirkungsart. - Balanceparameter wie Radius, Dauer, Stärke, Ziele, Abklingzeit und Betäubung; sie sind noch leer (`null` = nicht festgelegt). Sie sind nicht aktiv und nicht käuflich; die Partie bietet weiter nur die drei Referenzhelden, damit die Parität gilt. Für die Umsetzung sind neue Regeln nötig: - Betäubung (Excalibur). - Bindung/Aggro (Versteinerung). - Ausnahme vom Tempodeckel (Turbopfeile). - Kampfbefehle während der Kampfphase (Command-Menü; heute gibt es nur Planungsbefehle). ## Offene Punkte - ~~Unity-Lizenz aktivieren, dann Kern und Tests im Editor (Mono) laufen lassen und die Mono-Leistung messen.~~ Erledigt in U1.5. - Kerntests auf Windows 11 und Fedora 44 ausführen (Zielplattformen). Der Kern ist plattformneutral, der Lauf dort steht aber aus. - R1 später korrigieren (siehe oben).