2024-05-11: Converted global variables in code to local variables.
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% |
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% | 31% | +2% | |
2d | 31% | 52% | +2% | |
3d | 42% | 67% | +3% | |
4d | 52% | 77% | +3% | |
5d | 60% | 84% | +3% | |
6d | 67% | 89% | +2% | |
7d | 72% | 92% | +2% | |
8d | 77% | 95% | +2% | |
9d | 81% | 96% | +1% | |
10d | 84% | 97% | +1% |
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:
-
The player cannot push the roll, if that’s an option.
-
The player makes an immediate Panic Roll of 1d6 + Stress Points.
-
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.
-
The probability of rolling at least one success and the probability of rolling no successes adds up to 1 (100%).
-
Each roll or reroll of a die is independent from the others. The probability of rolling N dice and at least one of them being a 6 is equivalent to the probability of rolling 1 die up to N times, and one of them being a 6.
-
Likewise, rolling N dice and then rerolling them all if none succeed is equivalent to rolling 2 × N dice and checking if any succeed.
-
Finally, the probability of success from rolling N dice then “pushing” them is equivalent to the probability of success from rolling each die and “pushing” it repeated N times.
Listing:
#!/usr/bin/env lua
-- Chances of failure on a single die (no rerolls)
local FAIL = 5/6
-- Chances of failure 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 (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_FAIL = 13/18
-- chance of rolling at least one success on `n` dice
-- (single roll)
local function success(n)
return (1 - FAIL^n)
end
-- 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)
end
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
local p = success(n) * 100
local pp = success_push(n) * 100
local p2 = success(n*2) * 100
print(string.format(FMT, n, p, pp, p2, (pp + p2)/2))
end
end
print_success(10)
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:
-
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.
-
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.
-
Which might be negative, in which case the player rolls “negative” skill dice, i.e. any successes cancel successes on the other two dice. ↩︎
-
Which I did. – Future Frank ↩︎