RBY Link Battle Initial RNG - An Unnecessary, Incredibly Extra Look Into Yet Another Cart Accuracy Moment

Sabelette

from the river to the sea
is a Site Content Manageris a Community Contributoris a Top Contributoris a Forum Moderator Alumnus
First off, huge thanks to Shellnuts for helping me understand a whole lot of assembly instructions because I was literally looking up assembly as I dug through code to try to figure this shit out.

With that out of the way, story time! Yesterday on the RBY Discord, it came up that the first 10 random numbers (RNs) in a link battle are slightly rigged. Essentially, the first 10 RNs cannot be higher than 252, in a range that normally goes from 0-255, for 256 total numbers. This is not new - this has often been parroted in the RBY Discord and at various points on the forums, but nobody seemed to actually know where this idea came from, why it would be true, or have any proof of it being true. ABR was interested in the topic because well, this would mean you cannot miss a move due to a 255 roll on turn 1 (and often not on turn 2 either) because the number 255 cannot occur in the first 10 numbers of the link battle RNG, among other things. Allegedly, anyway.
1707969870132.png

1707969961255.png


Author's note: it was not, in fact, documented.

From there on, everyone in the conversation was pretty interested in where the proof was and whether this could be implemented, and so I started off by checking out the channels of some of RBY's famous glitch hunters/technical divers - Crystal_, TheZZAZZGlitch, ChickasaurusGL. Nothing. 13 years of videos, not a peep of this. On the forums, I could find people saying it but claiming it was the first 9 RNs, not 10, and of course, not a shred of proof there either. Not even a dead link to some old-ass forum or anything, just people saying it with supreme confidence. Ultimately, I decided to dive into the decomp to figure out where this came from and if it was real, as well as trying to figure out exactly what uses up RNs. Here's the initial questions I set out with.

1707970118768.png


I'll start off with what I've confirmed about RNs as of now, as that's probably the less interesting part:
1. RNs are called when the action occurs - if you try to attack but you paralyze, you don't call RNs for things like damage roll and crits
2. Sleep calls a single RN for duration
3. Damage rolls call RNs till they get a number between 217 and 255 - this is for the damage randomness. There are 39 possible rolls as we know, each linked to the numbers from 217-255, so a damage roll can use up any amount of RNs till it gets one in that range. Woo, headache!
4. Speed ties, confusion, paralysis, each of these use 1 RN. Stuff like Stoss does not waste RNs on a crit roll.
5. Multihit (2-5 hit) and partial trap moves call 2 RNs to determine number of hits.

After... quite some effort, I found some relevant bits of code for the 10 RNs thing:
1707970369623.png
1707970376732.png

if only we could all "pop af"

Isn't assembly... fun? You don't need to understand any of this to get to the point of this post, this was just me finding a reference to the fact that link battles use a different random number list than in-game stuff, which seemed to me like a good lead that there's something funky going on there. Anyway, we now could confirm 10 initial RNs are created, even if I couldn't figure out how tf those were generated and if they were truly random, or what.

The next couple finds, which I cannot explain to you remotely, were these:
1707970642489.png
1707970587579.png

To give you an idea of how out of my depth I was here, these were what I was consulting to get some sort of grasp on the assembly instructions:
http://z80-heaven.wikidot.com/instructions-set
http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf

To do my best here: In hexadecimal, FD = 253. FE = 254, FF = 255. This gave me a hunch that the screenshot on the left had something to do with why you couldn't get an RN above 252 initially, but I had less than zero proof; I just figured if FD was signaling the start of an array of bytes and FE was signaling the end and FF was signaling there was no connection, you probably don't want the game sending over FE prematurely or something and fucking up the data transfer. The bit on the right helped reinforce this in yet more ways I am not code-savvy enough to explain. This is where Shellnuts, the GOAT, comes in. Shell comes in and drops a pseudocode explanation of what's going on with the link battle RN list for my tiny brain:

BattleRandom Pseudocode:

Load contents at memory location wLinkState into A register and check if you are in a link battle
If you are not in a link battle, jump to the Random function (located in the "math" folder in the PokeRed decomp).

Push the contents of the HL and BC registers to the stack (saving the contents
of these registers for later use).

Load contents at memory location wLinkBattleRandomNumberListIndex into the A and C registers and 0 into the B register.

Load the value for the address wLinkBattleRandomNumberList into the HL register.
Add the value in the BC register to the HL register and store it in the HL register.
Increment the value in the A register and store that value into memory at the address wLinkBattleRandomNumberListIndex
Compare the contents of the A register with 9.
Load the value at the address pointed to by the contents of the HL register into A
Retrieve the values you stored on the stack for the HL and BC registers

Some stuff I am unsure about involving virtual console hooks and returns for reasons I don't understand.

Push the contents of all registers to the stack

Bitwise XOR A with itself (sets it to zero and clears the carry bit in 1 instruction) and store that value into memory at the address wLinkBattleRandomNumberListIndex

Load the value for the address wLinkBattleRandomNumberList into the HL register.
Load 9 into the B register
Perform the following loop:
Load the value in the HL register into A register and multiply it by 5
Add 1 to the value in the A register
Store the value in the A register into memory at the address pointed to by the value in the HL register
Increment the HL register
Decrement B by 1, if B is nonzero then jump to the top of the loop.

Retrive the values for all the registers that you pushed to the stack.
Return from Function Call.
And then Shell found the goldmine.

; This is called after completing a trade.
CableClub_DoBattleOrTradeAgain:
ld hl, wSerialPlayerDataBlock
ld a, SERIAL_PREAMBLE_BYTE
ld b, 6
.writePlayerDataBlockPreambleLoop
ld [hli], a
dec b
jr nz, .writePlayerDataBlockPreambleLoop
ld hl, wSerialRandomNumberListBlock
ld a, SERIAL_PREAMBLE_BYTE
ld b, 7
.writeRandomNumberListPreambleLoop
ld [hli], a
dec b
jr nz, .writeRandomNumberListPreambleLoop
ld b, 10
.generateRandomNumberListLoop
call Random
cp SERIAL_PREAMBLE_BYTE ; all the random numbers have to be less than the preamble byte
jr nc, .generateRandomNumberListLoop
ld [hli], a
dec b
jr nz, .generateRandomNumberListLoop
ld hl, wSerialPartyMonsPatchList
ld a, SERIAL_PREAMBLE_BYTE
ld [hli], a
ld [hli], a
ld [hli], a
ld b, $c8
xor a
.zeroPlayerDataPatchListLoop
ld [hli], a
dec b
jr nz, .zeroPlayerDataPatchListLoop
ld hl, wGrassRate
ld bc, wTrainerHeaderPtr - wGrassRate
.zeroEnemyPartyLoop
xor a
ld [hli], a
dec bc
ld a, b
or c
jr nz, .zeroEnemyPartyLoop
ld hl, wPartyMons - 1
ld de, wSerialPartyMonsPatchList + 10
ld bc, 0
.patchPartyMonsLoop
inc c
ld a, c
cp SERIAL_PREAMBLE_BYTE
jr z, .startPatchListPart2
ld a, b
dec a ; are we in part 2 of the patch list?
jr nz, .checkPlayerDataByte ; jump if in part 1
; if we're in part 2
ld a, c
cp (wPartyMonOT - (wPartyMons - 1)) - (SERIAL_PREAMBLE_BYTE - 1)
jr z, .finishedPatchingPlayerData
.checkPlayerDataByte
inc hl
ld a, [hl]
cp SERIAL_NO_DATA_BYTE
jr nz, .patchPartyMonsLoop
; if the player data byte matches SERIAL_NO_DATA_BYTE, patch it with $FF and record the offset in the patch list
ld a, c
ld [de], a
inc de
ld [hl], $ff
jr .patchPartyMonsLoop
.startPatchListPart2
ld a, SERIAL_PATCH_LIST_PART_TERMINATOR
ld [de], a ; end of part 1
inc de
lb bc, 1, 0
jr .patchPartyMonsLoop
.finishedPatchingPlayerData
ld a, SERIAL_PATCH_LIST_PART_TERMINATOR
ld [de], a ; end of part 2
call Serial_SyncAndExchangeNybble
ldh a, [hSerialConnectionStatus]
cp USING_INTERNAL_CLOCK
jr nz, .skipSendingTwoZeroBytes
; if using internal clock
; send two zero bytes for syncing purposes?
call Delay3
xor a
ldh [hSerialSendData], a
ld a, START_TRANSFER_INTERNAL_CLOCK
ldh [rSC], a
call DelayFrame
xor a
ldh [hSerialSendData], a
ld a, START_TRANSFER_INTERNAL_CLOCK
ldh [rSC], a
.skipSendingTwoZeroBytes
call Delay3
ld a, (1 << SERIAL)
ldh [rIE], a
ld hl, wSerialRandomNumberListBlock
ld de, wSerialOtherGameboyRandomNumberListBlock
ld bc, SERIAL_RN_PREAMBLE_LENGTH + SERIAL_RNS_LENGTH
vc_hook Wireless_ExchangeBytes_RNG_state_unknown_Type5
call Serial_ExchangeBytes
ld a, SERIAL_NO_DATA_BYTE
ld [de], a
ld hl, wSerialPlayerDataBlock
ld de, wSerialEnemyDataBlock
ld bc, SERIAL_PREAMBLE_LENGTH + NAME_LENGTH + 1 + PARTY_LENGTH + 1 + (PARTYMON_STRUCT_LENGTH + NAME_LENGTH * 2) * PARTY_LENGTH + 3
vc_hook Wireless_ExchangeBytes_party_structs
call Serial_ExchangeBytes
ld a, SERIAL_NO_DATA_BYTE
ld [de], a
ld hl, wSerialPartyMonsPatchList
ld de, wSerialEnemyMonsPatchList
ld bc, 200
vc_hook Wireless_ExchangeBytes_patch_lists
call Serial_ExchangeBytes
ld a, (1 << SERIAL) | (1 << TIMER) | (1 << VBLANK)
ldh [rIE], a
ld a, SFX_STOP_ALL_MUSIC
call PlaySound
ldh a, [hSerialConnectionStatus]
cp USING_INTERNAL_CLOCK
jr z, .skipCopyingRandomNumberList ; the list generated by the gameboy clocking the connection is used by both gameboys
ld hl, wSerialOtherGameboyRandomNumberListBlock
.findStartOfRandomNumberListLoop
ld a, [hli]
and a
jr z, .findStartOfRandomNumberListLoop
cp SERIAL_PREAMBLE_BYTE
jr z, .findStartOfRandomNumberListLoop
cp SERIAL_NO_DATA_BYTE
jr z, .findStartOfRandomNumberListLoop
dec hl
ld de, wLinkBattleRandomNumberList
ld c, 10
.copyRandomNumberListLoop
ld a, [hli]
cp SERIAL_NO_DATA_BYTE
jr z, .copyRandomNumberListLoop
ld [de], a
inc de
dec c
jr nz, .copyRandomNumberListLoop
.skipCopyingRandomNumberList
ld hl, wSerialEnemyDataBlock + 3

The important bit is here:
.generateRandomNumberListLoop
call Random
cp SERIAL_PREAMBLE_BYTE ; all the random numbers have to be less than the preamble byte
jr nc, .generateRandomNumberListLoop
ld [hli], a
dec b
jr nz, .generateRandomNumberListLoop
ld hl, wSerialPartyMonsPatchList
ld a, SERIAL_PREAMBLE_BYTE
ld [hli], a
ld [hli], a
ld [hli], a
ld b, $c8
xor a

Remember the "preamble byte" screenshot above? My hunch was dead on. They reserve the last 3 values - FD/FE/FF or 253/254/255 - to signal the start and end of the initial data sent across the link cable for a battle, and for the error if there's no connection.

Shell also found something else that was very important. We all just assumed the RNG would reroll those numbers till it got something acceptable, like how the damage roll RNG call works, and so the first 10 rolls were constricted uniformly to a range of 0-253. This is not accurate:
1707971079424.png


This means 2 things:
1. Only moves with 100% actually gain accuracy; said moves gain, well, a 1/256 boost to accuracy. 242 corresponds to the threshold for 95% accurate moves like Razor Leaf; only a roll of 241 or below hits, so the 242/247/252 chances don't help at all.
2. Damage rolls are slightly lower on average if they use one of the first 10 RNs! This can slightly affect things like the chance of causing a recovery failure in the first couple turns. Screencapping myself in the midst of a ramble:
1707972073551.png


