Balatro’s Least Erratic Decks
My most recent “just for fun” project was to find the least erratic of the Erratic decks in Balatro (a poker-ish roguelike deckbuilder game).
Before each game, you pick a deck that has a special power. The Erratic deck completely randomizes the ranks/suits of the cards (such that you could end up with 10 aces, or 25 spades, etc).
I was curious: what’s the least erratic deck (i.e., the most cards of the same rank or suit) you can possibly get?
Process
Balatro isn’t technically open source BUT you can simply unzip the executable to get the source code. My plan was to extract the pseudorandom number generator (PRNG) from that source code and run it through every possible seed in the game (taking note of any particularly un-erratic decks).
Easier said than done! Balatro rolled its own PRNG algorithm on top of LÖVE’s, so I had to do a bit of hacking to extract what I needed to generate Erratic decks. But the larger problem is that Lua, being an interpreted language, is just too dang slow (even with LÖVE’s JIT).
There are 2.3 trillion possible seeds in Balatro. My initial implementation could crunch ~95k seeds/minute. At that rate, it would take 46 years to finish checking every seed. Ouch! I was able to optimize my Lua to get it to ~7.5 million seeds/minute, but even then, that’d take 214 days to finish.
I decided I’d try porting my code to Rust, both because I knew a compiled language would be much faster, and because I’d always wanted to try Rust. Between Rust + taking advantage of all CPU cores, I was able to get up to ~100 million seeds/minute. At that rate, it would take only about two weeks of crunching numbers, so I started the program and waited patiently.
(I assume there’s some way to use a GPU to crunch these numbers way faster, but I know absolutely nothing about GPU programming and didn't want to wade into that world.)
Bugged Seeds
Before I get to the results, we must discuss bugged seeds.
There’s an infamous bug in Erratic deck generation wherein some seeds generate a deck that is filled with the ten of spades.
Due to a bug in the PRNG, some seeds generate "not a number" (nan) by accident. When you try to grab a random card from the deck using nan, it instead grabs the last entry in the “possible cards” array. And it turns out the last card in the array is the ten of spades (because the array is sorted by suit then rank, and ST comes out last). Every time Balatro tries to generate a new random number based on nan it just generates nan again, so the entire deck ends up filled with the ten of spades.
Obviously, these decks are the least Erratic decks you can get, but I find them less interesting because they’re due to a programming bug.
Results
Here’s the most interesting seeds I found:
I3EFEF17 has the highest number of the same rank w/ 25 tens, followed by RGJO14OZ w/ 24 fours.
8778L6US has the highest number of the same suit w/ 39 hearts.
77XX2TEK has the best combination of same suits & ranks, with 20 fours and 38 spades.
For posterity, here’s a list of all 35 bugged seeds (nothing but tens of spades).
The full results can be found here (the cutoff for a deck to be considered “good” is 20 of the same rank or 34 of the same suit).
Besides the bugged seeds, I was surprised to find that, generally speaking, there’s only so powerful a deck you can generate with an Erratic deck.
If I were to do it again, I’d look at distribution (not just highest count). E.g., it’d be interesting if a seed generated a deck w/ only a few ranks represented in it.
Details
Some nitty gritty details for those interested…
Source code is here. It’s not good code (my first time using Rust), but it got the job done!
Balatro uses LÖVE-specific PRNG functions. Balatro was built on LÖVE, which has a different PRNG implementation than Lua. It’s confusing because the function name is the same (math.randomseed()), but the one in Lua only allows integers whereas LÖVE’s allows for float (which Balatro requires).
Valid seeds are defined by the regex [A-Z1-9]{1,8}. In plain English: it’s anything from 1-8 characters in length, with letters and numbers (but NOT the number zero; you can enter a zero in the game, but it’s automatically replaced by the character ‘O’ instead, presumably to avoid confusion between 0/O.)
The number of seeds is the sum of 35^n for n=1..8 which is 2,318,107,019,760. (35 because that's how many unique letters/numbers you can use, and 1-8 because that's how long the seeds can be.)