champions-of-luneraria/Assets/Luneraria/Tests/Core/S3RulesSuite.cs
Martin Stickelbroeck fb5c92dd37 S3 Neutrallager und Fog of War mit Spähern (LoL, Dota 2)
Erweiterte Regeln: in jeder dritten Runde ein neutraler Wächter in jeder
Lanemitte; wer beim Fall mehr Einheiten (dann mehr Leben) in 8 m hat, nimmt
das Lager: Glutsegen (+10 % Schaden), Mondschild (Schild 10 %), Beute
(50 Gold je Spieler). Fog of War (LuneFog, nur Darstellung): Gegner sichtbar
in 9 m um eigene Einheiten, 10 m um Gebäude, 12 m um Späher; Späher (40 Gold,
3 Runden, Teamwirkung) zeigen in der Planung die gegnerische Armee ihrer Lane.
HUD: Teampanel SPÄHER und LAGER, Lager-Sechseck, Nebel in Feld, Minimap,
Namensschildern und Lane-Zählung.
Kern 597/597 (s3 31), EditMode 33/33, PlayMode 18/18, Verify 718/0,
Selbsttest 30/30, Golden 0, Fingerabdruck unverändert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:30:57 +02:00

183 lines
10 KiB
C#

using System.Collections.Generic;
using System.Linq;
namespace Luneraria.Core.Tests
{
/// <summary>
/// S3: bots that recruit their heroes early and a rotation over all profiles, neutral camps
/// (League of Legends buff camps), wards and the fog of war (Dota 2) under the extended
/// rules; none of it under the reference rules.
/// </summary>
public sealed class S3RulesSuite : CheckSuite
{
public override string Name { get { return "s3"; } }
protected override void RunChecks()
{
TestReference();
TestBots();
TestCamps();
TestWards();
TestFog();
}
static LuneMatch NewMatch(bool extended, int seed = 61)
{
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 TestReference()
{
LuneMatch state = NewMatch(false);
state.RoundNumber = 3;
state.Players[0].Draft.Gold += 500;
Check(!state.Submit(0, Command.Ward("mid")).Ok, "reference: no wards");
state.BeginCombat();
Check(state.Combat.Units.All(u => u.Kind != "camp"), "reference: no camps in round 3");
Check(LuneFog.Hidden(state, state.Combat, 0, false).Count == 0, "reference: no fog");
}
void TestBots()
{
var profiles = new HashSet<string>();
LuneMatch probe = NewMatch(true, 1);
for (int slot = 0; slot < 6; slot++)
profiles.Add(LuneBot.ProfileFor(probe, slot));
Check(profiles.Count == 6, "extended rotation gives six different profiles (" + string.Join(",", profiles) + ")");
int heroes = 0;
int seats = 0;
for (int seed = 1; seed <= 6; seed++)
{
LuneMatch match = new LuneMatch();
match.Setup(new MatchOptions { Seed = seed, HumanSlot = -1, Extended = true });
while (match.Phase != Phase.Over && match.RoundNumber <= 6)
{
for (int slot = 0; slot < 6; slot++)
LuneBot.Plan(match, slot);
match.BeginCombat();
match.FinishCombat();
}
heroes += match.Players.Count(p => p.Hero != null);
seats += 6;
}
Check(heroes * 2 >= seats, "extended bots have most heroes after round 6 (" + heroes + " of " + seats + ")");
LuneMatch reference = new LuneMatch();
reference.Setup(new MatchOptions { Seed = 1, HumanSlot = -1 });
for (int round = 0; round < 6; round++)
{
for (int slot = 0; slot < 6; slot++)
LuneBot.Plan(reference, slot);
reference.BeginCombat();
reference.FinishCombat();
}
Check(reference.Players.Count(p => p.Hero != null) == 0, "reference bots keep their late heroes (parity)");
LuneMatch bounty = NewMatch(true);
Check(bounty.ChampionBountyFor(100) == 50 && NewMatch(false).ChampionBountyFor(100) == 100, "extended: defenders get half a champion's bounty");
}
void TestCamps()
{
LuneMatch state = NewMatch(true);
Check(!state.CampRound(1) && !state.CampRound(2) && state.CampRound(3) && state.CampRound(6), "camps every third round");
state.BeginCombat();
Check(state.Combat.Units.All(u => u.Kind != "camp"), "no camp in round 1");
state.FinishCombat();
state.RoundNumber = 3;
state.Players[0].Draft = state.Snapshot(state.Players[0]);
state.BeginCombat();
List<LuneUnit> camps = state.Combat.Units.Where(u => u.Kind == "camp").ToList();
Check(camps.Count == 3 && camps.All(c => c.Team == LuneCombat.Neutral && c.Zone == "lane"), "three neutral guardians, one per lane");
LuneUnit top = camps.First(c => c.Lane == "top");
Vec2 start = top.Position;
for (int i = 0; i < 40; i++)
state.Combat.Step();
Check(top.Position.DistanceTo(start) < 0.000001, "a guardian never moves");
// Blau's units stand next to the top guardian when it falls.
foreach (LuneUnit unit in state.Combat.Units.Where(u => u.Team == 0 && u.IsMobile()).Take(4))
unit.Position = top.Position + new Vec2(1.0, 0.0);
top.Hp = 0.0;
LuneUnit bot = camps.First(c => c.Lane == "bot");
foreach (LuneUnit unit in state.Combat.Units.Where(u => u.Team == 1 && u.IsMobile()).Take(4))
unit.Position = bot.Position + new Vec2(1.0, 0.0);
bot.Hp = 0.0;
state.Combat.Step();
CombatEvent topEvent = state.Combat.Events.First(e => e.Kind == "camp" && e.Lane == "top");
Check(topEvent.ClaimTeam == 0, "the team with more units near the guardian takes the camp");
int[] before = state.Players.Select(p => p.Gold).ToArray();
state.FinishCombat();
Check(state.Boss.CampBuff[0] == 2 && state.Boss.CampBuff[3] == 0, "Glutsegen for Blau for two combats");
int redGain = state.Players[4].Gold - before[4] - state.LastSettlement.Gains[4] + 50;
Check(state.LastSettlement.Gains[3] >= 50 && state.LastSettlement.Gains[4] >= 50 && state.LastSettlement.Gains[5] >= 50, "Beute: Rot's players get 50 gold each");
state.Players[0].Draft.Gold += 500;
state.Submit(0, Command.Buy("g01", "top"));
state.BeginCombat();
LuneUnit troop = state.Combat.Units.First(u => u.Kind == "troop" && u.OwnerSlot == 0 && u.Def.Id == "g01");
Check(GdMath.IsEqualApprox(troop.Def.Damage, DataCatalog.GetUnit("g01").Damage * 1.10), "Glutsegen: +10 % damage");
state.FinishCombat();
state.Boss.CampBuff[4] = 1;
state.Players[3].Draft.Gold += 500;
state.Submit(3, Command.Buy("w01", "top"));
state.BeginCombat();
LuneUnit red = state.Combat.Units.First(u => u.Kind == "troop" && u.Team == 1);
Check(GdMath.IsEqualApprox(red.Shield, red.MaxHp * 0.10) || red.Shield > red.MaxHp * 0.10, "Mondschild: shield of 10 % at the start");
Check(redGain >= 0, "settlement consistent");
// Combats end without waiting for a lone guardian.
LuneMatch lonely = NewMatch(true);
lonely.RoundNumber = 3;
lonely.BeginCombat();
foreach (LuneUnit unit in lonely.Combat.Units.Where(u => u.Kind != "camp" && u.IsMobile()))
unit.Hp = 0.0;
lonely.Combat.Step();
lonely.Combat.Step();
Check(lonely.Combat.Finished && lonely.Combat.EndReason == "cleared", "a guardian alone does not hold the combat open");
}
void TestWards()
{
LuneMatch state = NewMatch(true);
int gold = state.GoldOf(0);
Check(!state.Submit(0, Command.Ward("river")).Ok, "unknown lane rejected");
Check(state.Submit(0, Command.Ward("mid")).Ok && state.GoldOf(0) == gold - 40, "a ward costs 40");
Check(!state.Submit(0, Command.Ward("mid")).Ok, "one ward per lane and planning");
Check(LuneFog.LaneScouted(state, 0, "mid", state.Players[0].Draft) && !LuneFog.LaneScouted(state, 0, "mid"), "the own draft ward scouts at once, the team sees it after the commit");
state.BeginCombat();
Check(state.Boss.Wards[1] == 3 && state.Boss.Wards[4] == 0, "the team ward watches mid for three rounds");
state.FinishCombat();
Check(state.Boss.Wards[1] == 2 && LuneFog.LaneScouted(state, 0, "mid") && !LuneFog.LaneScouted(state, 1, "mid"), "it counts down and scouts only for Blau");
state.BuildingHp["rot_top_outer"] = 0.0;
Check(LuneFog.LaneScouted(state, 0, "top"), "a fallen enemy outer tower scouts the lane");
}
void TestFog()
{
LuneMatch state = NewMatch(true);
state.RoundNumber = 3;
state.BeginCombat();
HashSet<int> hidden = LuneFog.Hidden(state, state.Combat, 0, false);
Check(state.Combat.Units.Where(u => u.Team == 1 && u.IsMobile()).All(u => hidden.Contains(u.Id)), "at combat start Blau sees no Rot unit (they stand at their base)");
Check(state.Combat.Units.Where(u => u.Team != 1).All(u => !hidden.Contains(u.Id)), "own and neutral units are always visible");
LuneUnit enemy = state.Combat.Units.First(u => u.Team == 1 && u.IsMobile());
LuneUnit own = state.Combat.Units.First(u => u.Team == 0 && u.IsMobile() && u.Zone == enemy.Zone);
enemy.Position = own.Position + new Vec2(5.0, 0.0);
Check(!LuneFog.Hidden(state, state.Combat, 0, false).Contains(enemy.Id), "an enemy within 9 m of an own unit is visible");
enemy.Position = LuneFog.WardPoint(state, enemy.Lane == "grotto" ? "mid" : enemy.Lane);
bool before = LuneFog.Hidden(state, state.Combat, 0, false).Contains(enemy.Id);
state.Boss.Wards[LuneMap.LaneIndex(enemy.Lane)] = 2;
Check(before && !LuneFog.Hidden(state, state.Combat, 0, false).Contains(enemy.Id), "a ward reveals the middle of its lane");
state.FinishCombat();
LuneMatch plan = NewMatch(true);
plan.Players[3].Draft.Gold += 200;
plan.Submit(3, Command.Buy("w01", "top"));
plan.Submit(3, Command.Buy("w01", "mid"));
plan.BeginCombat();
plan.FinishCombat();
plan.Boss.Wards[0] = 1;
List<LuneMatch.ArmyEntry> scouted = LuneFog.ScoutedEnemies(plan, 0);
Check(scouted.Any(e => e.Lane == "top" && e.Slot == 3) && scouted.All(e => e.Lane == "top"), "planning: the warded lane shows Rot's committed army, the others nothing");
Check(LuneFog.ScoutedEnemies(plan, 1).Count == 0 && LuneFog.ScoutedEnemies(NewMatch(false), 0).Count == 0, "no scouting without a ward or under the reference rules");
}
}
}