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);
}
}
}