Hinter MatchOptions.Extended (Standard im Spiel, Schalter im Startbildschirm): Söldner-Einkommen (Angriffschampion +Preis/12 Einkommen dauerhaft), Kasernen (gefallener innerer Turm: Gegnerwellen +25 % und ein Monster mehr), Erschöpfung und Rückkauf (60 % Leben, Rückkauf 60 + 20 x Stufe), Mondsegen (Grottensieg +4 % je Stapel, Mondseele bei 4). Bots wählen mit erweiterten Regeln auch Sylvara und kaufen Helden zurück. Netz überträgt Regeln und neue Zustände. HUD: Einkommen, Kasernen-Symbol, Rückkauf, Segen im Teamkopf. Kern 531/531 (s1 31), EditMode 31/31, PlayMode 16/16, Verify 718/0, Selbsttest 30/30, Golden 0 Abweichungen, Fingerabdruck unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
184 lines
10 KiB
C#
184 lines
10 KiB
C#
using System.Collections.Generic;
|
||
using System.Linq;
|
||
|
||
namespace Luneraria.Core.Tests
|
||
{
|
||
/// <summary>
|
||
/// S1: the extended rules after Legion TD 2, Dota 2 and League of Legends (MatchOptions.Extended).
|
||
/// Champion income, barracks, exhaustion and buyback, Mondsegen, the bot rotation with later
|
||
/// heroes, and that none of it acts under the reference rules.
|
||
/// </summary>
|
||
public sealed class S1RulesSuite : CheckSuite
|
||
{
|
||
public override string Name { get { return "s1"; } }
|
||
|
||
protected override void RunChecks()
|
||
{
|
||
TestReferenceUntouched();
|
||
TestChampionIncome();
|
||
TestBarracks();
|
||
TestExhaustionAndBuyback();
|
||
TestBlessing();
|
||
TestBots();
|
||
}
|
||
|
||
static LuneMatch NewMatch(bool extended, int seed = 41)
|
||
{
|
||
var state = new LuneMatch();
|
||
state.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Extended = extended,
|
||
Bunds = new List<string> { "glutpakt", "wurzelbund", "glutpakt", "wurzelbund", "glutpakt", "wurzelbund" } });
|
||
return state;
|
||
}
|
||
|
||
void TestReferenceUntouched()
|
||
{
|
||
LuneMatch state = NewMatch(false);
|
||
Check(!state.Extended, "default options keep the reference rules");
|
||
state.Players[0].Draft.Gold += 1000;
|
||
state.Submit(0, Command.ChampionBook("ch_schutz", "top"));
|
||
state.Submit(0, Command.HeroBuy("held_ringborke"));
|
||
state.BeginCombat();
|
||
Check(state.Players[0].IncomeBonus == 0, "reference: a champion gives no income");
|
||
foreach (LuneUnit unit in state.Combat.Units.Where(u => u.Kind == "hero"))
|
||
unit.Hp = 0.0;
|
||
state.FinishCombat();
|
||
Check(!state.Players[0].Hero.Exhausted, "reference: a fallen hero is not exhausted");
|
||
Check(!state.Submit(0, Command.Buyback()).Ok, "reference: no buyback");
|
||
var setup = new LuneMatch();
|
||
setup.Setup(new MatchOptions { Seed = 3 });
|
||
Check(!setup.Extended, "MatchOptions.Extended defaults to false");
|
||
}
|
||
|
||
void TestChampionIncome()
|
||
{
|
||
LuneMatch state = NewMatch(true);
|
||
state.Players[0].Draft.Gold += 1000;
|
||
Check(state.Submit(0, Command.ChampionBook("ch_schutz", "top")).Ok, "book a champion");
|
||
Check(state.Players[0].IncomeBonus == 0, "income rises only when the booking is committed");
|
||
state.Submit(0, Command.ChampionCancel(state.Players[0].Draft.NewBookings[0].Uid));
|
||
state.Submit(0, Command.ChampionBook("ch_belagerung", "mid"));
|
||
state.BeginCombat();
|
||
Check(state.Players[0].IncomeBonus == 180 / 12, "Keilbrecher adds 15 income for good (" + state.Players[0].IncomeBonus + ")");
|
||
int before = state.Players[0].Gold;
|
||
int beforeOther = state.Players[1].Gold;
|
||
state.FinishCombat();
|
||
int gain = state.Players[0].Gold - before;
|
||
int gainOther = state.Players[1].Gold - beforeOther;
|
||
Check(gain - state.LastSettlement.Gains[0] == state.LastSettlement.Income + 15, "settlement pays round income plus the champion income");
|
||
Check(gainOther - state.LastSettlement.Gains[1] == state.LastSettlement.Income, "other seats get the plain income");
|
||
}
|
||
|
||
void TestBarracks()
|
||
{
|
||
LuneMatch state = NewMatch(true);
|
||
Check(!state.BarracksBroken(0, "mid"), "barracks intact at the start");
|
||
state.BuildingHp["rot_mid_inner"] = 0.0;
|
||
Check(state.BarracksBroken(0, "mid") && !state.BarracksBroken(1, "mid") && !state.BarracksBroken(0, "top"), "Blau's mid waves are reinforced once Rot's inner mid tower fell");
|
||
state.BeginCombat();
|
||
List<LuneUnit> blueMid = state.Combat.Units.Where(u => u.Kind == "monster" && u.Team == 0 && u.Lane == "mid").ToList();
|
||
List<LuneUnit> blueTop = state.Combat.Units.Where(u => u.Kind == "monster" && u.Team == 0 && u.Lane == "top").ToList();
|
||
List<LuneUnit> redMid = state.Combat.Units.Where(u => u.Kind == "monster" && u.Team == 1 && u.Lane == "mid").ToList();
|
||
Check(blueMid.Count == blueTop.Count + 1 && redMid.Count == blueTop.Count, "one extra monster on the broken lane only");
|
||
Check(GdMath.IsEqualApprox(blueMid.Last().MaxHp, blueTop.Last().MaxHp * 1.25), "reinforced monsters have 125 % health");
|
||
LuneMatch reference = NewMatch(false);
|
||
reference.BuildingHp["rot_mid_inner"] = 0.0;
|
||
reference.BeginCombat();
|
||
Check(reference.Combat.Units.Count(u => u.Kind == "monster" && u.Team == 0 && u.Lane == "mid") == blueTop.Count, "reference: no barracks effect");
|
||
}
|
||
|
||
void TestExhaustionAndBuyback()
|
||
{
|
||
LuneMatch state = NewMatch(true);
|
||
state.Players[1].Draft.Gold += 2000;
|
||
state.Submit(1, Command.HeroBuy("held_sylvara"));
|
||
Check(!state.Submit(1, Command.Buyback()).Ok, "no buyback for a fresh hero");
|
||
state.BeginCombat();
|
||
LuneUnit hero = state.Combat.Units.First(u => u.Kind == "hero" && u.OwnerSlot == 1);
|
||
hero.Hp = 0.0;
|
||
state.FinishCombat();
|
||
HeroState committed = state.Players[1].Hero;
|
||
Check(committed.Exhausted, "a hero that fell is exhausted");
|
||
state.BeginCombat();
|
||
hero = state.Combat.Units.First(u => u.Kind == "hero" && u.OwnerSlot == 1);
|
||
Check(GdMath.IsEqualApprox(hero.Hp, hero.MaxHp * 0.6), "an exhausted hero starts with 60 % health");
|
||
state.FinishCombat();
|
||
Check(!state.Players[1].Hero.Exhausted || !state.Combat.Units.First(u => u.Kind == "hero" && u.OwnerSlot == 1).Alive, "surviving clears the exhaustion");
|
||
state.Players[1].Hero.Exhausted = true;
|
||
state.Players[1].Draft = state.Snapshot(state.Players[1]);
|
||
int level = LuneContent.LevelFor(state.Players[1].Hero.Xp);
|
||
int cost = 60 + 20 * level;
|
||
int gold = state.GoldOf(1);
|
||
Check(state.Submit(1, Command.Buyback()).Ok && state.GoldOf(1) == gold - cost, "buyback costs 60 + 20 × level (" + cost + ")");
|
||
Check(state.Undo(1).Ok && state.GoldOf(1) == gold && state.Players[1].Draft.Hero.Exhausted, "buyback can be undone");
|
||
state.Submit(1, Command.Buyback());
|
||
state.BeginCombat();
|
||
hero = state.Combat.Units.First(u => u.Kind == "hero" && u.OwnerSlot == 1);
|
||
Check(hero.Hp == hero.MaxHp, "a bought back hero starts at full health");
|
||
state.FinishCombat();
|
||
}
|
||
|
||
void TestBlessing()
|
||
{
|
||
LuneMatch state = NewMatch(true);
|
||
state.Players[0].Draft.Gold += 1000;
|
||
state.Submit(0, Command.Buy("g01", "top"));
|
||
state.Submit(0, Command.HeroBuy("held_funkenlanze"));
|
||
state.BeginCombat();
|
||
state.Combat.GrottoOutcome = "success";
|
||
state.Combat.GrottoClaimTeam = 0;
|
||
state.SettleBoss();
|
||
Check(state.Boss.Blessing[0] == 1 && state.Boss.Blessing[1] == 0, "a grotto victory blesses the winning team");
|
||
state.FinishCombat();
|
||
state.Boss.Blessing[0] = 2;
|
||
state.BeginCombat();
|
||
LuneUnit troop = state.Combat.Units.First(u => u.Kind == "troop" && u.OwnerSlot == 0);
|
||
Check(GdMath.IsEqualApprox(troop.MaxHp, DataCatalog.GetUnit("g01").MaxHp * 1.08), "two stacks: troops have 108 % health");
|
||
LuneUnit enemy = state.Combat.Units.FirstOrDefault(u => u.Kind == "troop" && u.Team == 1);
|
||
Check(enemy == null || GdMath.IsEqualApprox(enemy.MaxHp, DataCatalog.GetUnit(enemy.Def.Id).MaxHp), "the other team is not blessed");
|
||
state.FinishCombat();
|
||
state.Boss.Blessing[0] = 4;
|
||
state.BeginCombat();
|
||
LuneUnit hero = state.Combat.Units.First(u => u.Kind == "hero" && u.OwnerSlot == 0);
|
||
Check(GdMath.IsEqualApprox(hero.Def.Regen, 0.01), "Mondseele: heroes regenerate 1 % per second");
|
||
state.FinishCombat();
|
||
state.Combat.GrottoOutcome = "success";
|
||
state.Combat.GrottoClaimTeam = 0;
|
||
state.Boss.SettledRound = 0;
|
||
state.Boss.Claim = null;
|
||
state.Boss.NextAvailableRound = 1;
|
||
state.SettleBoss();
|
||
Check(state.Boss.Blessing[0] == 4, "Mondsegen stops at four stacks");
|
||
}
|
||
|
||
void TestBots()
|
||
{
|
||
bool sylvaraPicked = false;
|
||
for (int seed = 1; seed <= 20 && !sylvaraPicked; seed++)
|
||
{
|
||
LuneMatch probe = NewMatch(true, seed);
|
||
for (int slot = 0; slot < 6; slot++)
|
||
sylvaraPicked |= LuneBot.ProfileFor(probe, slot) == "assassine";
|
||
}
|
||
Check(sylvaraPicked, "extended rules: the bot rotation includes Sylvara's profile");
|
||
string first = Play(17, out LuneMatch played);
|
||
Check(first == Play(17, out LuneMatch _), "extended bot matches are deterministic");
|
||
Check(played.Phase == Phase.Over, "an extended bot match ends by an Ancient (" + first.Split('|')[0] + ")");
|
||
Check(played.Players.Any(p => p.IncomeBonus > 0), "bots earn champion income");
|
||
Note("extended seed 17: " + first.Split('|')[0] + ", income bonus " + string.Join(",", played.Players.Select(p => p.IncomeBonus)) + ", blessing " + played.Boss.Blessing[0] + "/" + played.Boss.Blessing[1]);
|
||
}
|
||
|
||
static string Play(int seed, out LuneMatch match)
|
||
{
|
||
match = NewMatch(true, seed);
|
||
while (match.Phase != Phase.Over && match.RoundNumber <= 80)
|
||
{
|
||
for (int slot = 0; slot < 6; slot++)
|
||
LuneBot.Plan(match, slot);
|
||
match.BeginCombat();
|
||
match.FinishCombat();
|
||
}
|
||
return match.RoundNumber + "/" + match.Winner + "|" + string.Join(",", match.Players.Select(p => p.Gold + ":" + p.Earned + ":" + p.IncomeBonus))
|
||
+ "|" + string.Join(",", match.BuildingHp.Values.Select(v => v.ToString("R", System.Globalization.CultureInfo.InvariantCulture)));
|
||
}
|
||
}
|
||
}
|