diff --git a/SVSim.BattleNode/Protocol/Bodies/PlayActionsBroadcastBody.cs b/SVSim.BattleNode/Protocol/Bodies/PlayActionsBroadcastBody.cs index 5280322..c639733 100644 --- a/SVSim.BattleNode/Protocol/Bodies/PlayActionsBroadcastBody.cs +++ b/SVSim.BattleNode/Protocol/Bodies/PlayActionsBroadcastBody.cs @@ -8,18 +8,15 @@ 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. 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). +/// 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). 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, - [property: JsonPropertyName("spin")] int? Spin = null) : IMsgBody; + [property: JsonPropertyName("keyAction")] IReadOnlyList? KeyAction = 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 6c60083..346e09c 100644 --- a/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs +++ b/SVSim.BattleNode/Sessions/Dispatch/Handlers/PlayActionsHandler.cs @@ -48,17 +48,6 @@ 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, @@ -67,8 +56,7 @@ 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), - Spin: spin > 0 ? spin : null); + KeyAction: KnownListBuilder.StripKeyActionForOpponent(keyAction)); var frame = ctx.Env with { Body = body }; return new[] { new DispatchRoute(ctx.Other, frame, false) }; diff --git a/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs b/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs index 2df5637..9ec92c0 100644 --- a/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs +++ b/SVSim.BattleNode/Sessions/Dispatch/KnownListBuilder.cs @@ -42,41 +42,6 @@ internal static class KnownListBuilder return null; } - /// Count hidden, non-reproduced shared-RNG draws implied by an orderList: move ops - /// from Deck(0) to Hand(10) for a non-played, unrevealed card (a random fetch the opponent can't - /// reproduce — it can't enumerate the active player's deck). Each such moved card = one crank the - /// receiver must apply (spin += 1). Skips: the played card's own move (idx == playIdx); - /// any idx in (its identity is already on the wire — a revealed uList - /// fetch); and lot-based randoms (a move op carrying a rand array — the receiver - /// reproduces those by re-rolling, so counting would double-crank). An ordinary card DRAW also - /// surfaces as a hidden move(0→10) but is a deterministic top-of-deck pop (no shared-stream - /// roll) and must NOT be counted — distinguishing it from a random fetch on the wire is the open - /// capture (real-spin design §3.1; the spec's Task 0). Base 0; see the real-spin design doc. - public static int CountHiddenDraws(object? orderList, int playIdx, IReadOnlySet revealed) - { - if (orderList is not IEnumerable ops) return 0; - var count = 0; - foreach (var op in ops) - { - if (op is not IDictionary opDict) continue; - if (!opDict.TryGetValue("move", out var moveRaw) || moveRaw is not IDictionary move) continue; - - if (move.ContainsKey("rand")) continue; // lot random → receiver re-rolls - if (!move.TryGetValue("from", out var fromRaw) || (int)AsLong(fromRaw) != 0) continue; // not from Deck - if (!move.TryGetValue("to", out var toRaw) || (int)AsLong(toRaw) != 10) continue; // not to Hand - if (!move.TryGetValue("idx", out var idxRaw) || idxRaw is not IEnumerable idxList) continue; - - foreach (var i in idxList) - { - var idx = (int)AsLong(i); - if (idx == playIdx) continue; // the played card's own move - if (revealed.Contains(idx)) continue; // opponent already knows this card's identity - count++; - } - } - return count; - } - /// Mine generated-token identities from a sender's add ops: yields /// (idx, cardId, isSelf) for every idx in each {add:{idx:[...], isSelf, card:{cardId}}} /// op. isSelf is surfaced verbatim (the sender's perspective tag on CardObj.IsPlayer, diff --git a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs index 7e4b19b..164792e 100644 --- a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs +++ b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionDispatchTests.cs @@ -285,97 +285,6 @@ 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 - { - new Dictionary - { - ["move"] = new Dictionary - { - ["idx"] = new List { 3L }, ["isSelf"] = 1L, ["from"] = 10L, ["to"] = 20L, - } - }, - new Dictionary - { - ["move"] = new Dictionary - { - ["idx"] = new List { 31L }, ["isSelf"] = 1L, ["from"] = 0L, ["to"] = 10L, - } - }, - }, - }; - - var routes = s.ComputeFrames(a, EnvWith(NetworkBattleUri.PlayActions, body)); - var pb = (PlayActionsBroadcastBody)routes[0].Frame.Body; - Assert.That(pb.Spin, Is.EqualTo(1)); - } - - [Test] - public void Pvp_PlayActions_emits_null_spin_for_a_vanilla_play() - { - var (s, a, b) = NewPvpSession(); - DriveToAfterReady(s, a); - DriveToAfterReady(s, b); - - var body = MoveOrderList(idx: 3, from: 10, to: 20); // played card only, no fetch - body["playIdx"] = 3L; - body["type"] = 30L; - - var routes = s.ComputeFrames(a, EnvWith(NetworkBattleUri.PlayActions, body)); - var pb = (PlayActionsBroadcastBody)routes[0].Frame.Body; - Assert.That(pb.Spin, Is.Null, "spin omitted (client assumes 0) when there are no hidden draws."); - } - - [Test] - public void Pvp_PlayActions_emits_null_spin_for_a_revealed_uList_fetch() - { - // A fetch whose identity is revealed via the uList (cardId present) is NOT counted — the - // receiver knows it, so its stream doesn't need a crank. - var (s, a, b) = NewPvpSession(); - DriveToAfterReady(s, a); - DriveToAfterReady(s, b); - - var body = new Dictionary - { - ["playIdx"] = 3L, - ["type"] = 30L, - ["orderList"] = new List - { - new Dictionary - { - ["move"] = new Dictionary - { - ["idx"] = new List { 31L }, ["isSelf"] = 1L, ["from"] = 0L, ["to"] = 10L, - } - }, - }, - ["uList"] = new List - { - new Dictionary - { - ["idxList"] = new List { 31L }, - ["from"] = 0L, ["to"] = 10L, ["isSelf"] = 1L, ["skill"] = "37|36|0", - ["cardId"] = 100_011_010L, // revealed identity - }, - }, - }; - - var routes = s.ComputeFrames(a, EnvWith(NetworkBattleUri.PlayActions, body)); - var pb = (PlayActionsBroadcastBody)routes[0].Frame.Body; - Assert.That(pb.Spin, Is.Null); - } - [Test] public void Pvp_PlayActions_without_uList_leaves_it_null() { diff --git a/SVSim.UnitTests/BattleNode/Sessions/KnownListBuilderTests.cs b/SVSim.UnitTests/BattleNode/Sessions/KnownListBuilderTests.cs index a7381a0..3133844 100644 --- a/SVSim.UnitTests/BattleNode/Sessions/KnownListBuilderTests.cs +++ b/SVSim.UnitTests/BattleNode/Sessions/KnownListBuilderTests.cs @@ -20,78 +20,6 @@ public class KnownListBuilderTests } }; - // A move op dict allowing extra keys (e.g. "rand") for CountHiddenDraws fixtures. - private static Dictionary MoveOp(long[] idxs, int from, int to, - IDictionary? extra = null) - { - var move = new Dictionary - { - ["idx"] = idxs.Select(i => (object?)i).ToList(), - ["isSelf"] = 1L, ["from"] = (long)from, ["to"] = (long)to, - }; - if (extra is not null) foreach (var kv in extra) move[kv.Key] = kv.Value; - return new Dictionary { ["move"] = move }; - } - - private static readonly IReadOnlySet NoneRevealed = new HashSet(); - - [Test] - public void CountHiddenDraws_counts_a_hidden_deck_to_hand_fetch() - { - // Hoverboard-shaped: move(0->10) for a non-played, unrevealed token idx. - var orderList = new List { MoveOp(new[] { 31L }, from: 0, to: 10) }; - Assert.That(KnownListBuilder.CountHiddenDraws(orderList, playIdx: 3, NoneRevealed), Is.EqualTo(1)); - } - - [Test] - public void CountHiddenDraws_ignores_the_played_cards_own_move() - { - // The played card moving hand(10)->field(20) is not a draw. - var orderList = new List { MoveOp(new[] { 3L }, from: 10, to: 20) }; - Assert.That(KnownListBuilder.CountHiddenDraws(orderList, playIdx: 3, NoneRevealed), Is.EqualTo(0)); - } - - [Test] - public void CountHiddenDraws_ignores_a_visible_destroy() - { - // A visible field(20)->cemetery(30) destroy is reproduced by the receiver → 0. - var orderList = new List { MoveOp(new[] { 8L }, from: 20, to: 30) }; - Assert.That(KnownListBuilder.CountHiddenDraws(orderList, playIdx: 3, NoneRevealed), Is.EqualTo(0)); - } - - [Test] - public void CountHiddenDraws_ignores_a_revealed_fetch() - { - // The fetched idx's identity was revealed to the opponent (in `revealed`) → 0. - var orderList = new List { MoveOp(new[] { 31L }, from: 0, to: 10) }; - var revealed = new HashSet { 31 }; - Assert.That(KnownListBuilder.CountHiddenDraws(orderList, playIdx: 3, revealed), Is.EqualTo(0)); - } - - [Test] - public void CountHiddenDraws_ignores_a_lot_based_random() - { - // A move op carrying a `rand` array is reproduced (re-rolled) by the receiver → 0. - var rand = new Dictionary { ["rand"] = new List { 0.5d } }; - var orderList = new List { MoveOp(new[] { 31L }, from: 0, to: 10, extra: rand) }; - Assert.That(KnownListBuilder.CountHiddenDraws(orderList, playIdx: 3, NoneRevealed), Is.EqualTo(0)); - } - - [Test] - public void CountHiddenDraws_counts_each_hidden_card_in_a_multi_fetch() - { - var orderList = new List { MoveOp(new[] { 31L, 32L }, from: 0, to: 10) }; - Assert.That(KnownListBuilder.CountHiddenDraws(orderList, playIdx: 3, NoneRevealed), Is.EqualTo(2)); - } - - [Test] - public void CountHiddenDraws_returns_zero_for_a_vanilla_play() - { - var orderList = new List { MoveOp(new[] { 3L }, from: 10, to: 20) }; - Assert.That(KnownListBuilder.CountHiddenDraws(orderList, playIdx: 3, NoneRevealed), Is.EqualTo(0)); - Assert.That(KnownListBuilder.CountHiddenDraws(null, playIdx: 3, NoneRevealed), Is.EqualTo(0)); - } - [Test] public void ExtractMoveTo_returns_to_for_matching_idx() {