Introduction
So there you are, playing a pass-the-controller team game of Mario Party 3, expecting the usual shenanigans, the back and forth of stars and coins, and a tight race to the end. Wrong. This mini-game in the first few turns of the game, a bit of luck, and Team Luigi has their coins multiplied by 32, and suddenly it’s a race for second place.
Here’s how it plays out: you land on the Shy Guy tile, all your coins are anted, and you’re given a choice to bet either on the Big Chomp finishing his cake first to double your bet, or on the Little Chomp doing so first for 4/8/16/32/64x your bet. Team Luigi chooses Little Chomp for a 32x multiplier, wins, and brings their 21-coin count up to a ridiculous 672. The game is a wash after that and they finish with 9 stars, with 2nd place sitting at 3 or 4.
But what are the actual odds? I sought to find out, and using Project64’s debugger, I did.
Plan of Attack
First, I tried determining where the math was done that actually multiplied your coin amount, since that would probably use a different value based on whether you won or lost the bet. So I saved state before starting the mini-game, and noticed that loading state would often have the game start with a different seed, and thus, a different multiplier for the Little Chomp bet. Searching for that value in memory, I found that the Big Chomp multiplier gets stored at 0x8010CF60
and the Little Chomp multiplier at 0x8010CF70
. As evidence, here’s me manipulating those memory addresses to get Big Chomp to 64x and Little Chomp to 4x.
This lead was kind of a dead end, since the values written here were used long after the actual winner got decided. If I traced where they came from, it probably would’ve got me to the answer sooner, but I instead chose to look at nearby memory regions and see if I could find where the player’s selection was written. I found a region of memory that stored a 0 or a 1 based on your bet, and was later read to compare with which Chomp won.
Following accesses to that memory, I traced where the “winner” was written, and eventually tracked it down to this two-iteration loop, starting at address 0x80107194
:
loop_start:
sra v0,v0,0xf
addu v0,v0,a3
lh v0,0x0(v0)=>FUN_8010cfe4
bne v0,a2,LAB_b4311170 ; Checks v0 != 6
_addiu v0,a0,0x1
lui at,0x8011
sh zero,-0x300e(at)=>LAB_8010cff0+2
lui at,0x8011
sh zero,-0x300a(at)=>FUN_8010cff4+2
lui at,0x8011
sh a0,-0x3094(at)=>LAB_8010cf6c
lui at,0x8011
sh a1,-0x3012(at)=>LAB_8010cfec+2
sw v1,0x14(s0)
move a0,v0
sll v0,v0,0x10
sra v0,v0,0x10
slti v0,v0,0x2
bne v0,zero,loop_start
Of note is the line with the comment noting that V0 is compared with A2, containing the value 6 (hard-coded in the assembly). As a hunch, I assumed this was checking whether either Chomp had finished the cake, and sure enough, forcing V0 to a value of 6 on the first iteration immediately caused Big Chomp to win and Little Chomp to win if done on the second iteration. Here’s proof of me betting on Big Chomp and forcing either winner before any cake has been eaten (something something having your cake and not eating it):
Forcing Big Chomp Win | Forcing Little Chomp Win |
---|---|
Great! So now to figure out where those counters get incremented and why. Long story short, this block here starting at 0x80310314
:
; Gets random integer from 0-9 inclusive
mtc1 v0,f12 ; Stores 10.0 in F12
nop
jal RandomIntegerUpToF12
_cvt.s.W f12,f12
; Sets V1 to 5 if 4x, 6 if 8x, 7 if 16x, etc.
lui v1,0x8011
lh v1,-0x3000(v1)=>multiplier_threshold
; If our random 0-9 >= V1, Little Chomp eats!
slt v0,v0,v1
beq v0,zero,small_wins:
_nop
; Otherwise, generate another number from 0-9
lui at,0x4120 ; This is hexadecimal representation of 10.0
mtc1 at,f12
jal RandomIntegerUpToF12
_nop
; If our random 0-9 >= 6 (40% chance), eat two
slti v0,v0,0x6
beq v0,zero,add_two_big
_nop
; Otherwise, eat one and jump to the store
lui v1,0x8011
addiu v1,v1,-0x301c
lhu v0,0x0(v1)=>big_chomp_counter(0x8010cfe4)
j store_big; Jump has one-instruction delay
_addiu v0,v0,0x1
add_two_big:
lui v1,0x8011
addiu v1,v1,-0x301c
lhu v0,0x0(v1)=>big_chomp_counter(0x8010cfe4)
addiu v0,v0,0x2
store_big:
sh v0,0x0(v1)=>big_chomp_counter(0x8010cfe4)
; ... returns before the next block ...
; Same 40% deal to eat two for Little Chomp...
small_wins:
lui at,0x4120
mtc1 at,f12
jal RandomIntegerUpToF12
_nop
slti v0,v0,0x6
beq v0,zero,add_two_little:
_nop
lui v1,0x8011
addiu v1,v1,-0x301a
lhu v0,0x0(v1)=>little_chomp_counter(0x8010cfe6)
j store_little
_addiu v0,v0,0x1
add_two_little:
lui v1,0x8011
addiu v1,v1,-0x301a
lhu v0,0x0(v1)=>little_chomp_counter(0x8010cfe6)
addiu v0,v0,0x2
store_little:
sh v0,0x0(v1)=>little_chomp_counter(0x8010cfe6)
TLDR? This block is periodically run based on a counter incremented every frame to, at fixed intervals, decide which Chomp gets a bite, and whether that Chomp gets one or two. It generates an integer from 0-9 and compares it with a dynamic value that’s set based on the multiplier (which is randomly selected as you load into the mini-game). If it’s less than that value, Big Chomp gets a bite, if it’s greater than or equal to it, Little Chomp does. Then, whichever Chomp wins the current bite rolls another 40% chance to get two bites in.
Where The Data At…a?
So now the only mystery remaining is: where are the random number thresholds that we compare our 0-9 roll to stored? Following our initial lead for where the multiplier gets read from, we find there are two parallel tables. The multiplier table starting at 0x8010CC78
:
0x0004 ; 4x
0x0008 ; 8x
0x0010 ; 16x
0x0020 ; 32x
0x0040 ; 64x
And immediately following it, the threshold table for whether the Big Chomp gets the bite at 0x8010CC86
:
0x0005 ; 0-9 < 5 for 4x = 50%
0x0006 ; 0-9 < 6 for 8x = 60%
0x0007 ; 0-9 < 7 for 16x = 70%
0x0008 ; 0-9 < 8 for 32x = 80%
0x0009 ; 0-9 < 9 for 64x = 90%
Just to demonstrate that, lets add a 44x multiplier with a guaranteed win for the little guy (that is, we’ll write 0x002C
to all entries in the first table and 0x0000
to all entries for the second table)…
Well… the UI doesn’t show 44x because it’s an image (thankfully it didn’t crash looking for one), but the little guy did win in our planned landslide. Our planned-slide.
There’s the 44x… but the math doesn’t add up here. I’m guessing it doesn’t expect the multiplier values to be non-powers-of-two or something. I didn’t dig too much into this one, but I noticed it goes up to 999 sometimes, so maybe it’s running into some undefined behaviour.
Overall Statistics
As a fun experiment, I wrote this code snippet to simulate this behaviour for 5 million iterations, and produced the following results:
For multiplier 4X, little one wins 49.9582%
For multiplier 8X, little one wins 29.0432%
For multiplier 16X, little one wins 12.7799%
For multiplier 32X, little one wins 3.5343%
For multiplier 64X, little one wins 0.34936%
Well then, 3.5% chance of ruining the party for everyone else. Interesting to note that you should always bet on Little Chomp with a 4x multiplier, and also, the Chomp dialogue of them saying how hungry they are doesn’t actually tell you anything (or at least no more than just looking at the multiplier)!
Additional Notes
In case you’re curious, the multipliers themselves are equally likely as well, calculated starting at 0x80106064
:
; Gets random index from 1-5
lui at, 0x40a0 ; 5.0 in floating point
mtc1 at, f12
jal RandomIntegerUpToF12
addui v0, v0, 0x0001
addu s1, v0, zero
; Continuing at 0x801060FC (S1 is yet changed)
; Load from threshold table above
sll v0, s1, 16
sra v0, v0, 15
addu v0, sp, v0
lhu v0, 0x0020(v0)
lui at, 0x8011
sh v0,-0x3000(at)=>multiplier_threshold