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