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:
gamer147
2026-06-01 08:30:44 -04:00
parent e06d97ef6f
commit 77fb93f3ea
5 changed files with 125 additions and 31 deletions

View File

@@ -50,18 +50,51 @@ public class ScriptedLifecycleTests
}
[Test]
public void BuildSwapResponse_EchoesSameHandIfNoSwap()
public void ComputeHandAfterSwap_NoSwap_ReturnsInitialHand()
{
var env = ScriptedLifecycle.BuildSwapResponse(swapIndices: Array.Empty<long>());
var self = (List<object?>)env.Body["self"]!;
Assert.That(self.Count, Is.EqualTo(3));
var hand = ScriptedLifecycle.ComputeHandAfterSwap(Array.Empty<long>());
Assert.That(hand, Is.EqualTo(new long[] { 1, 2, 3 }));
}
[Test]
public void BuildReady_IncludesIdxChangeSeedAndSpin()
public void ComputeHandAfterSwap_SwapMiddleCard_ReplacesWithFreshDeckIdx()
{
var env = ScriptedLifecycle.BuildReady();
var hand = ScriptedLifecycle.ComputeHandAfterSwap(new long[] { 2 });
// pos 0 keeps idx 1; pos 1 (was idx 2) gets next deck idx (4); pos 2 keeps idx 3.
Assert.That(hand, Is.EqualTo(new long[] { 1, 4, 3 }));
}
[Test]
public void ComputeHandAfterSwap_SwapAll_ReplacesAllWithFreshDeckIdxs()
{
var hand = ScriptedLifecycle.ComputeHandAfterSwap(new long[] { 1, 2, 3 });
Assert.That(hand, Is.EqualTo(new long[] { 4, 5, 6 }));
}
[Test]
public void BuildSwapResponse_RendersGivenHandAsPositions()
{
var env = ScriptedLifecycle.BuildSwapResponse(new long[] { 1, 4, 3 });
var self = (List<object?>)env.Body["self"]!;
Assert.That(self.Count, Is.EqualTo(3));
Assert.That(((Dictionary<string, object?>)self[1]!)["idx"], Is.EqualTo(4));
}
[Test]
public void BuildReady_IncludesIdxChangeSeedAndSpin_AndUsesGivenHand()
{
var env = ScriptedLifecycle.BuildReady(new long[] { 1, 4, 3 });
Assert.That(env.Body.ContainsKey("idxChangeSeed"), Is.True);
Assert.That(env.Body.ContainsKey("spin"), Is.True);
var self = (List<object?>)env.Body["self"]!;
Assert.That(((Dictionary<string, object?>)self[1]!)["idx"], Is.EqualTo(4));
}
[Test]
public void BuildOpponentTurnStart_HasUriTurnStartAndSpin()
{
var env = ScriptedLifecycle.BuildOpponentTurnStart();
Assert.That(env.Uri, Is.EqualTo(NetworkBattleUri.TurnStart));
Assert.That(env.Body.ContainsKey("spin"), Is.True);
}
}

View File

@@ -64,6 +64,19 @@ public class BattleSessionDispatchTests
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.AfterReady));
}
[Test]
public void TurnEnd_AfterReady_PushesOpponentTurnStart_TransitionsToOpponentTurn()
{
var s = NewSession();
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitNetwork));
s.ComputeResponses(NewEnvelope(NetworkBattleUri.InitBattle));
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Loaded));
s.ComputeResponses(NewEnvelope(NetworkBattleUri.Swap));
var responses = s.ComputeResponses(NewEnvelope(NetworkBattleUri.TurnEnd));
Assert.That(responses.Single().Envelope.Uri, Is.EqualTo(NetworkBattleUri.TurnStart));
Assert.That(s.Phase, Is.EqualTo(BattleSessionPhase.OpponentTurn));
}
[Test]
public void Retire_PushesBattleFinishNoContest_TransitionsToTerminal()
{