mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
pathing: unify jump moves into MoveJump + IMoveExpander
Replace seven hand-written IMove classes (MoveTraverse, MoveDiagonal, MoveAscend, MoveDiagonalAscend, MoveDiagonalDescend, MoveParkour, MoveSidewallParkour) with a single MoveJump driven by a JumpDescriptor (XOffset, ZOffset, YDelta, JumpFlavor). JumpFeasibility is the single source of truth for the physics/cost rules of every jump-family move. A* no longer iterates a flat IMove[]. The Calculate loop now drives an IMoveExpander[] that writes into a stackalloc Span<MoveNeighbor>, eliminating per-iteration heap traffic. JumpExpander enumerates every jump-family descriptor dynamically; LegacyMoveExpander wraps the remaining dynamic-landing moves (MoveDescend, MoveSprintDescend, MoveClimb, MoveFall) so callers that still pass a custom IMove[] keep working. Add two O(1) short-circuits at the top of JumpExpander.Expand: - Hoist the per-node parkour preconditions (AllowParkour + CanSprint, standing block climbability, feet-liquid, head clearance at y+2) so ~170 SprintJump + Sidewall descriptors never call JumpFeasibility when the node cannot take off at all. - Precompute an 8-way "first step has no floor" table indexed by (sign(dx), sign(dz)) so SprintJump descriptors in a direction that has a walkable floor underneath are dropped without Evaluate. - Add a conservative "any cardinal wall at y or y+1" probe that skips all 112 Sidewall descriptors when no wall exists adjacent to the takeoff. Move tests switch to the new MoveJump.* factory methods. Behavior is verified by the existing test suite: the 21 pre-existing baseline failures are preserved exactly, 0 regressions introduced. Made-with: Cursor
This commit is contained in:
parent
da52aa5c3c
commit
d002930a6a
16 changed files with 1650 additions and 848 deletions
154
docs/superpowers/plans/2026-04-19-unified-jump-move-plan.md
Normal file
154
docs/superpowers/plans/2026-04-19-unified-jump-move-plan.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# Unified Jump Move Refactor
|
||||
|
||||
Date: 2026-04-19
|
||||
Branch: `pathing/jump-entry-direct-yaw`
|
||||
|
||||
## Problem
|
||||
|
||||
MCC's A* uses a hard-coded enumeration of ~220 `IMove` instances covering the
|
||||
"jump family" (Traverse, Diagonal, Ascend, DiagonalAscend, DiagonalDescend,
|
||||
Parkour, SidewallParkour). Each geometric variant is a separate IMove subclass
|
||||
with its own `Calculate` method that re-implements the same physics checks
|
||||
(head clearance, run-up, flight path, landing clearance, gap check). Symptoms:
|
||||
|
||||
1. **Drift**: the same physics rule is implemented in 3-4 places. A fix to
|
||||
`HasDominantAxisRunUp` does not automatically propagate to `HasRunUp`.
|
||||
2. **Missing combinations silently become "impossible"**: until this week, the
|
||||
planner had no `MoveParkour(dx=1, dz=2, yDelta=+1)` entry, so the diagonal
|
||||
ascending jump (upper arrow in the user's pyramid image) was rejected
|
||||
entirely even though the physics allow it.
|
||||
3. **Slow expansion**: every A* node runs 220 feasibility checks and a lot of
|
||||
them are obviously irrelevant for that position (e.g. sidewall checks when
|
||||
there is no wall anywhere near the player).
|
||||
|
||||
Baritone does not have this problem: `MovementParkour` is a single class that
|
||||
dynamically probes reachable landings per direction, and its 8 `Moves` enums
|
||||
cover the entire movement space.
|
||||
|
||||
## Goal
|
||||
|
||||
Bring MCC's jump family to a single parameterized move class with one unified
|
||||
feasibility engine, then evolve to Baritone-style dynamic neighbor expansion.
|
||||
|
||||
## Scope
|
||||
|
||||
**In scope (unified under `MoveJump` + `JumpDescriptor`)**:
|
||||
|
||||
- `MoveTraverse` (dy=0 cardinal)
|
||||
- `MoveDiagonal` (dy=0 corner)
|
||||
- `MoveAscend` (dy=+1 cardinal)
|
||||
- `MoveDiagonalAscend` (dy=+1 corner)
|
||||
- `MoveDiagonalDescend` (dy=-1 corner)
|
||||
- `MoveParkour` (dy ∈ {+1, 0, -1, -2}, horiz up to 5 cardinal / sqrt(10) diag)
|
||||
- `MoveSidewallParkour` (parkour + inner wall requirement)
|
||||
|
||||
**Out of scope (stay as their own classes)**:
|
||||
|
||||
- `MoveDescend` — dynamic variable-depth fall with water/ladder grab logic
|
||||
- `MoveSprintDescend` — dynamic landing depth
|
||||
- `MoveClimb` — ladder/vine vertical movement
|
||||
- `MoveFall` — pure free fall
|
||||
|
||||
These are "descent family" and have a different feasibility model (unknown
|
||||
landing y, hazard scanning). Future refactor can unify them under a
|
||||
`MoveFallToLanding` family but that is a separate effort.
|
||||
|
||||
## Design
|
||||
|
||||
### Data
|
||||
|
||||
```csharp
|
||||
public readonly record struct JumpDescriptor(
|
||||
int XOffset,
|
||||
int ZOffset,
|
||||
int YDelta,
|
||||
JumpFlavor Flavor);
|
||||
|
||||
public enum JumpFlavor
|
||||
{
|
||||
Walk, // dy=0, 1 block move, no jump (Traverse/Diagonal)
|
||||
Step, // dy=±1, 1 block move with jump or step-off (Ascend/DiagDescend/DiagAscend)
|
||||
SprintJump, // horiz >= 2 with or without dy (Parkour)
|
||||
Sidewall, // SprintJump + inner-wall clearance (SidewallParkour)
|
||||
}
|
||||
```
|
||||
|
||||
The descriptor fully describes any jump-family move. `MoveType` (Traverse,
|
||||
Diagonal, Ascend, Descend, Parkour) is derived from `(Flavor, dy, horiz)` so
|
||||
downstream consumers (templates, cost tables) keep working.
|
||||
|
||||
### Evaluator
|
||||
|
||||
`JumpFeasibility.Evaluate(ctx, x, y, z, desc, ref result)` is the single source
|
||||
of truth. It dispatches on `desc.Flavor` but shares the following primitives:
|
||||
|
||||
1. **Guards**: `AllowParkour`, `AllowParkourAscend`, `MaxFallHeight`, `CanSprint`.
|
||||
2. **Profile check**: geometry falls in the valid range for this flavor.
|
||||
3. **Head clearance at start**: `y+2` always, plus `y+3` if ascending sprint jump.
|
||||
4. **Standing material**: reject climbable (ladder/vine) takeoffs.
|
||||
5. **Destination**: floor solid, body passable, head passable, no hazards.
|
||||
6. **Run-up**: cold-start reach tables plus prepared-entry lookup
|
||||
(`EntryPreparationState`). This replaces both `HasRunUp` and
|
||||
`HasDominantAxisRunUp`.
|
||||
7. **Flight path**: cardinal straight-line column sweep or diagonal
|
||||
proportional-step sweep. Needs `y+3` clearance only when ascending.
|
||||
8. **Wall** (Sidewall only): inner wall presence + outer clearance + arc span.
|
||||
9. **Gap check**: reject when a direct walk would work.
|
||||
10. **Cost**: unified sprint/walk cost × horizontal distance + penalties.
|
||||
|
||||
### Step 1 — introduce evaluator, existing classes delegate
|
||||
|
||||
No behavior change. Each of the 7 existing classes has `Calculate` shrunk to
|
||||
a single `JumpFeasibility.Evaluate(...)` call with a descriptor derived from
|
||||
its constructor args. All existing tests pass with the same pass/fail counts.
|
||||
|
||||
### Step 2 — single `MoveJump` class
|
||||
|
||||
Delete the 7 subclasses. `AStarPathFinder.BuildDefaultMoves` emits
|
||||
`MoveJump(descriptor)` instances from a declarative list. Tests that instantiate
|
||||
the old classes are updated to instantiate `MoveJump` with the equivalent
|
||||
descriptor (or use factory helpers like `MoveJump.Parkour(dx, dz, dy)`).
|
||||
|
||||
Templates (`SprintJumpTemplate`, `AscendTemplate`, `SidewallParkourController`,
|
||||
etc.) dispatch on `Flavor` / `MoveType` already, so they need no changes.
|
||||
|
||||
### Step 3 — dynamic neighbor expansion
|
||||
|
||||
`AStarPathFinder.Calculate` currently loops over `_allMoves` for every popped
|
||||
node. Replace with an `IMoveExpander[]` where each expander yields neighbors
|
||||
on demand:
|
||||
|
||||
```csharp
|
||||
public interface IMoveExpander
|
||||
{
|
||||
void Expand(CalculationContext ctx, int x, int y, int z, Action<MoveResult, MoveType, JumpDescriptor?> emit);
|
||||
}
|
||||
```
|
||||
|
||||
`JumpExpander.Expand` iterates the 4 cardinals + 4 diagonals. For each
|
||||
direction, it asks "what is the furthest reachable landing?" by scanning from
|
||||
max distance down to 1, emitting the first feasible result (the A* cost model
|
||||
already disprefers short jumps when long jumps work). This yields ~8-16
|
||||
neighbors per node instead of 220.
|
||||
|
||||
Baritone-style partial-path coefficients (`bestSoFar[6]`) are a separate
|
||||
improvement; not bundled here.
|
||||
|
||||
## Risk / rollback
|
||||
|
||||
- Step 1 is behavior-preserving and easy to revert (delete evaluator, restore
|
||||
the old `Calculate` bodies from git).
|
||||
- Step 2 deletes code; revert means restoring from git.
|
||||
- Step 3 changes the A* main loop. Keep the old `BuildDefaultMoves` path behind
|
||||
an `_useDynamicExpansion` flag so we can A/B test in live `tools/test-parkour.py`
|
||||
runs before deleting the old path.
|
||||
|
||||
## Test strategy
|
||||
|
||||
- `dotnet test MinecraftClient.Tests` after each step. Baseline is 21 failing
|
||||
tests (all pre-existing on this branch). Target: exact same failure set at
|
||||
each checkpoint.
|
||||
- At Step 2 end, verify upper-arrow scenario (this task's motivating bug) still
|
||||
plans correctly.
|
||||
- At Step 3 end, run `tools/test-parkour.py` linear + sidewall + ceiling
|
||||
scenarios on a real server.
|
||||
Loading…
Add table
Add a link
Reference in a new issue