Random Number Generation in Warframe


Disclaimer

The information included in this post is for educational purposes only. Any material on this webpage may not be reproduced, retransmitted, or redisplayed other than for personal or educational use.

Introduction

Random number generation is a process by which, often by means of a random number generator (RNG), a sequence of numbers or symbols that cannot be reasonably predicted better than by random chance is generated. Warframe uses Donald Knuth's variation of a linear congruential generator (LCG) with respect to obtaining in-game items, scaling off rarity weights that influence the drop chances of items in DropTables (enemy drop tables) and MissionDecks (mission reward tables). Note that the LCG formula only applies to mission types where players receive rewards over time, such as Defense and Survival. Mission types where players receive a reward upon completion of the mission, such as Capture and Exterminate, adhere to the process explained below and do not rely on the LCG for random number generation.

Rarity Weights

Warframe uses four rarity weights in its random number generation algorithm. These weights are uniform across DropTables and MissionDecks, except for any table that uses FixedWeights (weights that are manually assigned by the developers).

Rarity Weight Percentage
COMMON 0.755 75.50%
UNCOMMON 0.22 22.00%
RARE 0.02 2.00%
LEGENDARY 0.005 0.50%
Item Drop Chance Formula

The base drop chance of an item can be computed through the following equation, where rarity is COMMON, UNCOMMON, RARE, or LEGENDARY:

Rarity Drop Chance Per Item = Base Rarity Drop Chance / Number of Rarity Items

Exercise 1. This exercise demonstrates the formula with a test case of 8 COMMON, 6 UNCOMMON, 4 RARE, and 2 LEGENDARY items.

Data Selection: 8 COMMON, 6 UNCOMMON, 4 RARE, 2 LEGENDARY

COMMON: 0.755 / 8 = 0.094375 ≈ 9.44%
UNCOMMON: 0.22 / 6 = 0.0366666667 ≈ 3.67%
RARE: 0.02 / 4 = 0.005 = 0.5%
LEGENDARY: 0.005 / 2 = 0.0025 = 0.25%

Reversing the operation to determine the drop chance of an item from a particular rarity tier:

COMMON: 0.094375 * 8 = 0.755 = 75.5%
UNCOMMON: 0.0366666667 * 6 = 0.2200000002 ≈ 22%
RARE: 0.005 * 4 = 0.02 = 2%
LEGENDARY: 0.0025 * 2 = 0.005 = 0.5%
Normalization

Normalization refers to the division of available values such that the rarity weights of all items within a table fall between zero and one and amount to one. Normalization occurs when at least one of the rarity weights is absent from a DropTable or MissionDeck.

Exercise 1. This exercise demonstrates the normalization procedure with a test case of 1 COMMON, 1 UNCOMMON, 1 RARE, and 0 LEGENDARY items. The value of any rarity weight that applies to no items within a table is zero.

Data Selection: 1 COMMON, 1 UNCOMMON, 1 RARE, 0 LEGENDARY

COMMON: 0.755 / (0.755 + 0.22 + 0.02) = 0.7587939698 ≈ 75.88%
UNCOMMON: 0.22 / (0.755 + 0.22 + 0.02) = 0.2211055276 ≈ 22.11%
RARE: 0.02 / (0.755 + 0.22 + 0.02) = 0.0201005025 ≈ 2.01%

Adding up the normalized rarity weights:

0.7587939698 + 0.2211055276 + 0.0201005025 = 0.9999999999 ≈ 1
Bias

Bias is a variable exclusive to DropTables that unequally weighs items within DropTables, even if the relevant items have identical rarity weights. Bias can be applied to specific items within a particular DropTable but is not globally applied across any. The more bias an item has (larger value), the lower its drop chance. Conversely, the less bias an item has (smaller value), the higher its drop chance. Additionally, because bias scales off the rarity weight the item it is impacting has, the drop chance of items with rarity weights that hold more weight will be reduced more drastically than the drop chance of items with rarity weights that hold less weight. However, items with rarity weights that hold more weight will still tend to drop more often than items with rarity weights that hold less weight, depending on the amount of bias that is present.

