using System; using System.Collections.Generic; using System.Linq; namespace Luneraria.Core.Tests { /// /// Runs the operations of Golden/scenarios.json against the C# core and records snapshots in /// exactly the shape tools/godot-golden/golden_scenarios.gd writes for the Godot reference. /// public sealed class GoldenReplay { public LuneMatch State; public readonly List Snapshots = new List(); public readonly List Results = new List(); public void Apply(OrderedMap op) { switch (MiniJson.Str(op["op"])) { case "setup": { State = new LuneMatch(); var options = new MatchOptions { Seed = MiniJson.Int(op["seed"]), HumanSlot = op.ContainsKey("human") ? MiniJson.Int(op["human"]) : -1 }; if (op.ContainsKey("bunds")) options.Bunds = MiniJson.Arr(op["bunds"]).Select(MiniJson.Str).ToList(); if (op.ContainsKey("profiles")) options.Profiles = MiniJson.Arr(op["profiles"]).Select(MiniJson.Str).ToList(); State.Setup(options); break; } case "building": State.BuildingHp[MiniJson.Str(op["id"])] = MiniJson.Num(op["hp"]); break; case "buildings_full": foreach (BuildingDef definition in State.Map.Buildings) State.BuildingHp[definition.Id] = definition.MaxHp; break; case "round": State.RoundNumber = MiniJson.Int(op["value"]); break; case "gold": State.Players[MiniJson.Int(op["slot"])].Draft.Gold += MiniJson.Int(op["add"]); break; case "gold_all": foreach (Player player in State.Players) player.Draft.Gold += MiniJson.Int(op["add"]); break; case "cmd": { int slot = MiniJson.Int(op["slot"]); Command command = Resolve(MiniJson.Obj(op["command"])); CommandResult result = State.Submit(slot, command); Results.Add(new List { "cmd", (long)slot, command.Type, result.Ok, result.Message }); break; } case "bots": for (int slot = 0; slot < 6; slot++) LuneBot.Plan(State, slot, op.ContainsKey("profile") ? MiniJson.Str(op["profile"]) : ""); break; case "begin": Results.Add(new List { "begin", -1L, "", State.BeginCombat(), "" }); break; case "steps": for (int i = 0; i < MiniJson.Int(op["n"]); i++) { if (State.Phase != Phase.Combat) break; State.StepCombat(); } break; case "finish": State.FinishCombat(); break; case "unit_set": { var select = MiniJson.Obj(op["select"]); foreach (LuneUnit unit in State.Combat.Units) { if (select.ContainsKey("kind") && unit.Kind != MiniJson.Str(select["kind"])) continue; if (select.ContainsKey("zone") && unit.Zone != MiniJson.Str(select["zone"])) continue; if (select.ContainsKey("team") && unit.Team != MiniJson.Int(select["team"])) continue; if (op.ContainsKey("hp")) unit.Hp = MiniJson.Num(op["hp"]); } break; } case "settle_boss": Results.Add(new List { "settle_boss", -1L, "", true, State.SettleBoss() }); break; case "expedition": { int slot = MiniJson.Int(op["slot"]); State.Players[slot].Draft.Gold += 6000; List ids = State.BaseIds(State.Players[slot].Bund); foreach (object typeIndex in MiniJson.Arr(op["types"])) { Record(slot, Command.Buy(ids[MiniJson.Int(typeIndex)])); RosterEntry entry = State.Players[slot].Draft.Roster[State.Players[slot].Draft.Roster.Count - 1]; Record(slot, Command.Upgrade(entry.Uid)); Record(slot, Command.Assign(entry.Uid, "grotto")); } if (op.ContainsKey("hero")) { Record(slot, Command.HeroBuy(MiniJson.Str(op["hero"]))); Record(slot, Command.Assign(State.Players[slot].Draft.Hero.Uid, "grotto")); } break; } case "play": { bool bots = !op.ContainsKey("bots") || (bool)op["bots"]; string profile = op.ContainsKey("profile") ? MiniJson.Str(op["profile"]) : ""; for (int i = 0; i < MiniJson.Int(op["rounds"]); i++) { if (State.Phase == Phase.Over) break; if (bots) { for (int slot = 0; slot < 6; slot++) LuneBot.Plan(State, slot, profile); } int fought = State.RoundNumber; State.BeginCombat(); State.FinishCombat(); var label = new OrderedMap(); label["label"] = "round-" + fought; Snapshot(label); } break; } case "snapshot": Snapshot(op); break; default: throw new InvalidOperationException("unknown op " + MiniJson.Str(op["op"])); } } void Record(int slot, Command command) { CommandResult result = State.Submit(slot, command); Results.Add(new List { "cmd", (long)slot, command.Type, result.Ok, result.Message }); } Command Resolve(OrderedMap source) { var command = new Command { Type = MiniJson.Str(source["type"]) }; foreach (KeyValuePair pair in source) { switch (pair.Key) { case "type_id": command.TypeId = MiniJson.Str(pair.Value); break; case "lane": command.Lane = MiniJson.Str(pair.Value); break; case "uid": command.Uid = ResolveUid(MiniJson.Str(pair.Value)); break; case "hero_id": command.HeroId = MiniJson.Str(pair.Value); break; case "item_id": command.ItemId = MiniJson.Str(pair.Value); break; case "champion_id": command.ChampionId = MiniJson.Str(pair.Value); break; case "tier": command.Tier = MiniJson.Int(pair.Value); break; case "choice": command.Choice = MiniJson.Int(pair.Value); break; } } return command; } string ResolveUid(string uid) { if (!uid.StartsWith("$", StringComparison.Ordinal)) return uid; string[] parts = uid.Substring(1).Split(':'); Player owner = State.Players[int.Parse(parts[1])]; switch (parts[0]) { case "last": return owner.Draft.Roster[owner.Draft.Roster.Count - 1].Uid; case "roster": return owner.Draft.Roster[int.Parse(parts[2])].Uid; case "hero": return owner.Draft.Hero.Uid; case "booking": return owner.Draft.NewBookings[int.Parse(parts[2])].Uid; } return uid; } // ---------------------------------------------------------------- snapshot (same shape as the Godot tool) static string Bool(bool value) { return value ? "true" : "false"; } void Snapshot(OrderedMap op) { var players = new List(); foreach (Player player in State.Players) { var entry = new OrderedMap(); entry["gold"] = (long)player.Gold; entry["draft_gold"] = (long)player.Draft.Gold; entry["ready"] = player.Ready; entry["earned"] = (long)player.Earned; entry["spent"] = (long)player.Spent; entry["next_seq"] = (long)player.NextSeq; entry["roster"] = Roster(player.Roster, false); entry["hero"] = Hero(player.Hero); entry["bookings"] = player.Bookings.Select(b => (object)(b.Uid + "|" + b.ChampionId + "|" + b.Lane + "|" + b.Cost + "|" + b.DeployRound)).ToList(); entry["new_bookings"] = player.Draft.NewBookings.Select(b => (object)(b.Uid + "|" + b.ChampionId + "|" + b.Lane + "|" + b.Cost)).ToList(); entry["draft_roster"] = Roster(player.Draft.Roster, true); entry["draft_hero"] = Hero(player.Draft.Hero); entry["boss_lane"] = player.Draft.BossLane; entry["undo"] = (long)player.Undo.Count; players.Add(entry); } var snap = new OrderedMap(); snap["label"] = MiniJson.Str(op["label"]); snap["phase"] = (long)(int)State.Phase; snap["round"] = (long)State.RoundNumber; snap["winner"] = (long)State.Winner; snap["end_note"] = State.EndNote; snap["players"] = players; snap["buildings"] = State.Map.Buildings.Select(d => (object)State.BuildingHp[d.Id]).ToList(); var boss = new OrderedMap(); boss["next"] = (long)State.Boss.NextAvailableRound; boss["settled"] = (long)State.Boss.SettledRound; BossClaim claim = State.Boss.Claim; boss["claim"] = claim == null ? "" : claim.Team + "|" + claim.WonRound + "|" + claim.DeployRound + "|" + claim.ChooserSlot + "|" + claim.Lane; snap["boss"] = boss; snap["boss_log"] = State.BossLog.Select(e => (object)(e.Round + "|" + e.Event + "|" + e.Team + "|" + e.Lane + "|" + e.Chooser)).ToList(); snap["announcements"] = State.Announcements().Select(a => (object)(a.Kind + "|" + a.Team + "|" + a.Slot + "|" + a.Lane + "|" + a.ChampionId + "|" + a.Bounty)).ToList(); snap["settlement"] = SettlementSnapshot(); snap["combat"] = CombatSnapshot(); if (op.ContainsKey("units") && (bool)op["units"] && State.Combat != null) snap["units"] = State.Combat.Units.Select(u => (object)UnitSnapshot(u)).ToList(); if (op.ContainsKey("events") && (bool)op["events"] && State.Combat != null) { snap["events"] = State.Combat.Events.Select(e => (object)new List { (long)e.Serial, (long)e.Step, e.Time, (long)e.Team, e.Lane, e.Zone, e.Kind, (long)e.UnitId, e.DefId, (long)e.Bounty, e.BuildingId, (long)e.Xp, e.XpHeroes.Select(h => (object)h).ToList() }).ToList(); } if (op.ContainsKey("wave") && (bool)op["wave"]) snap["wave"] = LuneWaves.Build(State.Balance, State.RoundNumber).Select(m => (object)new List { m.Id, m.MaxHp, m.Damage, (long)m.Bounty }).ToList(); Snapshots.Add(snap); } static List Roster(List roster, bool withFresh) { return roster.Select(e => (object)(e.Uid + "|" + e.TypeId + "|" + e.Lane + "|" + e.Invested + (withFresh ? "|" + Bool(e.Fresh) : ""))).ToList(); } static string Hero(HeroState hero) { if (hero == null) return ""; return hero.Uid + "|" + hero.HeroId + "|" + hero.Xp + "|" + hero.Lane + "|" + string.Join("+", hero.Items) + "|" + string.Join("+", hero.Talents) + "|" + hero.Invested + "|" + Bool(hero.Fresh) + "|" + string.Join("+", hero.FreshItems); } object SettlementSnapshot() { Settlement s = State.LastSettlement; if (s == null) return null; var result = new OrderedMap(); result["round"] = (long)s.Round; result["gains"] = s.Gains.Select(g => (object)(long)g).ToList(); result["income"] = (long)s.Income; result["lost"] = s.BuildingsLost.Select(b => (object)b).ToList(); result["end_reason"] = s.EndReason; result["combat_time"] = s.CombatTime; result["boss"] = s.Boss; var laneKills = new OrderedMap(); foreach (KeyValuePair pair in s.LaneKills) laneKills[pair.Key] = (long)pair.Value; result["lane_kills"] = laneKills; var xp = new OrderedMap(); foreach (KeyValuePair pair in s.Xp) xp[pair.Key] = (long)pair.Value; result["xp"] = xp; return result; } object CombatSnapshot() { LuneCombat c = State.Combat; if (c == null) return null; var result = new OrderedMap(); result["round"] = (long)c.RoundNumber; result["steps"] = (long)c.Steps; result["elapsed"] = c.Elapsed; result["finished"] = c.Finished; result["end_reason"] = c.EndReason; result["destroyed"] = c.DestroyedCores.Select(t => (object)(long)t).ToList(); result["grotto"] = c.GrottoOutcome + "|" + c.GrottoClaimTeam + "|" + c.GrottoActiveTeam + "|" + Bool(c.GrottoResolved); result["grotto_boss"] = c.GrottoBoss == null ? "" : Bool(c.GrottoBoss.Alive) + "|" + Bool(c.GrottoBoss.Dormant) + "|" + c.GrottoBoss.Id; result["grotto_boss_hp"] = c.GrottoBoss != null ? c.GrottoBoss.Hp : 0.0; result["events"] = (long)c.Events.Count; result["mobile"] = new List { (long)c.MobileCount(0), (long)c.MobileCount(1), (long)c.MobileCount(2) }; return result; } static OrderedMap UnitSnapshot(LuneUnit unit) { var result = new OrderedMap(); result["id"] = (long)unit.Id; result["kind"] = unit.Kind; result["team"] = (long)unit.Team; result["lane"] = unit.Lane; result["zone"] = unit.Zone; result["slot"] = (long)unit.OwnerSlot; result["uid"] = unit.PersistentUid; result["def"] = unit.Def.Id; result["alive"] = unit.Alive; result["hp"] = unit.Hp; result["max_hp"] = unit.MaxHp; result["shield"] = unit.Shield; result["x"] = (double)unit.Position.X; result["y"] = (double)unit.Position.Y; result["facing"] = unit.Facing; result["progress"] = unit.Progress; result["target"] = (long)(unit.Target != null ? unit.Target.Id : -1); result["hits"] = (long)unit.HitCount; result["reached"] = unit.ReachedBase; result["dormant"] = unit.Dormant; result["effects"] = unit.Effects.Select(p => (object)new List { p.Key, p.Value.Kind, p.Value.Amount, p.Value.Remaining }).ToList(); result["dots"] = unit.Dots.Select(p => (object)new List { p.Key, p.Value.Damage, p.Value.Remaining, p.Value.NextTick, (long)p.Value.AttackType }).ToList(); return result; } } }