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))); + } +}