Der M4-Umfang ist als Protokoll 4 portiert, serverautoritativ. Der Host besitzt LuneMatch und rechnet jeden Kampf; Clients senden nur Absichten und zeigen den Kampf aus Host-Momentaufnahmen. - LuneSession (Core/Net): Rollen offline/Host/Gast, sechs Sitze, Lobby, Hallo mit Protokollprüfung und Namenskürzung, Beitritt und Wieder- einstieg über den Anzeigenamen, private Planung mit Spoof-Schutz, Bereit ist verbindlich, Bots planen einmal je Runde, Disconnect gibt den Platz an einen Bot (nie Kernverlust), Übernahme verwirft den unbestätigten Bot-Entwurf, Revanche hält die Plätze, Hostverlust meldet und führt ins Menü zurück. - Kampfsynchronisation: Beginn mit Einheitenätzen und Gebäude-HP, alle 0,05 s neue Einheiten und ein Frame (13 Werte je Einheit), späte Frames über die Serial verworfen, Gast interpoliert (frame_alpha). - Transport: LuneLoopback (In-Prozess) für die Tests, LuneTcp + LuneWire (Cursor-Codec) für Echtverbindungen statt ENet; ein Socket je Gast. - Oberfläche: LuneMainMenu (Offline/Hosten/Beitreten, Bund, Lane, Name, Adresse, Port) als Startbildschirm, LuneNetPlay als Schale wie lune_main.gd. LuneGame leitet Befehle, Rückgängig und Bereit im Netzspiel über die Sitzung; Tempo und Pause bestimmt der Host. - Tests: NetSuite 43/43 (Port von test_lune_net.gd), NetRoundsSuite 12/12 (Port von net_rounds.py: 5 Runden, Prüfsummen gleich, Gast rechnet 0 Schritte, Drop und Wiedereinstieg auf denselben Sitz), LuneWireTests Round-trip. EditMode 29/29, PlayMode 13/13, Kerntests 457/457. Fingerabdrücke der Bot-Partien unverändert. - Dokumentation: U4-NETWORK (Abnahme, Grenzen), PORT-QA (U4), PORT-STATUS (U4 fertig, U5 nächster Schritt), KNOWN-BEHAVIOR-DIFFERENCES. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
6.2 KiB
C#
168 lines
6.2 KiB
C#
using System;
|
|
using Luneraria.Core;
|
|
|
|
namespace Luneraria.View
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class LuneMatchRunner
|
|
{
|
|
public enum Stage { Planning, Combat, Settled, Over }
|
|
|
|
public readonly LuneMatch Match;
|
|
public readonly LuneClock Clock;
|
|
public readonly string Profile;
|
|
/// <summary>The seat a human plans through the HUD; -1 for a bot match.</summary>
|
|
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<LuneCombat> CombatStarted;
|
|
public event Action<Settlement> Settled;
|
|
public event Action<int> 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();
|
|
}
|
|
|
|
/// <summary>Adopts an existing match (U4 network session: the host owns it, the guest mirrors it).</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>A match with the public fixtures the golden scenarios use (start round, gold).</summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>Bound network session (U4); when set, the session owns planning and combat start.</summary>
|
|
public LuneSession Session;
|
|
|
|
/// <summary>Advances by simulated seconds (frame time times match speed).</summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The human's Ready (Godot _on_ready): the plan becomes binding; once every seat is ready
|
|
/// the combat starts at once.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|