Seeds and Determinism
In short
A run has one seed. Each system that needs randomness gets its own
SeededRandomstream, made withSeededRandom.Derive(seed, streamId). Because streams never share an RNG, a shop reroll can never shift the pitches. Never renumber a stream id: it would change every seed’s run.
How it works
SeededRandom
- Wraps
System.Random(seed). Methods:Value(0..1),Range(float, float),Range(int, int),NextSeed(),Pick(list),PickWeighted(list, weight). Derive(seed, stream)mixes two ints into a new seed with integer math only (identical on every platform):
x = (uint)seed ^ ((uint)stream * 0x9E3779B9)
x ^= x >> 16; x *= 0x7FEB352D; x ^= x >> 15; x *= 0x846CA68B; x ^= x >> 16
return x & 0x7FFFFFFF // non-negative for System.Random
The streams in a run
| What | Seed it uses | Where |
|---|---|---|
| The innings (pitcher per inning) | the raw run seed: new SeededRandom(seed) | RunState constructor, RunMapGenerator.Generate |
| One inning’s pitches | RoundSeed(node) = Derive(Derive(Seed, PitchStream = 1), node.index) | RunState.RoundSeed, passed in RoundContext.seed |
| Pitch type, location, speed | new SeededRandom(context.seed) | AtBatController builds PitcherAI |
| Special balls ($$$, Ghost…) | Derive(rng.Seed, BallStream = 0x0BA11) | PitcherAI constructor. Each pitch rolls the pitcher’s own ball first (PitcherDefinition.balls), then the park’s; a ball with an alternate (The Gale’s Headwind / Tailwind) draws once more for the 50/50. |
| The Phantom’s decoy balls | Derive(rng.Seed, DecoyStream = 0xDEC0) | PitcherAI.MakeDecoys |
| Which way a curveball / changeup loops | Derive(rng.Seed, ArcStream = 0xA2C) | PitcherAI.CreatePitch (arcAngle, only for arcSideways pitch types) |
| A hit’s spray spread and launch odds | new System.Random(context.seed ^ 0x6b1d), one per round | AtBatController.BeginRound (_hitRandom), passed to SwingResolver.FromContact (Batting Explained) |
| One shop visit’s offers and its rerolls | Derive(Derive(Seed, ShopStream = 2), visit) | RunState.CreateShopRng; ShopService.Open passes run.CurrentIndex as the visit |
| The daily map | Derive(DailySeed.Today, 0xDA11) % ballparks.Count | RunManager.DailyPark |
Why shop rerolls can’t change the pitches
The pitch seed depends only on the run seed and the inning index. Buying, rerolling or the order of purchases draw only from the shop visit’s own RNG. The special-ball rolls, the decoys and the sideways loops also have their own streams, so none of them changes which pitch type comes next. The one exception is deliberate: where a special ball only rides some pitch types, PitcherAI leans on those types (FavouredShare = 0.8 of picks, capped at 3 in a row) using the main pitch RNG, so that lean is part of the pitch sequence itself (Pitching). Dev-menu forced pitches keep rolling as normal, so turning them off leaves the rest of the sequence unchanged.
Each shop visit starts a fresh RNG, so rerolls in one shop don’t change the first offers of the next.
Resuming a run
run.json stores only the seed, the player’s choices and totals (Save System). The innings, their pitches and the shop offers are rebuilt from the seed, so quitting in the shop resumes with the same offers, and quitting mid-inning replays that inning with the same pitches.
Daily seed
DailySeed.Today = int.Parse(UtcNow "yyyyMMdd"), so everyone on the same UTC date gets the same seed.DailySeed.DateKeyis"yyyy-MM-dd".- The daily run is the seed’s map on Normal (
StartDailyRun). It unlocks nothing. - A daily checkpoint can only be resumed on its own date (
RunSaveData.dailyDate).
Co-op
- The host picks a random seed with
Random.Range(1, int.MaxValue)and sends it inStartRunwith the batting order (Net Messages). Every player builds the same innings from it. TurnSeed(node, turn): turn 0 usesRoundSeed(node); later turns useDerive(RoundSeed(node), turn). So each turn of an inning faces its own pitch sequence (Co-op Rules).ShopSalt = LocalId + 1on each player’sRunState. When it is not 0, the shop seed is salted again:Derive(shopSeed, ShopSalt). Each player sees their own offers; solo (ShopSalt = 0) is unchanged.
Numbers
| What | Value | Where to tune |
|---|---|---|
PitchStream | 1 | RunState (never renumber) |
ShopStream | 2 | RunState (never renumber) |
BallStream | 0x0BA11 | PitcherAI |
DailyParkStream | 0xDA11 | RunManager |
DecoyStream | 0xDEC0 | PitcherAI |
ArcStream | 0xA2C | PitcherAI |
| Hit random salt | 0x6b1d (XOR with the round seed, not Derive) | AtBatController |
| Solo run seed | UnityEngine.Random.Range(1, int.MaxValue) | RunManager.StartRandomRun |
| Batting cage seed | UnityEngine.Random.Range(1, int.MaxValue) (not reproducible on purpose) | BattingCage |
In the code
| File | What it does |
|---|---|
Scripts/Core/SeededRandom.cs | The RNG and Derive. |
Scripts/Run/RunState.cs | Stream ids, RoundSeed, TurnSeed, CreateShopRng, ShopSalt. |
Scripts/Run/RunMapGenerator.cs | Uses the raw seed to pick pitchers and bosses. |
Scripts/Pitching/PitcherAI.cs | Pitch RNG plus the ball-effect, decoy and arc streams. |
Scripts/Batting/AtBatController.cs | The per-round hit random. |
Scripts/Upgrades/ShopService.cs | Draws offers and rerolls from the visit’s RNG. |
Scripts/Progression/DailySeed.cs | Today, DateKey. |
Gotchas
Same seed is not always the same shop
The shop draws from
MetaProgression.UpgradePool(), the upgrades this player has unlocked. Two players on the same daily seed see the same innings and pitches, but different shop offers if their unlocks differ.
- Cosmetic randomness (scoreboard cracks, crowd waves, hat tumbles, blinks, the cutscenes’ handheld camera drift and fireworks) uses
UnityEngine.Randomon purpose: it never touches gameplay. - The dev tools are not seeded: the insta-home-run spray and TEST HITS’ Random pick use
UnityEngine.Random(Dev Menu and Shortcuts). - Need randomness in a new gameplay system? Add a new stream id; don’t borrow another system’s RNG.
- Changing
RunMapGenerator,PitcherAIorShopServicedraw order changes what every existing seed produces, including saved runs and today’s daily. Between e7dba51 and 45a2167 this happened on purpose:RunMapGeneratornow keeps every park’s own final boss out of the random boss pool (innings 3 and 6), andPitcherAIleans on special-ball pitches, so a seed from before gives different bosses and pitches now. Saved runs from before still load (RunSaveDataversion is unchanged) but replay differently.
Related
- Daily Seed and Leaderboard: the player side of the daily
- Pitching: what the pitch stream picks
- Shop and Economy: offers and rerolls
- Save System: the run checkpoint
- Co-op Rules: turns and the shared seed