Scandalous Dice Blocks in Mario Party 3

There are videos online that have discussed the Mario Party dice blocks not being skill-based, and some games even having them pre-determined before you even press a button.

Fiddling with save states in Mario Party 3, it’s clear that the input timing does matter, as reloading the state and pressing the button at a different time produces a different value. But is it purely random beyond that?

Early signs suggest yes. I quickly found address 0x800cdcb9 contains the dice roll after the dice block is struck, and setting a breakpoint on it quickly reveals some code that looks suspiciously like an RNG.

lui v0, 0x8009
lw v0, 0x7650 (v0)
lui v1, 0x41c6
ori v1, v1, 0x4e6d
mult v0, v1
mflo v0
addiu v1, v0, 0x3039
lui at, 0x8009
sw v1, 0x7650 (at)
addiu v0, v0, 0x303a
srl v0, v0, 16
jr ra
andi v0, v0, 0x00ff

A quick shortcut you can take when you see magic numbers being loaded into registers like 0x41c64e6d is to Google them. Searching for that value brings up a few source files for RNGs.

Opening the address that’s loaded in the beginning and stored in the end, we find the 32-bit seed and setting a breakpoint in this RNG function directly, we see that this function appears to be called at least once per frame (even when the character is just completing the move and no longer spinning the dice block). That explains why waiting results in a different roll.

The tail end of this function appears to be truncating the result to 16-bits and returning the last byte after masking it.

Let’s test our knowledge by breaking where we originally saw the call to the RNG and doing the math manually to predict the dice roll.

Breakpoint hit!

The seed at this point is 0xfdb462a7. Passing that through our RNG (full code linked below), we get the truncated value of 0x07. We’re still missing the step of how that value gets transformed into a 1-10 value, so let’s continue stepping here.

The code right after this function returns looks like:

andi v0, v0, 0x00ff
lui v1, 0xcccc
ori v1, v1 0xcccd
multu v0, v1
mfhi a2
srl a0, a2, 3
sll v1, a0, 2
addu v1, v1, a0
j 0x800dbe2c
sll v1, v1, 1

So first it masks the value redundantly, then it multiplies it by 0xcccccccd, shifts the result right by 3, and adds that to the result shifted left by 2, and then shifts that left once; wild. Most of this looks like a common magic number optimization for division, but I don’t quite have the knack for figuring out which one it’s meant to be. A good guess might be that it’s just a wonky remapping taking values from 0 to 255 and changing them to values from 1 to 10.

The next bit it jumps to reveals quite a bit:

subu v0, v0, v1
addiu v0, v0, 0x0001

This lines up with the theory of some kind of division, since this end bit would be “subtract the fully divided part out to get the remainder and add one”. So the code above is likely a division by 10 and we can confirm that by just plugging in values (we’ll do this later).

So here, our final value from the RNG was 7, and passing it through the alleged division gave us 0. Subtract 0 from 7 and add one and we get… 8? Is that our dice roll?


Now the last bit we really need to do is confirm that that algorithm is a divisor. Let’s just plug in all values from 0 to 255 and see what we get.

0 - 9 maps to 0.
10 -> 19 maps to 10.
20 - 29 maps to 20.
250 - 255 maps to 250.

So that magic code isn’t division directly, but it divides and then re-multiplies the “whole part” by 10, and then gets the remainder by subtracting it from the original value.

Here’s an interesting caveat you may have noticed; because 255 is not cleanly divisible by 10, we actually have a slightly higher chance of rolling 1-6!

Here is an easy C++ sample demonstrating the code that’s running here. Now you know, no sense in waiting to hit the block!

Addendum: we were also curious whether there’s any rigging to the mushroom items, and it turns out that no, fixing the seed to be one that rolls a 10 (0x17 works) each time results in three tens, so there’s no forced distribution to each roll.


Animal Crossing Visitor Scheduling

So I wrote another long live-blog about reverse-engineering how the next visitor to your Animal Crossing town is scheduled. Let’s boil it down to the interesting bit. The visitors are seeded when you enter your town on a date after the previous visitor’s visit date. This is evaluated using the expression:

((seed + 1) + (year - month) + day + hour) % 6

Where month is 1-12, day is 1-31, and hour is 0-23. Then that value is used to index the next visitor from a list in the following order, starting from 0 and counting up: Nobody (Copper just says nothing to report and talks about some of the characters), Gracie, Crazy Redd, Wendell, Saharah, and Katrina. If the generated visitor is the same as the previous one, it merely rolls onto the next one in the list instead.

So if you’re at least moderately mathematically savvy, you can see that the account seed only matters modulo 6, meaning 0, 6, and 12 are “identical” as far as this expression is concerned, as are 1, 7, 13, and so on. So we can easily determine the seed modulo 6 by noting who visited the town last, then generating a new visitor and using the date we entered the town on to figure out our seed.

There’s a bit of a tricky case here, though, where, say our last visitor was Crazy Redd and then we get Wendell next time. We don’t know at this point whether it rolled Wendell on the first try, or if it tried to roll Crazy Redd again, failed because he was the last visitor, and then rolled onto Wendell as a fallback. However, this ambiguity is easy to resolve: we merely have to roll just one more visitor after Wendell using the same seed again.

If it rolls Crazy Redd as our third visitor, then we know that Wendell was chosen as a fallback for the second visitor. If it rolls Saharah, we know that the second visitor was rolled as Wendell fair and square and now we’re falling back to the visitor after Wendell in the list, which is Saharah.

As an example of different dates with the same seed, examine the following:

