Skip to content

python API

If you want to call ttrpg-dice from within python, you can ...

ttrpg_dice

Various functions for TTRPG Gamesmasters to help with dice rolls.

LazyRollTable

LazyRollTable(maxdice: int, dicetype: int, target: int)

Table of values for lazyrolls of varying numbers of goblins.

Source code in ttrpg_dice/manydice.py
65
66
67
68
69
70
71
72
73
def __init__(self, maxdice: int, dicetype: int, target: int) -> None:
    """Create a table of lazyrolls for up to `maxdice`."""
    self._maxdice = maxdice
    self._maxdicerange = range(maxdice + 1)
    """use to iterate from `0` to `maxdice` rolls `for i in self._maxdicerange`"""
    self._dicetype = dicetype
    self._target = target
    self.rolls = [lazyroll(i, dicetype, target) for i in self._maxdicerange]
    """List of lists of resulting lazyrolls - (0-indexed, so _includes_ 0 dice and 0 hits)"""

rolls instance-attribute

rolls = [
    lazyroll(i, dicetype, target) for i in _maxdicerange
]

List of lists of resulting lazyrolls - (0-indexed, so includes 0 dice and 0 hits)

__eq__

__eq__(value: object) -> bool

Compare self.rolls if value is not another LazyRollTable.

Source code in ttrpg_dice/manydice.py
75
76
77
78
79
def __eq__(self, value: object) -> bool:
    """Compare self.rolls if `value` is not another `LazyRollTable`."""
    if not isinstance(value, LazyRollTable):
        return self.rolls == value
    return self.rolls == value.rolls

__str__

__str__() -> str

Format as a nice table ignoring zero dice and zero hits.

Source code in ttrpg_dice/manydice.py
84
85
86
87
88
89
90
91
92
93
94
95
96
def __str__(self) -> str:
    """Format as a nice table ignoring zero dice and zero hits."""
    tab = "\t"
    newline = "\n"

    def _formatroll(numdice: int, rolls: list[int]) -> str:
        lazytargets = tab.join(str(lazytarget) for lazytarget in rolls[1:])
        return tab.join([str(numdice), lazytargets])

    description = f"Lazyroll table for up to {self._maxdice}d{self._dicetype} targeting {self._target} for success:"
    table_header = f"\tHITS\n\t{tab.join(str(i) for i in self._maxdicerange[1:])}"
    table_lines = [_formatroll(d, r) for d, r in enumerate(self.rolls)]
    return newline.join([description, "", table_header, *table_lines[1:]])

PoolComparison

PoolComparison(
    pools: dict[str, Dice] | Iterable[Dice],
    outcomes: dict[str, slice],
)

Comparison of related dicepools.

Source code in ttrpg_dice/manydice.py
102
103
104
105
106
107
108
109
110
111
112
113
114
def __init__(self, pools: dict[str, Dice] | Iterable[Dice], outcomes: dict[str, slice]) -> None:
    """Create comparison based on dict of named pools and dict of named outcomes."""
    if isinstance(pools, Mapping):
        self.pools = pools
    else:
        self.pools = {pool: pool for pool in pools}
    self.outcomes = outcomes
    self.chances = {
        (pool, outcome): sum(die[index])
        for pool, die in self.pools.items()
        for outcome, index in self.outcomes.items()
    }
    """Dict of chances indexed by (pool, outcome)"""

chances instance-attribute

chances = {
    (pool, outcome): sum(die[index])
    for (pool, die) in items()
    for (outcome, index) in items()
}

Dict of chances indexed by (pool, outcome)

__str__

__str__() -> str

Nicely formatted table.

Source code in ttrpg_dice/manydice.py
116
117
118
119
120
def __str__(self) -> str:
    """Nicely formatted table."""
    data = [[pool] + [self.chances[pool, outcome] * 100 for outcome in self.outcomes] for pool in self.pools]
    headers = ["pool", *self.outcomes]
    return tabulate(data, headers=headers, tablefmt="plain", floatfmt=".2f")

