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:
@@ -458,30 +458,40 @@ public class BattleSessionDispatchTests
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Bot_InitBattle_acks_to_sender_with_no_Matched_push()
|
||||
public void Bot_InitBattle_pushes_Matched_to_sender()
|
||||
{
|
||||
var (s, a, _) = NewBotSession();
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
|
||||
Assert.That(routes.Count, Is.EqualTo(1), "Bot InitBattle is ack-only, no Matched push.");
|
||||
// Bot InitBattle reuses the regular handshake arm — pushes Matched. The
|
||||
// client's Matching.ReactionReceiveUri gates StartBattleLoad on receiving
|
||||
// Matched (Matching.cs:400), so without it the client hangs in
|
||||
// "Waiting for opponent." Cosmetics in the Matched body come from
|
||||
// NoOpBotParticipant.Context placeholders; the client renders opponent
|
||||
// cosmetics from AIBattleStart HTTP data, not from Matched, so the
|
||||
// placeholders are harmless here.
|
||||
Assert.That(routes.Count, Is.EqualTo(1));
|
||||
Assert.That(routes[0].Target, Is.SameAs(a));
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.InitBattle),
|
||||
"Expected an ack envelope for InitBattle, NOT a Matched envelope.");
|
||||
Assert.That(routes[0].Frame.Uri, Is.EqualTo(NetworkBattleUri.Matched));
|
||||
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AwaitingLoaded));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Bot_Loaded_produces_no_routes_but_advances_phase()
|
||||
public void Bot_Loaded_pushes_BattleStart_then_Deal_to_sender()
|
||||
{
|
||||
var (s, a, _) = NewBotSession();
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitNetwork));
|
||||
s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.InitBattle));
|
||||
var routes = s.ComputeFrames(a, NewEnvelope(NetworkBattleUri.Loaded));
|
||||
|
||||
Assert.That(routes, Is.Empty, "Bot Loaded is silent.");
|
||||
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AwaitingSwap),
|
||||
"Phase still advances even though there are no outbound routes.");
|
||||
// Bot Loaded reuses the regular handshake arm — pushes BattleStart + Deal.
|
||||
// BattleStart triggers GotoBattle on the client (Matching.cs:417); without
|
||||
// it the client hangs.
|
||||
Assert.That(routes.Select(r => r.Frame.Uri),
|
||||
Is.EqualTo(new[] { NetworkBattleUri.BattleStart, NetworkBattleUri.Deal }));
|
||||
Assert.That(routes.All(r => ReferenceEquals(r.Target, a)), Is.True);
|
||||
Assert.That(a.Phase, Is.EqualTo(BattleSessionPhase.AwaitingSwap));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
Reference in New Issue
Block a user