API Reference#

Basic dice rolls#

pathfinder2e_stats.roll(s: str, /, *, dims: Mapping[Hashable, int] | None = None) DataArray#
pathfinder2e_stats.roll(dice: int, faces: int, bonus: int = 0, /, *, dims: Mapping[Hashable, int] | None = None) DataArray

Roll the given number of dice with the given number of faces, sum them up, and add an optional flat bonus/penalty.

Parameters:
  • dice (int) – Number of dice to roll.

  • faces (int) – Number of faces on each die.

  • bonus (int) – Flat bonus/penalty to add to the roll. Default: 0

  • dims – Dimensions to create while rolling, in addition to roll. This is a mapping where the keys are the dimension names and the values are the number of elements along them.

Alternatively to dice, faces and bonus, you can pass a single string parameter in the format XdY, XdY+Z, or XdY-Z, which means “roll X dice with Y faces each, sum them, then add Z”.

Returns:

A DataArray containing a random series with the total result of the roll, rolled by default 100,000 times, with dims={"roll": 100_000, **dims}.

Examples:

Approximate the mean of 1d6:

>>> roll("1d6").mean().item()
3.49798

Figure out what’s the probability to get at least 10 when rolling 2d8+4:

>>> (roll("2d8+4") >= 10).mean().item()
0.84486

Attack three times with a +13 to hit, rolling separately for each attack but without increasing MAP:

>>> roll("d20+13", dims={"target": 3})
<xarray.DataArray (roll: 100000, target: 3)> Size: 2MB
array([[23, 18, 20],
       [14, 25, 28],
       [31, 20, 19],
       ...,
       [14, 20, 15],
       [32, 16, 24],
       [19, 31, 22]], shape=(100000, 3))
Dimensions without coordinates: roll, target

See Also:

pathfinder2e_stats.d20(*, fortune: bool | DataArray = False, misfortune: bool | DataArray = False, dims: Mapping[Hashable, int] | None = None) DataArray#

Roll a d20.

Parameters:
  • fortune – Set to True to roll twice and keep highest. Default: False.

  • misfortune – Set to True to roll twice and keep lowest. fortune and misfortune cancel each other out. fortune and/or misfortune can be DataArray with multiple elements. The result will be broadcasted depending on their dimensions. Default: False.

  • dims – Dimensions to create while rolling, in addition to roll. This is a mapping where the keys are the dimension names and the values are the number of elements along them.

Returns:

A DataArray containing a random series with the result of the d20 roll.

Examples:

Measure the effect of Sure Strike on the mean of an attack roll:

>>> sure_strike = xarray.DataArray(
...     [False, True], dims=["Sure Strike"],
...     coords={"Sure Strike": [False, True]},
... )
>>> d20(fortune=sure_strike).mean("roll").to_pandas()
Sure Strike
False    10.50545
True     13.83809
dtype: float64

Attack three times with a +13 to hit and increasing MAP:

>>> MAP = xarray.DataArray([0, -5, -10], dims=["target"])
>>> d20(dims={"target": 3}) + 13 + MAP
<xarray.DataArray (roll: 100000, target: 3)> Size: 2MB
array([[18, 21, 16],
       [18, 28, 15],
       [29, 24, 22],
       ...,
       [33, 24, 20],
       [19, 21, 17],
       [24, 15, 18]], shape=(100000, 3))
Dimensions without coordinates: roll, target

Note

In the last example above, the parameter dims={"target": 3} caused to roll separately for each target. Without it, the shape of the output array would be the same (due to broadcasting against the MAP array) but on each element along the roll dimension there would be a single attack roll minus 0, 5, and 10 respectively.

Degrees of Success#

class pathfinder2e_stats.DoS(*values)#

Enum for all possible check outcomes. In order to improve readability and reduce human error, you should not use the numeric values directly.

value

code

-2

no_roll

-1

critical_failure

0

failure

1

success

2

critical_success

Disequality comparisons work as expected. For example, mycheck.outcome >= DoS.success returns True for success and critical success.

See also: check() map_outcome()

Rolling checks#

pathfinder2e_stats.check(bonus: int | DataArray = 0, *, DC: int | DataArray, independent_dims: Mapping[Hashable, int | None] | Collection[Hashable] = (), dependent_dims: Collection[Hashable] = (), keen: bool | DataArray = False, perfected_form: bool | DataArray = False, fortune: bool | DataArray = False, misfortune: bool | DataArray = False, hero_point: DoS | int | Literal[False] | DataArray = False, evasion: bool | DataArray = False, incapacitation: bool | Literal[-1, 0, 1] | DataArray = False, allow_critical_failure: bool | DataArray = True, allow_failure: bool | DataArray = True, allow_critical_success: bool | DataArray = True) Dataset#

Roll a d20 and compare the result to a Difficulty Class (DC).

This can be used to simulate an attack roll, skill check, saving throw, etc. - basically anything other than a damage roll (but see damage()).

All parameters can be either scalars or DataArray. Providing array parameters will cause all the outputs to be broadcasted accordingly.

