champions-of-luneraria/Assets/Luneraria/Tests/Core/S1RulesSuite.cs
Martin Stickelbroeck 8e4b52f4ab S1 Strategie-Regeln nach Legion TD 2, Dota 2 und League of Legends
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>
2026-10-11 12:18:47 +02:00

184 lines
10 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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