plot

plot() -> tuple[Figure, Axes3D]

Plot as a 3d Bar with matplotlib and return the Axes.

Source code in ttrpg_dice/manydice.py
122
123
124
125
126
127
128
def plot(self) -> tuple[Figure, Axes3D]:
    """Plot as a 3d Bar with matplotlib and return the Axes."""
    fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
    ax.bar3d(**self.plotable(), shade=True)
    ax.set_yticks([y + 0.5 for y, _ in enumerate(self.pools)], [str(pool) for pool in self.pools])
    ax.set_xticks([x + 0.5 for x, _ in enumerate(self.outcomes)], [str(outcome) for outcome in self.outcomes])
    return fig, ax

plotable

plotable() -> dict[str, list]

Return bar location and sizes suitable for passing directly to matplotlib bar3d().

Source code in ttrpg_dice/manydice.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def plotable(self) -> dict[str, list]:
    """Return bar location and sizes suitable for passing directly to matplotlib bar3d()."""
    x_locations = []
    y_locations = []
    z_locations = []
    dx_widths = []
    dy_widths = []
    dz_heights = []
    colours = []
    alphas = [
        0,  # -Z
        1,  # +Z (top)
        0,  # -Y
        0,  # +Y
        0.2,  # -X
        0.2,  # +X
    ]
    poolcolours = "bgrcmy"

    # Basic Colour Cycle (per pool)
    # ("b",0),("b",1),("b",0),("b",0),("b",0.2),("b",0.2),
    # ("g",0),("g",1),("g",0),("g",0),("g",0.2),("g",0.2),
    # ("r",0),("r",1),("r",0),("r",0),("r",0.2),("r",0.2),
    # ("c",0),("c",1),("c",0),("c",0),("c",0.2),("c",0.2),
    # ("m",0),("m",1),("m",0),("m",0),("m",0.2),("m",0.2),
    # ("y",0),("y",1),("y",0),("y",0),("y",0.2),("y",0.2),

    for x, outcome in enumerate(self.outcomes.keys()):
        for y, pool in enumerate(self.pools.keys()):
            x_locations.append(x)
            y_locations.append(y)
            z_locations.append(0)
            dx_widths.append(1)
            dy_widths.append(1)
            dz_heights.append(self.chances[(pool, outcome)])
            colours.extend(
                zip_longest(poolcolours[y % len(poolcolours)], alphas, fillvalue=poolcolours[y % len(poolcolours)]),
            )

    return {
        "x": x_locations,
        "y": y_locations,
        "z": z_locations,
        "dx": dx_widths,
        "dy": dy_widths,
        "dz": dz_heights,
        "color": colours,
    }

StatBlock

StatBlock(*_args, **_kwargs)

A TTRPG StatBlock, acts as a mapping of stats.

Source code in ttrpg_dice/statblock.py
24
25
26
27
28
def __init__(self, *_args, **_kwargs) -> None:  # noqa: ANN002, ANN003
    """Raises TypeError - this method is replaced by `_init_` by the decorator."""
    if type(self) is StatBlock:
        msg = "Cannot directly instantiate a StatBlock, please use the @statblock decorator instead."
        raise TypeError(msg)

__add__

__add__(other: Self) -> Self

Adds each stat, raises AttributeError if stat missing in other.

Source code in ttrpg_dice/statblock.py
49
50
51
52
53
54
def __add__(self, other: Self) -> Self:
    """Adds each stat, raises AttributeError if stat missing in `other`."""
    newstats = {
        stat: min(self[stat] + other[stat], len(self._STATS[stat])) for stat in self._STATS
    }
    return type(self)(**newstats)

__getitem__

__getitem__(stat: str) -> int | Dice

Get a specific stat by subscripting.

