From 85c43a9a72a6fbcaeb2a82820ae553bcbd4a5d10 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 3 Jun 2026 13:47:35 -0400 Subject: [PATCH] refactor(battle-node): move session phase + post-swap hands into BattleSessionState --- SVSim.BattleNode/Sessions/BattleSession.cs | 26 +++++++------------ .../Sessions/Dispatch/BattleSessionState.cs | 11 ++++++++ 2 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 SVSim.BattleNode/Sessions/Dispatch/BattleSessionState.cs diff --git a/SVSim.BattleNode/Sessions/BattleSession.cs b/SVSim.BattleNode/Sessions/BattleSession.cs index 4e9e690..d5fab1e 100644 --- a/SVSim.BattleNode/Sessions/BattleSession.cs +++ b/SVSim.BattleNode/Sessions/BattleSession.cs @@ -22,19 +22,13 @@ public sealed class BattleSession { private readonly ILogger _log; - // Mulligan barrier: each side's post-mulligan hand, captured on its Swap. Ready is - // withheld until every IHasHandshakePhase participant has swapped (prod withholds it - // ~3-6 s — see data_dumps/captures/battle-traffic_tk2_*.ndjson). Keyed on the - // IBattleParticipant so the per-side hand is retrievable when releasing both Readys. - // NOTE: mutated from ComputeFrames, which is not lock-guarded — same pre-existing - // single-threaded-dispatch assumption as the Phase mutations (see plan § Out of scope). - private readonly Dictionary _postSwapHands = new(); + private readonly BattleSessionState _state = new(); public string BattleId { get; } public BattleType Type { get; } public IBattleParticipant A { get; } public IBattleParticipant B { get; } - public BattleSessionPhase Phase { get; private set; } = BattleSessionPhase.AwaitingInitNetwork; + public BattleSessionPhase Phase => _state.SessionPhase; public BattleSession(string battleId, BattleType type, IBattleParticipant a, IBattleParticipant b, ILogger log) @@ -82,7 +76,7 @@ public sealed class BattleSession "BattleSession {Bid}: failed to push BattleFinish to survivor (their WS may also be closed)", BattleId); } - Phase = BattleSessionPhase.Terminal; + _state.SessionPhase = BattleSessionPhase.Terminal; } cts.Cancel(); // unblock the survivor's RunAsync read loop @@ -235,7 +229,7 @@ public sealed class BattleSession var hand = ScriptedLifecycle.ComputeHandAfterSwap(ExtractIdxList(env)); // SwapResponse is always immediate — it completes the sender's own mulligan UI. result.Add(new DispatchRoute(from, ScriptedLifecycle.BuildSwapResponse(hand), false)); - _postSwapHands[from] = hand; + _state.PostSwapHands[from] = hand; phaseFrom!.Phase = BattleSessionPhase.AfterReady; // Release Ready to every swapper once all handshake-driving participants have @@ -244,14 +238,14 @@ public sealed class BattleSession // Scripted → {player, bot} (bot now emits Swap) → waits for both // Bot/AINet → {real} only (NoOp isn't a phase impl)→ releases on the one Swap var swappers = new[] { A, B }.Where(p => p is IHasHandshakePhase).ToList(); - if (swappers.All(_postSwapHands.ContainsKey)) + if (swappers.All(_state.PostSwapHands.ContainsKey)) { foreach (var p in swappers) { var opponent = ReferenceEquals(p, A) ? B : A; - var ready = opponent is IHasHandshakePhase && _postSwapHands.TryGetValue(opponent, out var oppoHand) - ? ScriptedLifecycle.BuildReady(_postSwapHands[p], oppoHand) // both hands known - : ScriptedLifecycle.BuildReady(_postSwapHands[p]); // non-interactive opponent + var ready = opponent is IHasHandshakePhase && _state.PostSwapHands.TryGetValue(opponent, out var oppoHand) + ? ScriptedLifecycle.BuildReady(_state.PostSwapHands[p], oppoHand) // both hands known + : ScriptedLifecycle.BuildReady(_state.PostSwapHands[p]); // non-interactive opponent result.Add(new DispatchRoute(p, ready, false)); } } @@ -290,7 +284,7 @@ public sealed class BattleSession result.Add(new DispatchRoute(other, env, false)); result.Add(new DispatchRoute(from, BuildBattleFinish(BattleResult.LifeWin), true)); result.Add(new DispatchRoute(other, BuildBattleFinish(BattleResult.LifeLose), true)); - Phase = BattleSessionPhase.Terminal; + _state.SessionPhase = BattleSessionPhase.Terminal; break; // Retire / Kill: sender concedes (Retire) or the client requested an immediate @@ -300,7 +294,7 @@ public sealed class BattleSession case NetworkBattleUri.Kill: result.Add(new DispatchRoute(from, BuildBattleFinish(BattleResult.RetireLose), true)); result.Add(new DispatchRoute(other, BuildBattleFinish(BattleResult.RetireWin), true)); - Phase = BattleSessionPhase.Terminal; + _state.SessionPhase = BattleSessionPhase.Terminal; break; // Frames emitted by the scripted bot (TurnStart / TurnEnd / Judge) — forward diff --git a/SVSim.BattleNode/Sessions/Dispatch/BattleSessionState.cs b/SVSim.BattleNode/Sessions/Dispatch/BattleSessionState.cs new file mode 100644 index 0000000..3a53df5 --- /dev/null +++ b/SVSim.BattleNode/Sessions/Dispatch/BattleSessionState.cs @@ -0,0 +1,11 @@ +namespace SVSim.BattleNode.Sessions.Dispatch; + +/// Mutable per-session state shared across frame handlers. Today: the session-level +/// phase (only ever advanced to ) and the mulligan +/// barrier's post-swap hands. FUTURE (PvP equivalency, NOT this refactor): per-side idx->cardId +/// maps + reveal-gating state land here. +internal sealed class BattleSessionState +{ + public BattleSessionPhase SessionPhase { get; set; } = BattleSessionPhase.AwaitingInitNetwork; + public Dictionary PostSwapHands { get; } = new(); +}