refactor(battlenode): name sender-only vs both-sides handshake checks (§D)

Behavior-preserving; 231 BattleNode tests green.

FrameDispatchContext.BothAfterReady() -> BothSidesAfterReady() (7 call sites). The
4 inline `SenderPhase == AfterReady` checks in TurnEndHandler/TurnEndFinalHandler now
read a new SenderIsAfterReady property. Both carry cross-referencing docs so the
Bot-arm (sender-only) vs PvP-arm (both-sides) distinction is explicit at the type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-05 07:49:27 -04:00
parent 578d0a75ef
commit 9b8a7f1e37
10 changed files with 24 additions and 17 deletions

View File

@@ -26,9 +26,16 @@ internal sealed class FrameDispatchContext
set { if (From is IHasHandshakePhase p && value is { } v) p.Phase = v; }
}
/// <summary>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.</summary>
internal bool BothAfterReady() =>
/// <summary>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
/// <see cref="BothSidesAfterReady"/>, which the PvP arms require. The sender-only vs both-sides
/// distinction is load-bearing for the Bot/PvP split (see TurnEndHandler / TurnEndFinalHandler).</summary>
internal bool SenderIsAfterReady => SenderPhase == HandshakePhase.AfterReady;
/// <summary>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
/// <see cref="SenderIsAfterReady"/> (sender only).</summary>
internal bool BothSidesAfterReady() =>
(A as IHasHandshakePhase)?.Phase == HandshakePhase.AfterReady &&
(B as IHasHandshakePhase)?.Phase == HandshakePhase.AfterReady;
}

View File

@@ -13,7 +13,7 @@ internal sealed class EchoHandler : IFrameHandler
{
public IReadOnlyList<DispatchRoute> 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);

View File

@@ -6,7 +6,7 @@ internal sealed class ForwardWhenBothReadyHandler : IFrameHandler
{
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
{
if (ctx.BothAfterReady())
if (ctx.BothSidesAfterReady())
return new[] { new DispatchRoute(ctx.Other, ctx.Env, Stock.Normal) };
return Array.Empty<DispatchRoute>();
}

View File

@@ -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) };

View File

@@ -14,7 +14,7 @@ internal sealed class PlayActionsHandler : IFrameHandler
{
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
{
if (ctx.Type != BattleType.Pvp || !ctx.BothAfterReady())
if (ctx.Type != BattleType.Pvp || !ctx.BothSidesAfterReady())
return Array.Empty<DispatchRoute>();
var entries = (ctx.Env.Body as RawBody)?.Entries ?? new Dictionary<string, object?>();

View File

@@ -9,7 +9,7 @@ internal sealed class TurnEndActionsHandler : IFrameHandler
{
public IReadOnlyList<DispatchRoute> 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<string, object?>()) };
return new[] { new DispatchRoute(ctx.Other, frame, Stock.Normal) };

View File

@@ -7,11 +7,11 @@ internal sealed class TurnEndFinalHandler : IFrameHandler
public IReadOnlyList<DispatchRoute> 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

View File

@@ -8,14 +8,14 @@ internal sealed class TurnEndHandler : IFrameHandler
public IReadOnlyList<DispatchRoute> 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

View File

@@ -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) };

View File

@@ -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);