#Can we crack Balatro's Seed Generation? [WIP]

32 messages · Page 1 of 1 (latest)

royal rune
#

Hello! not sure if this is necessairly the correct place for this, but i thought i'd share my progress and my general idea for this project.

What's the goal?

Finding a way to crack the seed generation so we can accurately predict / generate ourselves the next seed.
Which in this case, seems to be using a screenshot of the game, and a precise timestamp of when the screenshot was taken, compared to when the game started, to be able to generate the exact seed, without needing to inject or modify the actual game's code.

The Theory / Why crack this way?

It's simply because the seed for the game is calculated using these 3 values, which we can calculate from a screenshot:

  • G.CONTROLLER.cursor_hover.T.x, the X value of the "cursor hover"
  • G.CONTROLLER.cursor_hover.T.Y the Y value of the "cursor hover"
  • G.CONTROLLER.cursor_hover.time- the amount of time in seconds of the "cursor hover"

I'm using 1.01f version of balatro recently, so maybe other versions use a different formula, but for the most part it should be the same.

You can see these values as the paramaters in misc.functions.lua in the generate_starting_seed method. There's some if statement of if the stake is above 8 then it adds some extra number? but i couldn't quite understand how that works yet, and for 99% of the time it never executes anyway.

Now, the X & Y values are not actually the screen coordinates 1:1, but are scaled according to your screen resolution, and some other constants, which you can see in controller.lua in the set_cursor_hover method, which sets the "cursor hover" X & Y's as the same of the cursor coordinates, which are scaled from the screen position to the local position in set_cursor_positionmethod.
NOTE: G.TILESCALE in the code shows up to start with some value, but that value does change whenever you resize it, as can be seen in main.lua on the love.resize function.

#

Since we know how it makes it into local coordinates, we can simply reverse the order to get those local positions from a screen position.

There's now only one thing missing: Time. How is that calculated?

Looking at controller.lua again we can see it's based on G.TIMERS.TOTAL, which after testing seems to start after love.run(), love.load(),and the main loop starting only after game.startup().

Which is probably impossible to track, especially with how accurate we need to be, since the seed is so sensitive to any changes, we need to be accurate up to how many digits Lua after the decimal point tracks, which is 15, as of the IEEE 754 Double Precision Standard.
which ultimately mark the end for this whole idea.

But there's actually two really convenient things about this timer which could potentially make us able to track it, to when the player starts the run.

  • When you skip the cutscene of the splash screen, the timer gets set to 12 exactly.
  • When you press the play button in the main menu (the button before you actually start the run), the timer freezes or at least the value freezes for as long as you're in that menu.

With these things in mind, there could be a way to track it, which is what i need help with.

Since I already have the coordinates translation working fine, as it's just a lot of multiplying & dividing already existing constants, the missing piece is tracking the time.

I think C++ is probably the way to go for this, but i just don't have much experience it, so i'd love some help with the project or generally any other ideas you have to how we could track it accurately.

My going ideas currently are:

  • Tracking the time accurately between the first left click that skips the cutscene and sets it to 12, to the click that opens the menu.
  • Tracking when the game / proccess starts and again to the 2nd left click of opening the menu, arguably probably less possible.

If you read this far, thanks and i'd love your help!

nova portal
#

Would [roughly] tracking the position, then using the RNG of the hand draw/first shop/tags allow us to reverse the seed ?

#

More on that: We only have 36**8 possible seeds, that sure is a lot of information, but we do have quite a few bits of state from the very first draws of the rng

#

I dunno how the hand drawing is done and how tied it is to the main seed, but assuming perfect usage of information, that's like 30 bits just from drawing 8 cards .... what do you think @royal rune ?

#

for comparison, we got roughly 42 bits of seed

royal rune
#

yeah i haven't actually looked at how hands are drawn and all but that could be a way to do it

#

I'm not really that knowledgable about reverse-engineering seeds & like randomizers and all that, but probably, if each card gets picked with a specific number, and not like a range (0.25-0.5) you could probably track what randomizer outputs give what cards

nova portal
#

Yeah randomizers can be tricky to reverse depending on their quality and the amount of information lost

#

it's a path one could try to follow

royal rune
#

also, position can be tracked perfectly, given the resolution and mouse coordinates on the screen

#

although there's one weird edge case where for some reason, the resolution in the game doesn't coorelate to the actual resolution it sets for itself inside, like on my laptop screen which is 1080p for some reason it resizes to like 600x1500

