test(rng-seam): M12 constants + NewAuthoritativeBattle harness factory
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Reflection;
|
||||
using SVSim.BattleEngine.Rng;
|
||||
using UnityEngine;
|
||||
using Wizard;
|
||||
using Wizard.Battle;
|
||||
@@ -150,6 +151,26 @@ namespace SVSim.BattleEngine.Tests
|
||||
// (M4/M6/M8 discipline) varies this and watches the damage track it.
|
||||
public const int DynamicSeededPlayCount = 4;
|
||||
|
||||
// M12 (the design §5 RNG oracle): reuse the M9 draw spell (800114010, when_play `draw` 1 from the
|
||||
// caster's deck via a random_count=1 filter) but over a MULTI-card deck with IsRandomDraw=true.
|
||||
// M9 passed only because IsRandomDraw=false takes BattlePlayerBase.LotteryRandomDrawCard's
|
||||
// top-of-deck `else` branch (BattlePlayerBase.cs:3174-3185) — a 1-card pool made index 0 the only
|
||||
// card. With IsRandomDraw=true the selection runs through SkillRandomSelectFilter.Filtering, which
|
||||
// calls BattleManagerBase.GetIns().StableRandom(poolCount) per pick (SkillRandomSelectFilter.cs:42,
|
||||
// gated on IsRandomDraw) — the chokepoint HeadlessBattleMgr overrides. So the scripted source picks
|
||||
// exactly which deck card is drawn, proving a GENUINE multi-outcome roll (the dimension M9's
|
||||
// one-card pool deliberately avoided).
|
||||
//
|
||||
// Three distinguishable deck cards seeded at consecutive indices; SkillRandomSelectFilter orders
|
||||
// the pool by Index (line 34), so the pick index maps to position in this order:
|
||||
// index 0 -> RngDeckCardA (100011010), index 1 -> RngDeckCardB (103111050), index 2 -> RngDeckCardC (100011020)
|
||||
// All three are already loaded by HeadlessCardMaster.Load via EnsureInitialized (FollowerId,
|
||||
// BuffFollowerId, SummonedTokenId), so no Load change is needed.
|
||||
public const int RngDrawSpellId = DrawSpellId; // 800114010, when_play draw 1 (random_count=1)
|
||||
public const int RngDeckCardA = FollowerId; // neutral 1/2 -> Index-order position 0
|
||||
public const int RngDeckCardB = BuffFollowerId; // ELF 1/1 -> Index-order position 1
|
||||
public const int RngDeckCardC = SummonedTokenId; // neutral 2/2 -> Index-order position 2
|
||||
|
||||
private static bool _done;
|
||||
|
||||
public static void EnsureInitialized()
|
||||
@@ -284,6 +305,30 @@ namespace SVSim.BattleEngine.Tests
|
||||
return card;
|
||||
}
|
||||
|
||||
// Build a headless battle wired for AUTHORITATIVE RNG: real rolls under IsForecast (via the
|
||||
// injected source on HeadlessBattleMgr) AND IsRandomDraw=true (the second gate — without it the
|
||||
// random-select filters bypass the roll and pick index 0; BattleManagerBase.cs:415,
|
||||
// SkillRandomSelectFilter.cs:42). Mirrors the opponent/turn/leader-life wiring every oracle does.
|
||||
// Returns the constructed HeadlessBattleMgr; the caller seeds hands/decks/boards and plays.
|
||||
public static HeadlessBattleMgr NewAuthoritativeBattle(IRandomSource rng)
|
||||
{
|
||||
EnsureInitialized(); // sets IsForecast = true among other globals
|
||||
BattleManagerBase.IsRandomDraw = true; // the second RNG gate (F-RNG-2)
|
||||
var mgr = new HeadlessBattleMgr(new HeadlessContentsCreator(), rng);
|
||||
mgr.IsRecovery = true; // collapse wait delays to 0 (F1)
|
||||
|
||||
var player = mgr.BattlePlayer;
|
||||
var enemy = mgr.BattleEnemy;
|
||||
SetField(player, "_opponentBattlePlayer", enemy);
|
||||
SetField(enemy, "_opponentBattlePlayer", player);
|
||||
player.IsSelfTurn = true;
|
||||
enemy.IsSelfTurn = false;
|
||||
|
||||
InitLeaderLife(mgr); // a 0-life leader reads as game-over and blocks plays
|
||||
InitCardTemplates(mgr); // the draw VFX touches the drawn card's view layer
|
||||
return mgr;
|
||||
}
|
||||
|
||||
private static void SetField(object obj, string name, object value)
|
||||
{
|
||||
var f = obj.GetType().GetField(name,
|
||||
|
||||
Reference in New Issue
Block a user