refactor(battlenode): split BattleSessionPhase into HandshakePhase + SessionLifecycle

Behavior-preserving; 231 BattleNode tests green.

One enum conflated two axes. Split:
- HandshakePhase (per participant): AwaitingInitNetwork..AfterReady. On
  IHasHandshakePhase.Phase, FrameDispatchContext.SenderPhase, the handler gates.
- SessionLifecycle (per battle): Active | Terminal. On the renamed
  BattleSessionState.Lifecycle (was SessionPhase, defaulting to a handshake value)
  and BattleSession.Lifecycle (was Phase). Reads are only != Terminal, so the
  Active default is behavior-identical.

OpponentTurn was dead (never assigned) -> dropped. BattleSessionPhase deleted; the
two axes can no longer be cross-assigned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-05 07:21:59 -04:00
parent 7d4da69f22
commit 3e8901eec3
17 changed files with 82 additions and 65 deletions

View File

@@ -103,7 +103,7 @@ public class BattleSessionDispatchConcurrencyTests
private readonly ConcurrencyDetector _detector;
public long ViewerId { get; }
public MatchContext Context { get; }
public BattleSessionPhase Phase { get; set; } = BattleSessionPhase.AwaitingInitNetwork;
public HandshakePhase Phase { get; set; } = HandshakePhase.AwaitingInitNetwork;
public event Func<MsgEnvelope, CancellationToken, Task>? FrameEmitted;
public ProbeParticipant(long viewerId, MatchContext context, ConcurrencyDetector detector)

View File

