From fcc30ffe5e140402151ee7fbd5d4bff323a95cb3 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 6 Jun 2026 19:28:21 -0400 Subject: [PATCH] refactor(battlenode): drop obsolete pre-ingest spellboost peek (Phase 2 revised, O-HC-5) Co-Authored-By: Claude Sonnet 4.6 --- .../SessionEngineSpellboostTests.cs | 59 ------------------- .../Sessions/Engine/SessionBattleEngine.cs | 31 ---------- 2 files changed, 90 deletions(-) diff --git a/SVSim.BattleEngine.Tests/SessionEngine/SessionEngineSpellboostTests.cs b/SVSim.BattleEngine.Tests/SessionEngine/SessionEngineSpellboostTests.cs index ddc40e8..432a92f 100644 --- a/SVSim.BattleEngine.Tests/SessionEngine/SessionEngineSpellboostTests.cs +++ b/SVSim.BattleEngine.Tests/SessionEngine/SessionEngineSpellboostTests.cs @@ -1,10 +1,7 @@ using NUnit.Framework; using SVSim.BattleNode.Sessions.Engine; -using System.Collections.Generic; using System.Linq; using SVSim.BattleEngine.Tests; -using SVSim.BattleNode.Protocol; -using SVSim.BattleNode.Sessions.Dispatch; namespace SVSim.BattleEngine.Tests.SessionEngine; @@ -28,60 +25,4 @@ public class SessionEngineSpellboostTests Assert.That(engine.IsReady, Is.True, "engine must be ready after EngineGlobalInit (carried-risk fix)"); } - // BLOCKED (Phase 2 N2 Task 3): this is the non-circular oracle for engine-derived spellboost — it - // replays cl1's RAW send frames (which build charges via `alter` ops but NEVER carry the count) and - // asserts the engine's derived count equals prod's INDEPENDENT emission to cl2: - // cl1 playIdx 2 (cardId 100314020) -> spellboost 1 - // cl1 playIdx 14 (cardId 101314020) -> spellboost 2 - // - // It currently FAILS ({2:0, 14:0}) — NOT because of the read surface (PlayedCardSpellboost / - // PeekPlayedCardSpellboost are correct and wired), but because the headless receive path does not - // apply the wire's authoritative card resolution at all: - // * Deal / Swap / Ready do NOT seat the mulligan hand (hand stays empty through them); the - // authoritative hand seating is deferred into VfxMgr InstantVfx delegates + an OnReceiveDeal - // view callback that the headless shadow does not drive. - // * TurnStart draws the engine's OWN deck-top instead of the wire `move` op's idx (so e.g. the - // drawn card idx14 never enters hand). - // * PlayActions never removes the played card from hand (the play does not resolve), so the - // spell_charge skill never fires and no `alter` spellboost accumulates (all cards stay sb0). - // Both seats degrade to "top N of the seeded deck", every wire orderList (move/alter/play) a no-op. - // - // Closing this requires Engine/*.cs (and VfxMgr-execution) LOGIC changes to make the recovery-mode - // receive path consume the live wire orderList — which the N2 playbook classifies as an ESCALATION, - // not a mechanical no-op fill. [Ignore] keeps the SessionEngine suite green; remove it (and the - // skip-list ts-replay scaffolding can collapse to InterleavedSends) once the receive path applies - // wire-authoritative resolution. N1's shadow-replay passes only because it asserts structural - // INVARIANTS (life/pp/board/hand bounds), never the actual card identities or spellboost VALUE. - [Test] - [Ignore("BLOCKED N2 Task 3: headless receive path does not apply wire orderList (Deal/draw/play/alter); needs Engine logic change — see comment.")] - public void Engine_derives_played_card_spellboost_matching_prod_emission() - { - EngineGlobalInit.EnsureInitialized(); - - var cl1 = CaptureReplay.Load("battle_test_cl1.ndjson"); - var cl2 = CaptureReplay.Load("battle_test_cl2.ndjson"); - var deckA = CaptureReplay.SelfDeckFrom(cl1); - var deckB = CaptureReplay.SelfDeckFrom(cl2); - foreach (var id in deckA.Concat(deckB).Distinct()) HeadlessCardMaster.Load((int)id); - - var engine = new SessionBattleEngine(); - engine.Setup(masterSeed: CaptureReplay.SeedFrom(cl1), seatADeck: deckA, seatBDeck: deckB); - - var expected = new Dictionary { [2] = 1, [14] = 2 }; - var seen = new Dictionary(); - - foreach (var (env, seat) in CaptureReplay.InterleavedSends(cl1, cl2)) - { - engine.Receive(env, isPlayerSeat: seat); - if (seat && env.Uri == NetworkBattleUri.PlayActions) - { - int playIdx = (int)KnownListBuilder.AsLong( - ((env.Body as RawBody)?.Entries ?? new()).GetValueOrDefault(WireKeys.PlayIdx)); - if (expected.ContainsKey(playIdx)) seen[playIdx] = engine.PlayedCardSpellboost; - } - } - - Assert.That(seen, Is.EquivalentTo(expected), - "engine-derived spellboost must match prod's independent emission to cl2"); - } } diff --git a/SVSim.BattleNode/Sessions/Engine/SessionBattleEngine.cs b/SVSim.BattleNode/Sessions/Engine/SessionBattleEngine.cs index e8ff81a..239fe72 100644 --- a/SVSim.BattleNode/Sessions/Engine/SessionBattleEngine.cs +++ b/SVSim.BattleNode/Sessions/Engine/SessionBattleEngine.cs @@ -3,7 +3,6 @@ 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; @@ -41,19 +40,10 @@ internal sealed class SessionBattleEngine private HeadlessNetworkBattleMgr? _mgr; private NetworkBattleReceiver? _receiver; - private int _lastPlayedSpellboost; /// True once Setup has built the two-seat battle. public bool IsReady => _mgr is not null; - /// 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. - public int PlayedCardSpellboost => _lastPlayedSpellboost; - /// Construct the two-seat network battle from both decks + the master seed (design F-N-5). /// / are the per-side deck orders the node /// already computed (BattleSessionState.GetShuffledDeck) and handed each client. @@ -125,12 +115,6 @@ 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 @@ -147,21 +131,6 @@ internal sealed class SessionBattleEngine } } - /// 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 ). - 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,