Specter Mod Drop Chance Bias Count Observed
Feyarch Specter Shotgun Amp 45.83% 0.05 22 52.38%
Feyarch Specter Empowered Blades 4.17% 0 3 7.14%
Feyarch Specter Final Harbinger 45.83% 0.1 14 33.33%
Feyarch Specter High Noon 4.17% 0 3 7.14%
Specter Mod Drop Chance Bias Count Observed
Knave Specter Pistol Amp 45.83% 0.05 60 53.57%
Knave Specter Growing Power 4.17% 0 4 3.57%
Knave Specter Blind Justice 45.83% 0.1 43 38.39%
Knave Specter Crimson Dervish 4.17% 0 5 4.46%
Specter Mod Drop Chance Bias Count Observed
Orphid Specter Stand United 30.56% 0.05 29 27.62%
Orphid Specter Brief Respite 30.56% 0 51 48.57%
Orphid Specter Atlantis Vulcan 30.56% 0.1 17 16.19%
Orphid Specter Crossing Snakes 8.33% 0 8 7.62%
Attenuation

Attenuation is a variable exclusive to DropTables. The boolean OverrideLevelAdjustedBiasAtten determines whether attenuation is present within a DropTable or not. As the value of attenuation increases, the drop chance of an item should decrease. However, due to the insignificance of its set value (0.5, which comes from one of the boolean's secondary variables, RareAttenMax) and the fact that it impacts DropTables globally rather than individually across items, it is impossible to determine if it has a noticeable effect at all.

Reward Seeds

rewardSeed is a variable exclusive to MissionDecks that determines the missionReward players receive at the end of a mission. It is a 64-bit signed integer (range: −9,223,372,036,854,775,808 through 9,223,372,036,854,775,807 decimal). rewardSeeds are given to the host, and members of the host's group receive the sessionId in order to participate in the same matchmaking session. Players will only receive a rewardSeed when their client needs to distribute it to other players in a group (as the host). This means that players will receive a rewardSeed if they begin a Public, Friends Only, or Invite Only session, but if they begin a Solo session, then they will not be given a rewardSeed. Despite the SRand variable (the seeder for the pseudo-random number generator) differing across each player, each player will always receive the same missionReward as the host because of their identical sessionIds.

Sample Code
Reward Seed Generator

The following C++ code generates random 64-bit signed integers (corresponding to the fixed-width integer type of rewardSeeds). For randomness, it uses the 64-bit version of the Mersenne Twister pseudo-random number generator initialized with a non-deterministic random number as a seed.

#include <cstdint>  // Provide fixed-width integer types
#include <iostream> // Provide input and output facilities
#include <limits> // Allow access to numeric limits of data types
#include <random> // Provide facilities for random number generation

// Function to generate list of random 64-bit signed integers
void generateRewardSeeds(int numIntegers) {
// Create random device to obtain high-entropy 32-bit seed
std::random_device rd;

// Combine two 32-bit seeds from random device into single 64-bit seed for improved randomness
int64_t seed = static_cast<int64_t>(rd()) << 32 | rd();

// Initialize Mersenne Twister random number engine with 64-bit seed from random device
std::mt19937_64 rng(seed);

// Define uniform distribution range for 64-bit signed integers
std::uniform_int_distribution<int64_t> dist(
std::numeric_limits<int64_t>::min(),
std::numeric_limits<int64_t>::max()
);

// Display generated seed
std::cout << "mt19937_64 seeded with: " << seed << "\n\n";
// Display number of generated random 64-bit signed integers
std::cout << "Generating " << numIntegers << " random 64-bit signed integers:" << "\n";

// Generate and print random integers
for (int i = 0; i < numIntegers; ++i) {
// Generate random number using distribution and random number engine
int64_t rewardSeed = dist(rng);
std::cout << "rewardSeed=" << rewardSeed << "\n";
}
}

int main() {
// Define number of random integers to generate
int numIntegers = 5;

// Call function to generate and print random integers
generateRewardSeeds(numIntegers);

return 0;
}
Weighted Random Number Generator

The following C++ code simulates the process of drawing items with different rarities according to their specified rarity weights. For randomness, it uses the 64-bit version of the Mersenne Twister pseudo-random number generator initialized with a non-deterministic random number as a seed. The initial rarity weights set correspond to the rarity weights used in Warframe's random number generation algorithm.

#include <algorithm>     // Provide functions for operations on ranges of elements
#include <cstdint> // Provide fixed-width integer types
#include <iostream> // Provide input and output facilities
#include <random> // Provide facilities for random number generation
#include <stdexcept> // Provide standard exception classes for error handling
#include <vector> // Define the std::vector container for encapsulating dynamic arrays
#include <unordered_map> // Define the std::unordered_map container for fast key-value pair storage and retrieval

// Enum for rarities
enum Rarity {
COMMON,
UNCOMMON,
RARE,
LEGENDARY
};

// Structure to hold number, rarity, and rarity weight
struct RarityItem {
int number; // Number representing item
Rarity rarity; // Rarity of item
double rarityWeight; // Weight associated with rarity
};

// Function to generate weighted random number
int getWeightedRandomNumber(const std::vector<RarityItem>& rarityItems) {
if (rarityItems.empty()) {
throw std::runtime_error("Rarity items vector is empty.");
}

// Vector to store cumulative rarity weights
std::vector<double> cumulativeRarityWeights(rarityItems.size());
cumulativeRarityWeights[0] = rarityItems[0].rarityWeight; // Initialize first element

// Compute cumulative rarity weights
for (size_t i = 1; i < rarityItems.size(); ++i) {
cumulativeRarityWeights[i] = cumulativeRarityWeights[i - 1] + rarityItems[i].rarityWeight;
}

double totalWeight = cumulativeRarityWeights.back(); // Total weight is last cumulative weight
std::random_device rd; // Create random device to obtain high-entropy 32-bit seed
int64_t seed = static_cast<int64_t>(rd()) << 32 | rd(); // Combine two 32-bit seeds from random device into single 64-bit seed for improved randomness
std::mt19937_64 rng(seed); // Initialize Mersenne Twister random number engine with 64-bit seed from random device
std::uniform_real_distribution<> dist(0.0, totalWeight); // Define uniform distribution from 0 to totalWeight
double randomNumber = dist(rng); // Generate random number using distribution and random number engine

// Find position of random number in cumulative rarity weights
auto it = std::lower_bound(cumulativeRarityWeights.begin(), cumulativeRarityWeights.end(), randomNumber);
return rarityItems[std::distance(cumulativeRarityWeights.begin(), it)].number; // Return corresponding number
}

int main() {
// Vector of rarity items with associated rarity weights
std::vector<RarityItem> rarityItems = {
{1, COMMON, 0.755},
{2, UNCOMMON, 0.22},
{3, RARE, 0.02},
{4, LEGENDARY, 0.005}
};

const int numTrials = 1000; // Number of trials to run
// Map to count occurrences of each rarity
std::unordered_map<Rarity, int> rarityCounts = {
{COMMON, 0},
{UNCOMMON, 0},
{RARE, 0},
{LEGENDARY, 0}
};

// Run trials to generate random numbers and count rarities
for (int i = 0; i < numTrials; ++i) {
int number = getWeightedRandomNumber(rarityItems); // Generate weighted random number
for (const auto& item : rarityItems) { // Find corresponding rarity
if (item.number == number) {
++rarityCounts[item.rarity]; // Increment count for that rarity
break;
}
}
}

// Display number of trials and rarity counts for each rarity
std::cout << "Number of trials: " << numTrials << "\n\n";
std::cout << "COMMON: " << rarityCounts[COMMON] << "\n";
std::cout << "UNCOMMON: " << rarityCounts[UNCOMMON] << "\n";
std::cout << "RARE: " << rarityCounts[RARE] << "\n";
std::cout << "LEGENDARY: " << rarityCounts[LEGENDARY] << "\n";

return 0;
}