‘Faster Than Light: Nomad’ Probabilities

Posted: 2024-05-20
Last Modified: 2024-06-27
Word Count: 664
Tags: ftl-nomad rpg

Table of Contents

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