Minecraft-Console-Client/tools/pathing_theory/primitives.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

442 lines
12 KiB
Python

import math
from dataclasses import dataclass
from typing import Optional
PLAYER_WIDTH = 0.6
PLAYER_HEIGHT = 1.8
STEP_HEIGHT = 0.6
GRAVITY = 0.08
DRAG_Y = 0.98
FRICTION_MULTIPLIER = 0.91
DEFAULT_BLOCK_FRICTION = 0.6
INPUT_FRICTION = 0.98
GROUND_ACCEL_FACTOR = 0.21600002
AIR_ACCEL = 0.02
MOVEMENT_SPEED = 0.1
BASE_JUMP_POWER = 0.42
SPRINT_JUMP_HORIZONTAL_BOOST = 0.2
HORIZONTAL_VELOCITY_THRESHOLD_SQR = 9.0e-6
VERTICAL_VELOCITY_THRESHOLD = 0.003
HALF_WIDTH = PLAYER_WIDTH / 2.0
# Reliable late-jump timing is slightly before the full 0.8 block walk-off limit.
# Baritone uses a 0.7 threshold for the analogous 2-gap flat parkour execution.
EDGE_TAKEOFF_X = 0.7
TARGET_BLOCK_WIDTH = 1.0
@dataclass
class TickState:
tick: int = 0
x: float = 0.0
y: float = 0.0
z: float = 0.0
vx: float = 0.0
vy: float = 0.0
vz: float = 0.0
on_ground: bool = True
def get_ground_speed(block_friction: float = DEFAULT_BLOCK_FRICTION) -> float:
friction = block_friction * FRICTION_MULTIPLIER
return MOVEMENT_SPEED * (GROUND_ACCEL_FACTOR / (friction * friction * friction))
def build_momentum_velocity(
momentum_ticks: int,
block_friction: float = DEFAULT_BLOCK_FRICTION,
) -> float:
vx = 0.0
ground_friction = block_friction * FRICTION_MULTIPLIER
for _ in range(momentum_ticks):
vx += INPUT_FRICTION * get_ground_speed(block_friction)
vx *= ground_friction
return vx
def build_momentum_velocity_2d(
momentum_ticks: int,
yaw_rad: float,
strafe_input: float = 0.0,
block_friction: float = DEFAULT_BLOCK_FRICTION,
wall_z: Optional[float] = None,
start_z: float = 0.0,
) -> tuple[float, float, float]:
"""Build pre-jump velocity with yaw and optional strafe, returning (vx, vz, z).
Simulates ground ticks before the jump edge, accounting for yaw-split
acceleration, optional strafe, and wall collision on the z axis.
"""
cos_yaw = math.cos(yaw_rad)
sin_yaw = math.sin(yaw_rad)
ground_friction = block_friction * FRICTION_MULTIPLIER
ground_speed = get_ground_speed(block_friction)
vx, vz, z = 0.0, 0.0, start_z
for _ in range(momentum_ticks):
input_x = (cos_yaw + strafe_input * (-sin_yaw)) * INPUT_FRICTION
input_z = (sin_yaw + strafe_input * cos_yaw) * INPUT_FRICTION
vx += input_x * ground_speed
vz += input_z * ground_speed
z += vz
if wall_z is not None and z + HALF_WIDTH > wall_z:
z = wall_z - HALF_WIDTH
if vz > 0:
vz = 0.0
vx *= ground_friction
vz *= ground_friction
return vx, vz, z
def _get_overlap_window(
start_x: float,
end_x: float,
landing_x_start: float,
landing_width: Optional[float],
) -> Optional[tuple[float, float]]:
min_center_x = landing_x_start - HALF_WIDTH
max_center_x = (
None if landing_width is None else landing_x_start + landing_width + HALF_WIDTH
)
if start_x > end_x:
start_x, end_x = end_x, start_x
delta_x = end_x - start_x
if delta_x == 0.0:
if start_x < min_center_x:
return None
if max_center_x is not None and start_x > max_center_x:
return None
return 0.0, 1.0
if end_x < min_center_x:
return None
enter_t = 0.0 if start_x >= min_center_x else (min_center_x - start_x) / delta_x
if max_center_x is None:
exit_t = 1.0
else:
if start_x > max_center_x:
return None
exit_t = 1.0 if end_x <= max_center_x else (max_center_x - start_x) / delta_x
if exit_t < 0.0 or enter_t > 1.0 or enter_t > exit_t:
return None
return max(0.0, enter_t), min(1.0, exit_t)
def _find_landing_contact(
start_x: float,
start_y: float,
end_x: float,
end_y: float,
landing_y: float,
landing_x_start: float,
landing_width: Optional[float],
) -> Optional[tuple[float, float]]:
if start_y < landing_y or end_y > landing_y or start_y == end_y:
return None
overlap_window = _get_overlap_window(
start_x=start_x,
end_x=end_x,
landing_x_start=landing_x_start,
landing_width=landing_width,
)
if overlap_window is None:
return None
landing_t = (start_y - landing_y) / (start_y - end_y)
enter_t, exit_t = overlap_window
if landing_t < enter_t or landing_t > exit_t:
return None
landing_x = start_x + (end_x - start_x) * landing_t
return landing_x, landing_y
def simulate_jump(
sprint: bool = True,
momentum_ticks: int = 12,
ceiling_y: Optional[float] = None,
landing_y: float = 0.0,
landing_x_start: float = 0.0,
landing_width: Optional[float] = None,
max_ticks: int = 200,
yaw_degrees: float = 0.0,
strafe_input: float = 0.0,
wall_z: Optional[float] = None,
start_z: float = 0.0,
) -> list[TickState]:
yaw_rad = math.radians(yaw_degrees)
cos_yaw = math.cos(yaw_rad)
sin_yaw = math.sin(yaw_rad)
has_lateral = yaw_degrees != 0.0 or strafe_input != 0.0 or wall_z is not None
if has_lateral:
vx, vz, z = build_momentum_velocity_2d(
momentum_ticks, yaw_rad, strafe_input,
wall_z=wall_z, start_z=start_z,
)
else:
vx = build_momentum_velocity(momentum_ticks)
vz = 0.0
z = start_z
x, y, vy = EDGE_TAKEOFF_X, 0.0, 0.0
on_ground = True
trajectory: list[TickState] = []
jumped = False
ground_friction = DEFAULT_BLOCK_FRICTION * FRICTION_MULTIPLIER
trajectory.append(TickState(0, x, y, z, vx, vy, vz, on_ground))
for tick in range(1, max_ticks + 1):
if vx * vx + vz * vz < HORIZONTAL_VELOCITY_THRESHOLD_SQR:
vx = 0.0
vz = 0.0
if abs(vy) < VERTICAL_VELOCITY_THRESHOLD:
vy = 0.0
do_jump = False
if not jumped and on_ground:
do_jump = True
jumped = True
if do_jump:
vy = max(BASE_JUMP_POWER, vy)
if sprint:
vx += SPRINT_JUMP_HORIZONTAL_BOOST * cos_yaw
vz += SPRINT_JUMP_HORIZONTAL_BOOST * sin_yaw
speed = get_ground_speed() if on_ground else AIR_ACCEL
if has_lateral:
input_x = (cos_yaw + strafe_input * (-sin_yaw)) * INPUT_FRICTION
input_z = (sin_yaw + strafe_input * cos_yaw) * INPUT_FRICTION
vx += input_x * speed
vz += input_z * speed
else:
vx += INPUT_FRICTION * speed
new_x = x + vx
new_y = y + vy
new_z = z + vz
new_on_ground = False
if wall_z is not None and new_z + HALF_WIDTH > wall_z:
new_z = wall_z - HALF_WIDTH
if vz > 0:
vz = 0.0
if ceiling_y is not None:
head_y = new_y + PLAYER_HEIGHT
if head_y > ceiling_y:
new_y = ceiling_y - PLAYER_HEIGHT
if vy > 0:
vy = 0.0
if jumped:
contact = _find_landing_contact(
start_x=x,
start_y=y,
end_x=new_x,
end_y=new_y,
landing_y=landing_y,
landing_x_start=landing_x_start,
landing_width=landing_width,
)
if contact is not None:
new_x, new_y = contact
vy = 0.0
new_on_ground = True
x = new_x
y = new_y
z = new_z
on_ground = new_on_ground
vy -= GRAVITY
vy *= DRAG_Y
if on_ground:
vx *= ground_friction
vz *= ground_friction
else:
vx *= FRICTION_MULTIPLIER
vz *= FRICTION_MULTIPLIER
trajectory.append(TickState(tick, x, y, z, vx, vy, vz, on_ground))
if jumped and on_ground:
break
return trajectory
def get_landing(
sprint: bool,
target_y: float,
landing_x_start: float = 0.0,
momentum_ticks: int = 12,
ceiling_y: Optional[float] = None,
landing_width: Optional[float] = None,
yaw_degrees: float = 0.0,
strafe_input: float = 0.0,
wall_z: Optional[float] = None,
start_z: float = 0.0,
) -> Optional[tuple[float, float]]:
trajectory = simulate_jump(
sprint=sprint,
momentum_ticks=momentum_ticks,
ceiling_y=ceiling_y,
landing_y=target_y,
landing_x_start=landing_x_start,
landing_width=landing_width,
yaw_degrees=yaw_degrees,
strafe_input=strafe_input,
wall_z=wall_z,
start_z=start_z,
)
was_air = False
for state in trajectory:
if not state.on_ground:
was_air = True
if was_air and state.on_ground:
return state.x, state.y
return None
def get_apex(
sprint: bool,
momentum_ticks: int = 12,
ceiling_y: Optional[float] = None,
) -> tuple[float, float]:
trajectory = simulate_jump(
sprint=sprint,
momentum_ticks=momentum_ticks,
ceiling_y=ceiling_y,
landing_y=-1000.0,
landing_x_start=0.0,
max_ticks=300,
)
best_y, best_x = 0.0, 0.0
for state in trajectory:
if state.y > best_y:
best_y = state.y
best_x = state.x
return best_y, best_x
def can_reach_gap(
gap_blocks: int,
dy: float,
sprint: bool = True,
momentum_ticks: int = 12,
) -> tuple[bool, Optional[float], float]:
if dy > 1.252:
return False, None, 0.0
needed_x = 0.5 + gap_blocks - HALF_WIDTH
landing_platform_start = 0.5 + gap_blocks
if gap_blocks == 0 and dy > 0:
landing_platform_start = 0.5
result = get_landing(
sprint=sprint,
target_y=dy,
landing_x_start=landing_platform_start,
momentum_ticks=momentum_ticks,
landing_width=TARGET_BLOCK_WIDTH,
)
if result is None:
return False, None, needed_x
landing_x, landing_y = result
if abs(landing_y - dy) > 0.01:
return False, landing_x, needed_x
if gap_blocks > 0 and landing_x < needed_x:
return False, landing_x, needed_x
return True, landing_x, needed_x
SIDE_WALL_YAW_SWEEP = [0.0, 3.0, 5.0, 8.0, 10.0]
def can_reach_gap_with_side_wall(
gap_blocks: int,
dy: float,
wall_offset: int,
sprint: bool = True,
momentum_ticks: int = 12,
) -> tuple[bool, Optional[float], float]:
"""Check gap reachability with a side wall parallel to the jump direction.
wall_offset=0 means the wall is flush with the platform edge (wall at z=1.0
for a 1-wide platform centered at z=0.5). wall_offset=1 means one air block
between the platform edge and the wall face.
Sweeps yaw angles from 0 to 10 degrees toward the wall to find the
worst-case trajectory. Uses the most pessimistic result: if any realistic
yaw angle causes a failure, the case is marked unreachable or gets a
reduced margin. This models the real-world constraint where MCC's
pathfinder can't guarantee perfect yaw alignment.
"""
if dy > 1.252:
return False, None, 0.0
wall_z = 1.0 + wall_offset
start_z = 0.5
clearance = wall_z - (start_z + HALF_WIDTH)
if clearance < 0:
return False, None, 0.0
needed_x = 0.5 + gap_blocks - HALF_WIDTH
landing_platform_start = 0.5 + gap_blocks
if gap_blocks == 0 and dy > 0:
landing_platform_start = 0.5
worst_ok = True
worst_landing_x: Optional[float] = None
worst_margin: Optional[float] = None
for yaw in SIDE_WALL_YAW_SWEEP:
result = get_landing(
sprint=sprint,
target_y=dy,
landing_x_start=landing_platform_start,
momentum_ticks=momentum_ticks,
landing_width=TARGET_BLOCK_WIDTH,
yaw_degrees=yaw,
wall_z=wall_z,
start_z=start_z,
)
if result is None:
return False, worst_landing_x, needed_x
landing_x, landing_y = result
if abs(landing_y - dy) > 0.01:
return False, landing_x, needed_x
if gap_blocks > 0 and landing_x < needed_x:
return False, landing_x, needed_x
margin = landing_x - needed_x
if worst_margin is None or margin < worst_margin:
worst_margin = margin
worst_landing_x = landing_x
return True, worst_landing_x, needed_x