test(battle-engine-port): M10 — first dynamic {}-value card resolves headless

DynamicValueSpellOracleTests proves the engine COMPUTES an effect magnitude
from live game state (the value the wire can't carry). Card 112134010's
`when_play damage={me.play_count}-1` resolves via the proven IsForecast/
IsRecovery + ActionProcessor.PlayCard (DP4) path; the oracle asserts the
damage equals the engine's own live GetCurrentTurnPlayCount() - 1, not a
literal. Seeds play_count via M4's AddCurrentTrunPlayCount seam; lone
surviving enemy 13/13 gives a clean life-delta; selectedCards: null
(auto-target AoE). 10/10 green; zero Engine/shim/manifest changes; drift
clean. First-unknown resolved by the first RED: the per-play +1 lives in
OnBeforePlayCard (wired only via OperateMgr/Prediction), so the direct-
ActionProcessor harness reads exactly the seeded count (damage == seeded-1);
load-bearing proven by varying the seed 4->7 and watching damage track 3->6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-06 09:02:59 -04:00
parent eee8450144
commit c3590e9c9b
2 changed files with 173 additions and 1 deletions

View File

@@ -116,6 +116,40 @@ namespace SVSim.BattleEngine.Tests
public const int DrawSpellId = 800114010;
public const int DeckSeedCardId = FollowerId; // the single known deck card (neutral 1/2 vanilla)
// M10 next milestone: the first DYNAMIC `{}`-VALUE card — proves the engine COMPUTES an effect
// magnitude from live game state (a value the wire can't carry; per memory
// project_battle_relay_nontargeted_effects this state-derived-value problem is exactly what
// broke the PvP relay, so proving the engine resolves it headless is the direct validation that
// the port — not a relay — is the necessary path). Still non-RNG: a seeded state makes the value
// deterministic. 112134010 is an ELF (clan 1) cost-2 SPELL whose sole skill is `when_play`
// `damage={me.play_count}-1` to `character=both&target=inplay&card_type=unit` (with a
// `base_card_id!=900111010|900111020` exclusion) — an AoE over BOTH boards' units, auto-targeted
// (no select_count, so selectedCards: null like M2-M5), ungated (skill_condition=character=me).
//
// The `{}` value resolves (SkillOptionValue.ParseInt) as
// `_filterVariable.Parse("me.play_count") - 1`, where Parse routes to
// SkillEnvironmentalPlayCount.Filtering -> playerInfo.GetCurrentTurnPlayCount() (the
// `isPrePlay=false` resolution path). That is the SAME per-turn counter the public
// AddCurrentTrunPlayCount feeds (M4 proved this seam drove the play_count>2 GATE; M10 proves it
// also feeds the `{}` VALUE). The per-play auto-increment AddCurrentTrunPlayCount(1) lives in
// ActionProcessor's OnBeforePlayCard (BattlePlayerBase.cs:1400), subscribed by
// SetupActionProcessorEvent — which is ONLY called on the OperateMgr/Prediction/OperationSimulator
// paths, NOT on the direct `new ActionProcessor(pair).PlayCard` (DP4) path this harness uses. So
// the headless play does NOT self-bump the per-turn count: the skill reads EXACTLY the seeded
// GetCurrentTurnPlayCount() and the damage == seeded - 1. The oracle derives the expected
// magnitude from the engine's OWN live GetCurrentTurnPlayCount(), not from a hardcoded literal,
// which is the M10 dimension (engine-computed value, not a wire-carried constant).
//
// The target is the M6 vanilla NEUTRAL 13/13 follower (SelectTargetFollowerId, already loaded):
// life 13 > any reasonable seeded count, so it SURVIVES for a clean life-delta read (reusing the
// M3/M6/M8 damage->life path), and `card_type=unit` excludes both leaders (asserted untouched).
public const int DynamicDamageSpellId = 112134010;
public const int DynamicDamageTargetFollowerId = SelectTargetFollowerId; // neutral 13/13 (survives, clean delta)
// A deliberately non-trivial seeded per-turn play count so the computed damage (== this value)
// is an obvious state read, not a coincidence with a small literal. The load-bearing probe
// (M4/M6/M8 discipline) varies this and watches the damage track it.
public const int DynamicSeededPlayCount = 4;
private static bool _done;
public static void EnsureInitialized()
@@ -136,7 +170,8 @@ namespace SVSim.BattleEngine.Tests
// real stats. The summoned token id must be present: Skill_summon_token resolves it
// through CardMaster.GetCardParameterFromId during creation.
HeadlessCardMaster.Load(FollowerId, SpellId, BuffFollowerId, TokenSpellId, SummonedTokenId,
TargetSpellId, SelectTargetFollowerId, UnselectTargetFollowerId, DestroySpellId, DrawSpellId);
TargetSpellId, SelectTargetFollowerId, UnselectTargetFollowerId, DestroySpellId, DrawSpellId,
DynamicDamageSpellId);
// Master reference data (class-character list) for leader/class card resolution.
HeadlessMasterData.Install();
// Player/enemy leaders (chara ids must map to a ClassCharacterMasterData in Master).