48 comments

[ 3.0 ms ] story [ 62.0 ms ] thread
Super cool
Yeah, no notes. It's remarkable.

TL;DR: Wired up an in-game predictor of RNG output and used it to only craft legendary items when RNG would line up to roll legendary.

From base to legendary at a suprisingly high rate. Look very closely at the video at the top of the post - what I was seeing didn't sink in until I had finished the article. Amazing.

I did an easier version of this in my college intro class. There was a class competition that involved rock paper scissors as a subcomponent, and ties were broken with randomness. You could rig Java’s prng so you would win all ties.

The prng was seeded with usec time at first call. I called the rng a bunch of times to harvest entropy, and scanned the plausible usec times to find the seed. Then I primed the prng so I would win ties.

Frankly, I assume I implemented this wrong, but the theory was there lol.

That’s not at all what the article proposed. The author constructed and transpose to the linear shift register coefficients and built a circuit network based on this to predict the next state (in the sandbox) and direct recipes.
I am failing to find the article, but some early online poker systems used the server time as the seed coupled with a weak PRNG. With the information of the hole cards + community cards, after a few hands, could quickly determine exactly what seed was being used and perfectly predict everyone's cards.
I don't think this was the original source, but this paper matches your (& my) recollection: https://gwern.net/doc/cs/cryptography/2006-arkin.pdf

google surfaces a couple of HN posts, but the source (cigital.com) seems to be a dead domain at this point:

https://news.ycombinator.com/item?id=288138

https://news.ycombinator.com/item?id=9914607

It's a bad shuffle implementation + using time of day as seed (reducing search space). Using the player's 2 cards and the 3 flop cards, it finds the RNG seed in real time, and then future hands (on the same server) are solved in "under one second!"

Indeed this is not the article I originally read either, but very similar breakdown of the problem.
Impressive work!
Stardew Valley has two random number seeds. One is the normal character seed. The other, your multiplayer ID, can be determined by analyzing the save file.

Except! On the Switch, you can't easily access the save file AND the random number generator is different than on PC. There is a seed cracker that looks at your traveling cart listing and calculates the character seed. Maybe because it's less important and harder to observe, I haven't found any tool to crack the other seed and don't have time to attempt writing it myself.

By inspecting cracked geode contents, you should be able to isolate your multiplayer ID and then predict random events on Nintendo just as PC players have done for the last decade with access to the save file.

The C# code for the game is online and the Switch RNG is known, so you never have to work in the dark. It's three steps: ensure your Switch RNG implementation works by testing against the normal seed, ensure your geode RNG implementation works by testing against the PC RNG, and then apply the Switch RNG to the geode function enough times that only one seed could create your observed sequence.

-----

Two semi-related open questions: Are you able to solve as quickly while starting at ANY geode as you'd be solving from the first geode? Does the RNG eventually repeat, so it actually doesn't matter what your multiplayer ID is as long as you observe a unique sequence, since there will only be one continuation of that sequence?

My favorite is the Doom random number generator, which is just a list of “random” numbers that it cycles through and if you know how to use it, you can do things like concentrate BFG attacks.
Reminds me of a "Dice" electronics kit I assembled when I was a kid. It just used some standard sequential counter ICs run at very high rate. Pressing the "roll" button would just stop the counter!
EverQuest also used pre-generated numbers. The randomness was derived from the large number of players using the same list.
That sounds like a pretty strong source of true randomness, frankly.

