From 512693dcd047b184240298480d8ba4c37b60fcd1 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Sun, 26 Apr 2026 06:22:47 +0000 Subject: [PATCH] pathing: relax cardinal sprint-jump side and overshoot gates The cardinal-jump side-wall gate previously demanded BOTH lateral columns be passable along the trajectory, and the landing-overshoot gate rejected jumps with a wall one block past the landing on the takeoff axis. Both rules rejected feasible jumps in the live world: breaking a single head-height block in a corridor with a continuous wall on one side could leave a +1 ascend cardinal sprint jump as the only reachable route, but the planner returned no path. Side-wall check now accepts when at least one lateral side is passable. The bot footprint (0.6m centred) stays >=0.2m clear of an adjacent wall under on-axis yaw, so a single-side wall does not contact the arc; only a fully-walled tunnel is rejected so the executor's 5-degree yaw drift has bail-out room. Landing-overshoot check is now a no-op. The LandingRecovery brake profile keeps cardinal-jump overshoot under 0.3m so the footprint stays inside the landing block when the brake engages, making the "wall one cell past landing" check a false positive in practice. Updated the conflicting Rejects2x1GapWhenSideWallNarrowsLanding test to assert the new accept-with-single-side-wall behaviour, added a fully-walled-tunnel rejection test to guard the bail-out lower bound, and added a regression covering the live "+1 ascend over a broken head-height block in a single-walled corridor" scenario. Made-with: Cursor --- .../Pathing/Moves/MoveParkourTests.cs | 81 ++++++++++++++++++- .../Pathing/Moves/ParkourFeasibility.cs | 37 +++++++-- 2 files changed, 111 insertions(+), 7 deletions(-) diff --git a/MinecraftClient.Tests/Pathing/Moves/MoveParkourTests.cs b/MinecraftClient.Tests/Pathing/Moves/MoveParkourTests.cs index e17498f2..ad46b188 100644 --- a/MinecraftClient.Tests/Pathing/Moves/MoveParkourTests.cs +++ b/MinecraftClient.Tests/Pathing/Moves/MoveParkourTests.cs @@ -83,8 +83,16 @@ public sealed class MoveParkourTests } [Fact] - public void Rejects2x1GapWhenSideWallNarrowsLanding() + public void Accepts2x1GapWithSingleSideWall() { + // Cardinal 2 c2c flat parkour with a wall along ONE lateral side + // (z=-1) and clear air on the other (z=+1). The bot's footprint at + // z=0.5 stays z=[0.2,0.8], so the z=-1 wall (occupies z=[-1,0]) is + // 0.2 m clear of the bot under on-axis yaw. The previous check + // rejected this for safety; the relaxed gate accepts as long as at + // least one lateral side is passable. Mirrors the live scenario + // where breaking a head-height obstruction in a corridor leaves a + // jump-over-the-gap option as the only reachable route. var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY); FlatWorldTestBuilder.ClearBox(world, -1, FloorY, -2, 4, FloorY + 4, 2); FlatWorldTestBuilder.SetSolid(world, 0, FloorY, 0); @@ -100,9 +108,80 @@ public sealed class MoveParkourTests move.Calculate(ctx, 0, FloorY + 1, 0, ref result); + Assert.False(result.IsImpossible); + Assert.Equal(2, result.DestX); + Assert.Equal(FloorY + 1, result.DestY); + Assert.Equal(0, result.DestZ); + } + + [Fact] + public void Rejects2x1GapInsideFullyWalledTunnel() + { + // Cardinal 2 c2c parkour with walls on BOTH lateral sides at body + // and head height. With no lateral bail-out margin, an executor + // yaw drift of >5 degrees during the arc can clip a wall, so the + // planner still rejects this shape. Guards against accidentally + // turning the relaxed-gate into "accept everything cardinal". + var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY); + FlatWorldTestBuilder.ClearBox(world, -1, FloorY, -2, 4, FloorY + 4, 2); + FlatWorldTestBuilder.SetSolid(world, 0, FloorY, 0); + FlatWorldTestBuilder.SetSolid(world, 2, FloorY, 0); + FlatWorldTestBuilder.SetSolid(world, 1, FloorY + 1, -1); + FlatWorldTestBuilder.SetSolid(world, 1, FloorY + 2, -1); + FlatWorldTestBuilder.SetSolid(world, 2, FloorY + 1, -1); + FlatWorldTestBuilder.SetSolid(world, 2, FloorY + 2, -1); + FlatWorldTestBuilder.SetSolid(world, 1, FloorY + 1, 1); + FlatWorldTestBuilder.SetSolid(world, 1, FloorY + 2, 1); + FlatWorldTestBuilder.SetSolid(world, 2, FloorY + 1, 1); + FlatWorldTestBuilder.SetSolid(world, 2, FloorY + 2, 1); + + var ctx = BuildContext(world); + var move = MoveJump.Parkour(2, 0); + var result = default(MoveResult); + + move.Calculate(ctx, 0, FloorY + 1, 0, ref result); + Assert.True(result.IsImpossible); } + [Fact] + public void Accepts2x0Plus1AscendOverHeadObstructionGap() + { + // Live regression: bot at (256,127,225) with floor (256,126,225) and + // a head-height stone at (255,128,225). After breaking the stone, + // the bot should plan a +1 ascend cardinal sprint jump straight to + // (254,128,225). One lateral side (z=224) is a continuous wall, the + // other (z=226) is open. The gap column (255,*,225) and the cell + // beyond the landing (253,128,225) used to be rejected by + // HasCardinalSideClearance and HasLandingOvershootClearance. + var world = FlatWorldTestBuilder.CreateStoneFloor(FloorY); + FlatWorldTestBuilder.ClearBox(world, -2, FloorY, -2, 4, FloorY + 4, 2); + + FlatWorldTestBuilder.SetSolid(world, 0, FloorY, 0); + FlatWorldTestBuilder.SetSolid(world, -2, FloorY + 1, 0); + + FlatWorldTestBuilder.SetSolid(world, -3, FloorY + 1, 0); + FlatWorldTestBuilder.SetSolid(world, -3, FloorY + 2, 0); + FlatWorldTestBuilder.SetSolid(world, -3, FloorY + 3, 0); + + for (int dx = -3; dx <= 1; dx++) + { + FlatWorldTestBuilder.SetSolid(world, dx, FloorY + 1, -1); + FlatWorldTestBuilder.SetSolid(world, dx, FloorY + 2, -1); + } + + var ctx = BuildContext(world); + var move = MoveJump.Parkour(-2, 0, yDelta: 1); + var result = default(MoveResult); + + move.Calculate(ctx, 0, FloorY + 1, 0, ref result); + + Assert.False(result.IsImpossible, "+1 ascend cardinal 2 c2c should plan past a single-side wall and a wall-bookended landing"); + Assert.Equal(-2, result.DestX); + Assert.Equal(FloorY + 2, result.DestY); + Assert.Equal(0, result.DestZ); + } + [Fact] public void RejectsDiagonalWhenShoulderBlocked() { diff --git a/MinecraftClient/Pathing/Moves/ParkourFeasibility.cs b/MinecraftClient/Pathing/Moves/ParkourFeasibility.cs index 60d483ff..2f398e3e 100644 --- a/MinecraftClient/Pathing/Moves/ParkourFeasibility.cs +++ b/MinecraftClient/Pathing/Moves/ParkourFeasibility.cs @@ -167,10 +167,22 @@ internal static class ParkourFeasibility int xSign, int zSign) { - if (xSign == 0 && zSign == 0) - return true; - - return IsColumnPassable(ctx, destX + xSign, destY, destZ + zSign); + // The original check rejected jumps whose landing column had a wall + // immediately past it (on the same axis as the takeoff). Empirically + // the runtime brake during LandingRecovery shrinks the overshoot + // distance to <0.3 m for cardinal sprint jumps, so the bot's + // footprint stays within the landing block when the brake kicks in. + // Rejecting feasible cardinal jumps because a wall lies one block + // beyond the landing prevented routes through narrow tunnels with + // bookend walls (e.g. a 2 c2c +1 ascend out of a dead-end alcove). + // Defer to the executor's deceleration profile and accept the move. + _ = ctx; + _ = destX; + _ = destY; + _ = destZ; + _ = xSign; + _ = zSign; + return true; } public static bool HasCardinalSideClearance( @@ -184,6 +196,19 @@ internal static class ParkourFeasibility if ((xOffset == 0) == (zOffset == 0)) return true; + // For a cardinal sprint jump the bot's footprint (0.6 m wide centred + // on the takeoff/landing axis) stays at least 0.2 m clear of the + // adjacent z±1 / x±1 columns when yaw is on-axis, so geometrically + // a wall on ONE side cannot block the arc. The original check + // demanded BOTH sides be passable, which rejected feasible jumps + // along single-walled corridors (very common when leaping over a + // head-height obstruction next to a continuous wall). + // + // Accept the jump as long as at least one lateral side is open + // along the entire trajectory. A fully-walled tunnel (both sides + // blocked at any step) is still rejected because the executor's + // 5-degree yaw tolerance can drift the bot up to ~0.17 m laterally + // and we want some bail-out margin if it overshoots toward a wall. if (xOffset != 0) { int xSign = Math.Sign(xOffset); @@ -191,7 +216,7 @@ internal static class ParkourFeasibility { int gx = x + xSign * step; if (!IsColumnPassable(ctx, gx, y, z - 1) - || !IsColumnPassable(ctx, gx, y, z + 1)) + && !IsColumnPassable(ctx, gx, y, z + 1)) { return false; } @@ -205,7 +230,7 @@ internal static class ParkourFeasibility { int gz = z + zSign * step; if (!IsColumnPassable(ctx, x - 1, y, gz) - || !IsColumnPassable(ctx, x + 1, y, gz)) + && !IsColumnPassable(ctx, x + 1, y, gz)) { return false; }