#

and with my screenshots being in 1080p it makes the coordinates not link up properly, but again, it's an edge case that you could probably go around if it does happen

nova portal
#

I'll take a peek at what kind of RNG is used in the game and if it's easy to reverse state (and so, the seed) only by being given partial information thumbs_uppawft1

#

but uh, for now, work time.

royal rune
#

yeah, i mean it uses PRNG, so it's not something unique that isn't present everywhere

nova portal
#

(sure, but PRNGs range from "flawed and easily reversable even with partial information" to "impossible to reverse without perfect information")

royal rune
#

yeah on further testing it seems like it's physically impossible to measure the exact time the play menu button is pressed / the time we need, at least externally with C++, as it's can only go down to nanosecond (9 digits) accuracy (somewhat) and here we're dealing with sub-nanosecond timestamping, which literally require atomic clocks for that.
although since we are dealing with Love2D here, potentially, if i follow the exact same actions they use for timing their main loop you could potentially run a second Love2D project in parallel? and try to time it off that? not sure

royal rune
#

So, I've looked at how cards are being drawn to the hand, at least for the first round and from what it seems like this is the way it works:
(You can find this on state_events.new_round()

The deck is a CardArea object, which has the shuffle(), which as the name says shuffles the deck, in the game afterwards, the queue for the cards are from the last place to the first place (i.e. the card in the 52nd slot will be drawn first, 51 will be second, etc)

In the shuffle method, it uses two functions to shuffle the cards: misc_functions.psuedoseed & misc_functions.psuedoshuffle with the result of the psuedo-seed as the input for the psuedo shuffle.

The psuedoseed method, takes a key to store that value later on, and calculates the seed using a combination of the current general seed (Generated from the character seed) & the key given, using the psuedohash function, which gives a numeric value given a specific string value. (This is also how the current general seed is created from the character seed).

After that, the hash gets transformed more by being multiplied by a constant, adding some random value modulo'ing it by 1.
that seed is feeded into pseudoshuffle, which shuffles by:

  • Sorting the array/table by a 'sort_id'
  • Going over the array/table backwards and shuffling the cards with a random position in it.

Given a hand starts with 8 initial cards, there could be a way to potentially try to reverse engineer the seed from those, but the issue being that because we don't necessairly know which indexes swapped, we won't know which indexes necessairly moved to the first slots to be drawn out if i'm correct

There might be a way to maybe reverse it off the choices of tags / bosses, but even then that would probably be a lot more general of a seed less than it being THAT specific seed you're playing

#

unless there's like a math trick to solve 2 variable equations with one equation (there isn't) i'm pretty much at a roadblock, at least for what i know

#

would love some ideas

lament narwhal
lament narwhal
#

also, there are some animations that call math.random() without any arguments, and if that is trackable then it may be doable to reverse the RNG for one specific feature

#

However, localthunk’s RNG algorithm has some homebrewed features that make me think FPS manipulation may be the best strategy

royal rune
#

i actually didn't think of FPS manipulation, and that could probably work, but there's one thing that may break it is that i'm not sure if the time Love2D takes to load is taken to account for the timer or not, because if it does it's a lot harder to track even with limited FPS

#

Most of the random calls i've seen that are just math.random() seems to be for particles which i feel like that outside the game is kind of hard to track, plus i'm not sure it necessairly uses the same seed as the game seed for it

royal rune
#

Small update, I haven't really worked on it much, but one thing i've realized is that the RNG calculation can't really be backtracked, at least from what i know, unless someone else knows better I don't really see how to reverse it from it's output, other than brute-forcing it which isn't viable (about 10 trillion possibillites)
I tried FPS manipulating it by pausing/resuming the thread, to somewhat success? There's still some extra lag / delay that i just can't seem to find a way to track. Although using the proccess create time, seems to create a pretty close value to Love2D's love2d.getTime() method, but still missing it by a bit.

any ideas would be great

lament narwhal
#

I think the only route would be to force the game to run under 10 FPS

#

You would need to find some way to cap the FPS or lag the game enough so every frame takes >0.1 seconds to run

#

Or somehow inject into the process and make love.update() always pretend like it’s been more than 0.1 seconds since the last frame

#

writing an Immolate filter to reverse-engineer a seed is also doable, since searching the 1.8 trillion seeds you can get randomly (O isn't possible) might be doable in the timespan of a few days or weeks

#

But I don't think this is the type of thing you want out of this