fix(battle-node): type-agnostic mulligan barrier withholds Ready until both swap
Ready was sent per-side immediately carrying the placeholder opponent hand, so one client cleared mulligan before the other. The barrier now releases Ready to every IHasHandshakePhase participant only once all have swapped, each carrying the opponent's real post-mulligan hand. No Type check — NoOp (Bot/AINetwork) isn't a phase impl, so that mode still releases immediately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,14 @@ public sealed class BattleSession
|
||||
{
|
||||
private readonly ILogger<BattleSession> _log;
|
||||
|
||||
// Mulligan barrier: each side's post-mulligan hand, captured on its Swap. Ready is
|
||||
// withheld until every IHasHandshakePhase participant has swapped (prod withholds it
|
||||
// ~3-6 s — see data_dumps/captures/battle-traffic_tk2_*.ndjson). Keyed on the
|
||||
// IBattleParticipant so the per-side hand is retrievable when releasing both Readys.
|
||||
// NOTE: mutated from ComputeFrames, which is not lock-guarded — same pre-existing
|
||||
// single-threaded-dispatch assumption as the Phase mutations (see plan § Out of scope).
|
||||
private readonly Dictionary<IBattleParticipant, long[]> _postSwapHands = new();
|
||||
|
||||
public string BattleId { get; }
|
||||
public BattleType Type { get; }
|
||||
public IBattleParticipant A { get; }
|
||||
@@ -224,9 +232,28 @@ public sealed class BattleSession
|
||||
case NetworkBattleUri.Swap when phaseFrom?.Phase == BattleSessionPhase.AwaitingSwap:
|
||||
{
|
||||
var hand = ScriptedLifecycle.ComputeHandAfterSwap(ExtractIdxList(env));
|
||||
// SwapResponse is always immediate — it completes the sender's own mulligan UI.
|
||||
result.Add((from, ScriptedLifecycle.BuildSwapResponse(hand), false));
|
||||
result.Add((from, ScriptedLifecycle.BuildReady(hand), false));
|
||||
_postSwapHands[from] = hand;
|
||||
phaseFrom!.Phase = BattleSessionPhase.AfterReady;
|
||||
|
||||
// Release Ready to every swapper once all handshake-driving participants have
|
||||
// swapped. IHasHandshakePhase membership IS the "participates in mulligan" set:
|
||||
// PvP → {A, B} (both reals) → waits for both
|
||||
// Scripted → {player, bot} (bot now emits Swap) → waits for both
|
||||
// Bot/AINet → {real} only (NoOp isn't a phase impl)→ releases on the one Swap
|
||||
var swappers = new[] { A, B }.Where(p => p is IHasHandshakePhase).ToList();
|
||||
if (swappers.All(_postSwapHands.ContainsKey))
|
||||
{
|
||||
foreach (var p in swappers)
|
||||
{
|
||||
var opponent = ReferenceEquals(p, A) ? B : A;
|
||||
var ready = opponent is IHasHandshakePhase && _postSwapHands.TryGetValue(opponent, out var oppoHand)
|
||||
? ScriptedLifecycle.BuildReady(_postSwapHands[p], oppoHand) // both hands known
|
||||
: ScriptedLifecycle.BuildReady(_postSwapHands[p]); // non-interactive opponent
|
||||
result.Add((p, ready, false));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user