@@ -44,19 +44,19 @@ public class BattleSessionDispatchTests
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;
b.Phase = HandshakePhase.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));
Assert.That(a.Phase, Is.EqualTo(HandshakePhase.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));
Assert.That(b.Phase, Is.EqualTo(HandshakePhase.AwaitingLoaded));
}
[Test]
@@ -66,7 +66,7 @@ public class BattleSessionDispatchTests
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Swap));
Assert.That(routes, Is.Empty);
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitNetwork));
Assert.That(a.Phase, Is.EqualTo(HandshakePhase.AwaitingInitNetwork));
}
[Test]
@@ -169,7 +169,7 @@ public class BattleSessionDispatchTests
Assert.That(routes.Select(r => r.Frame.Uri), Is.EqualTo(new[] { NetworkBattleUri.Swap }),
"Ready is withheld until BOTH sides have mulliganed.");
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AfterReady),
Assert.That(a.Phase, Is.EqualTo(HandshakePhase.AfterReady),
"Phase advances on Swap even though Ready is withheld.");
}
@@ -833,7 +833,7 @@ public class BattleSessionDispatchTests
Assert.That(routes[2].Frame.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
Assert.That(((BattleFinishBody)routes[2].Frame.Body).Result, Is.EqualTo(BattleResult.LifeLose));
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.Terminal));
Assert.That(s.Lifecycle, Is.EqualTo(SessionLifecycle.Terminal));
}
[Test]
@@ -866,7 +866,7 @@ public class BattleSessionDispatchTests
Assert.That(((BattleFinishBody)bRoute.Frame.Body).Result, Is.EqualTo(BattleResult.RetireWin));
Assert.That(aRoute.Stock, Is.EqualTo(Stock.Bypass));
Assert.That(bRoute.Stock, Is.EqualTo(Stock.Bypass));
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.Terminal));
Assert.That(s.Lifecycle, Is.EqualTo(SessionLifecycle.Terminal));
}
[Test]
@@ -879,7 +879,7 @@ public class BattleSessionDispatchTests
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Kill));
Assert.That(routes.Count, Is.EqualTo(2));
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.Terminal));
Assert.That(s.Lifecycle, Is.EqualTo(SessionLifecycle.Terminal));
}
private static (BattleSession, FakeRealParticipant, FakeParticipant) NewBotSession()
@@ -905,7 +905,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.InitNetwork));
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitBattle));
Assert.That(a.Phase, Is.EqualTo(HandshakePhase.AwaitingInitBattle));
}
[Test]
@@ -923,7 +923,7 @@ public class BattleSessionDispatchTests
Assert.That(routes[0].Target, Is.SameAs(a));
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.InitBattle),
"Expected an ack envelope for InitBattle, NOT a Matched envelope.");
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AwaitingLoaded));
Assert.That(a.Phase, Is.EqualTo(HandshakePhase.AwaitingLoaded));
}
[Test]
@@ -939,7 +939,7 @@ public class BattleSessionDispatchTests
// handler at Matching.cs:417 → SetNetworkInfo overwrites it with our
// placeholder NoOpBotParticipant.Context zeros).
Assert.That(routes, Is.Empty, "Bot Loaded is silent.");
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AwaitingSwap),
Assert.That(a.Phase, Is.EqualTo(HandshakePhase.AwaitingSwap),
"Phase still advances even though there are no outbound routes.");
}
@@ -956,7 +956,7 @@ public class BattleSessionDispatchTests
Assert.That(routes.Select(r => r.Frame.Uri),
Is.EqualTo(new[] { NetworkBattleUri.Swap, NetworkBattleUri.Ready }));
Assert.That(routes.All(r => ReferenceEquals(r.Target, a)), Is.True);
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AfterReady));
Assert.That(a.Phase, Is.EqualTo(HandshakePhase.AfterReady));
}
[Test]
@@ -1114,7 +1114,7 @@ public class BattleSessionDispatchTests
{
public long ViewerId { get; }
public MatchContext Context { get; }
public BattleSessionPhase Phase { get; set; } = BattleSessionPhase.AwaitingInitNetwork;
public HandshakePhase Phase { get; set; } = HandshakePhase.AwaitingInitNetwork;
public event Func<MsgEnvelope, CancellationToken, Task>? FrameEmitted;
public FakeRealParticipant(long viewerId, MatchContext context) { ViewerId = viewerId; Context = context; }
public Task PushAsync(MsgEnvelope env, Stock stock, CancellationToken ct) => Task.CompletedTask;

View File

@@ -97,7 +97,7 @@ public class RealParticipantTests
var p = new RealParticipant(ws, viewerId: 1, FixtureCtx(),
NullLogger<RealParticipant>.Instance);
Assert.That(p.Phase, Is.EqualTo(SVSim.BattleNode.Sessions.BattleSessionPhase.AwaitingInitNetwork));
Assert.That(p.Phase, Is.EqualTo(SVSim.BattleNode.Sessions.HandshakePhase.AwaitingInitNetwork));
}
[Test]
@@ -108,9 +108,9 @@ public class RealParticipantTests
NullLogger<RealParticipant>.Instance);
// Setter is `internal`; SVSim.UnitTests has InternalsVisibleTo on SVSim.BattleNode.
p.Phase = SVSim.BattleNode.Sessions.BattleSessionPhase.AfterReady;
p.Phase = SVSim.BattleNode.Sessions.HandshakePhase.AfterReady;
Assert.That(p.Phase, Is.EqualTo(SVSim.BattleNode.Sessions.BattleSessionPhase.AfterReady));
Assert.That(p.Phase, Is.EqualTo(SVSim.BattleNode.Sessions.HandshakePhase.AfterReady));
}
[Test]
@@ -121,9 +121,9 @@ public class RealParticipantTests
var a = new RealParticipant(wsA, viewerId: 1, FixtureCtx(), NullLogger<RealParticipant>.Instance);
var b = new RealParticipant(wsB, viewerId: 2, FixtureCtx(), NullLogger<RealParticipant>.Instance);
a.Phase = SVSim.BattleNode.Sessions.BattleSessionPhase.AfterReady;
a.Phase = SVSim.BattleNode.Sessions.HandshakePhase.AfterReady;
Assert.That(b.Phase, Is.EqualTo(SVSim.BattleNode.Sessions.BattleSessionPhase.AwaitingInitNetwork),
Assert.That(b.Phase, Is.EqualTo(SVSim.BattleNode.Sessions.HandshakePhase.AwaitingInitNetwork),
"B's Phase must not change when A's Phase is set.");
}