Random Number Generation in Warframe
April 17, 2016
Last Modified: September 1, 2024
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;
}