(It's then filtered through the list, so the list needs to have good pseudorandomness properties anyway, but still.)

> (It's then filtered through the list, so the list needs to have good pseudorandomness properties anyway, but still.)

Not sure? Suppose your list only had two number 0 and 1, and you build your random numbers one bit at a time.

Or more realistically, you have 256 numbers on the list 0, 1, 2, ..., 255 in order. If the 'large numbers of players drawing from the same list' assumption holds, it doesn't matter much that the list is in order.

What's just a bit weird is why anyone would want to turn an embarrassingly parallel problem into something with a sequential bottleneck?

> Or more realistically, you have 256 numbers on the list 0, 1, 2, ..., 255 in order. If the 'large numbers of players drawing from the same list' assumption holds, it doesn't matter much that the list is in order.

Why not? That should convert your random number generation into draws from a Poisson process. If you were looking to simulate a Poisson distribution, you're set. If not, you probably just ruined your RNG.

> It's going to make asking for random numbers more than 8 bits long challenging though.

'Yield' to other players' threads or processes after each byte you draw.

(comment deleted)
by far the blackest magic I ever saw for that game.

I'm an upper intermediate at Factorio, resorting to someone else's blueprints only for belt balancers and rail intersections, and I can't even begin to figure out how it's done.

> We chose taus88 mainly because it is the fastest from boost’s generators.

That's an RNG from 1996. It seems neither the recent C++ standards, nor boost, know anything about the much faster yet less predictable modern RNGs, that other programming languages have no trouble adopting in their standards

Really cool example of how something that looks random can become predictable once you understand the underlying system.
Dedication like this is awesome. Finally, we can optimize those starting resource patches without endless map restarts!
At the conclusion, he said he spent two years on this! That's a thesis.
If you're playing/hosting locally you get a preview of the world and can reroll for a different seed if you'd like.
As if Factorio wasn't addictive enough, now we can predict ore patches! My sleep schedule is already ruined.
Factorio players reverse-engineering an RNG? Peak Factorio. The dedication to optimize everything, even randomness, is truly something.
What’s the goal here with posting AI comments on every post? Does it help you get a job, do you sell this account?
It's probably just a test for their bot. We will see them everywhere.
it works! others can stop the test now
One of the reasons I dislike boost is in the first code sample. What’s the point of implementing what boils down to a 10 line function (as shown in the decompiled output) like this

    typedef xor_combine_engine<
  xor_combine_engine<
    linear_feedback_shift_engine<uint32_t, 32, 31, 13, 12>, 0,
    linear_feedback_shift_engine<uint32_t, 32, 29, 2, 4>, 0>, 0,
  linear_feedback_shift_engine<uint32_t, 32, 28, 3, 17>, 0> taus88;
C++ is uglier than punching one's dad.
This is called template-based compile-time programming.

By making 3 instances of linear_feedback_shift_engine class template ( and 2 of xor_combine_engine ), you are forcing the compiler to expand the code exactly as-is 3 times, each with different parameters.

The parameters to the template are constant, therefore the compiler can easily look at how they are used and you are guaranteed ( even in a 1999 c++ compiler ) that the compiler will look at the copies of the code and merge them as much as possible into a single piece of code... which is the one that you see when you decompile the code.

So in summary, it means that you get to write fairly readable code while the final binary is fully optimized as-if you had spent the time merging all the variants as needed for the specific constants.

You could get something similar from CPP macros. But the compiler wouldn't be able to help you as much.
My point is that the straight-line non-templated function presented later in the article is significantly more readable, it’s like 8 lines of code with a few shifts and xors.
I completely agree. Every time I remove boost from projects the build times drop dramatically and it usually doesn't take much work to swap the boost code for something else that isn't template fetish art.
It gives me hope, that out there in the internet, there are still people just doing purely geeky deep dives. I wish you good luck in life.
This is so awesome! Thank you so much for this extremely fascinating read/work! I love combining gaming and learning!!
The coolest part of this for me is that it's not only reverse engineering the RNG and being able to predict random rolls, which is relatively simple for that PRNG algorithm, but then being able to implement that algorithm in-game using combinator networks. There are already a lot of amazing things that you can do with combinators and this is a great example to add to the list.
Games, especially multiplayer games, should start using CSPRNGs and hash functions for random events. This has in particular been a problem for Minecraft where people can reverse-engineer coordinates from screenshots of natural terrain. Most games don't need so much randomness that performance is an issue.
I can only applaud the dedication to write such a long and precise article about such a niche topic
say goodbye to that debug symbol file.