On April 21st, 2020 at 9 p.m., the seed is: (2020 – 4) + 21 + 21 = 2058.
On April 24th, 2020 at 6 p.m., the seed is: (2020 – 4) + 24 + 18 = 2058.
On April 27th, 2020 at 3 p.m., the seed is (2020 – 4) + 27 + 15 = 2058.

It’s basically just a matter of “add X days, subtract X hours” if you’re operating within the same month. For simplicity, I’ve created this page to help calculate your seed and then use it to determine when you should visit to get the visitor you want scheduled.

Just as an example usage of that page, for the save file that I was debugging with in the emulator, “nobody” was the previous visitor way back in 2011. When I logged in on April 21st, 2020 at 9 p.m., the visitor that Copper said was scheduled to come was Saharah. So I submit Nobody as the previous visitor event, hour 21, day 21, month 4, year 2020 and Saharah, and it generates a seed of 3.

Now say Saharah was going to visit my town on April 24th; if I wanted Crazy Redd to be the next visitor, I could enter April 25th, 2020 as my date and the page would tell me that I should only enter town at 3 a.m., 9 a.m., 3 p.m. or 9 p.m. that day. I tried it out on the emulator with the date set to April 25th, 2020 at 3 a.m. and…

One final cool trick that I made the generator take advantage of is the ambiguity I mentioned before. If the visitor we want to visit happens to be after the previous one in the list, we have twice as many hours in the day that we can generate them with, because we can also choose the hour that would generate the same visitor as last time, and then roll over to the next one. So for instance, if I wanted Gracie instead of Crazy Redd, I could visit town at 1 a.m., 2 a.m., 7 a.m., 8 a.m., 1 p.m., 2 p.m., 7 p.m., or 8 p.m.


Saharah When?


I had an interesting idea for a service that would tell you outside of the game when Saharah or Crazy Redd are going to visit town. Is this something that can be determined just by knowing what seed the game uses, or does it depend on user input or the time that they log in? Let’s find out.

Starting with Strings

Doing a similar string search as our last project, we quickly find the line that Copper tells you with the date in it; going up the stack even higher, we see the date string isolated in memory at 0x81297f00. Breaking on that, we see that it’s copied from 0x812f94d0.

Darn, it’s being used while we’re still at Copper’s prompt, so let’s just go up the stack until we find when “22nd” is written there.

It’s not written at the time we call 0x8056acdc, so we can step into there and see when it’s being called. Just before I did that, though, I decided to peep at the call stack and see if any of the registers scream “July 22nd” in one format or another, but no such shortcut exists here.

Let’s step through, then. It’s being copied at 0x803eee7c from a string of 20 counts: 20th21st22nd23rd24th25th26th27th. The offset into this string is 8 as we expect, coming from r30.

Let’s just try and take a shortcut here. Going up one call in the stack, we see r6 with a value of 0x663, which looks like it may be some form of time representation. Let’s fiddle with it.

This was for a value of 0x664.

So it appears to operate on granularity of days, but… 0x664 that’s entirely too high to be “day within the year”. Idea: perhaps it’s an offset from when the save file was created? 1636 days before July 23rd, 2011 was January 29, 2007. It’s conceivable; maybe I’ll try on a fresh save file later. For now, I think we have enough to go on; let’s figure out where this value comes from.

First call that appears to throw it on the stack is 0x803eefc8. In there, we see this instruction that loads 0x663 into r5:

0x803ef000    addi r5, r4, 1613

So there seems to be some hard offset that we add 22 onto. I wonder if we’re not actually operating on the level of dates yet and this is merely a string table index computation, so by bumping it to 0x664 we technically didn’t change the date computation, just bumped the string offset. Just for the heck of it, lets add more than 32; that should bump us past the numeric strings.

Definitely still in string-offset land.

An amusing result, but in any case, our next step is to trace where r4 comes from. Actually, I’m already having nightmares of having to step through code that handles leap years. Let’s just step up the stack and see if we can manipulate any of these registers/stack values to bump the date.

Date Get

While stepping around this code, I got excited after spotting 0x81286400 because I recognized 86400 as the number of seconds in a day, but then… I realized this is in hexadecimal, so it represents a totally different value. Anyway, I spotted that a value of 0x716 gets loaded from a seemingly fixed address 0x81286960. Here’s a hunch… 0x7 = July. 0x16 = 22. Let’s try and tweak it to August 7 by setting it to 0x0807!

Mic drop!

Okay, so now let’s see… does this value get written to when we talk to Copper or when we load the save file? A simple breakpoint reveals… no writes while we’re talking to Copper, so it must have been written earlier. I bet it happens when you “log in”, since it’d definitely have to evaluate how much time has passed and bump up the visit date if you missed the previous one. So let’s get out and jump back in.

Okay, I’ve hit a point here where I’m seeing it compare that value to others, but… I don’t remember what date I have the emulator set to. Restart one more time…

Okay, I definitely saw some 0x0714 value there which would represent July 20th. Stepping back in, we can see it being compared with 0x0717. This seems to be one day after the next scheduled visit. Just as a wild guess, this is perhaps checking if the last scheduled Saharah visit happened, since that’s the day after.

At this point, the best course of action would probably be to change the date to after Saharah’s “future” visit and see what we do upon entry. Restarting the emulator, we’re on the 28th of July, so this should do fine. Let’s save state and jump in.

