using System.Collections.Generic; using System.Linq; namespace Luneraria.Core.Tests { /// /// S2: glyph (Dota 2), item recipes (Dota 2 / League of Legends), Ancient levels (Legion TD 2 /// king) and the bots' grotto hunt under the extended rules; none of it under the reference rules. /// public sealed class S2RulesSuite : CheckSuite { public override string Name { get { return "s2"; } } protected override void RunChecks() { TestReference(); TestGlyph(); TestRecipes(); TestCore(); TestBots(); } static LuneMatch NewMatch(bool extended, int seed = 51) { var state = new LuneMatch(); state.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Extended = extended, Bunds = new List { "glutpakt", "wurzelbund", "glutpakt", "wurzelbund", "glutpakt", "wurzelbund" } }); return state; } void TestReference() { LuneMatch state = NewMatch(false); state.Players[0].Draft.Gold += 3000; Check(!state.Submit(0, Command.Glyph("mid")).Ok, "reference: no glyph"); Check(!state.Submit(0, Command.CoreUpgrade()).Ok, "reference: no Ancient levels"); state.Submit(0, Command.HeroBuy("held_ringborke")); state.Submit(0, Command.ItemBuy("basaltschild")); state.Submit(0, Command.ItemBuy("rankenmantel")); Check(!state.Submit(0, Command.ItemBuy("bollwerk")).Ok, "reference: no recipes"); state.BeginCombat(); Check(state.Combat.Buildings.Values.All(b => b.Kind != "core" || b.Def.Damage == 0.0), "reference: the Ancients do not shoot"); } void TestGlyph() { LuneMatch state = NewMatch(true); Check(!state.Submit(0, Command.Glyph("river")).Ok, "unknown lane rejected"); Check(state.Submit(1, Command.Glyph("mid")).Ok && state.Players[1].Draft.GlyphLane == "mid", "a player holds the team glyph for a lane"); state.Submit(0, Command.Glyph("top")); state.BeginCombat(); Check(state.Combat.GlyphLane[0] == "top", "the first seat in slot order decides the team glyph"); LuneUnit tower = state.Combat.Buildings["blau_top_outer"]; tower.Hp = tower.MaxHp * 0.4; state.Combat.Step(); Check(state.Combat.GlyphUsed[0] && state.Combat.Glyphed(tower), "the glyph fires once a tower of the lane is below half health"); double before = tower.Hp; var gates = state.Combat.AliveMap(); state.Combat.QueueDamage(tower, 500.0, gates); Check(!state.Combat.HasPendingDamage(tower), "glyphed towers take no damage"); Check(!state.Combat.Glyphed(state.Combat.Buildings["blau_mid_outer"]), "other lanes are not protected"); for (int i = 0; i < 120; i++) state.Combat.Step(); Check(!state.Combat.Glyphed(tower), "the glyph ends after 5 s"); state.FinishCombat(); Check(state.Boss.GlyphReady[0] == state.RoundNumber - 1 + 1 + 4, "a fired glyph returns after 4 rounds (" + state.Boss.GlyphReady[0] + ")"); Check(!state.Submit(0, Command.Glyph("top")).Ok, "on cooldown the glyph cannot be held"); Check(state.Boss.GlyphReady[1] == 1, "the other team's glyph is untouched"); LuneMatch idle = NewMatch(true); idle.Submit(2, Command.Glyph("bot")); idle.BeginCombat(); idle.FinishCombat(); Check(idle.Combat.GlyphUsed[0] || idle.Boss.GlyphReady[0] == 1, "a glyph that never fired costs no cooldown"); } void TestRecipes() { LuneMatch state = NewMatch(true); state.Players[0].Draft.Gold += 3000; state.Submit(0, Command.HeroBuy("held_ringborke")); Check(!state.Submit(0, Command.ItemBuy("bollwerk")).Ok, "a recipe needs its parts"); state.Submit(0, Command.ItemBuy("basaltschild")); state.Submit(0, Command.ItemBuy("rankenmantel")); state.Submit(0, Command.ItemBuy("glutklinge")); int gold = state.GoldOf(0); Check(state.Submit(0, Command.ItemBuy("bollwerk")).Ok && state.GoldOf(0) == gold - 120, "combining costs the fee of 120"); HeroState hero = state.Players[0].Draft.Hero; Check(hero.Items.SequenceEqual(new[] { "bollwerk", "glutklinge" }), "parts become the recipe item, a slot is free"); UnitDef definition = LuneContent.HeroDefinition(hero); Check(GdMath.IsEqualApprox(definition.MaxHp, 2200.0 + 600.0) && GdMath.IsEqualApprox(definition.ReductionBonus, 0.15), "Bollwerk: +600 health, +15 % reduction"); Check(state.Undo(0).Ok && state.Players[0].Draft.Hero.Items.Count == 3, "combining can be undone"); state.Submit(0, Command.ItemBuy("bollwerk")); Check(!state.Submit(0, Command.ItemBuy("splitterkrone")).Ok, "Splitterkrone needs its parts and level 5"); state.BeginCombat(); state.FinishCombat(); int sellGold = state.GoldOf(0); Check(state.Submit(0, Command.ItemSell("bollwerk")).Ok && state.GoldOf(0) == sellGold + 250, "a recipe item sells for half its total (250)"); } void TestCore() { LuneMatch state = NewMatch(true); state.Players[0].Draft.Gold += 3000; state.Players[1].Draft.Gold += 3000; state.BuildingHp["blau_ancient"] = 5000.0; int gold = state.GoldOf(0); Check(state.Submit(0, Command.CoreUpgrade()).Ok && state.GoldOf(0) == gold - 150, "the first Ancient level costs 150"); Check(state.Submit(0, Command.CoreUpgrade()).Ok && state.GoldOf(0) == gold - 450, "the second costs 300"); Check(state.Submit(1, Command.CoreUpgrade()).Ok && state.Submit(1, Command.CoreUpgrade()).Ok, "a teammate buys too (priced by the committed level)"); state.BeginCombat(); Check(state.Boss.CoreLevel[0] == 3 && state.Boss.CoreLevel[1] == 0, "levels are capped at 3 for the team"); Check(state.Players[1].Gold > 0, "the teammate's excess level is refunded"); LuneUnit core = state.Combat.Buildings["blau_ancient"]; Check(GdMath.IsEqualApprox(core.Def.Damage, 110.0) && GdMath.IsEqualApprox(core.Def.AttackRange, 7.5), "level 3: the Ancient shoots 110 in 7.5 m"); Check(GdMath.IsEqualApprox(core.Hp, 8000.0), "each level repaired 1,000 (5,000 → 8,000)"); Check(!state.Submit(0, Command.CoreUpgrade()).Ok, "no commands in combat"); state.FinishCombat(); Check(!state.Submit(0, Command.CoreUpgrade()).Ok, "no level above 3"); } void TestBots() { int hunts = 0; int blessings = 0; int combines = 0; int glyphs = 0; int cores = 0; string first = ""; for (int seed = 1; seed <= 12; seed++) { LuneMatch match = NewMatch(true, seed); while (match.Phase != Phase.Over && match.RoundNumber <= 80) { for (int slot = 0; slot < 6; slot++) LuneBot.Plan(match, slot); if (match.Players.Any(p => p.Draft.Roster.Any(r => r.Lane == LuneMatch.Grotto))) hunts++; match.BeginCombat(); match.FinishCombat(); glyphs += match.Combat.GlyphUsed.Count(used => used); } blessings += match.Boss.Blessing[0] + match.Boss.Blessing[1]; cores += match.Boss.CoreLevel[0] + match.Boss.CoreLevel[1]; combines += match.Players.Count(p => p.Hero != null && p.Hero.Items.Any(id => ContentData.Items[id].IsRecipe)); if (seed == 1) first = match.RoundNumber + "/" + match.Winner; } Check(hunts > 0 && blessings > 0, "extended bots go to the grotto and win Mondsegen (" + hunts + " expeditions, " + blessings + " stacks in 12 matches)"); // Bots recruit heroes late (reference bot behaviour), so recipes rarely come up in whole // matches; a prepared planning shows that a bot forges one when it can. LuneMatch forge = NewMatch(true, 3); Player bot = forge.Players[0]; bot.Hero = new HeroState { Uid = "h0-9", Seq = 9, HeroId = "held_ringborke", Lane = bot.Lane, Xp = 300, Invested = 260, Items = new List { "basaltschild", "rankenmantel" } }; bot.Gold = 1200; bot.Draft = forge.Snapshot(bot); LuneBot.Plan(forge, 0); Check(bot.Draft.Hero.Items.Contains("bollwerk"), "a bot forges Bollwerk from its parts (" + string.Join("+", bot.Draft.Hero.Items) + ")"); Check(glyphs > 0, "extended bots fire glyphs (" + glyphs + ")"); Note("12 extended matches: expeditions " + hunts + ", Mondsegen " + blessings + ", recipe heroes " + combines + ", glyphs " + glyphs + ", Ancient levels " + cores + "; seed 1 " + first); } } }