Balatro

Balatro

View Stats:
May XD May 20, 2024 @ 11:55am
Is Wheel of Fortune REALLY 1 in 4?
I pick this card a lot. And I swear, in 30-40 times I picked it, it worked like, 4-5 times.
Something feels off about it, idk :\
< >
Showing 91-105 of 203 comments
Exploding Carrot May 24, 2024 @ 5:53am 
Originally posted by May XD:
Originally posted by Exploding Carrot:
Small sample size so far, but I've used 13 WoF cards since I started recording, with 3 of them working so that's 23.08% which is basically 1 in 4.

everyone in this thread just flaunts their crazy luck, and to that i'll just say: GOOD FOR YOU
12 clicks (2 with dice joker), zero procs so far

I'm not flaunting anything. I just said i'd record the result every time I picked up a WoF card and add it to the spreadsheet I linked earlier from Google Docs.
May XD May 24, 2024 @ 10:31am 
Originally posted by fortydayweekend:
If it's fair, it'll fail 20 times in a row for 1 in 315 people

If it failed for you 20 times in a row you would be absolutely convinced that it was rigged. But it's just bad luck and thousands of other players have the same experience.

To make it feel fair you'd have to actually code it to be unfair (i.e. increase or decrease the odds based on recent hits/misses but have it average out to 1 in 4)

rip
i hate to be one in 315 people, it sucks in here
iheartdaikaiju May 24, 2024 @ 11:08am 
Ok I am glad someone pointed me at the LUA. I can say the odds are ALMOST CERTAINLY NOT ONE IN 4.

*gasp*

Let me make my case before you post the silly gambler's fallacy lecture, professor. You will save yourself some embarrassment.

There is actually a precondition you must meet before you have any chance of success, and it allows you to use the card even if these preconditions are not met. I am posting relevant code from card.lua


if self.ability.name == 'The Wheel of Fortune' or self.ability.name == 'Ectoplasm' or self.ability.name == 'Hex' then
local temp_pool = (self.ability.name == 'The Wheel of Fortune' and self.eligible_strength_jokers) or
((self.ability.name == 'Ectoplasm' or self.ability.name == 'Hex') and self.eligible_editionless_jokers) or {}
if self.ability.name == 'Ectoplasm' or self.ability.name == 'Hex' or pseudorandom('wheel_of_fortune') < G.GAME.probabilities.normal/self.ability.extra then

So right off the bat we see two things

* the eligible_strength_jokers must *EXIST* (very important in a minute)
* we need to see what G.GAME.probabilities.normal and ability.extra are

When we look for the 2nd bullet, here we have a potential race condition

if self.ability.name == 'The Wheel of Fortune' then
self.eligible_strength_jokers = EMPTY(self.eligible_strength_jokers)
for k, v in pairs(G.jokers.cards) do
if v.ability.set == 'Joker' and (not v.edition) then
table.insert(self.eligible_strength_jokers, v)
end
end
end

I'm calling that a potential race condition because

* On line 1467 we don't enter the loop unless eligible_strength_jokers is populated and contains a joker we own
* On line 1533, inside a separate loop, we find the code where eligible_strength_jokers is set

This means if you pop a wheel of fortune before this loop is entered,

if G.STATE ~= G.STATES.HAND_PLAYED and G.STATE ~= G.STATES.DRAW_TO_HAND and G.STATE ~= G.STATES.PLAY_TAROT or any_state then

*phoenix write style closeup of my face across a rushing blue background*
YOU HAVE A 0% CHANCE OF TRIGGERING THIS CARD
*audience murmers*

This is why I said "almost certainly". The first loop has a null check. That often happens as a cheap quick and easy fix for a compile error so you can keep coding, and they often get left in there. We aren't checking to see the contents of the array on that line. We're just seeing if it's set, which implies it wasn't when that loop was run at some point.

Anyway this is easy for all of you gumshoes to test. All you need to do is report back here, whether you are more likely to get Wheel of Fortune *after* popping a wheel of fortune *during gameplay* and *not* in the shop, and *before* popping another Tarot card. This would need to happen on your first draw, the tilde(~) means not.

Anyway. Regardless whether that dog hunts, let's look at the probabilities.

In game.lua

probabilities = {
normal = 1,
},

and self.ability in game.lua as well


self.ability = {
name = center.name,
effect = center.effect,
set = center.set,
mult = center.config.mult or 0,
h_mult = center.config.h_mult or 0,
h_x_mult = center.config.h_x_mult or 0,
h_dollars = center.config.h_dollars or 0,
p_dollars = center.config.p_dollars or 0,
t_mult = center.config.t_mult or 0,
t_chips = center.config.t_chips or 0,
x_mult = center.config.Xmult or 1,
h_size = center.config.h_size or 0,
d_size = center.config.d_size or 0,
extra = copy_table(center.config.extra) or nil,
extra_value = 0,
type = center.config.type or '',
order = center.order or nil,
forced_selection = self.ability and self.ability.forced_selection or nil,
perma_bonus = self.ability and self.ability.perma_bonus or 0,
}

