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

292 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Minecraft Jump Reachability Simulator (Java Edition 1.14+)
Simulates vanilla player physics tick-by-tick to determine which jump
destinations are reachable. Covers:
- Linear jumps: flat, ascending (+N), descending (-N)
- Sprint jumps vs walk jumps
- Neo jumps (wall jumps): 1-block and 2-block wide walls
- Headhitter (2bc ceiling) jumps
All physics constants match vanilla 1.21.x / MCC's PhysicsConsts.cs.
Usage:
python3 sim_jump_reach.py [--verbose] [--csv output.csv]
"""
import argparse
import csv
from pathlib import Path
import sys
REPO_ROOT = Path(__file__).resolve().parent.parent
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from tools.pathing_theory.capabilities import (
build_momentum_capability_bands,
format_momentum_capability_lines,
)
from tools.pathing_theory.canonical import build_canonical_live_cases
from tools.pathing_theory.primitives import (
PLAYER_WIDTH,
can_reach_gap,
can_reach_gap_with_side_wall,
get_apex,
get_landing,
simulate_jump,
)
from tools.pathing_theory.renderers import write_theory_artifacts
from tools.pathing_theory.simulator import build_theory_cases
# ============================================================
# Main analysis
# ============================================================
def analyze_all(verbose: bool = False) -> list[dict]:
results = []
print("=" * 78)
print(" Minecraft Jump Reachability Analysis (Java 1.14+)")
print(" Physics: vanilla 1.21.x constants from PhysicsConsts.cs")
print("=" * 78)
# --- Part 1: Apex ---
print("\n[1] Jump Apex (Maximum Height)")
print(f" {'Mode':<8} {'Momentum':>8} {'Apex Y':>10} {'X at Apex':>12}")
print(f" {'----':<8} {'--------':>8} {'------':>10} {'---------':>12}")
for sprint in [False, True]:
for mm in [0, 6, 12, 20]:
ay, ax = get_apex(sprint=sprint, momentum_ticks=mm)
label = "Sprint" if sprint else "Walk"
print(f" {label:<8} {mm:>6}t {ay:>10.4f} {ax:>12.4f}")
results.append({'type': 'apex', 'sprint': sprint,
'momentum': mm, 'apex_y': ay, 'x_at_apex': ax})
# --- Part 2: Landing distances (flat and descending) ---
print(f"\n[2] Landing Distance (sprint, 12t momentum)")
print(f" {'dy':>6} {'Landing X':>12}")
print(f" {'--':>6} {'---------':>12}")
for dy in [0.0, -1.0, -2.0, -3.0, -5.0, -10.0]:
r = get_landing(sprint=True, target_y=dy,
landing_x_start=0.0 if dy <= 0 else 0.5,
momentum_ticks=12)
sign = "+" if dy > 0 else " " if dy == 0 else ""
if r:
print(f" {sign}{dy:>5.1f} {r[0]:>12.4f}m")
else:
print(f" {sign}{dy:>5.1f} {'N/A':>12}")
# --- Part 3: Full feasibility matrix ---
print(f"\n[3] Gap Feasibility Matrix (Sprint, 12t momentum)")
print(f" Player width={PLAYER_WIDTH}m, max jump height=~1.252b")
print()
dy_values = [1.0, 0.5, 0.0, -1.0, -2.0, -3.0, -5.0]
header = f" {'Gap':>4}"
for dy in dy_values:
sign = "+" if dy > 0 else ""
header += f" {sign}{dy:>5.1f}"
print(header)
print(f" {'----':>4}" + " ------" * len(dy_values))
for gap in range(0, 7):
row = f" {gap:>4}"
for dy in dy_values:
ok, lx, needed = can_reach_gap(gap, dy, sprint=True, momentum_ticks=12)
if ok:
row += f" {'YES':>6}"
elif lx is None:
row += f" {'N/A':>6}"
else:
row += f" {'no':>6}"
print(row)
# Walk version
print(f"\n Walk jump (no sprint), 12t momentum:")
header = f" {'Gap':>4}"
for dy in dy_values:
sign = "+" if dy > 0 else ""
header += f" {sign}{dy:>5.1f}"
print(header)
print(f" {'----':>4}" + " ------" * len(dy_values))
for gap in range(0, 6):
row = f" {gap:>4}"
for dy in dy_values:
ok, lx, needed = can_reach_gap(gap, dy, sprint=False, momentum_ticks=12)
if ok:
row += f" {'YES':>6}"
elif lx is None:
row += f" {'N/A':>6}"
else:
row += f" {'no':>6}"
print(row)
# Standing jump (0 momentum)
print(f"\n Standing sprint jump (0t momentum):")
header = f" {'Gap':>4}"
for dy in dy_values:
sign = "+" if dy > 0 else ""
header += f" {sign}{dy:>5.1f}"
print(header)
print(f" {'----':>4}" + " ------" * len(dy_values))
for gap in range(0, 5):
row = f" {gap:>4}"
for dy in dy_values:
ok, lx, needed = can_reach_gap(gap, dy, sprint=True, momentum_ticks=0)
if ok:
row += f" {'YES':>6}"
elif lx is None:
row += f" {'N/A':>6}"
else:
row += f" {'no':>6}"
print(row)
# --- Part 4: Neo analysis ---
print(f"\n[4] Neo Jump Analysis (flat, 12t momentum)")
print(f" Wall extends perpendicular to movement.")
print(f" Player must travel wall_length + {PLAYER_WIDTH}m to clear wall end.\n")
print(f" {'Wall':>5} {'Mode':<8} {'LandingX':>10} {'Needed':>10} {'Margin':>10} {'OK':>6}")
print(f" {'----':>5} {'----':<8} {'--------':>10} {'------':>10} {'------':>10} {'--':>6}")
for wall_len in [1, 2, 3, 4]:
for sprint in [True, False]:
r = get_landing(sprint=sprint, target_y=0.0,
landing_x_start=0.0, momentum_ticks=12)
label = "Sprint" if sprint else "Walk"
if r is None:
print(f" {wall_len:>5} {label:<8} {'N/A':>10}")
continue
lx = r[0]
needed = wall_len + PLAYER_WIDTH
margin = lx - needed
ok = "YES" if margin >= 0 else "no"
print(f" {wall_len:>5} {label:<8} {lx:>10.4f} {needed:>10.4f} "
f"{margin:>+10.4f} {ok:>6}")
results.append({'type': 'neo', 'wall': wall_len, 'sprint': sprint,
'reach': lx, 'needed': needed, 'margin': margin,
'ok': margin >= 0})
# --- Part 5: Ceiling ---
print(f"\n[5] Ceiling-Constrained Jumps (Sprint, 12t mm, flat)")
base_r = get_landing(sprint=True, target_y=0.0, momentum_ticks=12)
base_lx = base_r[0] if base_r else 0
print(f" {'Ceiling':>8} {'LandingX':>12} {'Delta':>10}")
for ceil in [4.0, 3.0, 2.5, 2.0, 1.8125]:
r = get_landing(sprint=True, target_y=0.0, momentum_ticks=12,
ceiling_y=ceil)
if r:
diff = r[0] - base_lx
print(f" {ceil:>7.4f}b {r[0]:>11.4f}m {diff:>+10.4f}")
else:
print(f" {ceil:>7.4f}b {'N/A':>12}")
# --- Part 6: Side-wall jumps ---
print(f"\n[6] Side-Wall Jump Feasibility (Sprint, 12t momentum)")
print(f" Wall parallel to jump direction, flush with platform edge (wo=0)")
print(f" Yaw sweep 0-10 deg, worst-case used.")
print()
dy_values_sw = [1.0, 0.0, -1.0, -2.0]
header_sw = f" {'Gap':>4}"
for dy in dy_values_sw:
sign = "+" if dy > 0 else ""
header_sw += f" {sign}{dy:>5.1f}(L) {sign}{dy:>5.1f}(W)"
print(header_sw)
print(f" {'----':>4}" + " ---------- ----------" * len(dy_values_sw))
for gap in range(0, 7):
row = f" {gap:>4}"
for dy in dy_values_sw:
ok_lin, _, _ = can_reach_gap(gap, dy, sprint=True, momentum_ticks=12)
ok_sw, _, _ = can_reach_gap_with_side_wall(
gap, dy, wall_offset=0, sprint=True, momentum_ticks=12,
)
lin_str = "YES" if ok_lin else "no"
sw_str = "YES" if ok_sw else "no"
marker = " " if ok_lin == ok_sw else "*"
row += f" {lin_str:>6} {sw_str:>6}{marker}"
print(row)
print()
print(" L=linear (no wall), W=wall (wo=0), *=reachability differs")
# --- Part 7: Verbose ---
if verbose:
for label, sp in [("Sprint", True), ("Walk", False)]:
print(f"\n[V] {label} Jump Trajectory (12t momentum, flat)")
print(f" {'Tick':>4} {'X':>10} {'Y':>10} {'Z':>10} {'VX':>10} {'VY':>10} {'VZ':>10} {'Gnd':>5}")
traj = simulate_jump(sprint=sp, momentum_ticks=12, landing_y=0.0)
for s in traj:
g = "G" if s.on_ground else ""
print(f" {s.tick:>4} {s.x:>10.4f} {s.y:>10.4f} {s.z:>10.4f} "
f"{s.vx:>10.6f} {s.vy:>10.6f} {s.vz:>10.6f} {g:>5}")
print(f"\n[V] Sprint flat with side wall (12t mm, yaw=10, wo=0)")
print(f" {'Tick':>4} {'X':>10} {'Y':>10} {'Z':>10} {'VX':>10} {'VY':>10} {'VZ':>10} {'Gnd':>5}")
traj = simulate_jump(sprint=True, momentum_ticks=12, landing_y=0.0,
landing_x_start=4.5, landing_width=1.0,
yaw_degrees=10.0, wall_z=1.0, start_z=0.5)
for s in traj:
g = "G" if s.on_ground else ""
print(f" {s.tick:>4} {s.x:>10.4f} {s.y:>10.4f} {s.z:>10.4f} "
f"{s.vx:>10.6f} {s.vy:>10.6f} {s.vz:>10.6f} {g:>5}")
return results
def list_momentum_capabilities() -> None:
cases = build_theory_cases()
bands = build_momentum_capability_bands(cases)
for line in format_momentum_capability_lines(bands):
print(line)
def main():
parser = argparse.ArgumentParser(
description="Minecraft jump reachability simulator (Java 1.14+)")
parser.add_argument("--verbose", "-v", action="store_true",
help="Print per-tick trajectory data")
parser.add_argument("--csv", type=str, default=None,
help="Export results to CSV file")
parser.add_argument("--write-artifacts", type=str, default=None,
help="Write tracked theory artifacts to a directory")
parser.add_argument("--list-capabilities", action="store_true",
help="List compressed mm breakpoint capabilities")
args = parser.parse_args()
if args.write_artifacts:
cases = build_theory_cases()
canonical_cases = build_canonical_live_cases(cases)
capability_bands = build_momentum_capability_bands(cases)
write_theory_artifacts(
cases,
canonical_cases,
capability_bands,
Path(args.write_artifacts),
)
print(f"Wrote theory artifacts to {args.write_artifacts}")
return
if args.list_capabilities:
list_momentum_capabilities()
return
results = analyze_all(verbose=args.verbose)
if args.csv and results:
keys = set()
for r in results:
keys.update(r.keys())
with open(args.csv, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=sorted(keys))
writer.writeheader()
writer.writerows(results)
print(f"\nResults exported to {args.csv}")
if __name__ == "__main__":
main()