RNG in RBY Link Battles

Shellnuts

Rustiest Player Around
is a Forum Moderatoris a Community Contributor
Moderator
Hello, so I'm making this post here as a follow up to my previous thread on sleep mechanics that I made to give the correct results about how sleep RNG works in RBY, and after some delay, I'm now able to report back some of the findings I have about the sleep distribution and damage rolls. Before I get into this however, I would like to first explain RNG works in RBY (both for link-battles and in general) so everyone is on the same page.

How Generation 1 Generates Random Numbers

There are two different ways the game generates random numbers in Generation 1, which are both used in completely different situations. The first random number generator used in Generation 1 is the is the one used at most points in the games code; it is used for deciding if an encounter will happen while walking through tall grass, how much damage attacks will do against wild Pokémon or Enemy Trainers, whether a Poké Ball will capture something or not, and — relevant for our purposes here — it is used to generate the starting values for the random-number-generator which is used in link battles. A more comprehensive explanation of how this form of random number generation works and how it affects things like Poké Ball catch rates can be found here, but to briefly summarize how it works:
  • There is are 4 values that this random number generator uses, these are:
    • An 8-bit value called rDiv which increments once every 256 clock cycles, in reality these are just the highest 8 bits of a 16 bit timer that increases once every machine cycle and begins incrementing as soon as the game starts.
    • The carry bit, this value is used to signify whether we are carrying a 1 or not while performing addition or subtraction of two numbers. This is useful for checking if a value is above some specified threshold or whether the result of adding two numbers was greater than 255.
    • Two 8-bit values called the add and subtract bytes, these store the two random values that were last calculated by the random number generator.
  • Whenever the RNG subroutine is called, the game will add the value of rDiv and the carry bit to the add byte and subtract the value of rDiv and the carry bit from the subtract byte. These two bytes are then used as the random values generated. Normally in Generation 1, it only uses the value in the add byte, but in some cases it will also use the value in the subtract byte as well (when it needs 16-bit random numbers).
    • Note: In Generation 2 the calculation performed is identical however it instead uses just the subtract byte when it needs an 8-bit random number instead of the add byte.
  • This random function called once every frame in addition to being called whenever needed to try and randomize things as much as possible.
The second way of generating random numbers — and the one more important for us — is the random number generator used for link battles — which I will refer to as Link RNG. When writing the code for a link battle, the designers had to solve a simple problem; how can you generate random numbers during a link battle in a way that is synchronized between the two Gameboys, using code that is simple to write and debug (remember, link battles were only added in the last few weeks of development) while giving fairly random results.

Their solution to this problem was to have one of the two Gameboys connected by the link cable generate an array of 9 random values (it technically generates 10 values but only uses the first 9 of them for some reason, I really have no idea why this is the case at all), which are then shared between the two Gameboys. Once those 9 random numbers have been generated, the Link RNG will then iterate over that array whenever it needs a random number; once it hits the end of the array it then will multiply every one of the values by 5 and add 1 (modulo 256) before going back to the start of the array. This process of multiplying your previous random number by something and then adding a constant to get a new random number is a super common way for generating random numbers in games (among other applications) and has been given it's own special name: a Linear Congruential Generator (LCG for short). For reasons that are beyond the scope of this discussion, we can verify that the LCG used by the Gameboy has a period of 256, which means that after cycling through the array 256 times, you will end up with the same array of random numbers you started with.

An alternative way of describing how the Link RNG works is to imagine a queue of 9 numbers, whenever a random number is needed during a battle, the Link RNG will generate a new number by multiplying the value at the front of the queue by 5 and adding 1 to it, which it then sends to the back of the queue, before returning the value at the front of the queue as the random number it generated. This description of how the RNG works is much simpler to understand and will be helpful later when interrogating the output of our brute-forcer.

