using System.Collections.Generic; using System.Linq; using Luneraria.Core; using Luneraria.View; using NUnit.Framework; namespace Luneraria.UI.Tests { /// /// U3: the HUD's view model (LuneHudModel, LuneLaneThreat, LuneStats) against the rules of the /// G3 reference (lune_hud_model.gd, lune_lane_threat.gd, verify_g3 parts B, D and E at model /// level): privacy of gold, drafts and bookings, the same public picture for every seat, lane /// threat levels, boss states, the coming wave, number formats, and the human seat of the host /// loop (planning waits for Ready; bot matches unchanged). /// public sealed class HudModelTests { const int Human = 1; static LuneMatch Fresh(int seed = 4, int human = Human) { var match = new LuneMatch(); match.Setup(new MatchOptions { Seed = seed, HumanSlot = human }); for (int slot = 0; slot < 6; slot++) { if (slot != human) LuneBot.Plan(match, slot, ""); } return match; } static string Digest(LuneMatch match, int viewer) { return new LuneHudModel(match, viewer).PublicDigest(); } static void AssertSameForAll(LuneMatch match, string moment) { string reference = Digest(match, 0); for (int viewer = 1; viewer < 6; viewer++) Assert.AreEqual(reference, Digest(match, viewer), moment + ": public picture of seat " + viewer); } // ---------------------------------------------------------------- privacy (verify_g3 part E) [Test] public void EverySeatSeesTheSamePublicPicture() { LuneMatch match = Fresh(); AssertSameForAll(match, "planning round 1"); match.SetReady(Human); match.BeginCombat(); for (int step = 0; step < 400 && match.Phase == Phase.Combat; step++) { match.StepCombat(); if (step % 100 == 0) AssertSameForAll(match, "combat step " + step); } } [Test] public void OtherSeatsPlansStayPrivateUntilCombat() { LuneMatch match = Fresh(); match.SetReady(Human); match.BeginCombat(); while (match.Phase == Phase.Combat) match.StepCombat(); Assert.AreEqual(Phase.Planning, match.Phase, "the first wave must not end the match"); for (int slot = 0; slot < 6; slot++) { if (slot != Human) LuneBot.Plan(match, slot, ""); } var viewer = new LuneHudModel(match, Human); string before = viewer.PublicDigest(); string membersBefore = string.Join("|", viewer.Members(1)); // An enemy recruits a hero and books an attack champion on Mid: both stay private. int enemy = Enumerable.Range(3, 3).First(slot => match.Players[slot].Draft.NewBookings.Count < LuneContent.MaxBookings); match.Players[enemy].Draft.Gold += 2000; if (match.Players[enemy].Draft.Hero == null) Assert.IsTrue(match.Submit(enemy, Command.HeroBuy(LuneContent.Heroes.Keys[0])).Ok, "hero recruit"); string championId = LuneContent.Champions.Keys[0]; CommandResult booking = match.Submit(enemy, Command.ChampionBook(championId, "mid")); Assert.IsTrue(booking.Ok, "champion booking: " + booking.Message); Assert.AreEqual(before, viewer.PublicDigest(), "an enemy draft changed the public picture (buildings, lanes, seats, boss, wave)"); Assert.AreEqual(membersBefore, string.Join("|", viewer.Members(1)), "an enemy draft hero is visible before combat"); Assert.AreEqual(match.GoldOf(Human), viewer.OwnGold, "own gold"); // The enemy sees its own draft: gold, booking and hero. var enemyView = new LuneHudModel(match, enemy); Assert.AreEqual(match.Players[enemy].Draft.Gold, enemyView.OwnGold, "enemy's own gold"); Assert.IsTrue(enemyView.OwnBookings().Any(b => b.Fresh && b.ChampionId == championId), "own fresh booking"); Assert.IsNotNull(enemyView.OwnHero, "own draft hero"); } [Test] public void BossLaneChoiceIsPrivateToTheChooser() { LuneMatch match = Fresh(); match.RoundNumber = 4; match.Boss.Claim = new BossClaim { Team = 1, WonRound = 3, DeployRound = 5, ChooserSlot = 4, Lane = "top" }; match.Players[4].Draft.BossLane = "bot"; BossInfoCheck(new LuneHudModel(match, 4).Boss(), "deploy_next", "bot", true, true); BossInfoCheck(new LuneHudModel(match, Human).Boss(), "deploy_next", "top", false, false); AssertSameForAll(match, "boss claim with a private lane choice"); } static void BossInfoCheck(LuneHudModel.BossInfo boss, string state, string lane, bool own, bool chooser) { Assert.AreEqual(state, boss.State); Assert.AreEqual(lane, boss.Lane); Assert.AreEqual(own, boss.Own); Assert.AreEqual(chooser, boss.Chooser); } // ---------------------------------------------------------------- lane threat static LuneLaneThreat.Result Threat(LuneMatch match, int team, string lane) { return LuneLaneThreat.Evaluate(match, team, lane); } [Test] public void LaneThreatFollowsTowerHealth() { LuneMatch match = Fresh(); foreach (string lane in LuneMap.Lanes) { for (int team = 0; team < 2; team++) Assert.AreEqual(LuneLaneThreat.Calm, Threat(match, team, lane).Level, lane + "/" + team + " at the start"); } double outer = match.Map.BuildingDef("blau_mid_outer").MaxHp; double inner = match.Map.BuildingDef("blau_mid_inner").MaxHp; match.BuildingHp["blau_mid_outer"] = outer * .8; LuneLaneThreat.Result result = Threat(match, 0, "mid"); Assert.AreEqual(LuneLaneThreat.Contested, result.Level); CollectionAssert.AreEqual(new[] { "Grenzwacht bei 80 %" }, result.Reasons); Assert.AreEqual(LuneLaneThreat.Calm, Threat(match, 1, "mid").Level, "the enemy's view of its own half"); match.BuildingHp["blau_mid_outer"] = outer * .4; Assert.AreEqual(LuneLaneThreat.Pressure, Threat(match, 0, "mid").Level); match.BuildingHp["blau_mid_outer"] = 0.0; result = Threat(match, 0, "mid"); Assert.AreEqual(LuneLaneThreat.Pressure, result.Level); CollectionAssert.AreEqual(new[] { "Grenzwacht gefallen" }, result.Reasons); match.BuildingHp["blau_mid_inner"] = inner * .7; CollectionAssert.AreEqual(new[] { "Grenzwacht gefallen", "Bastion bei 70 %" }, Threat(match, 0, "mid").Reasons); match.BuildingHp["blau_mid_inner"] = inner * .4; Assert.AreEqual(LuneLaneThreat.Critical, Threat(match, 0, "mid").Level); match.BuildingHp["blau_mid_inner"] = 0.0; result = Threat(match, 0, "mid"); Assert.AreEqual(LuneLaneThreat.Critical, result.Level); Assert.AreEqual("KRITISCH", result.Name); } [Test] public void AnnouncedDangersRaiseTheThreat() { LuneMatch match = Fresh(); string championId = LuneContent.Champions.Keys[0]; match.Players[4].Bookings.Add(new Booking { Uid = "test-b1", ChampionId = championId, Lane = "mid", DeployRound = match.RoundNumber, Cost = 0 }); LuneLaneThreat.Result result = Threat(match, 0, "mid"); Assert.AreEqual(LuneLaneThreat.Contested, result.Level); CollectionAssert.AreEqual(new[] { "Angriffschampion " + LuneContent.Champions[championId].Name + " angekündigt" }, result.Reasons); Assert.AreEqual(LuneLaneThreat.Calm, Threat(match, 1, "mid").Level, "an own champion is no threat"); match.Boss.Claim = new BossClaim { Team = 1, WonRound = 1, DeployRound = match.RoundNumber, ChooserSlot = 4, Lane = "top" }; Assert.AreEqual(LuneLaneThreat.Pressure, Threat(match, 0, "top").Level, "enemy boss with the Grenzwacht standing"); match.BuildingHp["blau_top_outer"] = 0.0; Assert.AreEqual(LuneLaneThreat.Critical, Threat(match, 0, "top").Level, "enemy boss after the Grenzwacht fell"); var model = new LuneHudModel(match, Human); Assert.AreEqual(1, model.Lane("mid").Dangers.Count, "danger listed in the lane"); LuneHudModel.WaveInfo wave = model.NextWave(); Assert.AreEqual(1, wave.Champions.Count); Assert.IsFalse(wave.Champions[0].Own); Assert.IsNotNull(wave.Boss); Assert.AreEqual("top", wave.Boss.Lane); } [Test] public void BattlefieldCountsTheLaneUnits() { LuneMatch match = Fresh(); match.SetReady(Human); match.BeginCombat(); for (int step = 0; step < 300; step++) match.StepCombat(); var model = new LuneHudModel(match, Human); foreach (string lane in LuneMap.Lanes) { int own = match.Combat.Units.Count(u => u.Alive && u.IsMobile() && u.Zone == "lane" && u.Lane == lane && u.Team == 0); int enemy = match.Combat.Units.Count(u => u.Alive && u.IsMobile() && u.Zone == "lane" && u.Lane == lane && u.Team == 1); LuneHudModel.LaneInfo info = model.Lane(lane); Assert.IsTrue(info.HasCounts); Assert.AreEqual(own, info.OwnCount, lane + " own units"); Assert.AreEqual(enemy, info.EnemyCount, lane + " enemy units"); Assert.AreEqual(4, info.Towers.Length); } } // ---------------------------------------------------------------- boss and waves [Test] public void BossStatesFromPublicState() { LuneMatch match = Fresh(); Assert.AreEqual("available", new LuneHudModel(match, Human).Boss().State, "round 1"); match.RoundNumber = 2; match.Boss.NextAvailableRound = 5; LuneHudModel.BossInfo boss = new LuneHudModel(match, Human).Boss(); Assert.AreEqual("cooldown", boss.State); Assert.AreEqual(3, boss.RoundsLeft); match.Boss.Claim = new BossClaim { Team = 0, WonRound = 2, DeployRound = 3, ChooserSlot = 0, Lane = "bot" }; boss = new LuneHudModel(match, Human).Boss(); Assert.AreEqual("deploy_next", boss.State); Assert.IsTrue(boss.Own); Assert.AreEqual(LuneContent.BossBountyFor(3), boss.Bounty); Assert.IsFalse(new LuneHudModel(match, 4).Boss().Own, "the enemy's view"); } [Test] public void NextWaveIsTheRulesWave() { LuneMatch match = Fresh(); for (int round = 1; round <= 24; round++) { match.RoundNumber = round; LuneHudModel.WaveInfo wave = new LuneHudModel(match, Human).NextWave(); List rules = LuneWaves.Build(match.Balance, round); Assert.AreEqual(round, wave.Round); Assert.AreEqual(rules.Count, wave.Count); Assert.AreEqual(rules.Count, wave.Monsters.Sum(m => m.Count), "round " + round); List before = round > 1 ? LuneWaves.UnlockedTypes(match.Balance, round - 1) : new List(); foreach (LuneHudModel.MonsterGroup group in wave.Monsters) Assert.AreEqual(round > 1 && !before.Contains(group.Id), group.New, "round " + round + " " + group.Id); } } [Test] public void ReadyLocksThePlan() { LuneMatch match = Fresh(); var model = new LuneHudModel(match, Human); Assert.IsFalse(model.Locked); Assert.IsFalse(model.CanUndo, "nothing to undo yet"); Assert.IsTrue(match.Submit(Human, Command.Buy(match.BaseIds(match.Players[Human].Bund)[0])).Ok); Assert.IsTrue(model.CanUndo); match.SetReady(Human); Assert.IsTrue(model.Locked); Assert.IsFalse(model.CanUndo, "undo after Ready"); model.ReadyCount(out int ready, out int humans); Assert.AreEqual(1, humans); Assert.AreEqual(1, ready); Assert.AreEqual("self", model.StatusOf(Human)); Assert.AreEqual("bot", model.StatusOf(0)); } // ---------------------------------------------------------------- formats [Test] public void NumbersAreFormattedAsInGodot() { Assert.AreEqual("12.000", LuneUiTheme.Number(12000)); Assert.AreEqual("999", LuneUiTheme.Number(999)); Assert.AreEqual("-1.500", LuneUiTheme.Number(-1500)); Assert.AreEqual("1.234.568", LuneUiTheme.Number(1234567.5)); Assert.AreEqual("1,25", LuneUiTheme.Decimal(1.25, 2)); Assert.AreEqual("2,0", LuneUiTheme.Decimal(2.0)); Assert.AreEqual("Leicht", LuneStats.StrongAgainst(0)); Assert.AreEqual("Schwer", LuneStats.WeakAgainst(0)); Assert.AreEqual(1, LuneStats.StageOf("g01")); Assert.AreEqual(2, LuneStats.StageOf("w04")); Assert.AreEqual("Grenzwacht", LuneHudModel.BuildingTitle("blau_mid_outer")); Assert.AreEqual("Kernwacht", LuneHudModel.BuildingTitle("rot_base_2")); Assert.AreEqual("MID · Bastion Rot", LuneHudModel.BuildingPlace("rot_mid_inner")); Assert.AreEqual("Basis Blau · Kernwacht 1", LuneHudModel.BuildingPlace("blau_base_1")); } // ---------------------------------------------------------------- the human seat of the host loop [Test] public void HumanSeatPlanningWaitsForReady() { var runner = new LuneMatchRunner(new MatchOptions { Seed = 4, HumanSlot = Human }); Assert.AreEqual(Human, runner.HumanSlot); for (int frame = 0; frame < 600; frame++) runner.Advance(1.0 / 60.0); Assert.AreEqual(LuneMatchRunner.Stage.Planning, runner.Current, "planning waits for the human"); Assert.IsFalse(runner.Match.Players[Human].Ready); Assert.IsEmpty(runner.Match.Players[Human].Draft.Roster, "the bots never plan the human seat"); for (int slot = 0; slot < 6; slot++) { if (slot != Human) Assert.IsTrue(runner.Match.Players[slot].Ready, "bot seat " + slot + " ready"); } Assert.IsTrue(runner.Ready()); Assert.AreEqual(LuneMatchRunner.Stage.Combat, runner.Current, "Ready starts the combat when everyone is ready"); int frames = 0; while (runner.Current == LuneMatchRunner.Stage.Combat && frames++ < 100000) runner.Advance(1.0 / 60.0); runner.Advance(1.0 / 60.0); Assert.AreEqual(LuneMatchRunner.Stage.Planning, runner.Current, "planning follows the settlement at once"); Assert.IsFalse(runner.Match.Players[Human].Ready, "the human plans the next round again"); } [Test] public void BotMatchStillStartsByItself() { var runner = new LuneMatchRunner(new MatchOptions { Seed = 4, HumanSlot = -1 }); Assert.AreEqual(-1, runner.HumanSlot); Assert.IsFalse(runner.Ready(), "no human seat"); runner.Advance(runner.PlanningSeconds + .01); Assert.AreEqual(LuneMatchRunner.Stage.Combat, runner.Current); } } }