using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Luneraria.Core; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; namespace Luneraria.View.Tests { /// /// U2D vertical function test: complete bot matches visible in the Unity scene, before the full /// asset import. Match 1 (seed 2) runs from round 1 to the fall of an Ancient: lanes, towers /// fall, heroes fight, the grotto boss is won and deployed. Match 2 (seed 9) starts at round 21 /// with the fixture of the golden scenario high_wave_21 (waves above 20). Both must end with the /// fingerprint of the plain core run. A camera director photographs the key moments /// (docs/port/screenshots/u2d/); results and frame times go to docs/port/logs/u2d/ (or the /// LUNE_EVIDENCE folder). /// public sealed class VerticalFunctionTests { const float Speed = 6f; sealed class Shot { public string Label; public Func Trigger; public Action Aim; public float Delay = .35f; public bool Done; } sealed class Stats { public int Frames; public double Seconds; public readonly List FrameMs = new List(); public double AdvanceMs; public double SyncMs; public int Units; } [UnityTest] [Timeout(3600000)] public IEnumerator BotMatchesRunVisiblyAndMatchTheCore() { var report = new StringBuilder("{\n \"runs\": [\n"); var failures = new List(); yield return Play(2, 1, 0, "match-seed2", SeedTwoShots(), report, failures, false); report.Append(",\n"); yield return Play(9, 21, 4000, "match-seed9-round21", WaveShots(), report, failures, true); report.Append("\n ]\n}\n"); string logs = PlayModeSupport.Evidence("logs", "u2d"); Directory.CreateDirectory(logs); File.WriteAllText(Path.Combine(logs, "vertical-function-test.json"), report.ToString()); Assert.IsEmpty(failures, string.Join("\n", failures)); } static IEnumerator Play(int seed, int startRound, int extraGold, string label, List shots, StringBuilder report, List failures, bool measure) { var options = new MatchOptions { Seed = seed, HumanSlot = -1 }; LuneTrace core = LuneTrace.RunCore(options, startRound, extraGold, ""); QualitySettings.vSyncCount = 0; Application.targetFrameRate = 60; yield return PlayModeSupport.Load(); var driver = UnityEngine.Object.FindAnyObjectByType(); var view = UnityEngine.Object.FindAnyObjectByType(); var rig = UnityEngine.Object.FindAnyObjectByType(); Camera camera = Camera.main; rig.Interactive = false; driver.HumanSlot = -1; driver.Seed = seed; driver.StartRound = startRound; driver.ExtraGold = extraGold; driver.Speed = Speed; driver.Restart(); LuneMatchRunner runner = driver.Runner; rig.Overview(true); var rounds = new List(); var events = new List(); int maxUnits = 0; int maxHeroes = 0; runner.Settled += settlement => { rounds.Add(string.Format("{{\"round\": {0}, \"combat_seconds\": {1}, \"lost\": [{2}], \"boss\": \"{3}\", \"end\": \"{4}\"}}", settlement.Round, PlayModeSupport.Number(settlement.CombatTime), string.Join(", ", settlement.BuildingsLost.Select(b => "\"" + b + "\"")), settlement.Boss, settlement.EndReason)); foreach (string building in settlement.BuildingsLost) events.Add(string.Format("\"round {0}: {1} destroyed\"", settlement.Round, building)); if (settlement.Boss != "none") events.Add(string.Format("\"round {0}: grotto boss {1}\"", settlement.Round, settlement.Boss)); }; var captures = new List(); Shot active = null; float activeSince = 0f; var stats = new Stats(); bool measuring = false; float realStart = Time.realtimeSinceStartup; float overSince = -1f; while (Time.realtimeSinceStartup - realStart < 900f) { yield return null; LuneMatch match = runner.Match; if (match.Combat != null && runner.Current == LuneMatchRunner.Stage.Combat) { maxUnits = Math.Max(maxUnits, match.Combat.Units.Count(u => u.IsMobile() && u.Alive)); maxHeroes = Math.Max(maxHeroes, match.Combat.Units.Count(u => u.Kind == "hero" && u.Alive)); } // Frame times at normal game speed with the most units on the map (match 2, round 21). if (measure && runner.Current == LuneMatchRunner.Stage.Combat && match.RoundNumber == startRound) { double elapsed = match.Combat.Elapsed; if (!measuring && elapsed >= 10.0 && stats.Frames == 0) { measuring = true; driver.Speed = 1f; Application.targetFrameRate = -1; rig.LookAt(new Vec2(0, 0), 64f, true); } if (measuring) { stats.Frames++; stats.Seconds += Time.unscaledDeltaTime; stats.FrameMs.Add(Time.unscaledDeltaTime * 1000.0); stats.AdvanceMs += driver.LastAdvanceMs; stats.SyncMs += view.LastSyncMs; stats.Units = Math.Max(stats.Units, match.Combat.Units.Count(u => u.IsMobile() && u.Alive)); if (stats.Seconds >= 20.0) { measuring = false; driver.Speed = Speed; Application.targetFrameRate = 60; } } } if (measuring) continue; if (active == null) { active = shots.FirstOrDefault(s => !s.Done && s.Trigger(runner, view)); if (active != null) { rig.SetYaw(0f, true); active.Aim(runner, view, rig); activeSince = Time.realtimeSinceStartup; } } else if (Time.realtimeSinceStartup - activeSince >= active.Delay) { // Render directly: WaitForEndOfFrame never resumes in batch mode. string file = Path.Combine(PlayModeSupport.Evidence("screenshots", "u2d"), active.Label + ".png"); LuneCapture.Render(camera, file); captures.Add("\"" + active.Label + "\""); Debug.Log("LUNE_U2D shot " + active.Label + " round " + runner.Match.RoundNumber + " " + runner.Current); active.Done = true; active = null; } if (runner.Current == LuneMatchRunner.Stage.Over) { if (overSince < 0f) overSince = Time.realtimeSinceStartup; bool pending = shots.Any(s => !s.Done && s.Trigger(runner, view)) || active != null; if (!pending && Time.realtimeSinceStartup - overSince > 3.5f) break; if (Time.realtimeSinceStartup - overSince > 12f) break; } } LuneTrace trace = runner.Trace; bool same = runner.Current == LuneMatchRunner.Stage.Over && trace.Hash == core.Hash && trace.Steps == core.Steps; if (!same) failures.Add(label + ": fingerprint " + trace.Hash.ToString("X16") + " / core " + core.Hash.ToString("X16")); foreach (Shot shot in shots.Where(s => !s.Done)) failures.Add(label + ": no shot " + shot.Label); double[] frameMs = stats.FrameMs.OrderBy(v => v).ToArray(); string performance = stats.Frames == 0 ? "null" : string.Format("{{\"window\": \"20 s real time at 1x speed, unlimited frame rate, round {0}\", \"frames\": {1}, \"fps\": {2}, \"frame_ms_avg\": {3}, \"frame_ms_p95\": {4}, \"simulation_ms_per_frame\": {5}, \"views_ms_per_frame\": {6}, \"units_alive_max\": {7}}}", startRound, stats.Frames, PlayModeSupport.Number(stats.Frames / stats.Seconds), PlayModeSupport.Number(stats.Seconds * 1000.0 / stats.Frames), PlayModeSupport.Number(frameMs[(int)(frameMs.Length * .95)]), PlayModeSupport.Number(stats.AdvanceMs / stats.Frames), PlayModeSupport.Number(stats.SyncMs / stats.Frames), stats.Units); report.AppendFormat(" {{\"label\": \"{0}\", \"seed\": {1}, \"start_round\": {2}, \"extra_gold\": {3}, \"speed\": {4}, \"winner\": {5}, \"end_note\": \"{6}\", \"steps\": {7}, \"hash\": \"{8:X16}\", \"core_hash\": \"{9:X16}\", \"same_as_core\": {10},\n \"units_alive_max\": {11}, \"heroes_alive_max\": {12}, \"boss_log\": [{13}],\n \"rounds\": [{14}],\n \"events\": [{15}],\n \"shots\": [{16}],\n \"performance\": {17}}}", label, seed, startRound, extraGold, Speed, runner.Match.Winner, runner.Match.EndNote, trace.Steps, trace.Hash, core.Hash, same ? "true" : "false", maxUnits, maxHeroes, string.Join(", ", runner.Match.BossLog.Select(e => string.Format("\"round {0}: {1} team {2} {3}\"", e.Round, e.Event, e.Team, e.Lane))), string.Join(", ", rounds), string.Join(", ", events), string.Join(", ", captures), performance); Application.targetFrameRate = -1; } static bool InCombat(LuneMatchRunner runner, int round, double from) { return runner.Current == LuneMatchRunner.Stage.Combat && runner.Match.RoundNumber == round && runner.Match.Combat.Elapsed >= from; } static LuneUnit Find(LuneMatchRunner runner, Func predicate) { return runner.Match.Combat == null ? null : runner.Match.Combat.Units.FirstOrDefault(predicate); } static Shot Building(string label, Func pick, LuneBuildingView.Look look, float zoom, float delay) { LuneBuildingView chosen = null; return new Shot { Label = label, Delay = delay, Trigger = (runner, view) => { chosen = pick(view); return chosen != null && chosen.Current == look; }, Aim = (runner, view, rig) => { // Towers are watched from their lane (open ground), not through the forest. Vec2 at = LuneSpace.Map(chosen.transform.position); BuildingDef definition = runner.Match.Map.BuildingDef(chosen.BuildingId); float yaw = 0f; if (!chosen.IsAncient && definition.Tier != "base") yaw = LuneCameraRig.YawTowards(at, runner.Match.Map.LanePoint(definition.Team, definition.Lane, definition.Progress)); rig.SetYaw(yaw, true); rig.LookAt(at, zoom, true); }, }; } static List SeedTwoShots() { string fallen = null; return new List { new Shot { Label = "01-runde1-aufmarsch", Trigger = (r, v) => InCombat(r, 1, 9.0), Aim = (r, v, rig) => rig.LookAt(new Vec2(0, 0), 64f, true) }, new Shot { Label = "02-runde1-mid-kampf", Trigger = (r, v) => InCombat(r, 1, 24.0), Aim = (r, v, rig) => { LuneUnit fighter = Find(r, u => u.IsMobile() && u.Alive && u.Lane == "mid" && u.Target != null && u.InRange); rig.LookAt(fighter != null ? fighter.Position : new Vec2(0, 0), 20f, true); } }, Building("03-turm-faellt", v => AnyBuilding(v, b => !b.IsAncient && b.Current == LuneBuildingView.Look.Destroying, ref fallen), LuneBuildingView.Look.Destroying, 22f, .7f), Building("04-turm-ruine", v => fallen != null ? v.Building(fallen) : null, LuneBuildingView.Look.Ruin, 22f, .4f), new Shot { Label = "05-grotte-bosskampf", Delay = .5f, Trigger = (r, v) => Find(r, u => u.Kind == "boss" && u.Zone == "grotto" && u.Alive && !u.Dormant) != null, Aim = (r, v, rig) => rig.LookAt(LuneMap.Grotto, 20f, true) }, new Shot { Label = "06-held-kaempft", Delay = .3f, Trigger = (r, v) => Find(r, u => u.Kind == "hero" && u.Alive && u.Zone == "lane" && u.Target != null && u.InRange) != null, Aim = (r, v, rig) => rig.LookAt(Find(r, u => u.Kind == "hero" && u.Alive && u.Zone == "lane" && u.Target != null && u.InRange).Position, 16f, true) }, new Shot { Label = "07-boss-auf-der-lane", Delay = .3f, Trigger = (r, v) => r.Current == LuneMatchRunner.Stage.Combat && r.Match.Combat.Elapsed > 12.0 && Find(r, u => u.Kind == "boss" && u.Zone == "lane" && u.Alive) != null, Aim = (r, v, rig) => rig.LookAt(Find(r, u => u.Kind == "boss" && u.Zone == "lane" && u.Alive).Position, 22f, true) }, Building("08-ancient-faellt", v => v.Building("blau_ancient"), LuneBuildingView.Look.Destroying, 30f, .8f), Building("09-ancient-ruine", v => v.Building("blau_ancient"), LuneBuildingView.Look.Ruin, 30f, .5f), new Shot { Label = "10-ende-uebersicht", Delay = .5f, Trigger = (r, v) => r.Current == LuneMatchRunner.Stage.Over && v.Building("blau_ancient").Current == LuneBuildingView.Look.Ruin, Aim = (r, v, rig) => rig.Overview(true) }, }; } /// The first building that satisfies the condition; remembers its id. static LuneBuildingView AnyBuilding(LuneMatchView view, Func condition, ref string remembered) { foreach (LuneBuildingView building in UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.None).OrderBy(b => b.BuildingId, StringComparer.Ordinal)) { if (condition(building)) { remembered = building.BuildingId; return building; } } return null; } static List WaveShots() { return new List { new Shot { Label = "11-welle21-uebersicht", Trigger = (r, v) => InCombat(r, 21, 6.0), Aim = (r, v, rig) => rig.LookAt(new Vec2(0, 0), 70f, true) }, new Shot { Label = "12-welle21-kampf", Trigger = (r, v) => InCombat(r, 21, 36.0), Aim = (r, v, rig) => { LuneUnit fighter = Find(r, u => u.IsMobile() && u.Alive && u.Target != null && u.InRange && u.Zone == "lane"); rig.LookAt(fighter != null ? fighter.Position : new Vec2(0, 0), 24f, true); } }, Building("13-ancient-rot-faellt", v => v.Building("rot_ancient"), LuneBuildingView.Look.Destroying, 30f, .8f), new Shot { Label = "14-welle-ende", Delay = .6f, Trigger = (r, v) => r.Current == LuneMatchRunner.Stage.Over && v.Building("rot_ancient").Current == LuneBuildingView.Look.Ruin, Aim = (r, v, rig) => rig.LookAt(r.Match.Map.CorePosition(1), 34f, true) }, }; } } }