pathing: align parkour contracts with live budgets

This commit is contained in:
BruceChen 2026-04-15 17:36:20 +00:00
parent ce555da95e
commit cf8bf349db
16 changed files with 3190 additions and 318 deletions

View file

@ -150,6 +150,8 @@ public sealed class PathingContractStore
throw new InvalidDataException($"Timing budget '{budget.ScenarioId}' must use zero totals when it has no segments.");
var normalizedSegments = new List<PathingSegmentTimingBudget>(budget.Segments.Count);
int expectedSegmentTicksSum = 0;
int maxSegmentTicksSum = 0;
for (int i = 0; i < budget.Segments.Count; i++)
{
PathingSegmentTimingBudget segment = budget.Segments[i];
@ -159,9 +161,23 @@ public sealed class PathingContractStore
if (segment.ExpectedTicks > segment.MaxTicks)
throw new InvalidDataException($"Timing budget '{budget.ScenarioId}' segment {i} has ExpectedTicks greater than MaxTicks.");
expectedSegmentTicksSum = checked(expectedSegmentTicksSum + segment.ExpectedTicks);
maxSegmentTicksSum = checked(maxSegmentTicksSum + segment.MaxTicks);
normalizedSegments.Add(segment);
}
if (budget.ExpectedTotalTicks != expectedSegmentTicksSum)
{
throw new InvalidDataException(
$"Timing budget '{budget.ScenarioId}' ExpectedTotalTicks mismatch. Total={budget.ExpectedTotalTicks}, segmentSum={expectedSegmentTicksSum}.");
}
if (budget.MaxTotalTicks != maxSegmentTicksSum)
{
throw new InvalidDataException(
$"Timing budget '{budget.ScenarioId}' MaxTotalTicks mismatch. Total={budget.MaxTotalTicks}, segmentSum={maxSegmentTicksSum}.");
}
return budget with { Segments = normalizedSegments.AsReadOnly() };
}

View file

@ -5,6 +5,7 @@ using MinecraftClient.Pathing.Core;
using MinecraftClient.Pathing.Execution;
using MinecraftClient.Pathing.Execution.Templates;
using MinecraftClient.Pathing.Goals;
using MinecraftClient.Physics;
using Xunit;
namespace MinecraftClient.Tests.Pathing.Execution;
@ -35,6 +36,94 @@ public sealed class LivePathingRegressionTests
Assert.Empty(PathSegmentBuilder.FromPath(result.Path));
}
[Fact]
public void AStar_RepeatedSingleGapParkourChain_PrefersTwoLongJumpsOverFourShortJumps()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 578, max: 590);
FlatWorldTestBuilder.ClearBox(world, 578, 79, 578, 590, 90, 582);
FlatWorldTestBuilder.SetSolid(world, 580, 79, 580);
FlatWorldTestBuilder.SetSolid(world, 582, 79, 580);
FlatWorldTestBuilder.SetSolid(world, 584, 79, 580);
FlatWorldTestBuilder.SetSolid(world, 586, 79, 580);
FlatWorldTestBuilder.SetSolid(world, 588, 79, 580);
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
var finder = new AStarPathFinder();
PathResult result = finder.Calculate(
ctx,
startX: 580,
startY: 80,
startZ: 580,
new GoalBlock(588, 80, 580),
CancellationToken.None,
timeoutMs: 2000);
List<PathSegment> segments = PathSegmentBuilder.FromPath(result.Path);
Assert.Equal(PathStatus.Success, result.Status);
Assert.Collection(
segments,
first =>
{
Assert.Equal(MoveType.Parkour, first.MoveType);
Assert.Equal(new Location(580.5, 80, 580.5), first.Start);
Assert.Equal(new Location(584.5, 80, 580.5), first.End);
},
second =>
{
Assert.Equal(MoveType.Parkour, second.MoveType);
Assert.Equal(new Location(584.5, 80, 580.5), second.Start);
Assert.Equal(new Location(588.5, 80, 580.5), second.End);
});
}
[Fact]
public void PathExecutor_RepeatedSingleGapParkourChain_TwoLongJumps_CompletesWithoutReplan()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 578, max: 590);
FlatWorldTestBuilder.ClearBox(world, 578, 79, 578, 590, 90, 582);
FlatWorldTestBuilder.SetSolid(world, 580, 79, 580);
FlatWorldTestBuilder.SetSolid(world, 582, 79, 580);
FlatWorldTestBuilder.SetSolid(world, 584, 79, 580);
FlatWorldTestBuilder.SetSolid(world, 586, 79, 580);
FlatWorldTestBuilder.SetSolid(world, 588, 79, 580);
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
var finder = new AStarPathFinder();
PathResult result = finder.Calculate(
ctx,
startX: 580,
startY: 80,
startZ: 580,
new GoalBlock(588, 80, 580),
CancellationToken.None,
timeoutMs: 2000);
var debugLogs = new List<string>();
var infoLogs = new List<string>();
var manager = new PathSegmentManager(debugLogs.Add, infoLogs.Add);
var physics = TemplateSimulationRunner.CreateGroundedPhysics(new Location(580.5, 80, 580.5), yaw: 270f);
var input = new MovementInput();
manager.StartNavigation(new GoalBlock(588, 80, 580), result);
for (int tick = 0; tick < 240 && manager.IsNavigating; tick++)
{
input.Reset();
Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
manager.Tick(pos, physics, input, world);
if (!manager.IsNavigating)
break;
physics.ApplyInput(input);
physics.Tick(world);
}
Assert.True(!manager.IsNavigating && manager.ReplanCount == 0,
$"replanCount={manager.ReplanCount}\ninfo={string.Join('\n', infoLogs)}\ndebug={string.Join('\n', debugLogs)}");
}
[Fact]
public void SprintJumpTemplate_LandingRecoveryIntoTurn_CompletesInsideLandingBlock()
{

View file

@ -86,6 +86,49 @@ public sealed class PathPlanningContractTests
Assert.Contains("manager-accepted-ascend-chain", error.Message);
}
[Fact]
public void LoadFromJson_RejectsTimingBudget_WhenTotalsDoNotMatchSegments()
{
const string plannerJson = """
[
{
"scenarioId": "totals-mismatch",
"expectedStatus": "Success",
"segments": [
{
"moveType": "Traverse",
"startBlock": { "x": 0, "y": 80, "z": 0 },
"endBlock": { "x": 1, "y": 80, "z": 0 }
},
{
"moveType": "Ascend",
"startBlock": { "x": 1, "y": 80, "z": 0 },
"endBlock": { "x": 2, "y": 81, "z": 0 }
}
]
}
]
""";
const string timingJson = """
[
{
"scenarioId": "totals-mismatch",
"expectedTotalTicks": 1,
"maxTotalTicks": 2,
"segments": [
{ "moveType": "Traverse", "expectedTicks": 2, "maxTicks": 3 },
{ "moveType": "Ascend", "expectedTicks": 3, "maxTicks": 4 }
]
}
]
""";
InvalidDataException error = Assert.Throws<InvalidDataException>(
() => PathingContractStore.LoadFromJson(plannerJson, timingJson));
Assert.Contains("totals-mismatch", error.Message);
Assert.Contains("ExpectedTotalTicks mismatch", error.Message);
}
[Fact]
public void LoadFromJson_Rejects_WhenPlannerAndTimingScenarioSetsMismatch()
{
@ -214,25 +257,26 @@ public sealed class PathPlanningContractTests
}
[Theory]
[InlineData("manager-accepted-ascend-chain")]
[InlineData("same-move-ascend-staircase")]
[InlineData("same-move-descend-staircase")]
[InlineData("rejected-3x1-invalid-goal")]
public void Scenario_PlannerMatchesContract(string scenarioId)
{
PathingExecutionScenario scenario = PathingExecutionScenarioCatalog.Get(scenarioId);
PathResult planResult = PathingScenarioRunner.PlanOnly(scenario);
PathingPlannerContract contract = PathingContractStore.LoadFromRepositoryRoot().GetPlanner(scenarioId);
PathingContractAssert.PlannerMatches(contract, PathSegmentBuilder.FromPath(planResult.Path), planResult);
}
[Theory]
[InlineData("repeated-cardinal-parkour-chain")]
[InlineData("repeated-diagonal-parkour-chain")]
[InlineData("obstructed-parkour-l-turns")]
[InlineData("vertical-jump-mix")]
[InlineData("diagonal-vertical-mix")]
public void JumpCombo_PlannerMatchesContract(string scenarioId)
[InlineData("turn-density-alternating-traverse-diagonal-chain")]
[InlineData("mixed-traverse-ascend-parkour-descend")]
[InlineData("same-move-aligned-parkour-chain")]
[InlineData("mixed-diagonal-ascend-traverse-descend")]
[InlineData("speed-carry-repeated-traverse-ascend")]
[InlineData("speed-carry-repeated-traverse-descend")]
[InlineData("speed-carry-repeated-traverse-parkour")]
[InlineData("same-move-diagonal-chain")]
[InlineData("same-move-straight-traverse-chain")]
[InlineData("mixed-traverse-turn-parkour-turn-traverse")]
public void Scenario_PlannerMatchesContract(string scenarioId)
{
PathingExecutionScenario scenario = PathingExecutionScenarioCatalog.Get(scenarioId);
PathResult planResult = PathingScenarioRunner.PlanOnly(scenario);

View file

@ -62,9 +62,28 @@ internal static class PathingContractAssert
sb.AppendLine($"seg[{i}] move={actual.MoveType} actual={actual.ElapsedTicks} expected={expected.ExpectedTicks} max={expected.MaxTicks}");
}
if (result.InfoLogs.Count > 0)
{
sb.AppendLine("info tail:");
AppendTail(sb, result.InfoLogs, maxLines: 8);
}
if (result.DebugLogs.Count > 0)
{
sb.AppendLine("debug tail:");
AppendTail(sb, result.DebugLogs, maxLines: 12);
}
return sb.ToString();
}
private static void AppendTail(StringBuilder sb, IReadOnlyList<string> lines, int maxLines)
{
int start = Math.Max(0, lines.Count - maxLines);
for (int i = start; i < lines.Count; i++)
sb.AppendLine(lines[i]);
}
private static PathingBlock ToBlock(Location location) =>
new((int)Math.Floor(location.X), (int)Math.Floor(location.Y), (int)Math.Floor(location.Z));
}

View file

