refactor(battle-node): remove ScriptedBotParticipant and dev-affordance wiring
Deletes the scripted opponent and every entry point that created a BattleType.Scripted session (the ?scripted=1 query opt-in, the SoloDefaultsToScripted toggle, the resolver short-circuit, the WS handler case, the bridge validation arm). Real two-client PvP and the Bot matchmaking-timeout fallback are untouched. ResolveAsync drops its scriptedOptIn parameter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,17 +16,6 @@ public sealed class BattleNodeOptions
|
||||
/// </summary>
|
||||
public TimeSpan WaitingRoomTimeout { get; set; } = TimeSpan.FromSeconds(60);
|
||||
|
||||
/// <summary>
|
||||
/// Dev convenience: when true, matchmaking endpoints that would otherwise park
|
||||
/// a solo poller (returning 3002 RETRY until a partner arrives) instead return
|
||||
/// a Scripted match immediately — equivalent to passing <c>?scripted=1</c> on
|
||||
/// every request. Turn off to test real PvP with two clients. Default false.
|
||||
/// <para>Trade-off: while on, two viewers polling simultaneously each get
|
||||
/// their own Scripted match instead of pairing with each other. Toggling off
|
||||
/// is the only way to get PvP behavior.</para>
|
||||
/// </summary>
|
||||
public bool SoloDefaultsToScripted { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// When true, <see cref="Sessions.Participants.RealParticipant"/> emits per-frame
|
||||
/// diagnostic logs at Information level: <c>[sio-in]</c> on every inbound msg/alive/hand
|
||||
|
||||
@@ -47,9 +47,6 @@ public sealed class MatchingBridge : IMatchingBridge
|
||||
case BattleType.Bot:
|
||||
if (p2 is not null) throw new ArgumentException("Bot must have p2==null.", nameof(p2));
|
||||
break;
|
||||
case BattleType.Scripted:
|
||||
// p2 currently null; future server-driven bot will populate it.
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown BattleType.");
|
||||
}
|
||||
|
||||
@@ -120,18 +120,6 @@ public sealed class BattleNodeWebSocketHandler
|
||||
|
||||
switch (pending.Type)
|
||||
{
|
||||
case BattleType.Scripted:
|
||||
{
|
||||
_store.RemovePending(battleId);
|
||||
var realParticipant = new RealParticipant(ws, viewerId, pending.P1.Context,
|
||||
_loggerFactory.CreateLogger<RealParticipant>(), _options.DiagnosticLogging);
|
||||
var scriptedBot = new ScriptedBotParticipant();
|
||||
var session = new BattleSession(battleId, pending.Type, realParticipant, scriptedBot,
|
||||
_loggerFactory.CreateLogger<BattleSession>());
|
||||
await session.RunAsync(ctx.RequestAborted);
|
||||
break;
|
||||
}
|
||||
|
||||
case BattleType.Pvp:
|
||||
{
|
||||
// Pick this connection's MatchContext (P1's if isP1, P2's if isP2).
|
||||
|
||||
@@ -127,8 +127,8 @@ public static class ScriptedLifecycle
|
||||
IdxChangeSeed: ScriptedProfiles.ReadyIdxChangeSeed,
|
||||
Spin: ScriptedProfiles.ReadySpin));
|
||||
|
||||
// --- Client-shaped emissions used by ScriptedBotParticipant so the session brokers
|
||||
// the bot through the same handshake arms as a human. Bodies for the parameterless
|
||||
// --- Client-shaped emissions (legacy scripted-bot scaffolding, pending removal) so the
|
||||
// session brokers the bot through the same handshake arms as a human. Bodies for the parameterless
|
||||
// handshake frames are ignored by the session (it reads from.Context / phase); only
|
||||
// Swap's idxList is consumed (empty = keep the dealt hand).
|
||||
|
||||
|
||||
@@ -13,9 +13,8 @@ namespace SVSim.BattleNode.Sessions;
|
||||
/// flag) and dispatches via <see cref="IBattleParticipant.PushAsync"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Phase 1 wires this for <see cref="BattleType.Scripted"/> only — the dispatch logic
|
||||
/// preserves v1.2 behaviour. Phase 2 wires Pvp (broadcast Matched/BattleStart per-perspective,
|
||||
/// forward gameplay frames between participants). Phase 3 wires Bot (ack-only).
|
||||
/// Wires both battle modes: Pvp (broadcast Matched/BattleStart per-perspective, forward
|
||||
/// gameplay frames between the two real participants) and Bot (ack-only, NoOp opponent).
|
||||
/// </remarks>
|
||||
public sealed class BattleSession
|
||||
{
|
||||
@@ -85,10 +84,9 @@ public sealed class BattleSession
|
||||
|
||||
if (Type == BattleType.Pvp)
|
||||
{
|
||||
// WhenAny: first WS drop / first graceful close triggers cascade.
|
||||
// ScriptedBotParticipant.RunAsync also returns immediately; that's not used
|
||||
// here (Pvp has two RealParticipants), but we'd still want a synthesized
|
||||
// BattleFinish for the survivor if either side terminates first.
|
||||
// WhenAny: first WS drop / first graceful close triggers cascade. Pvp has two
|
||||
// RealParticipants; we synthesize a BattleFinish for the survivor if either side
|
||||
// terminates first.
|
||||
var first = await Task.WhenAny(aTask, bTask).ConfigureAwait(false);
|
||||
var survivor = first == aTask ? B : A;
|
||||
|
||||
@@ -118,8 +116,8 @@ public sealed class BattleSession
|
||||
}
|
||||
else
|
||||
{
|
||||
// Phase 1 semantics for Scripted/Bot: wait for ALL participants. The bot's
|
||||
// RunAsync returns immediately; the session keeps running for the real one.
|
||||
// Bot mode: the NoOp opponent's RunAsync returns immediately; wait for the real
|
||||
// participant. The session keeps running for the real one.
|
||||
try { await Task.WhenAll(aTask, bTask).ConfigureAwait(false); }
|
||||
catch { /* swallow */ }
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ namespace SVSim.BattleNode.Sessions;
|
||||
/// <list type="bullet">
|
||||
/// <item><c>RealParticipant</c> — WS-backed.</item>
|
||||
/// <item><c>NoOpBotParticipant</c> — silent; for <c>BattleType.Bot</c> (AI-passive).</item>
|
||||
/// <item><c>ScriptedBotParticipant</c> — wraps the v1.2 lifecycle for
|
||||
/// <c>BattleType.Scripted</c> (solo testing harness).</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public interface IBattleParticipant : IAsyncDisposable
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
using System.Linq;
|
||||
using SVSim.BattleNode.Bridge;
|
||||
using SVSim.BattleNode.Lifecycle;
|
||||
using SVSim.BattleNode.Protocol;
|
||||
|
||||
namespace SVSim.BattleNode.Sessions.Participants;
|
||||
|
||||
/// <summary>
|
||||
/// Server-scripted opponent that drives a client-shaped emit chain so the session brokers
|
||||
/// it through the same handshake arms as a human. <see cref="RunAsync"/> kicks off
|
||||
/// <c>InitNetwork</c>; the session's pushes then drive <see cref="PushAsync"/> reactively:
|
||||
/// <c>InitNetwork</c>(ack)→<c>InitBattle</c>, <c>Matched</c>→<c>Loaded</c>, <c>Deal</c>→<c>Swap</c>
|
||||
/// (empty mulligan). After the player's <c>TurnEnd</c> it fires the v1.2 three-frame burst
|
||||
/// (<c>OpponentTurnStart</c>, <c>OpponentTurnEnd</c>, <c>OpponentJudge</c>). All other URIs
|
||||
/// are swallowed. Implementing <see cref="IHasHandshakePhase"/> is what makes the session
|
||||
/// treat it as a real handshake participant (mulligan-barrier swapper included).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ViewerId, Context are fixtures matching <see cref="ScriptedLifecycle.FakeOpponentViewerId"/>
|
||||
/// and a scripted opponent profile. The Context fixture is the source of truth for the
|
||||
/// scripted opponent's half of Matched and BattleStart (cosmetics, deck count, class/chara) —
|
||||
/// <see cref="BattleSession.ComputeFrames"/> reads <c>other.Context</c> for those frames.
|
||||
/// Deal still uses fixed scripted frames that ignore Context.
|
||||
/// </remarks>
|
||||
public sealed class ScriptedBotParticipant : IBattleParticipant, IHasHandshakePhase
|
||||
{
|
||||
public long ViewerId => ScriptedLifecycle.FakeOpponentViewerId;
|
||||
|
||||
public MatchContext Context { get; } = new(
|
||||
// 30 dummy card ids so oppoCtx.SelfDeckCardIds.Count == 30 — prod frame[2] (Matched)
|
||||
// shipped OppoDeckCount: 30.
|
||||
SelfDeckCardIds: Enumerable.Range(1, 30).Select(_ => 0L).ToList(),
|
||||
// BattleStart opponent half (frame[5]): ClassId/CharaId both "8" (neutral test class).
|
||||
ClassId: "8", CharaId: "8", CardMasterName: "card_master_node_10015",
|
||||
// Matched opponent half (frame[2]): cosmetic fields from the prod capture.
|
||||
CountryCode: "JPN", UserName: "Opponent", SleeveId: "704141010",
|
||||
EmblemId: "400001100", DegreeId: "120027", FieldId: 5, IsOfficial: 0,
|
||||
BattleType: 0);
|
||||
|
||||
// Session reads/advances this through its phase-gated handshake arms, exactly as it
|
||||
// does for a RealParticipant. The bot doesn't read it — it reacts to pushed URIs —
|
||||
// but implementing IHasHandshakePhase is what makes the session treat the bot as a
|
||||
// real handshake participant (so its InitNetwork/InitBattle/Loaded/Swap emissions are
|
||||
// processed, and the mulligan barrier counts it as a swapper).
|
||||
public BattleSessionPhase Phase { get; set; } = BattleSessionPhase.AwaitingInitNetwork;
|
||||
|
||||
public event Func<MsgEnvelope, CancellationToken, Task>? FrameEmitted;
|
||||
|
||||
// Kick off the handshake like a connecting client. The session acks InitNetwork,
|
||||
// which drives PushAsync below through InitBattle → Loaded → Swap.
|
||||
public Task RunAsync(CancellationToken ct) => EmitAsync(ScriptedLifecycle.BuildClientInitNetwork(), ct);
|
||||
|
||||
public async Task PushAsync(MsgEnvelope envelope, bool noStock, CancellationToken ct)
|
||||
{
|
||||
switch (envelope.Uri)
|
||||
{
|
||||
case NetworkBattleUri.InitNetwork: // the ack
|
||||
await EmitAsync(ScriptedLifecycle.BuildClientInitBattle(), ct).ConfigureAwait(false);
|
||||
break;
|
||||
case NetworkBattleUri.Matched:
|
||||
await EmitAsync(ScriptedLifecycle.BuildClientLoaded(), ct).ConfigureAwait(false);
|
||||
break;
|
||||
case NetworkBattleUri.Deal:
|
||||
await EmitAsync(ScriptedLifecycle.BuildClientSwap(), ct).ConfigureAwait(false);
|
||||
break;
|
||||
case NetworkBattleUri.TurnEnd:
|
||||
// v1.2 scripted-turn burst, taken AFTER the player's turn (bot is second).
|
||||
await EmitAsync(ScriptedLifecycle.BuildOpponentTurnStart(), ct).ConfigureAwait(false);
|
||||
await EmitAsync(ScriptedLifecycle.BuildOpponentTurnEnd(), ct).ConfigureAwait(false);
|
||||
await EmitAsync(ScriptedLifecycle.BuildOpponentJudge(), ct).ConfigureAwait(false);
|
||||
break;
|
||||
// Everything else (BattleStart, our own Swap-response, Ready, TurnEndFinal,
|
||||
// Judge, BattleFinish, …) needs no bot reaction.
|
||||
}
|
||||
}
|
||||
|
||||
public Task TerminateAsync(BattleFinishReason reason) => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
private Task EmitAsync(MsgEnvelope env, CancellationToken ct) =>
|
||||
FrameEmitted?.Invoke(env, ct) ?? Task.CompletedTask;
|
||||
}
|
||||
Reference in New Issue
Block a user