Source code in ttrpg_dice/statblock.py
72
73
74
75
76
77
def __getitem__(self, stat: str) -> int | Dice:
    """Get a specific stat by subscripting."""
    if stat in self._STATS:
        return getattr(self, stat)
    msg = f"Unknown stat '{stat}'"
    raise KeyError(msg)

__iter__

__iter__() -> Iterator

Iterate over stats.

Source code in ttrpg_dice/statblock.py
83
84
85
def __iter__(self) -> Iterator:
    """Iterate over stats."""
    return iter(self._STATS)

__len__

__len__() -> int

Number of stats.

Source code in ttrpg_dice/statblock.py
79
80
81
def __len__(self) -> int:
    """Number of stats."""
    return len(self._STATS)

__or__

__or__(other: Self) -> Self

Merge stats, keeping the highest.

Source code in ttrpg_dice/statblock.py
67
68
69
70
def __or__(self, other: Self) -> Self:
    """Merge stats, keeping the highest."""
    newstats = {stat: max(self[stat], other[stat]) for stat in self._STATS}
    return type(self)(**newstats)

__repr__

__repr__() -> str

Statblock type as per str plus the stats and their challenge rolls.

Source code in ttrpg_dice/statblock.py
 94
 95
 96
 97
 98
 99
100
101
def __repr__(self) -> str:
    """Statblock type as per `str` plus the stats and their challenge rolls."""
    return (
        str(self)
        + "("
        + ", ".join(f"{statname}: {roll} = {self[statname]}" for statname, roll in self._STATS.items())
        + ")"
    )

__str__

__str__() -> str

A description of the Statblock type e.g. 'Human Warhammer StatBlock'.

Source code in ttrpg_dice/statblock.py
87
88
89
90
91
92
def __str__(self) -> str:
    """A description of the Statblock type e.g. 'Human Warhammer StatBlock'."""
    cls = type(self)
    bases = cls.mro()
    statblock_index = bases.index(StatBlock)
    return " ".join(base.__name__ for base in bases[:statblock_index+1])

__sub__

__sub__(other: Self) -> Self

Subtracts each stat, raises AttributeError if stat missing in other.

For example, if you want to take an NPC and remove a specific career.

Source code in ttrpg_dice/statblock.py
56
57
58
59
60
61
62
63
64
65
def __sub__(self, other: Self) -> Self:
    """
    Subtracts each stat, raises AttributeError if stat missing in `other`.

    For example, if you want to take an NPC and remove a specific career.
    """
    newstats = {
        stat: max(self[stat] - other[stat], 0) for stat in self._STATS
    }
    return type(self)(**newstats)

as_table

as_table() -> str

Render the StatBlock as a github markdown table.

Source code in ttrpg_dice/statblock.py
103
104
105
def as_table(self) -> str:
    """Render the StatBlock as a github markdown table."""
    return tabulate([[*self.values()]], headers=self.keys(), tablefmt="github")

d

d(faces: int)

A Dice class.

Source code in ttrpg_dice/dice.py
78
79
80
def __init__(self, faces: int) -> None:
    """Build a die with `faces` sides."""
    self.contents = self._Contents({faces: 1})

weighted property

weighted: bool

Is this Dice weighted, or are all results equally likely?

__add__

__add__(other: Self | SupportsInt) -> Self

Adding two Dice to gives the combined roll.

Source code in ttrpg_dice/dice.py
211
212
213
214
215
216
217
218
219
220
221
222
def __add__(self, other: Self | SupportsInt) -> Self:
    """Adding two Dice to gives the combined roll."""
    try:
        othercontents = other.contents  # pytype: disable=attribute-error
    except AttributeError:
        othercontents = defaultdict(int, {1: self._int(other, "add", "and")})

    contents = {
        face: self.contents[face] + othercontents[face] for face in set(self.contents.keys() | othercontents.keys())
    }

    return self.from_contents(contents)

__eq__

__eq__(value: object) -> bool

Dice are equal if they give the same probabilities, even with different contents.

