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
|
|
@ -31,7 +31,7 @@ public sealed class MoveParkourTests
|
|||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
world.SetBlock(new Location(-1, FloorY, 0), Block.Air);
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(3, 0);
|
||||
var move = MoveJump.Parkour(3, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -45,7 +45,7 @@ public sealed class MoveParkourTests
|
|||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
world.SetBlock(new Location(1, FloorY, 0), Block.Air);
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(2, 0);
|
||||
var move = MoveJump.Parkour(2, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -60,7 +60,7 @@ public sealed class MoveParkourTests
|
|||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
world.SetBlock(new Location(1, FloorY, 0), Block.Air);
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(2, 0);
|
||||
var move = MoveJump.Parkour(2, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -74,7 +74,7 @@ public sealed class MoveParkourTests
|
|||
{
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(2, 0);
|
||||
var move = MoveJump.Parkour(2, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -95,7 +95,7 @@ public sealed class MoveParkourTests
|
|||
FlatWorldTestBuilder.SetSolid(world, 2, FloorY + 2, -1);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(2, 0);
|
||||
var move = MoveJump.Parkour(2, 0);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -110,7 +110,7 @@ public sealed class MoveParkourTests
|
|||
world.SetBlock(new Location(1, FloorY + 1, 0), new Block(1));
|
||||
world.SetBlock(new Location(1, FloorY + 2, 0), new Block(1));
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(1, 1);
|
||||
var move = MoveJump.Parkour(1, 1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -128,7 +128,7 @@ public sealed class MoveParkourTests
|
|||
|
||||
var ctx = BuildContext(world);
|
||||
SetPreviousMoveType(ctx, MoveType.Parkour);
|
||||
var move = new MoveParkour(4, 0, yDelta: -1);
|
||||
var move = MoveJump.Parkour(4, 0, yDelta: -1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -147,7 +147,7 @@ public sealed class MoveParkourTests
|
|||
FlatWorldTestBuilder.SetSolid(world, 4, FloorY + 1, 0);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(4, 0, yDelta: 1);
|
||||
var move = MoveJump.Parkour(4, 0, yDelta: 1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -164,7 +164,7 @@ public sealed class MoveParkourTests
|
|||
FlatWorldTestBuilder.SetSolid(world, 6, FloorY - 1, 0);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(6, 0, yDelta: -1);
|
||||
var move = MoveJump.Parkour(6, 0, yDelta: -1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -181,7 +181,7 @@ public sealed class MoveParkourTests
|
|||
FlatWorldTestBuilder.SetSolid(world, 6, FloorY - 2, 0);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = new MoveParkour(6, 0, yDelta: -2);
|
||||
var move = MoveJump.Parkour(6, 0, yDelta: -2);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -199,7 +199,7 @@ public sealed class MoveParkourTests
|
|||
|
||||
var ctx = BuildContext(world);
|
||||
SetPreviousMoveType(ctx, MoveType.Parkour);
|
||||
var move = new MoveParkour(6, 0, yDelta: -1);
|
||||
var move = MoveJump.Parkour(6, 0, yDelta: -1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
|
@ -217,11 +217,66 @@ public sealed class MoveParkourTests
|
|||
|
||||
var ctx = BuildContext(world);
|
||||
SetPreviousMoveType(ctx, MoveType.Parkour);
|
||||
var move = new MoveParkour(6, 0, yDelta: -2);
|
||||
var move = MoveJump.Parkour(6, 0, yDelta: -2);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible);
|
||||
}
|
||||
|
||||
// Diagonal ascending parkour: +1 block up with diagonal offset, covers
|
||||
// the corner-step-up case seen in stepped pyramids where a straight
|
||||
// MoveSidewallParkour would demand an adjacent wall that isn't present.
|
||||
// Short (sqrt(5)) ascends work from a lone overhang block because a
|
||||
// cold-start sprint jump reaches ~2.5 blocks horizontally; longer
|
||||
// diagonals such as (2,2) require a runway and are exercised separately.
|
||||
[Theory]
|
||||
[InlineData(2, 1)]
|
||||
[InlineData(1, 2)]
|
||||
public void AcceptsDiagonalAscendingParkour_FromLoneStart(int dx, int dz)
|
||||
{
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
FlatWorldTestBuilder.ClearBox(world, -5, FloorY, -5, 10, FloorY + 5, 10);
|
||||
|
||||
FlatWorldTestBuilder.SetSolid(world, 0, FloorY, 0);
|
||||
int destFloorY = FloorY + 1;
|
||||
FlatWorldTestBuilder.SetSolid(world, dx, destFloorY, dz);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = MoveJump.Parkour(dx, dz, yDelta: 1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
||||
Assert.False(result.IsImpossible, $"diagonal ascend ({dx},{dz},+1) should plan from lone start");
|
||||
Assert.Equal(dx, result.DestX);
|
||||
Assert.Equal(destFloorY + 1, result.DestY);
|
||||
Assert.Equal(dz, result.DestZ);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AcceptsDiagonalAscendingParkour_2x2_WithRunway()
|
||||
{
|
||||
var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY);
|
||||
FlatWorldTestBuilder.ClearBox(world, -5, FloorY, -5, 10, FloorY + 5, 10);
|
||||
|
||||
// Diagonal runway behind the jump (opposite the jump direction)
|
||||
// so HasRunUp's back-step check at (-1,-1) succeeds.
|
||||
FlatWorldTestBuilder.SetSolid(world, -1, FloorY, -1);
|
||||
FlatWorldTestBuilder.SetSolid(world, 0, FloorY, 0);
|
||||
int destFloorY = FloorY + 1;
|
||||
FlatWorldTestBuilder.SetSolid(world, 2, destFloorY, 2);
|
||||
|
||||
var ctx = BuildContext(world);
|
||||
var move = MoveJump.Parkour(2, 2, yDelta: 1);
|
||||
var result = default(MoveResult);
|
||||
|
||||
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
|
||||
|
||||
Assert.False(result.IsImpossible, "(2,2,+1) should plan with a straight runway behind the jump");
|
||||
Assert.Equal(2, result.DestX);
|
||||
Assert.Equal(destFloorY + 1, result.DestY);
|
||||
Assert.Equal(2, result.DestZ);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,21 @@ namespace MinecraftClient.Tests.Pathing.Moves;
|
|||
|
||||
public sealed class MoveSidewallParkourTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("sidewall-descend-gap5-dy-1-wo0", 5, 0)]
|
||||
[InlineData("sidewall-descend-gap5-dy-1-wo1", 5, 1)]
|
||||
public void Calculate_LongDescendStaticEntry_RejectsWithoutPreparedRunup(string scenarioId, int gap, int wallOffset)
|
||||
{
|
||||
World world = SidewallParkourScenarioBuilder.BuildWorld(gap, deltaY: -1, wallOffset);
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var move = MoveJump.Sidewall(dx: -1, dz: gap, yDelta: -1);
|
||||
MoveResult result = default;
|
||||
|
||||
move.Calculate(ctx, 100, 80, 100, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible, scenarioId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("sidewall-flat-gap2-wo0", 2, 0, 0)]
|
||||
[InlineData("sidewall-flat-gap3-wo1", 3, 0, 1)]
|
||||
|
|
@ -21,7 +36,7 @@ public sealed class MoveSidewallParkourTests
|
|||
{
|
||||
World world = SidewallParkourScenarioBuilder.BuildWorld(gap, deltaY, wallOffset);
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var move = new MoveSidewallParkour(xOffset: -1, zOffset: gap, yDelta: deltaY);
|
||||
var move = MoveJump.Sidewall(dx: -1, dz: gap, yDelta: deltaY);
|
||||
MoveResult result = default;
|
||||
|
||||
move.Calculate(ctx, 100, 80, 100, ref result);
|
||||
|
|
@ -41,11 +56,80 @@ public sealed class MoveSidewallParkourTests
|
|||
{
|
||||
World world = SidewallParkourScenarioBuilder.BuildWorld(gap, deltaY, wallOffset);
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var move = new MoveSidewallParkour(xOffset: -1, zOffset: gap, yDelta: deltaY);
|
||||
var move = MoveJump.Sidewall(dx: -1, dz: gap, yDelta: deltaY);
|
||||
MoveResult result = default;
|
||||
|
||||
move.Calculate(ctx, 100, 80, 100, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible, scenarioId);
|
||||
}
|
||||
|
||||
// Scenarios captured from the staircase / step-pyramid image where the start
|
||||
// block is a lone, overhanging tread with no 2-block runway behind it.
|
||||
// Physics allows a cold-start sprint-jump to clear ~3 blocks horizontally,
|
||||
// so short sidewall gaps should still plan even without a runway.
|
||||
[Theory]
|
||||
[InlineData("sidewall-lone-start-flat-gap2-wo0", 2, 0, 0)]
|
||||
[InlineData("sidewall-lone-start-flat-gap3-wo0", 3, 0, 0)]
|
||||
[InlineData("sidewall-lone-start-flat-gap2-wo1", 2, 0, 1)]
|
||||
[InlineData("sidewall-lone-start-ascend-gap2-dy+1-wo0", 2, 1, 0)]
|
||||
[InlineData("sidewall-lone-start-descend-gap2-dy-1-wo0", 2, -1, 0)]
|
||||
[InlineData("sidewall-lone-start-descend-gap3-dy-1-wo0", 3, -1, 0)]
|
||||
public void Calculate_AcceptsLoneStart_ShortSidewallJumps(string scenarioId, int gap, int deltaY, int wallOffset)
|
||||
{
|
||||
World world = BuildLoneStartWorld(gap, deltaY, wallOffset);
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var move = MoveJump.Sidewall(dx: -1, dz: gap, yDelta: deltaY);
|
||||
MoveResult result = default;
|
||||
|
||||
move.Calculate(ctx, 100, 80, 100, ref result);
|
||||
|
||||
Assert.False(result.IsImpossible, scenarioId);
|
||||
Assert.Equal(ParkourProfile.Sidewall, result.ParkourProfile);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("sidewall-lone-start-flat-gap4-wo0", 4, 0, 0)]
|
||||
[InlineData("sidewall-lone-start-ascend-gap3-dy+1-wo0", 3, 1, 0)]
|
||||
[InlineData("sidewall-lone-start-descend-gap4-dy-1-wo0", 4, -1, 0)]
|
||||
public void Calculate_RejectsLoneStart_LongSidewallJumps(string scenarioId, int gap, int deltaY, int wallOffset)
|
||||
{
|
||||
World world = BuildLoneStartWorld(gap, deltaY, wallOffset);
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var move = MoveJump.Sidewall(dx: -1, dz: gap, yDelta: deltaY);
|
||||
MoveResult result = default;
|
||||
|
||||
move.Calculate(ctx, 100, 80, 100, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible, scenarioId);
|
||||
}
|
||||
|
||||
private static World BuildLoneStartWorld(int gap, int deltaY, int wallOffset)
|
||||
{
|
||||
const int startX = 100;
|
||||
const int startY = 80;
|
||||
const int startZ = 100;
|
||||
int floorY = startY - 1;
|
||||
int landX = startX - 1;
|
||||
int landY = startY + deltaY;
|
||||
int landZ = startZ + gap;
|
||||
|
||||
World world = FlatWorldTestBuilder.CreateStoneFloor(floorY: 0, min: 80, max: landZ + 8);
|
||||
FlatWorldTestBuilder.ClearBox(world, 90, 70, 90, 110, 96, landZ + 8);
|
||||
|
||||
FlatWorldTestBuilder.SetSolid(world, startX, floorY, startZ);
|
||||
|
||||
FlatWorldTestBuilder.FillSolid(
|
||||
world,
|
||||
landX,
|
||||
Math.Min(floorY, landY - 1),
|
||||
startZ,
|
||||
landX,
|
||||
Math.Max(floorY, landY - 1) + 7,
|
||||
startZ + wallOffset);
|
||||
|
||||
FlatWorldTestBuilder.SetSolid(world, landX, landY - 1, landZ);
|
||||
|
||||
return world;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue