test(rng-seam): M12 constants + NewAuthoritativeBattle harness factory

This commit is contained in:
gamer147
2026-06-06 10:40:59 -04:00
parent f6e3b67be1
commit 2fd0aac5b6

View File

@@ -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,