Correlated randomness in Slay the Spire 2 - Andy Tockman
portfolio
music
conlangs
video games
board games
puzzles
square dancing
food
Correlated randomness in Slay the Spire 2<br>2026-06-13<br>very long (6880 words) gamesvideo gamesslay the spiremath
Here are three true statements about the game of Slay the Spire 2<br>(in single player):
If you pick Neow's Bones in the Underdocks,<br>the random curse is ~54% likely to be Debt.*
It is impossible to receive Rebound from the Trash Heap event.
Your first fight is 76% likely to drop a potion in Underdocks,<br>and 4% likely to drop a potion in Overgrowth.**
(* assuming neither of the relics from Neow's Bones is New Leaf or Kaleidoscope)
(** assuming your Neow relic doesn't give cards or other relics)
(*** all on the current beta patch, v0.107.0)
What?!
Why? The culprit is unexpected correlation between different random number generators --<br>knowing the first output of one of the game's RNGs<br>gives information that helps predict the first output of all of the others.
The random number generators of Slay the Spire 2
For now,<br>I will give an extremely simplified explanation of this correlation.<br>If you want more details,<br>I will go into much greater depth at the end of this post.<br>If you don't care,<br>you can skip this section to see all the funny examples below.
The phenomenon of "correlated RNG" (or "CRNG")<br>is already known in the Slay the Spire community,<br>because Slay the Spire 1 had a similar issue,<br>described in detail in Forgotten Arbiter's blog post.[1]
Briefly,<br>in Spire 1,<br>the game used several distinct pseudorandom number generators,<br>to prevent e.g. randomness within a combat<br>from influencing future card rewards.<br>However,<br>they were all initialized to the same starting state,<br>which meant they produced the same sequence of numbers.<br>A crafty player could therefore pay attention to the results of past random events<br>and gain information about future random events.
In an attempt to avoid the same problem,<br>Spire 2 initializes its pseudorandom number generators<br>to different states.<br>The code looks something like this<br>(highly simplified for didactic purposes):
Rng UpFront = new Rng(seed + hash("up_front"));<br>Rng Shuffle = new Rng(seed + hash("shuffle"));<br>Rng UnknownMapPoint = new Rng(seed + hash("unknown_map_point"));<br>Rng CombatCardGeneration = new Rng(seed + hash("combat_card_generation"));<br>Rng CombatPotionGeneration = new Rng(seed + hash("combat_potion_generation"));<br>Rng CombatCardSelection = new Rng(seed + hash("combat_card_selection"));<br>Rng CombatEnergyCosts = new Rng(seed + hash("combat_energy_costs"));<br>Rng CombatTargets = new Rng(seed + hash("combat_targets"));<br>Rng MonsterAi = new Rng(seed + hash("monster_ai"));<br>Rng Niche = new Rng(seed + hash("niche"));<br>Rng CombatOrbGeneration = new Rng(seed + hash("combat_orbs"));<br>Rng TreasureRoomRelics = new Rng(seed + hash("treasure_room_relics"));<br>// ...
There are many more random number generators in the game<br>that I have not listed for brevity;<br>notably,<br>every event has its own RNG.
The hash function essentially produces a "random-looking" number<br>from the input string,<br>but the number is always the same for the same input.<br>So the idea is that the RNG states are shuffled around,<br>but the same seed still always results in the same run.
The problem comes when these seeds are passed<br>to the stock System.Random class in C#.<br>Unfortunately,<br>the pseudorandom number generation algorithm<br>used in C#<br>is almost entirely "linear" in the starting seed.
What this means exactly is a bit complicated --<br>again, I will go into greater detail later in this post.<br>But the consequence is that two RNGs<br>whose seeds differ by a known fixed amount<br>have their outputs differ by a fuzzier but still-exploitable amount.
How exploitable,<br>you might ask?<br>Well...
Here is a big pile of consequences of CRNG,<br>ranging from amusing-but-unimportant<br>to legitimately impactful on gameplay<br>(some of them even to casual players unaware of it!).
Neow's Bones
I'll start with the first example from the intro.<br>If you pick Neow's Bones in Underdocks,<br>the "random" curse you receive<br>actually has the following approximate distribution:
Clumsy<br>0.10%
Debt<br>54.25%
Decay<br>40.32%
Doubt<br>1.50%
Guilty<br>0%
Injury<br>0%
Normality<br>0%
Regret<br>0%
Shame<br>0%
Writhe<br>3.82%
However,<br>in Overgrowth,<br>you instead get a curse from this distribution:
Clumsy<br>0.51%
Debt<br>0%
Decay<br>0%
Doubt<br>0%
Guilty<br>0.19%
Injury<br>5.53%
Normality<br>1.18%
Regret<br>0%
Shame<br>18.85%
Writhe<br>73.74%
This one is quite funny to me --<br>people all over Reddit and Discord have been lamenting their terrible luck<br>that they keep rolling Debt from Neow's Bones.[2]<br>Even before discovering CRNG,<br>I saw some of these posts insisting it seemed more frequent than random.<br>It is hard to express how instantaneously my brain automatically dismissed them<br>as textbook confirmation bias.<br>And yet...
To understand this one,<br>we need to correlate three sources of randomness:
The "curse relic" available from Neow<br>comes from a call to Neow's...