Alien RPG Dice Table

Posted: 2019-12-31
Word Count: 1693
Tags: dice lua-code rpg year-zero-system

In previous posts (1) (2) I’ve been figuring out probability tables for various RPGs by Swedish publisher Fria Ligan, a.k.a. Free League Publishing in English-speaking countries. Their Alien RPG, recently released, has some interesting wrinkles which made analysis a little more difficult, at least for me.

Nearly all1 their RPGs use the same basic resolution mechanic, dubbed the Year Zero System:

  1. The player (and sometimes GM) assembles a pool of d6s. For most player actions, it’s the sum of a Base Attribute, a Skill, and modifiers for equipment and circumstances.

  2. The player (or GM) rolls their dice. If at least one die shows a 6, the action is a success. Depending on the skill and the characer’s Talent, every 6 after the first enhances the action. Some rules call these “stunts”.

  3. In some variations – Mutant Year Zero (the system namer), Forbidden Lands, and the Alien RPG – some dice in the pool, distinguished by color, have a negative effect when they roll a 1.

  4. In opposed rolls, both a player and the GM (or another player) roll simultaneously; the one with more 6s wins.

  5. A player may choose to Push a roll, either because had no successes or not enough to overcome the result of an opposed roll. The player rerolls all dice that did not roll 6s (or 1s where they have special meaning) but incurs some penalty, depending on the game. For example:

    • In Mutant Year Zero and Forbidden Lands, every 1 rolled on a Base Die, initially or during the Pushed Roll, does one point of damage to the Base Attribute used. Likewise, every 1 rolled on a Gear Die lowers the Gear Bonus of that gear by 1, until it’s repaired by a competent craftsman.

    • In Coriolis, the GM gains a Darkness Point, which they can spend later to mess up the PCs’ lives in specific ways.

    • In Vaesen (and I think Tales from The Loop and Things From The Flood) every Push imposes a Condition on the character, which imposes a penalty on subsequent actions. Too many Conditions and the character is “Broken”, and suffers a random physical or mental affliction.

In another previous article I went into a little more detail about the Alien RPG’s Stress and Panic rules. To review:

This last point means that I couldn’t use the Binomial Theorem and a few Stupid Probability Tricks. The program I came up with, listed at the end, required a Trinomial Theorem and careful analysis of each possible outcome. I ended up calculating probabilities in two complementary ways, just to make sure I ended up with a single right answer, and even now I’m not quite convinced I got it right.

Anyway, here’s the table I came up with:

Base No Stress 1d 2d 3d 4d 5d 6d 7d 8d 9d
1d 16.67% 30.56% 42.13% 51.77% 55.14% 54.08% 49.13% 40.93% 30.11% 17.21%
2d 30.56% 42.13% 51.77% 59.81% 61.18% 58.41% 52.07% 42.77% 31.10% 17.57%
3d 42.13% 51.77% 59.81% 66.51% 66.21% 62.02% 54.51% 44.30% 31.92% 17.88%
4d 51.77% 59.81% 66.51% 72.09% 70.40% 65.03% 56.55% 45.57% 32.61% 18.13%
5d 59.81% 66.51% 72.09% 76.74% 73.90% 67.53% 58.25% 46.63% 33.18% 18.34%
6d 66.51% 72.09% 76.74% 80.62% 76.81% 69.62% 59.67% 47.52% 33.66% 18.51%
7d 72.09% 76.74% 80.62% 83.85% 79.24% 71.36% 60.85% 48.25% 34.06% 18.65%
8d 76.74% 80.62% 83.85% 86.54% 81.26% 72.81% 61.83% 48.87% 34.39% 18.78%
9d 80.62% 83.85% 86.54% 88.78% 82.94% 74.02% 62.65% 49.38% 34.67% 18.88%
10d 83.85% 86.54% 88.78% 90.65% 84.35% 75.03% 63.33% 49.81% 34.90% 18.96%
panic 16.67% 30.56% 42.13% 51.77% 59.81% 66.51% 72.09% 76.74% 80.62%
p-action 0.00% 0.00% 0.00% 8.63% 19.94% 33.26% 48.06% 63.95% 80.62%

Each row save the last two represents the number of Base Dice the player rolls. Each column represents the number of Stress Dice in the total dice pool.

The numbers confirm one’s intuitive notion of what happens as Stress builds:

Building a table that combines the total success probability of thie initial attempt and the push was a little too complicated. If P(b, s) is the probability of success on one roll, with b Base Dice and s Stress Dice, then the total probility, unless I’m mistaken, would be something like:

P(b, s) + (1 - P(b, s)) * P(b, s+1)

Fit that into a two-dimensional table. (Plus, some Talents in Alien allow characters to push twice in very specific circumstances, accruing another Stress Point each time.)

Program Listing

Hold onto your hats. This code is less commented and more complicated.

#!/usr/bin/env lua

-- Chances of success and failure on a single die
local SUCCESS = 1/6
local BASE_NO_RESULT = 5/6
local STRESS_NO_RESULT = 4/6
local PANIC = 1/6
local NO_PANIC = 5/6


