Befund: Unitys Mono 6.13 rechnet float32-Zwischenwerte doppelt genau (ECMA-335 I.12.1.3). Der erste Editor-Lauf fand 5 von 157 Golden- Abweichungen: Blickrichtung bis 1,6e-3 rad; in mirror_match Runde 7 verliert eine Einheit in Schritt 599 ihr Ziel, und der Kampf dauert 4,8 s länger. Eine Toleranz ist dafür nicht begründbar. Maßnahme: Vec2 rundet jeden Zwischenwert explizit auf float32 (conv.r4). Keine Regeländerung, R1 unverändert. .NET 8: 402/402, Golden 0, Trace byteidentisch zu vorher. Unity-Editor (Release und Debug): 402/402, Golden bitgleich in allen Bereichen, Float-Probe 4000/4000. - CoreIntegrationTests: keine Engine-Referenzen im Kern, Float-Arithmetik wie Godot. - docs/port/U1.5-UNITY.md: Versionen, Lizenz, Pakete, Tests, Golden je Bereich, Float-Befund, Editor-Benchmark, fehlende Build-Module. - docs/port/KNOWN-BEHAVIOR-DIFFERENCES.md mit "Legacy defects retained for parity" (R1) und F1. - Protokolle unter docs/port/logs/u1_5/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
172 lines
14 KiB
Markdown
172 lines
14 KiB
Markdown
# 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).
|