using System; using System.Collections.Generic; using System.Linq; namespace Luneraria.Core.Tests { /// /// Multi-round loopback with two sessions (port of tools/luneraria/net_rounds.py, 12 checks). /// Host and guest play five rounds through the real LuneSession; after every settlement the /// public checksums must agree. Guest A leaves during the combat of round 2, the bot takes /// over, guest B returns with the same display name and must get the same seat back. The /// guest never runs a combat step and receives host snapshots. This is a local in-process /// loopback, not a six-player internet match. /// public sealed class NetRoundsSuite : CheckSuite { public override string Name { get { return "net_rounds"; } } const int Rounds = 5; const int DropRound = 2; /// Host combat steps per tick (net_pair.gd steps_per_frame); snapshots still flow. const int StepsPerTick = 8; sealed class CombatLog { public int Round; public int Frames; public int LocalSteps; } readonly Dictionary _hostSums = new Dictionary(); readonly Dictionary _guestSums = new Dictionary(); readonly List _combats = new List(); readonly List _seatsA = new List(); readonly List _seatsB = new List(); readonly List _hostMessages = new List(); readonly List _errors = new List(); protected override void RunChecks() { var hub = new LuneLoopback(); LuneSession host = hub.Attach(1, new LuneSession()); host.SessionMessage += (text, isError) => { if (isError) _errors.Add("host: " + text); else _hostMessages.Add(text); }; var options = new MatchOptions { Seed = 42, HumanSlot = 0, Bunds = new List { "glutpakt", "wurzelbund", "glutpakt", "wurzelbund", "glutpakt", "wurzelbund" } }; if (!host.HostNetwork(options, "Host")) { Check(false, "host starts: " + host.LastError); return; } const int peerA = 10; const int peerB = 11; LuneSession guest = Join(hub, peerA, "Gast", _seatsA, _errors); bool spoofRejected = !guest.HostCommand(1, 5, Command.Buy("w01")).Ok; bool dropped = false; int hostPlannedRound = 0; int guestPlannedRound = 0; int framesAtSegment = 0; var current = new CombatLog(); for (int guard = 0; guard < 8000 && host.MatchState.Phase != Phase.Over && host.MatchState.RoundNumber <= Rounds + 1; guard++) { LuneMatch match = host.MatchState; if (match.Phase == Phase.Planning) { if (match.RoundNumber > Rounds) break; host.PlanBotSeats(); PlanOnce(host, ref hostPlannedRound, host.LocalSlot, true); if (guest != null && guest.MatchState != null && guest.LocalSlot >= 0) PlanOnce(guest, ref guestPlannedRound, guest.LocalSlot, false); } else if (match.Phase == Phase.Combat) { if (current.Round != match.RoundNumber) { current = new CombatLog { Round = match.RoundNumber }; _combats.Add(current); framesAtSegment = guest != null ? guest.FramesReceived : 0; } for (int step = 0; step < StepsPerTick && match.Phase == Phase.Combat; step++) match.StepCombat(); host.HostTick(LuneNet.FrameInterval); if (guest != null && guest.MatchState != null && guest.MatchState.Combat != null) { current.Frames = Math.Max(current.Frames, guest.FramesReceived - framesAtSegment); current.LocalSteps = Math.Max(current.LocalSteps, guest.MatchState.Combat.Steps); } // Leave on a snapshot of the combat of DropRound: short combats may send only a few. if (!dropped && hub.Has(peerA) && match.RoundNumber == DropRound && current.Frames > 0) { if (guest != null) current.Frames = Math.Max(current.Frames, guest.FramesReceived - framesAtSegment); dropped = true; hub.Detach(peerA); // Guest B returns with the same display name while the seat is bot-held. guest = Join(hub, peerB, "Gast", _seatsB, _errors); guestPlannedRound = 0; framesAtSegment = guest.FramesReceived; } } // Settlement: the phase left combat in this or a previous step. if (current.Round != 0 && match.Phase != Phase.Combat) { if (guest != null && guest.MatchState != null && guest.MatchState.Combat != null) { current.Frames = Math.Max(current.Frames, guest.FramesReceived - framesAtSegment); current.LocalSteps = Math.Max(current.LocalSteps, guest.MatchState.Combat.Steps); } host.HostAfterCombat(); NoteSums(host, guest, current.Round); current = new CombatLog(); } } if (current.Round != 0) { host.HostAfterCombat(); NoteSums(host, guest, current.Round); } // ---------------------------------------------------------------- 12 checks List compared = _hostSums.Keys.Where(r => _guestSums.ContainsKey(r)).OrderBy(r => r).ToList(); List mismatches = compared.Where(r => _hostSums[r] != _guestSums[r]).ToList(); Check(host.MatchState != null, "host_ok"); Check(dropped, "guest_a_ok (left during the combat)"); Check(_seatsB.Count > 0, "guest_b_ok"); Check(spoofRejected, "spoof_rejected"); Check(compared.Count >= Rounds - 1, "rounds_compared " + compared.Count + " of " + Rounds + " (" + string.Join(",", compared) + ")"); Check(mismatches.Count == 0 && compared.Count > 0, "checksums_equal " + string.Join(",", mismatches)); Check(_combats.Count > 0 && _combats.All(c => c.LocalSteps == 0), "client_never_simulates " + string.Join(",", _combats.Select(c => c.Round + ":" + c.LocalSteps))); Check(_combats.Count > 0 && _combats.All(c => c.Frames > 0), "client_receives_frames " + string.Join(",", _combats.Select(c => c.Round + ":" + c.Frames))); Check(_seatsA.Count > 0 && _seatsB.Count > 0 && _seatsB[0] == _seatsA[0], "rejoin_same_seat a=" + string.Join(",", _seatsA) + " b=" + string.Join(",", _seatsB)); Check(_hostMessages.Any(m => m.Contains("Slot wird vom Bot übernommen")), "bot_took_over"); Check(_hostMessages.Any(m => m.Contains("kehrt zurück auf")), "rejoin_announced"); Check(_errors.Count == 0, "no_script_errors " + string.Join(";", _errors)); } static LuneSession Join(LuneLoopback hub, int peer, string name, List seats, List errors) { LuneSession guest = hub.Attach(peer, new LuneSession()); guest.SessionMessage += (text, isError) => { if (isError) errors.Add("guest " + peer + ": " + text); }; guest.MatchResetup += () => seats.Add(guest.LocalSlot); guest.JoinNetwork(name); guest.OnClientConnected(); return guest; } static void PlanOnce(LuneSession session, ref int plannedRound, int slot, bool waitForPeer) { LuneMatch match = session.MatchState; if (slot < 0 || plannedRound == match.RoundNumber || match.Players[slot].Ready) return; // The host waits for a seated guest in every planning phase (covers the rejoin case). if (waitForPeer && session.SeatPeers.Count < 2) return; plannedRound = match.RoundNumber; List ids = match.BaseIds(match.Players[slot].Bund); int gold = match.GoldOf(slot); List affordable = ids.Where(id => DataCatalog.GetUnit(id).Cost <= gold).ToList(); if (affordable.Count > 0) session.Submit(Command.Buy(affordable[match.RoundNumber % affordable.Count])); session.ReadyUp(); } void NoteSums(LuneSession host, LuneSession guest, int fought) { if (fought < 1 || host.MatchState == null) return; _hostSums[fought] = LuneSession.Checksum(host.MatchState); if (guest != null && guest.MatchState != null) _guestSums[fought] = LuneSession.Checksum(guest.MatchState); } } }