Minecraft-Console-Client/tools/pathing_theory/canonical.py
BruceChen 418f17b4a1 pathing: add side-wall theory, full-coverage parkour test suite, and live test fixes
Theory simulator:
- Add 2D side-wall jump physics with yaw sweep for worst-case margin
- Generate sidewall theory cases (flat/ascend/descend, wall_offset 0/1)
- Add momentum-capabilities.json with band compression and max_reach
- Extend models, capabilities, canonical, and renderers for sidewall

Full-coverage parkour test suite (tools/test-parkour.py):
- Derive test matrix from momentum-capabilities.json
- Build linear/neo/ceiling courses via RCON with 7-block clear margin
- Use /goto for pathfinding, parse A* and PathMgr log output
- Stop-at-first-failure per (family, subfamily, dy, ceil, wo) group
- Hierarchical --filter (e.g. linear/flat, ceiling/headhitter/ceil2.5)
- Exclude sidewall from default matrix (identical max_reach to linear)

Pathing execution fixes:
- Align parkour contracts and timing budgets with live test results
- Fix jump-entry yaw snapping for grounded handoffs
- Template helper and sprint jump template refinements

Made-with: Cursor
2026-04-15 17:52:47 +00:00

124 lines
4.5 KiB
Python

from tools.pathing_theory.models import CanonicalLiveCase, TheoryCase
def _world_recipe_id(case: TheoryCase) -> str:
if case.family == "linear":
return f"linear-{case.subfamily}"
if case.family == "neo":
return "neo-wall"
if case.family == "sidewall":
return f"sidewall-{case.subfamily}"
return "ceiling-headhitter"
def _canonical_goal(case: TheoryCase) -> tuple[dict[str, float], dict[str, float]]:
start = {"x": 100.5, "y": 80.0, "z": 100.5}
if case.family == "linear":
goal_y = 80.0 + (case.delta_y or 0.0)
goal_x = 100 + (case.gap_blocks or 0) + 1
return start, {"x": float(goal_x), "y": goal_y, "z": 100.0}
if case.family == "neo":
goal_z = 100 + (case.wall_width or 1)
return start, {"x": 102.0, "y": 80.0, "z": float(goal_z)}
if case.family == "sidewall":
goal_y = 80.0 + (case.delta_y or 0.0)
goal_x = 100 + (case.gap_blocks or 0) + 1
wall_z_offset = 100 + 1 + (case.wall_offset or 0)
return (
{"x": 100.5, "y": 80.0, "z": 100.5},
{"x": float(goal_x), "y": goal_y, "z": 100.0},
)
goal_x = 100 + (case.gap_blocks or 0) + 1
return start, {"x": float(goal_x), "y": 80.0, "z": 100.0}
def _select_boundary_case(
family: str,
subfamily: str,
reachable: list[TheoryCase],
) -> TheoryCase:
if family in {"linear", "sidewall"}:
preferred_gap_by_subfamily = {
"flat": 5,
"ascend": 2,
"descend": 2,
}
preferred_gap = preferred_gap_by_subfamily.get(subfamily)
if preferred_gap is not None:
for case in reachable:
if case.gap_blocks == preferred_gap:
return case
if family == "ceiling" and subfamily == "headhitter":
for case in reachable:
if case.gap_blocks == 3 and case.ceiling_height == 2.0:
return case
return reachable[0]
def build_canonical_live_cases(cases: list[TheoryCase]) -> list[CanonicalLiveCase]:
live_candidate_cases = [
case
for case in cases
if case.movement_mode == "sprint" and case.momentum_ticks == 12
]
by_bucket: dict[tuple[str, str, str], list[TheoryCase]] = {}
for case in live_candidate_cases:
by_bucket.setdefault(
(case.family, case.subfamily, case.movement_mode),
[],
).append(case)
canonical_cases: list[CanonicalLiveCase] = []
for family, subfamily, movement_mode in sorted(by_bucket):
bucket_cases = by_bucket[(family, subfamily, movement_mode)]
reachable = sorted(
[
case
for case in bucket_cases
if case.expected_reachable and case.margin is not None
],
key=lambda case: case.margin,
)
unreachable = sorted(
[case for case in bucket_cases if not case.expected_reachable],
key=lambda case: float("-inf") if case.margin is None else abs(case.margin),
)
selected: list[tuple[str, TheoryCase]] = []
if reachable:
easy = next(
(case for case in reversed(reachable) if (case.margin or 0.0) >= 0.50),
reachable[-1],
)
boundary = _select_boundary_case(family, subfamily, reachable)
selected.append(("easy", easy))
if boundary.case_id != easy.case_id:
selected.append(("boundary", boundary))
if unreachable:
selected.append(("reject", unreachable[0]))
for difficulty_band, case in selected:
start, goal = _canonical_goal(case)
canonical_cases.append(
CanonicalLiveCase(
case_id=case.case_id,
bucket_id=f"{family}:{subfamily}:{movement_mode}:{difficulty_band}",
family=family,
subfamily=subfamily,
movement_mode=movement_mode,
momentum_ticks=case.momentum_ticks,
difficulty_band=difficulty_band,
expected_result="pass" if case.expected_reachable else "reject",
world_recipe_id=_world_recipe_id(case),
gap_blocks=case.gap_blocks,
delta_y=case.delta_y,
ceiling_height=case.ceiling_height,
wall_width=case.wall_width,
wall_offset=case.wall_offset,
start=start,
goal=goal,
)
)
return canonical_cases