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