diff --git a/SVSim.BattleNode/Sessions/Dispatch/FrameDispatchContext.cs b/SVSim.BattleNode/Sessions/Dispatch/FrameDispatchContext.cs
index 1d8a163..8fe1a59 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/FrameDispatchContext.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/FrameDispatchContext.cs
@@ -26,9 +26,16 @@ internal sealed class FrameDispatchContext
set { if (From is IHasHandshakePhase p && value is { } v) p.Phase = v; }
}
- /// Both participants have completed the handshake. Reads A/B (not From/Other) so the
- /// result is identical regardless of which side sent the frame — matches legacy BothAfterReady.
- internal bool BothAfterReady() =>
+ /// Just the SENDER has finished the handshake — says nothing about the opponent. The
+ /// Bot arms gate on this (the bot has no handshake phase of its own); contrast
+ /// , which the PvP arms require. The sender-only vs both-sides
+ /// distinction is load-bearing for the Bot/PvP split (see TurnEndHandler / TurnEndFinalHandler).
+ internal bool SenderIsAfterReady => SenderPhase == HandshakePhase.AfterReady;
+
+ /// BOTH participants have finished the handshake. Reads A/B (not From/Other) so the
+ /// result is identical regardless of which side sent the frame. Contrast
+ /// (sender only).
+ internal bool BothSidesAfterReady() =>
(A as IHasHandshakePhase)?.Phase == HandshakePhase.AfterReady &&
(B as IHasHandshakePhase)?.Phase == HandshakePhase.AfterReady;
}
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/EchoHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/EchoHandler.cs
index 8efbb50..4aa4da3 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/EchoHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/EchoHandler.cs
@@ -13,7 +13,7 @@ internal sealed class EchoHandler : IFrameHandler
{
public IReadOnlyList Handle(FrameDispatchContext ctx)
{
- if (ctx.Type == BattleType.Pvp && ctx.BothAfterReady())
+ if (ctx.Type == BattleType.Pvp && ctx.BothSidesAfterReady())
{
var orderList = (ctx.Env.Body as RawBody)?.Entries.GetValueOrDefault(WireKeys.OrderList);
ctx.State.RecordTokensFrom(ctx.From, ctx.Other, orderList);
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/ForwardWhenBothReadyHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/ForwardWhenBothReadyHandler.cs
index 761132e..f62b413 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/ForwardWhenBothReadyHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/ForwardWhenBothReadyHandler.cs
@@ -6,7 +6,7 @@ internal sealed class ForwardWhenBothReadyHandler : IFrameHandler
{
public IReadOnlyList Handle(FrameDispatchContext ctx)
{
- if (ctx.BothAfterReady())
+ if (ctx.BothSidesAfterReady())
return new[] { new DispatchRoute(ctx.Other, ctx.Env, Stock.Normal) };
return Array.Empty();
}
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/JudgeHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/JudgeHandler.cs
index 2ec216d..b8a92a1 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/JudgeHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/JudgeHandler.cs
@@ -15,7 +15,7 @@ internal sealed class JudgeHandler : IFrameHandler
// start another one, stalling the loop; confirmed by the 2026-06-03 two-client capture).
// The sender then emits TurnStart, which TurnStartHandler relays to the opponent as {spin}.
// battleCode is dropped; spin=0 for the deterministic-turn slice.
- if (ctx.Type == BattleType.Pvp && ctx.BothAfterReady())
+ if (ctx.Type == BattleType.Pvp && ctx.BothSidesAfterReady())
{
var frame = ctx.Env with { Body = new JudgeBody(Spin: BattleFrameDefaults.DeterministicTurnSpin) };
return new[] { new DispatchRoute(ctx.From, frame, Stock.Normal) };
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs
index 62180f1..58e6e3c 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs
@@ -14,7 +14,7 @@ internal sealed class PlayActionsHandler : IFrameHandler
{
public IReadOnlyList Handle(FrameDispatchContext ctx)
{
- if (ctx.Type != BattleType.Pvp || !ctx.BothAfterReady())
+ if (ctx.Type != BattleType.Pvp || !ctx.BothSidesAfterReady())
return Array.Empty();
var entries = (ctx.Env.Body as RawBody)?.Entries ?? new Dictionary();
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndActionsHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndActionsHandler.cs
index 1ff09f7..f685299 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndActionsHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndActionsHandler.cs
@@ -9,7 +9,7 @@ internal sealed class TurnEndActionsHandler : IFrameHandler
{
public IReadOnlyList Handle(FrameDispatchContext ctx)
{
- if (ctx.Type == BattleType.Pvp && ctx.BothAfterReady())
+ if (ctx.Type == BattleType.Pvp && ctx.BothSidesAfterReady())
{
var frame = ctx.Env with { Body = new RawBody(new Dictionary()) };
return new[] { new DispatchRoute(ctx.Other, frame, Stock.Normal) };
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndFinalHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndFinalHandler.cs
index d02db46..aadaccd 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndFinalHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndFinalHandler.cs
@@ -7,11 +7,11 @@ internal sealed class TurnEndFinalHandler : IFrameHandler
public IReadOnlyList Handle(FrameDispatchContext ctx)
{
// case 4: Bot — Judge to sender only.
- if (ctx.Type == BattleType.Bot && ctx.SenderPhase == HandshakePhase.AfterReady)
+ if (ctx.Type == BattleType.Bot && ctx.SenderIsAfterReady)
return new[] { new DispatchRoute(ctx.From, BattleFrames.BuildJudgeBroadcast(), Stock.Normal) };
// case 9: general — forward the envelope to other + paired BattleFinish + Terminal.
- if (ctx.SenderPhase == HandshakePhase.AfterReady)
+ if (ctx.SenderIsAfterReady)
{
ctx.State.Lifecycle = SessionLifecycle.Terminal;
// Polarity: the SENDER dealt the lethal, so From WINS / Other LOSES. This is the
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndHandler.cs
index 3da849e..e33bc11 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnEndHandler.cs
@@ -8,14 +8,14 @@ internal sealed class TurnEndHandler : IFrameHandler
public IReadOnlyList Handle(FrameDispatchContext ctx)
{
// case 4: Bot — Judge to sender only (no real opponent; client flips back to its local AI).
- if (ctx.Type == BattleType.Bot && ctx.SenderPhase == HandshakePhase.AfterReady)
+ if (ctx.Type == BattleType.Bot && ctx.SenderIsAfterReady)
return new[] { new DispatchRoute(ctx.From, BattleFrames.BuildJudgeBroadcast(), Stock.Normal) };
// case 8: general AfterReady arm — PvP forwards a {turnState} TurnEnd to the opponent
// (handover gate). Any non-Pvp non-Bot type that reaches AfterReady consumes the frame.
- if (ctx.SenderPhase == HandshakePhase.AfterReady)
+ if (ctx.SenderIsAfterReady)
{
- if (ctx.Type == BattleType.Pvp && ctx.BothAfterReady())
+ if (ctx.Type == BattleType.Pvp && ctx.BothSidesAfterReady())
{
// Opponent sees {turnState}; receiving TurnEnd drives ITS SendJudge (handover gate):
// the opponent (the turn taker-over) then sends a Judge, which JudgeHandler reflects
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnStartHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnStartHandler.cs
index 798d157..ef009d3 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnStartHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/TurnStartHandler.cs
@@ -10,7 +10,7 @@ internal sealed class TurnStartHandler : IFrameHandler
{
// PvP: the active player's TurnStart{orderList} is dropped; the opponent receives {spin}
// (spin=0 for the deterministic-turn slice) and self-generates its turn-open.
- if (ctx.Type == BattleType.Pvp && ctx.BothAfterReady())
+ if (ctx.Type == BattleType.Pvp && ctx.BothSidesAfterReady())
{
var frame = ctx.Env with { Body = new OpponentTurnStartBody(Spin: BattleFrameDefaults.DeterministicTurnSpin) };
return new[] { new DispatchRoute(ctx.Other, frame, Stock.Normal) };
diff --git a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs
index e69fca9..d9cdd37 100644
--- a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs
+++ b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs
@@ -711,7 +711,7 @@ public class BattleSessionDispatchTests
{
var (s, a, b) = NewPvpSession();
DriveToAfterReady(s, a);
- // B not AfterReady → not BothAfterReady.
+ // B not AfterReady → not BothSidesAfterReady.
var body = MoveOrderList(3, 10, 20);
body["playIdx"] = 3L; body["type"] = 30L;
var routes = s.ComputeFrames(a, EnvWith(NetworkBattleUri.PlayActions, body));
@@ -719,7 +719,7 @@ public class BattleSessionDispatchTests
}
[Test]
- public void Pvp_Echo_from_A_in_BothAfterReady_is_consumed_not_relayed()
+ public void Pvp_Echo_from_A_in_BothSidesAfterReady_is_consumed_not_relayed()
{
var (s, a, b) = NewPvpSession();
DriveToAfterReady(s, a);
@@ -782,7 +782,7 @@ public class BattleSessionDispatchTests
}
[Test]
- public void Pvp_JudgeResult_from_A_in_BothAfterReady_forwards_to_B()
+ public void Pvp_JudgeResult_from_A_in_BothSidesAfterReady_forwards_to_B()
{
var (s, a, b) = NewPvpSession();
DriveToAfterReady(s, a);