Well that's interesting. extra is the return value for copy_table. Let's see what that is.

in functions/misc_functions


function copy_table(O)
local O_type = type(O)
local copy
if O_type == 'table' then
copy = {}
for k, v in next, O, nil do
copy[copy_table(k)] = copy_table(v)
end
setmetatable(copy, copy_table(getmetatable(O)))
else
copy = O
end
return copy
end

So yeah. This may very well be NaN (not a number).

Anyway TL;DR the code for Wheel of Fortune does, indeed, introduce a plausible scenario where it would trigger significantly less than 25% of the time, if
* a precondition is not met, and
* an internal array is not correctly set
iheartdaikaiju May 24, 2024 @ 11:15am 
Just to be clear when I say NaN I mean that division by zero is possible
srn347 May 24, 2024 @ 11:54am 
It is quite strange that wheel of fortune uses eligible_strength_jokers while ankh and hex use eligible_editionless_jokers, both of which are defined using the exact same code. Still, from what I can tell that section of code does what it should: returns an array consisting of your editionless jokers (which iirc, if you're fed an array in a boolean check, returns true if non-empty and false if empty). This means you can only use wheel of fortune, ankh, or ectoplasm if you have an editionless joker (which is expected behavior).

The value of extra isn't shown in that file because it's found in the game.lua file (wheel of fortune's "extra" variable is set to 4). Probabilities.normal is a variable set to 1 by default (oops all 6 doubles it, but that's not important here). This means to generate a probability of 1 in "extra" (for not just wheel of fortune but anything), it checks whether pseudorandom (a called value in the range of 0 to 1 uniformly among all floating point numbers) is less than 1/"extra". The value generated is never NaN (or anything greater than 1), but the value it's checked against can be if "extra" were 0 (which it should never be).
iheartdaikaiju May 24, 2024 @ 12:18pm 
Originally posted by srn347:
It is quite strange that wheel of fortune uses eligible_strength_jokers while ankh and hex use eligible_editionless_jokers, both of which are defined using the exact same code. Still, from what I can tell that section of code does what it should: returns an array consisting of your editionless jokers (which iirc, if you're fed an array in a boolean check, returns true if non-empty and false if empty). This means you can only use wheel of fortune, ankh, or ectoplasm if you have an editionless joker (which is expected behavior).

The value of extra isn't shown in that file because it's found in the game.lua file (wheel of fortune's "extra" variable is set to 4). Probabilities.normal is a variable set to 1 by default (oops all 6 doubles it, but that's not important here). This means to generate a probability of 1 in "extra" (for not just wheel of fortune but anything), it checks whether pseudorandom (a called value in the range of 0 to 1 uniformly among all floating point numbers) is less than 1/"extra". The value generated is never NaN (or anything greater than 1), but the value it's checked against can be if "extra" were 0 (which it should never be).

My argument isn't really around the code not doing what it's supposed to. I agree that's the case.

My argument is rather with the state. Loop A depends on loop B being entered with a condition at least one time, or loop A can't be entered through wheel of fortune at all.

The 2nd argument is I agree more thin and I agree it should never be the case, I was only pointing out the possibility for completeness. Basically I just wanted the fact that copy_table *could* return null entered into the discussion.

Thought I agree the extra variable here should never be anything other than 4, given that there is very likely a state transition error with the first argument, I was forced to mention this as the same sort of thing would cause this to fail as well.

In any event, just anecdotally, I have had *dramatically* better luck getting Wheel of Fortune to pop off when I first pop it off using a WoF card I got with an Emperor midgame if I pop off WoF right away before playing my first hand. Which would be in line with the conditions required to enter the 2nd loop.
Last edited by iheartdaikaiju; May 24, 2024 @ 12:19pm
Goblin May 24, 2024 @ 1:43pm 
Originally posted by iheartdaikaiju:
Originally posted by srn347:
It is quite strange that wheel of fortune uses eligible_strength_jokers while ankh and hex use eligible_editionless_jokers, both of which are defined using the exact same code. Still, from what I can tell that section of code does what it should: returns an array consisting of your editionless jokers (which iirc, if you're fed an array in a boolean check, returns true if non-empty and false if empty). This means you can only use wheel of fortune, ankh, or ectoplasm if you have an editionless joker (which is expected behavior).

The value of extra isn't shown in that file because it's found in the game.lua file (wheel of fortune's "extra" variable is set to 4). Probabilities.normal is a variable set to 1 by default (oops all 6 doubles it, but that's not important here). This means to generate a probability of 1 in "extra" (for not just wheel of fortune but anything), it checks whether pseudorandom (a called value in the range of 0 to 1 uniformly among all floating point numbers) is less than 1/"extra". The value generated is never NaN (or anything greater than 1), but the value it's checked against can be if "extra" were 0 (which it should never be).

My argument isn't really around the code not doing what it's supposed to. I agree that's the case.

My argument is rather with the state. Loop A depends on loop B being entered with a condition at least one time, or loop A can't be entered through wheel of fortune at all.

The 2nd argument is I agree more thin and I agree it should never be the case, I was only pointing out the possibility for completeness. Basically I just wanted the fact that copy_table *could* return null entered into the discussion.

Thought I agree the extra variable here should never be anything other than 4, given that there is very likely a state transition error with the first argument, I was forced to mention this as the same sort of thing would cause this to fail as well.

In any event, just anecdotally, I have had *dramatically* better luck getting Wheel of Fortune to pop off when I first pop it off using a WoF card I got with an Emperor midgame if I pop off WoF right away before playing my first hand. Which would be in line with the conditions required to enter the 2nd loop.
You don't understand. There is no possibility of "loops" being entered wrong at all. If that variable is not set the use button on the card is grayed out. There is no race condition, the code is linear and isn't even multithreaded. Maybe don't use words you don't understand. You end up just sounding like someone who doesn't know how to program but tries to pretend to anyway.
Same for your "what if extra isn't set" idea. WoF's extra is set at game initialisation, in game.lua. Just look for c_wheel_of_fortune. It's impossible for it to be anything other than 4, unless you mod the game.

Here's a little experiment for you though, since you're so convinced there must be ways for the code to break: get two Oops All 6 jokers. The odds will be 4 in 4, AKA 100%. Then find when your supposed race conditions are true making the odds 0%, or when extra isn't 4 but NaN meaning it can never trigger because a random value between 0 and 1 is never larger than NaN. It'll be easily recognisable because WoF will still Nope! despite 100% odds. Protip: you'll be wasting your time.
To SAVE you some time: _RELEASE_MODE in conf.lua set to false allows you to spawn in cards from your collection, hold tab for instructions. I've used that to debunk and verify tons of bugs for people over the past months.
iheartdaikaiju May 24, 2024 @ 2:41pm 
Originally posted by Goblin:
You don't understand. There is no possibility of "loops" being entered wrong at all. If that variable is not set the use button on the card is grayed out. There is no race condition, the code is linear and isn't even multithreaded.
That is easy enough to test. I'll admit I don't know the game data well enough, having literally just opened it for the first time today, to see where the code which enables the wheel of fortune is. That said...

Originally posted by Goblin:
Maybe don't use words you don't understand.
From this, forgive me but you seem laughably immature. So I am going to challenge you to pull up the location of the code that enables consumeables inside use_consumeable and prove that eligible_strength_jokers not being enabled will cause it to be greyed out. I have not even looked myself yet.

I am actually going to do the same and repack the exe, because that is a lot less of a silly waste of time than

Originally posted by Goblin:
Here's a little experiment for you though, since you're so convinced there must be ways for the code to break: get two Oops All 6 jokers

In fact that, again please forgive me, asanine experiment you proposed makes this next thing you said particularly ironic,

Originally posted by Goblin:
You end up just sounding like someone who doesn't know how to program but tries to pretend to anyway.

Since anyone competent would just hardcode a variable and repack the executable.

I'm sorry for the tone but I've learned to greet immaturity with immaturity. I'm not going to go into my accomplished career because, unlike you, I have nothing invested in impressing people on the internet.

And THAT said I would really like you to improve your basic grade school reading comprehension because,
Originally posted by Goblin:
Same for your "what if extra isn't set" idea. WoF's extra is set at game initialisation, in game.lua. Just look for c_wheel_of_fortune. It's impossible for it to be anything other than 4, unless you mod the game.

This was never in dispute, I agreed before you posted this that it would be always 4, and again, only pointed out that it was possible for the method to return 0 simply to make sure this was added to the discussion.

EDIT : I just realized your knee-jerk lizard brain may have latched onto what I just said and shouted objection and had you furiously typing a reply, which you'll post without reading this edit anyway. That will be hilarious because "the method" here refers to "copy_table".

Now if you want to continue bleating and wasting everyone's time humiliating yourself that's fine, I can just ignore you as you've proven beyond a shadow of a doubt you have nothing worthwhile to say, until you complete the very simple challenge I issued to you.
Last edited by iheartdaikaiju; May 24, 2024 @ 2:46pm
iheartdaikaiju May 24, 2024 @ 2:44pm 
To everyone else I have done nothing but introduce the possibility that there is a bug and invited people to look at the code for themselves, as I have done. As I said upfront I have only today taken my first glance at the LUA. I am not asserting there is a bug but I am asserting the idea there is one cannot be dismissed on simple inspection.
iheartdaikaiju May 24, 2024 @ 3:02pm 
To goblin I'm not interested until you've done the homework I assigned. And honestly not even then until you've grown up quite a lot.

To everyone else I'm just adding this after line 1468 like a normal person.

if temp_pool == {} then
self.ability.extra = G.GAME.probabilities.normal
local card = create_card('Joker', G.jokers, nil, 0.99, nil, nil, nil, 'wra')
card:add_to_deck()
G.jokers.emplace(card)
end
iheartdaikaiju May 24, 2024 @ 3:23pm 
Alright.

Goblin while I don't think either of us would respect me if I apologized after your desparate attempt at internet one-upmanship, I have never had a problem telling other people they were right and I was wrong.

You were right.

I was able to verify after both reviewing the code and repacking the lua - which again I only opened for the first time today and looked at for all of 10 minutes before our conversation - that the use button is indeed greyed out correctly when you have no jokers, making the state I was talking about unreachable.

The wheel of fortune is definitely a 1 in 4 chance and the bug I thought was possible is not something that can happen in ordinary play.
Last edited by iheartdaikaiju; May 24, 2024 @ 3:24pm
malogoss May 24, 2024 @ 3:36pm 
bruh
Goblin May 24, 2024 @ 4:30pm 
Damn. You, uh, got really heated there huh?

To give some more context, line 1533 is where eligible_strength_jokers is checked before even letting you hit the use button. So it was indeed impossible for local_pool to be empty.
Moreover, I just did a little experiment with your code: add it where you wanted it, and on the line before it add local_pool = {}. Y'know, to guarantee it would run what you added. It crashes the game - you adding a Joker joker doesn't add it to local_pool, you set the probability to 100% with your second line, and thus it tries to get a random card out of a set of no cards.
That's the danger with adding code yourself - you risk introducing new bugs. Spawning two Oopses is a foolproof test - if extra isn't 4, it wouldn't become a 100% chance with two of them. If the pool could ever not be filled, you'd be able to find a moment in the game to use WoF and not get an edition on any viable joker. But in reality, the game would have crashed in that scenario anyway as seen through the above test.

There's some irony in you demanding I do homework for you when you refused to do a test I gave you. In fact, plenty of irony throughout. But whatever, my point was never "one-upmanship" and always to just point out why your ideas of a possible cause were wrong. I already dug through this code weeks ago, and simply wanted to correct some misconceptions you had. At least you learned a little more about the codebase, here's hoping it motivates you to maybe mod the game :steamthumbsup:
iheartdaikaiju May 24, 2024 @ 4:43pm 
Originally posted by Goblin:
That's the danger with adding code yourself - you risk introducing new bugs. Spawning two Oopses is a foolproof test *snip*

Look, clearly neither of is is an idiot and deserves the way we were talking to each other. With this understood then, I am not going to reply with an explanation of what a test harness or a seam is, and why people in industry write automated tests in code, and how errors in the test code can themselves be mitigated with the unit test definition, because all that is likely to be knowledge we both have. Instead I am going to say that you have your approach and skillset and I respect that.

In that context I think you're capable of figuring out not only that setting the probability to 100% was clearly intentional, but why I did it, and what was under test. What was under test in the sample was not the probability directly ;)

My real aim here - which you performed beautifully at - was to put the conversation to rest, as is evident by my earlier posts. That was not possible to accomplish without a bit of drama so all told I am glad you showed up. The conversation was being dominated by people reposting stuff about the gamblers fallacy and a lot of unhelpful information. It was very clear that there was nothing wrong with the RNG at the outset, but that was never going to be enough until someone advanced the null hypothesis and wore that hat.

Someone had to come here, say there was a bug, post a case for the bug, and have it be shot down, before this conversation could finally die.

So thank you for your comments, I am actually working on my own Unreal game and can't take on another creative project right now but knowing how ridiculously easy Balatro is to mod and edit now I may consider it :)
Ronald Brain May 25, 2024 @ 4:12am 
I think this thread's gone long enough for the nerds and experts to have their opinions, so let's conclude what we knows:
-Code wise, there is no bug. WoF is 1 in 4, so there's a 75% Nope! when you pop a random WoF.
-Rate is seed-dependent. As soon as you fire up a new game, it's already decided which WoF will pop, which WoF will Nope!, no matter how many times you try to reload.
-There will be seed that WoF procs more than Nope!, and vice versa. The reason many people runs into Nope! is because 75% of the time we're unlucky.
-With enough digging, each seed WILL produce an amount of WoF that procs 1 in 4 (So like a seed has 20 WoF, 5 of them will proc). The reason we can't find them all is of course econ restriction.
< >
Showing 91-105 of 203 comments
Per page: 1530 50

Date Posted: May 20, 2024 @ 11:55am
Posts: 203