I read a cool article on Slay the Spire 2’s random number generator.

The game has multiple generators in order to isolate aspects of the game otherwise you get RNG manipulation like step counting in Stardew Valley. So their system looks like

\[ \begin{aligned} Y^{1} &\sim \operatorname{RNG}(\text{seed} + \operatorname{hash}(\text{“purpose1”})) \\ Y^{2} &\sim \operatorname{RNG}(\text{seed} + \operatorname{hash}(\text{“purpose2”})) \\ Y^{3} &\sim \operatorname{RNG}(\text{seed} + \operatorname{hash}(\text{“purpose3”})) \end{aligned} \]

All well and good except for the RNG function that they use is roughly linear with the following recurrence relationship:

\[ Y_{n} = (Y_{n-24} - Y_{n-55}) \bmod M \]

and the initialization which is a little complicated but is linear in the seed. This introduces correlations across draws because the initial seeds are linearly related to each other (hash("purpose2") - hash("purpose1")) and with linear operations the resulting outputs then remain linearly related!

The blogger could figure this out both through large scale simulations and also, you know, looking into the code after the fact. What I found the most interesting is how narrow this bug was. Like if they used a different generator this wouldn’t have happened. If they seeded the RNG like RNG(hash(seed + "purpose")) it wouldn’t happen (though you would still have “local” correlation). Just fascinating!