test(battle-node): real scripted bot drives handshake through the mulligan barrier
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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;
|
||||
using SVSim.BattleNode.Sessions.Participants;
|
||||
|
||||
namespace SVSim.UnitTests.BattleNode.Sessions;
|
||||
|
||||
[TestFixture]
|
||||
public class ScriptedBotHandshakeIntegrationTests
|
||||
{
|
||||
[Test]
|
||||
public async Task RealBot_completes_handshake_and_both_sides_reach_Ready_through_barrier()
|
||||
{
|
||||
var bot = new ScriptedBotParticipant(); // B — real participant under test
|
||||
var player = new RecordingPlayer(viewerId: 1, PlayerCtx());
|
||||
|
||||
var session = new BattleSession("bid-int-1", BattleType.Scripted, player, bot,
|
||||
NullLogger<BattleSession>.Instance);
|
||||
|
||||
// Wire the bot's emissions into the session the way BattleSession's ctor does for
|
||||
// real participants (the ctor already subscribed; we just need to start RunAsync,
|
||||
// which fires the bot's InitNetwork and lets the reactive cascade settle).
|
||||
await bot.RunAsync(CancellationToken.None);
|
||||
|
||||
// After the bot's cascade, the bot has emitted InitNetwork→InitBattle→Loaded→Swap
|
||||
// and is parked awaiting the player's Swap (barrier holds its Ready).
|
||||
Assert.That(((IHasHandshakePhase)bot).Phase, Is.EqualTo(BattleSessionPhase.AfterReady),
|
||||
"Bot should have driven its own handshake to AfterReady.");
|
||||
Assert.That(player.Received, Does.Not.Contain(NetworkBattleUri.Ready),
|
||||
"Player hasn't swapped yet → barrier must withhold the bot-triggered Ready from the player too.");
|
||||
|
||||
// Now drive the player's handshake through Swap; the player's Swap should release
|
||||
// Ready to BOTH sides.
|
||||
session.ComputeFrames(player, Env(NetworkBattleUri.InitNetwork));
|
||||
session.ComputeFrames(player, Env(NetworkBattleUri.InitBattle));
|
||||
var loadedRoutes = session.ComputeFrames(player, Env(NetworkBattleUri.Loaded));
|
||||
var bs = (BattleStartBody)loadedRoutes[0].Frame.Body;
|
||||
Assert.That(bs.TurnState, Is.EqualTo(0), "Player (A) goes first.");
|
||||
|
||||
var swapRoutes = session.ComputeFrames(player, Env(NetworkBattleUri.Swap));
|
||||
Assert.That(swapRoutes.Any(r => ReferenceEquals(r.Target, player) && r.Frame.Uri == NetworkBattleUri.Ready),
|
||||
Is.True, "Player's Swap (the second) releases Ready to the player.");
|
||||
Assert.That(swapRoutes.Any(r => ReferenceEquals(r.Target, bot) && r.Frame.Uri == NetworkBattleUri.Ready),
|
||||
Is.True, "...and to the bot.");
|
||||
}
|
||||
|
||||
private sealed class RecordingPlayer : IBattleParticipant, IHasHandshakePhase
|
||||
{
|
||||
public long ViewerId { get; }
|
||||
public MatchContext Context { get; }
|
||||
public BattleSessionPhase Phase { get; set; } = BattleSessionPhase.AwaitingInitNetwork;
|
||||
public List<NetworkBattleUri> Received { get; } = new();
|
||||
public event Func<MsgEnvelope, CancellationToken, Task>? FrameEmitted;
|
||||
public RecordingPlayer(long viewerId, MatchContext ctx) { ViewerId = viewerId; Context = ctx; }
|
||||
public Task PushAsync(MsgEnvelope env, bool noStock, CancellationToken ct)
|
||||
{ Received.Add(env.Uri); return 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);
|
||||
}
|
||||
|
||||
private static MatchContext PlayerCtx() => 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 MsgEnvelope Env(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?>()));
|
||||
}
|
||||
Reference in New Issue
Block a user