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>
13 KiB
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:
SimStep0,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).Dictionarydient 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 |
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).