fix(battle-node): real mulligan card replacement + opponent TurnStart push
Two issues caught during v1 smoke at the mulligan / first-turn boundary:
1) BuildSwapResponse ignored the player's idxList and echoed the same
3-card hand back. The client diffs the new self[] against the Deal
to compute "drawn cards" — empty diff against the same hand throws
"Card swap failed: AbandonCards[X]/DrawCards[]". Replace swapped
idxs with fresh deck idxs (initial hand was 1/2/3, deck has 4..30
still available). Same hand must flow into Ready since the client
diffs again there. Move the hand computation into a new helper
ComputeHandAfterSwap and have ComputeResponses thread it through
both BuildSwapResponse and BuildReady.
2) The client doesn't transition to the "Opponent's turn…" display
on its own after sending TurnEnd — it waits for the server to push
an opponent TurnStart (per prod TK2 capture line 14). Without it
the UI just sits on the end-of-turn frame. Add a TurnEnd handler
that pushes a minimal TurnStart{spin} and transitions to a new
OpponentTurn phase, which IS the documented v1 stopping point.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -96,45 +96,77 @@ public static class ScriptedLifecycle
|
||||
return EnvelopeForPush(NetworkBattleUri.Deal, body);
|
||||
}
|
||||
|
||||
public static MsgEnvelope BuildSwapResponse(IReadOnlyList<long> swapIndices)
|
||||
/// <summary>
|
||||
/// Initial 3-card hand idxs from <see cref="BuildDeal"/>. Each position in this array
|
||||
/// is one card; the value is the card's deck idx.
|
||||
/// </summary>
|
||||
private static readonly long[] InitialHand = { 1, 2, 3 };
|
||||
|
||||
/// <summary>
|
||||
/// Compute the player's hand after a mulligan. For every idx in <paramref name="swapIndices"/>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static long[] ComputeHandAfterSwap(IReadOnlyList<long> 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<long> hand)
|
||||
{
|
||||
// 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 },
|
||||
},
|
||||
["self"] = BuildPosIdxList(hand),
|
||||
};
|
||||
return EnvelopeForPush(NetworkBattleUri.Swap, body);
|
||||
}
|
||||
|
||||
public static MsgEnvelope BuildReady()
|
||||
public static MsgEnvelope BuildReady(IReadOnlyList<long> hand)
|
||||
{
|
||||
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 },
|
||||
},
|
||||
["self"] = BuildPosIdxList(hand),
|
||||
// Opponent hand stays at the static 3 cards for v1.
|
||||
["oppo"] = BuildPosIdxList(InitialHand),
|
||||
["idxChangeSeed"] = 771335280,
|
||||
["spin"] = 243,
|
||||
};
|
||||
return EnvelopeForPush(NetworkBattleUri.Ready, body);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static MsgEnvelope BuildOpponentTurnStart()
|
||||
{
|
||||
var body = new Dictionary<string, object?>
|
||||
{
|
||||
["spin"] = 100,
|
||||
};
|
||||
return EnvelopeForPush(NetworkBattleUri.TurnStart, body);
|
||||
}
|
||||
|
||||
private static List<object?> BuildPosIdxList(IReadOnlyList<long> hand)
|
||||
{
|
||||
var list = new List<object?>(hand.Count);
|
||||
for (var pos = 0; pos < hand.Count; pos++)
|
||||
{
|
||||
list.Add(new Dictionary<string, object?> { ["pos"] = pos, ["idx"] = (int)hand[pos] });
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static List<object?> BuildDummyDeck()
|
||||
{
|
||||
var deck = new List<object?>(30);
|
||||
|
||||
Reference in New Issue
Block a user