diff --git a/SVSim.BattleNode/Lifecycle/BattleSeeds.cs b/SVSim.BattleNode/Lifecycle/BattleSeeds.cs
new file mode 100644
index 0000000..0d67b0c
--- /dev/null
+++ b/SVSim.BattleNode/Lifecycle/BattleSeeds.cs
@@ -0,0 +1,41 @@
+namespace SVSim.BattleNode.Lifecycle;
+
+///
+/// Deterministic per-battle seed derivation. Given one random master seed (chosen once per battle
+/// on ), derives every RNG value the node hands
+/// the clients: the shared effect seed (Matched.seed), each side's deck-shuffle RNG seed, and each
+/// side's Ready.idxChangeSeed.
+///
+/// IMPORTANT: uses a fixed splitmix64-style bit-mix, NOT System.HashCode / string.GetHashCode
+/// (those are randomized per process). Stability across process runs is what makes "same master
+/// seed reproduces the same battle" — the foundation of replay — actually hold.
+///
+internal static class BattleSeeds
+{
+ /// Shared effect-RNG seed; identical for both sides (it seeds the synced stream).
+ public static int Stable(int master) => Derive(master, "stable");
+
+ /// Per-side Ready.idxChangeSeed (client XorShift for mid-battle card-into-deck).
+ public static int IdxChange(int master, long viewerId) => Derive(master, "idx", viewerId);
+
+ /// Per-side deck-shuffle RNG seed (node-side Fisher–Yates).
+ public static int DeckShuffle(int master, long viewerId) => Derive(master, "deck", viewerId);
+
+ /// Derive a stable non-negative int from (master, tag, discriminator). Pure arithmetic
+ /// — reproducible across process runs and platforms.
+ public static int Derive(int master, string tag, long disc = 0)
+ {
+ ulong h = Mix((uint)master);
+ foreach (char c in tag) h = Mix(h ^ c);
+ h = Mix(h ^ (ulong)disc);
+ return (int)(h & 0x7FFFFFFFUL);
+ }
+
+ private static ulong Mix(ulong x)
+ {
+ x += 0x9E3779B97F4A7C15UL;
+ x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9UL;
+ x = (x ^ (x >> 27)) * 0x94D049BB133111EBUL;
+ return x ^ (x >> 31);
+ }
+}
diff --git a/SVSim.UnitTests/BattleNode/Lifecycle/BattleSeedsTests.cs b/SVSim.UnitTests/BattleNode/Lifecycle/BattleSeedsTests.cs
new file mode 100644
index 0000000..7554240
--- /dev/null
+++ b/SVSim.UnitTests/BattleNode/Lifecycle/BattleSeedsTests.cs
@@ -0,0 +1,46 @@
+using NUnit.Framework;
+using SVSim.BattleNode.Lifecycle;
+
+namespace SVSim.UnitTests.BattleNode.Lifecycle;
+
+[TestFixture]
+public class BattleSeedsTests
+{
+ // Golden values pin cross-run/cross-platform stability. They were computed from the exact
+ // splitmix64 mix specified in BattleSeeds. If these ever change, replay reproducibility broke —
+ // do NOT "update them to match"; find what changed the algorithm (e.g. someone slipped in
+ // GetHashCode, which is per-process randomized).
+ [Test]
+ public void Derive_golden_values_are_stable()
+ {
+ Assert.That(BattleSeeds.Stable(12345), Is.EqualTo(1577307848));
+ Assert.That(BattleSeeds.IdxChange(12345, 906243102), Is.EqualTo(1638231407));
+ Assert.That(BattleSeeds.DeckShuffle(12345, 906243102), Is.EqualTo(355953180));
+ Assert.That(BattleSeeds.IdxChange(12345, 847666884), Is.EqualTo(518125159));
+ Assert.That(BattleSeeds.Stable(99999), Is.EqualTo(323349150));
+ }
+
+ [Test]
+ public void Derive_is_deterministic_for_same_inputs()
+ {
+ Assert.That(BattleSeeds.Derive(7, "x", 42), Is.EqualTo(BattleSeeds.Derive(7, "x", 42)));
+ }
+
+ [Test]
+ public void Derive_differs_across_tag_master_and_discriminator()
+ {
+ var baseline = BattleSeeds.Derive(7, "x", 42);
+ Assert.That(BattleSeeds.Derive(8, "x", 42), Is.Not.EqualTo(baseline), "different master");
+ Assert.That(BattleSeeds.Derive(7, "y", 42), Is.Not.EqualTo(baseline), "different tag");
+ Assert.That(BattleSeeds.Derive(7, "x", 43), Is.Not.EqualTo(baseline), "different disc");
+ }
+
+ [Test]
+ public void Derive_is_always_non_negative()
+ {
+ // System.Random tolerates any int, but a non-negative seed keeps parity with prod's
+ // positive seed values and avoids surprises.
+ Assert.That(BattleSeeds.Stable(int.MinValue), Is.GreaterThanOrEqualTo(0));
+ Assert.That(BattleSeeds.Stable(-1), Is.GreaterThanOrEqualTo(0));
+ }
+}