From 477faf3df3b52005bde8fdf6e7ea15c612734fdf Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 3 Jun 2026 14:13:26 -0400 Subject: [PATCH] refactor(battle-node): extract SwapHandler (mulligan barrier) --- SVSim.BattleNode/Sessions/BattleSession.cs | 1 + .../Sessions/Dispatch/Handlers/SwapHandler.cs | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 SVSim.BattleNode/Sessions/Dispatch/Handlers/SwapHandler.cs diff --git a/SVSim.BattleNode/Sessions/BattleSession.cs b/SVSim.BattleNode/Sessions/BattleSession.cs index bbdf5e4..ece7532 100644 --- a/SVSim.BattleNode/Sessions/BattleSession.cs +++ b/SVSim.BattleNode/Sessions/BattleSession.cs @@ -39,6 +39,7 @@ public sealed class BattleSession [NetworkBattleUri.InitNetwork] = new InitNetworkHandler(), [NetworkBattleUri.InitBattle] = new InitBattleHandler(), [NetworkBattleUri.Loaded] = new LoadedHandler(), + [NetworkBattleUri.Swap] = new SwapHandler(), }; private FrameDispatchContext BuildContext(IBattleParticipant from, MsgEnvelope env) => diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/SwapHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/SwapHandler.cs new file mode 100644 index 0000000..efc2db9 --- /dev/null +++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/SwapHandler.cs @@ -0,0 +1,39 @@ +using SVSim.BattleNode.Lifecycle; +using SVSim.BattleNode.Protocol; +using SVSim.BattleNode.Sessions.Participants; // IHasHandshakePhase + +namespace SVSim.BattleNode.Sessions.Dispatch.Handlers; + +internal sealed class SwapHandler : IFrameHandler +{ + public IReadOnlyList Handle(FrameDispatchContext ctx) + { + if (ctx.SenderPhase != BattleSessionPhase.AwaitingSwap) + return Array.Empty(); + + var routes = new List(); + var hand = ScriptedLifecycle.ComputeHandAfterSwap(BattleFrames.ExtractIdxList(ctx.Env)); + + // SwapResponse is always immediate — completes the sender's own mulligan UI. + routes.Add(new DispatchRoute(ctx.From, ScriptedLifecycle.BuildSwapResponse(hand), false)); + ctx.State.PostSwapHands[ctx.From] = hand; + ctx.SenderPhase = BattleSessionPhase.AfterReady; + + // Release Ready to every swapper once all handshake-driving participants have swapped. + // IHasHandshakePhase membership IS the "participates in mulligan" set. + var swappers = new[] { ctx.A, ctx.B }.Where(p => p is IHasHandshakePhase).ToList(); + if (swappers.All(ctx.State.PostSwapHands.ContainsKey)) + { + foreach (var p in swappers) + { + var opponent = ReferenceEquals(p, ctx.A) ? ctx.B : ctx.A; + var ready = opponent is IHasHandshakePhase + && ctx.State.PostSwapHands.TryGetValue(opponent, out var oppoHand) + ? ScriptedLifecycle.BuildReady(ctx.State.PostSwapHands[p], oppoHand) + : ScriptedLifecycle.BuildReady(ctx.State.PostSwapHands[p]); + routes.Add(new DispatchRoute(p, ready, false)); + } + } + return routes; + } +}