Okay, just tracking as I step through:

  1. 0x8039bd3c: r11 gets set to Saharah’s old scheduled date 0x0716.
  2. 0x8039bd48: r3 and r4 have 0x14 and 0x7 respectively. This is perhaps our last play date?
  3. 0x8039bd4c: our last play date is compared with 0x0717, which is the day after Saharah’s previously scheduled visit.
  4. At 0x8039bbe0, a value of 0x4f is being compared with a bunch of others. 0x4f is 79, so let’s follow where this goes.
  5. For a value of 79, this function returns 5. Can we guess that this is maybe the visit interval?

Just as an impatient check, let’s jump ahead and see what date ultimately gets computed for her next visit: 0x71f which puts her at… the 31st of July. Hm, it doesn’t seem immediately clear how the 5 is involved, 5 days after the 22nd would be the 27th. Alright, impatience didn’t pay off, as usual, let’s jump back.

I got to a point where I could just keep reloading and it would generate the same “next” date 3 times in a row, so chances are there’s some determinism to this. Let’s litter some breakpoints and try to catch the date generation in the act.

Who’s On The Way?

Okay, I had to delete a bit of stuff here, because there’s something I missed: that same memory location that stores the date is used regardless of which visitor is coming, and there’s another field that seems to determine who’s coming, just before the date at 0x81286954 which is subtracted with 74 at 0x8056adbc.

Following these values in memory a little more, we come upon a little loop at 0x8039ed30 which seems to copy an array of these integers in the following order: 0x4e, 0x4c, 0x4b, 0x4a, 0x4f, 0x4d. Perhaps this is a cyclic order of events that occur in the town? Later, we check an index that’s currently 4 (which lines up with the fact that our value is 0x4f in memory).

Again, I had to delete a bunch of stuff that led nowhere because I eventually came to a point where I performed the following steps:

  1. Load the 2011 save file with the date set to the 22nd.
  2. Copper says that Wendell is visiting on the 23rd.
  3. Change the date to the 24th.
  4. Copper says that Katrina is visiting on the 26th.

Then I restored the save file back to 2011 state and did:

  1. Load the 2011 save file with the date set to the 24th.
  2. Copper says that Gracie is coming on the 26th.

Determinism in Question

So this immediately threw out the suggestion that the order was fixed or cyclic and it seems to just be randomly seeded at the time that you check in since the last one. However, this is actually kind of a nicer result, because it means that if the seed doesn’t change too quickly, we can predict when you should turn the game on to schedule the visitor that you’re interested in.

My preliminary findings there were promising as well; setting a memory breakpoint on our “next visit date” at 0x81286960 and following it up, I spotted a certain value being used to compute the arrival date of the next visitor and after a series of following that seemed to be fixed as 9.

It took a fair bit of tracking down to figure out where it came from, but ultimately: it’s the hour of the day. It’s possible a more granular date value gets used as well, but given our previous results where we managed to produce the same visitor and date 3 times in a row, I’d say the hour is probably the fastest thing that changes the seed.

Planting/Generating a Seed

So, let’s step back into the bit of code that I started tracking for the computation of “next visit date” and see if that also has a determination for who the next visitor will be. This starts around 0x8039bd38. I moved the emulator’s time up to the modern world just so that it’s easier to differentiate old from new data, and here, we see all manner of date values on the stack: previous visit, current year, year of last visit, etc.

In this run, after a few instructions, we have the following registers and values of interest:

Current Hourr40x9
Current Dater80x0415 (April 21st)
Current Yearr60x7e4 (2020)
Current Year Offsetr70x14 (20)
Last Dater110x0712 (July 12th)
Last Yearr290x7db (2011)
Last Year Offsetr70xb (11)

These get pushed around on the stack and then it gets the value of the last visitor into r0, which in this case is 0x4e, which is 78. We call the same helper we saw before that returned 5. I restarted the emulation and stepped through again, just because I thought a breakpoint on the above values would be useful. This time we return 0x17 in r3.

At this point, we jump into a function with three different values being compared:

r3: 0x14041509
r4: 0x0b071000
r5: 0x0b071217

These seem constructed as: YYMMDDHH and it’s not immediately clear what the second two dates are. Perhaps one is last visitor visit date and one is last login date, but it’s difficult to say which is which. Anyway, current date gets compared against r4 and r5 and jumps to 0x8039b2f0 when we realize we’ve jumped ahead in time, which just returns 0. Perhaps this is a boolean expiration check.

The outside call checks the result, and jumps to 0x8039bde0 due to the time skip-ahead. A yet-mysterious value gets read into r23 from a hard-coded address at 0x81266420 that ultimately resolves to 0xf08d. This value is actually present in the save file immediately after the name of the character and town: this may be our magic seed.

Then this, starting at instruction 0x8039be00:

lbz r5, 0x6125 (r25) # M: 0x4
addi r7, r23, 1 # Magic + 1: 0xf08e
lhz r0, 0x6126 (r25) # Y: 0x7e4
li r3, 11 # Hard coded at 11
lbz r6, 0x6123 (r25) # D: 0x15
li r4, 164 # Hard coded at 164
sub r0, r0, r5 # r0 = Y - M
lbz r5, 0x6122 (r25) # H: 0x9
add r0, r0, r6 # r0 = (Y - M) + D
add r23, r0, r5 # r23 = (Y - M) + D + H
add r23, r7, r23 # r23 = Magic + 1 + (Y - M) + D + H
bl 0x8039b464 # Jump somewhere else?

And we currently are left with a value of 0xf88c. Stepping into that last function call… there’s at least a 77% chance we’re dealing with a random number generator.

Setting a Target

