Both single-cycle and consecutive-cycles tests now assert the v1.2 three-frame burst (TurnStart + TurnEnd + Judge). Currently failing — ComputeResponses still pushes only two frames. Implementation follows.
175 lines
8.0 KiB
C#
175 lines
8.0 KiB
C#
// SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NUnit.Framework;
|
|
using SVSim.BattleNode.Bridge;
|
|
using SVSim.BattleNode.Protocol;
|
|
using SVSim.BattleNode.Protocol.Bodies;
|
|
using SVSim.BattleNode.Sessions;
|
|
|
|
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()
|
|
{
|
|
var s = NewSession();
|
|
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
|
|
Assert.That(responses.Select(r => r.Envelope.Uri),
|
|
Is.EqualTo(new[] { NetworkBattleUri.InitNetwork }));
|
|
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitBattle));
|
|
}
|
|
|
|
[Test]
|
|
public void InitBattle_PushesMatched_TransitionsToAwaitingLoaded()
|
|
{
|
|
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));
|
|
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingLoaded));
|
|
}
|
|
|
|
[Test]
|
|
public void Loaded_PushesBattleStartThenDeal_TransitionsToAwaitingSwap()
|
|
{
|
|
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),
|
|
Is.EqualTo(new[] { NetworkBattleUri.BattleStart, NetworkBattleUri.Deal }));
|
|
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingSwap));
|
|
}
|
|
|
|
[Test]
|
|
public void Swap_WithIdxListContainingTwo_ProducesHandWithFreshIdxAtPosition1()
|
|
{
|
|
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 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),
|
|
Is.EqualTo(new[] { NetworkBattleUri.Swap, NetworkBattleUri.Ready }));
|
|
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AfterReady));
|
|
}
|
|
|
|
[Test]
|
|
public void TurnEnd_AfterReady_PushesTurnStart_TurnEnd_Judge_StaysInAfterReady()
|
|
{
|
|
var s = NewSession();
|
|
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
|
|
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitBattle));
|
|
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Loaded));
|
|
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Swap));
|
|
|
|
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(s.Phase, Is.EqualTo(BattleSessionPhase.AfterReady));
|
|
}
|
|
|
|
[Test]
|
|
public void TurnEnd_CanFireMultipleTimesConsecutively()
|
|
{
|
|
var s = NewSession();
|
|
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
|
|
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitBattle));
|
|
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Loaded));
|
|
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Swap));
|
|
|
|
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));
|
|
}
|
|
|
|
[Test]
|
|
public void Retire_PushesBattleFinishNoContest_TransitionsToTerminal()
|
|
{
|
|
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);
|
|
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.Terminal));
|
|
}
|
|
|
|
[Test]
|
|
public void Kill_PushesBattleFinishNoContest_TransitionsToTerminal()
|
|
{
|
|
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);
|
|
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.Terminal));
|
|
}
|
|
|
|
[Test]
|
|
public void Swap_ArrivingBeforeLoaded_ProducesNoResponseAndDoesNotAdvancePhase()
|
|
{
|
|
var s = NewSession();
|
|
// Skip Loaded — fire Swap straight out of AwaitingInitNetwork.
|
|
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.Swap));
|
|
Assert.That(responses, Is.Empty);
|
|
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AwaitingInitNetwork));
|
|
}
|
|
}
|