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/.
///
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 = Path.Combine(PlayModeSupport.Root, "docs/port/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.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.Root, "docs/port/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) },
};
}
}
}