When starting a link battle, unless we have additional information about how the system works or a specific RNG manipulation is performed, we have to assume all possible values of the carry bit, add and subtract bytes, and system timer are valid and are relevant to our calculation. Furthermore, since we aren't specifying exactly how many times in the battle random numbers were generated before some particular RNG call of interest (and since perfectly-deterministic cart-accurate RNG is unlikely to be implemented in showdown) we will assume that from that initial seed all of the 256 different configurations of the array of random numbers are equally likely, and that we are equally likely to start pulling random numbers at any point in that array. From this, we find out that — if we were to use a brute-force approach for simulating the probabilities of various numbers of sleep turns and damage rolls and whatnot — we would have to simulate around 19.79 trillion possible states (there are 2 * 256 * 256 * 65536 possible starting states for the Link RNG which could have been called up to 256 * 9 times before the results repeat), which while technically feasible to perform, isn't very cheap computationally. However, there are a couple of factors that reduce this number dramatically.

  1. Every assembly instruction on the Gameboy takes a multiple of 4 machine cycles to perform, this means that at the start of every instruction the lower 2 bits of the system timer must remain constant, reducing the number of possible starting seeds by a factor of 4.
  2. The carry bit is always reset by a XOR operation before generating a link battle (source), and by comparing the random number generated to see if it's less than the link battle preamble byte, it always either sets or resets the carry bit between calls of the RNG subroutine depending solely on the value of the add byte (specifically it sets the carry bit if the add byte is less than 253 and resets it otherwise). This makes the initial state of the carry bit constant, further reducing the number of possible starting seeds by a factor of 2.
  3. Since the only way that the subtract byte can at all influence the result of the add byte in any way is by how it changes the carry bit, which we already mention is then changed when it does the comparison against 253, the subtract byte is now completely irrelevant for how it generates random numbers in link battles, further reducing the number of starting seeds by a factor of 256.