Assembly analysis beyond this point probably doesn’t make a great deal of sense. Ultimately, it seems like it’s some kind of random number generator, so it depends entirely on the seed it’s given and we can’t do much without figuring out what seed we’re using.

So here are the next steps for us:

  1. Figure out how the generated number is used to pick the next visitor and visit date.
  2. Reimplement that generation in a separate program.
  3. Collect enough data points that we can reverse engineer the seed based on which visitors/dates are generated.

This’ll probably a weeks long process, since the seed is 16-bit, which is basically the maximum “information” or “entropy” (I never know if I’m using that word right) of our random number generator, no matter how complicated the code is.

Oh, hello there. You missed a whole lot. I stepped through a bunch of functions, annotated a ton of useless assembly, and can say with confidence now: it’s not much of a random number generator at all, more like an index generator. Here’s where the seed usage actually starts, at 0x8039be44:

lis r4, 0x8126
lis r5, 0x8065
addi r6, r4, 25600 # r6 = 0x81266400
lis r4, 0x8065
mr r29, r3 # r29 = 0xb1b
addi r30, r5, 8152 # r30 = 0x80651fd8
addis r27, r6, 2 # r27 = 0x81286400
addi r31, r4, 8140 # r31 = 0x80651fcc
lwz r4, 0 (r30) # r4 = 6
lbz r0, 0x554 (r28) # r0 = 0x4e
divw r3, r23, r4 # r3 = 0xf88c (seed) / 6 = 0x296c
mullw r3, r3, r4 = 0x296c * 6 = 0xf888 # Round to multiple of 6?
sub r3, r23, r3 # r3 = 0xf88c - 0xf888 = 4 # Is this modulus?
addi r23, r23, 1 # Add one to the seed
rlwinm r3, r3, 1, 0, 30 # r3 = r3 * 2 = 8
lhax r26, r31, r3 # Loads the next visitor code = 0x4f!
cmpw r26, r0 # Compares with previous visitor code = 0x4e!
beq+ 0x8039be64 # Jump to the lwz r4 above.
bl 0x803a2140

I deleted yet another insane wall of assembly here because I realized that now that we know it’s not fully deterministic regardless of log-in time, we don’t particularly care about the date that gets generated, we just want to be able to schedule the visitor of our choosing. Let’s re-implement the assembly above in C/C++.

const unsigned int accountSeed = 0xf08d;
const unsigned int hour = 9;
const unsigned int day = 21;
const unsigned int month = 4;
const unsigned int year = 2020;
const unsigned int dateSeed = (year - month) + day + hour;
unsigned int finalSeed =(accountSeed + 1) + dateSeed;
cout << hex << finalSeed << endl;

Accuracy confirmed so far, as that prints 0xf88c. Now, let’s proceed to the actual visitor generation.

const unsigned int lastVisitor = 0x4e;
const unsigned int visitorCount = 6;
const unsigned int visitors[visitorCount] =
int nextVisitor;
    int nextVisitorIndex = (finalSeed % visitorCount);
    nextVisitor = visitors[nextVisitorIndex];
} while (nextVisitor == lastVisitor);
cout << hex << nextVisitor << endl;

Which does indeed print 0x4f like the game generates. So basically what this reveals at this point is that because + hour + day is the last part of the seed generation, you have an opportunity every 6 hours to get the visitor you want to come (assuming they didn’t visit last time).

Now there’s an important step in executing this that I haven’t gone over yet: which of the 0x4f, 0x4e, etc. corresponds to which visitor? This should be pretty easy to fix in memory and ask Copper, so let’s do it.

0x4eNothing, Copper just asks you about Totake, Pete, etc.
0x4bCrazy Redd

Before we break apart the generation and figure out how to rig it, let’s confirm our understanding by fixing the hour value in such a way that it attempts to generate 0x4e again, fails, and move one up to 0x4c. Since our generated visitor at 9 a.m. was Saharah, we merely have to dial back 4 hours to 5 a.m.

Stepping with the debugger, we get a final seed of 0xf888, which, modulo 6 is 0, and so the compare at 0x8039be84 succeeds this time, and the generation is done again with a seed of 0xf889 and we generate 0xfc this time. Easy peasy!

Now, the difficulty with determining the order at this point is just that there’s a bit of ambiguity. Without accessing the memory yourself, it’s difficult to distinguish from “generated visitor A, however they visited last time, so we rerolled for B” from “generated B fair and square on first try”. However, there’s a fairly easy workaround to this. Here’s an example:

  1. Allow a visitor to come to your town, noting down who it was.
  2. Enter town the day after they visited, and note down the hour and day of the month; later in the evening will make the next steps easier. Let’s say it’s 9 p.m. on the 21st of April. Ask Copper again when the next visitor will be arriving and note down who it is.
  3. If the next visitor that’s scheduled to arrive is not immediately after the previous visitor in the table above (wrapping around from Katrina to Nothing), skip to step 6.
  4. If the next visitor is immediately after the previous visitor in the table above, then we have one of two possibilities: either it tried to generate that same visitor again and bumped up to the one after it, or it cleanly generated with no conflict. We resolve which one of these it is by entering town the day after that second visitor visits, say, on April 24th, choosing an hour such that the sum of the hour and the day of the month is the same as it was in step 2. So in this case, step 2 was at 9 p.m. on the 21st, so 21 + 21 = 42; so if we’re entering on the 24th of April, we want to enter at hour 42 – 24 = 18 = 6 p.m.
  5. Knowing what we do about the seed generation, the seed when it rolled last two visitors should be the exact same. So if the third visitor is the same as the first, we know that the second one attempted to roll that same first visitor, but bumped up due to the conflict, and then the third visitor cleanly generated the same as the first one. If the third visitor is one after the second, then we know the second visitor cleanly generated with no conflict, but the third attempted to re-roll that same one and got bumped ahead.
  6. After we determine which case we fall under, we can work back to determine who the initially generated visitor was for day 2 (before conflicts, if any, were resolved), and denoting their index with Y, we compute:

    accountSeed = (6001 - Y - dateSeed)

    The 6001 is just a dummy base seed of 6000 to make sure we’re positive once we subtract our date seed without changing where we would land modulo 6, plus the 1 that gets added in the final seed computation. Therefore, we can simply plug that accountSeed in the above code, allowing us to compute which visitor will be generated if we log in at a given hour on a given day, given a certain previous visitor.

