From a533e9d89d4e1e7997e9252181f618f3490d9da6 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 3 Jun 2026 10:49:38 -0400 Subject: [PATCH] feat(battle-node): client-shaped handshake builders for the scripted bot Co-Authored-By: Claude Opus 4.8 --- .../Lifecycle/ScriptedLifecycle.cs | 31 +++++++++++++++++++ .../Lifecycle/ScriptedLifecycleTests.cs | 21 +++++++++++++ 2 files changed, 52 insertions(+) diff --git a/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs b/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs index 4703a51..13fd832 100644 --- a/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs +++ b/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs @@ -127,6 +127,37 @@ public static class ScriptedLifecycle IdxChangeSeed: ScriptedProfiles.ReadyIdxChangeSeed, Spin: ScriptedProfiles.ReadySpin)); + // --- Client-shaped emissions used by ScriptedBotParticipant so the session brokers + // the bot through the same handshake arms as a human. Bodies for the parameterless + // handshake frames are ignored by the session (it reads from.Context / phase); only + // Swap's idxList is consumed (empty = keep the dealt hand). + + public static MsgEnvelope BuildClientInitNetwork() => ClientFrame(NetworkBattleUri.InitNetwork, EmitCategory.General); + public static MsgEnvelope BuildClientInitBattle() => ClientFrame(NetworkBattleUri.InitBattle, EmitCategory.General); + public static MsgEnvelope BuildClientLoaded() => ClientFrame(NetworkBattleUri.Loaded, EmitCategory.General); + + public static MsgEnvelope BuildClientSwap() => + new(NetworkBattleUri.Swap, + ViewerId: FakeOpponentViewerId, + Uuid: WireConstants.ServerUuid, + Bid: null, + Try: 0, + Cat: EmitCategory.Battle, + PubSeq: null, + PlaySeq: null, + Body: new RawBody(new Dictionary { ["idxList"] = new List() })); + + private static MsgEnvelope ClientFrame(NetworkBattleUri uri, EmitCategory cat) => + new(uri, + ViewerId: FakeOpponentViewerId, + Uuid: WireConstants.ServerUuid, + Bid: null, + Try: 0, + Cat: cat, + PubSeq: null, + PlaySeq: null, + Body: new ResultCodeOnlyBody()); + /// /// First half of the v1.1 scripted opponent turn cycle: pushed after the player's /// TurnEnd, transitions the client into "Opponent's turn…" state. Paired with diff --git a/SVSim.UnitTests/BattleNode/Lifecycle/ScriptedLifecycleTests.cs b/SVSim.UnitTests/BattleNode/Lifecycle/ScriptedLifecycleTests.cs index 3e62653..0335651 100644 --- a/SVSim.UnitTests/BattleNode/Lifecycle/ScriptedLifecycleTests.cs +++ b/SVSim.UnitTests/BattleNode/Lifecycle/ScriptedLifecycleTests.cs @@ -166,6 +166,27 @@ public class ScriptedLifecycleTests "single-arg overload (non-interactive opponent) keeps the placeholder hand."); } + [Test] + public void BuildClientSwap_has_empty_idxList_in_RawBody() + { + var env = ScriptedLifecycle.BuildClientSwap(); + Assert.That(env.Uri, Is.EqualTo(NetworkBattleUri.Swap)); + Assert.That(env.ViewerId, Is.EqualTo(ScriptedLifecycle.FakeOpponentViewerId)); + + var raw = (RawBody)env.Body; + Assert.That(raw.Entries.ContainsKey("idxList"), Is.True); + var idx = (System.Collections.IEnumerable)raw.Entries["idxList"]!; + Assert.That(idx.Cast().Count(), Is.EqualTo(0), "bot keeps its dealt hand (empty mulligan)."); + } + + [Test] + public void BuildClientHandshakeFrames_carry_expected_uris() + { + Assert.That(ScriptedLifecycle.BuildClientInitNetwork().Uri, Is.EqualTo(NetworkBattleUri.InitNetwork)); + Assert.That(ScriptedLifecycle.BuildClientInitBattle().Uri, Is.EqualTo(NetworkBattleUri.InitBattle)); + Assert.That(ScriptedLifecycle.BuildClientLoaded().Uri, Is.EqualTo(NetworkBattleUri.Loaded)); + } + [Test] public void BuildOpponentTurnStart_HasUriTurnStartAndSpin() {