refactor(battle-node): move handshake phase reads to per-participant
ComputeFrames now reads (from as IHasHandshakePhase)?.Phase for the four handshake arms (InitNetwork, InitBattle, Loaded, Swap) and the TurnEnd gate, transitioning the participant's Phase instead of the session's. RealParticipant implements IHasHandshakePhase via the new Phase property; the session-level BattleSession.Phase stays for the Terminal short-circuit. Scripted dispatch + wire shape unchanged (single-Real-participant case collapses to Phase 1 semantics). Test fixture migrates FakeParticipant to FakeRealParticipant for the side that drives handshake states. The bot's TurnEnd previously rode the session-level AfterReady arm; with that arm now gated on the sender's per-participant Phase (which the bot lacks), TurnEnd joins TurnStart/Judge in the scripted-bot forwarder arm so the v1.2 burst still reaches the real participant.
This commit is contained in:
@@ -5,6 +5,7 @@ using SVSim.BattleNode.Lifecycle;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
using SVSim.BattleNode.Protocol.Bodies;
|
||||
using SVSim.BattleNode.Sessions;
|
||||
using SVSim.BattleNode.Sessions.Participants;
|
||||
|
||||
namespace SVSim.UnitTests.BattleNode.Sessions;
|
||||
|
||||
@@ -21,7 +22,7 @@ public class BattleSessionDispatchTests
|
||||
Assert.That(routes[0].Target, Is.SameAs(a));
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.InitNetwork));
|
||||
Assert.That(routes[0].NoStock, Is.True);
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitBattle));
|
||||
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitBattle));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -34,7 +35,7 @@ public class BattleSessionDispatchTests
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(a));
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.Matched));
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingLoaded));
|
||||
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AwaitingLoaded));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -48,7 +49,7 @@ public class BattleSessionDispatchTests
|
||||
Assert.That(routes.Select(r => r.Frame.Uri),
|
||||
Is.EqualTo(new[] { NetworkBattleUri.BattleStart, NetworkBattleUri.Deal }));
|
||||
Assert.That(routes.All(r => ReferenceEquals(r.Target, a)), Is.True);
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingSwap));
|
||||
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AwaitingSwap));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -62,7 +63,7 @@ public class BattleSessionDispatchTests
|
||||
|
||||
Assert.That(routes.Select(r => r.Frame.Uri),
|
||||
Is.EqualTo(new[] { NetworkBattleUri.Swap, NetworkBattleUri.Ready }));
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AfterReady));
|
||||
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AfterReady));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -78,7 +79,30 @@ public class BattleSessionDispatchTests
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(b));
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.TurnEnd));
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AfterReady));
|
||||
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AfterReady));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Handshake_dispatch_reads_per_participant_Phase_not_session_Phase()
|
||||
{
|
||||
var a = new FakeRealParticipant(viewerId: 1, FixtureCtx());
|
||||
var b = new FakeRealParticipant(viewerId: 2, FixtureCtx());
|
||||
var s = new BattleSession("bid-1", BattleType.Pvp, a, b, NullLogger<BattleSession>.Instance);
|
||||
|
||||
// A is AwaitingInitNetwork; B is AwaitingInitBattle (manually set).
|
||||
b.Phase = BattleSessionPhase.AwaitingInitBattle;
|
||||
|
||||
// A's InitNetwork should ack (matches A's phase).
|
||||
var routesA = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
Assert.That(routesA.Count, Is.EqualTo(1));
|
||||
Assert.That(routesA[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.InitNetwork));
|
||||
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitBattle));
|
||||
|
||||
// B's InitBattle should produce Matched (matches B's phase, set above).
|
||||
var routesB = s.ComputeFrames(b, NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
Assert.That(routesB.Count, Is.EqualTo(1));
|
||||
Assert.That(routesB[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.Matched));
|
||||
Assert.That(b.Phase, Is.EqualTo(BattleSessionPhase.AwaitingLoaded));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -162,12 +186,12 @@ public class BattleSessionDispatchTests
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Swap));
|
||||
|
||||
Assert.That(routes, Is.Empty);
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitNetwork));
|
||||
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitNetwork));
|
||||
}
|
||||
|
||||
private static (BattleSession, FakeParticipant, FakeParticipant) NewSession()
|
||||
private static (BattleSession, FakeRealParticipant, FakeParticipant) NewSession()
|
||||
{
|
||||
var a = new FakeParticipant(viewerId: 1, FixtureCtx());
|
||||
var a = new FakeRealParticipant(viewerId: 1, FixtureCtx());
|
||||
var b = new FakeParticipant(viewerId: ScriptedLifecycle.FakeOpponentViewerId, ScriptedBotContext());
|
||||
var s = new BattleSession("bid-1", BattleType.Scripted, a, b, NullLogger<BattleSession>.Instance);
|
||||
return (s, a, b);
|
||||
@@ -206,4 +230,22 @@ public class BattleSessionDispatchTests
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
private void Touch() => FrameEmitted?.Invoke(null!, default);
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="FakeParticipant"/> but additionally implements
|
||||
/// <see cref="IHasHandshakePhase"/> so the dispatch tests can drive a participant's
|
||||
/// Phase without instantiating a full <c>RealParticipant</c> (which needs a real
|
||||
/// WebSocket).</summary>
|
||||
private sealed class FakeRealParticipant : IBattleParticipant, IHasHandshakePhase
|
||||
{
|
||||
public long ViewerId { get; }
|
||||
public MatchContext Context { get; }
|
||||
public BattleSessionPhase Phase { get; set; } = BattleSessionPhase.AwaitingInitNetwork;
|
||||
public event Func<MsgEnvelope, CancellationToken, Task>? FrameEmitted;
|
||||
public FakeRealParticipant(long viewerId, MatchContext context) { ViewerId = viewerId; Context = context; }
|
||||
public Task PushAsync(MsgEnvelope env, bool noStock, CancellationToken ct) => Task.CompletedTask;
|
||||
public Task RunAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
public Task TerminateAsync(BattleFinishReason reason) => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
private void Touch() => FrameEmitted?.Invoke(null!, default);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user