Before I fully confirm the above, however, I’m going to put it to the test with my town on my actual GameCube over the next few days and then I’ll write an easy web page that can compute your seed and when you should log in to get the visitor you want. Pretty pleased with the results so far, though!


Animal Crossing’s Perfect Town: Abridged

If you’d like to read the full, unfiltered disassembly ramblings that led to this conclusion, go here. The results are summarized below on how to get the perfect town in Animal Crossing (GameCube). The results were derived by stepping through the assembly on the NTSC version of the game, but I imagine similar results for other versions.

  1. Overall garbage count must be less than 5, excluding the Garbage Dump area (garbage in that acre, but outside the designated garbage area does count). Once the count becomes 5 or more, you must clear the garbage to zero.
  2. All acres, including “exempt” ones, must not have weeds outnumbering flowers by 5 or more.
  3. Non-exempt acres must have between 9-16 trees, inclusive.
  4. You must score 17 or higher “perfect acre” points, as outlined below.

Perfect acre scoring is done as follows. For each acre, including ones exempt from the strict checks above, the following evaluations are performed in this order (bailing out as soon as one condition is met):

  1. If the acre has any garbage on it, you receive no point.
  2. If weeds outnumber flowers by 3 or more, you receive no point.
  3. If the number of trees exceeds 14, you receive no point.
  4. If the number of trees is 11 or fewer, you receive one point for every two acres like this.
  5. If the number of trees is between 12-14, inclusive, you receive a full point.

“Exempt acres” refers to the following:

  1. The Wishing Well
  2. The River Pool (where fishing tourney takes place)
  3. Train Station
  4. Player Houses
  5. Museum

One caveat is that I believe the River Pool doesn’t have weeds grow there, so it sort of bypasses the rule that exempt acres must not have weed overgrowth.

So ultimately, you can have 17 fully perfect acres, or 34 nearly-perfect acres, or something in between, and you’ll achieve the perfect town. Maintain this for 15 days, and the golden axe is yours!


Animal Crossing’s Perfect Town

Nintendo’s Animal Crossing series allows humans in 2020 to dive into a fictional world and experience wild, unattainable realities like going outside regularly and interacting with other beings. So naturally, I’ve been leaving it idle in the background during quarantine and hopping in every time I have a moment to spare.

In these games, there’s a goal that you can strive for to create the perfect town, as judged by the wishing well in the town. Creating the perfect town involves removing weeds, making sure there are enough trees, making sure there aren’t too many trees, cleaning up garbage, and possibly spicing up the place with flowers? But I’ve never seen a good reference for how this is actually evaluated; just vague GameFAQs articles from 2003 that suggest there may be a scoring system.

What follows is only a half-filtered live blog of my thought process while trying to reverse engineer this mechanic.

Where To Begin

So let’s dive in. Dolphin’s debugger is pretty legit, so let’s start by searching for the strings that the wishing well gives you as an evaluation. I downloaded an old save of a “near-perfect” town, and loaded it once with the modern date so that it’s overrun by weeds (since it was uploaded nearly a decade ago), and another state loaded in its time when things were still wonderful in the world. This should give us two separate strings to search for and the ability to crawl the stack upwards and see where the string is decided.

Luckily, both strings were pretty easily found in memory while on screen because they were in a mostly-ASCII format (with some leading special characters). They seem to get copied from elsewhere to the point that they’re actually read from by this loop at 0x803c0a20:

mtctr r0 # Move size of string to counter register
cmpwi r0, 0
ble- done # Predict no branch, string is non-empty
lbz r0, 0 (r4) # Read byte from address at r4
addi r4, r4, 1 # Move r4 up
stb r0, 0 (r3) # Write byte to address at r3
addi r3, r3, 1 # Move r3 up
bdnz+ loop # Decrement, branch if non-zero, predict branch

So now before we go tracking down where r4 is calculated from, let’s do a quick experiment. Get the value at r4, and see if that address even contains the string at the beginning of this function. In my run, it was 0x81298364, and loading state and checking out that address at the start we see…

.....What errand
 have you at....
..the wishing we

So no, it’s not loaded yet; it’s still showing the prompt. So let’s track down which function call brings it in there by stepping through while watching the memory.

bl 0x8009aed0 # Nope, not this.
bl 0x803ac5c8 # Still nope.
rlwinm r3, r4, 0, 0, 26
addi r0, r5, 31
sub r31, r4, r3
addi r4, r28, 32
add r0, r31, r0
rlwinm r5, r0, 0, 0, 26
bl 0x80006c74 # Boom, it changed.

Hol’ Up

Actually, before we dive too deep and follow where the actual string copy takes place… maybe we should realign our target here. Loading up the “too many weeds” save file and prompting the wishing well, we hit this same block of code, so let’s compare the register values that gets set before this function call and see if perhaps which string to show has already been decided here.

