feat(battle-engine-port): M9 COMPLETE — when_play draw resolves headless (hand/deck-delta oracle)
Proves the deck->hand transfer dimension (design §5 draw oracle) — the last deterministic, non-RNG card-effect class no prior milestone touched (M3/M4/M6/M8 moved stats, M2/M5/M7 the board, M3 the leader). Card 800114010 (clan-1 ELF cost-1 when_play draw 1 from own deck, ungated, no evo/preprocess). The resume-guide's skill_target=none/no-RNG shape does not exist in cards.json — EVERY draw selects from the deck via a random_count filter (skill_option is always literally 'none'). RNG neutralized structurally: seed the deck with EXACTLY ONE known card so random_count=1 is deterministic regardless of seed. New primitive HeadlessEngineEnv.SeedDeck (create via the null-view seam + engine AddToDeck). Oracle DrawSpellOracleTests asserts: seeded card moves deck->hand (by id + by reference), deck -1, drawn card IsInHand, spell pays cost + leaves hand + resolves to cemetery, board/opponent untouched. Load-bearing confirmed the M7 way (seed a different id -> the by-id assertion fails). Shim gap fixed (the predicted M9 cost): Skill_draw's BattleLog tail (UpdateFusionedCardSkillDrewCard, unguarded; + the IsBattleLog AddLogSkillDrawCard calls) dereferences BattleLogManager.GetInstance(), an M1 'default!' null singleton -> NRE after the draw already committed. One-line HEADLESS-FIX (M9) in BattleLogManager.g.cs returns the existing _instance singleton (all its methods are no-ops), per the M2/M7 Null*-singleton playbook. No Engine/ edit (drift clean). 9/9 green; check_drift.py clean; engine still 0 Error(s). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,24 @@ namespace SVSim.BattleEngine.Tests
|
||||
public const int LethalTargetFollowerId = FollowerId; // neutral 1/2 (life 2 <= 5 -> dies)
|
||||
public const int SurvivorTargetFollowerId = UnselectTargetFollowerId; // neutral 6/7 (life 7 > 5 -> survives at 2)
|
||||
|
||||
// M9 next milestone: when_play DRAW — proves the HAND/DECK DELTA dimension (design §5's draw
|
||||
// oracle): the last deterministic, non-RNG card-effect class no prior milestone touched (M3/M4/
|
||||
// M6/M8 moved stats, M2/M5/M7 the board, M3 the leader — none read the deck->hand transfer).
|
||||
// 800114010 is an ELF (clan 1) cost-1 SPELL whose sole skill is `when_play` `draw` of ONE card
|
||||
// from the caster's own deck (skill_target=character=me&target=deck&card_type=all&random_count=1),
|
||||
// ungated (skill_condition=character=me), no evo skill, no preprocess, no dynamic `{}` value.
|
||||
//
|
||||
// ADAPTATION FROM THE RESUME-GUIDE SHAPE: the guide asked for a `skill_target=none` draw with
|
||||
// "no RNG", but no such card exists in cards.json — EVERY draw selects from the deck via a
|
||||
// `random_count=N` target filter (skill_option is always literally `none`; the count lives in
|
||||
// skill_target). The RNG is neutralized structurally instead: seed the deck with EXACTLY ONE
|
||||
// known card, so `random_count=1` over a single-card pool is deterministic regardless of the
|
||||
// RandomSeed. This keeps the oracle decisive (drawn id is forced) while exercising the real
|
||||
// draw path. Like the summon token, a drawn card is engine-CREATED off the deck the M5 prefab
|
||||
// way; unlike summon, the card already exists (we seed it) and the skill only MOVES it deck->hand.
|
||||
public const int DrawSpellId = 800114010;
|
||||
public const int DeckSeedCardId = FollowerId; // the single known deck card (neutral 1/2 vanilla)
|
||||
|
||||
private static bool _done;
|
||||
|
||||
public static void EnsureInitialized()
|
||||
@@ -118,7 +136,7 @@ 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);
|
||||
TargetSpellId, SelectTargetFollowerId, UnselectTargetFollowerId, DestroySpellId, DrawSpellId);
|
||||
// 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).
|
||||
@@ -214,6 +232,23 @@ namespace SVSim.BattleEngine.Tests
|
||||
return card;
|
||||
}
|
||||
|
||||
// Push a known card onto a player's DECK headless (the M9 draw oracle's setup primitive). The
|
||||
// bare `new SingleBattleMgr(...)` construction leaves DeckCardList non-null-but-empty (ctor at
|
||||
// BattlePlayerBase.cs:1050), and a card's deck membership IS its `IsInDeck` (BattleCardBase.cs:970
|
||||
// `=> SelfBattlePlayer.DeckCardList.Contains(this)`) — so no separate "in deck" flag is needed.
|
||||
// Create the card through the same null-view seam hand/board cards use, then drive the engine's
|
||||
// own AddToDeck (BattlePlayerBase.cs:3038): for a vanilla follower it is just DeckCardList.Add
|
||||
// (HasDeckSelfSkill is false; the XorShiftRandom/IsMulliganEnd reshuffle bookkeeping short-
|
||||
// circuits on the null/inactive headless RNG). The drawn card is then the engine's own deck
|
||||
// object, so the oracle can assert deck->hand identity by reference, not just by id.
|
||||
public static BattleCardBase SeedDeck(BattleManagerBase mgr, int cardId, int index, bool isPlayer)
|
||||
{
|
||||
var card = CreateHeadlessHandCard(cardId, index, isPlayer, mgr);
|
||||
BattlePlayerBase owner = isPlayer ? (BattlePlayerBase)mgr.BattlePlayer : mgr.BattleEnemy;
|
||||
owner.AddToDeck(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
private static void SetField(object obj, string name, object value)
|
||||
{
|
||||
var f = obj.GetType().GetField(name,
|
||||
|
||||
Reference in New Issue
Block a user