fix(combat): Thread 5-10-5 diagonal parity through all budget-consuming paths
The greedy move/run/flee/charge descent helpers hardcoded diagonal parity 0, letting a speed-30 walker cover 6 diagonal cells (cell-count budget). Under 5-10-5 (SRD Movement, Tactical) the same walker covers only 4 cells (1+2+1+2=6 squares). Threading the running diagonal count: - _best_move_step / _best_flee_step: accept diagonals_used param - _descend_path / _run_line_path / _flee_path: track and pass parity - _charge_end: use _path_cost_5105() instead of len(path) for max check - _path_cost_5105: new helper; computes total 5-10-5 cost of any path Updated golden paths in test_aoo_limit_one_per_round (5-cell path now takes an orthogonal detour to avoid expensive 2nd diagonal), test_run_* (speed-30 diagonal move stops at 4 cells), and staggered_withdraw (single-speed diagonal withdraw stops at 4 cells).
This commit is contained in:
@@ -118,6 +118,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from itertools import pairwise
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
@@ -3643,6 +3644,16 @@ class CombatEngine:
|
||||
return ChargeResult(weapon=weapon, distance_ft=distance_ft, reason=None)
|
||||
return ChargeResult(weapon=None, distance_ft=0, reason="no_charge_path")
|
||||
|
||||
def _path_cost_5105(self, path: list[Pos]) -> int:
|
||||
"""Total 5-10-5 movement cost of a path (diagonal parity threaded)."""
|
||||
total = 0
|
||||
diagonals_used = 0
|
||||
for frm, to in pairwise(path):
|
||||
total += self._grid.step_cost(frm, to, diagonals_used)
|
||||
if frm[0] != to[0] and frm[1] != to[1]:
|
||||
diagonals_used += 1
|
||||
return total
|
||||
|
||||
def _charge_end(
|
||||
self,
|
||||
state: CombatantState,
|
||||
@@ -3675,10 +3686,14 @@ class CombatEngine:
|
||||
if dist > reach_cells or dist < min_reach_cells:
|
||||
continue
|
||||
path = _bresenham_line(state.pos, end)
|
||||
if len(path) < _CHARGE_MIN_CELLS or len(path) > max_cells:
|
||||
if len(path) < _CHARGE_MIN_CELLS:
|
||||
continue
|
||||
if not self._charge_path_clear(path, state):
|
||||
continue
|
||||
# The charge budget is a movement budget: count it in 5-10-5
|
||||
# squares (diagonal parity threaded), not raw cells.
|
||||
if self._path_cost_5105(path) > max_cells:
|
||||
continue
|
||||
candidates.append((len(path), end))
|
||||
if not candidates:
|
||||
return None
|
||||
@@ -7071,14 +7086,20 @@ class CombatEngine:
|
||||
path: list[Pos] = []
|
||||
current = state.pos
|
||||
budget = speed_cells
|
||||
diagonals_used = 0
|
||||
while budget > 0:
|
||||
best = self._best_move_step(
|
||||
state, current, to_target, budget,
|
||||
state, current, to_target, budget, diagonals_used,
|
||||
avoid_difficult=avoid_difficult, friendly_passable=friendly_passable,
|
||||
)
|
||||
if best is None:
|
||||
break
|
||||
_, step_cost, nxt = best
|
||||
# PF1e 5-10-5: the diagonal cost alternates 1-2-1-2, so the
|
||||
# running diagonal count must follow the walk (grid.reachable
|
||||
# already tracks it; the greedy descent must too).
|
||||
if current[0] != nxt[0] and current[1] != nxt[1]:
|
||||
diagonals_used += 1
|
||||
current = nxt
|
||||
path.append(current)
|
||||
budget -= step_cost
|
||||
@@ -7111,16 +7132,19 @@ class CombatEngine:
|
||||
return []
|
||||
path: list[Pos] = []
|
||||
current = state.pos
|
||||
diagonals_used = 0
|
||||
for nxt in _bresenham_line(state.pos, goal):
|
||||
footprint = self._footprint_at(nxt, state)
|
||||
if self._footprint_touches_difficult(footprint):
|
||||
break
|
||||
if not self._cells_walkable(footprint, state):
|
||||
break
|
||||
step = self._grid.step_cost(current, nxt, 0)
|
||||
step = self._grid.step_cost(current, nxt, diagonals_used)
|
||||
if step > budget:
|
||||
break
|
||||
path.append(nxt)
|
||||
if current[0] != nxt[0] and current[1] != nxt[1]:
|
||||
diagonals_used += 1
|
||||
current = nxt
|
||||
budget -= step
|
||||
while path and not self._cells_free(self._footprint_at(path[-1], state), state):
|
||||
@@ -7133,6 +7157,7 @@ class CombatEngine:
|
||||
current: Pos,
|
||||
to_target: dict[Pos, int],
|
||||
budget: int,
|
||||
diagonals_used: int,
|
||||
*,
|
||||
avoid_difficult: bool = False,
|
||||
friendly_passable: bool = False,
|
||||
@@ -7169,7 +7194,7 @@ class CombatEngine:
|
||||
and not self._has_free_descent(state, nxt, to_target, nxt_cost)
|
||||
):
|
||||
continue
|
||||
step = self._grid.step_cost(current, nxt, 0)
|
||||
step = self._grid.step_cost(current, nxt, diagonals_used)
|
||||
if step > budget:
|
||||
continue
|
||||
score = step + nxt_cost
|
||||
@@ -7230,11 +7255,16 @@ class CombatEngine:
|
||||
path: list[Pos] = []
|
||||
current = state.pos
|
||||
budget = max_cells
|
||||
diagonals_used = 0
|
||||
while budget > 0:
|
||||
best = self._best_flee_step(state, current, from_threat, budget)
|
||||
best = self._best_flee_step(
|
||||
state, current, from_threat, budget, diagonals_used
|
||||
)
|
||||
if best is None:
|
||||
break
|
||||
_, step_cost, nxt = best
|
||||
if current[0] != nxt[0] and current[1] != nxt[1]:
|
||||
diagonals_used += 1
|
||||
current = nxt
|
||||
path.append(current)
|
||||
budget -= step_cost
|
||||
@@ -7246,6 +7276,7 @@ class CombatEngine:
|
||||
current: Pos,
|
||||
from_threat: dict[Pos, int],
|
||||
budget: int,
|
||||
diagonals_used: int,
|
||||
) -> tuple[int, int, Pos] | None:
|
||||
"""Pick the adjacent anchor that maximizes distance from the threat."""
|
||||
current_cost = self._footprint_cost(self._footprint_at(current, state), from_threat)
|
||||
@@ -7263,7 +7294,7 @@ class CombatEngine:
|
||||
nxt_cost = self._footprint_cost(footprint, from_threat)
|
||||
if nxt_cost is None or nxt_cost <= current_cost:
|
||||
continue
|
||||
step = self._grid.step_cost(current, nxt, 0)
|
||||
step = self._grid.step_cost(current, nxt, diagonals_used)
|
||||
if step > budget:
|
||||
continue
|
||||
score = -(nxt_cost * 100) + step
|
||||
|
||||
Reference in New Issue
Block a user