(This article refers to Faster Than Light: Nomad by Stellagama Publishing.)
Just showing off a new version of the Python script that I use to generate the probability tables for Nomad. It includes command line arguments to change parameters at runtime, and a much faster recursive algorithm to calculate probabilities. Same probabilities though, just with more dice in something less than several minutes. (Or hours … or days …)
The Table
./rollkeep.py -b 5
TN | Skill | -5D | -4D | -3D | -2D | -1D | +0D | +1D | +2D | +3D | +4D | +5D |
---|---|---|---|---|---|---|---|---|---|---|---|---|
2 | 6 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 | 100.00 |
3 | 5 | 66.98 | 73.68 | 80.38 | 86.81 | 92.59 | 97.22 | 99.54 | 99.92 | 99.99 | 100.00 | 100.00 |
4 | 4 | 38.15 | 46.66 | 56.65 | 67.98 | 80.09 | 91.67 | 98.15 | 99.61 | 99.92 | 99.98 | 100.00 |
5 | 3 | 17.92 | 25.07 | 34.84 | 47.84 | 64.35 | 83.33 | 94.91 | 98.46 | 99.52 | 99.85 | 99.95 |
6 | 2 | 7.84 | 12.32 | 19.41 | 30.56 | 47.69 | 72.22 | 89.35 | 95.99 | 98.50 | 99.44 | 99.79 |
7 | 1 | 2.77 | 5.11 | 9.43 | 17.36 | 31.94 | 58.33 | 80.56 | 90.97 | 95.78 | 98.01 | 99.06 |
8 | 0 | 0.94 | 1.99 | 4.22 | 9.03 | 19.44 | 41.67 | 68.06 | 82.64 | 90.57 | 94.89 | 97.23 |
9 | 0.21 | 0.56 | 1.50 | 4.01 | 10.65 | 27.78 | 52.31 | 69.44 | 80.59 | 87.68 | 92.16 | |
10 | 0.05 | 0.15 | 0.48 | 1.54 | 5.09 | 16.67 | 35.65 | 52.16 | 65.16 | 74.93 | 82.08 | |
11 | 0.00 | 0.02 | 0.08 | 0.39 | 1.85 | 8.33 | 19.91 | 32.02 | 43.35 | 53.34 | 61.85 | |
12 | 0.00 | 0.00 | 0.01 | 0.08 | 0.46 | 2.78 | 7.41 | 13.19 | 19.62 | 26.32 | 33.02 |
Usage
usage: rollkeep.py [-h] [-k KEEP] [-b BONUS] [-d SIDES] [-t TARGET] [-n]
Generate a probability table for roll-and-keep mechanics.
options:
-h, --help show this help message and exit
-k KEEP, --keep KEEP number of dice to keep (default 2)
-b BONUS, --bonus BONUS
max number of bonus/penalty dice (default 3)
-d SIDES, --sides SIDES
number of sides (default 6)
-t TARGET, --target TARGET
standard target number (default 8)
-n, --negative-skill show negative skill numbers
rollkeep.py
#!/usr/bin/env python3
import argparse
from functools import cache
from fractions import Fraction
from itertools import combinations_with_replacement, chain, product
import tabulate
NKEEP: int = 2
NBONUS: int = 3
NSIDES: int = 6
TARGET: int = 8
SetHistogram = dict[tuple[int, ...], Fraction]
Histogram = dict[int, Fraction]
Cell = Fraction | int | None
Table = list[list[Cell]]
def new_tuple(t: tuple[int, ...], d: int, low: bool) -> tuple[int, ...]:
temp_t = tuple(sorted((d,) + t))
if low:
return temp_t[:-1]
return temp_t[1:]
@cache
def set_histogram(ndice: int, nsides: int, nkeep: int, low: bool) -> SetHistogram:
assert nkeep <= ndice
result: SetHistogram = {}
for t in combinations_with_replacement(range(1, nsides + 1), nkeep):
result[t] = Fraction(0, 1)
if nkeep == ndice:
# Create simple set histogram
p: Fraction = Fraction(1, nsides**nkeep)
for t in product(range(1, nsides + 1), repeat=nkeep):
modt: tuple[int, ...] = tuple(sorted(t))
result[modt] += p
return result
# Multiply lesser order set_histogram by one die
lesser: SetHistogram = set_histogram(ndice - 1, nsides, nkeep, low)
onedie_p: Fraction = Fraction(1, nsides)
for d, t in product(range(1, nsides + 1), lesser):
new_t: tuple[int, ...] = new_tuple(t, d, low)
result[new_t] += lesser[t] * onedie_p
return result
def histogram(ndice: int, nsides: int, nkeep: int, low: bool) -> Histogram:
result: Histogram = {}
sethist: SetHistogram = set_histogram(ndice, nsides, nkeep, low)
# Sum the sets to provide a Histogram
for n in range(nkeep, nsides * nkeep + 1):
result[n] = Fraction(0, 1)
for t in sethist:
sum_n: int = sum(t)
result[sum_n] += sethist[t]
return result
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: dict, i: int) -> Cell:
if i not in h:
return None
return h[i] * 100
def skill(k: int, target: int, neg: bool = False) -> Cell:
if neg:
return target - k
return target - k if k <= target else None
def collate(hl: list[Histogram], target: int = TARGET, neg: bool = False) -> Table:
allkeys: list[int] = sorted(set(chain.from_iterable([list(h) for h in hl])))
return [
[k, skill(k, target, neg)] + [format_pct(h, k) for h in hl] for k in allkeys
]
def main() -> None:
parser = argparse.ArgumentParser(
description="Generate a probability table for roll-and-keep mechanics."
)
parser.add_argument(
"-k",
"--keep",
help=f"number of dice to keep (default {NKEEP})",
default=NKEEP,
type=int,
)
parser.add_argument(
"-b",
"--bonus",
help=f"max number of bonus/penalty dice (default {NBONUS})",
default=NBONUS,
type=int,
)
parser.add_argument(
"-d",
"--sides",
help=f"number of sides (default {NSIDES})",
default=NSIDES,
type=int,
)
parser.add_argument(
"-t",
"--target",
help=f"standard target number (default {TARGET})",
default=TARGET,
type=int,
)
parser.add_argument(
"-n",
"--negative-skill",
help="show negative skill numbers",
action="store_true",
)
args = parser.parse_args()
keep: int = args.keep
bonus: int = args.bonus
sides: int = args.sides
headers = ["TN", "Skill"] + [f"{n:+}D" for n in range(-bonus, bonus + 1)]
table = collate(
[
cumulative(histogram(ntotal, sides, nkeep=keep, low=True))
for ntotal in range(keep + bonus, keep, -1)
]
+ [
cumulative(histogram(ntotal, sides, nkeep=keep, low=False))
for ntotal in range(keep, keep + bonus + 1)
],
target=args.target,
neg=args.negative_skill,
)
print(tabulate.tabulate(table, headers=headers, tablefmt="pipe", floatfmt=".2f"))
if __name__ == "__main__":
main()
Postscript (2024-09-11)
If anyone wants to check my math, simply copy and paste this script into AnyDice.
NDICE: 2
NSIDES: 6
NBONUS: 5
output (NDICE)d(NSIDES) named "[NDICE]D[NSIDES]"
loop BONUS over {1 .. NBONUS} {
output [lowest NDICE of (NDICE+BONUS)d(NSIDES)] named "[NDICE]D[NSIDES] - [BONUS]b"
}
loop BONUS over {1 .. NBONUS} {
output [highest NDICE of (NDICE+BONUS)d(NSIDES)] named "[NDICE]D[NSIDES] + [BONUS]b"
}
Be sure to select “At Least”, since I’m using cumulative probabilities above.