using System.Collections.Generic;
using System.Linq;
namespace Luneraria.Core.Tests
{
///
/// H1: Sylvara, the first hero after the port, and the planned ultimate rules. Checks the
/// purchase and the rule command, the kit in small controlled combats (leap, leap strike,
/// sickle throw, ultimate per rule, talents), the bot profile outside the seed rotation and
/// determinism of matches with her. The reference matches stay untouched (golden suite).
///
public sealed class H1HeroSuite : CheckSuite
{
public override string Name { get { return "h1"; } }
protected override void RunChecks()
{
TestPurchaseAndRules();
TestLeap();
TestThrow();
TestUltimate();
TestTalents();
TestBots();
}
static LuneMatch NewMatch(int seed = 31)
{
var state = new LuneMatch();
state.Setup(new MatchOptions { Seed = seed, HumanSlot = -1 });
return state;
}
void TestPurchaseAndRules()
{
LuneMatch state = NewMatch();
state.Players[0].Draft.Gold += 1000;
Check(!state.Submit(0, Command.UltimateRule("sofort")).Ok, "ultimate rule needs a hero");
int gold = state.GoldOf(0);
Check(state.Submit(0, Command.HeroBuy("held_sylvara")).Ok && state.GoldOf(0) == gold - 260, "Sylvara costs 260 like every hero");
HeroState hero = state.Players[0].Draft.Hero;
Check(LuneContent.UltimateRuleFor(hero) == "gruppe", "default ultimate rule is the first (Gruppe)");
Check(!state.Submit(0, Command.UltimateRule("ueberall")).Ok, "unknown ultimate rule rejected");
Check(state.Submit(0, Command.UltimateRule("zweite_reihe")).Ok && hero.Ultimate == "zweite_reihe", "ultimate rule accepted in planning");
Check(state.Undo(0).Ok && state.Players[0].Draft.Hero.Ultimate == "", "rule change can be undone like any planning step");
state.Submit(0, Command.UltimateRule("notfall"));
state.BeginCombat();
List army = state.ArmyEntries(0);
LuneUnit spawned = state.Combat.Units.FirstOrDefault(u => u.Kind == "hero" && u.OwnerSlot == 0);
Check(spawned != null && spawned.Def.UltimateRule == "notfall" && spawned.Role() == "melee" && spawned.Def.Id == "sylvara", "combat unit carries rule, melee role and model id");
state.FinishCombat();
Check(state.Players[0].Hero.Ultimate == "notfall", "rule persists after the round");
state.Submit(0, Command.HeroCancel());
Check(state.Players[0].Draft.Hero != null, "committed Sylvara cannot be returned");
LuneMatch other = NewMatch();
other.Players[1].Draft.Gold += 1000;
other.Submit(1, Command.HeroBuy("held_ringborke"));
Check(!other.Submit(1, Command.UltimateRule("gruppe")).Ok, "reference heroes have no ultimate");
Check(LuneContent.HeroDefinition(other.Players[1].Draft.Hero).UltimateRule == "", "reference hero definitions carry no rule");
Check(army.Count(e => e.Kind == "hero") == 1 && army.First(e => e.Kind == "hero").Role == "melee", "Sylvara stands in the melee row");
}
/// A small fight on the mid lane without buildings: Sylvara (team 0) and given enemies (team 1).
static LuneCombat Arena(HeroState hero, out LuneUnit sylvara, params KeyValuePair[] enemies)
{
var balance = LuneBalance.Default();
var combat = new LuneCombat(new LuneMap(balance), balance, 5);
UnitDef definition = LuneContent.HeroDefinition(hero);
definition.Role = "melee";
sylvara = combat.Spawn("hero", 0, "mid", definition, 40.0, 0.0, 0, "h0-1");
Vec2 forward = combat.Map.LaneForward(0, "mid", 40.0);
foreach (KeyValuePair enemy in enemies)
{
UnitDef monster = ContentData.Enemies[enemy.Key].Clone();
monster.MaxHp = 50000.0;
monster.Speed = 0.0;
LuneUnit unit = combat.Spawn("monster", 1, "mid", monster, 40.0, 0.0);
unit.Position = sylvara.Position + forward * enemy.Value;
unit.PreviousPosition = unit.Position;
}
return combat;
}
static KeyValuePair At(string id, double distance) { return new KeyValuePair(id, distance); }
static void Run(LuneCombat combat, double seconds)
{
int steps = (int)(seconds / combat.Balance.SimStep + 0.5);
for (int i = 0; i < steps && !combat.Finished; i++)
combat.Step();
}
void TestLeap()
{
LuneUnit sylvara;
// A melee monster 3 m ahead, a ranged one (Fadenschütze, 6 m reach) 7 m ahead.
LuneCombat combat = Arena(new HeroState { HeroId = "held_sylvara" }, out sylvara, At("e01", 3.0), At("e04", 7.0));
LuneUnit ranged = combat.Units.First(u => u.Def.Id == "e04");
combat.Step();
Check(sylvara.LeapUsed && sylvara.LeapSerial == 1, "leap fires on the first step with a back-row enemy within 8 m");
Check(sylvara.Position.DistanceTo(ranged.Position) < 1.0 && sylvara.Target == ranged, "she lands next to the back-row enemy and targets it");
Run(combat, 3.0);
Check(sylvara.LeapSerial == 1, "leap only once per combat");
// Leap strike: the first hit deals 150 %, later hits 100 %.
LuneUnit lone;
LuneCombat strike = Arena(new HeroState { HeroId = "held_sylvara" }, out lone, At("e04", 5.0));
LuneUnit victim = strike.Units.First(u => u.Def.Id == "e04");
double before = victim.Hp;
int guard = 0;
while (lone.HitCount == 0 && guard++ < 200)
strike.Step();
double first = before - victim.Hp;
before = victim.Hp;
while (lone.HitCount == 1 && guard++ < 400)
strike.Step();
double second = before - victim.Hp;
double expected = victim.Mitigated(lone.Def.Damage * (1.0 + lone.DamageBonus()), lone.Def.AttackType);
Check(GdMath.IsEqualApprox(first, expected * 1.5) && GdMath.IsEqualApprox(second, expected), string.Format("leap strike 150 % then 100 % ({0:F1} / {1:F1}, base {2:F1})", first, second, expected));
// No leap without a back-row enemy, and never onto a boss.
LuneUnit plain;
LuneCombat melee = Arena(new HeroState { HeroId = "held_sylvara" }, out plain, At("e01", 3.0), At("e07", 6.0));
Run(melee, 1.0);
Check(!plain.LeapUsed, "no leap against melee enemies or bosses");
}
void TestThrow()
{
LuneUnit sylvara;
LuneCombat combat = Arena(new HeroState { HeroId = "held_sylvara" }, out sylvara, At("e01", 1.0), At("e01", 2.0), At("e01", 2.6), At("e01", 3.2));
Run(combat, LuneCombat.SylvaraFirstThrow - 0.2);
Check(sylvara.AbilitySerial == 0, "no sickle throw before 4 s");
Run(combat, 0.4);
Check(sylvara.AbilitySerial == 1, "first sickle throw at 4 s");
Run(combat, 6.0);
Check(sylvara.AbilitySerial == 2, "sickle throw every 6 s");
// The throw itself: 90 % to the target and two more within 2.5 m (one direct check).
LuneUnit probe;
LuneCombat single = Arena(new HeroState { HeroId = "held_sylvara" }, out probe, At("e01", 1.0), At("e01", 2.0), At("e01", 3.0), At("e01", 9.5));
Run(single, LuneCombat.SylvaraFirstThrow - 0.1);
var hpBefore = single.Units.Where(u => u.Kind == "monster").ToDictionary(u => u.Id, u => u.Hp);
Run(single, 0.15);
int touched = single.Units.Count(u => u.Kind == "monster" && u.Hp < hpBefore[u.Id]);
Check(probe.AbilitySerial == 1 && touched >= 3 && single.Units.First(u => u.Kind == "monster" && u.Position.DistanceTo(probe.Position) > 8.0).Hp == 50000.0,
"sickle throw hits three enemies near the target, not the far one");
}
void TestUltimate()
{
LuneUnit sylvara;
LuneCombat combat = Arena(new HeroState { HeroId = "held_sylvara", Ultimate = "sofort" }, out sylvara, At("e01", 1.0), At("e01", 1.5), At("e01", 2.0), At("e01", 2.5), At("e01", 3.0), At("e01", 3.5), At("e01", 4.0));
Run(combat, LuneContent.UltimateDelay - 0.1);
Check(sylvara.UltimateSerial == 0, "no ultimate before 2 s");
Run(combat, 0.2);
Check(sylvara.UltimateSerial == 1, "rule Sofort releases at 2 s with an enemy in 4 m");
int marked = combat.Units.Count(u => u.Effects.ContainsKey("moon_mark"));
Check(marked == 5, "Mondsicheltanz hits five enemies (" + marked + ")");
Run(combat, 10.0);
Check(sylvara.UltimateSerial == 1, "ultimate once per combat");
// Notfall waits for low health.
LuneUnit calm;
LuneCombat emergency = Arena(new HeroState { HeroId = "held_sylvara", Ultimate = "notfall" }, out calm, At("e01", 1.0));
Run(emergency, 3.0);
bool waited = calm.UltimateSerial == 0;
calm.Hp = calm.MaxHp * 0.3;
Run(emergency, 0.1);
Check(waited && calm.UltimateSerial == 1, "rule Notfall waits until health falls below 40 %");
// Gruppe needs three enemies within 4 m.
LuneUnit grouped;
LuneCombat few = Arena(new HeroState { HeroId = "held_sylvara" }, out grouped, At("e01", 1.0), At("e01", 2.0));
Run(few, 4.0);
Check(grouped.UltimateSerial == 0, "rule Gruppe ignores two enemies");
LuneUnit crowd;
LuneCombat many = Arena(new HeroState { HeroId = "held_sylvara" }, out crowd, At("e01", 1.0), At("e01", 2.0), At("e01", 3.0));
Run(many, 2.2);
Check(crowd.UltimateSerial == 1, "rule Gruppe fires with three enemies in 4 m");
// Zweite Reihe: a ranged enemy within 5 m.
LuneUnit hunter;
LuneCombat back = Arena(new HeroState { HeroId = "held_sylvara", Ultimate = "zweite_reihe" }, out hunter, At("e01", 1.0));
Run(back, 3.0);
Check(hunter.UltimateSerial == 0, "rule Zweite Reihe ignores melee enemies");
}
void TestTalents()
{
var hero = new HeroState { HeroId = "held_sylvara", Xp = 1000, Talents = new List { "sichelsturm", "neumond" } };
UnitDef definition = LuneContent.HeroDefinition(hero);
Check(GdMath.IsEqualApprox(definition.AttackSpeedBonus, 0.20) && definition.Level == 6, "Neumond: +20 % attack tempo");
LuneUnit full;
LuneCombat combat = Arena(new HeroState { HeroId = "held_sylvara", Ultimate = "sofort", Talents = new List { "mondsilber", "vollmond" } }, out full,
At("e01", 1.0), At("e01", 1.4), At("e01", 1.8), At("e01", 2.2), At("e01", 2.6), At("e01", 3.0), At("e01", 3.4), At("e01", 3.8));
Run(combat, 2.2);
Check(combat.Units.Count(u => u.Effects.ContainsKey("moon_mark")) == 7, "Vollmond: the dance hits seven enemies");
TalentOption[][] tiers = LuneContent.TalentsOf("held_sylvara");
Check(tiers.Length == 2 && tiers.All(t => t.Length == 2), "two talent tiers with two choices each");
}
void TestBots()
{
bool rotationClean = true;
for (int seed = 1; seed <= 60; seed++)
{
LuneMatch probe = NewMatch(seed);
for (int slot = 0; slot < 6; slot++)
rotationClean &= LuneBot.ProfileFor(probe, slot) != "assassine";
}
Check(rotationClean, "the seed rotation never picks the H1 profile (reference bot matches unchanged)");
string first = PlayWithSylvara(12, out LuneMatch played);
string second = PlayWithSylvara(12, out LuneMatch _);
Check(first == second, "matches with Sylvara bots are deterministic");
HeroState hero = played.Players[0].Hero;
Check(hero != null && hero.HeroId == "held_sylvara" && hero.Xp > 0, "the assassine bot recruits Sylvara and she gains XP");
Note("sylvara-bot match seed 12: " + first.Split('|')[0]);
}
static string PlayWithSylvara(int seed, out LuneMatch match)
{
match = new LuneMatch();
match.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Profiles = new List { "assassine", "druck", "fernkampf", "ausgewogen", "druck", "fernkampf" } });
var log = new List();
while (match.Phase != Phase.Over && match.RoundNumber <= 60)
{
for (int slot = 0; slot < 6; slot++)
LuneBot.Plan(match, slot);
match.BeginCombat();
int leaps = 0;
int ultimates = 0;
while (match.Phase == Phase.Combat)
{
match.StepCombat();
}
foreach (LuneUnit unit in match.Combat.Units)
{
leaps += unit.LeapSerial;
ultimates += unit.UltimateSerial;
}
log.Add(leaps + "/" + ultimates);
}
return match.RoundNumber + "/" + match.Winner + "|" + string.Join(",", log) + "|" + string.Join(",", match.Players.Select(p => p.Gold + ":" + p.Earned));
}
}
}