All combined, this cuts down the number of simulations we need to perform by a factor of 2048, bringing us down to around 9.664 billion possible states. 9.664 billion states is well-within my capabilities to perform a brute-force simulation to determine the probabilities of damage rolls, the number of sleep turns, or the chances of secondary effects (such as Body Slam paralyzing it's target). The code for the brute-force simulator can be found in in the spoilered section below, which has been double-checked by Enigami.

Python:
import math
import numba as nb
import numpy as np
import random

ARR_LENGTH = 9

# ==========================================================================================================================================
@nb.jit(boundscheck=False, fastmath=True)
def gen_init_vals_rby(slots, timer, rAdd):
    # I will be referring to the add and subtract bytes as rAdd and rSub here
    carry = 0
    # carry is set to 0 before any link battles take place from the XOR operation:
    # https://github.com/pret/pokeyellow/blob/master/engine/link/cable_club.asm#L19
    ii = 0

    CUTOFF_VAL = 253
    # NOTE:
    # These are just numbers of cycles between RNG calls for Pokemon Yellow, Pokemon Red/Blue takes 4 more cycles than Yellow does
    # because it uses different bank-switching code.
    INC_NO_JP = 472
    INC_JP = 452

    while ii < ARR_LENGTH:
        # initial 9 values for battle RNG are generated by following function:
        # https://github.com/pret/pokered/blob/master/engine/math/random.asm
        rDiv = timer >> 8
        rAdd += rDiv + carry
        # carry from this calculation or the calculation involving rSub doesn't matter since it is always set/reset by the comparison
        # against 253 to see if it needs to re-roll rng, so we don't need to write logic simulating it here.
        rAdd &= 255
  
        # checking if the value is less than the preamble byte (253) https://github.com/pret/pokered/blob/master/engine/link/cable_club.asm#L44
        # if so then we add it to the array and increment the index by 1, otherwise we jump back to the top of the loop
        if rAdd < CUTOFF_VAL:
            slots[ii] = rAdd
            ii += 1
            carry = 1
            timer += INC_NO_JP
        else:
            timer += INC_JP
            carry = 0
        timer &= 65535

    return slots
# ==========================================================================================================================================
@nb.jit(boundscheck=False, fastmath=True)
def cycle_rng(slots):
    # this performs the x5+1 operation that the RNG uses
    # once all 9 values in the array have been cycled through
    for ii in range(ARR_LENGTH):
        slots[ii] = (5 * slots[ii] + 1) & 255
# ==========================================================================================================================================
# to reduce memory footprint, I am going to be compressing how I handle the distribution of secondary effect calls with the following function
# numbers used in this are from https://www.smogon.com/rb/articles/rby_mechanics_guide
@nb.jit(boundscheck=False, fastmath=True)
def get_effect_rng_index(idx):
    if idx < 26:
        # All secondary effects would have occurred
        return 0
    elif idx < 52:
        # All secondary effects with a >10% chance would have occurred
        return 1
    elif idx < 77:
        # All secondary effects with a >20% chance would have occurred
        return 2
    elif idx < 85:
        # All secondary effects with a >30% chance would have occurred
        return 3
    elif idx < 103:
        # All secondary effects with a >33% chance would have occurred
        return 4
    # No secondary effects would have occurred.
    return 5
# ==========================================================================================================================================
@nb.jit(boundscheck=False, fastmath=True)
def get_acc_sleep_distrib(num_cycles, slots, timer_init, add_init, counts, tot_inserted):
    # note: counts is a 256x8 array where the value at index ii,jj stores the number of times
    # that with an accuracy roll [0,255] of ii that a mon was put to sleep for jj turns [0, 7]

    slots = gen_init_vals_rby(slots, timer_init, add_init)
    # generating the initial RNG distribution
    queue_length = 0
    ii = 0
    acc_queue = np.empty(11, dtype=np.int_)
    while True:
        for jj in range(ARR_LENGTH):
            # note that it always checks for accuracy first, then for sleep (if successful)
            if ii < num_cycles:
                acc_queue[queue_length] = slots[jj]
                queue_length += 1
      
            if jj == ARR_LENGTH - 1:
                slp_roll = (5 * slots[0] + 1) & 255
            else:
                slp_roll = slots[jj+1]
            slp_roll &= 7
            # If the Random Number generated for sleep turns mod 8 is not zero then it will put them to sleep for that many turns,
            # otherwise it will generate another value and try again.
            # https://github.com/pret/pokered/blob/master/engine/battle/effects.asm#L35

            if slp_roll != 0:
                for acc_idx in acc_queue[:queue_length]:
                    counts[acc_idx, slp_roll] += 1
                tot_inserted += queue_length
                queue_length = 0
      
            if queue_length == 0 and ii >= num_cycles:
                return tot_inserted

        cycle_rng(slots)
        ii += 1
# ==========================================================================================================================================
@nb.jit(boundscheck=False, fastmath=True)
def get_crit_dmg_acc_effect_distrib_rby(num_cycles, slots, timer_init, add_init, counts, queue, tot_inserted):
    slots = gen_init_vals_rby(slots, timer_init, add_init)
    queue_length = 0
    ii = 0
    while True:
        for jj in range(ARR_LENGTH):
            if ii < num_cycles:
                queue[queue_length] = slots[jj]
                queue_length += 1
     
            # First doing an crit roll, then if the damage roll afterwards is within the valid range [217, 255], if it passes then it gets accuracy
            # and effect rolls, otherwise it will check the next damage roll and so-on until it finds one in the range.
      
            if jj == ARR_LENGTH - 1:
                dmg_roll = (5 * slots[0] + 1) & 255
            else:
                dmg_roll = slots[jj+1]
      
            if dmg_roll >= 217:
                # if within the valid damage roll range then getting the accuracy and secondary effect rolls
                dmg_idx = dmg_roll - 217
                if jj >= ARR_LENGTH - 2:
                    acc_idx = (5 * slots[jj - ARR_LENGTH + 2] + 1) & 255
                else:
                    acc_idx = slots[jj+2]
          
                if jj >= ARR_LENGTH - 3:
                    effects_idx = (5 * slots[jj - ARR_LENGTH + 3] + 1) & 255
                else:
                    effects_idx = slots[jj+3]
          
                effects_idx = get_effect_rng_index(effects_idx)
          
                for crit_idx in queue[:queue_length]:
                    counts[crit_idx, dmg_idx, acc_idx, effects_idx] += 1
                tot_inserted += queue_length
                queue_length = 0
      
            if queue_length == 0 and ii >= num_cycles:
                return tot_inserted
      
        cycle_rng(slots)
        ii += 1
# ==========================================================================================================================================
@nb.jit(boundscheck=False, fastmath=True)
def iterate_over_crits_attack_rolls(slots, add_init, counts, queue, tot_inserted):
    for timer_init in range(16384):
        tot_inserted = get_crit_dmg_acc_effect_distrib_rby(256, slots, timer_init * 4, add_init, counts, queue, tot_inserted)
        if tot_inserted % (256*ARR_LENGTH) != 0:
            break
    return counts, tot_inserted
# ==========================================================================================================================================
@nb.jit(boundscheck=False, fastmath=True)
def iterate_over_sleep_rolls(slots, add_init, counts, tot_inserted):
    for timer_init in range(16384):
        # Gameboy instructions always take multiples of 4 cycles to execute.
        # So instead of the full 65536 possible initial values, this instead can only be one of 16384 initial values.
        #
        tot_inserted = get_acc_sleep_distrib(256, slots, timer_init * 4, add_init, counts, tot_inserted)
        if tot_inserted % (256*ARR_LENGTH) != 0:
            break
    return counts, tot_inserted
# ==========================================================================================================================================
def get_rng_distrib_attacks_crits(file_path: str):
    # NOTE: 1st dimension of array gives crit roll, second gives damage roll in range [217, 255],
    #       3rd gives accuracy roll, 4th tells us which secondary effects would have happened
    counts = np.zeros((256, 39, 256, 6), dtype=np.int_)
    slots = np.empty(ARR_LENGTH, dtype=np.int_)
    queue = np.empty(256, dtype=np.int_)
    tot_inserted = 0
    for add_init in range(256):
        counts, tot_inserted = iterate_over_crits_attack_rolls(slots, add_init, counts, queue, tot_inserted)
        if tot_inserted % (4194304*ARR_LENGTH) != 0:
            print(tot_inserted)
            raise Exception("Incorrect number of iterations performed!")
    np.save(file_path+"_rby", counts)
# ==========================================================================================================================================
def get_rng_distrib_sleep(file_path: str):
    counts = np.zeros((256, 8), dtype=np.int_)
    slots = np.empty(ARR_LENGTH, dtype=np.int_)
    tot_inserted = 0
    for add_init in range(256):
        counts, tot_inserted = iterate_over_sleep_rolls(slots, add_init, counts, tot_inserted)
        if tot_inserted % (4194304*ARR_LENGTH) != 0:
            print(tot_inserted)
            raise Exception("Incorrect number of iterations performed!")
    np.save(file_path+"_rby", counts)
# ==========================================================================================================================================
#
# The results of these calculations can be analyzed by performing sums over the various axes of the arrays and then
# performing divisions. For example, if we want to analyze the damage roll distribution of Snorlax using Hyper Beam if it crits,
# then we would perform the calculation as follows (noting that total_counts is a 4d array which stores the number of times each specific
# combination of crit roll, damage roll, accuracy roll, and which secondary effects proc'd at each index):
#
# snorlax_base_speed = 30
# hyper_beam_accuracy = 90
#
# crit_idx = snorlax_base_speed // 2
# acc_idx = math.floor(255 * 90 / 100)
# effect_idx = 6
#
# # Since Hyper Beam has no secondary effects, we just sum over that full axis (length 6),
# # in case we were dealing with say, Body Slam's paralysis chance, we would write:
# # effect_idx = get_effect_rng_index(math.floor(255 * 30 / 100) + 1)
# #
#
# effects_sum_counts = np.sum(total_counts[:, :, :, :effect_idx], axis=3)
# crit_counts = np.sum(effects_sum_counts[:crit_idx], axis=0)
#
# # If we wanted to get damage rolls if Snorlax doesn't crit, we would instead write something like:
# # nocrit_counts = np.sum(effects_sum_counts[crit_idx:], axis=0)
# # And if we wanted to get distribution of damage rolls regardless of if Snorlax crits or not, we would write something like:
# # total_roll_counts = np.sum(effects_sum_counts[], axis=0)
#
# crit_nomiss_counts = np.sum(crit_counts[:,:acc_idx], axis=1)
# tot_num_crit_nomiss = np.sum(crit_nomiss_counts)
# dmg_distrib = 100 * crit_nomiss_counts / tot_num_crit_nomiss
#

Results:

While I haven't gotten around to testing how everything works just yet, some of the results I have found can be summarized below:
  • Every damage roll of the form 217 + 5k is around 20% less likely than the other damage rolls (occurring around 1.97-1.99% of the time while the others occur at least 2.5% of the time or more)
  • The chances that secondary effects occur is higher than what is on showdown. Most significantly, the chances for Body Slam to paralyze a target and the chance for Psychic are around 1% higher than the current probabilities listed on showdown (around 31.4496% and 34.4502% or so as opposed to 30.0781% and 33.2031% on PS currently), and the change for Ice Beam to freeze a target are around 0.6% higher (around 10.7459% instead of 10.1562% on PS currently).
    • These probabilities are affected by how accurate the move is (for one example, the chance for Blizzard to freeze something if it hits a target is 10.8137% which is higher than Ice Beam's 10.7459%).
    • These probabilities are also correlated with Critical Hits, since whether a move crits or not is determined before the game performs damage rolls, accuracy checks, and rolls for secondary effects. This can lead to some significant changes in the probabilities listed. For example if Snorlax Body Slam's an opponent, it will it's target 35.3884% of the time if it lands a critical hit and 31.2043% of the time if it doesn't land a critical hit. These two values average out to the 31.4496% figure previously listed (note that accuracies of moves also slightly vary depending on whether they will crit or not, which affects these calculations slightly).
  • 2 turn sleep is slightly more likely than all other numbers of sleep turns (occurring around 15.1% of the time), though at low accuracies this gets highly skewed in favor of 2-turn sleeps (with it occurring 20.72% of the time or more when you have a 1/256 chance to hit the opponent with sleep).
    • In GSC similar behavior also appears to be present with regards to sleep rolls (though skewed towards the higher end instead with a 17.4% chance for max sleep turns and 16.9% for 5 turn sleep) however I haven't had my code simulating GSC sleep rolls audited yet, so take these results with a grain of salt.
  • Misses occur at different rates than on Pokémon Showdown (100% accuracy moves hit 99.6150% of the time instead of 99.6094% of the time for example, Thunder hits 69.4490% of the time instead of 69.5312% of the time, and so-on)
Verifying our Results:

This next part is primarily aimed at those with some reservations about how accurate the results given by the brute-forcer are (which, given my previous mistakes, is warranted). As of the time of writing this, I have been able to prove that every damage roll of the form 217 + 5k where k is an integer in the range [0, 7] must occur less frequently than other damage rolls, and with the use of a slightly simplified model of how the Link RNG works, I am able to arrive at values for the frequency that those damage rolls should occur and how frequently secondary effects should occur; both of which give values that agree with the results obtained by the brute-forcer. I haven't yet proven why sleep rolls are distributed the way they are, though hopefully I will be able to achieve this at some point in the near future.

The main reason for all of these odd results in the damage rolls (and therefore, why it skews secondary effect probabilities) is because of how the game generates a random number for damage rolls. In order to get a valid number used for damage rolls, it will generate a random number using the Link RNG, if that number is greater than 216 it will use it in the damage calculation, otherwise it will throw it out and try again, repeating until it gets a number greater than 216. This however, ends up causing problems once it cycles through the entire queue at least once since we now know that those values were all less than 217, changing the probability distribution to something decidedly less uniform. Using some back-of-the-envelope calculations, this happens (1 - 39/256)^9 ~ 22.59% of the time. After generating a valid random number for damage rolls, the game will then generate random numbers for the accuracy roll and (depending on the move) for the secondary effect chance as well.

To start this off, let us focus on proving that the damage rolls of the form 217 + 5k should occur less frequently than other damage rolls. To prove this, we first note that there is an LCG that inverts the standard "x5+1" LCG which is of the form f(n) = 205 n - 205 mod 256 (see below for proof). If we calculate the values of this inverse LCG for all n in the range [217, 255] we find that the result is also in the range [217, 255] if and only if n is of the form 217 + 5k.

Noting that n < 256 we have:
f(5n+1 mod 256) = 205 (5n+1 mod 256) - 205 mod 256 = 205 * 5n mod 256 = 1025 n mod 256 = n mod 256 = n

In simple terms, the n'th random number generated for a damage roll is of the form 217 + 5k if and only if the n-10'th number generated would be a valid damage roll as well.

Therefore, if we assume that it generates n random numbers for a single damage roll where n > 9, where the first n-1 numbers it generates are all not in the range for a valid damage roll and the n'th number it generated is a valid damage roll of the form 217 + 5k, then we know that the n-9th random number generated must have been in the range [217, 255] which would have been accepted since it is greater than 216, thus it would be impossible for the n'th value generated to be of the form 217 + 5k if n > 9 since it would have stopped early! Proving the statement if the Link RNG has generated 9 random numbers for a damage roll, subsequent values generated for the same damage roll will never be of the form 217 + 5k. Using near-identical reasoning we are able to also deduce that if the n'th random number generated was a damage roll not of the form 217 + 5k then the n-9'th random number could not have been a valid damage roll; thus after at least 9 random numbers have been generated by the Link RNG, all other values for damage rolls are still possible. Thus we can say for certain that damage rolls of the form 217 + 5k occur less frequently on cartridge relative to other possible damage rolls.

Furthermore, if we make the approximation that the first 9 values generated are uniformly distributed random integers in the range [0, 255], then we can calculate the probability that 1 specific damage roll out of the 39 valid ones is chosen in the first 9 Link RNG rolls we get:
Code:
P[Specific Damage Roll |  ≤ 9 Damage Rolls required] = (1/39) * (39/256) * [ 1 + (1-39/256) + (1-39/256)^2 + ... + (1-39/256)^8 ]
                                                      = (1/256) * [ 1 + (217/256) + (217/256)^2 + ... + (217/256)^8 ]
                                                      = (1/256) * [ 1 - (217/256)^9 ] / [ 1 - (217/256) ]
                                                      = [1-(217/256)^9 ] / 39
                                                      = 3655432360969436823399 / 184172292831916163334144
                                                      = 0.01984789...

Thus we would expect those specific damage rolls to occur 1.985% of the time or so, if we look at the actual distribution of the damage rolls (neglecting the effects of accuracy) obtained from the brute-forcer then we get the following sequence of probabilities (note that the top-left value is the percent of the time that it rolls 217, increasing as you go down and to the right, so the bottom-left value is the percent of the time you get a max-roll of 255):
Code:
1.9771 2.5257 2.7707 2.7824 2.7755 1.9790  2.7432 2.5286 2.7822 2.7782 1.9749 2.6968 2.7735
2.5292 2.7812 1.9813 2.7710  2.6901 2.7713 2.7738 1.9772 2.7804 2.7667 2.6994 2.7798 1.9791
2.5227 2.7756 2.7762 2.691 0 1.9744 2.7819 2.5270  2.7778 2.7721 1.9821 2.7464 2.7771 2.5271
Looking at the percent of the time the damage rolls of the form 217+5k occur specifically, we get this other sequence of probabilities:

1.9771 1.9790 1.9749 1.9813 1.9772 1.9791 1.9744 1.9821

All of these values are very close to our estimated probability of 1.985%, thus it is reasonable to conclude that the deviations from that 1.985% chance are due to the non-uniformity in how it initially generates random values which we neglected in our approximation. Therefore we can confidently treat the results of our brute-forcer for damage rolls as correct.

Using the same approximation that the first 9 randomly generated values are uniformly distributed random numbers in the range [0, 255], we can also try and figure out how likely secondary effects are to occur (specifically focusing on Body Slam paralysis chances). Since we know that the RNG roll for secondary effects occurs 2 cycles after the RNG roll for damage, then if we assume the move always hits, and noting that of the 217 values that the RNG could spit out that are rejected for damage rolls — after applying the LCG — 186 of them will still be rejected for damage rolls and 74 of them will paralyze the opponent; then out of the 186 that are still rejected then — after applying the LCG again — 161 will still be rejected for damage rolls and 65 will paralyze the opponent; then out of those 161 then — after applying the LCG yet again — 140 are still rejected for damage rolls and 59 will paralyze, and lastly, out of those 140 then — after applying the LCG one more time — 121 are rejected while 50 will paralyze; we can calculate the approximate probability that it will paralyze (up to 5 terms) with the following formula:

Code:
P[Body Slam Paralysis] ~ P[Body Slam Paralysis | ≤ 7 Random Numbers Generated] * P[Generates ≤ 7 Random Numbers]
                         + P[Body Slam Paralysis | > 7 and ≤ 16 Random Numbers generated] * P[Generates > 7 and ≤ 16 Damage Rolls]
                         + P[Body Slam Paralysis | > 16 and ≤ 25 Random Numbers generated] * P[Generates > 16 and ≤ 25 Random Numbers]
                         + P[Body Slam Paralysis | > 25 and ≤ 34 Random Numbers generated] * P[Generates > 25 and ≤ 34 Random Numbers]
                         + P[Body Slam Paralysis | > 34 and ≤ 43 Random Numbers generated] * P[Generates > 34 and ≤ 43 Random Numbers]
                       = (77/256) * [ 1 - (217/256)^7 ]
                         + (74/217) * (217/256)^7 * [ 1 - (217/256)^2 (186/217)^7 ]
                         + (65/186) * [ (217^2 * 186^7) / (256^9) ] * [ 1 - (186/217)^2 (161/186)^7 ]
                         + (59/161) * [ (186^2 * 161^7) / (256^9) ] * [ 1 - (161/186)^2 (140/161)^7 ]
                         + (50/140) * [ (161^2 * 140^7) / (256^9) ] * [ 1 - (140/161)^2 (121/140)^7 ]
                       = 0.3138...
which is approximately 31.4%, which is exactly in line with the numbers we gave earlier for Body Slam paralysis chances from our brute-forcer, giving us further confidence in our numbers our brute forcer's results are correct.

Once again, thank you to Enigami for checking over the code and verifying that it should be correct. I'm sorry for the confusion caused by me posting the previous thread before I fully understood how the RNG worked. Feel free to ask any questions to me about this or leave any corrections to these results below, take care and have a good rest of your day.
 
Last edited:
Hello, Sorry to once-again post here, but there are some corrections I have been made aware of to my original post as well as some notes on how GSC RNG works that I would like to document here as well.
  • While the game does generate 10 random numbers when initializing the Link RNG, as per this line of code here, apparently it only uses the first 9 of them while cycling through the queue. All numbers and calculations in the previous post have been updated.
  • In GSC the RNG works almost identically as it does in RBY with the same requirement the first 10 random numbers generated must be less than 253 (it also only uses the first 9 of them like in RBY), there are some differences though between how it and RBY generate random numbers which will be listed below:
    • GSC takes far less cycles to call the Random subroutine, only taking 156 cycles per call instead of 432 (so 196/176 if the branch fails or succeeds respectively).
    • GSC will use the subtract byte instead of the add byte when it needs a single 8-bit random value. This change makes brute-forcing GSC randomness much harder to do since you now can't neglect the subtract byte and have to account for the interactions between it and the carry-bit (increasing the number of states you need to simulate by a factor of 256).
If we were to write it in Python, the GSC initial random subroutine would appear as follows:

Python:
import numba as nb
import numpy as np

# ==========================================================================================================================================

ARR_LENGTH = 9

@nb.jit(fastmath=True)
def gen_init_vals_gsc(slots, timer, rAdd, rSub):
    # GSC randomness is mostly the same as RBY randomness when it generates the initial 10 values for a Link Battle
    # It also has the carry bit set to 0 before generating the numbers, still won't allow for values to be used that exceed 252,
    # and uses the same formulae (though it takes way less time between calls). It just has one tiny change between the two versions, where
    # it instead uses rSub instead of rAdd for single-byte random values. That's not a big deal, that just multiplies the
    # amount of work I need to do for one brute-force simulation by a factor of 256, no big deal at all...
    # https://github.com/pret/pokecrystal/blob/master/home/random.asm

    carry = 0
    # carry is set to 0 before any link battles take place in GSC as well.
    #
    ii = 0

    # NOTE: These are just numbers of cycles between accesses to rDiv in pokemon crystal
    # I need to also account for the amount added between accesses of rDiv during the Random subroutine since I cannot neglect rSub here unlike in RBY
    CUTOFF_VAL = 253
    INC_NO_JP = 152
    INC_JP = 132
    INC_BTWN_ADD_SUB = 44


    while ii < ARR_LENGTH:
        rDiv = timer >> 8
        rAdd += rDiv + carry
        if rAdd > 0xFF:
            carry = 1
        else:
            carry = 0
        rAdd &= 0xFF
        timer += INC_BTWN_ADD_SUB
        timer &= 0xFFFF
        rDiv = timer >> 8

        rSub -= rDiv + carry
        # don't need to handle carry from this, which is a small win I guess...
        rSub &= 0xFF
        if rSub < CUTOFF_VAL:
            slots[ii] = rSub
            ii += 1
            carry = 1
            timer += INC_NO_JP
        else:
            timer += INC_JP
            carry = 0
        timer &= 0xFFFF
    return slots

Additional corrections and follow up notes will be added to this reply as needed.

27/07/2024: Added additional information about how critical hits affect the probabilities of different secondary effects occurring, I thought I had included this previously but I guess I forgot to add it previously.

30/08/2024:

So I actually made a pretty big oversight in my code for simulating damage rolls. In the code when it generates random numbers, I missed the fact that on the random numbers generated for calculating critical hit rates and damage rolls (and nowhere else in the code used for the battle engine, which I suspect is why I missed it) it will perform bit-rotations on the numbers generated before using them for determining the odds of critical hit rates. Specifically for the number used for a critical hit it will perform 3 left bit rotations before using the number to check if a move will crit or not, and for damage rolls it performs a single right bit rotation before checking if the number is a valid damage roll or not.

This single change ends up changing the results of damage rolls and status effect rolls significantly, bringing them more in-line with a uniform damage roll distribution and the stated in-game effect chances. Now the least-likely damage-rolls (which are now 217, 221, 224, 227, 230, 233, 236, 239, 242, 246, 249, 252, and 255) which occur around 2.46-2.47% of the time, which does line up with an updated version of our previous approximation (since those damage rolls only are possible for the first 2 times the random number generator passes through the array, we can calculate the estimated probability those damage rolls occur to be around 2.4667% of the time, falling in-line with the numbers we obtained).

However, sadly, this doesn't seem to hold up when we try and calculate the probabilities for secondary effects occuring such as Body Slam paralysing the target or Ice Beam freezing, which occur 30.0609% and 10.1982% of the time respectively. From testing I performed using Monte-carlo simulations, I end up at the probabilities I calculated by hand (for Body Slam it occurs 30.65% of the time or so) when the entire queue is set to random numbers between 0 and 252, while simulations where I only randomize the initial values of the clock and add bits end up giving probabilities that fall in-line with the results obtained by the brute-forcer, indicating that the effects of how it was initially seeded are affecting the distribution in a big way. This also makes the influence of accuracy on secondary effects negligibly small.

For those curious to see the actual distribution of damage rolls (as percentage chances), here they are:
Code:
[[2.4617 2.6276 2.611  2.6255 2.4597 2.6172 2.6069 2.4692 2.6145 2.6156 2.4592 2.6255 2.6061]
[ 2.4653 2.5963 2.6249 2.4597 2.6141 2.6102 2.467  2.6178 2.6085 2.4578 2.6232 2.606  2.47  ]
[ 2.616  2.6179 2.6015 2.4655 2.6179 2.6269 2.4579 2.6147 2.6162 2.4734 2.6058 2.6095 2.4561]]

Some other things I noticed while going through the code and updating my brute forcer are:
  • In RBY and GSC game checks for whether you hit yourself while confused before it checks for paralysis, so a paralyzed pokemon still hits itself 50% of the time when confused.
  • Due to an off-by-one error in RBY and GSC, the chance a Pokemon fully paralyzes are actually 63/256.
  • All secondary effects in GSC have a 1/256 lower chance of them occuring if they were in RBY due to an off-by-one error.
  • On slower Pokemon, Critical hits increase the odds of secondary effects occurring by around 0.5% or so on average, with certain damage rolls increasing the odds by over 1%. This effect still exists for faster Pokemon but is less pronounced.
  • While paralyzed, due to some bizzare RNG jankiness, the odds of all secondary effects occurring when a Pokemon gets hit seem to drop by around 0.2% or so, and the damage roll distribution changes a fair amount to the following distribution.
Code:
[[2.5094 2.459  2.6596 2.6635 2.5067 2.6665 2.4665 2.516  2.6603 2.6517 2.5084 2.4597 2.6433]
[ 2.5108 2.6116 2.6608 2.3275 2.6273 2.6477 2.5148 2.6324 2.465  2.5037 2.6342 2.6406 2.5166]
[ 2.4503 2.6555 2.6492 2.511  2.6521 2.492  2.5032 2.6521 2.6653 2.5192 2.4271 2.6563 2.5035]]

At some point in the future I will try simulating GSC damage rolls and figuring out how they are distributed and how much secondary effects occur and whatnot, however it will take some time due to multithreading being annoying to work with and GSC using a lot more macros, but that will come later.
 
Last edited:
So by my maths there's 22 bits of entropy that you would need to recreate this on cart?

The major stumbling block being the variable number of rng calls for damage rolls, but I'm guessing that probably isnt that hard to deal with, just annoying to code?

If I were someone I would change showdown to use cart rng (with the values published in the UI obviously). It's time to turn the game on its head again.
 
Back
Top