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

@ -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"
```