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, orXdY-Z, which means “roll X dice with Y faces each, sum them, then add Z”.- Returns:
A
DataArraycontaining a random series with the total result of the roll, rolled by default 100,000 times, withdims={"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
DataArraywith 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
DataArraycontaining 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 theMAParray) 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.successreturns 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
DoSvalue to spend a hero point if the outcome is equal to or less than the given value. e.g.hero_point=DoS.critical_failurererolls only critical failures, whereashero_point=DoS.failurererolls 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
Datasetcontaining 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_dimsanddependent_dims. They causecheck()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
Datasetreturned bycheck()or just its outcome variable.map – An arbitrary
{from: to, ...}mapping or[(from, to), ...]sequence of tuples of outcomes. from must beDoSvalues 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()andrank2level().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
Datasetreturned bycheck(), return a shallow copy of it with the outcome variable replaced and the previous outcome stored in original_outcome. If outcome is aDataArray, 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
Datasetreturned bycheck()ormap_outcome()or just theiroutcomevariable.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
DataArraycontaining 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:untypedabilitycircumstanceproficiencystatusitem
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
Damageon its own, and never while manually building anExpandedDamage:- Parameters:
two_hands (int) – Number of faces on the die when using a weapon with the
two-hands dXtrait in two hands. Before expanding the damage, you will need to call thehands()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 dXtrait. 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 dXtrait. Default: nofataltrait.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 dXtrait that is being held in two hands. Before expanding the damage, you will need to call thehands()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
Damageinstances 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
ExpandedDamageinstead.- copy(**kwargs: Any) Damage#
Create a deep copy of this instance, changing one or more parameters.
- Parameters:
**kwargs – Any of the
Damageparameters.
e.g. a longsword with Inventive Offensive adding
deadly d6:>>> Damage("slashing", 1, 8).copy(deadly=6) **Damage** 1d8 slashing deadly d6
- expand() ExpandedDamage#
Convert this
Damageinstance to anExpandedDamage.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.
- 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
- 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]
- 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:
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 moreDamageobjects.- expand() ExpandedDamage#
Convert
DamageListtoExpandedDamage.
- 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 ofDamage, or a mapping ofDoSto lists ofDamage.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 aDamageobject.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
ExpandedDamageby auto-expanding the deadly trait by adding a dict to aDamageobject. 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
DamageandDamageListinterface.
- 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
DamageorExpandedDamageobjects together.
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 anExpandedDamage, 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()anddamage(); e.g. two iterative Strikes;independent in
check(), but dependent indamage(); e.g. multiple targets roll a check to save vs. Fireball, damage rolled only once;dependent in both
check()anddamage(), 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 matchDamage.type; e.g.{"fire": 5}.You may alternatively pass a
DataArraywith int dtype, adamage_typedimension 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 setimmunities={"vitality": True}.You may alternatively pass a
DataArraywith 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
DataArraywith 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 dimensionscheck.outcomehas. 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 aDataFrameand 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_valuenormalize – Return proportions rather than frequencies. Default: False
- Returns:
DataArraywith 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.
- class pathfinder2e_stats.config.Config#
dict returned by
get_config().
- 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().