refactor(battle-node): move session phase + post-swap hands into BattleSessionState

This commit is contained in:
gamer147
2026-06-03 13:47:35 -04:00
parent 95554cee04
commit 85c43a9a72
2 changed files with 21 additions and 16 deletions

View File

@@ -22,19 +22,13 @@ public sealed class BattleSession
{
private readonly ILogger<BattleSession> _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<IBattleParticipant, long[]> _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<BattleSession> 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

View File

@@ -0,0 +1,11 @@
namespace SVSim.BattleNode.Sessions.Dispatch;
/// <summary>Mutable per-session state shared across frame handlers. Today: the session-level
/// phase (only ever advanced to <see cref="BattleSessionPhase.Terminal"/>) and the mulligan
/// barrier's post-swap hands. FUTURE (PvP equivalency, NOT this refactor): per-side idx->cardId
/// maps + reveal-gating state land here.</summary>
internal sealed class BattleSessionState
{
public BattleSessionPhase SessionPhase { get; set; } = BattleSessionPhase.AwaitingInitNetwork;
public Dictionary<IBattleParticipant, long[]> PostSwapHands { get; } = new();
}