Three dispatch arms had Type-based branching that was either wrong or
unnecessary. Unified per the audit doc's recommended order, grounded in
verified facts about each participant's PushAsync.
(1) TurnEndFinal — was branched: PvP broadcast TurnEnd+Judge (wrong on a
game-end signal); Scripted pushed BattleFinish(LifeWin). Unified:
- forward the envelope to other (matches prod TK2 capture
battle-traffic_tk2_regular.ndjson:273 — loser receives TurnEndFinal
from server before BattleFinish)
- push BattleFinish(LifeWin) to from (winner)
- push BattleFinish(LifeLose) to other (loser)
- Phase → Terminal
Requires ScriptedBotParticipant.PushAsync to no longer fire its 3-frame
burst on TurnEndFinal (previously it reacted to both TurnEnd and
TurnEndFinal). The dispatch arm now owns TurnEndFinal's response; the
bot reacting too would race with the BattleFinish push. Bot still
fires on regular TurnEnd as before.
(2) Retire / Kill — was branched: PvP pushed Lose=0 (NotFinish) /
Win=1 (NoContest); Scripted pushed BuildBattleFinishNoContest() (Win=1).
Both shipped wrong RESULT_CODE values; the audit doc's outstanding item
documented this. Unified:
- push BattleFinish(RetireLose=106) to from (the retirer)
- push BattleFinish(RetireWin=105) to other (the survivor)
- Phase → Terminal
Added RetireWin=105 / RetireLose=106 to BattleResult enum with the
same player-perspective convention.
(3) PvP gameplay forwarder (TurnStart / PlayActions / Echo /
TurnEndActions / JudgeResult) — had a redundant `Type == BattleType.Pvp`
guard. Verified that BothAfterReady() is naturally only true when both
participants are RealParticipant (ScriptedBot / NoOpBot don't implement
IHasHandshakePhase per RealParticipant.cs:20-23 / Participants/*.cs grep).
Dropped the redundant guard.
Bot type still has its dedicated InitBattle/Loaded/TurnEnd arms above
the unified ones, so Bot-specific behavior is unchanged.
Tests: 177 battle-node tests passing.
- Updated 9 tests to match the unified dispatch (paired BattleFinish
pushes, correct RESULT_CODE values, forwarded TurnEndFinal envelope).
- ScriptedBotParticipantTests.PushAsync_TurnEndFinal_* rewritten to
assert the bot does NOT fire on TurnEndFinal (was asserting it did).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
81 lines
2.9 KiB
C#
81 lines
2.9 KiB
C#
using NUnit.Framework;
|
|
using SVSim.BattleNode.Bridge;
|
|
using SVSim.BattleNode.Protocol;
|
|
using SVSim.BattleNode.Protocol.Bodies;
|
|
using SVSim.BattleNode.Sessions;
|
|
using SVSim.BattleNode.Sessions.Participants;
|
|
|
|
namespace SVSim.UnitTests.BattleNode.Sessions.Participants;
|
|
|
|
[TestFixture]
|
|
public class ScriptedBotParticipantTests
|
|
{
|
|
[Test]
|
|
public async Task PushAsync_TurnEnd_fires_three_FrameEmitted_in_order()
|
|
{
|
|
var p = new ScriptedBotParticipant();
|
|
var emitted = new List<NetworkBattleUri>();
|
|
p.FrameEmitted += (env, _) => { emitted.Add(env.Uri); return Task.CompletedTask; };
|
|
|
|
await p.PushAsync(NewEnvelope(NetworkBattleUri.TurnEnd), noStock: false, CancellationToken.None);
|
|
|
|
Assert.That(emitted, Is.EqualTo(new[]
|
|
{
|
|
NetworkBattleUri.TurnStart,
|
|
NetworkBattleUri.TurnEnd,
|
|
NetworkBattleUri.Judge,
|
|
}));
|
|
}
|
|
|
|
[Test]
|
|
public async Task PushAsync_TurnEndFinal_does_NOT_fire_burst()
|
|
{
|
|
// TurnEndFinal is the game-end signal — owned by BattleSession's TurnEndFinal
|
|
// dispatch arm, which pushes BattleFinish per-side. The bot no longer reacts to
|
|
// it; reacting would race the BattleFinish with the no-longer-needed 3-frame
|
|
// burst. Only regular TurnEnd triggers the burst.
|
|
var p = new ScriptedBotParticipant();
|
|
var fired = 0;
|
|
p.FrameEmitted += (_, _) => { fired++; return Task.CompletedTask; };
|
|
|
|
await p.PushAsync(NewEnvelope(NetworkBattleUri.TurnEndFinal), noStock: false, CancellationToken.None);
|
|
|
|
Assert.That(fired, Is.EqualTo(0),
|
|
"TurnEndFinal must not trigger the bot's burst — the dispatch arm pushes BattleFinish directly.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task PushAsync_other_uris_do_not_fire()
|
|
{
|
|
var p = new ScriptedBotParticipant();
|
|
var fired = 0;
|
|
p.FrameEmitted += (_, _) => { fired++; return Task.CompletedTask; };
|
|
|
|
foreach (var uri in new[]
|
|
{
|
|
NetworkBattleUri.Matched, NetworkBattleUri.BattleStart, NetworkBattleUri.Deal,
|
|
NetworkBattleUri.Swap, NetworkBattleUri.Ready, NetworkBattleUri.PlayActions,
|
|
NetworkBattleUri.TurnStart, NetworkBattleUri.TurnEndActions, NetworkBattleUri.Echo,
|
|
NetworkBattleUri.Judge, NetworkBattleUri.BattleFinish,
|
|
})
|
|
{
|
|
await p.PushAsync(NewEnvelope(uri), noStock: false, CancellationToken.None);
|
|
}
|
|
|
|
Assert.That(fired, Is.EqualTo(0));
|
|
}
|
|
|
|
[Test]
|
|
public async Task RunAsync_returns_immediately()
|
|
{
|
|
var p = new ScriptedBotParticipant();
|
|
await p.RunAsync(CancellationToken.None);
|
|
Assert.Pass();
|
|
}
|
|
|
|
private static MsgEnvelope NewEnvelope(NetworkBattleUri uri) =>
|
|
new(uri, ViewerId: 1, Uuid: "u", Bid: null, Try: 0,
|
|
Cat: EmitCategory.Battle, PubSeq: null, PlaySeq: null,
|
|
Body: new ResultCodeOnlyBody());
|
|
}
|