This note explains how to manually calculate the probability distribution of Wynncraft crafted item roll values.

Notation

  • Let be the number of ingredients.
  • For each ingredient (where ):
    • : Minimum base roll (minValue).
    • : Maximum base roll (maxValue).
    • : Boost percentage (boost).
    • : Effectiveness multiplier, converting the boost into a decimal factor.

Step 1: Generate base ingredient roll values

By “base” we unboosted ingredient roll values.

For each ingredient , generate a vector of 101 base values, linearly spaced from to .

These represent the possible unboosted “roll values” distributed uniformly.

Define the base value for ingredient at index :

So,

Step 2: Compute boosted ingredient roll values

For each base value , calculate the boosted roll by applying the effectiveness multiplier , rounding to the nearest integer, and then flooring the result:

rounds to the nearest integer.

Note that is a natural number, i.e.,

Step 3: Compute roll probability mass function (PMF) for each ingredient

Let be a vector of boosted ingredient roll values for ingredient .

For each ingredient , compute the PMF of the boosted rolls.

We do this by counting the occurrences of each boosted roll value in :

Since there are 101 base values, the denominator is 101, assuming a uniform distribution over .

Represent as a vector of length , where:

Step 5: Compute Total Roll Range

The total roll is the sum of the boosted rolls from all ingredients:

where is the random variable representing the boosted roll of ingredient , with PMF .

The minimum and maximum possible total rolls are the sums of the individual minima and maxima:

The total number of possible roll values is:

Step 6: Compute PMF of Total Roll Using Convolution

Since the ingredients’ contributions are independent, the PMF of the total roll is the convolution of the individual PMFs:

Convolution Definition

For two discrete PMFs and with supports starting at and , the convolution is:

where the sum is over all where both PMFs are defined. In vector form, if and are the PMF vectors of lengths and , the result has length , and:

Iterative Convolution

  1. Initialize: Start with , a unit impulse at roll 0 (length 1).
  2. For each ingredient :
    • Convolve with to update .
    • Adjust the support: After convolving with , the new minimum roll is the previous minimum plus .
  3. Repeat: Continue until all PMFs are convolved.

After convolving all PMFs, has length:

The final PMF is:

Final Result

The probability mass function gives the probability of each total roll from to .

This can be stored as a function associating each roll value with its probability .

Example

Suppose ingredients:

  • Ingredient 1: , , ().
  • Ingredient 2: , , ().
  1. Base Values:

    • : .
    • : .
  2. Boosted Rolls:

    • : Compute , e.g., to .
    • : Compute , e.g., to .
  3. PMFs:

    • from to .
    • from to .
  4. Total Range: , .

  5. Convolution: Compute to get probabilities from to .

Code example

package com.fazuh.faz.wynn.util;
 
import com.fazuh.faz.wynn.model.IngredientIdentification;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
/**
 * Class to calculate the probability distribution of an identification's roll when crafting an item.
 */
public class CraftedRollProbability {
    private final List<IngredientIdentification> ingredients;
    private final List<double[]> ingredientProbDists;
    private int minRoll;
    private int maxRoll;
    private final Map<Integer, BigDecimal> rollPmfs;
 
    /**
     * Constructor for CraftedRollProbability.
     * @param ingredients list of ingredients of the crafted item
     */
    public CraftedRollProbability(List<IngredientIdentification> ingredients) {
        this.ingredients = ingredients;
        this.ingredientProbDists = new ArrayList<>();
        this.minRoll = 0;
        this.maxRoll = 0;
        this.rollPmfs = new HashMap<>();
 
        calculateIngredientProbabilities();
        calculateRollProbabilities();
    }
 
    /**
     * Get the minimum possible roll value.
     * @return the minimum possible roll value
     */
    public int getMinRoll() {
        return minRoll;
    }
 
    /**
     * Get the maximum possible roll value.
     * @return the maximum possible roll value
     */
    public int getMaxRoll() {
        return maxRoll;
    }
 
    /**
     * Get the probability mass functions of the rolls.
     * @return the probability mass functions of the rolls
     */
    public Map<Integer, BigDecimal> getRollPmfs() {
        return rollPmfs;
    }
 
    /**
     * Get the ingredients of the crafted item.
     * @return the ingredients of the crafted item
     */
    public List<IngredientIdentification> getIngredients() {
        return ingredients;
    }
 
    private void calculateIngredientProbabilities() {
        for (IngredientIdentification ing : ingredients) {
            double ingStatEff = (ing.getBoost() + 100) * 0.01;
 
            // Calculate ingredient probability distribution
            double[] ingBaseValues = linspace(ing.getMinValue(), ing.getMaxValue(), 101);
            int[] ingRollsBoosted = new int[ingBaseValues.length];
 
            // Calculate boosted rolls
            int minBoostedRoll = Integer.MAX_VALUE;
            int maxBoostedRoll = Integer.MIN_VALUE;
            for (int i = 0; i < ingBaseValues.length; i++) {
                int boostedValue = (int) Math.floor(Math.round(ingBaseValues[i]) * ingStatEff);
                ingRollsBoosted[i] = boostedValue;
                minBoostedRoll = Math.min(minBoostedRoll, boostedValue);
                maxBoostedRoll = Math.max(maxBoostedRoll, boostedValue);
            }
 
            // Calculate offset and occurrences
            int offset = -minBoostedRoll;
            int[] occurrences = new int[maxBoostedRoll - minBoostedRoll + 1];
            for (int roll : ingRollsBoosted) {
                occurrences[roll + offset]++;
            }
 
            // Calculate probability distribution
            double[] probDist = new double[occurrences.length];
            for (int i = 0; i < occurrences.length; i++) {
                probDist[i] = occurrences[i] / 101.0;
            }
 
            ingredientProbDists.add(probDist);
            minRoll += minBoostedRoll;
            maxRoll += maxBoostedRoll;
        }
    }
 
    private void calculateRollProbabilities() {
        // Start with unit impulse
        double[] convolution = {1.0};
 
        // Convolve with each ingredient's probability distribution
        for (double[] probDist : ingredientProbDists) {
            convolution = convolve(convolution, probDist);
        }
 
        // Build roll_pmfs map
        double[] craftedRolls = linspace(minRoll, maxRoll, convolution.length);
        for (int i = 0; i < convolution.length; i++) {
            if (convolution[i] == 0) continue;
            rollPmfs.put((int) craftedRolls[i], BigDecimal.valueOf(convolution[i]));
        }
    }
 
    // Utility method to create linearly spaced array (like numpy.linspace)
    private static double[] linspace(double start, double end, int points) {
        double[] result = new double[points];
        double step = (end - start) / (points - 1);
        for (int i = 0; i < points; i++) {
            result[i] = start + (step * i);
        }
        return result;
    }
 
    // Utility method to perform convolution (like numpy.convolve)
    private static double[] convolve(double[] a, double[] b) {
        int resultLength = a.length + b.length - 1;
        double[] result = new double[resultLength];
 
        for (int i = 0; i < a.length; i++) {
            for (int j = 0; j < b.length; j++) {
                result[i + j] += a[i] * b[j];
            }
        }
 
        return result;
    }
}