So, TLDR? This often-cited thing is true, but it does not give Hypnosis or Lovely Kiss an accuracy boost like everyone assumed. It would remove 256s on turn 1 (at least for whoever attacks first - who the fuck knows how many RNs a damage roll actually burns, could be 1 or could be 50 on any given roll), and it would slightly mess with damage rolls sometimes, but it really has almost no practical effect. With that, I think we have 3 options here:

1. Ignore this and keep playing Pokemon Showdown RBY exactly as before. This is probably what's gonna happen.
2. Partially implement this: Count RNs and block 1/256 misses until 10 RNs are used. Probably not gonna happen, but more likely than option 3.
3. Fully implement this, including fucking around with the damage rolls on turn 1 to make the game like, 0.1% more cart accurate. Somehow I think this is not gonna fly if we couldn't even make Sleep Clause cart accurate.

Anyway, I just wanted to tell a story about something that drove me nuts trying to figure out and share what is ultimately fairly useless RBY mechanics research, because I think it matters to just document this somewhere so the next time it comes up, we have some sort of reference to point to.
1707971511578.png


Hope you all enjoyed a strange bit of mechanics research I decided to embark on for zero reason. Thanks to Shellnuts and Enigami for contributing their technical knowledge to figuring out how some of this weird-ass stuff works.
 

Attachments

pre

pkmn.cc
On the forums, I could find people saying it but claiming it was the first 9 RNs, not 10, and of course, not a shred of proof there either. Not even a dead link to some old-ass forum or anything, just people saying it with supreme confidence. Ultimately, I decided to dive into the decomp to figure out where this came from and if it was real, as well as trying to figure out exactly what uses up RNs. Here's the initial questions I set out with.
I'm curious why you would dismiss SnowyTotodile's research when it agrees with the comment in the code in your screenshot ("if we picked the last seed, we need to recalculate the nine seeds", emphasis mine) and the most straightforward reading of the code? I would suggest setting up a memory watch in bgb or similar during a link battle to watch the wLinkBattleRandomNumberList location to verify for yourself - I'm sure a video here would help put any future debates on this topic to rest. :)

1. Ignore this and keep playing Pokemon Showdown RBY exactly as before. This is probably what's gonna happen.
2. Partially implement this: Count RNs and block 1/256 misses until 10 RNs are used. Probably not gonna happen, but more likely than option 3.
3. Fully implement this, including fucking around with the damage rolls on turn 1 to make the game like, 0.1% more cart accurate. Somehow I think this is not gonna fly if we couldn't even make Sleep Clause cart accurate.
Pokémon Showdown emphatically does not implement cartridge correct RNG in literally any generation, period. To quote the PS FAQ (emphasis mine):

Pokémon Showdown uses the exact same type of RNG as the actual Pokémon games on the Switch and 3DS use.
To quote in detail from my own research:

