Files

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)