Source code in ttrpg_dice/dice.py
167
168
169
170
171
172
def __eq__(self, value: object) -> bool:
    """Dice are equal if they give the same probabilities, even with different contents."""
    try:
        return self._probabilities == value._probabilities  # pytype: disable=attribute-error
    except AttributeError:
        return False

__getitem__

__getitem__(
    index: int | slice,
) -> float | list[float] | None

Get the probability of a specific result, or a list of probabilities in the case of a slice.

This handles the fact that dice faces are numbered from 1, not 0. Slicing with a step > 1 will return the probabilities of results beginning with the given step, not beginning with 1.

Example
>>> from ttrpg_dice import d
>>> dice = 2 * d(2)
>>> list(dice)
[0.0, 0.25, 0.5, 0.25]

>>> dice[:]
[0.0, 0.25, 0.5, 0.25]

>>> dice[1]
0.0

>>> dice[::2] # evens
[0.25, 0.25]

>>> dice[1::2] # odds
[0.0, 0.5]
Source code in ttrpg_dice/dice.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def __getitem__(self, index: int | slice) -> float | list[float] | None:
    """
    Get the probability of a specific result, or a list of probabilities in the case of a slice.

    This handles the fact that dice faces are numbered from 1, not 0. Slicing with a step > 1 will
    return the probabilities of results beginning with the given step, not beginning with 1.

    Example:
        ```
        >>> from ttrpg_dice import d
        >>> dice = 2 * d(2)
        >>> list(dice)
        [0.0, 0.25, 0.5, 0.25]

        >>> dice[:]
        [0.0, 0.25, 0.5, 0.25]

        >>> dice[1]
        0.0

        >>> dice[::2] # evens
        [0.25, 0.25]

        >>> dice[1::2] # odds
        [0.0, 0.5]
        ```
    """
    if index == 0 or index == -(len(self) + 1):
        raise DiceIndexError(self)

    try:
        # pytype: disable=attribute-error
        if index.step is None or index.step > 0:  # Positive step
            if index.start is None:
                index = slice(1 if index.step is None else index.step, index.stop, index.step)
            elif index.start == 0:  # To avoid possible confusion by slicing [0:]
                raise DiceIndexError(self)
        else:  # Negative Step  # noqa: PLR5501
            if index.stop is None:
                index = slice(index.start, 0, index.step)
        # pytype: enable=attribute-error
    except AttributeError:
        pass

    try:
        return self._probabilities[index]
    except TypeError as e:
        msg = f"Cannot index '{type(self).__name__}' with '{type(index).__name__}'."
        raise TypeError(msg) from e
    except IndexError as e:
        raise DiceIndexError(self) from e

__hash__

__hash__() -> int

Use contents for hashing - but NOT equality.

Source code in ttrpg_dice/dice.py
174
175
176
def __hash__(self) -> int:
    """Use contents for hashing - but NOT equality."""
    return hash(self.contents)

__iter__

__iter__() -> Iterator

Iterating over a Dice yields the probabilities starting with P(1).

Source code in ttrpg_dice/dice.py
111
112
113
def __iter__(self) -> Iterator:
    """Iterating over a Dice yields the probabilities starting with P(1)."""
    yield from self._probabilities[1:]

__len__

__len__() -> int

Number of faces.

Source code in ttrpg_dice/dice.py
178
179
180
def __len__(self) -> int:
    """Number of faces."""
    return len(self._probabilities) - 1

__repr__

__repr__() -> str

Classname: ndX (contents).

Source code in ttrpg_dice/dice.py
197
198
199
200
def __repr__(self) -> str:
    """Classname: ndX (contents)."""
    contents = ", ".join([f"{d}: {n}" for d, n in self.contents.items()])
    return f"{type(self).__name__}: {self} ({{{contents}}})"

__rmul__

__rmul__(other: SupportsInt) -> Self

2 * Dice(4) returns a Dice with probabilities for 2d4.

