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>
The client's OnReceived routing drops any synchronize push whose
resultCode != Success(1) — and absent counts as 0(None), which is
also dropped. Our InitNetwork ack and BattleFinish already included
resultCode=1, but the five lifecycle bodies (Matched, BattleStart,
Deal, Swap response, Ready) didn't, so the client silently dropped
every one of them.
Symptom: battle-traffic.ndjson capture showed the client receiving
InitNetwork/Matched/BattleStart, but the UI stayed at the matchmaking
screen until timeout — Matched/BattleStart were dropped at the
routing layer before they ever reached the state machine. Move the
resultCode injection into the shared EnvelopeForPush helper so every
scripted push gets it.
Caught during v1 smoke walkthrough.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>