feat(battle-node): ScriptedLifecycle frame builders (Path-A static opponent)
This commit is contained in:
162
SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs
Normal file
162
SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
using SVSim.BattleNode.Protocol;
|
||||||
|
|
||||||
|
namespace SVSim.BattleNode.Lifecycle;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// v1 Path-A scripted opponent. Hand-rolled static frames good enough to land the client on
|
||||||
|
/// the mulligan screen and let them play turn 1. Templates derived from
|
||||||
|
/// data_dumps/captures/battle-traffic_tk2_regular.ndjson.
|
||||||
|
/// </summary>
|
||||||
|
public static class ScriptedLifecycle
|
||||||
|
{
|
||||||
|
/// <summary>30 dummy cardIds — repeats of a stable neutral card.</summary>
|
||||||
|
public static readonly long DummyCardId = 100011010;
|
||||||
|
|
||||||
|
public const long FakeOpponentViewerId = 999_999_999L;
|
||||||
|
|
||||||
|
public static MsgEnvelope BuildMatched(long playerViewerId, long opponentViewerId, string battleId)
|
||||||
|
{
|
||||||
|
var body = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["selfInfo"] = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["country_code"] = "KOR",
|
||||||
|
["userName"] = "Player",
|
||||||
|
["sleeveId"] = "3000011",
|
||||||
|
["emblemId"] = "701441011",
|
||||||
|
["degreeId"] = "300003",
|
||||||
|
["fieldId"] = 43,
|
||||||
|
["isOfficial"] = 0,
|
||||||
|
["oppoId"] = opponentViewerId,
|
||||||
|
["seed"] = 17548138L,
|
||||||
|
},
|
||||||
|
["oppoInfo"] = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["country_code"] = "JPN",
|
||||||
|
["userName"] = "Opponent",
|
||||||
|
["sleeveId"] = "704141010",
|
||||||
|
["emblemId"] = "400001100",
|
||||||
|
["degreeId"] = "120027",
|
||||||
|
["fieldId"] = 5,
|
||||||
|
["isOfficial"] = 0,
|
||||||
|
["oppoId"] = playerViewerId,
|
||||||
|
["seed"] = 17548138L,
|
||||||
|
["oppoDeckCount"] = 30,
|
||||||
|
},
|
||||||
|
["selfDeck"] = BuildDummyDeck(),
|
||||||
|
};
|
||||||
|
return EnvelopeForPush(NetworkBattleUri.Matched, body, bid: battleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MsgEnvelope BuildBattleStart(long playerViewerId)
|
||||||
|
{
|
||||||
|
var body = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["turnState"] = 0, // player goes first
|
||||||
|
["battleType"] = 11, // TK2 NetworkBattleType
|
||||||
|
["selfInfo"] = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["rank"] = "10",
|
||||||
|
["battlePoint"] = "6270",
|
||||||
|
["classId"] = "1",
|
||||||
|
["charaId"] = "1",
|
||||||
|
["cardMasterName"] = "card_master_node_10015",
|
||||||
|
},
|
||||||
|
["oppoInfo"] = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["rank"] = "1",
|
||||||
|
["isMasterRank"] = "0",
|
||||||
|
["battlePoint"] = 0,
|
||||||
|
["masterPoint"] = "0",
|
||||||
|
["classId"] = "8",
|
||||||
|
["charaId"] = "8",
|
||||||
|
["cardMasterName"] = "card_master_node_10015",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return EnvelopeForPush(NetworkBattleUri.BattleStart, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MsgEnvelope BuildDeal()
|
||||||
|
{
|
||||||
|
var body = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["self"] = new List<object?>
|
||||||
|
{
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 0, ["idx"] = 1 },
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 1, ["idx"] = 2 },
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 2, ["idx"] = 3 },
|
||||||
|
},
|
||||||
|
["oppo"] = new List<object?>
|
||||||
|
{
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 0, ["idx"] = 1 },
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 1, ["idx"] = 2 },
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 2, ["idx"] = 3 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return EnvelopeForPush(NetworkBattleUri.Deal, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MsgEnvelope BuildSwapResponse(IReadOnlyList<long> swapIndices)
|
||||||
|
{
|
||||||
|
// v1: ignore the player's chosen indices and echo the same hand back.
|
||||||
|
// (Acceptable because the client doesn't validate which idxs come back — it just renders them.)
|
||||||
|
_ = swapIndices;
|
||||||
|
var body = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["self"] = new List<object?>
|
||||||
|
{
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 0, ["idx"] = 1 },
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 1, ["idx"] = 2 },
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 2, ["idx"] = 3 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return EnvelopeForPush(NetworkBattleUri.Swap, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MsgEnvelope BuildReady()
|
||||||
|
{
|
||||||
|
var body = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["self"] = new List<object?>
|
||||||
|
{
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 0, ["idx"] = 1 },
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 1, ["idx"] = 2 },
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 2, ["idx"] = 3 },
|
||||||
|
},
|
||||||
|
["oppo"] = new List<object?>
|
||||||
|
{
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 0, ["idx"] = 1 },
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 1, ["idx"] = 2 },
|
||||||
|
new Dictionary<string, object?> { ["pos"] = 2, ["idx"] = 3 },
|
||||||
|
},
|
||||||
|
["idxChangeSeed"] = 771335280,
|
||||||
|
["spin"] = 243,
|
||||||
|
};
|
||||||
|
return EnvelopeForPush(NetworkBattleUri.Ready, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<object?> BuildDummyDeck()
|
||||||
|
{
|
||||||
|
var deck = new List<object?>(30);
|
||||||
|
for (var i = 1; i <= 30; i++)
|
||||||
|
{
|
||||||
|
deck.Add(new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["idx"] = i,
|
||||||
|
["cardId"] = DummyCardId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return deck;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MsgEnvelope EnvelopeForPush(NetworkBattleUri uri, Dictionary<string, object?> body, string? bid = null) =>
|
||||||
|
new(uri,
|
||||||
|
ViewerId: FakeOpponentViewerId,
|
||||||
|
Uuid: "node-stub",
|
||||||
|
Bid: bid,
|
||||||
|
Try: 0,
|
||||||
|
Cat: EmitCategory.Battle,
|
||||||
|
PubSeq: null,
|
||||||
|
PlaySeq: null, // OutboundSequencer.AssignAndArchive stamps this
|
||||||
|
Body: body);
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using SVSim.BattleNode.Lifecycle;
|
||||||
|
using SVSim.BattleNode.Protocol;
|
||||||
|
|
||||||
|
namespace SVSim.UnitTests.BattleNode.Lifecycle;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class ScriptedLifecycleTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void BuildMatched_PutsOppoIdInSelfInfoEqualToTheRealOpponentVid()
|
||||||
|
{
|
||||||
|
var env = ScriptedLifecycle.BuildMatched(playerViewerId: 906243102, opponentViewerId: 847666884, battleId: "b");
|
||||||
|
|
||||||
|
Assert.That(env.Uri, Is.EqualTo(NetworkBattleUri.Matched));
|
||||||
|
var selfInfo = (Dictionary<string, object?>)env.Body["selfInfo"]!;
|
||||||
|
Assert.That(selfInfo["oppoId"], Is.EqualTo(847666884L));
|
||||||
|
var oppoInfo = (Dictionary<string, object?>)env.Body["oppoInfo"]!;
|
||||||
|
Assert.That(oppoInfo["oppoId"], Is.EqualTo(906243102L));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void BuildMatched_ContainsThirtyCardSelfDeck()
|
||||||
|
{
|
||||||
|
var env = ScriptedLifecycle.BuildMatched(1, 2, "b");
|
||||||
|
var deck = (List<object?>)env.Body["selfDeck"]!;
|
||||||
|
Assert.That(deck.Count, Is.EqualTo(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void BuildBattleStart_HasTurnStateZeroAndBattleTypeEleven()
|
||||||
|
{
|
||||||
|
var env = ScriptedLifecycle.BuildBattleStart(playerViewerId: 1);
|
||||||
|
Assert.That(env.Body["turnState"], Is.EqualTo(0));
|
||||||
|
Assert.That(env.Body["battleType"], Is.EqualTo(11));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void BuildDeal_HasThreeSelfAndThreeOppoEntries()
|
||||||
|
{
|
||||||
|
var env = ScriptedLifecycle.BuildDeal();
|
||||||
|
var self = (List<object?>)env.Body["self"]!;
|
||||||
|
var oppo = (List<object?>)env.Body["oppo"]!;
|
||||||
|
Assert.That(self.Count, Is.EqualTo(3));
|
||||||
|
Assert.That(oppo.Count, Is.EqualTo(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void BuildSwapResponse_EchoesSameHandIfNoSwap()
|
||||||
|
{
|
||||||
|
var env = ScriptedLifecycle.BuildSwapResponse(swapIndices: Array.Empty<long>());
|
||||||
|
var self = (List<object?>)env.Body["self"]!;
|
||||||
|
Assert.That(self.Count, Is.EqualTo(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void BuildReady_IncludesIdxChangeSeedAndSpin()
|
||||||
|
{
|
||||||
|
var env = ScriptedLifecycle.BuildReady();
|
||||||
|
Assert.That(env.Body.ContainsKey("idxChangeSeed"), Is.True);
|
||||||
|
Assert.That(env.Body.ContainsKey("spin"), Is.True);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user