205 lines
7.1 KiB
Python
205 lines
7.1 KiB
Python
"""Tests for the effect system: StatModifier and PF1e bonus stacking rules."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pf1e_simulator.effects import StatModifier, resolve_modifiers
|
|
|
|
# ── Stacking types: sum together ──────────────────────────────────────────────
|
|
|
|
|
|
class TestStackingTypes:
|
|
"""Given: modifiers with stacking bonus types
|
|
When: resolve_modifiers is called
|
|
Then: values sum together."""
|
|
|
|
def test_two_dodge_bonus_stack(self) -> None:
|
|
mods = [
|
|
StatModifier("ac", 1, "dodge"),
|
|
StatModifier("ac", 2, "dodge"),
|
|
]
|
|
assert resolve_modifiers(mods) == 3
|
|
|
|
def test_two_racial_bonus_stack(self) -> None:
|
|
mods = [
|
|
StatModifier("save", 1, "racial"),
|
|
StatModifier("save", 1, "racial"),
|
|
]
|
|
assert resolve_modifiers(mods) == 2
|
|
|
|
def test_two_trait_bonus_stack(self) -> None:
|
|
mods = [
|
|
StatModifier("attack", 1, "trait"),
|
|
StatModifier("attack", 1, "trait"),
|
|
]
|
|
assert resolve_modifiers(mods) == 2
|
|
|
|
def test_two_untyped_bonus_stack(self) -> None:
|
|
mods = [
|
|
StatModifier("attack", 1),
|
|
StatModifier("attack", 2),
|
|
]
|
|
assert resolve_modifiers(mods) == 3
|
|
|
|
def test_mixed_stacking_types_all_sum(self) -> None:
|
|
mods = [
|
|
StatModifier("ac", 1, "dodge"),
|
|
StatModifier("ac", 2, "racial"),
|
|
StatModifier("ac", 1, "trait"),
|
|
StatModifier("ac", 1),
|
|
]
|
|
assert resolve_modifiers(mods) == 5
|
|
|
|
def test_negative_untyped_stack(self) -> None:
|
|
mods = [
|
|
StatModifier("attack", -1),
|
|
StatModifier("attack", -2),
|
|
]
|
|
assert resolve_modifiers(mods) == -3
|
|
|
|
|
|
# ── Non-stacking types: keep highest ──────────────────────────────────────────
|
|
|
|
|
|
class TestNonStackingTypes:
|
|
"""Given: modifiers with non-stacking bonus types
|
|
When: resolve_modifiers is called
|
|
Then: only the highest value per type is kept."""
|
|
|
|
def test_two_morale_keep_highest(self) -> None:
|
|
mods = [
|
|
StatModifier("attack", 2, "morale"),
|
|
StatModifier("attack", 1, "morale"),
|
|
]
|
|
assert resolve_modifiers(mods) == 2
|
|
|
|
def test_two_sacred_keep_highest(self) -> None:
|
|
mods = [
|
|
StatModifier("ac", 2, "sacred"),
|
|
StatModifier("ac", 3, "sacred"),
|
|
]
|
|
assert resolve_modifiers(mods) == 3
|
|
|
|
def test_two_enhancement_keep_highest(self) -> None:
|
|
mods = [
|
|
StatModifier("ac", 2, "enhancement"),
|
|
StatModifier("ac", 4, "enhancement"),
|
|
]
|
|
assert resolve_modifiers(mods) == 4
|
|
|
|
def test_different_non_stacking_types_each_apply(self) -> None:
|
|
mods = [
|
|
StatModifier("ac", 2, "morale"),
|
|
StatModifier("ac", 1, "sacred"),
|
|
StatModifier("ac", 3, "enhancement"),
|
|
]
|
|
assert resolve_modifiers(mods) == 6
|
|
|
|
def test_circumstance_does_not_stack(self) -> None:
|
|
mods = [
|
|
StatModifier("attack", 2, "circumstance"),
|
|
StatModifier("attack", 1, "circumstance"),
|
|
]
|
|
assert resolve_modifiers(mods) == 2
|
|
|
|
|
|
# ── Penalties ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestPenalties:
|
|
"""Given: penalties (negative values) with non-stacking types
|
|
When: resolve_modifiers is called
|
|
Then: least severe penalty is kept (max value)."""
|
|
|
|
def test_same_type_penalties_keep_least_severe(self) -> None:
|
|
mods = [
|
|
StatModifier("attack", -2, "morale"),
|
|
StatModifier("attack", -1, "morale"),
|
|
]
|
|
assert resolve_modifiers(mods) == -1
|
|
|
|
def test_same_type_bonus_and_penalty_keep_most_beneficial(self) -> None:
|
|
mods = [
|
|
StatModifier("attack", 2, "morale"),
|
|
StatModifier("attack", -1, "morale"),
|
|
]
|
|
assert resolve_modifiers(mods) == 2
|
|
|
|
def test_untyped_penalties_stack(self) -> None:
|
|
mods = [
|
|
StatModifier("attack", -1),
|
|
StatModifier("attack", -2),
|
|
]
|
|
assert resolve_modifiers(mods) == -3
|
|
|
|
|
|
# ── Mixed: stacking + non-stacking ────────────────────────────────────────────
|
|
|
|
|
|
class TestMixedStacking:
|
|
"""Given: a mix of stacking and non-stacking modifiers
|
|
When: resolve_modifiers is called
|
|
Then: stacking types sum, non-stacking types keep highest, totals combine."""
|
|
|
|
def test_dodge_plus_morale_plus_untyped(self) -> None:
|
|
mods = [
|
|
StatModifier("ac", 1, "dodge"),
|
|
StatModifier("ac", 2, "morale"),
|
|
StatModifier("ac", 1),
|
|
]
|
|
assert resolve_modifiers(mods) == 4
|
|
|
|
def test_two_morale_plus_dodge_plus_racial(self) -> None:
|
|
mods = [
|
|
StatModifier("ac", 2, "morale"),
|
|
StatModifier("ac", 3, "morale"),
|
|
StatModifier("ac", 1, "dodge"),
|
|
StatModifier("ac", 1, "racial"),
|
|
]
|
|
assert resolve_modifiers(mods) == 5 # 3 (max morale) + 1 (dodge) + 1 (racial)
|
|
|
|
def test_penalties_and_bonus_mixed(self) -> None:
|
|
mods = [
|
|
StatModifier("attack", 2, "morale"),
|
|
StatModifier("attack", -1, "morale"),
|
|
StatModifier("attack", -1),
|
|
StatModifier("attack", 1, "dodge"),
|
|
]
|
|
assert resolve_modifiers(mods) == 2 # 2 (max morale) + -1 (untyped) + 1 (dodge)
|
|
|
|
|
|
# ── Edge cases ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestEdgeCases:
|
|
"""Given: edge-case modifier lists
|
|
When: resolve_modifiers is called
|
|
Then: returns the correct total."""
|
|
|
|
def test_empty_list_returns_zero(self) -> None:
|
|
assert resolve_modifiers([]) == 0
|
|
|
|
def test_single_modifier(self) -> None:
|
|
assert resolve_modifiers([StatModifier("ac", 4, "dodge")]) == 4
|
|
|
|
def test_single_negative_modifier(self) -> None:
|
|
assert resolve_modifiers([StatModifier("attack", -3, "morale")]) == -3
|
|
|
|
def test_all_zero_values(self) -> None:
|
|
mods = [
|
|
StatModifier("ac", 0, "dodge"),
|
|
StatModifier("ac", 0, "morale"),
|
|
]
|
|
assert resolve_modifiers(mods) == 0
|
|
|
|
def test_resolve_does_not_filter_by_target(self) -> None:
|
|
"""resolve_modifiers treats all modifiers together regardless of target.
|
|
|
|
The caller must filter by target before calling — the function only
|
|
applies bonus-type stacking rules, not target grouping.
|
|
"""
|
|
mods = [
|
|
StatModifier("attack", 2, "morale"),
|
|
StatModifier("ac", 3, "morale"),
|
|
]
|
|
assert resolve_modifiers(mods) == 3 # same non-stacking type → max(2, 3)
|