@ -42,6 +42,26 @@ public sealed class MoveParkourTests
Assert.Equal(2, result.DestX);
}
[Fact]
public void Accepts4x1JumpWithoutRearSupport_WhenTakeoffBlockProvidesRunway()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: -2, max: 6);
FlatWorldTestBuilder.ClearBox(world, -2, FloorY, -1, 6, FloorY + 4, 1);
FlatWorldTestBuilder.SetSolid(world, 0, FloorY, 0);
FlatWorldTestBuilder.SetSolid(world, 2, FloorY, 0);
FlatWorldTestBuilder.SetSolid(world, 4, FloorY, 0);
var ctx = BuildContext(world);
var move = new MoveParkour(4, 0);
var result = default(MoveResult);
move.Calculate(ctx, 0, FloorY + 1, 0, ref result);
Assert.False(result.IsImpossible);
Assert.Equal(4, result.DestX);
Assert.Equal(0, result.DestZ);
}
[Fact]
public void Rejects2x1WhenAdjacentBlockIsStillWalkable()
{

View file

@ -215,19 +215,6 @@
"y": 80,
"z": 580
},
"endBlock": {
"x": 582,
"y": 80,
"z": 580
}
},
{
"moveType": "Parkour",
"startBlock": {
"x": 582,
"y": 80,
"z": 580
},
"endBlock": {
"x": 584,
"y": 80,
@ -241,19 +228,6 @@
"y": 80,
"z": 580
},
"endBlock": {
"x": 586,
"y": 80,
"z": 580
}
},
{
"moveType": "Parkour",
"startBlock": {
"x": 586,
"y": 80,
"z": 580
},
"endBlock": {
"x": 588,
"y": 80,
@ -370,25 +344,12 @@
}
},
{
"moveType": "Descend",
"moveType": "Parkour",
"startBlock": {
"x": 642,
"y": 81,
"z": 620
},
"endBlock": {
"x": 644,
"y": 80,
"z": 620
}
},
{
"moveType": "Parkour",
"startBlock": {
"x": 644,
"y": 80,
"z": 620
},
"endBlock": {
"x": 646,
"y": 81,
@ -634,19 +595,6 @@
"y": 80,
"z": 380
},
"endBlock": {
"x": 382,
"y": 80,
"z": 380
}
},
{
"moveType": "Parkour",
"startBlock": {
"x": 382,
"y": 80,
"z": 380
},
"endBlock": {
"x": 384,
"y": 80,
@ -660,19 +608,6 @@
"y": 80,
"z": 380
},
"endBlock": {
"x": 386,
"y": 80,
"z": 380
}
},
{
"moveType": "Parkour",
"startBlock": {
"x": 386,
"y": 80,
"z": 380
},
"endBlock": {
"x": 388,
"y": 80,

View file

@ -38,55 +38,55 @@
},
{
"scenarioId": "same-move-ascend-staircase",
"expectedTotalTicks": 56,
"maxTotalTicks": 68,
"expectedTotalTicks": 60,
"maxTotalTicks": 74,
"segments": [
{
"moveType": "Ascend",
"expectedTicks": 11,
"maxTicks": 14
},
{
"moveType": "Ascend",
"expectedTicks": 11,
"maxTicks": 14
},
{
"moveType": "Ascend",
"expectedTicks": 11,
"maxTicks": 14
},
{
"moveType": "Ascend",
"expectedTicks": 11,
"maxTicks": 14
"expectedTicks": 10,
"maxTicks": 12
},
{
"moveType": "Ascend",
"expectedTicks": 12,
"maxTicks": 15
},
{
"moveType": "Ascend",
"expectedTicks": 12,
"maxTicks": 15
},
{
"moveType": "Ascend",
"expectedTicks": 12,
"maxTicks": 15
},
{
"moveType": "Ascend",
"expectedTicks": 14,
"maxTicks": 17
}
]
},
{
"scenarioId": "same-move-descend-staircase",
"expectedTotalTicks": 61,
"maxTotalTicks": 74,
"expectedTotalTicks": 57,
"maxTotalTicks": 70,
"segments": [
{
"moveType": "Descend",
"expectedTicks": 24,
"maxTicks": 29
"expectedTicks": 23,
"maxTicks": 28
},
{
"moveType": "Descend",
"expectedTicks": 25,
"maxTicks": 30
"expectedTicks": 23,
"maxTicks": 28
},
{
"moveType": "Descend",
"expectedTicks": 12,
"maxTicks": 15
"expectedTicks": 11,
"maxTicks": 14
}
]
},
@ -98,79 +98,47 @@
},
{
"scenarioId": "repeated-cardinal-parkour-chain",
"expectedTotalTicks": 0,
"maxTotalTicks": 2,
"expectedTotalTicks": 37,
"maxTotalTicks": 45,
"segments": [
{
"moveType": "Parkour",
"expectedTicks": 61,
"maxTicks": 74
"expectedTicks": 18,
"maxTicks": 22
},
{
"moveType": "Parkour",
"expectedTicks": 61,
"maxTicks": 74
},
{
"moveType": "Parkour",
"expectedTicks": 61,
"maxTicks": 74
},
{
"moveType": "Parkour",
"expectedTicks": 27,
"maxTicks": 33
"expectedTicks": 19,
"maxTicks": 23
}
]
},
{
"scenarioId": "repeated-diagonal-parkour-chain",
"expectedTotalTicks": 20,
"maxTotalTicks": 24,
"expectedTotalTicks": 67,
"maxTotalTicks": 82,
"segments": [
{
"moveType": "Parkour",
"expectedTicks": 61,
"maxTicks": 74
"expectedTicks": 14,
"maxTicks": 17
},
{
"moveType": "Parkour",
"expectedTicks": 61,
"maxTicks": 74
"expectedTicks": 36,
"maxTicks": 44
},
{
"moveType": "Parkour",
"expectedTicks": 20,
"maxTicks": 24
"expectedTicks": 17,
"maxTicks": 21
}
]
},
{
"scenarioId": "obstructed-parkour-l-turns",
"expectedTotalTicks": 0,
"maxTotalTicks": 2,
"segments": [
{
"moveType": "Parkour",
"expectedTicks": 27,
"maxTicks": 33
},
{
"moveType": "Parkour",
"expectedTicks": 27,
"maxTicks": 33
},
{
"moveType": "Parkour",
"expectedTicks": 27,
"maxTicks": 33
}
]
},
{
"scenarioId": "vertical-jump-mix",
"expectedTotalTicks": 33,
"maxTotalTicks": 40,
"expectedTotalTicks": 50,
"maxTotalTicks": 62,
"segments": [
{
"moveType": "Parkour",
@ -178,14 +146,53 @@
"maxTicks": 16
},
{
"moveType": "Descend",
"expectedTicks": 201,
"maxTicks": 242
"moveType": "Parkour",
"expectedTicks": 21,
"maxTicks": 26
},
{
"moveType": "Parkour",
"expectedTicks": 19,
"maxTicks": 23
"expectedTicks": 16,
"maxTicks": 20
}
]
},
{
"scenarioId": "vertical-jump-mix",
"expectedTotalTicks": 41,
"maxTotalTicks": 50,
"segments": [
{
"moveType": "Parkour",
"expectedTicks": 10,
"maxTicks": 12
},
{
"moveType": "Parkour",
"expectedTicks": 18,
"maxTicks": 22
},
{
"moveType": "Descend",
"expectedTicks": 13,
"maxTicks": 16
}
]
},
{
"scenarioId": "diagonal-vertical-mix",
"expectedTotalTicks": 38,
"maxTotalTicks": 46,
"segments": [
{
"moveType": "Ascend",
"expectedTicks": 10,
"maxTicks": 12
},
{
"moveType": "Parkour",
"expectedTicks": 14,
"maxTicks": 17
},
{
"moveType": "Descend",
@ -194,32 +201,10 @@
}
]
},
{
"scenarioId": "diagonal-vertical-mix",
"expectedTotalTicks": 31,
"maxTotalTicks": 38,
"segments": [
{
"moveType": "Ascend",
"expectedTicks": 81,
"maxTicks": 98
},
{
"moveType": "Parkour",
"expectedTicks": 16,
"maxTicks": 20
},
{
"moveType": "Descend",
"expectedTicks": 15,
"maxTicks": 18
}
]
},
{
"scenarioId": "turn-density-alternating-traverse-diagonal-chain",
"expectedTotalTicks": 47,
"maxTotalTicks": 57,
"maxTotalTicks": 59,
"segments": [
{
"moveType": "Diagonal",
@ -255,72 +240,62 @@
},
{
"scenarioId": "mixed-traverse-ascend-parkour-descend",
"expectedTotalTicks": 40,
"maxTotalTicks": 48,
"expectedTotalTicks": 70,
"maxTotalTicks": 88,
"segments": [
{
"moveType": "Traverse",
"expectedTicks": 6,
"maxTicks": 8
"expectedTicks": 5,
"maxTicks": 7
},
{
"moveType": "Ascend",
"expectedTicks": 11,
"maxTicks": 14
"expectedTicks": 12,
"maxTicks": 15
},
{
"moveType": "Ascend",
"expectedTicks": 11,
"maxTicks": 14
"expectedTicks": 13,
"maxTicks": 16
},
{
"moveType": "Traverse",
"expectedTicks": 81,
"maxTicks": 98
"expectedTicks": 5,
"maxTicks": 7
},
{
"moveType": "Parkour",
"expectedTicks": 14,
"maxTicks": 17
},
{
"moveType": "Descend",
"expectedTicks": 21,
"maxTicks": 26
}
]
},
{
"scenarioId": "same-move-aligned-parkour-chain",
"expectedTotalTicks": 37,
"maxTotalTicks": 45,
"segments": [
{
"moveType": "Parkour",
"expectedTicks": 18,
"maxTicks": 22
},
{
"moveType": "Descend",
"expectedTicks": 22,
"maxTicks": 27
}
]
},
{
"scenarioId": "same-move-aligned-parkour-chain",
"expectedTotalTicks": 0,
"maxTotalTicks": 2,
"segments": [
{
"moveType": "Parkour",
"expectedTicks": 61,
"maxTicks": 74
},
{
"moveType": "Parkour",
"expectedTicks": 61,
"maxTicks": 74
},
{
"moveType": "Parkour",
"expectedTicks": 61,
"maxTicks": 74
},
{
"moveType": "Parkour",
"expectedTicks": 27,
"maxTicks": 33
"expectedTicks": 19,
"maxTicks": 23
}
]
},
{
"scenarioId": "mixed-diagonal-ascend-traverse-descend",
"expectedTotalTicks": 96,
"maxTotalTicks": 116,
"expectedTotalTicks": 76,
"maxTotalTicks": 95,
"segments": [
{
"moveType": "Diagonal",
@ -334,13 +309,13 @@
},
{
"moveType": "Ascend",
"expectedTicks": 32,
"maxTicks": 39
"expectedTicks": 10,
"maxTicks": 12
},
{
"moveType": "Ascend",
"expectedTicks": 11,
"maxTicks": 14
"expectedTicks": 13,
"maxTicks": 16
},
{
"moveType": "Traverse",
@ -361,19 +336,9 @@
},
{
"scenarioId": "speed-carry-repeated-traverse-ascend",
"expectedTotalTicks": 66,
"maxTotalTicks": 80,
"expectedTotalTicks": 70,
"maxTotalTicks": 90,
"segments": [
{
"moveType": "Traverse",
"expectedTicks": 6,
"maxTicks": 8
},
{
"moveType": "Ascend",
"expectedTicks": 11,
"maxTicks": 14
},
{
"moveType": "Traverse",
"expectedTicks": 5,
@ -381,18 +346,8 @@
},
{
"moveType": "Ascend",
"expectedTicks": 11,
"maxTicks": 14
},
{
"moveType": "Traverse",
"expectedTicks": 5,
"maxTicks": 7
},
{
"moveType": "Ascend",
"expectedTicks": 11,
"maxTicks": 14
"expectedTicks": 13,
"maxTicks": 16
},
{
"moveType": "Traverse",
@ -403,23 +358,43 @@
"moveType": "Ascend",
"expectedTicks": 12,
"maxTicks": 15
},
{
"moveType": "Traverse",
"expectedTicks": 5,
"maxTicks": 7
},
{
"moveType": "Ascend",
"expectedTicks": 12,
"maxTicks": 15
},
{
"moveType": "Traverse",
"expectedTicks": 5,
"maxTicks": 7
},
{
"moveType": "Ascend",
"expectedTicks": 13,
"maxTicks": 16
}
]
},
{
"scenarioId": "speed-carry-repeated-traverse-descend",
"expectedTotalTicks": 41,
"maxTotalTicks": 50,
"expectedTotalTicks": 45,
"maxTotalTicks": 57,
"segments": [
{
"moveType": "Traverse",
"expectedTicks": 81,
"maxTicks": 98
"expectedTicks": 5,
"maxTicks": 7
},
{
"moveType": "Parkour",
"expectedTicks": 22,
"maxTicks": 27
"expectedTicks": 21,
"maxTicks": 26
},
{
"moveType": "Traverse",
@ -435,45 +410,45 @@
},
{
"scenarioId": "speed-carry-repeated-traverse-parkour",
"expectedTotalTicks": 0,
"maxTotalTicks": 2,
"expectedTotalTicks": 58,
"maxTotalTicks": 73,
"segments": [
{
"moveType": "Traverse",
"expectedTicks": 81,
"maxTicks": 98
"expectedTicks": 5,
"maxTicks": 7
},
{
"moveType": "Parkour",
"expectedTicks": 18,
"maxTicks": 22
"expectedTicks": 14,
"maxTicks": 17
},
{
"moveType": "Traverse",
"expectedTicks": 81,
"maxTicks": 98
"expectedTicks": 5,
"maxTicks": 7
},
{
"moveType": "Parkour",
"expectedTicks": 14,
"maxTicks": 17
},
{
"moveType": "Traverse",
"expectedTicks": 5,
"maxTicks": 7
},
{
"moveType": "Parkour",
"expectedTicks": 15,
"maxTicks": 18
},
{
"moveType": "Traverse",
"expectedTicks": 81,
"maxTicks": 98
},
{
"moveType": "Parkour",
"expectedTicks": 29,
"maxTicks": 35
}
]
},
{
"scenarioId": "same-move-diagonal-chain",
"expectedTotalTicks": 55,
"maxTotalTicks": 66,
"maxTotalTicks": 69,
"segments": [
{
"moveType": "Diagonal",
@ -515,7 +490,7 @@
{
"scenarioId": "same-move-straight-traverse-chain",
"expectedTotalTicks": 70,
"maxTotalTicks": 84,
"maxTotalTicks": 94,
"segments": [
{
"moveType": "Traverse",
@ -581,8 +556,8 @@
},
{
"scenarioId": "mixed-traverse-turn-parkour-turn-traverse",
"expectedTotalTicks": 46,
"maxTotalTicks": 56,
"expectedTotalTicks": 60,
"maxTotalTicks": 75,
"segments": [
{
"moveType": "Traverse",
@ -591,8 +566,8 @@
},
{
"moveType": "Diagonal",
"expectedTicks": 81,
"maxTicks": 98
"expectedTicks": 6,
"maxTicks": 8
},
{
"moveType": "Parkour",
@ -601,8 +576,8 @@
},
{
"moveType": "Traverse",
"expectedTicks": 7,
"maxTicks": 9
"expectedTicks": 8,
"maxTicks": 10
},
{
"moveType": "Diagonal",
@ -616,8 +591,8 @@
},
{
"moveType": "Traverse",
"expectedTicks": 8,
"maxTicks": 10
"expectedTicks": 7,
"maxTicks": 9
}
]
}

View file

@ -15,7 +15,10 @@ internal static class ParkourFeasibility
int yDelta)
{
double horiz = Math.Sqrt(xOffset * xOffset + zOffset * zOffset);
double threshold = yDelta > 0 ? 2.5 : 3.5;
if (yDelta <= 0)
return true;
double threshold = 2.5;
if (horiz < threshold)
return true;

View file

@ -317,6 +317,8 @@ For rejection scenarios, the requirement is stricter:
Residual speed carried from one movement to the next inside a route is expected and must not be normalized away just to satisfy the harness. The route is only considered reliable if that natural speed carry still produces `0 replan`.
Independent live-route cases must reset position, yaw, and pitch to the scenario start state before each run. Cross-case orientation residue is harness noise, not valid pathing difficulty.
## Baritone Reference Notes For Zero-Replan Work
MCC can borrow specific ideas from the local Baritone reference under `ThirdpartyReference/baritone/`, but not its looser success semantics.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,877 @@
# Pathing Execution Regression Fixes Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remove the current execution-layer regressions exposed by the contract/timing harness so deterministic jump-combo and long-route scenarios complete with `0` replans and within their existing budgets.
**Architecture:** Treat the failures as three runtime bugs, not as harness problems. First tighten parkour landing recovery so chained jumps hand off with the right speed instead of stalling or replan-looping. Second make transition braking and lookahead score the next segment entry contract, so mixed turn/ascend/descend routes stop choosing the wrong carry-or-brake profile. Third harden chained ascends for live-runtime carry states so staircases stop burning extra ticks after each landing. Keep the existing JSON contracts, scenario catalog, and shell harnesses unchanged except for verification.
**Tech Stack:** C# 14 / .NET 10, xUnit, MCC pathing execution templates, `PlayerPhysics`, existing `MinecraftClient.Tests` scenario runner and timing contracts, local `1.21.11-Vanilla` live harness via `tools/mcc-env.sh`.
---
## Scope Check
This plan only covers runtime execution fixes in the existing pathing stack.
Out of scope:
- planner-contract schema changes
- theory-matrix generation changes
- telemetry/report format changes
- new live harness features
- broad planner heuristics refactors
## Current Failure Inventory
Focused xUnit evidence from:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~Pathing.Execution" -v minimal
```
Current failing families:
- repeated parkour chains do not complete cleanly
- `repeated-cardinal-parkour-chain`: navigation did not complete, `replans=4`
- `repeated-diagonal-parkour-chain`: expected `0` replans, saw `2`
- `obstructed-parkour-l-turns`: navigation did not complete, `replans=1`
- `same-move-aligned-parkour-chain`: navigation did not complete, `replans=4`
- mixed vertical and mixed long routes over-brake or replan unexpectedly
- `vertical-jump-mix`: expected `0` replans, saw `1`
- `diagonal-vertical-mix`: expected `0` replans, saw `1`
- `mixed-traverse-turn-parkour-turn-traverse`: expected `0` replans, saw `1`
- `mixed-traverse-ascend-parkour-descend`: expected `0` replans, saw `1`
- `speed-carry-repeated-traverse-descend`: expected `0` replans, saw `1`
- `speed-carry-repeated-traverse-parkour`: navigation did not complete, `replans=4`
Live harness evidence:
```bash
source tools/mcc-env.sh && bash tools/test-pathing-jump-combos.sh 1.21.11-Vanilla
source tools/mcc-env.sh && bash tools/test-pathing-long-routes.sh 1.21.11-Vanilla
```
Current live failures:
- `same-move-ascend-staircase`: `actual=145 max=68`, first four ascend segments each over by roughly `+22` to `+23` ticks
- `vertical-jump-mix`: `actual=54 max=40`
- repeated parkour chains fail with segment failure followed by replan loops
## Problem Map
1. `MinecraftClient/Pathing/Execution/Templates/SprintJumpTemplate.cs`
- landing recovery is still biased toward “settle fully” behavior
- `pastTarget` release is too blunt for repeated parkour and mixed jump chains
- completion rules do not preserve enough entry speed for immediate follow-up jumps
2. `MinecraftClient/Pathing/Execution/TransitionBrakingPlanner.cs`
- planning still reasons mostly about the current segment
- special-cases landing-recovery turns, but not the broader mixed-route handoff problem
3. `MinecraftClient/Pathing/Execution/TransitionLookaheadEvaluator.cs`
- air and ground scoring ignore too much next-segment intent
- current profiles cannot distinguish “slow down for stable turn entry” from “keep enough speed for the next descend or jump”
4. `MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs`
- chained ascends do not explicitly separate takeoff, airborne, and landing handoff
- live staircase traces show repeated post-landing delay before the next step starts
5. `MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs`
- grounded completion is strong for final stops, but too conservative for continue-straight ascend handoff
## File Structure
### Production files
- Modify: `MinecraftClient/Pathing/Execution/Templates/SprintJumpTemplate.cs`
- parkour landing recovery completion and in-air release rules
- Modify: `MinecraftClient/Pathing/Execution/TransitionBrakingPlanner.cs`
- next-segment-aware braking decisions
- Modify: `MinecraftClient/Pathing/Execution/TransitionLookaheadEvaluator.cs`
- ground and air profile scoring that considers the next segment contract
- Modify: `MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs`
- explicit ascend phase handling and faster landing handoff
- Modify: `MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs`
- shared completion rules for continue-straight ascend chaining
### Test files
- Modify: `MinecraftClient.Tests/Pathing/Execution/PathTimingContractTests.cs`
- named regression entry points for representative failing scenarios
- Modify: `MinecraftClient.Tests/Pathing/Execution/SprintJumpTemplateScenarioTests.cs`
- deterministic chained-jump handoff regression
- Modify: `MinecraftClient.Tests/Pathing/Execution/TransitionLookaheadEvaluatorTests.cs`
- ground and air next-segment profile regressions
- Modify: `MinecraftClient.Tests/Pathing/Execution/TransitionBrakingPlannerTests.cs`
- planner decisions for mixed handoff states
- Modify: `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`
- chained-ascend convergence regression
### Verification only
- Reuse: `tools/test-pathing-jump-combos.sh`
- Reuse: `tools/test-pathing-long-routes.sh`
---
### Task 1: Stabilize Repeated Parkour Landing Recovery
**Files:**
- Modify: `MinecraftClient.Tests/Pathing/Execution/PathTimingContractTests.cs`
- Modify: `MinecraftClient.Tests/Pathing/Execution/SprintJumpTemplateScenarioTests.cs`
- Modify: `MinecraftClient/Pathing/Execution/Templates/SprintJumpTemplate.cs`
- [ ] **Step 1: Write failing parkour-focused regression tests**
Update `MinecraftClient.Tests/Pathing/Execution/PathTimingContractTests.cs`:
```csharp
using MinecraftClient.Tests.Pathing.Execution.Contracts;
using Xunit;
namespace MinecraftClient.Tests.Pathing.Execution;
public sealed class PathTimingContractTests
{
[Fact]
public void RepeatedCardinalParkourChain_ExecutionStaysWithinBudget() =>
AssertScenarioWithinBudget("repeated-cardinal-parkour-chain");
[Fact]
public void RepeatedDiagonalParkourChain_ExecutionStaysWithinBudget() =>
AssertScenarioWithinBudget("repeated-diagonal-parkour-chain");
private static void AssertScenarioWithinBudget(string scenarioId)
{
PathingExecutionScenario scenario = PathingExecutionScenarioCatalog.Get(scenarioId);
PathingTimingBudget budget = PathingContractStore.LoadFromRepositoryRoot().GetTiming(scenarioId);
PathingScenarioResult result = PathingScenarioRunner.RunAccepted(scenario);
PathingContractAssert.TimingMatches(budget, result);
}
}
```
Update `MinecraftClient.Tests/Pathing/Execution/SprintJumpTemplateScenarioTests.cs`:
```csharp
[Fact]
public void SprintJumpTemplate_LandingRecovery_LeavesEnoughSpeedForNextParkour()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 578, max: 586);
FlatWorldTestBuilder.ClearBox(world, 578, 79, 578, 586, 90, 582);
FlatWorldTestBuilder.SetSolid(world, 580, 79, 580);
FlatWorldTestBuilder.SetSolid(world, 582, 79, 580);
FlatWorldTestBuilder.SetSolid(world, 584, 79, 580);
var current = new PathSegment
{
Start = new Location(580.5, 80, 580.5),
End = new Location(582.5, 80, 580.5),
MoveType = MoveType.Parkour,
ExitTransition = PathTransitionType.LandingRecovery,
ExitHints = new PathTransitionHints(1, 0, 0.12, 0.20, false, true, true, true, 12),
PreserveSprint = true
};
var next = new PathSegment
{
Start = current.End,
End = new Location(584.5, 80, 580.5),
MoveType = MoveType.Parkour,
ExitTransition = PathTransitionType.LandingRecovery,
ExitHints = new PathTransitionHints(1, 0, 0.12, 0.20, false, true, true, true, 12),
PreserveSprint = true
};
var template = new SprintJumpTemplate(current, next);
var physics = TemplateSimulationRunner.CreateGroundedPhysics(current.Start, yaw: 270f);
TemplateState state = TemplateSimulationRunner.Run(template, physics, world, maxTicks: 140, out Location finalPos);
Assert.Equal(TemplateState.Complete, state);
Assert.True(TemplateFootingHelper.IsCenterInsideTargetBlock(finalPos, current.End), $"finalPos={finalPos} vel={physics.DeltaMovement}");
Assert.InRange(physics.DeltaMovement.X, 0.12, 0.30);
}
```
- [ ] **Step 2: Run the focused tests and verify they fail**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~Pathing.Execution.PathTimingContractTests.RepeatedCardinalParkourChain_ExecutionStaysWithinBudget|FullyQualifiedName~Pathing.Execution.PathTimingContractTests.RepeatedDiagonalParkourChain_ExecutionStaysWithinBudget|FullyQualifiedName~Pathing.Execution.SprintJumpTemplateScenarioTests.SprintJumpTemplate_LandingRecovery_LeavesEnoughSpeedForNextParkour" -v minimal
```
Expected: FAIL with either `navigation did not complete`, nonzero replans, or residual speed below the handoff minimum.
- [ ] **Step 3: Make `SprintJumpTemplate` preserve jump-ready handoff instead of over-settling**
Update `MinecraftClient/Pathing/Execution/Templates/SprintJumpTemplate.cs`:
```csharp
case Phase.Airborne:
{
if (!physics.OnGround)
_leftGround = true;
bool releaseInAir = ShouldReleaseInAir(pos, physics, world);
bool hardRelease = releaseInAir;
if (_segment.ExitTransition != PathTransitionType.LandingRecovery && IsPastTarget(pos))
hardRelease = true;
if (hardRelease)
{
input.Forward = false;
input.Sprint = false;
}
else
{
input.Forward = true;
input.Sprint = true;
}
if (_leftGround && physics.OnGround)
{
_phase = Phase.Landing;
goto case Phase.Landing;
}
break;
}
case Phase.Landing:
if (ShouldCompleteLandingRecoveryHandoff(pos, physics))
return TemplateState.Complete;
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(_segment, _nextSegment, pos, physics, world);
TemplateHelper.ApplyDecision(input, decision);
if (decision.HoldBack)
TemplateHelper.FaceSegmentHeading(physics, _segment);
else if (TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment))
TemplateHelper.FaceExitHeading(physics, _segment);
if (_segment.ExitTransition == PathTransitionType.ContinueStraight
&& horizDistSq < 2.25
&& Math.Abs(dy) < 1.0)
{
return TemplateState.Complete;
}
break;
private bool ShouldCompleteLandingRecoveryHandoff(Location pos, PlayerPhysics physics)
{
if (_segment.ExitTransition != PathTransitionType.LandingRecovery || _nextSegment is null || !physics.OnGround)
return false;
double exitSpeed = TemplateHelper.ProjectHorizontalSpeedAlongHint(physics, _segment);
if (_nextSegment.ExitHints.RequireJumpReady)
{
return TemplateFootingHelper.IsCenterInsideTargetBlock(pos, ExpectedEnd)
&& !TemplateFootingHelper.WillCenterLeaveTargetBlockNextTick(pos, physics, ExpectedEnd)
&& exitSpeed >= _nextSegment.ExitHints.MinExitSpeed;
}
return TemplateFootingHelper.IsCenterInsideSupportStrip(pos, ExpectedEnd, _nextSegment.End)
&& !TemplateFootingHelper.WillCenterLeaveSupportStripNextTick(pos, physics, ExpectedEnd, _nextSegment.End)
&& exitSpeed <= _segment.ExitHints.MaxExitSpeed;
}
```
- [ ] **Step 4: Re-run the parkour-focused tests and then the whole jump-combo contract group**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~Pathing.Execution.PathTimingContractTests.RepeatedCardinalParkourChain_ExecutionStaysWithinBudget|FullyQualifiedName~Pathing.Execution.PathTimingContractTests.RepeatedDiagonalParkourChain_ExecutionStaysWithinBudget|FullyQualifiedName~Pathing.Execution.PathTimingContractTests.JumpCombo_ExecutionStaysWithinBudget|FullyQualifiedName~Pathing.Execution.SprintJumpTemplateScenarioTests" -v minimal
```
Expected: PASS for the two named regressions and no new failures in the broader jump-template coverage.
- [ ] **Step 5: Commit the parkour landing recovery fix**
```bash
git add MinecraftClient.Tests/Pathing/Execution/PathTimingContractTests.cs \
MinecraftClient.Tests/Pathing/Execution/SprintJumpTemplateScenarioTests.cs \
MinecraftClient/Pathing/Execution/Templates/SprintJumpTemplate.cs
git commit -m "fix: preserve jump-ready speed through parkour landing recovery"
```
### Task 2: Make Braking And Lookahead Respect The Next Segment Contract
**Files:**
- Modify: `MinecraftClient.Tests/Pathing/Execution/PathTimingContractTests.cs`
- Modify: `MinecraftClient.Tests/Pathing/Execution/TransitionLookaheadEvaluatorTests.cs`
- Modify: `MinecraftClient.Tests/Pathing/Execution/TransitionBrakingPlannerTests.cs`
- Modify: `MinecraftClient/Pathing/Execution/TransitionLookaheadEvaluator.cs`
- Modify: `MinecraftClient/Pathing/Execution/TransitionBrakingPlanner.cs`
- [ ] **Step 1: Add failing mixed-route regression tests**
Update `MinecraftClient.Tests/Pathing/Execution/PathTimingContractTests.cs`:
```csharp
[Fact]
public void MixedTraverseAscendParkourDescend_ExecutionStaysWithinBudget() =>
AssertScenarioWithinBudget("mixed-traverse-ascend-parkour-descend");
[Fact]
public void MixedTraverseTurnParkourTurnTraverse_ExecutionStaysWithinBudget() =>
AssertScenarioWithinBudget("mixed-traverse-turn-parkour-turn-traverse");
[Fact]
public void SpeedCarryRepeatedTraverseDescend_ExecutionStaysWithinBudget() =>
AssertScenarioWithinBudget("speed-carry-repeated-traverse-descend");
```
Update `MinecraftClient.Tests/Pathing/Execution/TransitionLookaheadEvaluatorTests.cs`:
```csharp
[Fact]
public void ChooseGroundProfile_PicksBrake_WhenLandingRecoveryTurnWouldOvershootSupportStrip()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 108, max: 126);
FlatWorldTestBuilder.ClearBox(world, 118, 79, 108, 126, 90, 112);
FlatWorldTestBuilder.SetSolid(world, 120, 79, 110);
FlatWorldTestBuilder.SetSolid(world, 122, 79, 110);
FlatWorldTestBuilder.SetSolid(world, 122, 79, 111);
var current = new PathSegment
{
Start = new Location(120.5, 80, 110.5),
End = new Location(122.5, 80, 110.5),
MoveType = MoveType.Parkour,
ExitTransition = PathTransitionType.LandingRecovery,
ExitHints = new PathTransitionHints(0, 1, 0.0, 0.035, true, true, false, true, 12)
};
var next = new PathSegment
{
Start = current.End,
End = new Location(122.5, 80, 111.5),
MoveType = MoveType.Traverse,
ExitTransition = PathTransitionType.PrepareJump,
ExitHints = new PathTransitionHints(0, 1, 0.12, double.PositiveInfinity, false, true, true, false, 10),
PreserveSprint = true
};
var physics = new PlayerPhysics
{
Position = new Vec3d(122.58, 80.0, 110.68),
DeltaMovement = new Vec3d(0.118, 0.0, 0.018),
OnGround = true,
MovementSpeed = 0.1f,
Yaw = 270f
};
TransitionInputProfile profile = TransitionLookaheadEvaluator.ChooseGroundProfile(
current,
next,
new Location(122.58, 80.0, 110.68),
physics,
world);
Assert.Equal(TransitionInputProfile.Brake, profile);
}
```
Update `MinecraftClient.Tests/Pathing/Execution/TransitionBrakingPlannerTests.cs`:
```csharp
[Fact]
public void Plan_Carries_ForLandingRecovery_WhenNextDescendStillNeedsRunway()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 438, max: 448);
FlatWorldTestBuilder.ClearBox(world, 438, 79, 438, 448, 84, 442);
FlatWorldTestBuilder.SetSolid(world, 440, 79, 440);
FlatWorldTestBuilder.SetSolid(world, 441, 79, 440);
FlatWorldTestBuilder.SetSolid(world, 442, 79, 440);
FlatWorldTestBuilder.SetSolid(world, 443, 80, 440);
FlatWorldTestBuilder.SetSolid(world, 444, 79, 440);
var current = new PathSegment
{
Start = new Location(441.5, 81, 440.5),
End = new Location(443.5, 81, 440.5),
MoveType = MoveType.Parkour,
ExitTransition = PathTransitionType.LandingRecovery,
ExitHints = new PathTransitionHints(1, 0, 0.0, 0.035, true, true, false, true, 12)
};
var next = new PathSegment
{
Start = current.End,
End = new Location(444.5, 80, 440.5),
MoveType = MoveType.Descend,
ExitTransition = PathTransitionType.FinalStop,
ExitHints = new PathTransitionHints(1, 0, 0.0, 0.02, true, true, false, false, 12)
};
var physics = CreatePhysics(0.086, 0.0, onGround: true);
physics.Position = new Vec3d(443.18, 81.0, 440.5);
TransitionBrakingDecision decision = TransitionBrakingPlanner.Plan(
current,
next,
new Location(443.18, 81.0, 440.5),
physics,
world);
Assert.True(decision.HoldForward);
Assert.False(decision.HoldBack);
}
```
- [ ] **Step 2: Run the mixed-route tests and verify they fail**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~Pathing.Execution.PathTimingContractTests.MixedTraverseAscendParkourDescend_ExecutionStaysWithinBudget|FullyQualifiedName~Pathing.Execution.PathTimingContractTests.MixedTraverseTurnParkourTurnTraverse_ExecutionStaysWithinBudget|FullyQualifiedName~Pathing.Execution.PathTimingContractTests.SpeedCarryRepeatedTraverseDescend_ExecutionStaysWithinBudget|FullyQualifiedName~Pathing.Execution.TransitionLookaheadEvaluatorTests.ChooseGroundProfile_PicksBrake_WhenLandingRecoveryTurnWouldOvershootSupportStrip|FullyQualifiedName~Pathing.Execution.TransitionBrakingPlannerTests.Plan_Carries_ForLandingRecovery_WhenNextDescendStillNeedsRunway" -v minimal
```
Expected: FAIL because current lookahead and planner logic either brake when the next segment needs carry, or carry when the turn entry should already be slowing down.
- [ ] **Step 3: Thread `nextSegment` through lookahead scoring and braking decisions**
Update `MinecraftClient/Pathing/Execution/TransitionLookaheadEvaluator.cs`:
```csharp
public static TransitionInputProfile ChooseGroundProfile(PathSegment current, PathSegment? next, Location pos, PlayerPhysics physics, World world)
{
double remaining = TemplateHelper.RemainingDistanceAlongSegment(pos, current);
double forwardSpeed = Math.Max(0.0,
TemplateHelper.ProjectHorizontalSpeedAlongHeading(physics, current.HeadingX, current.HeadingZ));
bool requiresJumpEntry = current.ExitHints.RequireJumpReady
|| current.ExitTransition == PathTransitionType.PrepareJump;
if (current.ExitTransition == PathTransitionType.ContinueStraight && !requiresJumpEntry)
return TransitionInputProfile.Carry;
if (requiresJumpEntry)
return TransitionInputProfile.Carry;
if (next is not null && current.ExitTransition == PathTransitionType.LandingRecovery)
{
bool headingChange = current.HeadingX != next.HeadingX || current.HeadingZ != next.HeadingZ;
if (headingChange && forwardSpeed > GetTargetMaxExitSpeed(current))
return TransitionInputProfile.Brake;
if (next.ExitHints.RequireJumpReady && forwardSpeed < next.ExitHints.MinExitSpeed)
return TransitionInputProfile.Carry;
}
bool requiresSlowEntry = current.ExitHints.RequireStableFooting
|| current.ExitTransition is PathTransitionType.FinalStop or PathTransitionType.Turn
|| (current.ExitTransition == PathTransitionType.LandingRecovery
&& (current.ExitHints.AllowAirBrake || IsFiniteSpeedCap(current)));
if (!requiresSlowEntry)
return TransitionInputProfile.Carry;
double maxExitSpeed = GetTargetMaxExitSpeed(current);
double hardBrakeDistance = TransitionBrakingPlanner.EstimateGroundStopDistance(
physics, world, current.HeadingX, current.HeadingZ, applyBackBrake: true);
double coastStopDistance = TransitionBrakingPlanner.EstimateGroundStopDistance(
physics, world, current.HeadingX, current.HeadingZ, applyBackBrake: false);
if (remaining < 0.0)
return TransitionInputProfile.Brake;
if (forwardSpeed > maxExitSpeed && remaining <= hardBrakeDistance + 0.10)
return TransitionInputProfile.Brake;
if (forwardSpeed <= maxExitSpeed && remaining > 0.0)
return TransitionInputProfile.Carry;
if (remaining <= coastStopDistance + 0.06)
return TransitionInputProfile.Coast;
return TransitionInputProfile.Carry;
}
public static TransitionInputProfile ChooseAirProfile(PathSegment current, PathSegment? next, Location pos, PlayerPhysics physics, World world)
{
if (!current.ExitHints.AllowAirBrake)
return TransitionInputProfile.AirHoldForward;
TransitionInputProfile[] candidates =
[
TransitionInputProfile.AirHoldForward,
TransitionInputProfile.AirRelease,
TransitionInputProfile.AirBrake
];
return ChooseBest(current, next, pos, physics, world, candidates);
}
private static TransitionInputProfile ChooseBest(PathSegment segment, PathSegment? next, Location pos, PlayerPhysics physics, World world,
TransitionInputProfile[] candidates)
{
TransitionInputProfile best = candidates[0];
double bestScore = double.PositiveInfinity;
foreach (TransitionInputProfile candidate in candidates)
{
double score = Score(segment, next, pos, physics, world, candidate);
if (score < bestScore)
{
best = candidate;
bestScore = score;
}
}
return best;
}
private static double Score(PathSegment segment, PathSegment? next, Location pos, PlayerPhysics physics, World world, TransitionInputProfile candidate)
{
PlayerPhysics sim = TemplateHelper.ClonePhysicsForPlanning(physics);
sim.Position = new Vec3d(pos.X, pos.Y, pos.Z);
var input = new MovementInput();
Location simPos = pos;
for (int tick = 0; tick < segment.ExitHints.HorizonTicks; tick++)
{
if (TemplateHelper.ShouldBiasTowardExitHeading(simPos, segment))
TemplateHelper.FaceExitHeading(sim, segment);
input.Reset();
ApplyCandidateInput(input, candidate, segment);
sim.ApplyInput(input);
sim.Tick(world);
simPos = new Location(sim.Position.X, sim.Position.Y, sim.Position.Z);
}
double score = ScoreNextSegmentEntry(segment, next, simPos, sim);
score += TemplateHelper.HeadingPenaltyDegrees(sim.Yaw, segment);
score += Math.Abs(TemplateHelper.RemainingDistanceAlongSegment(simPos, segment)) * 10.0;
return score;
}
private static double ScoreNextSegmentEntry(PathSegment current, PathSegment? next, Location simPos, PlayerPhysics sim)
{
if (next is null)
return 0.0;
double score = 0.0;
if (current.ExitTransition == PathTransitionType.LandingRecovery
&& (current.HeadingX != next.HeadingX || current.HeadingZ != next.HeadingZ)
&& !TemplateFootingHelper.IsCenterInsideSupportStrip(simPos, current.End, next.End))
{
score += 1200.0;
}
if (next.ExitHints.RequireJumpReady)
{
double nextSpeed = TemplateHelper.ProjectHorizontalSpeedAlongHeading(sim, next.HeadingX, next.HeadingZ);
if (nextSpeed < next.ExitHints.MinExitSpeed)
score += (next.ExitHints.MinExitSpeed - nextSpeed) * 600.0;
}
return score;
}
```
Update `MinecraftClient/Pathing/Execution/TransitionBrakingPlanner.cs`:
```csharp
TransitionInputProfile profile;
if (physics.OnGround)
{
profile = TransitionLookaheadEvaluator.ChooseGroundProfile(current, next, pos, physics, world);
}
else
{
if (!current.ExitHints.AllowAirBrake)
return TransitionBrakingDecision.CarryMomentum(current.PreserveSprint);
profile = TransitionLookaheadEvaluator.ChooseAirProfile(current, next, pos, physics, world);
}
return profile switch
{
TransitionInputProfile.Carry => TransitionBrakingDecision.CarryMomentum(current.PreserveSprint || next?.ExitHints.RequireJumpReady == true),
TransitionInputProfile.Coast => TransitionBrakingDecision.Coast,
TransitionInputProfile.Brake => TransitionBrakingDecision.Brake,
TransitionInputProfile.AirHoldForward => TransitionBrakingDecision.CarryMomentum(current.PreserveSprint || next?.ExitHints.RequireJumpReady == true),
TransitionInputProfile.AirRelease => TransitionBrakingDecision.Coast,
TransitionInputProfile.AirBrake => TransitionBrakingDecision.Brake,
_ => TransitionBrakingDecision.Coast
};
```
- [ ] **Step 4: Re-run focused mixed-route tests and the broader long-route contract group**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~Pathing.Execution.PathTimingContractTests.MixedTraverseAscendParkourDescend_ExecutionStaysWithinBudget|FullyQualifiedName~Pathing.Execution.PathTimingContractTests.MixedTraverseTurnParkourTurnTraverse_ExecutionStaysWithinBudget|FullyQualifiedName~Pathing.Execution.PathTimingContractTests.SpeedCarryRepeatedTraverseDescend_ExecutionStaysWithinBudget|FullyQualifiedName~Pathing.Execution.PathTimingContractTests.LongRoute_ExecutionStaysWithinBudget|FullyQualifiedName~Pathing.Execution.TransitionLookaheadEvaluatorTests|FullyQualifiedName~Pathing.Execution.TransitionBrakingPlannerTests" -v minimal
```
Expected: PASS for the new explicit regressions and no new failures in the broader lookahead/braking coverage.
- [ ] **Step 5: Commit the mixed-route braking fix**
```bash
git add MinecraftClient.Tests/Pathing/Execution/PathTimingContractTests.cs \
MinecraftClient.Tests/Pathing/Execution/TransitionLookaheadEvaluatorTests.cs \
MinecraftClient.Tests/Pathing/Execution/TransitionBrakingPlannerTests.cs \
MinecraftClient/Pathing/Execution/TransitionLookaheadEvaluator.cs \
MinecraftClient/Pathing/Execution/TransitionBrakingPlanner.cs
git commit -m "fix: align transition lookahead with next segment entry"
```
### Task 3: Remove Chained-Ascend Landing Stall In Live Staircases
**Files:**
- Modify: `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`
- Modify: `MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs`
- Modify: `MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs`
- [ ] **Step 1: Add a failing chained-ascend convergence test**
Update `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`:
```csharp
[Fact]
public void AscendTemplate_ContinueStraight_CompletesWithoutSettlingToZeroSpeed()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 338, max: 347);
FlatWorldTestBuilder.ClearBox(world, 340, 80, 338, 347, 86, 342);
FlatWorldTestBuilder.FillSolid(world, 341, 80, 339, 341, 80, 341);
FlatWorldTestBuilder.FillSolid(world, 342, 81, 339, 342, 81, 341);
FlatWorldTestBuilder.FillSolid(world, 343, 82, 339, 343, 82, 341);
var current = new PathSegment
{
Start = new Location(341.5, 81, 340.5),
End = new Location(342.5, 82, 340.5),
MoveType = MoveType.Ascend,
ExitTransition = PathTransitionType.ContinueStraight,
ExitHints = new PathTransitionHints(1, 0, 0.08, double.PositiveInfinity, false, true, false, false, 8),
PreserveSprint = true
};
var next = new PathSegment
{
Start = current.End,
End = new Location(343.5, 83, 340.5),
MoveType = MoveType.Ascend,
ExitTransition = PathTransitionType.ContinueStraight,
ExitHints = new PathTransitionHints(1, 0, 0.08, double.PositiveInfinity, false, true, false, false, 8),
PreserveSprint = true
};
var template = new AscendTemplate(current, next);
var physics = new PlayerPhysics
{
Position = new Vec3d(current.Start.X, current.Start.Y, current.Start.Z),
DeltaMovement = new Vec3d(0.11, 0.0, 0.0),
OnGround = true,
MovementSpeed = 0.1f,
Yaw = 270f,
Pitch = 0f
};
var input = new MovementInput();
TemplateState state = TemplateState.InProgress;
int ticks = 0;
for (; ticks < 30; ticks++)
{
input.Reset();
Location pos = new(physics.Position.X, physics.Position.Y, physics.Position.Z);
state = template.Tick(pos, physics, input, world);
if (state != TemplateState.InProgress)
break;
physics.ApplyInput(input);
physics.Tick(world);
}
Assert.Equal(TemplateState.Complete, state);
Assert.InRange(ticks, 1, 14);
Assert.InRange(physics.DeltaMovement.X, 0.05, 0.20);
}
```
- [ ] **Step 2: Run the new unit test and the live long-route harness to confirm current failure**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter FullyQualifiedName~Pathing.Execution.GroundedTemplateConvergenceTests.AscendTemplate_ContinueStraight_CompletesWithoutSettlingToZeroSpeed -v minimal
source tools/mcc-env.sh && bash tools/test-pathing-long-routes.sh 1.21.11-Vanilla
```
Expected: the unit test fails on tick count or residual speed, and the live harness still reports `same-move-ascend-staircase` over budget.
- [ ] **Step 3: Split ascend execution into takeoff, airborne, and landing handoff**
Update `MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs`:
```csharp
private enum Phase { Takeoff, Airborne, Landing }
private Phase _phase = Phase.Takeoff;
private bool _leftGround;
private int _landingTicks;
public TemplateState Tick(Location pos, PlayerPhysics physics, MovementInput input, World world)
{
_tickCount++;
double dx = ExpectedEnd.X - pos.X;
double dz = ExpectedEnd.Z - pos.Z;
double dy = ExpectedEnd.Y - pos.Y;
float targetYaw = TemplateHelper.CalculateYaw(dx, dz);
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
switch (_phase)
{
case Phase.Takeoff:
input.Forward = true;
input.Sprint = true;
if (physics.OnGround && dy > 0.1)
{
input.Jump = true;
_phase = Phase.Airborne;
}
break;
case Phase.Airborne:
input.Forward = true;
input.Sprint = true;
if (!physics.OnGround)
_leftGround = true;
if (_leftGround && physics.OnGround)
{
_phase = Phase.Landing;
_landingTicks = 0;
goto case Phase.Landing;
}
break;
case Phase.Landing:
_landingTicks++;
GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world);
if (GroundedSegmentController.ShouldComplete(_segment, pos, physics))
return TemplateState.Complete;
break;
}
if (_stuckTicks > 20 || _tickCount > 50)
return TemplateState.Failed;
return TemplateState.InProgress;
}
```
Update `MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs`:
```csharp
if (segment.MoveType == MoveType.Ascend
&& segment.ExitTransition == PathTransitionType.ContinueStraight
&& physics.OnGround
&& TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, segment.End)
&& !TemplateFootingHelper.WillLeaveTargetBlockNextTick(pos, physics, segment.End))
{
double exitSpeed = TemplateHelper.ProjectHorizontalSpeedAlongHint(physics, segment);
return exitSpeed >= Math.Max(0.02, segment.ExitHints.MinExitSpeed);
}
```
- [ ] **Step 4: Re-run the ascend convergence test and the live long-route harness**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~Pathing.Execution.GroundedTemplateConvergenceTests.AscendTemplate_ContinueStraight_CompletesWithoutSettlingToZeroSpeed|FullyQualifiedName~Pathing.Execution.PathTimingContractTests.Scenario_ExecutionStaysWithinTimingBudget" -v minimal
source tools/mcc-env.sh && bash tools/test-pathing-long-routes.sh 1.21.11-Vanilla
```
Expected: PASS for the new unit test and the live long-route suite, including `same-move-ascend-staircase`.
- [ ] **Step 5: Commit the ascend convergence fix**
```bash
git add MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs \
MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs \
MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs
git commit -m "fix: reduce chained ascend landing stalls"
```
### Task 4: Run The Full Regression Sweep And Stop On Any Residual Family
**Files:**
- No code changes required unless verification reveals a new, scoped defect
- [ ] **Step 1: Re-run all focused pathing execution tests**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~Pathing.Execution" -v minimal
```
Expected: PASS with `0` failing pathing execution tests.
- [ ] **Step 2: Re-run the live accepted-route suites that previously failed**
Run:
```bash
source tools/mcc-env.sh && bash tools/test-pathing-jump-combos.sh 1.21.11-Vanilla
source tools/mcc-env.sh && bash tools/test-pathing-long-routes.sh 1.21.11-Vanilla
```
Expected: both scripts exit `0`, with no accepted-route replans and no contract-budget overruns.
- [ ] **Step 3: If any live case still fails, capture the exact family before doing more coding**
Use the existing contract report output already printed by the harnesses. Record:
```text
scenario id
total actual / max ticks
which segment index exceeded
whether the failure was replan, timeout, or budget overrun
```
Do not widen scope beyond:
- parkour landing recovery
- next-segment braking/lookahead
- chained ascend landing handoff
- [ ] **Step 4: End the plan cleanly once verification is green**
Run:
```bash
git status --short
```
Expected: only the intentional runtime/test edits from Tasks 1 through 3 remain. If verification is green and no extra follow-up patch was needed, do not create an empty commit. If verification exposes a new defect family, stop and write a separate scoped plan instead of slipping extra repair work into this one.
## Self-Review
Spec coverage check:
- repeated parkour failures map to Task 1
- mixed-route carry/brake failures map to Task 2
- live staircase ascend overrun maps to Task 3
- full xUnit and live verification maps to Task 4
Placeholder scan:
- no `TODO`, `TBD`, or “similar to above” placeholders remain
- each task includes concrete file paths, test code, commands, and commit steps
Type consistency:
- all next-segment-aware changes consistently use `PathSegment? next`
- named test helpers use `AssertScenarioWithinBudget`
- runtime fixes stay inside the already failing execution files

View file

@ -0,0 +1,520 @@
# Jump-Entry Direct Yaw Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Remove unnecessary yaw smoothing in jump-entry states so opposite-yaw jump starts commit immediately without changing normal walk, descend, climb, or final-stop behavior.
**Architecture:** Introduce a small helper-level yaw alignment policy, then opt in only the jump-entry states: sprint-jump approach, ascend pre-jump alignment, grounded prepare-jump freeze, and grounded walk segments that are explicitly preparing a jump. Keep air control, grounded braking, descend, climb, and ordinary walk/final-stop behavior on smooth yaw, and prove the scope boundary with focused unit tests plus sequential live harness runs.
**Tech Stack:** C# 14, .NET 10, xUnit, MCC local harness scripts (`tools/mcc-env.sh`, `mcc-preflight`, `tools/test-pathing-jump-combos.sh`, `tools/test-pathing-long-routes.sh`)
---
## File Map
- Modify: `MinecraftClient/Pathing/Execution/Templates/TemplateHelper.cs`
- Add a small yaw-alignment helper and heading-facing overloads so templates can request `Smooth` or `Snap` without open-coding raw yaw assignment.
- Modify: `MinecraftClient/Pathing/Execution/Templates/SprintJumpTemplate.cs`
- Snap yaw only during `Phase.Approach`; keep air and landing phases on smooth yaw.
- Modify: `MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs`
- Snap yaw only while aligning for jump commitment; preserve the existing grounded prepare-jump handoff carveout.
- Modify: `MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs`
- Snap exit heading in the frozen `PrepareJump` turn branch only.
- Modify: `MinecraftClient/Pathing/Execution/Templates/WalkTemplate.cs`
- Use snap yaw only for grounded `PrepareJump` segments with `ExitHints.RequireJumpReady == true`.
- Modify: `MinecraftClient.Tests/Pathing/Execution/SprintJumpTemplateScenarioTests.cs`
- Add a focused regression that proves sprint-jump approach snaps immediately from opposite yaw.
- Modify: `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`
- Add focused regressions for ascend pre-jump snap, walk run-up snap, grounded freeze snap, and ordinary final-stop smoothness.
### Task 1: Add Failing Sprint-Jump Snap Test
**Files:**
- Modify: `MinecraftClient.Tests/Pathing/Execution/SprintJumpTemplateScenarioTests.cs`
- Test: `MinecraftClient.Tests/Pathing/Execution/SprintJumpTemplateScenarioTests.cs`
- [ ] **Step 1: Write the failing test**
Add this test near the existing opposite-yaw sprint-jump regressions:
```csharp
[Fact]
public void SprintJumpTemplate_Approach_SnapsYawImmediatelyFromOppositeYaw()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 0, max: 16);
FlatWorldTestBuilder.ClearBox(world, 0, 79, 0, 4, 82, 1);
FlatWorldTestBuilder.SetSolid(world, 0, 79, 0);
FlatWorldTestBuilder.SetSolid(world, 2, 79, 0);
var segment = new PathSegment
{
Start = new Location(0.5, 80, 0.5),
End = new Location(2.5, 80, 0.5),
MoveType = MoveType.Parkour,
ExitTransition = PathTransitionType.FinalStop
};
var template = new SprintJumpTemplate(segment, null);
var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 90f);
var input = new MovementInput();
TemplateState state = template.Tick(segment.Start, physics, input, world);
Assert.Equal(TemplateState.InProgress, state);
Assert.InRange(physics.Yaw, 269.9f, 270.1f);
Assert.True(input.Forward);
Assert.True(input.Sprint);
Assert.True(input.Jump);
}
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~SprintJumpTemplate_Approach_SnapsYawImmediatelyFromOppositeYaw" -v minimal
```
Expected:
- `FAIL`
- The failure should show `physics.Yaw` still near `125` and movement input still blocked by the turn-in-place gate.
- [ ] **Step 3: Write minimal implementation**
In `MinecraftClient/Pathing/Execution/Templates/TemplateHelper.cs`, add the alignment helper and overloads:
```csharp
internal enum YawAlignmentMode
{
Smooth,
Snap
}
internal static float AlignYaw(float current, float target, YawAlignmentMode mode, float maxStep = MaxYawStepPerTick)
{
target = NormalizeYaw(target);
return mode == YawAlignmentMode.Snap
? target
: SmoothYaw(current, target, maxStep);
}
internal static void FaceSegmentHeading(PlayerPhysics physics, PathSegment segment, YawAlignmentMode mode = YawAlignmentMode.Smooth)
{
float headingYaw = CalculateYaw(segment.HeadingX, segment.HeadingZ);
physics.Yaw = AlignYaw(physics.Yaw, headingYaw, mode);
}
internal static void FaceExitHeading(PlayerPhysics physics, PathSegment segment, YawAlignmentMode mode = YawAlignmentMode.Smooth)
{
float headingYaw = GetExitHeadingYaw(segment);
physics.Yaw = AlignYaw(physics.Yaw, headingYaw, mode);
}
private static float NormalizeYaw(float yaw)
{
while (yaw < 0f) yaw += 360f;
while (yaw >= 360f) yaw -= 360f;
return yaw;
}
```
In `MinecraftClient/Pathing/Execution/Templates/SprintJumpTemplate.cs`, switch only `Phase.Approach` to snap yaw:
```csharp
YawAlignmentMode yawMode = _phase == Phase.Approach
? YawAlignmentMode.Snap
: YawAlignmentMode.Smooth;
physics.Yaw = TemplateHelper.AlignYaw(physics.Yaw, targetYaw, yawMode);
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
```
- [ ] **Step 4: Run tests to verify they pass**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~SprintJumpTemplate_Approach_SnapsYawImmediatelyFromOppositeYaw|FullyQualifiedName~SprintJumpTemplate_TwoBlockGap_FinalStop_CompletesFromOppositeYawWithinTwentyTicks|FullyQualifiedName~SprintJumpTemplate_ThreeBlockGap_FinalStop_Completes" -v minimal
```
Expected:
- `PASS`
- The new test passes.
- The existing opposite-yaw timing regression stays green.
- The 3-block final-stop sprint jump still completes.
- [ ] **Step 5: Commit**
```bash
git add \
MinecraftClient/Pathing/Execution/Templates/TemplateHelper.cs \
MinecraftClient/Pathing/Execution/Templates/SprintJumpTemplate.cs \
MinecraftClient.Tests/Pathing/Execution/SprintJumpTemplateScenarioTests.cs
git commit -m "pathing: snap yaw for sprint jump approach"
```
### Task 2: Add Failing Ascend And Frozen Prepare-Jump Snap Tests
**Files:**
- Modify: `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`
- Modify: `MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs`
- Modify: `MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs`
- Test: `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`
- [ ] **Step 1: Write the failing tests**
Add these tests to `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs` near the existing prepare-jump regressions:
```csharp
[Fact]
public void AscendTemplate_PrepareJump_SnapsYawImmediatelyFromOppositeYaw()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 338, max: 344);
FlatWorldTestBuilder.ClearBox(world, 340, 80, 338, 344, 84, 342);
FlatWorldTestBuilder.FillSolid(world, 341, 80, 339, 341, 80, 341);
FlatWorldTestBuilder.FillSolid(world, 342, 81, 339, 342, 81, 341);
var segment = new PathSegment
{
Start = new Location(340.5, 80, 340.5),
End = new Location(341.5, 81, 340.5),
MoveType = MoveType.Ascend,
ExitTransition = PathTransitionType.PrepareJump,
ExitHints = new PathTransitionHints(1, 0, 0.10, double.PositiveInfinity, false, true, true, false, 10),
PreserveSprint = true
};
var next = new PathSegment
{
Start = new Location(341.5, 81, 340.5),
End = new Location(342.5, 82, 340.5),
MoveType = MoveType.Ascend,
ExitTransition = PathTransitionType.FinalStop
};
var template = new AscendTemplate(segment, next);
var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 90f);
var input = new MovementInput();
TemplateState state = template.Tick(segment.Start, physics, input, world);
Assert.Equal(TemplateState.InProgress, state);
Assert.InRange(physics.Yaw, 269.9f, 270.1f);
Assert.True(input.Forward);
Assert.True(input.Sprint);
Assert.True(input.Jump);
}
[Fact]
public void WalkTemplate_PrepareJump_FreezeForTurn_SnapsExitHeadingImmediately()
{
World world = FlatWorldTestBuilder.CreateStoneFloor();
var current = new PathSegment
{
Start = new Location(0.5, 80, 0.5),
End = new Location(1.5, 80, 0.5),
MoveType = MoveType.Traverse,
ExitTransition = PathTransitionType.PrepareJump,
ExitHints = new PathTransitionHints(0, 1, 0.10, double.PositiveInfinity, false, true, true, false, 10),
PreserveSprint = true
};
var next = new PathSegment
{
Start = new Location(1.5, 80, 0.5),
End = new Location(1.5, 80, 1.5),
MoveType = MoveType.Parkour,
ExitTransition = PathTransitionType.FinalStop
};
var template = new WalkTemplate(current, next);
var physics = new PlayerPhysics
{
Position = new Vec3d(1.5, 80.0, 0.5),
DeltaMovement = Vec3d.Zero,
OnGround = true,
MovementSpeed = 0.1f,
Yaw = 180f,
Pitch = 0f
};
var input = new MovementInput();
TemplateState state = template.Tick(new Location(1.5, 80, 0.5), physics, input, world);
Assert.Equal(TemplateState.InProgress, state);
Assert.InRange(physics.Yaw, -0.1f, 0.1f);
Assert.False(input.Forward);
Assert.False(input.Sprint);
Assert.False(input.Back);
}
```
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~AscendTemplate_PrepareJump_SnapsYawImmediatelyFromOppositeYaw|FullyQualifiedName~WalkTemplate_PrepareJump_FreezeForTurn_SnapsExitHeadingImmediately" -v minimal
```
Expected:
- `FAIL`
- The ascend test should show yaw still part-way through the turn.
- The frozen prepare-jump test should show yaw still around `145` instead of `0`.
- [ ] **Step 3: Write minimal implementation**
In `MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs`, snap yaw only before jump commitment and keep the handoff carveout:
```csharp
bool snapYawForJumpCommit = !_initiatedJump && !groundedPrepareJumpHandoff;
physics.Yaw = TemplateHelper.AlignYaw(
physics.Yaw,
targetYaw,
snapYawForJumpCommit ? YawAlignmentMode.Snap : YawAlignmentMode.Smooth);
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
```
In `MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs`, snap the frozen exit-heading turn:
```csharp
if (segment.ExitTransition == PathTransitionType.PrepareJump
&& segment.ExitHints.RequireJumpReady
&& physics.OnGround
&& TemplateFootingHelper.IsCenterInsideTargetBlock(pos, segment.End)
&& IsReadyToFreezeForTurn(segment, pos)
&& TemplateHelper.HeadingPenaltyDegrees(physics.Yaw, segment) > 8.0)
{
input.Forward = false;
input.Sprint = false;
input.Back = false;
TemplateHelper.FaceExitHeading(physics, segment, YawAlignmentMode.Snap);
return;
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~AscendTemplate_PrepareJump_SnapsYawImmediatelyFromOppositeYaw|FullyQualifiedName~WalkTemplate_PrepareJump_FreezeForTurn_SnapsExitHeadingImmediately|FullyQualifiedName~AscendTemplate_PrepareJump_CompletesFromOppositeYawWithinTwentyTicks|FullyQualifiedName~WalkTemplate_TurnIntoParkour_CompletesOnlyWhenTurnEntryIsSlowAndJumpReady" -v minimal
```
Expected:
- `PASS`
- The new snap regressions pass.
- Existing opposite-yaw ascend timing stays green.
- The turn-into-parkour convergence regression still passes.
- [ ] **Step 5: Commit**
```bash
git add \
MinecraftClient/Pathing/Execution/Templates/AscendTemplate.cs \
MinecraftClient/Pathing/Execution/Templates/GroundedSegmentController.cs \
MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs
git commit -m "pathing: snap yaw for jump-ready grounded handoffs"
```
### Task 3: Add Failing Walk Jump-Entry Scope Tests
**Files:**
- Modify: `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`
- Modify: `MinecraftClient/Pathing/Execution/Templates/WalkTemplate.cs`
- Test: `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`
- [ ] **Step 1: Write the failing tests**
Add these tests to `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs` near the existing walk prepare-jump coverage:
```csharp
[Fact]
public void WalkTemplate_PrepareJump_SnapsYawImmediatelyDuringRunUp()
{
World world = FlatWorldTestBuilder.CreateStoneFloor();
var current = new PathSegment
{
Start = new Location(0.5, 80, 0.5),
End = new Location(1.5, 80, 0.5),
MoveType = MoveType.Traverse,
ExitTransition = PathTransitionType.PrepareJump,
ExitHints = new PathTransitionHints(1, 0, 0.10, double.PositiveInfinity, false, true, true, false, 10),
PreserveSprint = true
};
var next = new PathSegment
{
Start = new Location(1.5, 80, 0.5),
End = new Location(3.5, 80, 0.5),
MoveType = MoveType.Parkour,
ExitTransition = PathTransitionType.FinalStop
};
var template = new WalkTemplate(current, next);
var physics = TemplateSimulationRunner.CreateGroundedPhysics(current.Start, yaw: 90f);
var input = new MovementInput();
TemplateState state = template.Tick(current.Start, physics, input, world);
Assert.Equal(TemplateState.InProgress, state);
Assert.InRange(physics.Yaw, 269.9f, 270.1f);
Assert.True(input.Forward);
Assert.True(input.Sprint);
}
[Fact]
public void WalkTemplate_FinalStop_RetainsSmoothYawOutsideJumpEntry()
{
World world = FlatWorldTestBuilder.CreateStoneFloor();
var segment = new PathSegment
{
Start = new Location(0.5, 80, 0.5),
End = new Location(1.5, 80, 0.5),
MoveType = MoveType.Traverse,
ExitTransition = PathTransitionType.FinalStop
};
var template = new WalkTemplate(segment, null);
var physics = TemplateSimulationRunner.CreateGroundedPhysics(segment.Start, yaw: 90f);
var input = new MovementInput();
TemplateState state = template.Tick(segment.Start, physics, input, world);
Assert.Equal(TemplateState.InProgress, state);
Assert.InRange(physics.Yaw, 124.9f, 125.1f);
}
```
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~WalkTemplate_PrepareJump_SnapsYawImmediatelyDuringRunUp|FullyQualifiedName~WalkTemplate_FinalStop_RetainsSmoothYawOutsideJumpEntry" -v minimal
```
Expected:
- `FAIL`
- The prepare-jump test should show smooth partial rotation instead of an immediate snap.
- The final-stop control test should already pass and act as the scope guard for the next step.
- [ ] **Step 3: Write minimal implementation**
In `MinecraftClient/Pathing/Execution/Templates/WalkTemplate.cs`, gate snap yaw to grounded jump-entry segments only:
```csharp
bool snapYawForJumpEntry = physics.OnGround
&& _segment.ExitTransition == PathTransitionType.PrepareJump
&& _segment.ExitHints.RequireJumpReady;
float targetYaw = TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment)
? TemplateHelper.GetExitHeadingYaw(_segment)
: TemplateHelper.CalculateYaw(dx, dz);
physics.Yaw = TemplateHelper.AlignYaw(
physics.Yaw,
targetYaw,
snapYawForJumpEntry ? YawAlignmentMode.Snap : YawAlignmentMode.Smooth);
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
```
- [ ] **Step 4: Run tests to verify they pass**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~WalkTemplate_PrepareJump_SnapsYawImmediatelyDuringRunUp|FullyQualifiedName~WalkTemplate_FinalStop_RetainsSmoothYawOutsideJumpEntry|FullyQualifiedName~WalkTemplate_PrepareJump_CompletesWithoutSettlingOnRunUpBlock|FullyQualifiedName~WalkTemplate_DiagonalPrepareJumpIntoAscend_CompletesFromTargetBlockEntry" -v minimal
```
Expected:
- `PASS`
- The new run-up snap regression passes.
- The final-stop scope guard stays green.
- Existing walk prepare-jump convergence regressions remain green.
- [ ] **Step 5: Commit**
```bash
git add \
MinecraftClient/Pathing/Execution/Templates/WalkTemplate.cs \
MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs
git commit -m "pathing: snap yaw only for grounded jump-entry walk states"
```
### Task 4: Full Verification And Evidence Capture
**Files:**
- Modify only if timing evidence demands it:
- `MinecraftClient.Tests/TestData/Pathing/pathing-timing-budgets.json`
- `MinecraftClient.Tests/TestData/Pathing/pathing-planner-contracts.json`
- Verify:
- `MinecraftClient.Tests/Pathing/Execution/SprintJumpTemplateScenarioTests.cs`
- `MinecraftClient.Tests/Pathing/Execution/GroundedTemplateConvergenceTests.cs`
- `MinecraftClient.Tests/Pathing/Execution/LivePathingRegressionTests.cs`
- `MinecraftClient.Tests/Pathing/Execution/PathPlanningContractTests.cs`
- `MinecraftClient.Tests/Pathing/Execution/PathTimingContractTests.cs`
- [ ] **Step 1: Run the focused unit regression set**
Run:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~SprintJumpTemplateScenarioTests|FullyQualifiedName~GroundedTemplateConvergenceTests|FullyQualifiedName~LivePathingRegressionTests|FullyQualifiedName~MoveParkourTests.Accepts4x1JumpWithoutRearSupport_WhenTakeoffBlockProvidesRunway|FullyQualifiedName~PathPlanningContractTests.Scenario_PlannerMatchesContract|FullyQualifiedName~PathTimingContractTests.JumpCombo_ExecutionStaysWithinBudget|FullyQualifiedName~PathTimingContractTests.LongRoute_ExecutionStaysWithinBudget" -v minimal
```
Expected:
- `PASS`
- No planner regressions.
- No timing budget failures.
- [ ] **Step 2: If a timing contract fails, refresh it from evidence before rerunning**
Use the bootstrap printer first:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~PathingContractBootstrapTests" -v minimal
```
Only if a contract mismatch is stable and explained by the new snap behavior, update the matching JSON entries with the printed values, then rerun the focused contract tests:
```bash
dotnet test MinecraftClient.Tests/MinecraftClient.Tests.csproj --filter "FullyQualifiedName~PathPlanningContractTests.Scenario_PlannerMatchesContract|FullyQualifiedName~PathTimingContractTests.JumpCombo_ExecutionStaysWithinBudget|FullyQualifiedName~PathTimingContractTests.LongRoute_ExecutionStaysWithinBudget" -v minimal
```
Expected:
- Either no JSON changes are needed, or the rerun passes with fresh values backed by bootstrap output.
- [ ] **Step 3: Run jump-combo live harness sequentially**
Run:
```bash
bash -lc 'source tools/mcc-env.sh && mcc-preflight 1.21.11-Vanilla && bash tools/test-pathing-jump-combos.sh 1.21.11-Vanilla'
```
Expected:
- `PASS` summary for all jump-combo scenarios.
- No `Replan #`, `Partial`, `Replan failed`, or `Giving up`.
- [ ] **Step 4: Run long-route live harness sequentially**
Run:
```bash
bash -lc 'source tools/mcc-env.sh && mcc-preflight 1.21.11-Vanilla && bash tools/test-pathing-long-routes.sh 1.21.11-Vanilla'
```
Expected:
- `Pathing long-route suite complete.`
- No `Replan #`, `Partial`, `Replan failed`, or `Giving up`.
- Repeated jump-entry routes remain within current max budgets.
- [ ] **Step 5: Commit only additional contract refreshes from Task 4**
If Task 4 needed no JSON or script edits, do not create another commit. Record that verification completed with no additional file changes.
If timing contracts changed in Task 4, commit only those refreshes:
```bash
git add MinecraftClient.Tests/TestData/Pathing/pathing-timing-budgets.json
git commit -m "test: refresh jump-entry snap yaw timing budgets"
```

