feat(battlenode): engine read surface for played-card spellboost (Phase 2 N2 Task 3 — oracle BLOCKED)

Adds SessionBattleEngine.PlayedCardSpellboost + PeekPlayedCardSpellboost (pre-resolve
read of the acting seat's hand card by Index==playIdx) and a CaptureReplay.InterleavedSends
helper. The non-circular capture oracle (engine-derived spellboost vs prod's independent
emission to cl2: idx2->1, idx14->2) is added but [Ignore]'d: the headless receive path does
not apply the wire's authoritative orderList (Deal/Swap don't seat the mulligan hand, draws
follow the seeded deck top instead of the wire move ops, plays never remove the card, alter
spellboost never accumulates), so the engine cannot yet DERIVE the count. Closing this needs
an Engine/*.cs + VfxMgr-execution logic change (escalation per the N2 playbook), not a
mechanical no-op fill. Read surface, node + engine builds, drift, and the rest of the
SessionEngine suite are green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-06 16:55:47 -04:00
parent eb52890251
commit fcd64c8c11
3 changed files with 104 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ using System.Reflection;
using System.Runtime.Serialization;
using engine::SVSim.BattleEngine.Rng;
using SVSim.BattleNode.Protocol;
using SVSim.BattleNode.Sessions.Dispatch;
using NetworkBattleReceiver = engine::NetworkBattleReceiver;
using NetworkBattleDefine = engine::NetworkBattleDefine;
using BattleManagerBase = engine::BattleManagerBase;
@@ -40,10 +41,19 @@ internal sealed class SessionBattleEngine
private HeadlessNetworkBattleMgr? _mgr;
private NetworkBattleReceiver? _receiver;
private int _lastPlayedSpellboost;
/// <summary>True once Setup has built the two-seat battle.</summary>
public bool IsReady => _mgr is not null;
/// <summary>The spellboost (spell-charge) COUNT of the card the most-recently-ingested PlayActions
/// frame played, read from the acting seat's hand BEFORE the frame resolved (the count is fixed as the
/// card leaves hand; a play that grants spellboost targets the REST of the hand, not the card just
/// played). 0 for a non-play frame, a token/unmapped idx, or a card not in hand. PlayActionsHandler
/// reads this right after Receive — the BattleSession _dispatchGate serializes Receive→Handle, so this
/// is unambiguously this frame's value.</summary>
public int PlayedCardSpellboost => _lastPlayedSpellboost;
/// <summary>Construct the two-seat network battle from both decks + the master seed (design F-N-5).
/// <paramref name="seatADeck"/>/<paramref name="seatBDeck"/> are the per-side deck orders the node
/// already computed (BattleSessionState.GetShuffledDeck) and handed each client.
@@ -115,6 +125,12 @@ internal sealed class SessionBattleEngine
var dict = ToEngineDict((env.Body as RawBody)?.Entries);
var uri = MapUri(env.Uri);
// Peek the played card's accumulated spellboost count BEFORE resolution: the count is fixed as
// the card leaves hand, so it must be read while the card is still in hand. 0 for any non-play.
_lastPlayedSpellboost = uri == NetworkBattleDefine.NetworkBattleURI.PlayActions
? PeekPlayedCardSpellboost(env, isPlayerSeat)
: 0;
try
{
// Mirror the engine's own recorded-frame replay (RecoveryDataHandler.cs:283): every
@@ -131,6 +147,21 @@ internal sealed class SessionBattleEngine
}
}
/// <summary>Read the played card's accumulated spellboost count off the acting seat's hand, matching
/// the card by Index == wire playIdx. Returns 0 when the body has no playIdx, or no hand card matches
/// (a token/unmapped idx, or a card already gone from hand). Pre-resolve read (see <see
/// cref="PlayedCardSpellboost"/>).</summary>
private int PeekPlayedCardSpellboost(MsgEnvelope env, bool isPlayerSeat)
{
if (_mgr is null) return 0;
var entries = (env.Body as RawBody)?.Entries;
if (entries is null) return 0;
int playIdx = (int)KnownListBuilder.AsLong(entries.GetValueOrDefault(WireKeys.PlayIdx));
foreach (var card in _mgr.GetBattlePlayer(isPlayerSeat).HandCardList)
if (card.Index == playIdx) return card.SpellChargeCount;
return 0;
}
// --- live board-state reads (N1 oracle surface; design F-N-4 board-state reads) ----------------
// Each returns LIVE engine state off the seated player, mirroring the Phase-1 oracle reads
// (VanillaFollowerOracleTests: player.Pp, player.HandCardList.Count, ClassAndInPlayCardList,