I watched a fantastic analysis of the Super Mario Bros 3 roulette game: [SMB3 Roulette & Card Matching Games Explained - YouTube](https://www.youtube.com/watch?v=QGeLzCmUDDk)
Very cool video, I loved its presentation. What it skipped was giving you a sense of your chance of getting any particular item in a given row. And I was a little curious about how it worked.
# Overview
Each row has a pattern of (and I'm choosing an arbitrary starting point here): Mushroom, Flower, Mushroom, Starman.
In rows 1 and 2, you get 19 frames of Mushroom, 19 of Flower, 19 of Mushroom, 19 of Starman, and then you repeat. In row 3, you get 16 frames of each, based on that video
So I wrote some code (see below) that calculated the item you'd end up selecting for any given frame and given the range of random delay frames, for each row.
Much of this is based on the data from the video. I also checked the assembly, and did some work on checking the speeds for each row to try to match this to the offset ranges shown in the video, but have not completed the work.
# Row 1
Depending on what frame you hit (relative to the start of the Mushroom -> Flower -> Mushroom -> Starman sequence), you can guarantee a mushroom by pressing on frames **6-9** or **44-47** (all 0-based frame counts).
There are only 4 frames on which you can guarantee a flower, and only 4 for a starman. (He says 3 — one of us is off-by-one, and I _think_ it's him, based on looking at his math).
Chances increase as you approach those blocks of frames, and reduce as you move away from them. There are exactly 4 frame in which you have a 50/50 chance between two items.
If you map this out, it looks like this.
![[Pasted image 20250226151753.png]]
# Row 2
Your chances on row 2 top out at just shy of 60% for any given item, with your biggest chances of course being mushrooms (there's 2 of them, as opposed to 1 flower, 1 starman). At any point, you're between ~40-60% chance of getting a mushroom.
![[Pasted image 20250226151825.png]]
# Row 3
Things get both more simple and less predictable in row 3.
Given the wide range of randomness in the delay, at any point you have a 50% chance of a mushroom, 25% of a flower, 25% of a starman. So it seems that, short of there being another factor involved in the randomness that he missed, you can't time this.
We probably remember the times we felt we really nailed it, and forget the times we missed.
If we went for mushrooms, we had a higher chance of nailing it more often.
![[Pasted image 20250226151929.png]]
The delay works out to anywhere between 1 to 2 full cycles after you press the button. (Minimum 1 full cycle, maximum 2). The randomness would place you somewhere in-between.
# Randomness
The video states that the random value used for rows 2 and 3 doesn't change once the game starts. The thing I'd be curious about is whether there are particular "random" values that are likely to be set once you go into the game. If so, it could drastically alter what those graphs look like, and maybe make it much easier to hit the star more often.
And the reason that could be possible is that, as the video states, there are multiple random number slots used for different things throughout the game (different enemies and such). Rows 2 and 3 each use different random number slots than row 1, and different things may affect those slots. As the game plays, the random numbers for row 1 changes, but 2 and 3 do not.
If there's anything _else_ in SMB3 that might end up handling random number generation differently that would help seed particular ranges of values once you got into roulette, then that could mean a higher chance of more helpful ranges of random numbers for rows 2 and 3. But the video doesn't go into anything there.
# Simulation Code
The following simulates your chances of any given match based on the frame the button is pressed for rows 1, 2, and 3. The results are CSV data you can place in a spreadsheet.
```python
#!/usr/bin/env python3
from collections import Counter
SPRITES = ('mushroom', 'flower', 'mushroom', 'starman')
NUM_SPRITES = len(SPRITES)
def compute(
*,
row: int,
frame_width: int,
rand_range: tuple[int, int],
) -> None:
"""Generate CSV output showing chances of matches based on the frame.
This shows, for every frame, the chances of getting a particular item
when pressing the button. This considers the
"""
total_frames = frame_width * NUM_SPRITES
frame_targets: dict[int, Counter[str]] = {}
for start_frame in range(total_frames):
start_frame_targets: Counter[str] = Counter()
frame_targets[start_frame] = start_frame_targets
for rand_frame in range(rand_range[0], rand_range[1] + 1):
end_frame = (start_frame + rand_frame) % total_frames
end_sprite = SPRITES[int(end_frame / frame_width)]
start_frame_targets[end_sprite] += 1
print(f'== Row {row} ==')
print('frame,sprite,%s' % ','.join(
f'{sprite}_hits,{sprite}_pct'
for sprite in sorted(set(SPRITES))
))
for frame, vals in frame_targets.items():
stat_totals = sum(vals.values())
frame_stats: list[str] = []
for sprite in sorted(set(SPRITES)):
stat_hits = vals[sprite]
frame_stats += [
str(stat_hits),
str(stat_hits / stat_totals)
]
sprite = SPRITES[int(frame / frame_width)]
print(f'{frame},{sprite},{",".join(frame_stats)}')
print()
compute(row=1,
frame_width=19,
rand_range=(32, 47))
compute(row=2,
frame_width=19,
rand_range=(32, 63))
compute(row=3,
frame_width=16,
rand_range=(64, 127))
def test_speed(
*,
speed: int, # 4.4FP
pos_frac: int = 3,
) -> None:
"""Simulates the scroll positions for the given row speeds.
This computes the X scroll position across the row of items
based on a row's speed. Mario 3 uses a pixel counter for
display and a high-precision counter for button events.
"""
pos_x = 0
pos_hi = 0 # High-precision
print()
print('== Test speed: %s ==' % format(speed, '#010b'))
for i in range(120):
temp2 = speed
temp2 <<= 4
temp2 &= 0xFF
temp1 = speed
temp1 >>= 4
if temp1 & 0x00001000:
temp1 |= 0x11110000
temp1 &= 0xFF
temp3 = 0
if temp1 & 0x10000000:
temp3 = 0xFF
else:
temp3 = 0
pos_frac += temp2
if pos_frac > 0xFF:
c = 1
else:
c = 0
pos_frac &= 0xFF
pos_x += temp1 + c
pos_hi += temp3 + c
print(pos_x, pos_hi)
test_speed(speed=0x70) # Row 1
test_speed(speed=0x90) # Row 2
test_speed(speed=0x7f) # Row 3
```