- 2024-06-23: WHOLLY REWRITTEN to use a Python script rather than https://anydice.com.
- 2024-06-27: Minor corrections to the code and explanation.
Inevitably I wondered what the probability table for Faster Than Light: Nomad looked like.
The Table
And here it is:
TN | Skill | -3D | -2D | -1D | +0D | +1D | +2D | +3D |
---|---|---|---|---|---|---|---|---|
2 | 6 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 |
3 | 5 | 80.38 | 86.81 | 92.59 | 97.22 | 99.54 | 99.92 | 99.99 |
4 | 4 | 56.65 | 67.98 | 80.09 | 91.67 | 98.15 | 99.61 | 99.92 |
5 | 3 | 34.84 | 47.84 | 64.35 | 83.33 | 94.91 | 98.46 | 99.52 |
6 | 2 | 19.41 | 30.56 | 47.69 | 72.22 | 89.35 | 95.99 | 98.50 |
7 | 1 | 9.43 | 17.36 | 31.94 | 58.33 | 80.56 | 90.97 | 95.78 |
8 | 0 | 4.22 | 9.03 | 19.44 | 41.67 | 68.06 | 82.64 | 90.57 |
9 | 1.50 | 4.01 | 10.65 | 27.78 | 52.31 | 69.44 | 80.59 | |
10 | 0.48 | 1.54 | 5.09 | 16.67 | 35.65 | 52.16 | 65.16 | |
11 | 0.08 | 0.39 | 1.85 | 8.33 | 19.91 | 32.02 | 43.35 | |
12 | 0.01 | 0.08 | 0.46 | 2.78 | 7.41 | 13.19 | 19.62 |
The Code
#!/usr/bin/env python3
from collections.abc import Callable, Collection, Mapping
from fractions import Fraction
from functools import partial
from itertools import chain, product
import tabulate
NKEEP: int = 2
NBONUS: int = 3
NSIDES: int = 6
Evaluator = Callable[[Collection[int]], int]
Histogram = Mapping[int, Fraction]
Table = Collection[Collection[Fraction | int | None]]
def keep_highest(roll: Collection[int], nkeep: int, low: bool = False) -> int:
ndice: int = len(roll)
if ndice <= nkeep:
return sum(roll)
sroll = sorted(roll)
if low:
return sum(sroll[:nkeep])
return sum(sroll[-nkeep:])
def histogram(ndice: int, nsides: int, func: Evaluator) -> Histogram:
hist: Histogram = {}
die: list[int] = [x for x in range(1, nsides + 1)]
incr: Fraction = Fraction(1, nsides**ndice)
for roll in product(die, repeat=ndice):
result: int = func(roll)
if result not in hist:
hist[result] = Fraction(0, 1)
hist[result] = hist[result] + incr
return hist
def cumulative(h: Histogram) -> Histogram:
result: Histogram = {}
counter: Fraction = Fraction(0, 1)
for key in reversed(sorted(list(h))):
counter += h[key]
result[key] = counter
return result
def format_pct(h: Histogram, i: int) -> Fraction | None:
if i not in h:
return None
return h[i] * 100
def skill(k: int) -> int | None:
return 8 - k if k <= 8 else None
def collate(hl: Collection[Histogram]) -> Table:
allkeys: list[int] = sorted(set(chain.from_iterable([list(h) for h in hl])))
return [[k, skill(k)] + [format_pct(h, k) for h in hl] for k in allkeys]
if __name__ == "__main__":
headers = ["TN", "Skill"] + [f"{n:+}D" for n in range(-NBONUS, NBONUS + 1)]
table = collate(
[
cumulative(
histogram(ntotal, NSIDES, partial(keep_highest, nkeep=NKEEP, low=True))
)
for ntotal in range(NKEEP + NBONUS, NKEEP, -1)
]
+ [
cumulative(histogram(ntotal, NSIDES, partial(keep_highest, nkeep=NKEEP)))
for ntotal in range(NKEEP, NKEEP + NBONUS + 1)
]
)
print(tabulate.tabulate(table, headers=headers, tablefmt="pipe", floatfmt=".2f"))
Notes on the Code
-
This code has zero doc strings or code comments. Sorry.
-
To run this code, you need to install
tabulate
in your local Python environment, e.g. withpip install tabulate
. -
The “constants” at the top (not really constant under Python) determine how many dice one keeps (
NKEEP
), how many sides each die has (NSIDES
), and the maximum Advantage/Disadvantage dice to add (NBONUS
).Unlike my previous attempt using AnyDice, the Python script lets me easily generate a full table just by tweaking one or more parameters.
-
This code essentially generates all possible combinations of rolling (
NKEEP
+ (0 ..NBONUS
)) d (NSIDES
), then evaluates each combination with anEvaluator
function to derive a final (integer) result.-
I probably could have done something clever with generators and pipelining results.
-
I also maybe could have made the code more efficient by using only unique results and multiplying by the number of combinations. As it stands, it generates the table above in less than a second using Python 3.12 on my old ThinkPad T400 under Linux, so I decided to let it be.
-
-
The
cumulative
function converts the die results from “exactly X” to “X or more”.8
, for example, means “8+” or “≥8”. -
The
collate
function “flips” the histograms to arrange the die results in “rows” and the number of Advantage/Disadvantage dice in “columns”. -
I abandoned code annotations in the main function. Again, sorry.