fix(rules): correct P2/P3 PF1e conformance bugs (flanking, touch, blinded, disabled, death, maneuvers, DR, sneak)
This commit is contained in:
+31
-18
@@ -240,7 +240,7 @@ def test_count_two_resolves_two_swings() -> None:
|
||||
a = make_state(a_spec, "players", (1, 1))
|
||||
b = make_state(b_spec, "monsters", (1, 2))
|
||||
engine = make_engine(
|
||||
[10, 9, 12, 3, 10, 7, 13, 4, 11, 2, 8, 12, 3], [a, b]
|
||||
[10, 9, 12, 3, 10, 7, 13, 4, 11, 2, 8, 12, 4], [a, b]
|
||||
)
|
||||
result = engine.run()
|
||||
assert result.winner == "players"
|
||||
@@ -254,12 +254,12 @@ def test_count_two_resolves_two_swings() -> None:
|
||||
"round 2 a: short sword #1 vs b d20=13+2=15 AC 13 -> HIT 4 damage (9->5)",
|
||||
"round 2 a: short sword #2 vs b d20=11+2=13 AC 13 -> HIT 2 damage (5->3)",
|
||||
"round 2 b: short sword vs a d20=8+2=10 AC 13 -> MISS",
|
||||
"round 3 a: short sword #1 vs b d20=12+2=14 AC 13 -> HIT 3 damage (3->0)",
|
||||
"round 3 a: short sword #1 vs b d20=12+2=14 AC 13 -> HIT 4 damage (3->-1)",
|
||||
"round 3 a: b down",
|
||||
"battle over: players win in 3 rounds",
|
||||
)
|
||||
assert result.stats["a"] == CombatantStats(hits=4, crits=0, damage_dealt=12, damage_taken=0)
|
||||
assert result.stats["b"] == CombatantStats(hits=0, crits=0, damage_dealt=0, damage_taken=12)
|
||||
assert result.stats["a"] == CombatantStats(hits=4, crits=0, damage_dealt=13, damage_taken=0)
|
||||
assert result.stats["b"] == CombatantStats(hits=0, crits=0, damage_dealt=0, damage_taken=13)
|
||||
|
||||
|
||||
def test_count_swings_stop_when_target_drops() -> None:
|
||||
@@ -310,16 +310,18 @@ def test_hp_states_active_down_dead() -> None:
|
||||
assert st.active is True
|
||||
assert st.dead is False
|
||||
st.hp = 0
|
||||
assert st.active is False
|
||||
# B11 conformance: exactly 0 hp is DISABLED — active (single action round).
|
||||
assert st.active is True
|
||||
assert st.dead is False
|
||||
st.hp = -1
|
||||
assert st.active is False # dying
|
||||
assert st.dead is False
|
||||
st.hp = -11
|
||||
assert st.active is False
|
||||
assert st.dead is False
|
||||
# B12 conformance (CRB): death when negative hp reaches CON (hp <= -12).
|
||||
st.hp = -12
|
||||
assert st.active is False
|
||||
assert st.dead is False # dead requires hp < -12, not <=
|
||||
st.hp = -13
|
||||
assert st.active is False
|
||||
assert st.dead is True
|
||||
|
||||
|
||||
@@ -556,7 +558,7 @@ def test_ranged_no_line_of_effect_moves_around_wall() -> None:
|
||||
archer = make_combatant("archer", attack_bonus=4, kind="ranged", range_increment_ft=60)
|
||||
target = make_combatant("target", speed=0)
|
||||
states = [make_state(archer, "players", (0, 1)), make_state(target, "monsters", (0, 4))]
|
||||
rng = ScriptedRng([10, 9, 5, 15, 3, 10, 3, 12, 2, 8, 2, 14, 1])
|
||||
rng = ScriptedRng([10, 9, 5, 15, 3, 10, 4, 12, 2, 8, 2, 14, 1])
|
||||
engine = CombatEngine(rng, grid, states, round_cap=4)
|
||||
result = engine.run()
|
||||
assert result.winner == "players"
|
||||
@@ -568,7 +570,7 @@ def test_ranged_no_line_of_effect_moves_around_wall() -> None:
|
||||
"round 1 target: wait",
|
||||
"round 2 archer: short bow vs target d20=15+4=19 AC 13 -> HIT 3 damage (6->3)",
|
||||
"round 2 target: wait",
|
||||
"round 3 archer: short bow vs target d20=10+4=14 AC 13 -> HIT 3 damage (3->0)",
|
||||
"round 3 archer: short bow vs target d20=10+4=14 AC 13 -> HIT 4 damage (3->-1)",
|
||||
"round 3 archer: target down",
|
||||
"battle over: players win in 3 rounds",
|
||||
)
|
||||
@@ -842,24 +844,31 @@ def test_diagonal_flanking() -> None:
|
||||
|
||||
|
||||
def test_flanking_requires_active_ally() -> None:
|
||||
"""A downed ally on the opposite side does not grant flanking."""
|
||||
"""A downed ally (below 0 hp, dying) on the opposite side does not flank.
|
||||
|
||||
B11 conformance: an ally at exactly 0 hp is disabled but active — it
|
||||
would flank; only hp < 0 removes it from the fight."""
|
||||
atk = make_combatant("atk", attack_bonus=2, speed=0)
|
||||
aly = make_combatant("aly", attack_bonus=0, speed=0)
|
||||
tgt = make_combatant("tgt", hp=10, ac=15, attack_bonus=2, speed=0)
|
||||
aly_state = make_state(aly, "players", (5, 4))
|
||||
aly_state.hp = 0
|
||||
aly_state.hp = -1
|
||||
states = [
|
||||
make_state(atk, "players", (3, 4)),
|
||||
aly_state,
|
||||
make_state(tgt, "monsters", (4, 4)),
|
||||
]
|
||||
engine = make_engine([10, 9, 8, 12, 5], states, round_cap=1)
|
||||
engine = make_engine([10, 9, 8, 12, 5, 5], states, round_cap=1)
|
||||
result = engine.run()
|
||||
assert result.transcript == (
|
||||
"initiative: atk d20=10+0=10",
|
||||
"initiative: aly d20=9+0=9",
|
||||
"initiative: tgt d20=8+0=8",
|
||||
"round 1 atk: short sword vs tgt d20=12+2=14 AC 15 -> MISS",
|
||||
(
|
||||
"round 1 aly: dying — Constitution check d20(5) + CON(1) - 1 = 5 "
|
||||
"vs DC 10 failed, loses 1 hp (hp -1 -> -2)"
|
||||
),
|
||||
"round 1 tgt: short sword vs atk d20=5+2=7 AC 13 -> MISS",
|
||||
"battle over: draw after 1 rounds",
|
||||
)
|
||||
@@ -935,9 +944,13 @@ def test_full_attack_bab11_three_iteratives() -> None:
|
||||
|
||||
|
||||
def test_full_attack_stops_on_target_down() -> None:
|
||||
"""Remaining iteratives are lost when the target drops mid-full-attack."""
|
||||
"""Remaining iteratives are lost when the target drops mid-full-attack.
|
||||
|
||||
The scripted blow takes tgt below 0 (hp 4 - 5 = -1): a target at exactly
|
||||
0 hp is disabled but still ACTIVE (B11 conformance) and would not drop.
|
||||
"""
|
||||
war = make_combatant("war", attack_bonus=6, hp=10, speed=0)
|
||||
tgt = make_combatant("tgt", hp=5, ac=15, attack_bonus=0, speed=0)
|
||||
tgt = make_combatant("tgt", hp=4, ac=15, attack_bonus=0, speed=0)
|
||||
states = [
|
||||
make_state(war, "players", (3, 4)),
|
||||
make_state(tgt, "monsters", (4, 4)),
|
||||
@@ -947,7 +960,7 @@ def test_full_attack_stops_on_target_down() -> None:
|
||||
assert result.transcript == (
|
||||
"initiative: war d20=10+0=10",
|
||||
"initiative: tgt d20=8+0=8",
|
||||
"round 1 war: short sword #1 vs tgt d20=12+6=18 AC 15 -> HIT 5 damage (5->0)",
|
||||
"round 1 war: short sword #1 vs tgt d20=12+6=18 AC 15 -> HIT 5 damage (4->-1)",
|
||||
"round 1 war: tgt down",
|
||||
"battle over: players win in 1 rounds",
|
||||
)
|
||||
@@ -1020,7 +1033,7 @@ def test_aoo_drops_mover() -> None:
|
||||
make_state(dest, "monsters", (4, 5)),
|
||||
]
|
||||
engine = CombatEngine(
|
||||
ScriptedRng([10, 9, 8, 15, 3]),
|
||||
ScriptedRng([10, 9, 8, 15, 4]),
|
||||
make_grid(), states, round_cap=1, policy=_move_to_dest,
|
||||
)
|
||||
result = engine.run()
|
||||
@@ -1029,7 +1042,7 @@ def test_aoo_drops_mover() -> None:
|
||||
"initiative: runner d20=10+0=10",
|
||||
"initiative: guard d20=9+0=9",
|
||||
"initiative: dest d20=8+0=8",
|
||||
"round 1 guard: AoO vs runner d20=15+5=20 AC 12 -> HIT 3 damage (3->0)",
|
||||
"round 1 guard: AoO vs runner d20=15+5=20 AC 12 -> HIT 4 damage (3->-1)",
|
||||
"round 1 guard: runner down",
|
||||
"battle over: monsters win in 1 rounds",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user