refactor(battle-node): drop old BattleSession; rename V2 -> BattleSession
Old single-WS BattleSession + its dispatch/pump/ClipAckArg tests are obsolete after the Task 9 handler cutover. ClipAckArg overflow + boundary coverage moved into RealParticipantTests. BattleSessionV2 renamed back to BattleSession; the V2 suffix was a placeholder during the parallel -build refactor.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NUnit.Framework;
|
||||
using SVSim.BattleNode.Bridge;
|
||||
using SVSim.BattleNode.Lifecycle;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
using SVSim.BattleNode.Protocol.Bodies;
|
||||
using SVSim.BattleNode.Sessions;
|
||||
@@ -11,164 +11,199 @@ namespace SVSim.UnitTests.BattleNode.Sessions;
|
||||
[TestFixture]
|
||||
public class BattleSessionDispatchTests
|
||||
{
|
||||
private static BattleSession NewSession()
|
||||
{
|
||||
// ws is unused by ComputeResponses; pass null! and rely on the test never invoking the pump.
|
||||
return new BattleSession(ws: null!, battleId: "bid-1", viewerId: 1, context: FixtureCtx(), log: NullLogger<BattleSession>.Instance);
|
||||
}
|
||||
|
||||
private static MatchContext FixtureCtx() => new(
|
||||
SelfDeckCardIds: Enumerable.Range(1, 30).Select(i => 100_011_010L).ToList(),
|
||||
ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015",
|
||||
CountryCode: "KOR", UserName: "Player", SleeveId: "3000011",
|
||||
EmblemId: "701441011", DegreeId: "300003", FieldId: 43, IsOfficial: 0,
|
||||
BattleType: 11);
|
||||
|
||||
private static MsgEnvelope NewEnvelope(NetworkBattleUri uri) =>
|
||||
new(uri, ViewerId: 1, Uuid: "u", Bid: null, Try: 0, Cat: EmitCategory.Battle,
|
||||
PubSeq: null, PlaySeq: null, Body: new RawBody(new Dictionary<string, object?>()));
|
||||
|
||||
[Test]
|
||||
public void InitNetwork_PushesAckOnly_TransitionsToAwaitingInitBattle()
|
||||
public void InitNetwork_acks_to_sender_transitions_to_AwaitingInitBattle()
|
||||
{
|
||||
var s = NewSession();
|
||||
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
Assert.That(responses.Select(r => r.Envelope.Uri),
|
||||
Is.EqualTo(new[] { NetworkBattleUri.InitNetwork }));
|
||||
var (s, a, b) = NewSession();
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
|
||||
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(routes[0].NoStock, Is.True);
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitBattle));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InitBattle_PushesMatched_TransitionsToAwaitingLoaded()
|
||||
public void InitBattle_pushes_Matched_to_sender_only()
|
||||
{
|
||||
var s = NewSession();
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
Assert.That(responses.Single().Envelope.Uri, Is.EqualTo(NetworkBattleUri.Matched));
|
||||
var (s, a, b) = NewSession();
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Loaded_PushesBattleStartThenDeal_TransitionsToAwaitingSwap()
|
||||
public void Loaded_pushes_BattleStart_then_Deal_to_sender()
|
||||
{
|
||||
var s = NewSession();
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Loaded));
|
||||
Assert.That(responses.Select(r => r.Envelope.Uri),
|
||||
var (s, a, b) = NewSession();
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Loaded));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Swap_WithIdxListContainingTwo_ProducesHandWithFreshIdxAtPosition1()
|
||||
public void Swap_pushes_SwapResponse_then_Ready_to_sender()
|
||||
{
|
||||
var s = NewSession();
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Loaded));
|
||||
// Simulate the client's Swap{idxList:[2]}: the dict shape produced by MsgEnvelope.FromJson
|
||||
// (a List<object?> of boxed long values), wrapped in a RawBody as the inbound type.
|
||||
var swapEnv = new MsgEnvelope(
|
||||
NetworkBattleUri.Swap, ViewerId: 1, Uuid: "u", Bid: null, Try: 0,
|
||||
Cat: EmitCategory.Battle, PubSeq: null, PlaySeq: null,
|
||||
Body: new RawBody(new Dictionary<string, object?>
|
||||
{
|
||||
["idxList"] = new List<object?> { 2L },
|
||||
}));
|
||||
var (s, a, b) = NewSession();
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Loaded));
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Swap));
|
||||
|
||||
var responses = s.ComputeResponses(swapEnv);
|
||||
|
||||
var swapBody = (SwapResponseBody)responses[0].Envelope.Body;
|
||||
Assert.That(swapBody.Self[0].Idx, Is.EqualTo(1));
|
||||
Assert.That(swapBody.Self[1].Idx, Is.EqualTo(4)); // swapped — fresh deck idx
|
||||
Assert.That(swapBody.Self[2].Idx, Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Swap_PushesSwapResponseThenReady_TransitionsToAfterReady()
|
||||
{
|
||||
var s = NewSession();
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Loaded));
|
||||
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Swap));
|
||||
Assert.That(responses.Select(r => r.Envelope.Uri),
|
||||
Assert.That(routes.Select(r => r.Frame.Uri),
|
||||
Is.EqualTo(new[] { NetworkBattleUri.Swap, NetworkBattleUri.Ready }));
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AfterReady));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TurnEnd_AfterReady_PushesTurnStart_TurnEnd_Judge_StaysInAfterReady()
|
||||
public void TurnEnd_from_real_forwards_to_other_participant()
|
||||
{
|
||||
var s = NewSession();
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Loaded));
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Swap));
|
||||
var (s, a, b) = NewSession();
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Loaded));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Swap));
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.TurnEnd));
|
||||
|
||||
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.TurnEnd));
|
||||
|
||||
// Three-frame cycle: opponent opens its turn, ends it, sends Judge so the client's
|
||||
// JudgeOperation -> ControlTurnStartPlayer fires and the player's next turn begins.
|
||||
Assert.That(responses.Select(r => r.Envelope.Uri),
|
||||
Is.EqualTo(new[] { NetworkBattleUri.TurnStart, NetworkBattleUri.TurnEnd, NetworkBattleUri.Judge }));
|
||||
Assert.That(responses.Select(r => r.NoStock),
|
||||
Is.EqualTo(new[] { false, false, false }));
|
||||
// Phase returns to AfterReady within the same call so the next player TurnEnd can fire
|
||||
// the cycle again. OpponentTurn is set transiently and is never externally observable.
|
||||
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));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TurnEnd_CanFireMultipleTimesConsecutively()
|
||||
public void ScriptedBot_emitted_OpponentTurnStart_forwards_to_real()
|
||||
{
|
||||
var s = NewSession();
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Loaded));
|
||||
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Swap));
|
||||
var (s, a, b) = NewSession();
|
||||
// Bot emits a TurnStart frame (carrying ViewerId == FakeOpponentViewerId per the
|
||||
// ScriptedBotParticipant impl). Session should route it to the real participant.
|
||||
var botFrame = ScriptedLifecycle.BuildOpponentTurnStart();
|
||||
var routes = s.ComputeFrames(b, botFrame);
|
||||
|
||||
var first = s.ComputeResponses(NewEnvelope(NetworkBattleUri.TurnEnd));
|
||||
var second = s.ComputeResponses(NewEnvelope(NetworkBattleUri.TurnEnd));
|
||||
|
||||
// Both calls produce the same three-frame burst.
|
||||
Assert.That(first.Select(r => r.Envelope.Uri),
|
||||
Is.EqualTo(new[] { NetworkBattleUri.TurnStart, NetworkBattleUri.TurnEnd, NetworkBattleUri.Judge }));
|
||||
Assert.That(second.Select(r => r.Envelope.Uri),
|
||||
Is.EqualTo(new[] { NetworkBattleUri.TurnStart, NetworkBattleUri.TurnEnd, NetworkBattleUri.Judge }));
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AfterReady));
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(a));
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.TurnStart));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Retire_PushesBattleFinishNoContest_TransitionsToTerminal()
|
||||
public void ScriptedBot_emitted_Judge_forwards_to_real()
|
||||
{
|
||||
var s = NewSession();
|
||||
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Retire));
|
||||
var (env, noStock) = responses.Single();
|
||||
Assert.That(env.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
|
||||
Assert.That(noStock, Is.True);
|
||||
var (s, a, b) = NewSession();
|
||||
var botFrame = ScriptedLifecycle.BuildOpponentJudge();
|
||||
var routes = s.ComputeFrames(b, botFrame);
|
||||
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(a));
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.Judge));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ScriptedBot_emitted_TurnEnd_forwards_to_real()
|
||||
{
|
||||
// TurnEnd from the bot is also one of the burst frames. The case is handled
|
||||
// by the TurnEnd-from-scripted arm (bot ViewerId matches FakeOpponentViewerId).
|
||||
var (s, a, b) = NewSession();
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Loaded));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Swap));
|
||||
// Drive player TurnEnd first (transitions Phase if needed) — but TurnEnd from bot
|
||||
// arrives in Phase AfterReady too. The bot's TurnEnd is routed via the dispatch
|
||||
// arm that forwards any frame from the FakeOpponentViewerId participant.
|
||||
|
||||
var botFrame = ScriptedLifecycle.BuildOpponentTurnEnd();
|
||||
var routes = s.ComputeFrames(b, botFrame);
|
||||
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(a));
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.TurnEnd));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Retire_pushes_BattleFinish_no_contest_terminates()
|
||||
{
|
||||
var (s, a, _) = NewSession();
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Retire));
|
||||
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(a));
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
|
||||
Assert.That(routes[0].NoStock, Is.True);
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.Terminal));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Kill_PushesBattleFinishNoContest_TransitionsToTerminal()
|
||||
public void Kill_pushes_BattleFinish_no_contest_terminates()
|
||||
{
|
||||
var s = NewSession();
|
||||
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Kill));
|
||||
var (env, noStock) = responses.Single();
|
||||
Assert.That(env.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
|
||||
Assert.That(noStock, Is.True);
|
||||
var (s, a, _) = NewSession();
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Kill));
|
||||
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(a));
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.BattleFinish));
|
||||
Assert.That(routes[0].NoStock, Is.True);
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.Terminal));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Swap_ArrivingBeforeLoaded_ProducesNoResponseAndDoesNotAdvancePhase()
|
||||
public void OutOfOrder_dispatch_returns_empty_and_does_not_advance_phase()
|
||||
{
|
||||
var s = NewSession();
|
||||
// Skip Loaded — fire Swap straight out of AwaitingInitNetwork.
|
||||
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Swap));
|
||||
Assert.That(responses, Is.Empty);
|
||||
var (s, a, _) = NewSession();
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Swap));
|
||||
|
||||
Assert.That(routes, Is.Empty);
|
||||
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitNetwork));
|
||||
}
|
||||
|
||||
private static (BattleSession, FakeParticipant, FakeParticipant) NewSession()
|
||||
{
|
||||
var a = new FakeParticipant(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);
|
||||
}
|
||||
|
||||
private static MatchContext FixtureCtx() => new(
|
||||
SelfDeckCardIds: Enumerable.Range(1, 30).Select(_ => 100_011_010L).ToList(),
|
||||
ClassId: "1", CharaId: "1", CardMasterName: "card_master_node_10015",
|
||||
CountryCode: "KOR", UserName: "Player", SleeveId: "3000011",
|
||||
EmblemId: "701441011", DegreeId: "300003", FieldId: 43, IsOfficial: 0,
|
||||
BattleType: 11);
|
||||
|
||||
private static MatchContext ScriptedBotContext() => new(
|
||||
SelfDeckCardIds: Array.Empty<long>(),
|
||||
ClassId: "0", CharaId: "0", CardMasterName: "card_master_node_10015",
|
||||
CountryCode: "JPN", UserName: "Opponent", SleeveId: "704141010",
|
||||
EmblemId: "400001100", DegreeId: "120027", FieldId: 5, IsOfficial: 0,
|
||||
BattleType: 0);
|
||||
|
||||
private static MsgEnvelope NewEnvelope(NetworkBattleUri uri) =>
|
||||
new(uri, ViewerId: 1, Uuid: "u", Bid: null, Try: 0,
|
||||
Cat: EmitCategory.Battle, PubSeq: null, PlaySeq: null,
|
||||
Body: new RawBody(new Dictionary<string, object?>()));
|
||||
|
||||
/// <summary>Data-only IBattleParticipant stub for dispatch tests. PushAsync/RunAsync
|
||||
/// are no-ops; FrameEmitted exists but is never invoked by the test.</summary>
|
||||
private sealed class FakeParticipant : IBattleParticipant
|
||||
{
|
||||
public long ViewerId { get; }
|
||||
public MatchContext Context { get; }
|
||||
public event Func<MsgEnvelope, CancellationToken, Task>? FrameEmitted;
|
||||
public FakeParticipant(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