Source code in ttrpg_dice/dice.py
206
207
208
209
def __rmul__(self, other: SupportsInt) -> Self:
    """2 * Dice(4) returns a Dice with probabilities for 2d4."""
    other = self._int(other, "multiply", "by")
    return self.from_contents({f: n * other for f, n in self.contents.items()})

__str__

__str__() -> str

The type of Dice in NdX notation.

Source code in ttrpg_dice/dice.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def __str__(self) -> str:
    """The type of Dice in NdX notation."""
    sortedcontents = deque(sorted(self.contents.items()))

    # Place any constant at the end ("d4 + 2" not "2 + d4")
    if sortedcontents[0][0] == 1:
        sortedcontents.rotate(-1)

    def _ndx(n: int, x: int) -> str:
        if x == 1:
            return str(n)
        return f"{n if n > 1 else ''}d{x}"

    return " + ".join(_ndx(n, x) for x, n in sortedcontents)

from_contents classmethod

from_contents(contents: dict) -> Self

Create a new die from a dict of contents.

Source code in ttrpg_dice/dice.py
224
225
226
227
228
229
@classmethod
def from_contents(cls, contents: dict) -> Self:
    """Create a new die from a dict of contents."""
    die = cls.__new__(cls)
    die.contents = cls._Contents(contents)
    return die

from_str classmethod

from_str(description: str) -> Self

Create a new die from ndX notation.

Source code in ttrpg_dice/dice.py
231
232
233
234
235
236
237
238
239
240
@classmethod
def from_str(cls, description: str) -> Self:
    """Create a new die from ndX notation."""
    dice = description.split("+")
    contents = {
        int(x) if x else 1: int(n) if n else 1
        for die in dice
        for n, d, x in [die.strip().partition("d")]
    }
    return cls.from_contents(contents)

lazyroll

lazyroll(
    numdice: int, dicetype: int, target: int
) -> list[int]

Calculate equivalent single roll instead of rolling multiple dice targetting the same success value.

PARAMETER DESCRIPTION
numdice

the number of identical dice to roll

TYPE: int

dicetype

the number of faces on the dice to roll

TYPE: int

target

the target for a successful test

TYPE: int

Examples:

Instead of rolling 4d100 to see which of your (skilled) goblins hit your party with their arrows you can use:

>>> from ttrpg_dice import lazyroll
>>> lazyroll (4, 100, 33)
[100, 80, 40, 11, 1]
Then roll 1d100 and interpret the hits:
81-100:  0 hits
 41-80:  1 hit
 12-40:  2 hits
  2-11:  3 hits
     1:  4 hits

Source code in ttrpg_dice/manydice.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def lazyroll(numdice: int, dicetype: int, target: int) -> list[int]:
    """
    Calculate equivalent single roll instead of rolling multiple dice targetting the same success value.

    Arguments:
        numdice: the number of identical dice to roll
        dicetype: the number of faces on the dice to roll
        target: the target for a successful test

    Examples:
        Instead of rolling 4d100 to see which of your (skilled) goblins hit your party with their arrows you can use:
        ```
        >>> from ttrpg_dice import lazyroll
        >>> lazyroll (4, 100, 33)
        [100, 80, 40, 11, 1]
        ```
        Then roll 1d100 and interpret the hits:
        ```
        81-100:  0 hits
         41-80:  1 hit
         12-40:  2 hits
          2-11:  3 hits
             1:  4 hits
        ```
    """
    if target > dicetype:
        msg = f"Good luck rolling {target} on a d{dicetype}!"
        raise ValueError(msg)

    def _p(hits: int) -> float:
        """Calculates the probability of an exact number of hits."""
        misses = numdice - hits
        p_successes = (target / dicetype) ** hits
        p_fails = (1 - (target / dicetype)) ** (misses)
        return p_successes * p_fails * comb(numdice, hits)

    probs = [_p(hits) for hits in range(numdice + 1)]
    return [round(sum(probs[i:]) * dicetype) for i, _ in enumerate(probs)]