The pkmn engine aims to match the cartridge's RNG frame-accurately, in that provided with the same initial seed and inputs it should produce the same battle playout as the Pokémon Red cartridge. Pokémon Showdown doesn't correctly implement frame-accurate RNG in any generation, and along with the bugs discussed earlier this results in large differences in the codebase. Because the pkmn engine aims to be as compatible with Pokémon Showdown as possible when in -Dshowdown compatibility mode, the implications of these differences are outlined below:
  • RNG: Pokémon Showdown uses the RNG from Generation V & VI in every generation, despite the seeds and algorithm being different. Pokémon Red uses a simple 8-bit RNG with 9 distinct seeds generated when the link connection is established, whereas Pokémon Showdown uses a 64-bit RNG with a 32-bit output.
  • Algorithm: As detailed in the table below, the algorithm used by Pokémon Showdown in the places randomness is required is often different than on the cartridge, so even if Pokémon Showdown were using the correct RNG the values would still diverge (including using a completely incorrect distribution for multi-hit moves).
  • Bias: Pokémon Showdown often needs to reduce its 32-bit output range to a smaller range in order to implement various effects, and does so using a biased integer multiplication method as opposed to debiasing via rejection sampling to ensure uniformity as is done on the cartridge. This means that certain values are fractionally more likely to be chosen than others, though this bias is usually quite small (e.g. in the case of Metronome instead of selecting moves with an equal 1/163 chance, Pokémon Showdown selects some with a 1/2**32 greater chance than others).
  • Order of operations: RNG calls effectively introduce something similar to a "memory barrier" in that they must be sequenced correctly (though operations which occur between them may happen in any order). Pokémon Showdown violates this by introducing additional operations (see below) and changing up the order of existing operations (e.g. choosing to check for hit/miss, determine the number of hits for moves with multiple hits, determine if a move hit critically and then the damage instead of checking for hit/miss and determining number of hits after the other two). While it's often desirable to rearrange code for performance or to improve readability this can only be done if it doesn't affect accuracy.
  • Speed-ties: In addition to breaking switch-in speed ties with an RNG call, speed ties in Pokémon Showdown actually result in a large number of spurious frame advancements due to the internal implementation details of Pokémon Showdown's event and "action" systems. Ultimately, without keeping track of all of the handlers for the internal events Pokémon Showdown invents and implementing the same "action" system, matching Pokémon Showdown's RNG in the presence of speed ties proves impossible, and thus the reference Pokémon Showdown implementation pkmn engine aims to match has been patched to change this behavior.
  • Effects: Pokémon Showdown occasionally incorrectly inserts RNG calls in move effect handlers when they're not relevant:
    • Roar / Whirlwind roll to hit and can "miss" as opposed to simply failing
Finally, the initial 9-byte seed for link battles on Pokémon Red can't include bytes larger than the SERIAL_PREAMBLE_BYTE, so must be in the range [0,252]. This has implications on the first 9 random numbers generated during the battle and has non-trivial competitive implications (at the start of the battle move effects become more likely, the "1/256-miss" glitch can't happen, Player 1 is more likely to win speed ties, etc) that Pokémon Showdown can't replicate due to everything described earlier.
The initial seed RNG mechanics are only relevant in the context of RNG frame-accuracy which Pokémon Showdown is nowhere near being able to implement, so you don't really have an option other than (1). I personally believe RNG accuracy is important (though I almost certainly don't get it right everywhere myself), but if I felt like being charitable there would possibly be an argument to be made that given how weak the Generation I & II RNG is and how easy it is to reverse that for online play Pokémon Showdown's alternative RNG semantics are at least tolerable for ensuring the game more faithfully recreates the spirit of cartridge play.
 

Sabelette

from the river to the sea
is a Site Content Manageris a Community Contributoris a Top Contributoris a Forum Moderator Alumnus
I didn’t dismiss any research, my search through the forums just didn’t turn that post up at all. I’m sure you can imagine how hard it is to search words like “RNG,” “PRNG,” etc alongside “Gen 1” or “RBY” or what have you and actually turn up anything related to this instead of page after page of random posts! A video sounds like a good idea and I’m open to doing that when I’ve got some time.

Thank you for the links, getting a little more detail on all of this is helpful!
 

Hipmonlee

Have a nice day
is a Community Contributoris a Senior Staff Member Alumnusis a Smogon Discord Contributor Alumnusis a Tiering Contributor Alumnusis a Top Contributor Alumnusis a Battle Simulator Moderator Alumnusis a Four-Time Past WCoP Champion
Someone check my maths, but I think this means that a Chansey crit TBolt would require an absolute max roll (or to cycle through a lot of failed damage rolls) to OHKO a Starmie on the first turn. Actually, I think it needs a 252 roll, so I guess there would be two chances to get it.

Any other very close damage rolls that would be significantly harder on t1 because of this?
 
i would be really intrested in this being implemented in any way into showdown especialy for lower tiers the first 10 turns are so masivly important so less chance for hax at the start would be great (i like the 3rd idea more but the second idea is also ok)
 

Sabelette

from the river to the sea
is a Site Content Manageris a Community Contributoris a Top Contributoris a Forum Moderator Alumnus
This would not affect the first 10 turns, it would affect the first 1-2 turns and often only the first move; this also only reduces the chance of one specific and extremely rare form of bad luck
 

Users Who Are Viewing This Thread (Users: 1, Guests: 0)

Top