From 1a108fa393309bf71e4b04a1b2bcd0fd05364b5c Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sat, 6 Jun 2026 10:21:44 -0400 Subject: [PATCH] feat(rng-seam): ScriptedRandomSource (throw-on-overrun deterministic source) --- SVSim.BattleEngine.Tests/RngSeamTests.cs | 16 +++++++++ .../Rng/ScriptedRandomSource.cs | 35 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 SVSim.BattleEngine/Rng/ScriptedRandomSource.cs diff --git a/SVSim.BattleEngine.Tests/RngSeamTests.cs b/SVSim.BattleEngine.Tests/RngSeamTests.cs index 21f7edb..a3b3fe7 100644 --- a/SVSim.BattleEngine.Tests/RngSeamTests.cs +++ b/SVSim.BattleEngine.Tests/RngSeamTests.cs @@ -35,5 +35,21 @@ namespace SVSim.BattleEngine.Tests for (int i = 0; i < 8; i++) Assert.That(src.NextSelf(100), Is.EqualTo(refSelf.Next(100)), $"NextSelf drift at {i}"); } + + // ScriptedRandomSource feeds a known sequence (the oracle's control + the Phase-3 replay seam). + // It MUST throw on overrun, not wrap: an unexpected extra roll should fail loudly so a test + // surfaces a miscount of engine RNG calls rather than silently reusing a value. + [Test] + public void ScriptedSource_returns_sequence_then_throws_on_overrun() + { + var src = new ScriptedRandomSource(new[] { 0.1, 0.5 }, new[] { 3 }); + + Assert.That(src.NextUnit(), Is.EqualTo(0.1)); + Assert.That(src.NextUnit(), Is.EqualTo(0.5)); + Assert.That(() => src.NextUnit(), Throws.InvalidOperationException, "should throw on unit overrun"); + + Assert.That(src.NextSelf(99), Is.EqualTo(3)); + Assert.That(() => src.NextSelf(99), Throws.InvalidOperationException, "should throw on self overrun"); + } } } diff --git a/SVSim.BattleEngine/Rng/ScriptedRandomSource.cs b/SVSim.BattleEngine/Rng/ScriptedRandomSource.cs new file mode 100644 index 0000000..3531af5 --- /dev/null +++ b/SVSim.BattleEngine/Rng/ScriptedRandomSource.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SVSim.BattleEngine.Rng +{ + // Deterministic source feeding a pre-scripted sequence. Used by oracles to control which outcome a + // roll selects, and the precursor to the Phase-3 capture-replay source (feed a captured rand list). + // Throws on overrun so an unexpected extra engine roll fails loudly. + public sealed class ScriptedRandomSource : IRandomSource + { + private readonly Queue _units; + private readonly Queue _selfPicks; + + public ScriptedRandomSource(IEnumerable units, IEnumerable selfPicks = null) + { + _units = new Queue(units ?? Enumerable.Empty()); + _selfPicks = new Queue(selfPicks ?? Enumerable.Empty()); + } + + public double NextUnit() + { + if (_units.Count == 0) + throw new InvalidOperationException("ScriptedRandomSource: NextUnit overrun (more synced rolls than scripted)"); + return _units.Dequeue(); + } + + public int NextSelf(int max) + { + if (_selfPicks.Count == 0) + throw new InvalidOperationException("ScriptedRandomSource: NextSelf overrun (more self rolls than scripted)"); + return _selfPicks.Dequeue(); + } + } +}