feat(battle-node): Bot dispatch arms in ComputeFrames

Three new arms gated on Type == BattleType.Bot, placed before the
existing PvP / Scripted arms:
- InitBattle → ack to sender (no Matched push — client uses AIBattleStart HTTP data)
- Loaded → silent (no BattleStart, no Deal — client short-circuits to GotoBattle)
- TurnEnd / TurnEndFinal → Judge to sender only (not broadcast)

Other URIs in Bot mode fall through existing arms: Swap is Type-agnostic
(per-sender SwapResponse + Ready), Retire/Kill hits the existing
Scripted no-contest BattleFinish(Win), gameplay forwarders are gated on
Pvp so Bot's PlayActions/Echo/etc. fall through default (drop). 8 new
dispatch tests cover the wire contract.

Reference: docs/api-spec/in-battle/ai-passive.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-02 01:27:08 -04:00
parent 07eb6f1c05
commit a4685a9188
2 changed files with 166 additions and 0 deletions

View File

@@ -135,6 +135,38 @@ public sealed class BattleSession
phaseFrom!.Phase = BattleSessionPhase.AwaitingInitBattle;
break;
// --- Phase 3 Bot arms — placed BEFORE the existing handshake arms so they
// win pattern matching on Type == Bot. Bot mode: ack handshake, silent Loaded,
// Judge-to-sender on TurnEnd. The rest reuse Scripted's arms (Retire/Kill ->
// BattleFinishNoContest, Swap -> per-sender response, default -> drop).
// Reference: docs/api-spec/in-battle/ai-passive.md.
case NetworkBattleUri.InitBattle
when Type == BattleType.Bot && phaseFrom?.Phase == BattleSessionPhase.AwaitingInitBattle:
// Ack only — NO Matched push. Client uses AIBattleStart HTTP data for
// the lobby plates instead of the Matched envelope.
result.Add((from, BuildAck(NetworkBattleUri.InitBattle), true));
phaseFrom!.Phase = BattleSessionPhase.AwaitingLoaded;
break;
case NetworkBattleUri.Loaded
when Type == BattleType.Bot && phaseFrom?.Phase == BattleSessionPhase.AwaitingLoaded:
// Silent — no BattleStart, no Deal. Client's AINetworkBattleManager
// short-circuits to its local AI flow after Loaded; the server has
// nothing to contribute.
phaseFrom!.Phase = BattleSessionPhase.AwaitingSwap;
break;
case NetworkBattleUri.TurnEnd
when Type == BattleType.Bot && phaseFrom?.Phase == BattleSessionPhase.AfterReady:
case NetworkBattleUri.TurnEndFinal
when Type == BattleType.Bot && phaseFrom?.Phase == BattleSessionPhase.AfterReady:
// Judge to sender ONLY (not broadcast — there's no real other side).
// The client's JudgeOperation → ControlTurnStartPlayer flips back to
// the local AI's turn after this Judge arrives.
result.Add((from, BuildJudgeBroadcast(), false));
break;
case NetworkBattleUri.InitBattle when phaseFrom?.Phase == BattleSessionPhase.AwaitingInitBattle:
// Phase 1: push Matched only to the "real" participant. The session reads
// selfInfo from from.Context and oppoInfo from other.Context (the scripted