using SVSim.BattleNode.Protocol; using SVSim.BattleNode.Protocol.Bodies; namespace SVSim.BattleNode.Lifecycle; /// /// v1 hand-rolled scripted opponent. Static frame builders for the five lifecycle uris /// (Matched / BattleStart / Deal / Swap response / Ready) plus a trivial opponent TurnStart /// the dispatch pushes after the player's TurnEnd. The values are templated from the TK2 /// captures at data_dumps/captures/battle-traffic_tk2_regular.ndjson — anything /// hardcoded here came from a real prod frame, with names + provenance in /// . /// public static class ScriptedLifecycle { /// /// CardId used for all 30 entries in the dummy deck. A stable neutral card that exists in /// every card-master version we care about, so the client can render it without /// triggering a card-master-mismatch error. /// public const long DummyCardId = 100011010L; /// /// Viewer id we present as the opponent on every scripted push. Out-of-range vs. real /// viewer ids so it can't collide with a real account in the auth pipeline. /// public const long FakeOpponentViewerId = 999_999_999L; public static MsgEnvelope BuildMatched(long playerViewerId, long opponentViewerId, string battleId) => EnvelopeForPush(NetworkBattleUri.Matched, new MatchedBody( SelfInfo: ScriptedProfiles.PlayerMatchedProfile with { OppoId = opponentViewerId }, OppoInfo: ScriptedProfiles.OpponentMatchedProfile with { OppoId = playerViewerId }, SelfDeck: BuildDummyDeck()), bid: battleId); public static MsgEnvelope BuildBattleStart(long playerViewerId) => EnvelopeForPush(NetworkBattleUri.BattleStart, new BattleStartBody( TurnState: 0, // player goes first BattleType: 11, // TK2 NetworkBattleType SelfInfo: ScriptedProfiles.PlayerBattleStartProfile, OppoInfo: ScriptedProfiles.OpponentBattleStartProfile)); public static MsgEnvelope BuildDeal() => EnvelopeForPush(NetworkBattleUri.Deal, new DealBody( Self: new[] { new PosIdx(0, 1), new PosIdx(1, 2), new PosIdx(2, 3) }, Oppo: new[] { new PosIdx(0, 1), new PosIdx(1, 2), new PosIdx(2, 3) })); /// /// Initial 3-card hand idxs from . Each position in this array /// is one card; the value is the card's deck idx. /// private static readonly long[] InitialHand = { 1, 2, 3 }; /// /// Compute the player's hand after a mulligan. For every idx in /// that is currently in the hand, replace it with the next unused deck idx (starting at 4, /// since 1..3 were dealt). Positions of kept cards are preserved. /// public static long[] ComputeHandAfterSwap(IReadOnlyList swapIndices) { var hand = (long[])InitialHand.Clone(); var nextDeckIdx = 4L; for (var pos = 0; pos < hand.Length; pos++) { if (swapIndices.Contains(hand[pos])) { hand[pos] = nextDeckIdx++; } } return hand; } public static MsgEnvelope BuildSwapResponse(IReadOnlyList hand) => EnvelopeForPush(NetworkBattleUri.Swap, new SwapResponseBody(Self: BuildPosIdxList(hand))); public static MsgEnvelope BuildReady(IReadOnlyList hand) => EnvelopeForPush(NetworkBattleUri.Ready, new ReadyBody( Self: BuildPosIdxList(hand), Oppo: BuildPosIdxList(InitialHand), IdxChangeSeed: ScriptedProfiles.ReadyIdxChangeSeed, Spin: ScriptedProfiles.ReadySpin)); /// /// Generic TurnStart push used to transition the client into "Opponent's turn…" state /// after the player's TurnEnd. v1 doesn't simulate the opponent — once this lands the /// client sits at the opponent-turn display indefinitely. /// public static MsgEnvelope BuildOpponentTurnStart() => EnvelopeForPush(NetworkBattleUri.TurnStart, new OpponentTurnStartBody(Spin: ScriptedProfiles.OpponentTurnStartSpin)); private static IReadOnlyList BuildPosIdxList(IReadOnlyList hand) { var list = new List(hand.Count); for (var pos = 0; pos < hand.Count; pos++) { list.Add(new PosIdx(Pos: pos, Idx: (int)hand[pos])); } return list; } private static IReadOnlyList BuildDummyDeck() { var deck = new List(30); for (var i = 1; i <= 30; i++) { deck.Add(new DeckCardRef(Idx: i, CardId: DummyCardId)); } return deck; } private static MsgEnvelope EnvelopeForPush(NetworkBattleUri uri, IMsgBody body, string? bid = null) => new(uri, ViewerId: FakeOpponentViewerId, Uuid: "node-stub", Bid: bid, Try: 0, Cat: EmitCategory.Battle, PubSeq: null, PlaySeq: null, Body: body); }