diff --git a/SVSim.BattleNode/Protocol/Bodies/PlayActionsBroadcastBody.cs b/SVSim.BattleNode/Protocol/Bodies/PlayActionsBroadcastBody.cs
index c639733..5280322 100644
--- a/SVSim.BattleNode/Protocol/Bodies/PlayActionsBroadcastBody.cs
+++ b/SVSim.BattleNode/Protocol/Bodies/PlayActionsBroadcastBody.cs
@@ -8,15 +8,18 @@ namespace SVSim.BattleNode.Protocol.Bodies;
/// (independent of KnownList — a targeted hand play carries both). KeyAction forwards a
/// choice/Discover play's {type,cardId} so the opponent renders the choice-token generation;
/// the pick (selectCard) is stripped for a hidden (open:0) draw-to-hand choice. UList
-/// forwards the sender's unapproved-movement list (deck-sourced summons/fetches) verbatim. All are
-/// omitted when null via the envelope's WhenWritingNull policy (a vanilla play carries none).
+/// forwards the sender's unapproved-movement list (deck-sourced summons/fetches) verbatim. Spin
+/// is the count of hidden, non-reproduced shared-RNG draws in THIS frame (real-spin design §3.2,
+/// per-frame delivery); null/omitted = 0 (the client's assumed default). All are omitted when null
+/// via the envelope's WhenWritingNull policy (a vanilla play carries none).
public sealed record PlayActionsBroadcastBody(
[property: JsonPropertyName("playIdx")] int PlayIdx,
[property: JsonPropertyName("type")] int Type,
[property: JsonPropertyName("knownList")] IReadOnlyList? KnownList,
[property: JsonPropertyName("oppoTargetList")] IReadOnlyList? OppoTargetList,
[property: JsonPropertyName("uList")] IReadOnlyList? UList = null,
- [property: JsonPropertyName("keyAction")] IReadOnlyList? KeyAction = null) : IMsgBody;
+ [property: JsonPropertyName("keyAction")] IReadOnlyList? KeyAction = null,
+ [property: JsonPropertyName("spin")] int? Spin = null) : IMsgBody;
/// Opponent-facing keyAction entry for a choice/Discover play. type/cardId
/// (the GENERATING card) pass through so the opponent re-derives the candidate pool from that card's
diff --git a/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs b/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs
index 346e09c..6c60083 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs
@@ -48,6 +48,17 @@ internal sealed class PlayActionsHandler : IFrameHandler
// knownList in the same frame (capture line 75).
var uList = KnownListBuilder.RelayUList(entries.GetValueOrDefault("uList"));
+ // Hidden shared-RNG draws (a random deck→hand fetch the opponent can't reproduce) advance the
+ // sender's shared _stableRandom; the receiver doesn't re-run them, so it needs a spin crank to
+ // stay aligned for later visible randoms. Per-frame delivery (real-spin design §3.2): the count
+ // rides THIS PlayActions. A fetch we relay with an identity (uList cardId present) is already
+ // known to the opponent — exclude its idxs so it isn't counted. 0 → null (omitted; client
+ // assumes spin:0).
+ var revealed = uList is null
+ ? (IReadOnlySet)new HashSet()
+ : uList.Where(u => u.CardId is not null).SelectMany(u => u.IdxList).ToHashSet();
+ var spin = KnownListBuilder.CountHiddenDraws(orderList, playIdx, revealed);
+
var body = new PlayActionsBroadcastBody(
PlayIdx: playIdx,
Type: type,
@@ -56,7 +67,8 @@ internal sealed class PlayActionsHandler : IFrameHandler
UList: uList,
// {type,cardId} forwarded so the opponent renders the choice token; selectCard dropped
// when open==0 (hidden draw-to-hand pick). Null for a vanilla play (no keyAction).
- KeyAction: KnownListBuilder.StripKeyActionForOpponent(keyAction));
+ KeyAction: KnownListBuilder.StripKeyActionForOpponent(keyAction),
+ Spin: spin > 0 ? spin : null);
var frame = ctx.Env with { Body = body };
return new[] { new DispatchRoute(ctx.Other, frame, false) };
diff --git a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs
index 164792e..7e4b19b 100644
--- a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs
+++ b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs
@@ -285,6 +285,97 @@ public class BattleSessionDispatchTests
Assert.That(pb.UList[0].Skill, Is.EqualTo("37|36|0"));
}
+ [Test]
+ public void Pvp_PlayActions_emits_spin_for_a_hidden_fetch()
+ {
+ var (s, a, b) = NewPvpSession();
+ DriveToAfterReady(s, a);
+ DriveToAfterReady(s, b);
+
+ // Played card 3 (hand->field) PLUS a hidden deck->hand fetch of a non-played token idx 31.
+ var body = new Dictionary
+ {
+ ["playIdx"] = 3L,
+ ["type"] = 30L,
+ ["orderList"] = new List