champions-of-luneraria/docs/port/U1-RULES.md
Martin Stickelbroeck 4d6ed17dca U1: Regelkern in C#, verhaltensgleich zur Godot-Referenz
Vollständiger, engineunabhängiger Regelkern (Luneraria.Core, ohne
UnityEngine): Partie mit sechs Plätzen und zwei Teams, private Planung mit
atomarer Übernahme, Gold, Käufe, Aufwertungen, Formation, drei Lanes mit
zwei Laneturmen und zwei Basistürmen je Team, Ancients, unbegrenzte Wellen,
Helden mit Stufen, Items und Talenten, Angriffschampions, Bossgrotte mit
Anspruch, Entsendung, Kopfgeld und dreirundiger Sperre, Bots und
Siegbedingung (nur Ancient-Fall). Serverautoritative Befehlsschnittstelle.

- Deterministisch: fester Takt, geordnete Maps, fortlaufende IDs, kein
  Zufall, keine Uhr, keine Threads
- Tabellen einmal aus Godot erzeugt (tools/port/gen_content.py)
- Golden-Referenzen aus Godot (tools/godot-golden): 19 Szenarien,
  120 Snapshots, 51.221 Werte, Abweichung 0 (Toleranzen dokumentiert)
- Tests: content 75, core 91, m2 73, golden 157, determinism 6 = 402/402
- Leistung (M5): Simulationsschritt 0,259 ms statt 1,587 ms in GDScript
- Referenzfehler R1 im Bot-Gedächtnis bewusst übernommen und dokumentiert
- Geplante Helden (Lucy, Ghaliga, Siegfried, Feena) im Datenmodell, inaktiv
- Doku unter docs/port (U1-RULES, PORT-STATUS, PORT-QA, U2 bis U5)

Unity-Editor-Lauf offen: Die Unity-Lizenz ist noch nicht aktiviert; gebaut
und getestet mit dem .NET 8 SDK auf Unitys Sprachstand (C# 9, .NET
Standard 2.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:31:18 +02:00

170 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
## 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`. Im Unity Test Runner laufen dieselben Suiten über `Luneraria.Core.Tests` (EditMode), sobald die Lizenz aktiv ist.
## 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 folgt mit U2.
- 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.
- 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).