Parameters:
  • bonus – The bonus or penalty to add to the d20 roll.

  • DC – The Difficulty Class to compare the result to.

  • independent_dims

    Dimensions along which to roll independently for each point.

    This can be either a mapping where the keys are the dimension names and the values are the number of elements along them, or a collection of a subset of the dimensions of any of the input parameters.

    You may also mix dimensions that already exist in the input parameters with new dimensions in a mapping; the values for the already existing dimensions will be ignored.

    Dimension roll is always independent and must not be included.

    See examples below.

  • dependent_dims

    Dimensions along which there must be a single dice roll for all points. They must be a subset of the dimensions of the input parameters. independent_dims plus dependent_dims must cover all dimensions of the input parameters. The name of these two parameters comes from the concept in statistics of dependent and independent variables.

    Global configuration

    independent_dims and depedent_dims add to config keys check_independent_dims and check_dependent_dims respectively. If a dimension is always going to be independent or dependent throughout your workflow, you can avoid specifying it every time:

    Instead of:

    >>> check(10, DC=DC,
    ...       independent_dims=["x"],
    ...       dependent_dims=["y"])
    

    You can write: >>> set_config(check_independent_dims=[“x”], check_dependent_dims=[“y”]) >>> check(10, DC=DC) # doctest: +SKIP

Parameters:
  • keen – Set to True to Strike with a weapon inscribed with a Keen rune. Attacks with this weapon are a critical hit on a 19 on the die as long as that result is a success. This property has no effect on a 19 if the result would be a failure. Default: False.

  • perfected_form – Level 19 monk feature. On your first Strike of your turn, if you roll lower than 10, you can treat the attack roll as a 10. This is a fortune effect. Disabled when fortune=True. Default: False.

  • fortune – Set to True to roll twice and keep highest, e.g. when under the effect of Sure Strike. Default: False.

  • misfortune – Set to True to roll twice and keep lowest, e.g. when under the effect of Ill Omen. Default: False. Fortune and misfortune cancel each other out.

  • hero_point – Set to a DoS value to spend a hero point if the outcome is equal to or less than the given value. e.g. hero_point=DoS.critical_failure rerolls only critical failures, whereas hero_point=DoS.failure rerolls anything less than a success. Hero points are a fortune effect, so they can’t be used when fortune is True.

  • evasion – Passed to map_outcome() post-processing.

  • incapacitation – Passed to map_outcome() post-processing.

  • allow_critical_failure – Passed to map_outcome() post-processing.

  • allow_failure – Passed to map_outcome() post-processing.

  • allow_critical_success – Passed to map_outcome() post-processing.

Returns:

A Dataset containing the following variables:

bonus, etc.

As the parameter. Only present when not the default value.

natural

The result of the natural d20 roll before adding the bonus

use_hero_point

Whether a hero point was used to reroll the outcome. Only present if hero_point is not False.

original_outcome

The outcome of the check before any modifications by map_outcome(). Only present if any parameters to the function are specified.

outcome

The final outcome of the check

Examples:

Strike an enemy with AC18 with a +10 weapon:

>>> check(10, DC=18)
<xarray.Dataset> Size: 2MB
Dimensions:  (roll: 100000)
Dimensions without coordinates: roll
Data variables:
    bonus    int64 8B 10
    DC       int64 8B 18
    natural  (roll) int64 800kB 18 13 11 6 7 1 2 1 4 17 ... 8 4 15 3 14 13 1 1 4
    outcome  (roll) int64 800kB 2 1 1 0 0 -1 0 -1 0 1 ... -1 1 0 1 0 1 1 -1 -1 0
