feat(combat): add charge action (straight-line 2x speed, +2 attack, -2 AC)
This commit is contained in:
+75
-6
@@ -367,8 +367,8 @@ def test_scripted_2v2_transcript() -> None:
|
||||
"round 1 orc-2: gob-2 down",
|
||||
"round 2 gob-1: short sword vs orc-1 d20=19+2=21 AC 13 -> CRIT 4 damage (3->-1)",
|
||||
"round 2 gob-1: orc-1 down",
|
||||
"round 2 orc-2: move (3,4)->(2,3)",
|
||||
"round 2 orc-2: falchion vs gob-1 d20=15+5=20 AC 16 -> HIT 9 damage (6->-3)",
|
||||
"round 2 orc-2: charge (3,4)->(2,4)->(1,3)",
|
||||
"round 2 orc-2: charge vs gob-1 d20=15+7=22 AC 16 -> HIT 9 damage (6->-3)",
|
||||
"round 2 orc-2: gob-1 down",
|
||||
"battle over: monsters win in 2 rounds",
|
||||
)
|
||||
@@ -397,7 +397,7 @@ def test_same_seed_replay_is_identical() -> None:
|
||||
assert first.rounds == second.rounds
|
||||
|
||||
|
||||
def test_default_policy_moves_toward_enemy() -> None:
|
||||
def test_default_policy_charges_toward_enemy() -> None:
|
||||
gob = make_combatant("gob", speed=30)
|
||||
orc = make_combatant("orc", speed=0)
|
||||
states = [make_state(gob, "players", (1, 1)), make_state(orc, "monsters", (1, 5))]
|
||||
@@ -406,9 +406,9 @@ def test_default_policy_moves_toward_enemy() -> None:
|
||||
assert result.transcript == (
|
||||
"initiative: gob d20=10+0=10",
|
||||
"initiative: orc d20=9+0=9",
|
||||
"round 1 gob: move (1,1)->(0,2)->(0,3)->(0,4)",
|
||||
"round 1 gob: short sword vs orc d20=12+2=14 AC 13 -> HIT 3 damage (6->3)",
|
||||
"round 1 orc: short sword vs gob d20=11+2=13 AC 13 -> HIT 2 damage (6->4)",
|
||||
"round 1 gob: charge (1,1)->(1,2)->(0,3)->(0,4)",
|
||||
"round 1 gob: charge vs orc d20=12+4=16 AC 13 -> HIT 3 damage (6->3)",
|
||||
"round 1 orc: short sword vs gob d20=11+2=13 AC 11 -> HIT 2 damage (6->4)",
|
||||
"round 2 gob: short sword vs orc d20=10+2=12 AC 13 -> MISS",
|
||||
"round 2 orc: short sword vs gob d20=1+2=3 AC 13 -> MISS",
|
||||
"battle over: draw after 2 rounds",
|
||||
@@ -1076,3 +1076,72 @@ def test_two_guards_each_one_aoo() -> None:
|
||||
"round 1 dest: wait",
|
||||
"battle over: draw after 1 rounds",
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Charge (CRB: straight-line 2x speed, +2 attack, -2 AC, single melee attack) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def _gob_charges_orc_attacks(
|
||||
engine: CombatEngine, state: CombatantState
|
||||
) -> tuple[Action, ...]:
|
||||
if state.combatant.id == "gob":
|
||||
return (Action(kind="charge", target_id="orc"),)
|
||||
return (Action(kind="attack", target_id="gob"),)
|
||||
|
||||
|
||||
def test_charge_straight_line_and_bonus() -> None:
|
||||
"""Charge moves in a straight line and attacks at +2; enemy retaliates."""
|
||||
gob = make_combatant("gob", hp=10, ac=15, attack_bonus=2, speed=30)
|
||||
orc = make_combatant("orc", hp=10, ac=15, attack_bonus=0, speed=0)
|
||||
states = [make_state(gob, "players", (1, 1)), make_state(orc, "monsters", (1, 5))]
|
||||
engine = CombatEngine(
|
||||
ScriptedRng([10, 9, 12, 3, 11, 2]),
|
||||
make_grid(), states, round_cap=1, policy=_gob_charges_orc_attacks,
|
||||
)
|
||||
result = engine.run()
|
||||
assert result.transcript == (
|
||||
"initiative: gob d20=10+0=10",
|
||||
"initiative: orc d20=9+0=9",
|
||||
"round 1 gob: charge (1,1)->(1,2)->(0,3)->(0,4)",
|
||||
"round 1 gob: charge vs orc d20=12+4=16 AC 15 -> HIT 3 damage (10->7)",
|
||||
"round 1 orc: short sword vs gob d20=11+0=11 AC 13 -> MISS",
|
||||
"battle over: draw after 1 rounds",
|
||||
)
|
||||
|
||||
|
||||
def test_charge_ac_penalty_applies() -> None:
|
||||
"""The -2 AC penalty from charging applies to the enemy's attack the same round."""
|
||||
gob = make_combatant("gob", hp=20, ac=15, attack_bonus=2, speed=30)
|
||||
orc = make_combatant("orc", hp=20, ac=15, attack_bonus=2, speed=0)
|
||||
states = [make_state(gob, "players", (1, 1)), make_state(orc, "monsters", (1, 4))]
|
||||
engine = CombatEngine(
|
||||
ScriptedRng([10, 9, 12, 3, 11, 2]),
|
||||
make_grid(), states, round_cap=1, policy=_gob_charges_orc_attacks,
|
||||
)
|
||||
result = engine.run()
|
||||
assert result.transcript == (
|
||||
"initiative: gob d20=10+0=10",
|
||||
"initiative: orc d20=9+0=9",
|
||||
"round 1 gob: charge (1,1)->(1,2)->(0,3)",
|
||||
"round 1 gob: charge vs orc d20=12+4=16 AC 15 -> HIT 3 damage (20->17)",
|
||||
"round 1 orc: short sword vs gob d20=11+2=13 AC 13 -> HIT 2 damage (20->18)",
|
||||
"battle over: draw after 1 rounds",
|
||||
)
|
||||
|
||||
|
||||
def test_charge_too_close_full_attacks_instead() -> None:
|
||||
"""When adjacent (cannot charge minimum 2 squares), default_policy full-attacks."""
|
||||
gob = make_combatant("gob", hp=10, ac=15, attack_bonus=2, speed=30)
|
||||
orc = make_combatant("orc", hp=10, ac=15, attack_bonus=0, speed=0)
|
||||
states = [make_state(gob, "players", (1, 1)), make_state(orc, "monsters", (1, 2))]
|
||||
engine = make_engine([10, 9, 12, 3], states, round_cap=1)
|
||||
result = engine.run()
|
||||
assert result.transcript == (
|
||||
"initiative: gob d20=10+0=10",
|
||||
"initiative: orc d20=9+0=9",
|
||||
"round 1 gob: short sword vs orc d20=12+2=14 AC 15 -> MISS",
|
||||
"round 1 orc: short sword vs gob d20=3+0=3 AC 15 -> MISS",
|
||||
"battle over: draw after 1 rounds",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user