View file

@ -183,11 +183,13 @@ prepare_independent_route() {
local start_x="$2"
local start_y="$3"
local start_z="$4"
local start_yaw="${5:-270}"
local start_pitch="${6:-0}"
echo ""
echo "Preparing independent route: $label"
mc-rcon "effect clear $USERNAME" >/dev/null 2>&1 || true
mc-rcon "tp $USERNAME $start_x $start_y $start_z" >/dev/null
mc-rcon "tp $USERNAME $start_x $start_y $start_z $start_yaw $start_pitch" >/dev/null
wait_for_location_in_block "$start_x" "$start_y" "$start_z" 10
}
@ -261,9 +263,11 @@ run_accepted_route() {
local goal_x="$6"
local goal_y="$7"
local goal_z="$8"
local timeout="${9:-45}"
local start_yaw="${9:-270}"
local start_pitch="${10:-0}"
local timeout="${11:-45}"
prepare_independent_route "$label" "$start_x" "$start_y" "$start_z"
prepare_independent_route "$label" "$start_x" "$start_y" "$start_z" "$start_yaw" "$start_pitch"
capture_debug_state_before_route "$label"
local start_line
@ -327,7 +331,7 @@ scenario_repeated_cardinal_parkour() {
set_stone 584 79 580
set_stone 586 79 580
set_stone 588 79 580
run_accepted_route "repeated-cardinal-parkour-chain" "Repeated jump - cardinal parkour chain" "580.5" "80" "580.5" "588" "80.00" "580"
run_accepted_route "repeated-cardinal-parkour-chain" "Repeated jump - cardinal parkour chain" "580.5" "80" "580.5" "588" "80.00" "580" "270"
}
scenario_repeated_diagonal_parkour() {
@ -337,7 +341,7 @@ scenario_repeated_diagonal_parkour() {
set_stone 602 79 602
set_stone 604 79 604
set_stone 606 79 606
run_accepted_route "repeated-diagonal-parkour-chain" "Repeated jump - diagonal parkour chain" "600.5" "80" "600.5" "606" "80.00" "606"
run_accepted_route "repeated-diagonal-parkour-chain" "Repeated jump - diagonal parkour chain" "600.5" "80" "600.5" "606" "80.00" "606" "315"
}
scenario_obstructed_parkour_turn_mix() {
@ -353,7 +357,7 @@ scenario_obstructed_parkour_turn_mix() {
set_stone 620 81 621
set_stone 622 80 622
set_stone 622 81 622
run_accepted_route "obstructed-parkour-l-turns" "Obstructed jump mix - repeated parkour L-turns" "620.5" "80" "620.5" "626" "80.00" "622"
run_accepted_route "obstructed-parkour-l-turns" "Obstructed jump mix - repeated parkour L-turns" "620.5" "80" "620.5" "626" "80.00" "622" "270"
}
scenario_parkour_ascend_descend_chain() {
@ -364,7 +368,7 @@ scenario_parkour_ascend_descend_chain() {
set_stone 644 79 620
set_stone 646 80 620
set_stone 648 79 620
run_accepted_route "vertical-jump-mix" "Vertical jump mix - parkour ascend descend chain" "640.5" "80" "620.5" "648" "80.00" "620"
run_accepted_route "vertical-jump-mix" "Vertical jump mix - parkour ascend descend chain" "640.5" "80" "620.5" "648" "80.00" "620" "270"
}
scenario_diagonal_ascend_descend_chain() {
@ -375,7 +379,7 @@ scenario_diagonal_ascend_descend_chain() {
set_stone 682 79 622
set_stone 683 80 623
set_stone 684 79 624
run_accepted_route "diagonal-vertical-mix" "Diagonal vertical mix - ascend descend chain" "680.5" "80" "620.5" "684" "80.00" "624"
run_accepted_route "diagonal-vertical-mix" "Diagonal vertical mix - ascend descend chain" "680.5" "80" "620.5" "684" "80.00" "624" "315"
}
start_mcc

View file

@ -144,11 +144,13 @@ prepare_independent_route() {
local start_x="$2"
local start_y="$3"
local start_z="$4"
local start_yaw="${5:-270}"
local start_pitch="${6:-0}"
echo ""
echo "Preparing independent route: $label"
mc-rcon "effect clear $USERNAME" >/dev/null 2>&1 || true
mc-rcon "tp $USERNAME $start_x $start_y $start_z" >/dev/null
mc-rcon "tp $USERNAME $start_x $start_y $start_z $start_yaw $start_pitch" >/dev/null
wait_for_location_in_block "$start_x" "$start_y" "$start_z" 10
}
@ -258,9 +260,11 @@ run_accepted_route() {
local goal_x="$6"
local goal_y="$7"
local goal_z="$8"
local timeout="${9:-45}"
local start_yaw="${9:-270}"
local start_pitch="${10:-0}"
local timeout="${11:-45}"
prepare_independent_route "$label" "$start_x" "$start_y" "$start_z"
prepare_independent_route "$label" "$start_x" "$start_y" "$start_z" "$start_yaw" "$start_pitch"
capture_debug_state_before_route "$label"
local start_line
@ -299,7 +303,7 @@ run_same_move_routes() {
fill_box 298 79 298 314 79 302 air
fill_box 298 80 298 314 90 302 air
fill_box 300 79 300 312 79 300 stone
run_accepted_route "same-move-straight-traverse-chain" "Same move - straight traverse chain" "300.5" "80" "300.5" "312" "80.00" "300"
run_accepted_route "same-move-straight-traverse-chain" "Same move - straight traverse chain" "300.5" "80" "300.5" "312" "80.00" "300" "270"
fill_box 318 79 318 330 79 330 air
fill_box 318 80 318 330 90 330 air
@ -311,7 +315,7 @@ run_same_move_routes() {
set_stone 325 79 325
set_stone 326 79 326
set_stone 327 79 327
run_accepted_route "same-move-diagonal-chain" "Same move - diagonal chain" "320.5" "80" "320.5" "327" "80.00" "327"
run_accepted_route "same-move-diagonal-chain" "Same move - diagonal chain" "320.5" "80" "320.5" "327" "80.00" "327" "315"
fill_box 338 79 338 347 85 342 air
fill_box 338 80 338 347 90 342 air
@ -321,7 +325,7 @@ run_same_move_routes() {
fill_box 343 82 339 343 82 341 stone
fill_box 344 83 339 344 83 341 stone
fill_box 345 84 339 345 84 341 stone
run_accepted_route "same-move-ascend-staircase" "Same move - ascend staircase" "340.5" "80" "340.5" "345" "85.00" "340"
run_accepted_route "same-move-ascend-staircase" "Same move - ascend staircase" "340.5" "80" "340.5" "345" "85.00" "340" "270"
fill_box 360 79 358 369 85 362 air
fill_box 360 80 358 369 90 362 air
@ -331,7 +335,7 @@ run_same_move_routes() {
fill_box 365 81 359 365 81 361 stone
fill_box 366 80 359 366 80 361 stone
fill_box 367 79 359 367 79 361 stone
run_accepted_route "same-move-descend-staircase" "Same move - descend staircase" "362.5" "85" "360.5" "367" "80.00" "360"
run_accepted_route "same-move-descend-staircase" "Same move - descend staircase" "362.5" "85" "360.5" "367" "80.00" "360" "270"
fill_box 378 79 378 390 79 382 air
fill_box 378 80 378 390 90 382 air
@ -340,7 +344,7 @@ run_same_move_routes() {
set_stone 384 79 380
set_stone 386 79 380
set_stone 388 79 380
run_accepted_route "same-move-aligned-parkour-chain" "Same move - aligned parkour chain" "380.5" "80" "380.5" "388" "80.00" "380"
run_accepted_route "same-move-aligned-parkour-chain" "Same move - aligned parkour chain" "380.5" "80" "380.5" "388" "80.00" "380" "270"
}
run_mixed_move_routes() {
@ -360,7 +364,7 @@ run_mixed_move_routes() {
set_stone 406 79 404
set_stone 407 79 404
set_stone 408 79 404
run_accepted_route "mixed-traverse-turn-parkour-turn-traverse" "Mixed - traverse turn parkour turn traverse" "400.5" "80" "400.5" "408" "80.00" "404"
run_accepted_route "mixed-traverse-turn-parkour-turn-traverse" "Mixed - traverse turn parkour turn traverse" "400.5" "80" "400.5" "408" "80.00" "404" "270"
fill_box 418 79 418 430 82 424 air
fill_box 418 80 418 430 92 424 air
@ -373,7 +377,7 @@ run_mixed_move_routes() {
set_stone 426 81 422
set_stone 427 80 422
set_stone 428 79 422
run_accepted_route "mixed-diagonal-ascend-traverse-descend" "Mixed - diagonal ascend traverse descend" "420.5" "80" "420.5" "428" "80.00" "422"
run_accepted_route "mixed-diagonal-ascend-traverse-descend" "Mixed - diagonal ascend traverse descend" "420.5" "80" "420.5" "428" "80.00" "422" "315"
fill_box 438 79 438 450 82 442 air
fill_box 438 80 438 450 92 442 air
@ -385,7 +389,7 @@ run_mixed_move_routes() {
set_stone 446 81 440
set_stone 447 80 440
set_stone 448 79 440
run_accepted_route "mixed-traverse-ascend-parkour-descend" "Mixed - traverse ascend parkour descend" "440.5" "80" "440.5" "448" "80.00" "440"
run_accepted_route "mixed-traverse-ascend-parkour-descend" "Mixed - traverse ascend parkour descend" "440.5" "80" "440.5" "448" "80.00" "440" "270"
}
run_turn_density_routes() {
@ -403,7 +407,7 @@ run_turn_density_routes() {
set_stone 465 79 464
set_stone 465 79 465
set_stone 466 79 466
run_accepted_route "turn-density-alternating-traverse-diagonal-chain" "Turn density - alternating traverse diagonal chain" "460.5" "80" "460.5" "466" "80.00" "466"
run_accepted_route "turn-density-alternating-traverse-diagonal-chain" "Turn density - alternating traverse diagonal chain" "460.5" "80" "460.5" "466" "80.00" "466" "270"
}
run_speed_carry_routes() {
@ -420,7 +424,7 @@ run_speed_carry_routes() {
set_stone 486 82 480
set_stone 487 82 480
set_stone 488 83 480
run_accepted_route "speed-carry-repeated-traverse-ascend" "Speed carry - repeated traverse ascend" "480.5" "80" "480.5" "488" "84.00" "480"
run_accepted_route "speed-carry-repeated-traverse-ascend" "Speed carry - repeated traverse ascend" "480.5" "80" "480.5" "488" "84.00" "480" "270"
fill_box 498 79 498 510 82 502 air
fill_box 498 80 498 510 94 502 air
@ -432,7 +436,7 @@ run_speed_carry_routes() {
set_stone 505 80 500
set_stone 506 79 500
set_stone 507 79 500
run_accepted_route "speed-carry-repeated-traverse-descend" "Speed carry - repeated traverse descend" "500.5" "83" "500.5" "507" "80.00" "500"
run_accepted_route "speed-carry-repeated-traverse-descend" "Speed carry - repeated traverse descend" "500.5" "83" "500.5" "507" "80.00" "500" "270"
fill_box 518 79 518 532 79 522 air
fill_box 518 80 518 532 90 522 air
@ -443,7 +447,7 @@ run_speed_carry_routes() {
set_stone 526 79 520
set_stone 527 79 520
set_stone 529 79 520
run_accepted_route "speed-carry-repeated-traverse-parkour" "Speed carry - repeated traverse parkour" "520.5" "80" "520.5" "529" "80.00" "520"
run_accepted_route "speed-carry-repeated-traverse-parkour" "Speed carry - repeated traverse parkour" "520.5" "80" "520.5" "529" "80.00" "520" "270"
}
start_mcc

View file

@ -160,11 +160,13 @@ prepare_independent_route() {
local start_x="$2"
local start_y="$3"
local start_z="$4"
local start_yaw="${5:-270}"
local start_pitch="${6:-0}"
echo ""
echo "Preparing independent route: $label"
mc-rcon "effect clear $USERNAME" >/dev/null 2>&1 || true
mc-rcon "tp $USERNAME $start_x $start_y $start_z" >/dev/null
mc-rcon "tp $USERNAME $start_x $start_y $start_z $start_yaw $start_pitch" >/dev/null
wait_for_location_in_block "$start_x" "$start_y" "$start_z" 10
}
@ -289,7 +291,7 @@ run_flat_final_stop() {
echo "== Flat final stop =="
mc-rcon "fill 95 79 95 115 79 105 stone" >/dev/null
mc-rcon "fill 95 80 95 115 85 105 air" >/dev/null
prepare_independent_route "Flat final stop" "100.5" "80" "100.5"
prepare_independent_route "Flat final stop" "100.5" "80" "100.5" "270"
capture_debug_state_before_route "Flat final stop"
local start_line
start_line="$(log_line_count)"
@ -315,7 +317,7 @@ run_parkour_into_turn() {
mc-rcon "setblock 122 79 111 stone" >/dev/null
mc-rcon "setblock 120 80 111 stone" >/dev/null
mc-rcon "setblock 120 81 111 stone" >/dev/null
prepare_independent_route "Parkour into L-turn" "120.5" "80" "110.5"
prepare_independent_route "Parkour into L-turn" "120.5" "80" "110.5" "270"
capture_debug_state_before_route "Parkour into L-turn"
local start_line
start_line="$(log_line_count)"
@ -342,7 +344,7 @@ run_side_wall_jump() {
mc-rcon "setblock 132 81 126 stone" >/dev/null
mc-rcon "setblock 133 80 126 stone" >/dev/null
mc-rcon "setblock 133 81 126 stone" >/dev/null
prepare_independent_route "Rejected 2x1 side-wall jump" "131.5" "80" "127.5"
prepare_independent_route "Rejected 2x1 side-wall jump" "131.5" "80" "127.5" "270"
capture_debug_state_before_route "Rejected 2x1 side-wall jump"
local start_line
start_line="$(log_line_count)"
@ -365,7 +367,7 @@ run_reject_3x1_gap() {
mc-rcon "fill 140 79 135 148 79 140 stone" >/dev/null
mc-rcon "fill 140 80 135 148 85 140 air" >/dev/null
mc-rcon "setblock 143 80 138 stone" >/dev/null
prepare_independent_route "Rejected 3x1 no-run-up gap" "141.5" "80" "138.5"
prepare_independent_route "Rejected 3x1 no-run-up gap" "141.5" "80" "138.5" "270"
capture_debug_state_before_route "Rejected 3x1 gap"
local start_line
start_line="$(log_line_count)"
@ -390,7 +392,7 @@ run_corner_ascend_around_wall() {
mc-rcon "setblock 191 80 171 stone" >/dev/null
mc-rcon "setblock 191 80 170 stone" >/dev/null
mc-rcon "setblock 191 81 170 stone" >/dev/null
prepare_independent_route "Corner ascend around wall" "190.5" "80" "170.5"
prepare_independent_route "Corner ascend around wall" "190.5" "80" "170.5" "315"
capture_debug_state_before_route "Corner ascend around wall"
local start_line
start_line="$(log_line_count)"
@ -417,7 +419,7 @@ run_wall_adjacent_descend_smoke() {
mc-rcon "setblock 202 80 199 stone" >/dev/null
mc-rcon "setblock 201 81 199 stone" >/dev/null
mc-rcon "setblock 202 81 199 stone" >/dev/null
prepare_independent_route "Wall-adjacent descend" "200.5" "81" "200.5"
prepare_independent_route "Wall-adjacent descend" "200.5" "81" "200.5" "270"
capture_debug_state_before_route "Wall-adjacent descend"
local start_line
start_line="$(log_line_count)"
@ -441,7 +443,7 @@ run_ascend_chain_smoke() {
mc-rcon "setblock 175 80 162 stone" >/dev/null
mc-rcon "setblock 176 81 162 stone" >/dev/null
mc-rcon "setblock 177 82 162 stone" >/dev/null
prepare_independent_route "Ascend chain smoke" "171.5" "80" "160.5"
prepare_independent_route "Ascend chain smoke" "171.5" "80" "160.5" "315"
capture_debug_state_before_route "Ascend chain smoke"
local start_line
start_line="$(log_line_count)"

View file

@ -142,11 +142,13 @@ prepare_independent_route() {
local start_x="$2"
local start_y="$3"
local start_z="$4"
local start_yaw="${5:-270}"
local start_pitch="${6:-0}"
echo ""
echo "Preparing independent route: $label"
mc-rcon "effect clear $USERNAME" >/dev/null 2>&1 || true
mc-rcon "tp $USERNAME $start_x $start_y $start_z" >/dev/null
mc-rcon "tp $USERNAME $start_x $start_y $start_z $start_yaw $start_pitch" >/dev/null
wait_for_location_in_block "$start_x" "$start_y" "$start_z" 10
}
@ -240,7 +242,7 @@ run_flat_final_stop() {
echo "== Flat final stop =="
mc-rcon "fill 95 79 95 115 79 105 stone" >/dev/null
mc-rcon "fill 95 80 95 115 85 105 air" >/dev/null
prepare_independent_route "Flat final stop" "100.5" "80" "100.5"
prepare_independent_route "Flat final stop" "100.5" "80" "100.5" "270"
capture_debug_state_before_route "Flat final stop"
local start_line
start_line="$(log_line_count)"
@ -263,7 +265,7 @@ run_parkour_into_turn() {
mc-rcon "setblock 120 79 110 stone" >/dev/null
mc-rcon "setblock 123 79 110 stone" >/dev/null
mc-rcon "setblock 123 79 111 stone" >/dev/null
prepare_independent_route "Parkour into turn" "120.5" "80" "110.5"
prepare_independent_route "Parkour into turn" "120.5" "80" "110.5" "270"
capture_debug_state_before_route "Parkour into turn"
local start_line
start_line="$(log_line_count)"