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