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.

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?

Bam! 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, unless you believe in good dice juju!

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.