Meet The Chomps: Part II

One last thing I wanted to do for the reverse engineering effort I did last week was get exact probability values for the different multipliers. I figured the easiest way was to generate all permutations of bite orders for “Big Chomp wins” and then use Python’s fraction library to multiply and sum out the exact odds (since all permutations are mutually exclusive). Maybe there’s a better way, but seems to have worked decently well:

from fractions import Fraction

# Get winner for a given state, if any
def evaluate(string):
    big_score = 0
    little_score = 0
    for c in string:
        if c == 'b':
            big_score += 1
        elif c == 'B':
            big_score += 2
        elif c == 'l':
            little_score += 1
            little_score += 2
    if big_score >= 6:
        return 'B'
    elif little_score >= 6:
        return 'L'
        return None

# Generate next possible permutations
def next_permutations(string):
    result = set()
    result.add(string + 'b')
    result.add(string + 'B')
    result.add(string + 'l')
    result.add(string + 'L')
    return result

# Build all possible win states
def get_permutations():
    visited = set()
    finished = set()
    candidates = next_permutations("")
    while candidates:
        current = candidates.pop()
        winner = evaluate(current)
        if winner is None:
            # Get next permutations
            next = next_permutations(current)
            for next_candidate in next:
                if next_candidate not in visited:
        elif winner == 'B':
    return finished

# Get odds multiplication table
one_bite_chance = Fraction(6, 10)
def get_probability_map(big_probability):
    result = {
        'b': big_probability * one_bite_chance,
        'B': big_probability * (1 - one_bite_chance),
        'l': (1 - big_probability) * one_bite_chance,
        'L': (1 - big_probability) * (1 - one_bite_chance)
    return result              

# Get probability of a given state occurring
def get_probability(string, probability_map):
    result = 1
    for c in string:
        result *= probability_map[c]
    return result

# Get total probability for Big Chomp winning
permutations = get_permutations()
def get_total_probability(big_probability):
    probability_map = get_probability_map(big_probability)
    total = 0
    for string in permutations:
        total += get_probability(string, probability_map)

# Now get the total odds for each Big Chomp bite-chance
entries = {
    4 : Fraction(5, 10),
    8 : Fraction(6, 10),
    16 : Fraction(7, 10),
    32 : Fraction(8, 10),
    64 : Fraction(9, 10)
for multiplier, big_probability in entries.items():
    big_odds = get_total_probability(big_probability)
    little_odds = 1 - big_odds
    print("{}x: {} ~= {}".format(multiplier, little_odds, little_odds.numerator / little_odds.denominator * 100.0))

Which outputs some huge fractions, and the percentages are prettified as follows:


The empirical numbers were pretty close!


Big Chomp and Little Chomp

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:

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:

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
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:

; Otherwise, generate another number from 0-9
lui        at,0x4120 ; This is hexadecimal representation of 10.0
mtc1       at,f12
jal        RandomIntegerUpToF12

; If our random 0-9 >= 6 (40% chance), eat two
slti       v0,v0,0x6
beq        v0,zero,add_two_big

; 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

lui        v1,0x8011
addiu      v1,v1,-0x301c
lhu        v0,0x0(v1)=>big_chomp_counter(0x8010cfe4)
addiu      v0,v0,0x2

sh         v0,0x0(v1)=>big_chomp_counter(0x8010cfe4)

; ... returns before the next block ...

; Same 40% deal to eat two for Little Chomp...
lui        at,0x4120
mtc1       at,f12
jal        RandomIntegerUpToF12
slti       v0,v0,0x6
beq        v0,zero,add_two_little:
lui        v1,0x8011
addiu      v1,v1,-0x301a
lhu        v0,0x0(v1)=>little_chomp_counter(0x8010cfe6)
j          store_little
_addiu     v0,v0,0x1
lui        v1,0x8011
addiu      v1,v1,-0x301a
lhu        v0,0x0(v1)=>little_chomp_counter(0x8010cfe6) 
addiu      v0,v0,0x2
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 Data?

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)…

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

Never forget…

On this day, April 27th, 2016… Jordan… was full of shit.


What’s this? A moderately aesthetic website?

“Man, that Jengerer has certainly changed, I can hear you utter. “The invertebrate stooge went and cashed in his Times New Roman and manually written HTML page chips for a decent home page. Indeed, our world is a truly dismal thing… a dreary landscape bereft of any principles or moral fiber. If there be such a place as heaven, its monarchs have plainly and unlovingly cast us from their cares.” In response, I nervously insist that you relax with the hyperbole, man, because it’s just a god damn website and you’re being extremely irrational about it.