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:
@@ -26,9 +26,16 @@ internal sealed class FrameDispatchContext
|
|||||||
set { if (From is IHasHandshakePhase p && value is { } v) p.Phase = v; }
|
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
|
/// <summary>Just the SENDER has finished the handshake — says nothing about the opponent. The
|
||||||
/// result is identical regardless of which side sent the frame — matches legacy BothAfterReady.</summary>
|
/// Bot arms gate on this (the bot has no handshake phase of its own); contrast
|
||||||
internal bool BothAfterReady() =>
|
/// <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 &&
|
(A as IHasHandshakePhase)?.Phase == HandshakePhase.AfterReady &&
|
||||||
(B as IHasHandshakePhase)?.Phase == HandshakePhase.AfterReady;
|
(B as IHasHandshakePhase)?.Phase == HandshakePhase.AfterReady;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ internal sealed class EchoHandler : IFrameHandler
|
|||||||
{
|
{
|
||||||
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
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);
|
var orderList = (ctx.Env.Body as RawBody)?.Entries.GetValueOrDefault(WireKeys.OrderList);
|
||||||
ctx.State.RecordTokensFrom(ctx.From, ctx.Other, orderList);
|
ctx.State.RecordTokensFrom(ctx.From, ctx.Other, orderList);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ internal sealed class ForwardWhenBothReadyHandler : IFrameHandler
|
|||||||
{
|
{
|
||||||
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
||||||
{
|
{
|
||||||
if (ctx.BothAfterReady())
|
if (ctx.BothSidesAfterReady())
|
||||||
return new[] { new DispatchRoute(ctx.Other, ctx.Env, Stock.Normal) };
|
return new[] { new DispatchRoute(ctx.Other, ctx.Env, Stock.Normal) };
|
||||||
return Array.Empty<DispatchRoute>();
|
return Array.Empty<DispatchRoute>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ internal sealed class JudgeHandler : IFrameHandler
|
|||||||
// start another one, stalling the loop; confirmed by the 2026-06-03 two-client capture).
|
// 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}.
|
// The sender then emits TurnStart, which TurnStartHandler relays to the opponent as {spin}.
|
||||||
// battleCode is dropped; spin=0 for the deterministic-turn slice.
|
// 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) };
|
var frame = ctx.Env with { Body = new JudgeBody(Spin: BattleFrameDefaults.DeterministicTurnSpin) };
|
||||||
return new[] { new DispatchRoute(ctx.From, frame, Stock.Normal) };
|
return new[] { new DispatchRoute(ctx.From, frame, Stock.Normal) };
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ internal sealed class PlayActionsHandler : IFrameHandler
|
|||||||
{
|
{
|
||||||
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
||||||
{
|
{
|
||||||
if (ctx.Type != BattleType.Pvp || !ctx.BothAfterReady())
|
if (ctx.Type != BattleType.Pvp || !ctx.BothSidesAfterReady())
|
||||||
return Array.Empty<DispatchRoute>();
|
return Array.Empty<DispatchRoute>();
|
||||||
|
|
||||||
var entries = (ctx.Env.Body as RawBody)?.Entries ?? new Dictionary<string, object?>();
|
var entries = (ctx.Env.Body as RawBody)?.Entries ?? new Dictionary<string, object?>();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ internal sealed class TurnEndActionsHandler : IFrameHandler
|
|||||||
{
|
{
|
||||||
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
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?>()) };
|
var frame = ctx.Env with { Body = new RawBody(new Dictionary<string, object?>()) };
|
||||||
return new[] { new DispatchRoute(ctx.Other, frame, Stock.Normal) };
|
return new[] { new DispatchRoute(ctx.Other, frame, Stock.Normal) };
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ internal sealed class TurnEndFinalHandler : IFrameHandler
|
|||||||
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
||||||
{
|
{
|
||||||
// case 4: Bot — Judge to sender only.
|
// 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) };
|
return new[] { new DispatchRoute(ctx.From, BattleFrames.BuildJudgeBroadcast(), Stock.Normal) };
|
||||||
|
|
||||||
// case 9: general — forward the envelope to other + paired BattleFinish + Terminal.
|
// case 9: general — forward the envelope to other + paired BattleFinish + Terminal.
|
||||||
if (ctx.SenderPhase == HandshakePhase.AfterReady)
|
if (ctx.SenderIsAfterReady)
|
||||||
{
|
{
|
||||||
ctx.State.Lifecycle = SessionLifecycle.Terminal;
|
ctx.State.Lifecycle = SessionLifecycle.Terminal;
|
||||||
// Polarity: the SENDER dealt the lethal, so From WINS / Other LOSES. This is the
|
// Polarity: the SENDER dealt the lethal, so From WINS / Other LOSES. This is the
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ internal sealed class TurnEndHandler : IFrameHandler
|
|||||||
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
public IReadOnlyList<DispatchRoute> Handle(FrameDispatchContext ctx)
|
||||||
{
|
{
|
||||||
// case 4: Bot — Judge to sender only (no real opponent; client flips back to its local AI).
|
// 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) };
|
return new[] { new DispatchRoute(ctx.From, BattleFrames.BuildJudgeBroadcast(), Stock.Normal) };
|
||||||
|
|
||||||
// case 8: general AfterReady arm — PvP forwards a {turnState} TurnEnd to the opponent
|
// 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.
|
// (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):
|
// Opponent sees {turnState}; receiving TurnEnd drives ITS SendJudge (handover gate):
|
||||||
// the opponent (the turn taker-over) then sends a Judge, which JudgeHandler reflects
|
// the opponent (the turn taker-over) then sends a Judge, which JudgeHandler reflects
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ internal sealed class TurnStartHandler : IFrameHandler
|
|||||||
{
|
{
|
||||||
// PvP: the active player's TurnStart{orderList} is dropped; the opponent receives {spin}
|
// 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.
|
// (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) };
|
var frame = ctx.Env with { Body = new OpponentTurnStartBody(Spin: BattleFrameDefaults.DeterministicTurnSpin) };
|
||||||
return new[] { new DispatchRoute(ctx.Other, frame, Stock.Normal) };
|
return new[] { new DispatchRoute(ctx.Other, frame, Stock.Normal) };
|
||||||
|
|||||||
@@ -711,7 +711,7 @@ public class BattleSessionDispatchTests
|
|||||||
{
|
{
|
||||||
var (s, a, b) = NewPvpSession();
|
var (s, a, b) = NewPvpSession();
|
||||||
DriveToAfterReady(s, a);
|
DriveToAfterReady(s, a);
|
||||||
// B not AfterReady → not BothAfterReady.
|
// B not AfterReady → not BothSidesAfterReady.
|
||||||
var body = MoveOrderList(3, 10, 20);
|
var body = MoveOrderList(3, 10, 20);
|
||||||
body["playIdx"] = 3L; body["type"] = 30L;
|
body["playIdx"] = 3L; body["type"] = 30L;
|
||||||
var routes = s.ComputeFrames(a, EnvWith(NetworkBattleUri.PlayActions, body));
|
var routes = s.ComputeFrames(a, EnvWith(NetworkBattleUri.PlayActions, body));
|
||||||
@@ -719,7 +719,7 @@ public class BattleSessionDispatchTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[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();
|
var (s, a, b) = NewPvpSession();
|
||||||
DriveToAfterReady(s, a);
|
DriveToAfterReady(s, a);
|
||||||
@@ -782,7 +782,7 @@ public class BattleSessionDispatchTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[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();
|
var (s, a, b) = NewPvpSession();
|
||||||
DriveToAfterReady(s, a);
|
DriveToAfterReady(s, a);
|
||||||
|
|||||||
Reference in New Issue
Block a user