champions-of-luneraria/Assets/Luneraria/Tests/Core/MiniJson.cs
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

194 lines
6.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace Luneraria.Core.Tests
{
/// <summary>
/// Minimal JSON reader for the golden files, without package dependencies (the same code
/// runs in Unity and in the .NET harness). Objects keep their key order; numbers without
/// fraction or exponent become long, all others double.
/// </summary>
public static class MiniJson
{
public static object Parse(string text)
{
int at = 0;
object value = ReadValue(text, ref at);
SkipSpace(text, ref at);
if (at != text.Length)
throw new FormatException("trailing characters at " + at);
return value;
}
static void SkipSpace(string text, ref int at)
{
while (at < text.Length && char.IsWhiteSpace(text[at]))
at++;
}
static object ReadValue(string text, ref int at)
{
SkipSpace(text, ref at);
char c = text[at];
if (c == '{')
return ReadObject(text, ref at);
if (c == '[')
return ReadArray(text, ref at);
if (c == '"')
return ReadString(text, ref at);
if (text.Length - at >= 4 && string.CompareOrdinal(text, at, "true", 0, 4) == 0)
{
at += 4;
return true;
}
if (text.Length - at >= 5 && string.CompareOrdinal(text, at, "false", 0, 5) == 0)
{
at += 5;
return false;
}
if (text.Length - at >= 4 && string.CompareOrdinal(text, at, "null", 0, 4) == 0)
{
at += 4;
return null;
}
return ReadNumber(text, ref at);
}
static OrderedMap<string, object> ReadObject(string text, ref int at)
{
var result = new OrderedMap<string, object>();
at++;
SkipSpace(text, ref at);
if (text[at] == '}')
{
at++;
return result;
}
while (true)
{
SkipSpace(text, ref at);
string key = ReadString(text, ref at);
SkipSpace(text, ref at);
if (text[at] != ':')
throw new FormatException("expected ':' at " + at);
at++;
result[key] = ReadValue(text, ref at);
SkipSpace(text, ref at);
if (text[at] == ',')
{
at++;
continue;
}
if (text[at] == '}')
{
at++;
return result;
}
throw new FormatException("expected ',' or '}' at " + at);
}
}
static List<object> ReadArray(string text, ref int at)
{
var result = new List<object>();
at++;
SkipSpace(text, ref at);
if (text[at] == ']')
{
at++;
return result;
}
while (true)
{
result.Add(ReadValue(text, ref at));
SkipSpace(text, ref at);
if (text[at] == ',')
{
at++;
continue;
}
if (text[at] == ']')
{
at++;
return result;
}
throw new FormatException("expected ',' or ']' at " + at);
}
}
static string ReadString(string text, ref int at)
{
if (text[at] != '"')
throw new FormatException("expected string at " + at);
at++;
var builder = new StringBuilder();
while (true)
{
char c = text[at++];
if (c == '"')
return builder.ToString();
if (c != '\\')
{
builder.Append(c);
continue;
}
char escape = text[at++];
switch (escape)
{
case '"': builder.Append('"'); break;
case '\\': builder.Append('\\'); break;
case '/': builder.Append('/'); break;
case 'b': builder.Append('\b'); break;
case 'f': builder.Append('\f'); break;
case 'n': builder.Append('\n'); break;
case 'r': builder.Append('\r'); break;
case 't': builder.Append('\t'); break;
case 'u':
builder.Append((char)int.Parse(text.Substring(at, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture));
at += 4;
break;
default: throw new FormatException("bad escape at " + at);
}
}
}
static object ReadNumber(string text, ref int at)
{
int start = at;
bool fraction = false;
while (at < text.Length && "+-0123456789.eE".IndexOf(text[at]) >= 0)
{
if (text[at] == '.' || text[at] == 'e' || text[at] == 'E')
fraction = true;
at++;
}
string token = text.Substring(start, at - start);
if (!fraction)
{
long whole;
if (long.TryParse(token, NumberStyles.Integer, CultureInfo.InvariantCulture, out whole))
return whole;
}
return double.Parse(token, NumberStyles.Float, CultureInfo.InvariantCulture);
}
// ---------------------------------------------------------------- access helpers
public static OrderedMap<string, object> Obj(object value) { return (OrderedMap<string, object>)value; }
public static List<object> Arr(object value) { return (List<object>)value; }
public static double Num(object value) { return value is long l ? l : (double)value; }
public static int Int(object value) { return value is long l ? (int)l : (int)(double)value; }
public static string Str(object value) { return value == null ? "" : value.ToString(); }
public static object Get(OrderedMap<string, object> map, string key)
{
object value;
return map.TryGetValue(key, out value) ? value : null;
}
}
}