# Too many weeds
r0 = 0xfe
r3 = 0xb4d9a0
r4 = 0x81298360
r5 = 0xe0
r31 = 0x12

# Town is mostly okay
r0 = 0xf8
r3 = 0xb4dac0
r4 = 0x81298360
r5 = 0xe0
r31 = 0x4

So r0, r3, and r31 are different and seem like ripe candidates for a string table offset/size or something along those lines. At the risk of crashing the emulator, let’s try and change them in the good case to the values used in the bad case.

Good town showing the bad town string; getting somewhere.

So the state of the town has already been decided at this point. Repeating this register-inspection as we go up the stack, we note that the calls to 0x803c0998 differ in the value of register r4, and we soon find out that 0x2c49 gives us good text and 0x2c47 gives us bad text. Just out of curiosity, can we assume that 0x2c48 is some intermediate state of disrepair?

Close, it’s the coveted perfect town string!

Okay, let’s wipe our breakpoints from the string copy since we know the computation happens outside of here. Repeating the pattern up the stack, we eventually find that the string table index is written in lwz r4, 0x0444 (r30) so let’s reload the good town, and set a memory breakpoint on where that evaluates to at 0x812982cc.

Interestingly, the first hit on that address is 0x803c00ac writing 0x2c45 (which is close, but not exact) instead of the value we expect. Continuing to the next one, we see the exact same instruction writing the expected value of 0x2c49. Curiosity strikes again, what string is 0x2c45?

It actually loads that string first in both the good and bad towns, so I guess it defaults to “you need more trees” before actually counting them? Let’s actually take this moment to compare the call stacks in the good and bad cases.

# Mostly good town
[ LR = 0x805bad48 ]
[ addr = 0x805bb244 ]
[ addr = 0x8037528c ]
[ addr = 0x8062aa18 ]
[ addr = 0x8062ab30 ]

# Weeded town
[ LR = 0x805bad48 ]
[ addr = 0x805bb244 ]
[ addr = 0x8037528c ]
[ addr = 0x8062aa18 ]
[ addr = 0x8062ab30 ]

Seems identical to me, so sadly no “this call is for good town, this call for bad town” just yet. Let’s keep climbing the stack until we stop seeing our table string offset values as the main argument being passed so we can properly discern when it’s computed.

Hitting the Callstack Ceiling

Okay, we’re at a tricky spot where we’re now stepping through functions that are called for other purposes other than this one, so we need to find the “earliest unique breakpoint” for this value getting computed and written. We do this by just hamfisting a bunch of breakpoints that come earlier than our last known one and at last, we come upon our possible first eureka moment: 0x805bad34 gets called in the good case but not the bad case. Looking around this method we can see some assembly I’ve labeled after figuring it out:

b prepare_print # We land here in bad case, skip past
lwz r0, 0x10 (sp) # We land on this in good case
li r29, 4
subfic r31, r0, 11342 # 0x2c49 gets loaded into r31
mr r3, r30
mr r4, r31
bl 0x803c00ac

Unfortunately, “stepping out” just brings us to a generic “branch to control register address” call which is used for a bunch of other code, so we can’t set a breakpoint there; we’re just going to spam some more breakpoints above this area until we find the function entry point.

First common point I’ve found is at 0x805bac94 where we have:

cmpwi r31, 4
beq- almost_good

And as you can probably guess, almost good town takes the branch, bad town doesn’t. Just for fun, let’s try different values of r31 at this point.

Seems that we’ve missed the mark for perfect town at this point, so that check may have happened earlier since values 2-5 all give “it’s fine”. Let’s try go earlier:

lwz r0, 0x10 (sp)
cmpwi r0, 6
bne- evaluation_start # The start of the comparisons above
cmpwi r3, 0
beq- evaluation_start

So here and a few times after this, we have branches to our “what’s wrong” evaluation, and there seem to be at least 5 values that it checks. Let’s try and hack all of these branches to be skipped.


I almost didn’t recognize that first string, because we’ve skipped past “your town is perfect now” to the full “you’ve maintained the perfect town for X days” one, so we’ve definitely got all the way to the finish line. Because each of the fail checks branch to the same location, it’s probably easier to reverse engineer where the value of 3 meaning “too many weeds” comes from.

Acre Inspection

I came across a block of code at 0x803a1c44 that I see being called multiple times during this evaluation… could this be a loop that checks all acres? We find the loop variable at r23 and it goes up to and including, you (maybe) guessed it, 29, the last tile index in our 5 by 6 grid of 30 acres.

Now may be a good point to start getting some values that we can recognize so that we can figure out where different checks/sums are done. I’ve found some a point in this loop at 0x803a1d48 where we have register values for A-1 such as: 0x26, 0x18, and 0x9. Removing 3 weeds, 2 flowers, and 1 tree from A-1, these values change to 0x23, 0x16, and 0x8, which maps these out pretty directly as r0 = weeds, r3 = flowers, and r4 trees. Looking a little further down we see:

add r20, r20, r4
add r19, r19, r3
addi r29, r29, 512
add r18, r18, r0

So it seems like we do some overall counting and in the end, this unkempt town has 1529 weeds, 277 flowers, and 246 trees… that seems a little crazy to me, but let’s not immediately discredit these numbers and compare against the good town: 10 weeds, 278 flowers, 247 trees. Seems to line up for the most part (I probably ran over a flower). Hacking the weed total to 0 doesn’t change the fact that it complains about them, so it must be used elsewhere.

