diff --git a/SVSim.BattleNode/Sessions/Dispatch/BattleSessionState.cs b/SVSim.BattleNode/Sessions/Dispatch/BattleSessionState.cs
index 3a53df5..b4873b8 100644
--- a/SVSim.BattleNode/Sessions/Dispatch/BattleSessionState.cs
+++ b/SVSim.BattleNode/Sessions/Dispatch/BattleSessionState.cs
@@ -1,11 +1,31 @@
+using SVSim.BattleNode.Sessions;
+
namespace SVSim.BattleNode.Sessions.Dispatch;
-/// Mutable per-session state shared across frame handlers. Today: the session-level
-/// phase (only ever advanced to ) and the mulligan
-/// barrier's post-swap hands. FUTURE (PvP equivalency, NOT this refactor): per-side idx->cardId
-/// maps + reveal-gating state land here.
+/// Mutable per-session state shared across frame handlers. The mulligan barrier's
+/// post-swap hands, plus (PvP-equivalency, vanilla slice) the per-side idx->cardId map used to
+/// synthesize the opponent-facing knownList. FUTURE: a token map (cardIds mined from
+/// orderList add ops, idx>30) + a reveal-gate set land alongside .
internal sealed class BattleSessionState
{
public BattleSessionPhase SessionPhase { get; set; } = BattleSessionPhase.AwaitingInitNetwork;
public Dictionary PostSwapHands { get; } = new();
+
+ /// Per-side idx->cardId, seeded lazily from .
+ /// Deck cards only (idx 1..deckCount); tokens (idx>deckCount) are deferred.
+ public Dictionary> IdxToCardId { get; } = new();
+
+ /// The sender's idx->cardId map, seeding it from its on first
+ /// use. BuildPlayerDeck assigns deck idx = position+1, so entry (i+1) -> cardIds[i].
+ public IReadOnlyDictionary GetOrSeedDeckMap(IBattleParticipant side)
+ {
+ if (!IdxToCardId.TryGetValue(side, out var map))
+ {
+ map = new Dictionary();
+ var deck = side.Context.SelfDeckCardIds;
+ for (var i = 0; i < deck.Count; i++) map[i + 1] = deck[i];
+ IdxToCardId[side] = map;
+ }
+ return map;
+ }
}
diff --git a/SVSim.UnitTests/BattleNode/Sessions/BattleSessionStateTests.cs b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionStateTests.cs
new file mode 100644
index 0000000..873984a
--- /dev/null
+++ b/SVSim.UnitTests/BattleNode/Sessions/BattleSessionStateTests.cs
@@ -0,0 +1,50 @@
+using NUnit.Framework;
+using SVSim.BattleNode.Bridge;
+using SVSim.BattleNode.Sessions;
+using SVSim.BattleNode.Sessions.Dispatch;
+
+namespace SVSim.UnitTests.BattleNode.Sessions;
+
+[TestFixture]
+public class BattleSessionStateTests
+{
+ private sealed class StubParticipant : IBattleParticipant
+ {
+ public long ViewerId { get; }
+ public MatchContext Context { get; }
+ public event Func? FrameEmitted;
+ public StubParticipant(long id, MatchContext ctx) { ViewerId = id; Context = ctx; }
+ public Task PushAsync(SVSim.BattleNode.Protocol.MsgEnvelope e, bool n, CancellationToken c) => Task.CompletedTask;
+ public Task RunAsync(CancellationToken c) => Task.CompletedTask;
+ public Task TerminateAsync(BattleFinishReason r) => Task.CompletedTask;
+ public ValueTask DisposeAsync() => ValueTask.CompletedTask;
+ private void Touch() => FrameEmitted?.Invoke(null!, default);
+ }
+
+ private static MatchContext Ctx(params long[] deck) => new(
+ SelfDeckCardIds: deck, ClassId: "1", CharaId: "1", CardMasterName: "cm",
+ CountryCode: "KOR", UserName: "P", SleeveId: "0", EmblemId: "0", DegreeId: "0",
+ FieldId: 0, IsOfficial: 0, BattleType: 11);
+
+ [Test]
+ public void GetOrSeedDeckMap_maps_idx_1based_to_deck_cardIds()
+ {
+ var state = new BattleSessionState();
+ var p = new StubParticipant(1, Ctx(900L, 901L, 902L));
+
+ var map = state.GetOrSeedDeckMap(p);
+
+ Assert.That(map[1], Is.EqualTo(900L));
+ Assert.That(map[2], Is.EqualTo(901L));
+ Assert.That(map[3], Is.EqualTo(902L));
+ Assert.That(map.ContainsKey(4), Is.False);
+ }
+
+ [Test]
+ public void GetOrSeedDeckMap_is_idempotent_same_instance()
+ {
+ var state = new BattleSessionState();
+ var p = new StubParticipant(1, Ctx(900L));
+ Assert.That(state.GetOrSeedDeckMap(p), Is.SameAs(state.GetOrSeedDeckMap(p)));
+ }
+}