champions-of-luneraria/docs/port/U1-RULES.md
Martin Stickelbroeck 9bf397e765 U1.5 Unity Core Integration (2/2): explizite float32-Rundung, Nachweise
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>
2026-09-30 15:22:36 +02:00

14 KiB
Raw Blame History

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.

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
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: 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).