Tracking down the memory changes, we find a loop check at 0x803a17e4 that seems to compare each tile’s type as an integer with a certain range:

cmplwi r3, 8
blt- 0x803a1804
cmplwi r3, 10
bgt- 0x803a1804
lwz r3, 0 (r24)
addi r0, r3, 1
stw r0, 0 (r24) # Add 1 to the number of weeds

Seems to accept a range of values here for a weed, 8-10. Perhaps dead trees count as weeds? Following where we’re reading those type integers from, we come upon the actual memory representation of the acre, A-1 starting at 0x81279ba8. If we edit, then leave and re-enter the acre:

So it’s a safe assumption that the leading 0x08 is a flower class and then the digit afterwards is an extra specifier. Let’s see what our 8-10 weed values represent now by placing them one after another:

Of course, ya dingus, there are three variations of weeds! Brilliant! Some more fun:

Begone fence! No, you can’t walk onto/past the tracks still, sadly.

Anyway, getting back on track. Hacking the overall weed count didn’t seem to change what the well says we have to work on, but let’s try and see where the per-acre counts are read. Once it’s gone through each tile, we see at 0x803a1980:

sub r0, r0, r4 # r0 = weeds - flowers
stw r0, 0 (r24)

Interesting, so we’ve already revealed that it seems to factor in flower count versus weed count, not just a raw weed count.


Immediately following this computation, we seem to be running through a gauntlet of checks and it’s a little difficult to figure out exactly what they all are. At a certain point, however, one of these checks does the following:

li r0, 5
srawi r5, r4, 31
rlwinm r3, r0, 1, 31, 31
subc r0, r4, r0
adde r3, r5, r3

Here, r4 contains our weeds - flowers value and r3 ends up as 1 in the bad case and 0 in the good one. This is a fun little assembly exercise if you want to try wrap your head around PowerPC, but long story short, it’s just a r3 = weeds - flowers < 5 check, so first mechanic has been reversed here; you can merely overpower the number of weeds with flowers (caveat coming later).

This was the 3rd or 4th check in the chain, so let’s try and circle back and see what the others were. The first check seems to just always return success, the second one at 0x803a19a8 is a little more interesting. r4 seems to come in as the number of trees in the acre and:

lis r5, 0x8065
rlwinm r4, r3, 1, 31, 31
lbz r0, 0x35a0 (r5)
srawi r5, r0, 31
subc r0, r0, r3
adde r3, r5, r4

The value loaded from 0x806535a0 is 8, so this is a similarly roundabout way of checking that the number of trees is more than 8. Next one, coming in again with r3 being the number of trees:

lis r4, 0x8065
srawi r5, r3, 31
addi r4, r4, 13728
lbz r0, 0x0003 (r4)
rlwinm r4, r0, 1, 31, 31
subc r0, r3, r0
adde r3, r5, r4

The value read from 0x806535a3 is 0x11, which, as you may have guessed, is our upper limit for trees (must be less than 17). Next is the weed check, and… after that, we’re done! Just as a matter of science, let’s try and check if we can trigger each of these checks to fail for A-1 in our mostly good town state.

So that about covers all the bases as far as the “what is amiss” checks, but there’s two important pieces missing: where is the check for garbage and why is this town just okay and not perfect?

The answer: A-3, along with other acres, seems to fail the “not enough trees” check and other checks, but it still says the town is “mostly okay” so this lines up with theories/guides I’ve read that suggest some tiles are exempt. Just as a test, I tried chopping down all trees in A-3 and it still said “mostly okay”.

We’ll come back to exempt acres later; for now, let’s crawl back up the stack and try to find where the values that the “how bad is it” checks get set.

Degrees of Perfection

The first check is at 0x805bac20 doing a comparison of a value on the stack at 0x812f9578 with 6. It gets set to 5 in our “mostly good town” at instruction address 0x803a2118. This ultimately happens shortly after our per-acre check, but not before another method runs with entry point 0x803a1e40. Let’s jump in there and see what it does with the 0xd that the good town passes it (the bad town passes a flat zero).

It first gets compared at 0x803a16ac with zero, then again at the same address with 2, and again with 4, 7, 12, 16 (at this point I started hacking the value so that it’d succeed just to see where this was going), and lastly with 255, which I assume we’re not supposed to go above. Let’s keep it above 16. My hunch at this point is that 0xd is the number of “perfect acres” we have and we need over 16 to have a perfect town. Let’s see what our value hacking did…

Why the tiered scoring system? Well, hacking smaller values there we get:

Well, those get really depressing. The lower ones are probably very unlikely to be seen since I’m guessing you have to fail only that many exempt acres, since non-exempt acres seem to complain more specifically. For now, let’s confirm that this 0xd value is indeed our “perfect” acre count; it seems likely given that the bad town reported 0, but let’s see where it gets set/incremented.

  1. r3 gets 0xd from r26 at 0x803a1de8.
  2. r26 gets set by adding r26 and r0 at 0x803a1d7c.
  3. r0 gets its value by dividing r25 in half at 0x803a1d6c.
  4. r26 has a value of 5 when it’s added to r0, so we get 0x8 + 0x5 = 0xd, as expected.

Let’s track down what r25and r26 represent before the final sum. r25 gets incremented at 0x803a1d30 so let’s see what conditions bring us there. For one, A-1 causes it to be incremented, so let’s step through the values there.

Some value gets loaded to r0 from the stack, but it’s zero for A-1. Perhaps garbage? We can circle back to this later.

