fix(battle-node): Bot mode must push Matched + BattleStart (client state-machine triggers)
Phase 3 shipped a Bot dispatch table that ack'd InitBattle without pushing Matched and stayed silent on Loaded, per the architecture spec's inference that "the client uses AIBattleStart HTTP data instead of Matched in Bot mode." That inference was wrong. The client's matching state machine (Matching.ReactionReceiveUri, Matching.cs:400) gates StartBattleLoad() on the Matched envelope, and BattleStart at Matching.cs:417 triggers GotoBattle. Without those envelopes the client never transitions out of MatchingStatus.Connect — which renders as the "Waiting for opponent" hang on the loading screen. AIBattleStart HTTP only provides opponent cosmetics, not state-machine triggers. Fix: drop the Bot-specific InitBattle ack-only and Loaded silent arms; let Bot fall through to the existing handshake arms that push Matched and BattleStart + Deal. Only TurnEnd stays Bot-specific (Judge to sender, not broadcast — there's no real other side to broadcast to). Tests updated to match the corrected contract. ai-passive.md doc amended with a correction note. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -440,20 +440,25 @@ public class BattleNodeFlowTests
|
||||
var ack1 = await client.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(ack1.Uri, Is.EqualTo(NetworkBattleUri.InitNetwork));
|
||||
|
||||
// InitBattle → ack (NOT Matched).
|
||||
// InitBattle → Matched. (Same handshake arm as Scripted/PvP; the client's
|
||||
// matching state machine gates StartBattleLoad on receiving Matched, so the
|
||||
// envelope MUST be sent. Opponent cosmetics in the body are placeholders;
|
||||
// the client renders opponent UI from AIBattleStart HTTP data.)
|
||||
await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.InitBattle, pubSeq: 2), key, ct);
|
||||
var ack2 = await client.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(ack2.Uri, Is.EqualTo(NetworkBattleUri.InitBattle),
|
||||
"Bot's InitBattle is ack-only — no Matched envelope.");
|
||||
var matched = await client.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(matched.Uri, Is.EqualTo(NetworkBattleUri.Matched),
|
||||
"Bot's InitBattle pushes Matched (the state-machine trigger).");
|
||||
|
||||
// Loaded → silent. Send Swap right after; the next inbound must be SwapResponse
|
||||
// (no orphan BattleStart / Deal in the queue).
|
||||
// Loaded → BattleStart + Deal (BattleStart triggers GotoBattle on the client).
|
||||
await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.Loaded, pubSeq: 3), key, ct);
|
||||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.BattleStart));
|
||||
Assert.That((await client.ReceiveSynchronizeAsync(ct)).Uri, Is.EqualTo(NetworkBattleUri.Deal));
|
||||
|
||||
// Swap → SwapResponse + Ready.
|
||||
await client.SendMsgAsync(MakeEnvelopeWith(vid, NetworkBattleUri.Swap, pubSeq: 4,
|
||||
body: new Dictionary<string, object?> { ["idxList"] = new List<object?>() }), key, ct);
|
||||
var swapResp = await client.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(swapResp.Uri, Is.EqualTo(NetworkBattleUri.Swap),
|
||||
"Expected Swap response (mulligan ack). Got " + swapResp.Uri + " — Loaded may have leaked a frame.");
|
||||
Assert.That(swapResp.Uri, Is.EqualTo(NetworkBattleUri.Swap));
|
||||
var readyResp = await client.ReceiveSynchronizeAsync(ct);
|
||||
Assert.That(readyResp.Uri, Is.EqualTo(NetworkBattleUri.Ready));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user