Year Zero Probabilities

Posted: 2019-12-12
Last Modified: 2023-03-18
Word Count: 1456
Tags: dice lua-code rpg year-zero-system

Table of Contents

SUMMARY: All the Year Zero System games from Free League that I’ve seen contain the same table of probabilities, despite mechanical differences that should change probabilities. In this post I try to derive the results they did, fail, and derive tables that are close and as far as I can tell more accurate. The post ends with a Lua program to generate a basic probability table. You have been warned.

2023-03-18: On Which Date I Add A Table Of Contents And Old-Timey Titles.

In Which A Table Perplexes Me

In an earlier post I reprinted the following table from Forbidden Lands.

# of dice Chance of Success Pushed Roll
1 17% 29%
2 31% 50%
3 42% 64%
4 52% 74%
5 60% 81%
6 67% 87%
7 72% 90%
8 77% 93%
9 81% 95%
10 84% 96%

“Surely”, I thought to myself, “the Pushed Roll column is an approximation.” (Yes, I talk to myself out loud, especially about dice probabilities.)

In Mutant Year Zero and Forbidden Lands, the player rolls a mix of Base Dice, Skill Dice, and Gear Dice depending on the values of the PC’s base attribute, adjusted skill rank1, and gear bonus. Unless the gear is a Forbidden Lands Artifact, they’re all six-siders … but, when pushing the roll, the player can’t reroll any dice that show a 6 (which indicates a success), nor can they reroll any Base or Gear Dice that show a 1 (which when pushing a roll damages the attribute or gear, respectively).

The Coriolis Quickstart and the Alien RPG contain the same table. That’s … even less right. I’ve no idea what’s in Tales from the Loop, Things from the Flood, or the upcoming Vaesen, which all use the same system, but if I were a betting man I’d wager they contain the same table.

Where did this table come from?

In Which I Make My Own Table

Below is a table generated by a Lua program that I’ve reproduced at the end so people can check my math.

ndice no reroll reroll 2-5 reroll 1-5 ‘push’ (avg.)
1d 16.67% 27.78% 30.56% 29.17%
2d 30.56% 47.84% 51.77% 49.81%
3d 42.13% 62.33% 66.51% 64.42%
4d 51.77% 72.79% 76.74% 74.77%
5d 59.81% 80.35% 83.85% 82.10%
6d 66.51% 85.81% 88.78% 87.30%
7d 72.09% 89.75% 92.21% 90.98%
8d 76.74% 92.60% 94.59% 93.59%
9d 80.62% 94.65% 96.24% 95.45%
10d 83.85% 96.14% 97.39% 96.77%

The second column is the probability without pushing the dice, and save for decimal places matches the second column of the Official Table.

The third column assumes that the player rerolls all dice not 1 or 6, the rule for Base Dice and Gear Dice in MYZ and FL, while the fourth assumes that the player rerolls all dice not 6, the rule for Skill Dice in MYZ and FL. The last is the average of the previous columns, and it’s fairly close to the Official Table, with a difference of about 1%.

# of dice Chance of Success Pushed Roll (calculated)
1 17% 29% 29%
2 31% 50% 50%
3 42% 64% 64%
4 52% 74% 75%
5 60% 81% 82%
6 67% 87% 87%
7 72% 90% 91%
8 77% 93% 94%
9 81% 95% 95%
10 84% 96% 97%
NOTE (Jan 3, 2020): Table added for clarity.

In Which Other Tables Vex Me

However, the tables for Coriolis, TftL, TftF, and Vaesen should match the fourth column exactly, since players can reroll all dice that don’t succeed.

# of dice Chance of Success Pushed Roll (revised) diff
1d 17% 29% 31% +2%
2d 31% 50% 52% +2%
3d 42% 64% 67% +3%
4d 52% 74% 77% +3%
5d 60% 81% 84% +3%
6d 67% 87% 89% +2%
7d 72% 90% 92% +2%
8d 77% 93% 95% +2%
9d 81% 95% 96% +1%
10d 84% 96% 97% +1%
NOTE (Jan 3, 2020): Table revised for clarity and simplicity.