local function panic_action(stress)
    if stress <= 3 then
       return 0
    end
    if stress >= 9 then
       return 1
    end
    return (stress - 3) / 6
end

local function base_success(base)
    return 1 - (BASE_NO_RESULT ^ base)
end

local function stress_no_panic(stress)
    return NO_PANIC^(stress)
end

local function stress_panic(stress)
    return 1 - NO_PANIC^(stress)
end

local function factorial(n)
    if n <= 1 then
        return 1
    end
    return n * factorial(n - 1)
end

local function exact_stress_result(ndice, nsuccess, npanic)
    if (nsuccess + npanic) > ndice then
        return 0
    end
    local nremain = ndice - nsuccess - npanic
    return 
        (factorial(ndice) / 
         (factorial(npanic) * factorial(nsuccess) * factorial(nremain)))
        * SUCCESS^nsuccess
        * PANIC^npanic
        * STRESS_NO_RESULT^nremain
end

local function stress_success_no_panic(ndice)
    -- TODO: simpler way to get same result?
    local result = 0

    for nsuccess = 1, ndice do
        result = result + exact_stress_result(ndice, nsuccess, 0)
    end

    return result
end

local function stress_success_with_panic(ndice)
    -- TODO: simpler way to get same result?
    local result = 0

    for nsuccess = 1, ndice do
        for npanic = 1, (ndice - nsuccess) do
            result = result + exact_stress_result(ndice, nsuccess, npanic)
        end
    end

    return result
end

local function no_success(ndice)
    return BASE_NO_RESULT ^ ndice
end

local function success_1(base, stress)
    local pact = panic_action(stress)

    if pact == 0 then
        -- No chance of Panic Actions, so strictly based on number of dice
        return base_success(base + stress)
    end

    --[[
    The entire roll succeeds in the following conditions:
    - Base Dice Succeed, Stress Dice have no Panics
    - Base Dice Succeed, Stress Dice have Panic but no Panic Action
    - Base Dice Fail, Stress Dice Succeed with no Panics
    - Base Dice Fail, Stress Dice Succeed with a Panic but no Panic Action
    ]]
    local bsuccess = base_success(base)
    local bfail = 1 - bsuccess
    local nopact = 1 - pact

    return
        bsuccess * stress_no_panic(stress)
        + bsuccess * stress_panic(stress) * nopact
        + bfail * stress_success_no_panic(stress)
        + bfail * stress_success_with_panic(stress) * nopact
end

local function success_2(base, stress)
    local pact = panic_action(stress)

    if pact == 0 then
        -- No chance of Panic Actions, so strictly based on number of dice
        return 1 - no_success(base + stress)
    end

    --[[
    The entire roll *fails* in the following conditions
    - no succes on Base Dice or Stress Dice, with or without panic
    - Base Dice Fail, Stress Dice Succeed but have Panic and Panic Action
    - Base Dice Succeed, Stress Dice have Panic and Panic Action
    ]]
    local bsuccess = base_success(base)
    local bfail = 1 - bsuccess

    local fail = no_success(base + stress)
                 + bfail * stress_success_with_panic(stress) * pact
                 + bsuccess * stress_panic(stress) * pact

    return 1 - fail
end

local function print_table(maxbase, maxstress)
    local mb, ms = (maxbase or 10), (maxstress or 3)
    local rowbuf

    -- print header
    rowbuf = { "Base", "No Stress" }
    if ms > 0 then
        table.insert(rowbuf, " 1d")
    end
    for n = 2, ms do
        table.insert(rowbuf, string.format("%5dd", n))
    end
    print(table.concat(rowbuf, ' | '))

    rowbuf = { ":---:" }
    for n = 0, ms do
        table.insert(rowbuf, "-------:")
    end
    print(table.concat(rowbuf, '|'))

    -- print each row

    for b = 1, mb do
        rowbuf = { string.format(" %2dd ", b) }
        for s = 0, ms do
            local p1, p2 = success_1(b, s),  success_2(b, s)
            local cellstr
            if (p1 - p2) < 0.0001 then
                cellstr = string.format(" %5.2f%% ", p1 * 100)
            else
                cellstr = string.format("%5.2f-%5.2f%%", p1 * 100, p2 * 100)
            end
            table.insert(rowbuf, cellstr)
        end
        print(table.concat(rowbuf, '|'))
    end

    rowbuf = { "**panic**  ", "  " }
    for s = 1, ms do
        local p = stress_panic(s)
        table.insert(rowbuf, string.format(" %5.2f%% ", p * 100))
    end
    print(table.concat(rowbuf, '|'))

    rowbuf = { "*p-action* ", "  " }
    for s = 1, ms do
        local p = stress_panic(s) * panic_action(s)
        table.insert(rowbuf, string.format(" %5.2f%% ", p * 100))
    end
    print(table.concat(rowbuf, '|'))

end

print_table(10, 9)

  1. Except Symbaroum which uses a d20 roll under system and possibly some Swedish-only games of which I’m not aware. ↩︎

  2. Weighted by the chance of Panic. When the dice indicate Panic, the chance of a Panic Action between 4 and 8 Stress Points is (Stress - 3) × 16.67%. ↩︎