using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; namespace Luneraria.Core.Tests { /// /// Visual evidence for U1: the map with lanes and buildings, the Godot reference positions of /// every unit of a golden snapshot as filled dots (Blau blue, Rot red, grotto violet) and the /// C# positions of the same snapshot as white rings. Identical positions show as ringed dots. /// Usage: Luneraria.Core.Tests.dll --image <scenario> <label> <out.png> /// public static class ReplayImage { const int Size = 1100; public static int Write(string golden, string scenarioId, string label, string outPath) { var spec = MiniJson.Obj(MiniJson.Parse(File.ReadAllText(Path.Combine(golden, "scenarios.json")))); var scenario = MiniJson.Arr(spec["scenarios"]).Select(MiniJson.Obj).First(s => MiniJson.Str(s["id"]) == scenarioId); var replay = new GoldenReplay(); foreach (object op in MiniJson.Arr(scenario["ops"])) replay.Apply(MiniJson.Obj(op)); var reference = MiniJson.Obj(MiniJson.Obj(MiniJson.Obj(MiniJson.Parse(File.ReadAllText(Path.Combine(golden, "golden.json"))))["scenarios"])[scenarioId]); var expected = MiniJson.Arr(reference["snapshots"]).Select(MiniJson.Obj).First(s => MiniJson.Str(s["label"]) == label); var actual = replay.Snapshots.Select(o => (OrderedMap)o).First(s => MiniJson.Str(s["label"]) == label); var pixels = new byte[Size * Size * 3]; Fill(pixels, 12, 16, 20); LuneMap map = replay.State.Map; foreach (LanePath path in map.LanePaths.Values) { for (int i = 1; i < path.Points.Length; i++) Line(pixels, path.Points[i - 1], path.Points[i], 110, 100, 80); } foreach (BuildingDef building in map.Buildings) Square(pixels, building.Position, building.Tier == "core" ? 9 : 6, building.Team == 0 ? (byte)70 : (byte)200, 90, building.Team == 0 ? (byte)200 : (byte)70); int same = 0; int total = 0; List expectedUnits = MiniJson.Arr(expected["units"]); List actualUnits = MiniJson.Arr(actual["units"]); for (int i = 0; i < expectedUnits.Count; i++) { var unit = MiniJson.Obj(expectedUnits[i]); if (!(bool)unit["alive"] || MiniJson.Str(unit["kind"]) == "tower" || MiniJson.Str(unit["kind"]) == "core") continue; int team = MiniJson.Int(unit["team"]); var at = new Vec2(MiniJson.Num(unit["x"]), MiniJson.Num(unit["y"])); Disc(pixels, at, 4, team == 0 ? (byte)90 : team == 1 ? (byte)255 : (byte)180, team == 0 ? (byte)170 : team == 1 ? (byte)110 : (byte)120, team == 0 ? (byte)255 : team == 1 ? (byte)90 : (byte)255); var mine = MiniJson.Obj(actualUnits[i]); var mineAt = new Vec2(MiniJson.Num(mine["x"]), MiniJson.Num(mine["y"])); Ring(pixels, mineAt, 6, 255, 255, 255); total++; if (mineAt.DistanceTo(at) <= 0.001f) same++; } WritePng(outPath, pixels); Console.WriteLine("REPLAY_IMAGE {0}/{1}: {2} of {3} living units at the reference position -> {4}", scenarioId, label, same, total, outPath); return same == total ? 0 : 1; } static int Px(float world) { return (int)Math.Round((world + 40f) / 80f * (Size - 1)); } static void Set(byte[] pixels, int x, int y, byte r, byte g, byte b) { if (x < 0 || y < 0 || x >= Size || y >= Size) return; int at = (y * Size + x) * 3; pixels[at] = r; pixels[at + 1] = g; pixels[at + 2] = b; } static void Fill(byte[] pixels, byte r, byte g, byte b) { for (int i = 0; i < pixels.Length; i += 3) { pixels[i] = r; pixels[i + 1] = g; pixels[i + 2] = b; } } static void Line(byte[] pixels, Vec2 a, Vec2 b, byte r, byte g, byte bl) { int steps = Math.Max(1, (int)(a.DistanceTo(b) / 80f * Size * 2f)); for (int i = 0; i <= steps; i++) { Vec2 p = a.Lerp(b, (double)i / steps); for (int dx = -2; dx <= 2; dx++) for (int dy = -2; dy <= 2; dy++) Set(pixels, Px(p.X) + dx, Px(p.Y) + dy, r, g, bl); } } static void Square(byte[] pixels, Vec2 at, int half, byte r, byte g, byte b) { for (int dx = -half; dx <= half; dx++) for (int dy = -half; dy <= half; dy++) Set(pixels, Px(at.X) + dx, Px(at.Y) + dy, r, g, b); } static void Disc(byte[] pixels, Vec2 at, int radius, byte r, byte g, byte b) { for (int dx = -radius; dx <= radius; dx++) for (int dy = -radius; dy <= radius; dy++) if (dx * dx + dy * dy <= radius * radius) Set(pixels, Px(at.X) + dx, Px(at.Y) + dy, r, g, b); } static void Ring(byte[] pixels, Vec2 at, int radius, byte r, byte g, byte b) { for (int i = 0; i < 48; i++) { double angle = Math.PI * 2.0 * i / 48.0; Set(pixels, Px(at.X) + (int)Math.Round(Math.Cos(angle) * radius), Px(at.Y) + (int)Math.Round(Math.Sin(angle) * radius), r, g, b); } } // ---------------------------------------------------------------- minimal PNG writer (RGB, no dependencies) static void WritePng(string path, byte[] rgb) { Directory.CreateDirectory(Path.GetDirectoryName(path)); var raw = new MemoryStream(); for (int y = 0; y < Size; y++) { raw.WriteByte(0); raw.Write(rgb, y * Size * 3, Size * 3); } byte[] data = raw.ToArray(); var zlib = new MemoryStream(); zlib.WriteByte(0x78); zlib.WriteByte(0x9C); using (var deflate = new DeflateStream(zlib, CompressionLevel.Optimal, true)) deflate.Write(data, 0, data.Length); uint adler = Adler32(data); zlib.WriteByte((byte)(adler >> 24)); zlib.WriteByte((byte)(adler >> 16)); zlib.WriteByte((byte)(adler >> 8)); zlib.WriteByte((byte)adler); using (FileStream file = File.Create(path)) { file.Write(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }, 0, 8); var header = new List(); header.AddRange(BigEndian(Size)); header.AddRange(BigEndian(Size)); header.AddRange(new byte[] { 8, 2, 0, 0, 0 }); Chunk(file, "IHDR", header.ToArray()); Chunk(file, "IDAT", zlib.ToArray()); Chunk(file, "IEND", new byte[0]); } } static byte[] BigEndian(int value) { return new[] { (byte)(value >> 24), (byte)(value >> 16), (byte)(value >> 8), (byte)value }; } static void Chunk(Stream stream, string type, byte[] data) { byte[] kind = System.Text.Encoding.ASCII.GetBytes(type); stream.Write(BigEndian(data.Length), 0, 4); stream.Write(kind, 0, 4); stream.Write(data, 0, data.Length); byte[] both = kind.Concat(data).ToArray(); stream.Write(BigEndian((int)Crc32(both)), 0, 4); } static uint Crc32(byte[] data) { uint crc = 0xFFFFFFFF; foreach (byte value in data) { crc ^= value; for (int k = 0; k < 8; k++) crc = (crc & 1) != 0 ? 0xEDB88320 ^ (crc >> 1) : crc >> 1; } return ~crc; } static uint Adler32(byte[] data) { uint a = 1; uint b = 0; foreach (byte value in data) { a = (a + value) % 65521; b = (b + a) % 65521; } return (b << 16) | a; } } }