Attributes:
    legend:   {-2: 'No roll', -1: 'Critical failure', 0: 'Failure', 1: 'Succe...

Strike three times in sequence, with MAP, and test how the same strike works out differently against a henchman with AC16 or a boss with AC20:

>>> MAP = DataArray([0, -5, -10], dims=["action"])
>>> targets = DataArray([16, 20], coords={"target": ["henchman", "boss"]})
>>> outcome = check(10 + MAP, DC = targets,
...                 independent_dims=["action"],
...                 dependent_dims=["target"])
>>> outcome
<xarray.Dataset> Size: 7MB
Dimensions:  (action: 3, target: 2, roll: 100000)
Coordinates:
  * target   (target) <U8 64B 'henchman' 'boss'
Dimensions without coordinates: action, roll
Data variables:
    bonus    (action) int64 24B 10 5 0
    DC       (target) int64 16B 16 20
    natural  (roll, action) int64 2MB 10 12 3 19 9 5 1 19 ... 6 2 15 2 4 15 11 9
    outcome  (roll, action, target) int64 5MB 1 1 1 0 -1 -1 2 ... 1 1 1 0 0 -1
Attributes:
    legend:   {-2: 'No roll', -1: 'Critical failure', 0: 'Failure', 1: 'Succe...

Note the parameters independent_dims and dependent_dims. They cause check() to roll independently for each value of MAP, but to reuse the same roll against different targets. This is reflected by the dimensionality of the natural and the outcome arrays.

Study the roll above:

>>> (
...     outcome_counts(outcome)
...     .stack(row=["target", "action"])
...     .round(2).T.to_pandas() * 100.0
... )
outcome          Critical success  Success  Failure  Critical failure
target   action
henchman 0                   25.0     50.0     20.0               5.0
         1                    5.0     45.0     45.0               5.0
         2                    5.0     20.0     45.0              30.0
boss     0                    5.0     50.0     40.0               5.0
         1                    5.0     25.0     45.0              25.0
         2                    5.0      0.0     45.0              50.0

Roll a DC20 reflex save with a +12 bonus, evasion (which converts a success into a critical success), and spend a hero point on failure or critical failure:

>>> c = check(12, DC=20, hero_point=DoS.failure, evasion=True)
>>> outcome_counts(c).to_pandas()
outcome
Critical success    0.87786
Failure             0.10424
Critical failure    0.01790
Name: outcome, dtype: float64
>>> c.use_hero_point.value_counts("roll", normalize=True).to_pandas()
unique_value
False    0.6524
True     0.3476
Name: use_hero_point, dtype: float64
pathfinder2e_stats.map_outcome(outcome: DataArray | Dataset, map_: Mapping[DoS | int | DataArray, Any] | Iterable[tuple[DoS | int | DataArray, Any]] | None = None, /, *, evasion: bool | DataArray = False, incapacitation: bool | Literal[-1, 0, 1] | DataArray = False, allow_critical_failure: bool | DataArray = True, allow_failure: bool | DataArray = True, allow_critical_success: bool | DataArray = True) DataArray | Dataset#

Convert the output of check() following a set of rules.

This function is typically called indirectly, through the keyword arguments of check().

All parameters can either be scalars or DataArray.

Parameters:
  • outcome – Either the Dataset returned by check() or just its outcome variable.

  • map – An arbitrary {from: to, ...} mapping or [(from, to), ...] sequence of tuples of outcomes. from must be DoS values or their int equivalents. to can be anything, including other dtypes such as strings. This is applied after all other rules. Any DoS value not in from is mapped to the null value by default. Default: no bespoke mapping.

  • evasion

    Set to True to convert a success into a critical success. Default: False.

    Note

    This is a catch-all parameter for any equivalent class feature or feat, such as juggernaut, bravery, risky surgery, etc. Each class has a different name for them, most times purely for the sake of flavour.

  • incapacitation

    Set to True when an incapacitation effect is applied to a creature whose level is more than twice the effect rank. If 1 or True, all outcomes are improved by one notch (use this for the creature’s saving throws). If -1, all outcomes are worsened by one notch (use this for checks against the creature). Default: False.

    See also level2rank() and rank2level().

  • allow_critical_failure – Set to False if there is no critical failure effect. If False, all critical failures are mapped to simple failures. Default: True.

  • allow_failure – Set to False if there is no failure effect. If False, all failures will be mapped to success. Default: True.

  • allow_critical_success – Set to False if there is no critical success effect. If False, all critical successes will be mapped to simple successes. Default: True.

Returns:

If outcome is the Dataset returned by check(), return a shallow copy of it with the outcome variable replaced and the previous outcome stored in original_outcome. If outcome is a DataArray, return a new DataArray with the mapped outcomes. If map_ is defined, the dtype of the return value will be the dtype of the values of map_; otherwise it will be int like the input.

Examples:

Cast a 5th rank Calm spell (DC30) and catch in the area three targets:

  • A level 8 creature;

  • A level 11 creature, who therefore benefits from the spell’s incapacitation trait;

  • A level 9 cleric, who benefits from the Resolute Faith class feature:

    >>> spell_rank = 5
    >>> targets = xarray.Dataset({
    ...     "level": ("target", [8, 11, 9]),
    ...     "bonus": ("target", [16, 21, 24]),
    ...     "evasion": ("target", [False, False, True])})
    >>> check(targets.bonus,
    ...       DC=30,
    ...       independent_dims=["target"],
    ...       evasion=targets.evasion,
    ...       incapacitation=rank2level(spell_rank) < targets.level)
    <xarray.Dataset> Size: 7MB
    Dimensions:           (target: 3, roll: 100000)
    Dimensions without coordinates: target, roll
    Data variables:
        bonus             (target) int64 24B 16 21 24
        DC                int64 8B 30
        evasion           (target) bool 3B False False True
        incapacitation    (target) bool 3B False True False
        natural           (roll, target) int64 2MB 18 13 11 6 7 1 ... 10 9 11 12 12
        original_outcome  (roll, target) int64 2MB 1 1 1 0 0 -1 -1 ... -1 1 1 0 1 1
        outcome           (roll, target) int64 2MB 1 2 2 0 1 -1 -1 ... -1 2 2 0 2 2
    Attributes:
        legend:    {-2: 'No roll', -1: 'Critical failure', 0: 'Failure', 1: 'Succe...
    
pathfinder2e_stats.outcome_counts(outcome: DataArray | Dataset, dim: Hashable = 'roll', *, new_dim: Hashable = 'outcome', normalize: bool = True) DataArray#

Count the occurrences of each outcome in a check.

Parameters:
  • outcome – Either the Dataset returned by check() or map_outcome() or just their outcome variable.

  • dim – The dimension to reduce when counting the outcomes. Default: roll.

  • new_dim – The name of the new dimension containing all outcome values. Default: outcome. The new dimension is sorted from critical success to critical failure and contains only the values that actually occurred.

  • normalize – If True, normalize the counts so that they add up to 1. If False, return the raw counts. Default: True.

Returns:

A DataArray containing the counts of each outcome, with the same dimensions as the input, minus dim, plus new_dim.

See also: value_counts

Examples:

>>> outcome_counts(check(12, DC=20))
<xarray.DataArray 'outcome' (outcome: 4)> Size: 32B
array([0.14827, 0.50276, 0.29844, 0.05053])
Coordinates:
  * outcome  (outcome) <U16 256B 'Critical success' ... 'Critical failure'
pathfinder2e_stats.sum_bonuses(*args: tuple[Literal['untyped', 'ability', 'circumstance', 'proficiency', 'status', 'item'], int | DataArray]) Any#

Sum bonuses and penalties by type to calculate the total bonus/penalty.

Bonuses of the same type don’t stack. Penalties of the same type don’t stack, but bonuses and penalties of the same type subtract from each other. Untyped bonuses and penalties always stack.

Parameters:

*args

(bonus type, value), ..., where bonus type must be one of:

  • untyped

  • ability

  • circumstance

  • proficiency

  • status

  • item

and value must be an integer or a DataArray.

Returns:

Sum of all bonuses and penalties. If all values are int, return an int; otherwise return a DataArray.

Examples:

>>> sum_bonuses(("status", 1), ("status", 2), ("circumstance", 3))
5

Damage profiles#

class pathfinder2e_stats.Damage(type: str, dice: int, faces: int, bonus: int = 0, multiplier: float = 1, persistent: bool = False, splash: bool = False, two_hands: int = 0, deadly: int = 0, fatal: int = 0, fatal_aim: int = 0, basic_save: bool = False)#

Damage roll specification, e.g. for a weapon or a spell.

Parameters:
  • type (str) – Damage type, e.g. “fire”.

  • dice (int) – Number of dice to roll.

  • faces (int) – Number of faces on each die.

  • bonus (int) – Flat bonus to add to the roll.

  • persistent (bool) – Whether the damage is persistent. It is tracked separately by damage(). Default: False.

  • splash (bool) – Whether the damage is splash. Splash damage is applied on a simple miss and doesn’t double on a critical hit. It is tracked separately by damage(). Default: False.

The following parameters can only be used when using Damage on its own, and never while manually building an ExpandedDamage:

Parameters:
  • two_hands (int) – Number of faces on the die when using a weapon with the two-hands dX trait in two hands. Before expanding the damage, you will need to call the hands() method to clarify how many hands you’re using. Default: no two-hands trait.

  • deadly (int) – Number of faces on the extra dice to add on a critical hit for weapons with the deadly dX trait. Default: no deadly trait.

  • fatal (int) – Number of faces to promote all dice to and for the extra die on critical hits with a weapon with the fatal dX trait. Default: no fatal trait.

  • fatal_aim (int) – Number of faces to promote all dice to and for the extra die on critical hits with a weapon with the fatal aim dX trait that is being held in two hands. Before expanding the damage, you will need to call the hands() method to clarify how many hands you’re using. Default: no fatal aim trait.

  • basic_save (bool) –

    False (default)

    This is a weapon or an attack spell that follows the normal rules for Strikes and spell attack rolls: full damage on a success, double damage on a critical success, and no damage on a failure.

    This can be altered by the splash trait.

    True

    This is a spell with a basic saving throw: full damage on a failure, double damage on a critical failure, half damage on a success and no damage on a critical success.

The following parameter can only be used when manually building a ExpandedDamage:

Parameters:

multiplier (float) – 0.5, 1, or 2. Number to multiply the damage by after rolling it.

Compact damage specification

Basic weapons and most spells can be specified with a single Damage. e.g. a Fireball:

>>> Damage("fire", 6, 6, basic_save=True)
**Damage** 6d6 fire, with a basic saving throw

A +1 striking longbow:

>>> Damage("piercing", 2, 8, deadly=10)
**Damage** 2d8 piercing deadly d10

You can define effects such as weapons with runes by chaining Damage instances with the + operator. For example:

A +1 striking wounding longsword, wielded by a PC with a +4 STR modifier:

>>> Damage("slashing", 2, 8, 4) + Damage("bleed", 1, 6, persistent=True)
**Damage** 2d8+4 slashing plus 1d6 persistent bleed

A weapon or spell with direct, splash and/or persistent component can be specified by adding everything up with +. For example, an Alchemist’s Fire:

>>> (Damage("fire", 1, 8)
... + Damage("fire", 0, 0, 1, persistent=True)
... + Damage("fire", 0, 0, 1, splash=True))
**Damage** 1d8 fire plus 1 persistent fire plus 1 fire splash

Complex cases

Some damage profiles follow neither the general rule for weapons and attack spells nor the rule for basic saving throws. To define them, you need to use ExpandedDamage instead.

basic_save: bool#
bonus: int#
copy(**kwargs: Any) Damage#

Create a deep copy of this instance, changing one or more parameters.

Parameters:

**kwargs – Any of the Damage parameters.

e.g. a longsword with Inventive Offensive adding deadly d6:

>>> Damage("slashing", 1, 8).copy(deadly=6)
**Damage** 1d8 slashing deadly d6
deadly: int#
dice: int#
expand() ExpandedDamage#

Convert this Damage instance to an ExpandedDamage.

This resolves the two-hands, deadly, fatal, and fatal_aim traits, as well as applying the success profile for weapon strikes (basic_save=False), spells with a basic saving throw (basic_save=True), and splash damage.

You typically don’t need to call this method explicitly.

faces: int#
fatal: int#
fatal_aim: int#
hands(hands: Literal[1, 2]) Damage#

Specify how many hands are used to wield the weapon.

You should always call this method explicitly for weapons with either the two-hands or the fatal aim traits.

increase_die() Damage#

Increase the damage die size by 1 step.

e.g. a Dagger with Deadly Simplicity:

>>> Damage("piercing", 1, 4).increase_die()
**Damage** 1d6 piercing
multiplier: float#
persistent: bool#
reduce_die() Damage#

Reduce the damage die size by 1 step.

e.g. a +1 Striking Maul with Grasping Reach:

>>> Damage("bludgeoning", 2, 12).reduce_die()
**Damage** 2d10 bludgeoning
static simplify(damages: Iterable[Damage], /) list[Damage]#

Attempt to reduce multiple Damage instances to a shorter form.

Only compatible damage types are combined; e.g. slashing + fire will remain separate, as will direct fire damage and splash fire damage.

This method is typically called automatically.

Example:

>>> Damage.simplify([Damage("fire", 1, 6), Damage("fire", 1, 6, 4)])
[**Damage** 2d6+4 fire]
splash: bool#
two_hands: int#
type: str#
vicious_swing(dice: int = 1) Damage | ExpandedDamage#

Vicious Swing, a.k.a. Power Attack, and similar effects.

Add extra weapon dice, which impact the fatal trait but not the deadly trait.

Parameters:

dice (int) – Number of extra weapon dice to add. Default: 1

Returns:

Damage or ExpandedDamage

Note

This is not the same as just adding a damage die. Observe the difference:

  • A +2 Striking Glaive with Vicious Swing, which deals 3d8 damage on a hit and an extra d8 on a critical hit:

    >>> Damage("slashing", 2, 8, deadly=8).vicious_swing()
    **Critical success** (3d8)x2 slashing plus 1d8 slashing
    **Success** 3d8 slashing
    
  • A +2 Greater Striking Glaive, which deals 3d8 damage on a hit and an extra 2d8 on a critical hit:

    >>> Damage("slashing", 3, 8, deadly=8).expand()
    **Critical success** (3d8)x2 slashing plus 2d8 slashing
    **Success** 3d8 slashing
    
class pathfinder2e_stats.DamageList(initlist=None)#

Output of the addition of Damage.

This class should never be initialised directly; it is returned by applying the + operator to two or more Damage objects.

property basic_save: bool#
expand() ExpandedDamage#

Convert DamageList to ExpandedDamage.

simplify() DamageList#

See Damage.simplify().

class pathfinder2e_stats.ExpandedDamage(data: Damage | Iterable[Damage] | ExpandedDamage | Mapping[int, Collection[Damage]] | Mapping[DoS, Collection[Damage]] | None = None, /)#

Expanded damage specification.

This can be either generated by calling Damage.expand() or by manually building a complex damage profile, which doesn’t need to respect the rules for weapon strikes or basic saving throws.

Parameters:

data

A Damage, a sequence of Damage, or a mapping of DoS to lists of Damage.

In the latter case, you need to explicitly specify what happens on each roll outcome; you cannot use the basic_save, deadly, fatal, or fatal_aim attributes. Omitted outcomes deal no damage.

You can also initialize this class by adding a plain dict of {DoS:[Damage]} to a Damage object.

Examples

A Flaming rune adds 1d6 fire to your weapon, with an additional 1d10 persistent fire on a critical hit. The 1d6 can be expressed with a simple Damage, but the extra effect on the critical hit can’t.

A +1 Striking Flaming Rapier can be defined as:

>>> Damage("piercing", 2, 6, deadly=8) + Damage("fire", 1, 6) + {
...     DoS.critical_success: [Damage("fire", 1, 10, persistent=True)]
... }
**Critical success** (2d6)x2 piercing plus 1d8 piercing plus (1d6)x2 fire
plus 1d10 persistent fire
**Success** 2d6 piercing plus 1d6 fire

Above we implicitly initialized an ExpandedDamage by auto-expanding the deadly trait by adding a dict to a Damage object. The above is equivalent to:

>>> rapier = ExpandedDamage({
...     DoS.success: [Damage("piercing", 2, 6)],
...     DoS.critical_success: [
...         Damage("piercing", 2, 6, multiplier=2),
...         Damage("piercing", 1, 8),
...     ],
... })
>>> flaming = ExpandedDamage({
...     DoS.success: [Damage("fire", 1, 6)],
...     DoS.critical_success: [
...         Damage("fire", 1, 6, multiplier=2),
...         Damage("fire", 1, 10, persistent=True),
...     ],
... })
>>> rapier + flaming
**Critical success** (2d6)x2 piercing plus 1d8 piercing plus (1d6)x2 fire
plus 1d10 persistent fire
**Success** 2d6 piercing plus 1d6 fire

Which is the same as writing:

>>> ExpandedDamage({
...     DoS.success: [
...         Damage("piercing", 2, 6),
...         Damage("fire", 1, 6),
...     ],
...     DoS.critical_success: [
...         Damage("piercing", 2, 6, multiplier=2),
...         Damage("piercing", 1, 8),
...         Damage("fire", 1, 6, multiplier=2),
...         Damage("fire", 1, 10, persistent=True),
...     ],
... })
**Critical success** (2d6)x2 piercing plus 1d8 piercing plus (1d6)x2 fire
plus 1d10 persistent fire
**Success** 2d6 piercing plus 1d6 fire

What if the reapier was used for a swashbuckler’s Confident Finisher, which adds 2d6 precision damage, and half as much on a failure?

>>> p = Damage("precision", 2, 6)
>>> finisher = {
...     DoS.failure: [p.copy(multiplier=0.5)],
...     DoS.success: [p],
...     DoS.critical_success: [p.copy(multiplier=2)],
... }
>>> rapier + flaming + finisher
**Critical success** (2d6)x2 piercing plus 1d8 piercing plus (1d6)x2 fire
plus (2d6)x2 precision plus 1d10 persistent fire
**Success** 2d6 piercing plus 1d6 fire plus 2d6 precision
**Failure** (2d6)/2 precision
expand() ExpandedDamage#

Dummy method to match Damage and DamageList interface.

filter(*which: Literal['direct', 'persistent', 'splash']) ExpandedDamage#

Select only direct and/or persistent and/or splash damage.

simplify() ExpandedDamage#

See Damage.simplify().

static sum(items: Iterable[Damage | Iterable[Damage] | ExpandedDamage | Mapping[int, Collection[Damage]] | Mapping[DoS, Collection[Damage]]]) ExpandedDamage#

Sum multiple Damage or ExpandedDamage objects together.

to_dict_of_str() dict[str, str]#

Pretty-print as a dict

Rolling for damage#

pathfinder2e_stats.damage(check_outcome: Dataset, damage_spec: Damage | Iterable[Damage] | ExpandedDamage | Mapping[int, Collection[Damage]] | Mapping[DoS, Collection[Damage]], *, independent_dims: Collection[Hashable] = (), dependent_dims: Collection[Hashable] = (), weaknesses: Mapping[str, int] | DataArray | None = None, resistances: Mapping[str, int] | DataArray | None = None, immunities: Mapping[str, bool] | Collection[str] | DataArray | None = None, persistent_damage_rounds: int = 3, persistent_damage_DC: int | Mapping[str, int] | DataArray = 15, splash_damage_targets: int = 2) Dataset#

Roll for damage.

Parameters:
  • check_outcome – The outcome of the check that caused the damage. This must be the return value of check(), typically either for an attack roll or for a saving throw (but it could also be a skill check).

  • damage_spec – The damage specification to use for rolling damage. This must be a Damage, ExpandedDamage, a dict representing an ExpandedDamage, or a combination thereof using the + operator.

  • independent_dims

    Dimensions along which to roll independently for each point.

    This must be a subset of the dimensions of any of the input parameters. Note that a dimension may be:

    • independent in both check() and damage(); e.g. two iterative Strikes;

    • independent in check(), but dependent in damage(); e.g. multiple targets roll a check to save vs. Fireball, damage rolled only once;

    • dependent in both check() and damage(), e.g. Swipe vs. two targets, or a what-if analysis of the same strike vs. two different targets or from two different attackers.

    Dimensions roll and damage_type are always independent and must not be included.

    See examples below.

  • dependent_dims

    Dimensions along which there must be a single dice roll for all points.

    See check() for more details.

    Global configuration

    independent_dims and depedent_dims add to config keys damage_independent_dims and damage_dependent_dims respectively. If a dimension is always going to be independent or dependent throughout your workflow, you can avoid specifying it every time:

    Instead of:

    >>> damage(check_outcome, spec,
    ...        independent_dims=["x"],
    ...        dependent_dims=["y"])
    

    You can write: >>> set_config(damage_independent_dims=[“x”], damage_dependent_dims=[“y”]) >>> damage(check_outcome, spec) # doctest: +SKIP

  • weaknesses

    Optional weaknesses to apply to the damage, in the format {damage type: value}, where the damage type may or may not match Damage.type; e.g. {"fire": 5}.

    You may alternatively pass a DataArray with int dtype, a damage_type dimension with associated coordinate, and optional dimensions matching different targets.

    e.g.:

    >>> weaknesses = xarray.DataArray(
    ...     [[5, 5, 0], [0, 10, 10]], dims=["target", "damage_type"],
    ...     coords={"target": ["assassin vine", "zombie  brute"],
    ...             "damage_type": ["fire", "slashing", "vitality"]})
    

  • resistances – Optional resistances to apply to the damage, in the same format as weaknesses.

  • immunities

    Optional immunities to apply to the damage, in the format [damage type, ...] or {damage type: bool}; e.g. ["fire"] or {"fire": True}.

    Note

    Living creatures are not immune to vitality by default. If you want to simulate e.g. a vitalizing() weapon against a living target, you must explicitly set immunities={"vitality": True}.

    You may alternatively pass a DataArray with bool dtype and otherwise the same format as weaknesses. Continuing from the example above:

    >>> immunities = xarray.DataArray(
    ...     [[True, False], [False, True]],
    ...     dims=["target", "damage_type"],
    ...     coords={"target": ["assassin vine", "zombie  brute"],
    ...             "damage_type": ["vitality", "void"]})
    

  • persistent_damage_rounds (int) – The number of rounds for which persistent damage should be applied at most, beyond which one expects that either the target was defeated or the encounter ended (and the persistent damage alone is assumed not to be life threatening). Default: 3 rounds.

  • persistent_damage_DC – The DC of the flat check to end persistent damage. This may be an integer between 2 and 20, a dict mapping damage types to DCs, or a DataArray with the same format as weaknesses. Default: DC15.

  • splash_damage_targets (int) – The number of targets affected by splash damage, including the main target. When calculating total damage, splash damage will be multiplied by this number. Default: 2 targets (main + 1 secondary target).

Returns:

A shallow copy of check_outcome with additional variables for the damage:

damage_type

Coordinate matching each damage type. Always one-dimensional even when there is a single damage type.

direct_damage

The direct damage dealt to the target, grouped by damage type. Has dimensions ("roll", "damage_type") plus whatever additional dimensions check.outcome has. For each point it contains a roll of the damage_spec matching the check outcome, so in case of critical hit it is typically doubled (unless the damage spec specifies otherwise) etc. There is only one roll per outcome per roll, so e.g. in case of multiple targets the damage is rolled only once. Only present if there is any direct damage in the damage_spec.

splash_damage

The splash damage dealt to each target, with the same dimensions as direct_damage. Only present if there is any splash damage in the damage_spec.

persistent_damage

The persistent damage dealt, grouped by damage type, with dimensions ("roll", "damage_type", "persistent_round"). persistent_round has size equal to persistent_damage_rounds and no associated coordinate. Element 0 is the damage received at the end of the target’s first round after the damage is applied, and so on. This is rolled before the flat check to end damage so it is always populated even after a successful check. Only present if there is any persistent damage in the damage_spec.

persistent_damage_DC

As the parameter. Only present if there is any persistent damage in the damage_spec.

persistent_damage_check

Outcome of the flat check to end persistent damage at the end of each round, with dimensions ("roll", "damage_type", "persistent_round"). Only present if there is any persistent damage in the damage_spec.

apply_persistent_damage

True if persistent damage is applied at the end of the target’s round; False if there was a successful check on a previous round.

total_damage

An approximation of total damage dealt to the target(s), assuming that

  • there are splash_damage_targets targets affected by splash damage;

  • the target doesn’t expire before the end of the persistent damage rounds;

  • there is no assistance to end persistent damage earlier.

Total damage is calculated as:

total_damage = (
    direct_damage
    + splash_damage * splash_damage_targets
    + where(apply_persistent_damage, persistent_damage, 0)
            .sum("persistent_round")
).sum("damage_type")

The dataset also includes a new attribute:

damage_spec

String representation of the damage_spec parameter.

Examples:

Strike an AC17 enemy with a Longsword (+8 to hit, 1d8+4 damage):

>>> spec = Damage("slashing", 1, 8, 4)
>>> attack_roll = check(8, DC=17)
>>> damage(attack_roll, spec)
<xarray.Dataset> Size: 3MB
Dimensions:        (roll: 100000, damage_type: 1)
Coordinates:
  * damage_type    (damage_type) <U8 32B 'slashing'
Dimensions without coordinates: roll
Data variables:
    bonus          int64 8B 8
    DC             int64 8B 17
    natural        (roll) int64 800kB 18 13 11 6 7 1 2 1 ... 4 15 3 14 13 1 1 4
    outcome        (roll) int64 800kB 1 1 1 0 0 -1 0 -1 0 ... 0 1 0 1 1 -1 -1 0
    direct_damage  (roll, damage_type) int64 800kB 8 9 5 0 0 0 ... 0 11 6 0 0 0
    total_damage   (roll) int64 800kB 8 9 5 0 0 0 0 0 0 ... 0 0 5 0 11 6 0 0 0
Attributes:
    legend:       {-2: 'No roll', -1: 'Critical failure', 0: 'Failure', 1: 'S...
    damage_spec:  {'Critical success': '(1d8+4)x2 slashing', 'Success': '1d8+...

Strike with an Alchemist’s Fire:

>>> spec = (Damage("fire", 1, 8)
... + Damage("fire", 0, 0, 1, persistent=True)
... + Damage("fire", 0, 0, 1, splash=True))
>>> attack_roll = check(8, DC=17)
>>> damage(attack_roll, spec)
<xarray.Dataset> Size: 9MB
Dimensions:                  (roll: 100000, damage_type: 1, persistent_round: 3)
Coordinates:
  * damage_type              (damage_type) <U4 16B 'fire'
Dimensions without coordinates: roll, persistent_round
Data variables:
    bonus                    int64 8B 8
    DC                       int64 8B 17
    natural                  (roll) int64 800kB 5 13 13 5 20 ... 10 9 11 12 12
    outcome                  (roll) int64 800kB 0 1 1 0 2 1 1 ... 0 0 1 1 1 1 1
    direct_damage            (roll, damage_type) int64 800kB 1 3 4 1 ... 3 7 6 5
    splash_damage            (roll, damage_type) int64 800kB 1 1 1 1 ... 1 1 1 1
    persistent_damage        (roll, damage_type, persistent_round) int64 2MB ...
    persistent_damage_DC     (damage_type) int64 8B 15
    persistent_damage_check  (roll, damage_type, persistent_round) int64 2MB ...
    apply_persistent_damage  (roll, damage_type, persistent_round) bool 300kB ...
    total_damage             (roll) int64 800kB 2 7 6 2 18 11 ... 2 6 7 11 9 9
Attributes:
    legend:                 {-2: 'No roll', -1: 'Critical failure', 0: 'Failu...
    damage_spec:            {'Critical success': '(1d8)x2 fire plus 2 persist...
    splash_damage_targets:  2

Engulf three targets in a Fireball with DC21 basic reflex save. The targets have respectively Reflex bonus +11, +13, and +15. The last target also has resistance 5 to fire:

>>> spec = Damage("fire", 6, 6, basic_save=True)
>>> reflex_bonus = DataArray([11, 13, 15], dims=["target"])
>>> resistances = DataArray(
...     [[0, 0, 5]], dims=["damage_type", "target"],
...     coords={"damage_type": ["fire"]})
>>> # Roll savint throw separately for each target
>>> saving_throw = check(reflex_bonus, DC=21, independent_dims=["target"])
>>> dmg = damage(saving_throw, spec, resistances=resistances,
...              # Roll damage once for all targets and halve/double as needed
...              dependent_dims=["target"])
>>> dmg
<xarray.Dataset> Size: 10MB
Dimensions:        (target: 3, roll: 100000, damage_type: 1)
Coordinates:
  * damage_type    (damage_type) <U4 16B 'fire'
Dimensions without coordinates: target, roll
Data variables:
    bonus          (target) int64 24B 11 13 15
    DC             int64 8B 21
    natural        (roll, target) int64 2MB 13 4 9 11 12 7 5 ... 12 2 3 7 11 4 1
    outcome        (roll, target) int64 2MB 1 0 1 1 1 1 0 1 ... 0 1 0 0 1 1 0 -1
    direct_damage  (roll, target, damage_type) int64 2MB 15 30 10 11 ... 6 13 21
    resistances    (damage_type, target) int64 24B 0 0 5
    total_damage   (roll, target) int64 2MB 15 30 10 11 11 6 ... 12 12 1 6 13 21
Attributes:
    legend:       {-2: 'No roll', -1: 'Critical failure', 0: 'Failure', 1: 'S...
    damage_spec:  {'Success': '(6d6)/2 fire', 'Failure': '6d6 fire', 'Critica...

How much damage did each target take, on average?

>>> dmg.total_damage.mean("roll").to_pandas()
target
0    15.65510
1    13.47350
2     7.64498
Name: total_damage, dtype: float64

Utility functions#

pathfinder2e_stats.level2rank(level: int | DataArray, *, dedication: bool = False) int | DataArray#

Convert a creature’s or item’s level to their rank, e.g. to determine if they’re affected by the incapacitation trait or to counteract their abilities. It can also be used to determine a spellcaster’s maximum spell rank.

Parameters:
  • level – The creature’s level

  • dedication – Set to True to return the highest spell slot rank of a character with spellcaster Dedication who took Basic, Expert and Master Spellcasting feats at levels 4, 12 and 18 respectively. Defaults to False.

Returns:

The creature’s rank or spellcaster’s maximum spell rank. Return type matches the type of level.

pathfinder2e_stats.rank2level(rank: int | DataArray, *, dedication: bool = False) int | DataArray#

Convert a spell or effect’s rank to a creature’s or item’s maximum level in that rank, e.g. the maximum level of a creature that doesn’t benefit from the incapacitation trait.

Subtract one to the output for the minimum level in the same rank, or to determine the minimum level of a spellcaster in order to be able to cast a spell of a given rank.

Parameters:
  • rank – The spell or effect’s rank

  • dedication – Set to True to return the level a character with spellcaster Dedication who took Basic, Expert and Master Spellcasting feats at levels 4, 12 and 18 respectively needs to be to gain a spell slot of this rank. Defaults to False.

Returns:

The creature’s maximum level within the rank. Return type matches the type of rank.

Xarray extensions#

When you import pathfinder2e_stats, all DataArray and Dataset objects gain these new methods:

xarray.DataArray.value_counts(dim: Hashable, *, new_dim: Hashable = 'unique_value', normalize: bool = False) xarray.DataArray#

Return the count of unique values for every point along dim, individually for each other dimension.

This is conceptually the same as calling pandas.Series.value_counts() individually for every series of a DataFrame and then merging the output.

Parameters:
  • dim – Name of the dimension to count the values along. It will be removed in the output array.

  • new_dim – Name of the new dimension in the output array. Default: unique_value

  • normalize – Return proportions rather than frequencies. Default: False

Returns:

DataArray with the same dimensions as the input array, minus dim, plus new_dim.

xarray.DataArray.display(name: str = None, *, max_rows: int = 26, describe: bool | Literal['auto'] = 'auto', transpose: bool = False) None#
xarray.Dataset.display(*, max_rows: int = 26, describe: bool | Literal['auto'] = 'auto', transpose: bool = False) None#

Pretty-print the DataArray or Dataset in Jupyter notebook. Unlike the default xarray display, this method prioritizes observing the data rather than the structure.

The longest dimension of the DataArray/Dataset is plotted on the rows; all other dimensions are stacked along the columns. Display multiple dataframes if there are variables that don’t share the longest dimension.

Parameters:
  • name – Override DataArray name. Not used for Datasets.

  • max_rows – Maximum number of rows to display. Default: 26

  • describe

    auto (default)

    If the rows of a DataFrame are more than max_rows, replace them with a statistical summary (min, max, mean, etc.).

    True

    Always replace the rows regardless of number

    False

    Always show the individual rows, but potentially trim those in the middle if they’re more than max_rows.

    See pandas.DataFrame.describe() for details on the summary statistics.

  • transpose – If True, transpose rows and columns just before displaying. Default: False

Configuration#

pathfinder2e_stats.set_config(roll_size: int | None = None, check_independent_dims: Collection[Hashable] | None = None, check_dependent_dims: Collection[Hashable] | None = None, damage_independent_dims: Collection[Hashable] | None = None, damage_dependent_dims: Collection[Hashable] | None = None) None#

Set one or more library settings. All settings are thread-local.

Parameters:
  • roll_size – Number of rolls in all simulations. Default: 100_000.

  • check_independent_dims

    Default independent_dims parameter for check().

    If the independent_dims parameter is explicitly specified in the function call, the parameter items adds to this set. If the dependent_dims parameter is explicitly specified in the function call, the parameter items detract from this set.

    You may also add/remove single elements with:

    >>> get_config()["check_independent_dims"].add("my_dim")
    

    Default: empty set (but roll is always independent).

  • check_dependent_dims

    Default dependent_dims parameter for check().

    If the dependent_dims parameter is explicitly specified in the function call, the parameter items adds to this set. If the independent_dims parameter is explicitly specified in the function call, the parameter items detract from this set.

    You may also add/remove single elements with:

    >>> get_config()["check_dependent_dims"].add("my_dim")
    

    Default: empty set.

  • damage_independent_dims

    Default independent_dims parameter for damage(). All notes for check_independent_dims apply.

    Default: empty set (but roll and damage_type are always independent).

  • damage_dependent_dims

    Default dependent_dims parameter for damage(). All notes for check_dependent_dims apply.

    Default: empty set.

pathfinder2e_stats.get_config() Config#

Return the current configuration settings.

class pathfinder2e_stats.config.Config#

dict returned by get_config().

check_dependent_dims: set[Hashable]#

Default dependent_dims parameter for check().

check_independent_dims: set[Hashable]#

Default independent_dims parameter for check().

damage_dependent_dims: set[Hashable]#

Default dependent_dims parameter for damage().

damage_independent_dims: set[Hashable]#

Default independent_dims parameter for damage().

roll_size: int#

Number of rolls in all simulations. Default: 100_000.

pathfinder2e_stats.seed(n: Any | None = None) None#

Seed the library-global, thread-local random number generator.

Accepts the same parameter as numpy.random.default_rng(), which means that calling it with no arguments will produce a different random sequence every time.

By default, the random number generator is seeded to 0 for all new threads. This means, for example, that restarting and rerunning the same Jupyter notebook will produce identical results, but true multi-threaded applications don’t need to worry about seeding. However, the side effect is that multi-process and multi-threaded applications need to be careful to call seed() on each thread and process or will produce the same sequence of random numbers everywhere.

See also seed().