using System; using Luneraria.Core; namespace Luneraria.View { /// /// Runs a match on the host (U2C/U2D, human seat since U3) against wall-clock time: planning /// (the bots plan through the command interface), combat in fixed steps, a pause after the /// settlement, the next round, until an Ancient falls. Only the core decides; time only paces /// the calls, so a bot match calls the core in the order of LuneTrace.RunCore at every frame /// rate. With a human seat (HumanSlot >= 0) the bots plan the other five seats and planning /// waits for the human's Ready, as offline in Godot (lune_game.gd); there is no pause after /// the settlement. /// public sealed class LuneMatchRunner { public enum Stage { Planning, Combat, Settled, Over } public readonly LuneMatch Match; public readonly LuneClock Clock; public readonly string Profile; /// The seat a human plans through the HUD; -1 for a bot match. public readonly int HumanSlot; public LuneTrace Trace; public double PlanningSeconds = 1.0; public double SettledSeconds = 2.0; public int MaxRound = 60; public Stage Current { get; private set; } public int StepsThisFrame { get; private set; } public event Action CombatStarted; public event Action Settled; public event Action Finished; double _stageTime; public LuneMatchRunner(MatchOptions options, int startRound = 1, int extraGold = 0, string profile = "") { Match = NewMatch(options, startRound, extraGold); Clock = new LuneClock(Match.Balance.SimStep); Profile = profile; HumanSlot = options.HumanSlot >= 0 && options.HumanSlot < 6 ? options.HumanSlot : -1; if (HumanSlot >= 0) SettledSeconds = 0.0; EnterPlanning(); } /// Adopts an existing match (U4 network session: the host owns it, the guest mirrors it). public LuneMatchRunner(LuneMatch match, int humanSlot, LuneSession session = null) { Match = match; Clock = new LuneClock(Match.Balance.SimStep); Profile = ""; Session = session; HumanSlot = humanSlot >= 0 && humanSlot < 6 ? humanSlot : -1; if (HumanSlot >= 0) SettledSeconds = 0.0; Current = match.Phase == Phase.Combat ? Stage.Combat : match.Phase == Phase.Over ? Stage.Over : Stage.Planning; _stageTime = 0.0; } /// A match with the public fixtures the golden scenarios use (start round, gold). public static LuneMatch NewMatch(MatchOptions options, int startRound, int extraGold) { var match = new LuneMatch(); match.Setup(options); if (startRound > 1) match.RoundNumber = startRound; if (extraGold != 0) foreach (Player player in match.Players) player.Draft.Gold += extraGold; return match; } void EnterPlanning() { Current = Stage.Planning; _stageTime = 0.0; if (Session != null) return; for (int slot = 0; slot < 6; slot++) { if (slot != HumanSlot) LuneBot.Plan(Match, slot, Profile); } } /// Bound network session (U4); when set, the session owns planning and combat start. public LuneSession Session; /// Advances by simulated seconds (frame time times match speed). public void Advance(double seconds) { StepsThisFrame = 0; switch (Current) { case Stage.Planning: _stageTime += seconds; // A bot match starts after a short look at the plans; a human starts it with Ready. // With a session the host already began the combat when every seat was ready. if (Session != null) { if (Match.Phase == Phase.Combat) StartCombat(); } else if (HumanSlot < 0 ? _stageTime >= PlanningSeconds : Match.AllReady()) StartCombat(); break; case Stage.Combat: StepsThisFrame = Clock.Advance(seconds, StepOnce); if (Match.Phase != Phase.Combat) EndCombat(); break; case Stage.Settled: _stageTime += seconds; if (_stageTime >= SettledSeconds) EnterPlanning(); break; } } /// /// The human's Ready (Godot _on_ready): the plan becomes binding; once every seat is ready /// the combat starts at once. /// public bool Ready() { if (Current != Stage.Planning || HumanSlot < 0 || Match.Phase != Phase.Planning) return false; Match.SetReady(HumanSlot); if (Match.AllReady()) StartCombat(); return true; } void StartCombat() { if (Match.Phase != Phase.Combat) Match.BeginCombat(); Clock.Reset(); Current = Stage.Combat; CombatStarted?.Invoke(Match.Combat); } bool StepOnce() { Match.StepCombat(); Trace?.RecordStep(Match.Combat); return Match.Phase == Phase.Combat; } void EndCombat() { Trace?.RecordSettlement(Match); Settled?.Invoke(Match.LastSettlement); _stageTime = 0.0; if (Match.Phase == Phase.Over || Match.RoundNumber > MaxRound) { Current = Stage.Over; Finished?.Invoke(Match.Winner); } else { Current = Stage.Settled; } } } }