The difference between the table above and the Official Table is within 3%, though, so maybe it’s not that big a deal.

In Which Another Table Eludes Me

Alien is a different thorax of chestbursters entirely. A push adds a Stress Point, which adds a die to the pushed roll and all subsequent rolls until the characer loses stress. Furthermore, the player may reroll all dice that don’t succeed. If any Stress die comes up as 1, either on the initial roll or on the push, the following happens:

  1. The player cannot push the roll, if that’s an option.

  2. The player makes an immediate Panic Roll of 1d6 + Stress Points.

  3. If the sum is 10 or more, the skill test automatically fails and the character takes a Panic Action instead.

Note that the chance of a Panic is equal to the Chance of Success for that many Stress dice on the table above. At 4 Stress you’re not only liable to Panic more than half the time, you’re at risk for Panic Actions, starting at a 16.67% chance.

Constructing a table that takes into account the number of Base Dice, the number of Stress Dice, the chance of Panic as a function of the number of Stress Dice, and the probability of success contingent on the character not failing due to a Panic Action is left as an exercise for the reader. (At least until I get around to it.)2

In Which I Show My Code

The program relies on the following equivalences.


#!/usr/bin/env lua

-- Chances of success and failure on a single die (no rerolls)

local SUCCESS, FAIL = 1/6, 5/6

-- Chances of success and failure on a single die with a "push" (reroll).
-- These magic values come from the following analysis:
-- a. on 6 (1/6), the user keeps the success
-- b. on 1 (1/6), the user cannot reroll
-- c. on 2-5 (4/6), the user rerolls, with a 5/6 chance of failure.
-- Ergo, the chance of a success on one die with a reroll is
-- (1/6 + (4/6)*(1/6)) = (6 + 4)/36 = 5/18
-- and the chance of *no* successes is therefore 13/18

local PUSH_SUCCESS, PUSH_FAIL = 5/18, 13/18

-- chance of rolling at least one success on `n` dice
-- (single roll)

local function success(n)
    return (1 - FAIL^n)

-- chance of rolling at least one success on `n` dice
-- assuming the player can reroll any die not 1 or 6 exactly once

local function success_push(n)
    return (1 - PUSH_FAIL^n)

local function print_success(ndice)
    local max = (ndice or 10)

    print "ndice| no reroll | reroll 2-5 | reroll 1-5 | 'push' (avg.)"
    print ":---:|-------:|-------:|-------:|-------:"

    local FMT = " %2dd | %5.2f%% | %5.2f%% | %5.2f%% | %5.2f%%"

    for n = 1, max do
        p = success(n) * 100
        pp = success_push(n) * 100
        p2 = success(n*2) * 100

        print(string.format(FMT, n, p, pp, p2, (pp + p2)/2))


P.S. I didn’t have to use local everywhere, or anywhere, in the program above. It’s a good habit for Lua programmers to cultivate, though, for two reasons:

  1. Hygiene: If a variable or function isn’t declared local, it goes into the global namespace. In a small, standalone program that (usually) doesn’t matter, but in a larger one, or in a module included in a larger program, adding or rewriting global variables is a very bad idea. Also, unlike some versions of LISP, public functions and global variables share the same Lua environment.

  2. Performance: The Lua interpreter compiles all “chunks” – files or program texts – into its own internal bytecode before running them. Local references get a register index in the compiled chunk; global references are simply referenced by name. Thus every reference to a global requires a hash table lookup, while every reference to a local simply looks up an index in an internal structure.

  1. Which might be negative, in which case the player rolls “negative” skill dice, i.e. any successes cancel successes on the other two dice. ↩︎

  2. Which I did. – Future Frank ↩︎