Then our weeds - flowers difference gets loaded again and compared as less than 3, so it would seem that the threshold of 5 we had before is only for it to complain explicitly about weeds, whereas having 3 or 4 wouldn’t.

Another check follows, seemingly using the number of trees, and does another sliding scoring system by comparing whether it’s greater than 8, 11, 14, 17, and 255. This score is compared with 2 at 0x803a1d18, and if they’re equal, it increments r26, otherwise it increments r25.

Interestingly, something I didn’t catch before, but see now is that after the acre check fails, there’s a similar scoring call that merely checks an array of coordinates for exemption; it scores 1 if the tile is exempt and 0 otherwise. If it’s exempt, it does the same “stricter” scoring above to decide whether to add it to the perfect tile.

So ultimately this all pieces together as: an acre with a weed-flower difference less than 3, and between 12-14 trees scores a full point. An acre that has the correct weed-flower difference but not the perfect tree count scores half a point since r25 gets halved later before being added to r26.


One major final piece remains: is that zero-comparison we passed on before for garbage? At the start of each acre loop, we have an explicit write of zero to the address, and I never hit a write breakpoint on that address again, so it may never get changed. Where does garbage get checked, then? I caught a piece from fishing, laid it down in A-1, and duplicated it 6 times in memory.

Great, it seems to get detected here at 0x803a2098:

lbz r0, 0x1394 (r30)
cmplwi r0, 1
bne- 0x803a20d8
mr r3, r28
mr r4, r29
bl 0x803a1e70
cmpwi r3, 0

The first comparison seems to possibly be a flag for whether to bother to check for garbage (maybe as an optimization), since it’s r3 that contains the actual garbage amount. And indeed, 0x803a1e70 is a function that seems to just count garbage tiles (0x250f) and returns 7 for us. The comparison indicates it must be zero to pass.

But what about acres like the dump? Surely garbage is allowed there and not included in this sum. Throwing some into my A-2 tile seems to match this theory, and then more information arises:

Dumping a single piece of garbage into C-2 didn’t produce the garbage complaint; it merely made it fail the “perfect acre” check, confirming that the branch we were curious about above was a garbage count. Adding more garbage, however, eventually had it produce the full complaint to go clean it up. This must be what that first flag was responsible for: perhaps it only enables garbage count checks when the amount dropped exceeds a certain amount, and then doesn’t get cleared until the count drops to zero. Let’s find out when it’s set.

lbz r0, 0x1394 (r30) # The same address as our garbage flag
cmplwi r0, 0
bne -0x803a2100

Eventually, this jumps to a function at 0x803a1ae8 with a bit that loops through all acres again, summing all garbage counts to r14 and then goes something like:

cmpwi r14, 5
blt- past_flag_set
li 0, 1
addis r3, r3, 2
li r26, 0
stb r0, 0x1394 (r3)

So there’s a threshold for 5 pieces of garbage before it starts complaining, at which point you have to remove all the garbage in those acres. But why is the dump exempt, and are there any others? For one, it seems like the dump’s tiles get read explicitly before the actual “every acre” loop, so let’s step into that function at 0x805141d8.

It seems to load the exact coordinates of the dump and then compares each tile with a value of 0x583B; we’ll check what that value corresponds to later. The per-acre loop after has a comparison of the acre’s coordinates to values that sit on the stack:

lwz r0, 0x20 (sp)
cmpw r0, r28
bne- do_garbage_count
lwz r0, 0x24 (sp)
cmpw r0, r27
bne- do_garbage_count
addi r8, sp, 32
b skip_acre

Those coordinates in my case? Albert Einste– the garbage dump, at (2, 1). So indeed it appears the dump tile is a very specific exception to the garbage sum. With a caveat, of course:

Going back to that 0x583B value we saw the garbage dump loop checking for, I threw one down in A-1 and… it’s the actual visual garbage dump area. It seems to search for it so that it can note its coordinates down within the tile and compare garbage that it finds in that tile with those coordinates to determine if it’s inside or outside the actual dump area. Putting 2 pieces of garbage in the dump and 4 outside doesn’t trigger the check, but 5 outside does. Mystery solved.

How Long Though?

One thing that I could easily look up, but I figured we could just find in the code: how many days do you have to maintain perfection for, again? Let’s step back to that score = 6 check. The check for whether to give the axe is predicated based on town state being 6 and r3 being 1, which we see is set here at 0x803a1584:

li r3, 0
addi r4, r4, 25600
addis r4, r4, 2
lwz r0, 0xr180 (r4)
cmpwi r0, 15
li r3, 1

There it is. 15 days.


I lied, another main piece of the puzzle remains: which tiles are exempt from the strict complaint checks? The array used in the exemption scoring seems to suggest there are 5 in this town:

  1. (1, 3) which is C-1 which is the Wishing Well
  2. (5, 2) which is B-5 which is the River Pool
  3. (3, 1) which is A-3 which is the Train Station
  4. (3, 2) which is B-3 which is the Player Houses
  5. (5, 5) which is E-5 which is the Museum

However, stepping through the actual method that should index that array, the B-3 acre for the Houses does not return exemption for this town in the bad state. It seems there’s a value stored in r6 that dictates this, and it turns out it’s the index of the check that failed in the chain, i.e. the “issue” with the acre. Too few (index 1) or too many (index 2) trees is fine, too many weeds (index 3) is not. Just as a test, let’s fix the registers to return too many weeds in the Train Station in our otherwise good town:

There we go. All major mysteries solved! Onward to perfection, and sweet, sweet, sleep.


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.