Yet More Year Zero, Again

2020-09-14

(approx. 1200 words)

So far I’ve read half of Coriolis and have just started the published version of Vaesen. I’ll have more to say about them in future posts.

In the meantime I’ll look into how Difficulty Factors in Tales From the Loop and Vaesen change probabilities of success.

In Year Zero Engine games, players roll a pool of six-sided dice based on their character’s attribute and skill scores; each six counts as a “success”. Players can “push” a roll – re-roll failed dice once – at a cost, such as a Condition that negatively affects their character. In Coriolis, Vaesen, Tales From the Loop, and Things From the Flood, 1-5 have no effect.1

Many Year Zero Engine games increase the difficulty of a roll by subtracting up to three dice from the player’s dice pool. In TFL the GM can make rolls more difficult by requiring more than one success on the dice. The published version of Vaesen uses the same mechanic2: a “Challenging” roll requires two, a “Difficult” one three. (Vaesen also subtracts dice to reflect penalties from armor, Conditions (non-critical impairment), and critical injuries.)

Previously I assumed that the probability of success for pushing a roll is always equal to the probability for rolling twice as many dice. That’s true when any number of sixes is a success, but it doesn’t take into account that, if the first roll turns up some sixes but not enough, you’re rolling fewer dice the second time.

Naturally I wrote a Lua program to figure out the exact probabilities. It’s what I do. The program calculates pushed probabilities two ways:

  1. Count up all cases when the first roll didn’t roll enough successes, but the second roll did.

  2. Conceptually roll each die and the reroll it immediately if it didn’t produce a success. Thus each die doesn’t have a mere 1 in 6 probability of success, but 1/6 + (5/6)*(1/6).

Thank the Vanir (or the Icons3), the two numbers (eventually) matched. Here they are:

Dice Normal Pushed Challenging (Pushed) Difficult (Pushed)
1d 16.67% 30.56% 0.00% 0.00% 0.00% 0.00%
2d 30.56% 51.77% 2.78% 9.34% 0.00% 0.00%
3d 42.13% 66.51% 7.41% 22.30% 0.46% 2.85%
4d 51.77% 76.74% 13.19% 35.81% 1.62% 8.80%
5d 59.81% 83.85% 19.62% 48.32% 3.55% 17.05%
6d 66.51% 88.78% 26.32% 59.17% 6.23% 26.60%
7d 72.09% 92.21% 33.02% 68.22% 9.58% 36.56%
8d 76.74% 94.59% 39.53% 75.55% 13.48% 46.23%
9d 80.62% 96.24% 45.73% 81.37% 17.83% 55.19%
10d 83.85% 97.39% 51.55% 85.91% 22.48% 63.19%
11d 86.54% 98.19% 56.93% 89.42% 27.32% 70.13%
12d 88.78% 98.74% 61.87% 92.10% 32.26% 76.03%

So, yeah, the probabilities of Challenging and Difficult tests are worse than subtracting dice. The chance of success when pushing such rolls is also significantly less than simply doubling the number of dice.

Coriolis doesn’t use Difficulty Factors, but three successes on a roll is a “Critical Success” and typically has a beneficial side effect. Looking at the last two columns of the table above, Criticals in Coriolis (the last two columns) are aptly named, although Criticals come far more often to those who have more dice to push. (In Coriolis, though, the penalty for pushing is Darkness Points, which is essentially bad luck that can rebound on any PC whenever the GM feels like it.)

As I said in my post on TFL, while subtracting from the dice pool sometimes feels fiddly and arbitrary, this difficulty mechanic is a little too coarse-grained, especially when characters aren’t rolling a lot of dice. Starting characters in Vaesen have Attributes ranging from 2 to 4, or 5 in their Archetype’s primary Attribute, and a maximum of 2 points in a Skill, or 3 in their Archetype’s primary Skill. So a maxed-out character might have 8 dice in their Archetype’s specialty; most will have between 7 and 5 dice for primary skills, and 4 to 2 dice for little-used or untrained skills. Especially at these lower levels, even pushing won’t compensate for Challenging rolls, let alone Difficult.

TFL aptly if verbosely called their Difficulty Levels “Extremely Difficult” and “Almost Impossible”, respectively. If I ran Vaesen I’d use Difficulty Levels sparingly. Maybe if a player wanted to do something too improbable. And maybe I’d give partial credit or consolation prizes for too few successes.

#!/usr/bin/env lua

-- Chances of success on a single die

local SUCCESS = 1/6

-- Chances of success on a single die with a "push" (reroll).
-- This magic value comes from the following analysis:
--
-- a. on 6 (1/6), the user keeps the success
-- b. on 1-5 (5/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 + (5/6)*(1/6)) = (6 + 5)/36 = 11/36

local PUSH_SUCCESS = 11/36

local function choose(n, k)
    if n < 1 or k < 0 or k > n then
        return 0
    end

    local result = 1
    for i = 1, k do
        result = result * (n + 1 - i) / i
    end
    return result
end

local function binomial(n, k, p)
    return choose(n, k) * (p)^k * (1-p)^(n-k)
end

-- chance of rolling at least `k` successes on `n` dice
-- with probability `p`
local function success_at_least(n, k, p)
    if k <= 0 then
        return 1.0
    end

    if k == 1 then
        return 1 - (1 - p)^n
    end

    local result = 0
    for i = k, n do
        result = result + binomial(n, i, p)
    end
    return result
end

-- chance of rolling at least `k` successes on `n` dice with reroll
-- with probability `p`
local function push_success_at_least(n, k, p)
    local result = success_at_least(n, k, p)
    -- only `j` successes from first roll
    for j = 0, k-1 do
        local first = binomial(n, j, p)
        result = result + first * success_at_least(n-j, k-j, p)
    end
    return result
end

local function format_percent(p)
    return string.format(" %5.2f%% ", p * 100)
end

local function print_table(maxdice)
    local md = (maxdice or 10)
    local rowbuf

    -- print header
    print("Dice | Normal | Pushed | Challenging |(Pushed)| Difficult |(Pushed)")
    print("----:|:------:|:------:|:-----------:|:------:|:---------:|:------:")

    -- print each row

    for n = 1, md do
        rowbuf = { string.format(" %2dd ", n) }

        for ms = 1,3 do
            local p  = success_at_least(n, ms, SUCCESS)
            local p2 = success_at_least(n, ms, PUSH_SUCCESS)
            local pp = push_success_at_least(n, ms, SUCCESS)

            table.insert(rowbuf, format_percent(p))
            if math.abs(pp - p2) < 0.00001 then
                table.insert(rowbuf, format_percent(pp))
            else
                table.insert(rowbuf, 
                             format_percent(pp) .. '/' .. format_percent(p2))
            end
        end

        table.insert(rowbuf, "")
        print(table.concat(rowbuf, '|'))
    end
end

print_table(12)


  1. See the Year Zero Engine SRD for how the parent games Mutant Year Zero and Forbidden Lands work. Their Alien RPG is a little different. ↩︎

  2. I don’t remember this from the Alpha and Beta drafts during the Kickstarter, and I could have sworn it suggested subtracting dice to reflect difficulty instead, like earlier Year Zero Engine games. Unfortunately I didn’t keep those drafts. ↩︎

  3. Focus of the dominant religion in the Coriolis setting. ↩︎