mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: extract reusable pathing theory generator
This commit is contained in:
parent
07c948a8b8
commit
7fbb32d8b9
7 changed files with 389 additions and 260 deletions
1
tools/pathing_theory/__init__.py
Normal file
1
tools/pathing_theory/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Reusable theory generation helpers for pathing analysis tools."""
|
||||
19
tools/pathing_theory/models.py
Normal file
19
tools/pathing_theory/models.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TheoryCase:
|
||||
case_id: str
|
||||
family: str
|
||||
subfamily: str
|
||||
movement_mode: str
|
||||
momentum_ticks: int
|
||||
gap_blocks: int | None
|
||||
delta_y: float | None
|
||||
ceiling_height: float | None
|
||||
wall_width: int | None
|
||||
expected_reachable: bool
|
||||
landing_x: float | None
|
||||
apex_y: float | None
|
||||
margin: float | None
|
||||
notes: str = ""
|
||||
214
tools/pathing_theory/primitives.py
Normal file
214
tools/pathing_theory/primitives.py
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
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
|
||||
|
||||
|
||||
@dataclass
|
||||
class TickState:
|
||||
tick: int = 0
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
vx: float = 0.0
|
||||
vy: 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 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,
|
||||
max_ticks: int = 200,
|
||||
) -> list[TickState]:
|
||||
x, y, vx, vy = 0.0, 0.0, 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, vx, vy, on_ground))
|
||||
|
||||
for tick in range(1, max_ticks + 1):
|
||||
if vx * vx < HORIZONTAL_VELOCITY_THRESHOLD_SQR:
|
||||
vx = 0.0
|
||||
if abs(vy) < VERTICAL_VELOCITY_THRESHOLD:
|
||||
vy = 0.0
|
||||
|
||||
do_jump = False
|
||||
if not jumped and tick > momentum_ticks and on_ground:
|
||||
do_jump = True
|
||||
jumped = True
|
||||
|
||||
if do_jump:
|
||||
vy = max(BASE_JUMP_POWER, vy)
|
||||
if sprint:
|
||||
vx += SPRINT_JUMP_HORIZONTAL_BOOST
|
||||
|
||||
forward_input = 1.0 * INPUT_FRICTION
|
||||
speed = get_ground_speed() if on_ground else AIR_ACCEL
|
||||
vx += forward_input * speed
|
||||
|
||||
new_x = x + vx
|
||||
new_y = y + vy
|
||||
new_on_ground = False
|
||||
|
||||
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
|
||||
|
||||
floor_y = 0.0 if new_x < landing_x_start else landing_y
|
||||
|
||||
if jumped:
|
||||
if new_x >= landing_x_start:
|
||||
if landing_y >= 0:
|
||||
if vy <= 0 and y >= landing_y and new_y <= landing_y:
|
||||
new_y = landing_y
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
elif vy <= 0 and new_y <= landing_y:
|
||||
new_y = landing_y
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
else:
|
||||
if new_y <= landing_y:
|
||||
new_y = landing_y
|
||||
if vy < 0:
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
|
||||
if not new_on_ground and new_x < landing_x_start and new_y <= floor_y:
|
||||
new_y = floor_y
|
||||
if vy < 0:
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
elif new_y <= 0.0:
|
||||
new_y = 0.0
|
||||
if vy < 0:
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
|
||||
x = new_x
|
||||
y = new_y
|
||||
on_ground = new_on_ground
|
||||
|
||||
vy -= GRAVITY
|
||||
vy *= DRAG_Y
|
||||
|
||||
if on_ground:
|
||||
vx *= ground_friction
|
||||
else:
|
||||
vx *= FRICTION_MULTIPLIER
|
||||
|
||||
trajectory.append(TickState(tick, x, y, vx, vy, 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,
|
||||
) -> 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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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
|
||||
119
tools/pathing_theory/simulator.py
Normal file
119
tools/pathing_theory/simulator.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
from tools.pathing_theory.models import TheoryCase
|
||||
from tools.pathing_theory.primitives import PLAYER_WIDTH, can_reach_gap, get_apex, get_landing
|
||||
|
||||
|
||||
def _float_token(value: float) -> str:
|
||||
return f"{value:.1f}".replace("-", "m").replace(".", "p")
|
||||
|
||||
|
||||
def build_theory_cases() -> list[TheoryCase]:
|
||||
cases: list[TheoryCase] = []
|
||||
|
||||
for sprint, movement_mode, momentum_ticks in [
|
||||
(False, "walk", 12),
|
||||
(True, "sprint", 0),
|
||||
(True, "sprint", 12),
|
||||
]:
|
||||
for gap in range(0, 7):
|
||||
for delta_y in [0.0, 1.0, -1.0, -2.0]:
|
||||
ok, landing_x, needed_x = can_reach_gap(
|
||||
gap_blocks=gap,
|
||||
dy=delta_y,
|
||||
sprint=sprint,
|
||||
momentum_ticks=momentum_ticks,
|
||||
)
|
||||
apex_y, _ = get_apex(sprint=sprint, momentum_ticks=momentum_ticks)
|
||||
subfamily = (
|
||||
"flat"
|
||||
if delta_y == 0.0
|
||||
else "ascend"
|
||||
if delta_y > 0.0
|
||||
else "descend"
|
||||
)
|
||||
cases.append(
|
||||
TheoryCase(
|
||||
case_id=(
|
||||
f"linear-{subfamily}-{movement_mode}-mm{momentum_ticks}"
|
||||
f"-gap{gap}-dy{_float_token(delta_y)}"
|
||||
),
|
||||
family="linear",
|
||||
subfamily=subfamily,
|
||||
movement_mode=movement_mode,
|
||||
momentum_ticks=momentum_ticks,
|
||||
gap_blocks=gap,
|
||||
delta_y=delta_y,
|
||||
ceiling_height=None,
|
||||
wall_width=None,
|
||||
expected_reachable=ok,
|
||||
landing_x=landing_x,
|
||||
apex_y=apex_y,
|
||||
margin=None if landing_x is None else landing_x - needed_x,
|
||||
)
|
||||
)
|
||||
|
||||
landing = get_landing(
|
||||
sprint=True,
|
||||
target_y=0.0,
|
||||
landing_x_start=0.0,
|
||||
momentum_ticks=12,
|
||||
)
|
||||
for wall_width in [1, 2, 3, 4]:
|
||||
landing_x = None if landing is None else landing[0]
|
||||
needed_x = wall_width + PLAYER_WIDTH
|
||||
margin = None if landing_x is None else landing_x - needed_x
|
||||
cases.append(
|
||||
TheoryCase(
|
||||
case_id=f"neo-neo-sprint-mm12-wall{wall_width}",
|
||||
family="neo",
|
||||
subfamily="neo",
|
||||
movement_mode="sprint",
|
||||
momentum_ticks=12,
|
||||
gap_blocks=None,
|
||||
delta_y=0.0,
|
||||
ceiling_height=None,
|
||||
wall_width=wall_width,
|
||||
expected_reachable=margin is not None and margin >= 0.0,
|
||||
landing_x=landing_x,
|
||||
apex_y=get_apex(sprint=True, momentum_ticks=12)[0],
|
||||
margin=margin,
|
||||
)
|
||||
)
|
||||
|
||||
for ceiling_height in [4.0, 3.0, 2.5, 2.0, 1.8125]:
|
||||
for gap in [1, 2, 3, 4]:
|
||||
landing = get_landing(
|
||||
sprint=True,
|
||||
target_y=0.0,
|
||||
landing_x_start=0.5 + gap,
|
||||
momentum_ticks=12,
|
||||
ceiling_y=ceiling_height,
|
||||
)
|
||||
landing_x = None if landing is None else landing[0]
|
||||
needed_x = 0.5 + gap + (PLAYER_WIDTH / 2.0)
|
||||
margin = None if landing_x is None else landing_x - needed_x
|
||||
cases.append(
|
||||
TheoryCase(
|
||||
case_id=(
|
||||
f"ceiling-headhitter-sprint-mm12-gap{gap}"
|
||||
f"-ceil{str(ceiling_height).replace('.', 'p')}"
|
||||
),
|
||||
family="ceiling",
|
||||
subfamily="headhitter",
|
||||
movement_mode="sprint",
|
||||
momentum_ticks=12,
|
||||
gap_blocks=gap,
|
||||
delta_y=0.0,
|
||||
ceiling_height=ceiling_height,
|
||||
wall_width=None,
|
||||
expected_reachable=margin is not None and margin >= 0.0,
|
||||
landing_x=landing_x,
|
||||
apex_y=get_apex(
|
||||
sprint=True,
|
||||
momentum_ticks=12,
|
||||
ceiling_y=ceiling_height,
|
||||
)[0],
|
||||
margin=margin,
|
||||
)
|
||||
)
|
||||
|
||||
return cases
|
||||
|
|
@ -16,267 +16,15 @@ Usage:
|
|||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import csv
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
# ============================================================
|
||||
# Vanilla physics constants (match PhysicsConsts.cs)
|
||||
# ============================================================
|
||||
|
||||
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 # 0.3
|
||||
|
||||
|
||||
@dataclass
|
||||
class TickState:
|
||||
tick: int = 0
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
vx: float = 0.0
|
||||
vy: float = 0.0
|
||||
on_ground: bool = True
|
||||
|
||||
|
||||
def get_ground_speed(block_friction: float = DEFAULT_BLOCK_FRICTION) -> float:
|
||||
f = block_friction * FRICTION_MULTIPLIER
|
||||
return MOVEMENT_SPEED * (GROUND_ACCEL_FACTOR / (f * f * f))
|
||||
|
||||
|
||||
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,
|
||||
max_ticks: int = 200) -> list[TickState]:
|
||||
"""
|
||||
Simulate a complete jump sequence: momentum phase on ground, then jump.
|
||||
|
||||
The player starts at x=0, y=0 on a platform at y=0.
|
||||
|
||||
landing_y: Y coordinate of the landing surface.
|
||||
landing_x_start: the X coordinate where the landing surface begins.
|
||||
For flat jumps (landing_y=0), this is 0 (same level everywhere).
|
||||
For ascending jumps (landing_y>0), this is typically gap_start
|
||||
(the landing platform isn't under the player at takeoff).
|
||||
For descending jumps (landing_y<0), this is gap_start.
|
||||
|
||||
The starting platform is at y=0 from x=-inf to x=landing_x_start.
|
||||
The landing platform is at y=landing_y from x=landing_x_start onward.
|
||||
"""
|
||||
x, y, vx, vy = 0.0, 0.0, 0.0, 0.0
|
||||
on_ground = True
|
||||
trajectory: list[TickState] = []
|
||||
jumped = False
|
||||
f_ground = DEFAULT_BLOCK_FRICTION * FRICTION_MULTIPLIER
|
||||
|
||||
trajectory.append(TickState(0, x, y, vx, vy, on_ground))
|
||||
|
||||
for tick in range(1, max_ticks + 1):
|
||||
# --- Zero tiny velocity ---
|
||||
if vx * vx < HORIZONTAL_VELOCITY_THRESHOLD_SQR:
|
||||
vx = 0.0
|
||||
if abs(vy) < VERTICAL_VELOCITY_THRESHOLD:
|
||||
vy = 0.0
|
||||
|
||||
# --- Jump on the tick after momentum ---
|
||||
do_jump = False
|
||||
if not jumped and tick > momentum_ticks and on_ground:
|
||||
do_jump = True
|
||||
jumped = True
|
||||
|
||||
if do_jump:
|
||||
vy = max(BASE_JUMP_POWER, vy)
|
||||
if sprint:
|
||||
vx += SPRINT_JUMP_HORIZONTAL_BOOST
|
||||
|
||||
# --- Input acceleration ---
|
||||
forward_input = 1.0 * INPUT_FRICTION
|
||||
if on_ground:
|
||||
speed = get_ground_speed()
|
||||
else:
|
||||
speed = AIR_ACCEL
|
||||
vx += forward_input * speed
|
||||
|
||||
# --- Move ---
|
||||
new_x = x + vx
|
||||
new_y = y + vy
|
||||
new_on_ground = False
|
||||
|
||||
# Ceiling collision
|
||||
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
|
||||
|
||||
# Floor collision: two-region terrain model
|
||||
# Region 1: x < landing_x_start -> floor at y=0 (starting platform)
|
||||
# Region 2: x >= landing_x_start -> floor at y=landing_y
|
||||
# Player bounding box trailing edge is at (new_x - HALF_WIDTH)
|
||||
# Use player center for region determination
|
||||
if new_x < landing_x_start:
|
||||
floor_y = 0.0
|
||||
else:
|
||||
floor_y = landing_y
|
||||
|
||||
if jumped:
|
||||
if new_x >= landing_x_start:
|
||||
# Over the landing platform region
|
||||
if landing_y >= 0:
|
||||
# Ascending or flat: only land when falling DOWN through the surface
|
||||
if vy <= 0 and y >= landing_y and new_y <= landing_y:
|
||||
new_y = landing_y
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
elif vy <= 0 and new_y <= landing_y:
|
||||
# Already below the surface (fell through on a prior tick
|
||||
# that didn't trigger -- shouldn't happen but safety check)
|
||||
new_y = landing_y
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
else:
|
||||
# Descending: land when reaching the lower floor
|
||||
if new_y <= landing_y:
|
||||
new_y = landing_y
|
||||
if vy < 0:
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
|
||||
if not new_on_ground and new_x < landing_x_start:
|
||||
# Still over starting platform area or in the gap
|
||||
if new_y <= 0.0:
|
||||
new_y = 0.0
|
||||
if vy < 0:
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
else:
|
||||
# Momentum phase: always on starting platform
|
||||
if new_y <= 0.0:
|
||||
new_y = 0.0
|
||||
if vy < 0:
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
|
||||
x = new_x
|
||||
y = new_y
|
||||
on_ground = new_on_ground
|
||||
|
||||
# --- Post-move: gravity + friction/drag ---
|
||||
vy -= GRAVITY
|
||||
vy *= DRAG_Y
|
||||
|
||||
if on_ground:
|
||||
vx *= f_ground
|
||||
else:
|
||||
vx *= FRICTION_MULTIPLIER
|
||||
|
||||
trajectory.append(TickState(tick, x, y, vx, vy, on_ground))
|
||||
|
||||
# Stop once landed after being airborne
|
||||
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) -> Optional[tuple[float, float]]:
|
||||
"""Get (x, y) where the player lands. Returns None if no landing."""
|
||||
traj = simulate_jump(sprint=sprint, momentum_ticks=momentum_ticks,
|
||||
ceiling_y=ceiling_y, landing_y=target_y,
|
||||
landing_x_start=landing_x_start)
|
||||
was_air = False
|
||||
for s in traj:
|
||||
if not s.on_ground:
|
||||
was_air = True
|
||||
if was_air and s.on_ground:
|
||||
return s.x, s.y
|
||||
return None
|
||||
|
||||
|
||||
def get_apex(sprint: bool, momentum_ticks: int = 12,
|
||||
ceiling_y: Optional[float] = None) -> tuple[float, float]:
|
||||
traj = 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 s in traj:
|
||||
if s.y > best_y:
|
||||
best_y = s.y
|
||||
best_x = s.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]:
|
||||
"""
|
||||
Check if the player can cross a gap of `gap_blocks` blocks to a surface
|
||||
at height offset `dy`.
|
||||
|
||||
Geometry (player starts centered on block, center at x=0):
|
||||
- Starting platform right edge: x = 0.5
|
||||
- Gap: 0.5 to 0.5 + gap_blocks
|
||||
- Landing platform left edge: x = 0.5 + gap_blocks
|
||||
- Player center must reach x >= 0.5 + gap_blocks + HALF_WIDTH to land
|
||||
(trailing bounding box edge clears the gap)
|
||||
|
||||
For ascending jumps (dy > 0):
|
||||
- Landing surface at y=dy begins at x = 0.5 + gap_blocks
|
||||
- The gap region has NO floor (void) if gap > 0, or floor at dy if gap = 0
|
||||
|
||||
For gap = 0 and dy > 0:
|
||||
- This means stepping up to an adjacent block 1m higher.
|
||||
- Player just needs to jump and move forward 1 block.
|
||||
"""
|
||||
if dy > 1.252:
|
||||
return False, None, 0.0
|
||||
|
||||
needed_x = 0.5 + gap_blocks + HALF_WIDTH
|
||||
landing_platform_start = 0.5 + gap_blocks
|
||||
|
||||
# For gap=0 ascending, the landing platform is right next to the start
|
||||
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)
|
||||
if result is None:
|
||||
return False, None, needed_x
|
||||
|
||||
lx, ly = result
|
||||
# Check if we actually landed on the target surface (not back on start)
|
||||
if abs(ly - dy) > 0.01:
|
||||
# Landed back on starting platform
|
||||
return False, lx, needed_x
|
||||
|
||||
# For gap > 0, check player center is past the gap
|
||||
if gap_blocks > 0 and lx < needed_x:
|
||||
return False, lx, needed_x
|
||||
|
||||
return True, lx, needed_x
|
||||
from tools.pathing_theory.primitives import (
|
||||
PLAYER_WIDTH,
|
||||
can_reach_gap,
|
||||
get_apex,
|
||||
get_landing,
|
||||
simulate_jump,
|
||||
)
|
||||
from tools.pathing_theory.simulator import build_theory_cases
|
||||
|
||||
|
||||
# ============================================================
|
||||
|
|
|
|||
1
tools/tests/__init__.py
Normal file
1
tools/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Test package for Python tooling."""
|
||||
27
tools/tests/test_pathing_theory_matrix.py
Normal file
27
tools/tests/test_pathing_theory_matrix.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import unittest
|
||||
|
||||
from tools.pathing_theory.simulator import build_theory_cases
|
||||
|
||||
|
||||
class PathingTheoryMatrixTests(unittest.TestCase):
|
||||
def test_build_theory_cases_returns_first_wave_families(self) -> None:
|
||||
cases = build_theory_cases()
|
||||
families = {(case.family, case.subfamily) for case in cases}
|
||||
|
||||
self.assertIn(("linear", "flat"), families)
|
||||
self.assertIn(("linear", "ascend"), families)
|
||||
self.assertIn(("linear", "descend"), families)
|
||||
self.assertIn(("neo", "neo"), families)
|
||||
self.assertIn(("ceiling", "headhitter"), families)
|
||||
|
||||
linear_boundary = next(
|
||||
case
|
||||
for case in cases
|
||||
if case.case_id == "linear-flat-sprint-mm12-gap5-dy0p0"
|
||||
)
|
||||
self.assertTrue(linear_boundary.expected_reachable)
|
||||
self.assertGreater(linear_boundary.margin, 0.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue