using System.Collections.Generic; using System.Linq; namespace Luneraria.Core.Tests { /// /// Determinism of the C# core: the same inputs give the same match, repeatedly, with no /// dependence on hashing, object identity, wall-clock time or the order in which matches run. /// Each .NET run also randomises string hashing per process, so passing golden replays in /// several processes shows that no rule depends on dictionary iteration order. /// public sealed class DeterminismSuite : CheckSuite { public override string Name { get { return "determinism"; } } protected override void RunChecks() { foreach (int seed in new[] { 5, 13, 28 }) { string first = Fingerprint(Play(seed)); string second = Fingerprint(Play(seed)); Check(first == second, "seed " + seed + ": two runs of the same match are identical"); } // Interleaving two matches must not change either (no shared static state). LuneMatch a = New(7); LuneMatch b = New(9); for (int round = 0; round < 6; round++) { foreach (LuneMatch match in new[] { a, b }) { if (match.Phase == Phase.Over) continue; for (int slot = 0; slot < 6; slot++) LuneBot.Plan(match, slot); match.BeginCombat(); } bool running = true; while (running) { running = false; foreach (LuneMatch match in new[] { a, b }) { if (match.Phase == Phase.Combat) { match.StepCombat(); running = true; } } } } LuneMatch aAlone = New(7); LuneMatch bAlone = New(9); for (int round = 0; round < 6; round++) { foreach (LuneMatch match in new[] { aAlone, bAlone }) { if (match.Phase == Phase.Over) continue; for (int slot = 0; slot < 6; slot++) LuneBot.Plan(match, slot); match.BeginCombat(); match.FinishCombat(); } } Check(Fingerprint(a) == Fingerprint(aAlone) && Fingerprint(b) == Fingerprint(bAlone), "interleaved matches equal matches run alone (no shared state)"); // Unit ids are sequential per combat and follow spawn order, never object identity. LuneMatch ids = New(4); for (int slot = 0; slot < 6; slot++) LuneBot.Plan(ids, slot); ids.BeginCombat(); Check(ids.Combat.Units.Select(u => u.Id).SequenceEqual(Enumerable.Range(1, ids.Combat.Units.Count)), "unit ids are 1..n in spawn order"); // The combat step is fixed; the simulation never reads a clock or a frame rate. Check(ids.Balance.SimStep == 0.05, "fixed simulation step of 0.05 s (20 steps per combat second)"); } static LuneMatch New(int seed) { var match = new LuneMatch(); match.Setup(new MatchOptions { Seed = seed, HumanSlot = -1 }); return match; } static LuneMatch Play(int seed) { LuneMatch match = New(seed); while (match.Phase != Phase.Over && match.RoundNumber <= 60) { for (int slot = 0; slot < 6; slot++) LuneBot.Plan(match, slot); match.BeginCombat(); match.FinishCombat(); } return match; } static string Fingerprint(LuneMatch match) { var parts = new List { match.RoundNumber + "/" + match.Winner + "/" + match.Phase }; foreach (Player player in match.Players) { parts.Add(player.Gold + ":" + player.Earned + ":" + player.Spent + ":" + string.Join(",", player.Roster.Select(e => e.Uid + e.TypeId + e.Lane))); if (player.Hero != null) parts.Add(player.Hero.HeroId + player.Hero.Xp + string.Join("+", player.Hero.Items) + string.Join("+", player.Hero.Talents)); } foreach (KeyValuePair pair in match.BuildingHp) parts.Add(pair.Key + "=" + pair.Value.ToString("R", System.Globalization.CultureInfo.InvariantCulture)); foreach (HistoryEntry entry in match.History) parts.Add(entry.Round + entry.EndReason + entry.CombatTime.ToString("R", System.Globalization.CultureInfo.InvariantCulture) + string.Join(",", entry.Gold)); return string.Join("|", parts); } } }