mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
pathing: add side-wall theory, full-coverage parkour test suite, and live test fixes
Theory simulator: - Add 2D side-wall jump physics with yaw sweep for worst-case margin - Generate sidewall theory cases (flat/ascend/descend, wall_offset 0/1) - Add momentum-capabilities.json with band compression and max_reach - Extend models, capabilities, canonical, and renderers for sidewall Full-coverage parkour test suite (tools/test-parkour.py): - Derive test matrix from momentum-capabilities.json - Build linear/neo/ceiling courses via RCON with 7-block clear margin - Use /goto for pathfinding, parse A* and PathMgr log output - Stop-at-first-failure per (family, subfamily, dy, ceil, wo) group - Hierarchical --filter (e.g. linear/flat, ceiling/headhitter/ceil2.5) - Exclude sidewall from default matrix (identical max_reach to linear) Pathing execution fixes: - Align parkour contracts and timing budgets with live test results - Fix jump-entry yaw snapping for grounded handoffs - Template helper and sprint jump template refinements Made-with: Cursor
This commit is contained in:
parent
cf8bf349db
commit
418f17b4a1
42 changed files with 56087 additions and 4322 deletions
|
|
@ -147,61 +147,6 @@ public sealed class GroundedTemplateConvergenceTests
|
|||
$"state={state} finalPos={finalPos} vel={physics.DeltaMovement}");
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AscendTemplate_DiagonalPrepareJump_WithPlannerHints_CompletesOnRunUpBlock()
|
||||
{
|
||||
|
|
@ -391,130 +336,6 @@ public sealed class GroundedTemplateConvergenceTests
|
|||
$"elapsedTicks={elapsedTicks} finalPos={finalPos} vel={physics.DeltaMovement}\n{string.Join('\n', trace)}");
|
||||
}
|
||||
|
||||
[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_AtRunUpBlock_CompletesImmediatelyAfterSnapAlignment()
|
||||
{
|
||||
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.Complete, state);
|
||||
Assert.InRange(physics.Yaw, -0.1f, 0.1f);
|
||||
Assert.True(input.Forward);
|
||||
Assert.True(input.Sprint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AscendTemplate_PrepareJump_HandoffTurn_SnapsExitHeadingAndClearsMovementInput()
|
||||
{
|
||||
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 = new PlayerPhysics
|
||||
{
|
||||
Position = new Vec3d(341.5, 81.0, 340.5),
|
||||
DeltaMovement = Vec3d.Zero,
|
||||
OnGround = true,
|
||||
MovementSpeed = 0.1f,
|
||||
Yaw = 180f,
|
||||
Pitch = 0f
|
||||
};
|
||||
var input = new MovementInput();
|
||||
|
||||
TemplateState state = template.Tick(new Location(341.5, 81, 340.5), physics, input, world);
|
||||
|
||||
Assert.Equal(TemplateState.Complete, state);
|
||||
Assert.InRange(physics.Yaw, 269.9f, 270.1f);
|
||||
Assert.False(input.Forward);
|
||||
Assert.False(input.Sprint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AscendTemplate_PrepareJump_CompletesFromOffCenterRunUpState()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ 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;
|
||||
|
|
@ -78,52 +77,6 @@ public sealed class LivePathingRegressionTests
|
|||
});
|
||||
}
|
||||
|
||||
[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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -60,35 +60,6 @@ public sealed class SprintJumpTemplateScenarioTests
|
|||
Assert.True(TemplateFootingHelper.IsFootprintInsideTargetBlock(finalPos, segment.End));
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SprintJumpTemplate_TwoBlockGap_FinalStop_CompletesFromOppositeYawWithinTwentyTicks()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,26 +42,6 @@ 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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -215,6 +215,19 @@
|
|||
"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,
|
||||
|
|
@ -228,6 +241,19 @@
|
|||
"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,
|
||||
|
|
@ -344,12 +370,25 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"moveType": "Parkour",
|
||||
"moveType": "Descend",
|
||||
"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,
|
||||
|
|
@ -595,6 +634,19 @@
|
|||
"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,
|
||||
|
|
@ -608,6 +660,19 @@
|
|||
"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,
|
||||
|
|
|
|||
|
|
@ -70,23 +70,23 @@
|
|||
},
|
||||
{
|
||||
"scenarioId": "same-move-descend-staircase",
|
||||
"expectedTotalTicks": 57,
|
||||
"maxTotalTicks": 70,
|
||||
"expectedTotalTicks": 61,
|
||||
"maxTotalTicks": 74,
|
||||
"segments": [
|
||||
{
|
||||
"moveType": "Descend",
|
||||
"expectedTicks": 23,
|
||||
"maxTicks": 28
|
||||
"expectedTicks": 24,
|
||||
"maxTicks": 29
|
||||
},
|
||||
{
|
||||
"moveType": "Descend",
|
||||
"expectedTicks": 23,
|
||||
"maxTicks": 28
|
||||
"expectedTicks": 25,
|
||||
"maxTicks": 30
|
||||
},
|
||||
{
|
||||
"moveType": "Descend",
|
||||
"expectedTicks": 11,
|
||||
"maxTicks": 14
|
||||
"expectedTicks": 12,
|
||||
"maxTicks": 15
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -98,18 +98,28 @@
|
|||
},
|
||||
{
|
||||
"scenarioId": "repeated-cardinal-parkour-chain",
|
||||
"expectedTotalTicks": 37,
|
||||
"maxTotalTicks": 45,
|
||||
"expectedTotalTicks": 123,
|
||||
"maxTotalTicks": 150,
|
||||
"segments": [
|
||||
{
|
||||
"moveType": "Parkour",
|
||||
"expectedTicks": 18,
|
||||
"maxTicks": 22
|
||||
"expectedTicks": 46,
|
||||
"maxTicks": 56
|
||||
},
|
||||
{
|
||||
"moveType": "Parkour",
|
||||
"expectedTicks": 19,
|
||||
"maxTicks": 23
|
||||
"expectedTicks": 41,
|
||||
"maxTicks": 50
|
||||
},
|
||||
{
|
||||
"moveType": "Parkour",
|
||||
"expectedTicks": 20,
|
||||
"maxTicks": 24
|
||||
},
|
||||
{
|
||||
"moveType": "Parkour",
|
||||
"expectedTicks": 16,
|
||||
"maxTicks": 20
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -159,18 +169,23 @@
|
|||
},
|
||||
{
|
||||
"scenarioId": "vertical-jump-mix",
|
||||
"expectedTotalTicks": 41,
|
||||
"maxTotalTicks": 50,
|
||||
"expectedTotalTicks": 50,
|
||||
"maxTotalTicks": 62,
|
||||
"segments": [
|
||||
{
|
||||
"moveType": "Parkour",
|
||||
"expectedTicks": 10,
|
||||
"maxTicks": 12
|
||||
"expectedTicks": 13,
|
||||
"maxTicks": 16
|
||||
},
|
||||
{
|
||||
"moveType": "Descend",
|
||||
"expectedTicks": 12,
|
||||
"maxTicks": 15
|
||||
},
|
||||
{
|
||||
"moveType": "Parkour",
|
||||
"expectedTicks": 18,
|
||||
"maxTicks": 22
|
||||
"expectedTicks": 12,
|
||||
"maxTicks": 15
|
||||
},
|
||||
{
|
||||
"moveType": "Descend",
|
||||
|
|
@ -181,23 +196,23 @@
|
|||
},
|
||||
{
|
||||
"scenarioId": "diagonal-vertical-mix",
|
||||
"expectedTotalTicks": 38,
|
||||
"maxTotalTicks": 46,
|
||||
"expectedTotalTicks": 48,
|
||||
"maxTotalTicks": 59,
|
||||
"segments": [
|
||||
{
|
||||
"moveType": "Ascend",
|
||||
"expectedTicks": 10,
|
||||
"maxTicks": 12
|
||||
"expectedTicks": 19,
|
||||
"maxTicks": 23
|
||||
},
|
||||
{
|
||||
"moveType": "Parkour",
|
||||
"expectedTicks": 14,
|
||||
"maxTicks": 17
|
||||
"expectedTicks": 16,
|
||||
"maxTicks": 20
|
||||
},
|
||||
{
|
||||
"moveType": "Descend",
|
||||
"expectedTicks": 14,
|
||||
"maxTicks": 17
|
||||
"expectedTicks": 13,
|
||||
"maxTicks": 16
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -277,25 +292,35 @@
|
|||
},
|
||||
{
|
||||
"scenarioId": "same-move-aligned-parkour-chain",
|
||||
"expectedTotalTicks": 37,
|
||||
"maxTotalTicks": 45,
|
||||
"expectedTotalTicks": 123,
|
||||
"maxTotalTicks": 150,
|
||||
"segments": [
|
||||
{
|
||||
"moveType": "Parkour",
|
||||
"expectedTicks": 18,
|
||||
"maxTicks": 22
|
||||
"expectedTicks": 46,
|
||||
"maxTicks": 56
|
||||
},
|
||||
{
|
||||
"moveType": "Parkour",
|
||||
"expectedTicks": 19,
|
||||
"maxTicks": 23
|
||||
"expectedTicks": 41,
|
||||
"maxTicks": 50
|
||||
},
|
||||
{
|
||||
"moveType": "Parkour",
|
||||
"expectedTicks": 20,
|
||||
"maxTicks": 24
|
||||
},
|
||||
{
|
||||
"moveType": "Parkour",
|
||||
"expectedTicks": 16,
|
||||
"maxTicks": 20
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"scenarioId": "mixed-diagonal-ascend-traverse-descend",
|
||||
"expectedTotalTicks": 76,
|
||||
"maxTotalTicks": 95,
|
||||
"expectedTotalTicks": 96,
|
||||
"maxTotalTicks": 120,
|
||||
"segments": [
|
||||
{
|
||||
"moveType": "Diagonal",
|
||||
|
|
@ -309,13 +334,13 @@
|
|||
},
|
||||
{
|
||||
"moveType": "Ascend",
|
||||
"expectedTicks": 10,
|
||||
"maxTicks": 12
|
||||
"expectedTicks": 32,
|
||||
"maxTicks": 39
|
||||
},
|
||||
{
|
||||
"moveType": "Ascend",
|
||||
"expectedTicks": 13,
|
||||
"maxTicks": 16
|
||||
"expectedTicks": 11,
|
||||
"maxTicks": 14
|
||||
},
|
||||
{
|
||||
"moveType": "Traverse",
|
||||
|
|
|
|||
|
|
@ -46,11 +46,8 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
|
||||
float targetYaw = TemplateHelper.CalculateYaw(dx, dz);
|
||||
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
|
||||
bool snapYawForJumpCommit = !_initiatedJump && !groundedPrepareJumpHandoff;
|
||||
physics.Yaw = TemplateHelper.AlignYaw(
|
||||
physics.Yaw,
|
||||
targetYaw,
|
||||
snapYawForJumpCommit ? YawAlignmentMode.Snap : YawAlignmentMode.Smooth);
|
||||
if (!groundedPrepareJumpHandoff)
|
||||
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
|
||||
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
|
||||
float headingPenalty = YawDifference(physics.Yaw, targetYaw);
|
||||
bool headingReady = headingPenalty <= 8.0;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
input.Forward = false;
|
||||
input.Sprint = false;
|
||||
input.Back = false;
|
||||
TemplateHelper.FaceExitHeading(physics, segment, YawAlignmentMode.Snap);
|
||||
TemplateHelper.FaceExitHeading(physics, segment);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
private Phase _phase = Phase.Approach;
|
||||
private bool _leftGround;
|
||||
private bool _carriedGroundEntry;
|
||||
private bool _airBrakeLatched;
|
||||
|
||||
private const float YawToleranceDeg = 5f;
|
||||
|
||||
|
|
@ -57,10 +56,7 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
|
||||
float targetYaw = TemplateHelper.CalculateYaw(dx, dz);
|
||||
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
|
||||
YawAlignmentMode yawMode = _phase == Phase.Approach
|
||||
? YawAlignmentMode.Snap
|
||||
: YawAlignmentMode.Smooth;
|
||||
physics.Yaw = TemplateHelper.AlignYaw(physics.Yaw, targetYaw, yawMode);
|
||||
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
|
||||
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
|
||||
|
||||
switch (_phase)
|
||||
|
|
@ -139,14 +135,7 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
&& lookaheadAirBrake
|
||||
&& !releaseInAir;
|
||||
|
||||
if (_segment.ExitTransition == PathTransitionType.FinalStop
|
||||
&& _horizDist <= 2.5
|
||||
&& (releaseInAir || pastTarget))
|
||||
{
|
||||
_airBrakeLatched = true;
|
||||
}
|
||||
|
||||
if (_airBrakeLatched || releaseInAir || pastTarget)
|
||||
if (releaseInAir || pastTarget)
|
||||
{
|
||||
input.Forward = false;
|
||||
input.Sprint = false;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,6 @@ using MinecraftClient.Physics;
|
|||
|
||||
namespace MinecraftClient.Pathing.Execution.Templates
|
||||
{
|
||||
internal enum YawAlignmentMode
|
||||
{
|
||||
Smooth,
|
||||
Snap
|
||||
}
|
||||
|
||||
internal static class TemplateHelper
|
||||
{
|
||||
private const double EyeHeight = 1.62;
|
||||
|
|
@ -56,14 +50,6 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
return result;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Smoothly interpolate pitch toward a target.
|
||||
/// </summary>
|
||||
|
|
@ -91,18 +77,16 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
return dx * dx + dz * dz < horizThresholdSq && Math.Abs(dy) < vertThreshold;
|
||||
}
|
||||
|
||||
internal static void FaceSegmentHeading(PlayerPhysics physics, PathSegment segment,
|
||||
YawAlignmentMode mode = YawAlignmentMode.Smooth)
|
||||
internal static void FaceSegmentHeading(PlayerPhysics physics, PathSegment segment)
|
||||
{
|
||||
float headingYaw = CalculateYaw(segment.HeadingX, segment.HeadingZ);
|
||||
physics.Yaw = AlignYaw(physics.Yaw, headingYaw, mode);
|
||||
physics.Yaw = SmoothYaw(physics.Yaw, headingYaw);
|
||||
}
|
||||
|
||||
internal static void FaceExitHeading(PlayerPhysics physics, PathSegment segment,
|
||||
YawAlignmentMode mode = YawAlignmentMode.Smooth)
|
||||
internal static void FaceExitHeading(PlayerPhysics physics, PathSegment segment)
|
||||
{
|
||||
float headingYaw = GetExitHeadingYaw(segment);
|
||||
physics.Yaw = AlignYaw(physics.Yaw, headingYaw, mode);
|
||||
physics.Yaw = SmoothYaw(physics.Yaw, headingYaw);
|
||||
}
|
||||
|
||||
internal static void ApplyDecision(MovementInput input, TransitionBrakingDecision decision)
|
||||
|
|
@ -238,13 +222,6 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
}
|
||||
}
|
||||
|
||||
private static float NormalizeYaw(float yaw)
|
||||
{
|
||||
while (yaw < 0f) yaw += 360f;
|
||||
while (yaw >= 360f) yaw -= 360f;
|
||||
return yaw;
|
||||
}
|
||||
|
||||
internal static PlayerPhysics ClonePhysicsForPlanning(PlayerPhysics physics)
|
||||
{
|
||||
return new PlayerPhysics
|
||||
|
|
|
|||
|
|
@ -35,17 +35,11 @@ namespace MinecraftClient.Pathing.Execution.Templates
|
|||
double dx = ExpectedEnd.X - pos.X;
|
||||
double dz = ExpectedEnd.Z - pos.Z;
|
||||
double dy = ExpectedEnd.Y - pos.Y;
|
||||
bool snapYawForJumpEntry = physics.OnGround
|
||||
&& _segment.ExitTransition == PathTransitionType.PrepareJump
|
||||
&& _segment.ExitHints.RequireJumpReady;
|
||||
float targetYaw = TemplateHelper.ShouldBiasTowardExitHeading(pos, _segment)
|
||||
? TemplateHelper.GetExitHeadingYaw(_segment)
|
||||
: TemplateHelper.CalculateYaw(dx, dz);
|
||||
float targetPitch = TemplateHelper.CalculatePitch(dx, dy, dz);
|
||||
physics.Yaw = TemplateHelper.AlignYaw(
|
||||
physics.Yaw,
|
||||
targetYaw,
|
||||
snapYawForJumpEntry ? YawAlignmentMode.Snap : YawAlignmentMode.Smooth);
|
||||
physics.Yaw = TemplateHelper.SmoothYaw(physics.Yaw, targetYaw);
|
||||
physics.Pitch = TemplateHelper.SmoothPitch(physics.Pitch, targetPitch);
|
||||
|
||||
GroundedSegmentController.Apply(_segment, _nextSegment, pos, physics, input, world);
|
||||
|
|
|
|||
|
|
@ -15,10 +15,7 @@ internal static class ParkourFeasibility
|
|||
int yDelta)
|
||||
{
|
||||
double horiz = Math.Sqrt(xOffset * xOffset + zOffset * zOffset);
|
||||
if (yDelta <= 0)
|
||||
return true;
|
||||
|
||||
double threshold = 2.5;
|
||||
double threshold = yDelta > 0 ? 2.5 : 3.5;
|
||||
if (horiz < threshold)
|
||||
return true;
|
||||
|
||||
|
|
|
|||
|
|
@ -317,8 +317,6 @@ 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
|
|
@ -1,877 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,520 +0,0 @@
|
|||
# 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"
|
||||
```
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
# Jump-Entry Direct Yaw Design
|
||||
|
||||
## Context
|
||||
Recent pathing work exposed a consistent execution cost in sterile test worlds: jump-capable segments can spend several ticks rotating in place before they are willing to commit to the action. This is most visible in short parkour jumps and ascend chains started from opposite yaw, where the path is correct and no replan should happen, but execution still burns ticks on gradual heading convergence.
|
||||
|
||||
The current implementation uses `TemplateHelper.SmoothYaw(...)` across all movement templates. That smooth turn is not purely visual. Forward and back inputs are resolved using the current `physics.Yaw`, so intermediate yaw values change real acceleration, timing, and landing state. Because of that, replacing all yaw smoothing with snap rotation would be a behavior change across the whole execution system, not just a cosmetic cleanup.
|
||||
|
||||
The goal of this change is narrower: remove unnecessary yaw convergence cost only in states where the controller is explicitly trying to become jump-ready, while preserving current grounded braking, descend timing, climb centering, and final-stop settling behavior elsewhere.
|
||||
|
||||
## Requirements
|
||||
- Remove avoidable yaw-convergence tax from jump-entry states.
|
||||
- Preserve the current hard requirement of `0 replan` in sterile live test worlds.
|
||||
- Preserve planner behavior and existing path contracts.
|
||||
- Keep pitch smoothing unchanged.
|
||||
- Do not change normal `Walk`, `Descend`, `Climb`, or grounded final-stop semantics in the first pass.
|
||||
- Keep the existing guarded `PrepareJump` handoff behavior intact.
|
||||
|
||||
## Approaches
|
||||
|
||||
### 1. Global snap yaw in all templates
|
||||
Replace all `SmoothYaw(...)` calls with direct target yaw assignment.
|
||||
|
||||
Pros:
|
||||
- Simplest implementation model.
|
||||
- Removes all rotation latency.
|
||||
|
||||
Cons:
|
||||
- Changes grounded traversal, descent lip approach, climb centering, and turn braking at once.
|
||||
- Would invalidate current assumptions in templates that use heading penalty and gradual exit-heading bias as part of real motion control.
|
||||
- Too broad for the current bug and too risky for the current regression surface.
|
||||
|
||||
### 2. Phase-scoped direct yaw only in jump-entry states
|
||||
Keep smoothing by default, but explicitly snap yaw in states whose sole purpose is to prepare for a jump.
|
||||
|
||||
Pros:
|
||||
- Targets the observed cost directly.
|
||||
- Preserves current non-jump motion semantics.
|
||||
- Matches the theoretical intent: if the state is already waiting for jump-ready alignment, gradual turning is wasted time.
|
||||
|
||||
Cons:
|
||||
- Requires template-specific gating instead of one global rule.
|
||||
|
||||
This is the recommended approach.
|
||||
|
||||
### 3. Faster smoothing instead of snap
|
||||
Raise `MaxYawStepPerTick` or add a faster smoothing mode for some templates.
|
||||
|
||||
Pros:
|
||||
- Smaller conceptual jump from the current implementation.
|
||||
|
||||
Cons:
|
||||
- Keeps the same state model and the same basic failure mode, only with smaller delays.
|
||||
- Makes behavior harder to reason about because "how fast is fast enough" becomes another tuning problem.
|
||||
|
||||
This is not recommended for the first pass.
|
||||
|
||||
## Design
|
||||
|
||||
### Scope boundary
|
||||
The first pass should only change yaw behavior in jump-entry states:
|
||||
|
||||
- `SprintJumpTemplate` while approaching takeoff
|
||||
- `AscendTemplate` while aligning for jump commitment
|
||||
- `GroundedSegmentController` when freezing in place for `PrepareJump`
|
||||
- `WalkTemplate` only when the segment is a grounded jump-entry segment with `ExitHints.RequireJumpReady == true`
|
||||
|
||||
The first pass should not change:
|
||||
|
||||
- ordinary `WalkTemplate` traversal, turn, or final-stop control
|
||||
- `DescendTemplate`
|
||||
- `ClimbTemplate`
|
||||
- air control during jump flight
|
||||
- grounded landing recovery and final-stop braking after a jump
|
||||
|
||||
### Yaw policy model
|
||||
Add an explicit notion of yaw alignment mode at the helper layer, with two behaviors:
|
||||
|
||||
- `Smooth`
|
||||
- `Snap`
|
||||
|
||||
The helper should centralize the policy so templates do not open-code direct `physics.Yaw = targetYaw` in unrelated ways. Pitch should remain smooth.
|
||||
|
||||
The implementation does not need a broad architecture. A small helper API is enough, for example:
|
||||
|
||||
- `AlignYaw(current, target, mode)`
|
||||
- or a narrowly named helper such as `SnapYaw(target)`
|
||||
|
||||
The key contract is that templates opt into snap only when they are inside the jump-entry boundary above.
|
||||
|
||||
### Sprint jump behavior
|
||||
`SprintJumpTemplate` should use direct yaw alignment during `Phase.Approach`.
|
||||
|
||||
Why:
|
||||
- This phase already treats heading alignment as a hard precondition for jumping.
|
||||
- When started from opposite yaw, smooth turning creates pure startup tax before the jump can begin.
|
||||
- For short `FinalStop` jumps, this cost is disproportionately large relative to route time.
|
||||
|
||||
Effect:
|
||||
- `yawAligned` becomes immediately satisfiable once the state ticks.
|
||||
- The template can begin acceleration or jump commitment on the same tick rather than waiting several ticks for smooth convergence.
|
||||
- The recent short-jump air-brake latch remains unchanged and still handles landing-side overshoot.
|
||||
|
||||
### Ascend behavior
|
||||
`AscendTemplate` should use direct yaw alignment in the pre-jump phase, except for the existing grounded prepare-jump handoff carveout.
|
||||
|
||||
Why:
|
||||
- Ascend already waits for heading readiness before jumping.
|
||||
- The current opposite-yaw staircase spin is the same problem as short parkour: a jump state paying a gradual-turn tax before action.
|
||||
- The existing `groundedPrepareJumpHandoff` guard must still prevent double ownership of yaw at the moment control is handed to the next jump-ready segment.
|
||||
|
||||
Effect:
|
||||
- Opposite-yaw ascend starts become immediate.
|
||||
- Existing handoff protection remains intact.
|
||||
|
||||
### Grounded prepare-jump freeze
|
||||
When `GroundedSegmentController` enters its freeze-for-turn branch for `PrepareJump`, it should directly align to exit heading rather than smoothing.
|
||||
|
||||
Why:
|
||||
- In this branch, movement is already frozen.
|
||||
- There is no benefit to burning extra ticks on a smooth turn while stationary.
|
||||
- This is the cleanest place to remove residual turn latency for grounded jump handoffs.
|
||||
|
||||
### WalkTemplate jump-entry alignment
|
||||
`WalkTemplate` should keep smooth yaw for normal traversal. It should switch to direct yaw only for grounded segments that are explicitly preparing for a jump:
|
||||
|
||||
- `ExitTransition == PrepareJump`
|
||||
- `ExitHints.RequireJumpReady == true`
|
||||
|
||||
Why:
|
||||
- This is still part of the jump-entry pipeline, not ordinary path following.
|
||||
- Snap yaw here allows the segment to convert remaining forward ticks into the correct heading immediately, which better preserves exit-speed intent for the next jump.
|
||||
- Restricting this to jump-ready segments avoids changing ordinary traversal and braking behavior.
|
||||
|
||||
### Non-goals
|
||||
This change does not attempt to:
|
||||
|
||||
- remove all yaw smoothing from execution
|
||||
- re-tune descend, climb, or landing-recovery behavior
|
||||
- change planner costs or move admissibility
|
||||
- rewrite transition braking around direct yaw assumptions
|
||||
|
||||
## Expected effects
|
||||
|
||||
### Positive effects
|
||||
- Short opposite-yaw parkour and ascend starts should lose their upfront turn tax.
|
||||
- Repeated jump-entry chains should start more promptly.
|
||||
- Jump-ready handoff states should stop wasting ticks while frozen.
|
||||
|
||||
### Risks
|
||||
- Jump-entry segments may now redirect horizontal acceleration more abruptly.
|
||||
- A few jump-entry timing expectations may improve by 1 to 5 ticks and need contract updates only if they are stricter than reality.
|
||||
|
||||
These risks are acceptable because the affected scope is intentionally limited to states whose semantics are already "become jump-ready now."
|
||||
|
||||
## Validation
|
||||
- Unit tests:
|
||||
- `SprintJumpTemplate_TwoBlockGap_FinalStop_CompletesFromOppositeYawWithinTwentyTicks`
|
||||
- `AscendTemplate_PrepareJump_CompletesFromOppositeYawWithinTwentyTicks`
|
||||
- existing grounded convergence and sprint-jump scenario suites
|
||||
- Contract tests:
|
||||
- planner contracts remain unchanged
|
||||
- timing contracts are rerun and only updated if fresh evidence shows stable, improved timings
|
||||
- Live harnesses, sequentially:
|
||||
- `tools/test-pathing-jump-combos.sh`
|
||||
- `tools/test-pathing-long-routes.sh`
|
||||
- Success criteria:
|
||||
- no new replans in sterile routes
|
||||
- no regression in planner-selected long-jump chains
|
||||
- short opposite-yaw jump regressions stay green
|
||||
|
||||
## Delivery order
|
||||
1. Add the yaw-policy helper.
|
||||
2. Apply snap yaw to `SprintJumpTemplate` approach.
|
||||
3. Apply snap yaw to `AscendTemplate` pre-jump alignment.
|
||||
4. Apply snap yaw to `GroundedSegmentController` prepare-jump freeze.
|
||||
5. Apply gated snap yaw to `WalkTemplate` jump-entry alignment only.
|
||||
6. Rerun focused unit suites.
|
||||
7. Rerun live jump-combo and long-route harnesses sequentially.
|
||||
|
||||
## Open questions
|
||||
- None for the first pass. `Descend` and `Climb` are explicitly deferred until there is evidence that their current smoothing is the next limiting factor.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[
|
||||
{
|
||||
"case_id": "ceiling-headhitter-sprint-mm12-gap1-ceil4p0",
|
||||
"case_id": "ceiling-headhitter-sprint-mm12-gap3-ceil3p0",
|
||||
"bucket_id": "ceiling:headhitter:sprint:easy",
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
|
|
@ -9,35 +9,11 @@
|
|||
"difficulty_band": "easy",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "ceiling-headhitter",
|
||||
"gap_blocks": 1,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": 4.0,
|
||||
"wall_width": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 102.0,
|
||||
"y": 80.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "ceiling-headhitter-sprint-mm12-gap3-ceil2p0",
|
||||
"bucket_id": "ceiling:headhitter:sprint:boundary",
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "boundary",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "ceiling-headhitter",
|
||||
"gap_blocks": 3,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": 2.0,
|
||||
"ceiling_height": 3.0,
|
||||
"wall_width": null,
|
||||
"wall_offset": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
|
|
@ -50,7 +26,33 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"case_id": "ceiling-headhitter-sprint-mm12-gap4-ceil2p0",
|
||||
"case_id": "ceiling-headhitter-sprint-mm12-gap3-ceil2p5",
|
||||
"bucket_id": "ceiling:headhitter:sprint:boundary",
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "boundary",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "ceiling-headhitter",
|
||||
"gap_blocks": 3,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": 2.5,
|
||||
"wall_width": null,
|
||||
"wall_offset": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 104.0,
|
||||
"y": 80.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "ceiling-headhitter-sprint-mm12-gap1-ceil4p0",
|
||||
"bucket_id": "ceiling:headhitter:sprint:reject",
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
|
|
@ -59,10 +61,167 @@
|
|||
"difficulty_band": "reject",
|
||||
"expected_result": "reject",
|
||||
"world_recipe_id": "ceiling-headhitter",
|
||||
"gap_blocks": 1,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": 4.0,
|
||||
"wall_width": null,
|
||||
"wall_offset": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 102.0,
|
||||
"y": 80.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-ascend-sprint-mm12-gap3-dy1p0",
|
||||
"bucket_id": "linear:ascend:sprint:easy",
|
||||
"family": "linear",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "easy",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "linear-ascend",
|
||||
"gap_blocks": 3,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 104.0,
|
||||
"y": 81.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-ascend-sprint-mm12-gap0-dy1p0",
|
||||
"bucket_id": "linear:ascend:sprint:reject",
|
||||
"family": "linear",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "reject",
|
||||
"expected_result": "reject",
|
||||
"world_recipe_id": "linear-ascend",
|
||||
"gap_blocks": 0,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 101.0,
|
||||
"y": 81.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-descend-sprint-mm12-gap4-dym1p0",
|
||||
"bucket_id": "linear:descend:sprint:easy",
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "easy",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "linear-descend",
|
||||
"gap_blocks": 4,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 105.0,
|
||||
"y": 79.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-descend-sprint-mm12-gap5-dym1p0",
|
||||
"bucket_id": "linear:descend:sprint:boundary",
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "boundary",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "linear-descend",
|
||||
"gap_blocks": 5,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 106.0,
|
||||
"y": 79.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-descend-sprint-mm12-gap0-dym1p0",
|
||||
"bucket_id": "linear:descend:sprint:reject",
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "reject",
|
||||
"expected_result": "reject",
|
||||
"world_recipe_id": "linear-descend",
|
||||
"gap_blocks": 0,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 101.0,
|
||||
"y": 79.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-flat-sprint-mm12-gap4-dy0p0",
|
||||
"bucket_id": "linear:flat:sprint:easy",
|
||||
"family": "linear",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "easy",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "linear-flat",
|
||||
"gap_blocks": 4,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": 2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
|
|
@ -74,183 +233,8 @@
|
|||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-ascend-sprint-mm12-gap0-dy1p0",
|
||||
"bucket_id": "linear:ascend:sprint:easy",
|
||||
"family": "linear",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "easy",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "linear-ascend",
|
||||
"gap_blocks": 0,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 101.0,
|
||||
"y": 81.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-ascend-sprint-mm12-gap2-dy1p0",
|
||||
"bucket_id": "linear:ascend:sprint:boundary",
|
||||
"family": "linear",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "boundary",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "linear-ascend",
|
||||
"gap_blocks": 2,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 103.0,
|
||||
"y": 81.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-ascend-sprint-mm12-gap6-dy1p0",
|
||||
"bucket_id": "linear:ascend:sprint:reject",
|
||||
"family": "linear",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "reject",
|
||||
"expected_result": "reject",
|
||||
"world_recipe_id": "linear-ascend",
|
||||
"gap_blocks": 6,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 107.0,
|
||||
"y": 81.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-descend-sprint-mm12-gap0-dym2p0",
|
||||
"bucket_id": "linear:descend:sprint:easy",
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "easy",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "linear-descend",
|
||||
"gap_blocks": 0,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 101.0,
|
||||
"y": 78.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-descend-sprint-mm12-gap2-dym1p0",
|
||||
"bucket_id": "linear:descend:sprint:boundary",
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "boundary",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "linear-descend",
|
||||
"gap_blocks": 2,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 103.0,
|
||||
"y": 79.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-flat-sprint-mm12-gap0-dy0p0",
|
||||
"bucket_id": "linear:flat:sprint:easy",
|
||||
"family": "linear",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "easy",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "linear-flat",
|
||||
"gap_blocks": 0,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 101.0,
|
||||
"y": 80.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-flat-sprint-mm12-gap5-dy0p0",
|
||||
"bucket_id": "linear:flat:sprint:boundary",
|
||||
"family": "linear",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "boundary",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "linear-flat",
|
||||
"gap_blocks": 5,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 106.0,
|
||||
"y": 80.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "linear-flat-sprint-mm12-gap7-dy0p0",
|
||||
"bucket_id": "linear:flat:sprint:reject",
|
||||
"family": "linear",
|
||||
"subfamily": "flat",
|
||||
|
|
@ -259,17 +243,18 @@
|
|||
"difficulty_band": "reject",
|
||||
"expected_result": "reject",
|
||||
"world_recipe_id": "linear-flat",
|
||||
"gap_blocks": 7,
|
||||
"gap_blocks": 0,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 108.0,
|
||||
"x": 101.0,
|
||||
"y": 80.0,
|
||||
"z": 100.0
|
||||
}
|
||||
|
|
@ -288,6 +273,7 @@
|
|||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": 1,
|
||||
"wall_offset": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
|
|
@ -313,6 +299,7 @@
|
|||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": 4,
|
||||
"wall_offset": null,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
|
|
@ -323,5 +310,239 @@
|
|||
"y": 80.0,
|
||||
"z": 104.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "sidewall-ascend-sprint-mm12-gap3-dy1p0-wo1",
|
||||
"bucket_id": "sidewall:ascend:sprint:easy",
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "easy",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "sidewall-ascend",
|
||||
"gap_blocks": 3,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": 1,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 104.0,
|
||||
"y": 81.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "sidewall-ascend-sprint-mm12-gap3-dy1p0-wo0",
|
||||
"bucket_id": "sidewall:ascend:sprint:boundary",
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "boundary",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "sidewall-ascend",
|
||||
"gap_blocks": 3,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": 0,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 104.0,
|
||||
"y": 81.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "sidewall-ascend-sprint-mm12-gap0-dy1p0-wo0",
|
||||
"bucket_id": "sidewall:ascend:sprint:reject",
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "reject",
|
||||
"expected_result": "reject",
|
||||
"world_recipe_id": "sidewall-ascend",
|
||||
"gap_blocks": 0,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": 0,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 101.0,
|
||||
"y": 81.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "sidewall-descend-sprint-mm12-gap4-dym1p0-wo1",
|
||||
"bucket_id": "sidewall:descend:sprint:easy",
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "easy",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "sidewall-descend",
|
||||
"gap_blocks": 4,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": 1,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 105.0,
|
||||
"y": 79.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "sidewall-descend-sprint-mm12-gap5-dym1p0-wo0",
|
||||
"bucket_id": "sidewall:descend:sprint:boundary",
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "boundary",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "sidewall-descend",
|
||||
"gap_blocks": 5,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": 0,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 106.0,
|
||||
"y": 79.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "sidewall-descend-sprint-mm12-gap0-dym1p0-wo0",
|
||||
"bucket_id": "sidewall:descend:sprint:reject",
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "reject",
|
||||
"expected_result": "reject",
|
||||
"world_recipe_id": "sidewall-descend",
|
||||
"gap_blocks": 0,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": 0,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 101.0,
|
||||
"y": 79.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "sidewall-flat-sprint-mm12-gap4-dy0p0-wo1",
|
||||
"bucket_id": "sidewall:flat:sprint:easy",
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "easy",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "sidewall-flat",
|
||||
"gap_blocks": 4,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": 1,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 105.0,
|
||||
"y": 80.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "sidewall-flat-sprint-mm12-gap4-dy0p0-wo0",
|
||||
"bucket_id": "sidewall:flat:sprint:boundary",
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "boundary",
|
||||
"expected_result": "pass",
|
||||
"world_recipe_id": "sidewall-flat",
|
||||
"gap_blocks": 4,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": 0,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 105.0,
|
||||
"y": 80.0,
|
||||
"z": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"case_id": "sidewall-flat-sprint-mm12-gap0-dy0p0-wo0",
|
||||
"bucket_id": "sidewall:flat:sprint:reject",
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"momentum_ticks": 12,
|
||||
"difficulty_band": "reject",
|
||||
"expected_result": "reject",
|
||||
"world_recipe_id": "sidewall-flat",
|
||||
"gap_blocks": 0,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_width": null,
|
||||
"wall_offset": 0,
|
||||
"start": {
|
||||
"x": 100.5,
|
||||
"y": 80.0,
|
||||
"z": 100.5
|
||||
},
|
||||
"goal": {
|
||||
"x": 101.0,
|
||||
"y": 80.0,
|
||||
"z": 100.0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
834
tools/pathing_data/momentum-capabilities.json
Normal file
834
tools/pathing_data/momentum-capabilities.json
Normal file
|
|
@ -0,0 +1,834 @@
|
|||
[
|
||||
{
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 1,
|
||||
"max_reach": null,
|
||||
"delta_y": null,
|
||||
"ceiling_height": 1.8125,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 2,
|
||||
"max_mm": 12,
|
||||
"max_reach": 1,
|
||||
"delta_y": null,
|
||||
"ceiling_height": 1.8125,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 12,
|
||||
"max_reach": 1,
|
||||
"delta_y": null,
|
||||
"ceiling_height": 2.0,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 7,
|
||||
"max_reach": 2,
|
||||
"delta_y": null,
|
||||
"ceiling_height": 2.5,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 8,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": null,
|
||||
"ceiling_height": 2.5,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 1,
|
||||
"max_reach": 3,
|
||||
"delta_y": null,
|
||||
"ceiling_height": 3.0,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 2,
|
||||
"max_mm": 12,
|
||||
"max_reach": 4,
|
||||
"delta_y": null,
|
||||
"ceiling_height": 3.0,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 3,
|
||||
"delta_y": null,
|
||||
"ceiling_height": 4.0,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "ceiling",
|
||||
"subfamily": "headhitter",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 4,
|
||||
"delta_y": null,
|
||||
"ceiling_height": 4.0,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 2,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 1,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 2,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 4,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 5,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 1,
|
||||
"max_reach": 4,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 2,
|
||||
"max_mm": 12,
|
||||
"max_reach": 5,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 2,
|
||||
"max_reach": 3,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 3,
|
||||
"max_mm": 12,
|
||||
"max_reach": 4,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 2,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 3,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 4,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 1,
|
||||
"max_reach": 2,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "linear",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 2,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "neo",
|
||||
"subfamily": "neo",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "wall_width",
|
||||
"min_mm": 0,
|
||||
"max_mm": 1,
|
||||
"max_reach": 3,
|
||||
"delta_y": null,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "neo",
|
||||
"subfamily": "neo",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "wall_width",
|
||||
"min_mm": 2,
|
||||
"max_mm": 12,
|
||||
"max_reach": 4,
|
||||
"delta_y": null,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "neo",
|
||||
"subfamily": "neo",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "wall_width",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 1,
|
||||
"delta_y": null,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "neo",
|
||||
"subfamily": "neo",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "wall_width",
|
||||
"min_mm": 1,
|
||||
"max_mm": 5,
|
||||
"max_reach": 2,
|
||||
"delta_y": null,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "neo",
|
||||
"subfamily": "neo",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "wall_width",
|
||||
"min_mm": 6,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": null,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": null,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 2,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 2,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 1,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 2,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 1,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "ascend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 2,
|
||||
"delta_y": 1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 4,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 5,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 4,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 5,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 1,
|
||||
"max_reach": 4,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 2,
|
||||
"max_mm": 12,
|
||||
"max_reach": 5,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 1,
|
||||
"max_reach": 4,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 2,
|
||||
"max_mm": 12,
|
||||
"max_reach": 5,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 2,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 2,
|
||||
"max_reach": 3,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 3,
|
||||
"max_mm": 12,
|
||||
"max_reach": 4,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 2,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 2,
|
||||
"max_reach": 3,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 3,
|
||||
"max_mm": 12,
|
||||
"max_reach": 4,
|
||||
"delta_y": -2.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 2,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 2,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "descend",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": -1.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 3,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 4,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 0,
|
||||
"max_reach": 3,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "sprint",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 1,
|
||||
"max_mm": 12,
|
||||
"max_reach": 4,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 1,
|
||||
"max_reach": 2,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 2,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 0,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 0,
|
||||
"max_mm": 1,
|
||||
"max_reach": 2,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"family": "sidewall",
|
||||
"subfamily": "flat",
|
||||
"movement_mode": "walk",
|
||||
"capability_metric": "gap_blocks",
|
||||
"min_mm": 2,
|
||||
"max_mm": 12,
|
||||
"max_reach": 3,
|
||||
"delta_y": 0.0,
|
||||
"ceiling_height": null,
|
||||
"wall_offset": 1,
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
71
tools/pathing_data/momentum-capabilities.md
Normal file
71
tools/pathing_data/momentum-capabilities.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Momentum Capabilities
|
||||
|
||||
This file compresses the full theory matrix into `mm` breakpoint bands that can
|
||||
be consumed directly by the planner.
|
||||
|
||||
| family | subfamily | movement_mode | qualifiers | mm_range | reach |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ceiling | headhitter | sprint | ceil=1.8125 | 0..1 | max_gap=none |
|
||||
| ceiling | headhitter | sprint | ceil=1.8125 | 2..12 | max_gap=1 |
|
||||
| ceiling | headhitter | sprint | ceil=2.0 | 0..12 | max_gap=1 |
|
||||
| ceiling | headhitter | sprint | ceil=2.5 | 0..7 | max_gap=2 |
|
||||
| ceiling | headhitter | sprint | ceil=2.5 | 8..12 | max_gap=3 |
|
||||
| ceiling | headhitter | sprint | ceil=3.0 | 0..1 | max_gap=3 |
|
||||
| ceiling | headhitter | sprint | ceil=3.0 | 2..12 | max_gap=4 |
|
||||
| ceiling | headhitter | sprint | ceil=4.0 | 0..0 | max_gap=3 |
|
||||
| ceiling | headhitter | sprint | ceil=4.0 | 1..12 | max_gap=4 |
|
||||
| linear | ascend | sprint | dy=1.0 | 0..0 | max_gap=2 |
|
||||
| linear | ascend | sprint | dy=1.0 | 1..12 | max_gap=3 |
|
||||
| linear | ascend | walk | dy=1.0 | 0..0 | max_gap=1 |
|
||||
| linear | ascend | walk | dy=1.0 | 1..12 | max_gap=2 |
|
||||
| linear | descend | sprint | dy=-2.0 | 0..0 | max_gap=4 |
|
||||
| linear | descend | sprint | dy=-2.0 | 1..12 | max_gap=5 |
|
||||
| linear | descend | sprint | dy=-1.0 | 0..1 | max_gap=4 |
|
||||
| linear | descend | sprint | dy=-1.0 | 2..12 | max_gap=5 |
|
||||
| linear | descend | walk | dy=-2.0 | 0..2 | max_gap=3 |
|
||||
| linear | descend | walk | dy=-2.0 | 3..12 | max_gap=4 |
|
||||
| linear | descend | walk | dy=-1.0 | 0..0 | max_gap=2 |
|
||||
| linear | descend | walk | dy=-1.0 | 1..12 | max_gap=3 |
|
||||
| linear | flat | sprint | dy=0.0 | 0..0 | max_gap=3 |
|
||||
| linear | flat | sprint | dy=0.0 | 1..12 | max_gap=4 |
|
||||
| linear | flat | walk | dy=0.0 | 0..1 | max_gap=2 |
|
||||
| linear | flat | walk | dy=0.0 | 2..12 | max_gap=3 |
|
||||
| neo | neo | sprint | - | 0..1 | max_wall_width=3 |
|
||||
| neo | neo | sprint | - | 2..12 | max_wall_width=4 |
|
||||
| neo | neo | walk | - | 0..0 | max_wall_width=1 |
|
||||
| neo | neo | walk | - | 1..5 | max_wall_width=2 |
|
||||
| neo | neo | walk | - | 6..12 | max_wall_width=3 |
|
||||
| sidewall | ascend | sprint | dy=1.0, wo=0 | 0..0 | max_gap=2 |
|
||||
| sidewall | ascend | sprint | dy=1.0, wo=0 | 1..12 | max_gap=3 |
|
||||
| sidewall | ascend | sprint | dy=1.0, wo=1 | 0..0 | max_gap=2 |
|
||||
| sidewall | ascend | sprint | dy=1.0, wo=1 | 1..12 | max_gap=3 |
|
||||
| sidewall | ascend | walk | dy=1.0, wo=0 | 0..0 | max_gap=1 |
|
||||
| sidewall | ascend | walk | dy=1.0, wo=0 | 1..12 | max_gap=2 |
|
||||
| sidewall | ascend | walk | dy=1.0, wo=1 | 0..0 | max_gap=1 |
|
||||
| sidewall | ascend | walk | dy=1.0, wo=1 | 1..12 | max_gap=2 |
|
||||
| sidewall | descend | sprint | dy=-2.0, wo=0 | 0..0 | max_gap=4 |
|
||||
| sidewall | descend | sprint | dy=-2.0, wo=0 | 1..12 | max_gap=5 |
|
||||
| sidewall | descend | sprint | dy=-2.0, wo=1 | 0..0 | max_gap=4 |
|
||||
| sidewall | descend | sprint | dy=-2.0, wo=1 | 1..12 | max_gap=5 |
|
||||
| sidewall | descend | sprint | dy=-1.0, wo=0 | 0..1 | max_gap=4 |
|
||||
| sidewall | descend | sprint | dy=-1.0, wo=0 | 2..12 | max_gap=5 |
|
||||
| sidewall | descend | sprint | dy=-1.0, wo=1 | 0..1 | max_gap=4 |
|
||||
| sidewall | descend | sprint | dy=-1.0, wo=1 | 2..12 | max_gap=5 |
|
||||
| sidewall | descend | walk | dy=-2.0, wo=0 | 0..0 | max_gap=2 |
|
||||
| sidewall | descend | walk | dy=-2.0, wo=0 | 1..2 | max_gap=3 |
|
||||
| sidewall | descend | walk | dy=-2.0, wo=0 | 3..12 | max_gap=4 |
|
||||
| sidewall | descend | walk | dy=-2.0, wo=1 | 0..0 | max_gap=2 |
|
||||
| sidewall | descend | walk | dy=-2.0, wo=1 | 1..2 | max_gap=3 |
|
||||
| sidewall | descend | walk | dy=-2.0, wo=1 | 3..12 | max_gap=4 |
|
||||
| sidewall | descend | walk | dy=-1.0, wo=0 | 0..0 | max_gap=2 |
|
||||
| sidewall | descend | walk | dy=-1.0, wo=0 | 1..12 | max_gap=3 |
|
||||
| sidewall | descend | walk | dy=-1.0, wo=1 | 0..0 | max_gap=2 |
|
||||
| sidewall | descend | walk | dy=-1.0, wo=1 | 1..12 | max_gap=3 |
|
||||
| sidewall | flat | sprint | dy=0.0, wo=0 | 0..0 | max_gap=3 |
|
||||
| sidewall | flat | sprint | dy=0.0, wo=0 | 1..12 | max_gap=4 |
|
||||
| sidewall | flat | sprint | dy=0.0, wo=1 | 0..0 | max_gap=3 |
|
||||
| sidewall | flat | sprint | dy=0.0, wo=1 | 1..12 | max_gap=4 |
|
||||
| sidewall | flat | walk | dy=0.0, wo=0 | 0..1 | max_gap=2 |
|
||||
| sidewall | flat | walk | dy=0.0, wo=0 | 2..12 | max_gap=3 |
|
||||
| sidewall | flat | walk | dy=0.0, wo=1 | 0..1 | max_gap=2 |
|
||||
| sidewall | flat | walk | dy=0.0, wo=1 | 2..12 | max_gap=3 |
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,13 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
mcc_cmd_live() {
|
||||
if [[ -n "${SESSION:-}" ]]; then
|
||||
mcc-cmd --session "$SESSION" "$1"
|
||||
else
|
||||
mcc-cmd "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
manifest_cases_for_query() {
|
||||
local manifest_path="$1"
|
||||
local family_csv="$2"
|
||||
|
|
@ -64,41 +72,42 @@ run_test() {
|
|||
local name="$1"
|
||||
local start_x="$2" start_y="$3" start_z="$4"
|
||||
local dest_x="$5" dest_y="$6" dest_z="$7"
|
||||
local username="${USERNAME:-MCCBot}"
|
||||
|
||||
TEST_NUM=$((TEST_NUM + 1))
|
||||
echo ""
|
||||
echo "=== TEST $TEST_NUM: $name ==="
|
||||
echo " Start: ($start_x, $start_y, $start_z) -> Dest: ($dest_x, $dest_y, $dest_z)"
|
||||
|
||||
mcc-cmd "respawn" 2>/dev/null || true
|
||||
mcc_cmd_live "respawn" 2>/dev/null || true
|
||||
sleep 0.5
|
||||
mc-rcon "gamemode creative MCCBot" >/dev/null 2>&1
|
||||
mc-rcon "gamemode creative $username" >/dev/null 2>&1
|
||||
sleep 0.3
|
||||
mc-rcon "tp MCCBot ${start_x}.5 ${start_y} ${start_z}.5" >/dev/null 2>&1
|
||||
mc-rcon "tp $username ${start_x}.5 ${start_y} ${start_z}.5" >/dev/null 2>&1
|
||||
sleep 2
|
||||
mc-rcon "gamemode survival MCCBot" >/dev/null 2>&1
|
||||
mc-rcon "gamemode survival $username" >/dev/null 2>&1
|
||||
sleep 1
|
||||
|
||||
: > "$LOG"
|
||||
sleep 0.5
|
||||
|
||||
mcc-cmd "pathfind $dest_x $dest_y $dest_z"
|
||||
mcc_cmd_live "pathfind $dest_x $dest_y $dest_z"
|
||||
sleep 8
|
||||
|
||||
local a_star_result
|
||||
a_star_result=$(grep -a '\[A\*\]' "$LOG" | head -3 | sed 's/\x1b\[[0-9;]*m//g')
|
||||
a_star_result=$(grep -a '\[A\*\]' "$LOG" | head -3 | sed 's/\x1b\[[0-9;]*m//g' || true)
|
||||
|
||||
local path_exec
|
||||
path_exec=$(grep -a '\[PathExec\]' "$LOG" | sed 's/\x1b\[[0-9;]*m//g')
|
||||
path_exec=$(grep -a '\[PathExec\]' "$LOG" | sed 's/\x1b\[[0-9;]*m//g' || true)
|
||||
|
||||
local path_mgr
|
||||
path_mgr=$(grep -a '\[PathMgr\]' "$LOG" | sed 's/\x1b\[[0-9;]*m//g')
|
||||
path_mgr=$(grep -a '\[PathMgr\]' "$LOG" | sed 's/\x1b\[[0-9;]*m//g' || true)
|
||||
|
||||
local nav_segs
|
||||
nav_segs=$(grep -a '\[Navigate\].*seg' "$LOG" | sed 's/\x1b\[[0-9;]*m//g')
|
||||
nav_segs=$(grep -a '\[Navigate\].*seg' "$LOG" | sed 's/\x1b\[[0-9;]*m//g' || true)
|
||||
|
||||
local physics_line
|
||||
physics_line=$(grep -a '\[Physics\]' "$LOG" | tail -1 | sed 's/\x1b\[[0-9;]*m//g')
|
||||
physics_line=$(grep -a '\[Physics\]' "$LOG" | tail -1 | sed 's/\x1b\[[0-9;]*m//g' || true)
|
||||
|
||||
local result="invalid_live_case"
|
||||
if echo "$path_mgr" | grep -q "complete"; then
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ def _world_recipe_id(case: TheoryCase) -> str:
|
|||
return f"linear-{case.subfamily}"
|
||||
if case.family == "neo":
|
||||
return "neo-wall"
|
||||
if case.family == "sidewall":
|
||||
return f"sidewall-{case.subfamily}"
|
||||
return "ceiling-headhitter"
|
||||
|
||||
|
||||
|
|
@ -18,6 +20,14 @@ def _canonical_goal(case: TheoryCase) -> tuple[dict[str, float], dict[str, float
|
|||
if case.family == "neo":
|
||||
goal_z = 100 + (case.wall_width or 1)
|
||||
return start, {"x": 102.0, "y": 80.0, "z": float(goal_z)}
|
||||
if case.family == "sidewall":
|
||||
goal_y = 80.0 + (case.delta_y or 0.0)
|
||||
goal_x = 100 + (case.gap_blocks or 0) + 1
|
||||
wall_z_offset = 100 + 1 + (case.wall_offset or 0)
|
||||
return (
|
||||
{"x": 100.5, "y": 80.0, "z": 100.5},
|
||||
{"x": float(goal_x), "y": goal_y, "z": 100.0},
|
||||
)
|
||||
goal_x = 100 + (case.gap_blocks or 0) + 1
|
||||
return start, {"x": float(goal_x), "y": 80.0, "z": 100.0}
|
||||
|
||||
|
|
@ -27,7 +37,7 @@ def _select_boundary_case(
|
|||
subfamily: str,
|
||||
reachable: list[TheoryCase],
|
||||
) -> TheoryCase:
|
||||
if family == "linear":
|
||||
if family in {"linear", "sidewall"}:
|
||||
preferred_gap_by_subfamily = {
|
||||
"flat": 5,
|
||||
"ascend": 2,
|
||||
|
|
@ -105,6 +115,7 @@ def build_canonical_live_cases(cases: list[TheoryCase]) -> list[CanonicalLiveCas
|
|||
delta_y=case.delta_y,
|
||||
ceiling_height=case.ceiling_height,
|
||||
wall_width=case.wall_width,
|
||||
wall_offset=case.wall_offset,
|
||||
start=start,
|
||||
goal=goal,
|
||||
)
|
||||
|
|
|
|||
200
tools/pathing_theory/capabilities.py
Normal file
200
tools/pathing_theory/capabilities.py
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
from collections import defaultdict
|
||||
|
||||
from tools.pathing_theory.models import MomentumCapabilityBand, TheoryCase
|
||||
|
||||
|
||||
def _linear_group_key(case: TheoryCase) -> tuple[object, ...]:
|
||||
return (
|
||||
case.family,
|
||||
case.subfamily,
|
||||
case.movement_mode,
|
||||
"gap_blocks",
|
||||
case.delta_y,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _neo_group_key(case: TheoryCase) -> tuple[object, ...]:
|
||||
return (
|
||||
case.family,
|
||||
case.subfamily,
|
||||
case.movement_mode,
|
||||
"wall_width",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _ceiling_group_key(case: TheoryCase) -> tuple[object, ...]:
|
||||
return (
|
||||
case.family,
|
||||
case.subfamily,
|
||||
case.movement_mode,
|
||||
"gap_blocks",
|
||||
None,
|
||||
case.ceiling_height,
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def _sidewall_group_key(case: TheoryCase) -> tuple[object, ...]:
|
||||
return (
|
||||
case.family,
|
||||
case.subfamily,
|
||||
case.movement_mode,
|
||||
"gap_blocks",
|
||||
case.delta_y,
|
||||
None,
|
||||
case.wall_offset,
|
||||
)
|
||||
|
||||
|
||||
def _case_group_key(case: TheoryCase) -> tuple[object, ...]:
|
||||
if case.family == "linear":
|
||||
return _linear_group_key(case)
|
||||
if case.family == "neo":
|
||||
return _neo_group_key(case)
|
||||
if case.family == "ceiling":
|
||||
return _ceiling_group_key(case)
|
||||
if case.family == "sidewall":
|
||||
return _sidewall_group_key(case)
|
||||
raise ValueError(f"Unsupported theory family for capability bands: {case.family}")
|
||||
|
||||
|
||||
def _case_reach_value(case: TheoryCase) -> int | None:
|
||||
if not case.expected_reachable:
|
||||
return None
|
||||
if case.family in {"linear", "ceiling", "sidewall"}:
|
||||
return case.gap_blocks
|
||||
if case.family == "neo":
|
||||
return case.wall_width
|
||||
raise ValueError(f"Unsupported theory family for capability bands: {case.family}")
|
||||
|
||||
|
||||
def _compress_mm_ranges(
|
||||
mm_to_reach: list[tuple[int, int | None]],
|
||||
family: str,
|
||||
subfamily: str,
|
||||
movement_mode: str,
|
||||
capability_metric: str,
|
||||
delta_y: float | None,
|
||||
ceiling_height: float | None,
|
||||
wall_offset: int | None = None,
|
||||
) -> list[MomentumCapabilityBand]:
|
||||
bands: list[MomentumCapabilityBand] = []
|
||||
current_start = mm_to_reach[0][0]
|
||||
current_end = current_start
|
||||
current_reach = mm_to_reach[0][1]
|
||||
|
||||
for mm, max_reach in mm_to_reach[1:]:
|
||||
if max_reach == current_reach and mm == current_end + 1:
|
||||
current_end = mm
|
||||
continue
|
||||
|
||||
bands.append(
|
||||
MomentumCapabilityBand(
|
||||
family=family,
|
||||
subfamily=subfamily,
|
||||
movement_mode=movement_mode,
|
||||
capability_metric=capability_metric,
|
||||
min_mm=current_start,
|
||||
max_mm=current_end,
|
||||
max_reach=current_reach,
|
||||
delta_y=delta_y,
|
||||
ceiling_height=ceiling_height,
|
||||
wall_offset=wall_offset,
|
||||
)
|
||||
)
|
||||
current_start = mm
|
||||
current_end = mm
|
||||
current_reach = max_reach
|
||||
|
||||
bands.append(
|
||||
MomentumCapabilityBand(
|
||||
family=family,
|
||||
subfamily=subfamily,
|
||||
movement_mode=movement_mode,
|
||||
capability_metric=capability_metric,
|
||||
min_mm=current_start,
|
||||
max_mm=current_end,
|
||||
max_reach=current_reach,
|
||||
delta_y=delta_y,
|
||||
ceiling_height=ceiling_height,
|
||||
wall_offset=wall_offset,
|
||||
)
|
||||
)
|
||||
return bands
|
||||
|
||||
|
||||
def build_momentum_capability_bands(
|
||||
cases: list[TheoryCase],
|
||||
) -> list[MomentumCapabilityBand]:
|
||||
grouped_cases: dict[tuple[object, ...], list[TheoryCase]] = defaultdict(list)
|
||||
for case in cases:
|
||||
grouped_cases[_case_group_key(case)].append(case)
|
||||
|
||||
bands: list[MomentumCapabilityBand] = []
|
||||
for key in sorted(grouped_cases):
|
||||
family, subfamily, movement_mode, capability_metric, delta_y, ceiling_height, wall_offset = key
|
||||
mm_groups: dict[int, list[TheoryCase]] = defaultdict(list)
|
||||
for case in grouped_cases[key]:
|
||||
mm_groups[case.momentum_ticks].append(case)
|
||||
|
||||
mm_to_reach = [
|
||||
(
|
||||
mm,
|
||||
max(
|
||||
(
|
||||
_case_reach_value(case)
|
||||
for case in mm_groups[mm]
|
||||
if _case_reach_value(case) is not None
|
||||
),
|
||||
default=None,
|
||||
),
|
||||
)
|
||||
for mm in sorted(mm_groups)
|
||||
]
|
||||
bands.extend(
|
||||
_compress_mm_ranges(
|
||||
mm_to_reach=mm_to_reach,
|
||||
family=family,
|
||||
subfamily=subfamily,
|
||||
movement_mode=movement_mode,
|
||||
capability_metric=capability_metric,
|
||||
delta_y=delta_y,
|
||||
ceiling_height=ceiling_height,
|
||||
wall_offset=wall_offset,
|
||||
)
|
||||
)
|
||||
|
||||
return bands
|
||||
|
||||
|
||||
def _format_range(band: MomentumCapabilityBand) -> str:
|
||||
return f"{band.min_mm}..{band.max_mm}"
|
||||
|
||||
|
||||
def _format_reach(band: MomentumCapabilityBand) -> str:
|
||||
label = "max_gap" if band.capability_metric == "gap_blocks" else "max_wall_width"
|
||||
value = "none" if band.max_reach is None else str(band.max_reach)
|
||||
return f"{label}={value}"
|
||||
|
||||
|
||||
def format_momentum_capability_lines(
|
||||
bands: list[MomentumCapabilityBand],
|
||||
) -> list[str]:
|
||||
lines: list[str] = []
|
||||
for band in bands:
|
||||
parts = [band.family, band.subfamily, band.movement_mode]
|
||||
if band.delta_y is not None:
|
||||
parts.append(f"dy={band.delta_y}")
|
||||
if band.ceiling_height is not None:
|
||||
parts.append(f"ceil={band.ceiling_height}")
|
||||
if band.wall_offset is not None:
|
||||
parts.append(f"wo={band.wall_offset}")
|
||||
parts.append(f"mm={_format_range(band)}")
|
||||
parts.append(_format_reach(band))
|
||||
lines.append(" | ".join(parts))
|
||||
return lines
|
||||
|
|
@ -12,6 +12,7 @@ class TheoryCase:
|
|||
delta_y: float | None
|
||||
ceiling_height: float | None
|
||||
wall_width: int | None
|
||||
wall_offset: int | None
|
||||
expected_reachable: bool
|
||||
landing_x: float | None
|
||||
apex_y: float | None
|
||||
|
|
@ -19,6 +20,21 @@ class TheoryCase:
|
|||
notes: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MomentumCapabilityBand:
|
||||
family: str
|
||||
subfamily: str
|
||||
movement_mode: str
|
||||
capability_metric: str
|
||||
min_mm: int
|
||||
max_mm: int
|
||||
max_reach: int | None
|
||||
delta_y: float | None = None
|
||||
ceiling_height: float | None = None
|
||||
wall_offset: int | None = None
|
||||
notes: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CanonicalLiveCase:
|
||||
case_id: str
|
||||
|
|
@ -34,5 +50,6 @@ class CanonicalLiveCase:
|
|||
delta_y: float | None
|
||||
ceiling_height: float | None
|
||||
wall_width: int | None
|
||||
wall_offset: int | None
|
||||
start: dict[str, float]
|
||||
goal: dict[str, float]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
|
@ -21,6 +22,10 @@ HORIZONTAL_VELOCITY_THRESHOLD_SQR = 9.0e-6
|
|||
VERTICAL_VELOCITY_THRESHOLD = 0.003
|
||||
|
||||
HALF_WIDTH = PLAYER_WIDTH / 2.0
|
||||
# Reliable late-jump timing is slightly before the full 0.8 block walk-off limit.
|
||||
# Baritone uses a 0.7 threshold for the analogous 2-gap flat parkour execution.
|
||||
EDGE_TAKEOFF_X = 0.7
|
||||
TARGET_BLOCK_WIDTH = 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -28,8 +33,10 @@ class TickState:
|
|||
tick: int = 0
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
z: float = 0.0
|
||||
vx: float = 0.0
|
||||
vy: float = 0.0
|
||||
vz: float = 0.0
|
||||
on_ground: bool = True
|
||||
|
||||
|
||||
|
|
@ -38,46 +45,198 @@ def get_ground_speed(block_friction: float = DEFAULT_BLOCK_FRICTION) -> float:
|
|||
return MOVEMENT_SPEED * (GROUND_ACCEL_FACTOR / (friction * friction * friction))
|
||||
|
||||
|
||||
def build_momentum_velocity(
|
||||
momentum_ticks: int,
|
||||
block_friction: float = DEFAULT_BLOCK_FRICTION,
|
||||
) -> float:
|
||||
vx = 0.0
|
||||
ground_friction = block_friction * FRICTION_MULTIPLIER
|
||||
for _ in range(momentum_ticks):
|
||||
vx += INPUT_FRICTION * get_ground_speed(block_friction)
|
||||
vx *= ground_friction
|
||||
return vx
|
||||
|
||||
|
||||
def build_momentum_velocity_2d(
|
||||
momentum_ticks: int,
|
||||
yaw_rad: float,
|
||||
strafe_input: float = 0.0,
|
||||
block_friction: float = DEFAULT_BLOCK_FRICTION,
|
||||
wall_z: Optional[float] = None,
|
||||
start_z: float = 0.0,
|
||||
) -> tuple[float, float, float]:
|
||||
"""Build pre-jump velocity with yaw and optional strafe, returning (vx, vz, z).
|
||||
|
||||
Simulates ground ticks before the jump edge, accounting for yaw-split
|
||||
acceleration, optional strafe, and wall collision on the z axis.
|
||||
"""
|
||||
cos_yaw = math.cos(yaw_rad)
|
||||
sin_yaw = math.sin(yaw_rad)
|
||||
ground_friction = block_friction * FRICTION_MULTIPLIER
|
||||
ground_speed = get_ground_speed(block_friction)
|
||||
vx, vz, z = 0.0, 0.0, start_z
|
||||
|
||||
for _ in range(momentum_ticks):
|
||||
input_x = (cos_yaw + strafe_input * (-sin_yaw)) * INPUT_FRICTION
|
||||
input_z = (sin_yaw + strafe_input * cos_yaw) * INPUT_FRICTION
|
||||
vx += input_x * ground_speed
|
||||
vz += input_z * ground_speed
|
||||
|
||||
z += vz
|
||||
if wall_z is not None and z + HALF_WIDTH > wall_z:
|
||||
z = wall_z - HALF_WIDTH
|
||||
if vz > 0:
|
||||
vz = 0.0
|
||||
|
||||
vx *= ground_friction
|
||||
vz *= ground_friction
|
||||
|
||||
return vx, vz, z
|
||||
|
||||
|
||||
def _get_overlap_window(
|
||||
start_x: float,
|
||||
end_x: float,
|
||||
landing_x_start: float,
|
||||
landing_width: Optional[float],
|
||||
) -> Optional[tuple[float, float]]:
|
||||
min_center_x = landing_x_start - HALF_WIDTH
|
||||
max_center_x = (
|
||||
None if landing_width is None else landing_x_start + landing_width + HALF_WIDTH
|
||||
)
|
||||
|
||||
if start_x > end_x:
|
||||
start_x, end_x = end_x, start_x
|
||||
|
||||
delta_x = end_x - start_x
|
||||
if delta_x == 0.0:
|
||||
if start_x < min_center_x:
|
||||
return None
|
||||
if max_center_x is not None and start_x > max_center_x:
|
||||
return None
|
||||
return 0.0, 1.0
|
||||
|
||||
if end_x < min_center_x:
|
||||
return None
|
||||
|
||||
enter_t = 0.0 if start_x >= min_center_x else (min_center_x - start_x) / delta_x
|
||||
|
||||
if max_center_x is None:
|
||||
exit_t = 1.0
|
||||
else:
|
||||
if start_x > max_center_x:
|
||||
return None
|
||||
exit_t = 1.0 if end_x <= max_center_x else (max_center_x - start_x) / delta_x
|
||||
|
||||
if exit_t < 0.0 or enter_t > 1.0 or enter_t > exit_t:
|
||||
return None
|
||||
|
||||
return max(0.0, enter_t), min(1.0, exit_t)
|
||||
|
||||
|
||||
def _find_landing_contact(
|
||||
start_x: float,
|
||||
start_y: float,
|
||||
end_x: float,
|
||||
end_y: float,
|
||||
landing_y: float,
|
||||
landing_x_start: float,
|
||||
landing_width: Optional[float],
|
||||
) -> Optional[tuple[float, float]]:
|
||||
if start_y < landing_y or end_y > landing_y or start_y == end_y:
|
||||
return None
|
||||
|
||||
overlap_window = _get_overlap_window(
|
||||
start_x=start_x,
|
||||
end_x=end_x,
|
||||
landing_x_start=landing_x_start,
|
||||
landing_width=landing_width,
|
||||
)
|
||||
if overlap_window is None:
|
||||
return None
|
||||
|
||||
landing_t = (start_y - landing_y) / (start_y - end_y)
|
||||
enter_t, exit_t = overlap_window
|
||||
if landing_t < enter_t or landing_t > exit_t:
|
||||
return None
|
||||
|
||||
landing_x = start_x + (end_x - start_x) * landing_t
|
||||
return landing_x, landing_y
|
||||
|
||||
|
||||
def simulate_jump(
|
||||
sprint: bool = True,
|
||||
momentum_ticks: int = 12,
|
||||
ceiling_y: Optional[float] = None,
|
||||
landing_y: float = 0.0,
|
||||
landing_x_start: float = 0.0,
|
||||
landing_width: Optional[float] = None,
|
||||
max_ticks: int = 200,
|
||||
yaw_degrees: float = 0.0,
|
||||
strafe_input: float = 0.0,
|
||||
wall_z: Optional[float] = None,
|
||||
start_z: float = 0.0,
|
||||
) -> list[TickState]:
|
||||
x, y, vx, vy = 0.0, 0.0, 0.0, 0.0
|
||||
yaw_rad = math.radians(yaw_degrees)
|
||||
cos_yaw = math.cos(yaw_rad)
|
||||
sin_yaw = math.sin(yaw_rad)
|
||||
has_lateral = yaw_degrees != 0.0 or strafe_input != 0.0 or wall_z is not None
|
||||
|
||||
if has_lateral:
|
||||
vx, vz, z = build_momentum_velocity_2d(
|
||||
momentum_ticks, yaw_rad, strafe_input,
|
||||
wall_z=wall_z, start_z=start_z,
|
||||
)
|
||||
else:
|
||||
vx = build_momentum_velocity(momentum_ticks)
|
||||
vz = 0.0
|
||||
z = start_z
|
||||
|
||||
x, y, vy = EDGE_TAKEOFF_X, 0.0, 0.0
|
||||
on_ground = True
|
||||
trajectory: list[TickState] = []
|
||||
jumped = False
|
||||
ground_friction = DEFAULT_BLOCK_FRICTION * FRICTION_MULTIPLIER
|
||||
|
||||
trajectory.append(TickState(0, x, y, vx, vy, on_ground))
|
||||
trajectory.append(TickState(0, x, y, z, vx, vy, vz, on_ground))
|
||||
|
||||
for tick in range(1, max_ticks + 1):
|
||||
if vx * vx < HORIZONTAL_VELOCITY_THRESHOLD_SQR:
|
||||
if vx * vx + vz * vz < HORIZONTAL_VELOCITY_THRESHOLD_SQR:
|
||||
vx = 0.0
|
||||
vz = 0.0
|
||||
if abs(vy) < VERTICAL_VELOCITY_THRESHOLD:
|
||||
vy = 0.0
|
||||
|
||||
do_jump = False
|
||||
if not jumped and tick > momentum_ticks and on_ground:
|
||||
if not jumped and on_ground:
|
||||
do_jump = True
|
||||
jumped = True
|
||||
|
||||
if do_jump:
|
||||
vy = max(BASE_JUMP_POWER, vy)
|
||||
if sprint:
|
||||
vx += SPRINT_JUMP_HORIZONTAL_BOOST
|
||||
vx += SPRINT_JUMP_HORIZONTAL_BOOST * cos_yaw
|
||||
vz += SPRINT_JUMP_HORIZONTAL_BOOST * sin_yaw
|
||||
|
||||
forward_input = 1.0 * INPUT_FRICTION
|
||||
speed = get_ground_speed() if on_ground else AIR_ACCEL
|
||||
vx += forward_input * speed
|
||||
if has_lateral:
|
||||
input_x = (cos_yaw + strafe_input * (-sin_yaw)) * INPUT_FRICTION
|
||||
input_z = (sin_yaw + strafe_input * cos_yaw) * INPUT_FRICTION
|
||||
vx += input_x * speed
|
||||
vz += input_z * speed
|
||||
else:
|
||||
vx += INPUT_FRICTION * speed
|
||||
|
||||
new_x = x + vx
|
||||
new_y = y + vy
|
||||
new_z = z + vz
|
||||
new_on_ground = False
|
||||
|
||||
if wall_z is not None and new_z + HALF_WIDTH > wall_z:
|
||||
new_z = wall_z - HALF_WIDTH
|
||||
if vz > 0:
|
||||
vz = 0.0
|
||||
|
||||
if ceiling_y is not None:
|
||||
head_y = new_y + PLAYER_HEIGHT
|
||||
if head_y > ceiling_y:
|
||||
|
|
@ -85,39 +244,24 @@ def simulate_jump(
|
|||
if vy > 0:
|
||||
vy = 0.0
|
||||
|
||||
floor_y = 0.0 if new_x < landing_x_start else landing_y
|
||||
|
||||
if jumped:
|
||||
if new_x >= landing_x_start:
|
||||
if landing_y >= 0:
|
||||
if vy <= 0 and y >= landing_y and new_y <= landing_y:
|
||||
new_y = landing_y
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
elif vy <= 0 and new_y <= landing_y:
|
||||
new_y = landing_y
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
else:
|
||||
if new_y <= landing_y:
|
||||
new_y = landing_y
|
||||
if vy < 0:
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
|
||||
if not new_on_ground and new_x < landing_x_start and new_y <= floor_y:
|
||||
new_y = floor_y
|
||||
if vy < 0:
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
elif new_y <= 0.0:
|
||||
new_y = 0.0
|
||||
if vy < 0:
|
||||
contact = _find_landing_contact(
|
||||
start_x=x,
|
||||
start_y=y,
|
||||
end_x=new_x,
|
||||
end_y=new_y,
|
||||
landing_y=landing_y,
|
||||
landing_x_start=landing_x_start,
|
||||
landing_width=landing_width,
|
||||
)
|
||||
if contact is not None:
|
||||
new_x, new_y = contact
|
||||
vy = 0.0
|
||||
new_on_ground = True
|
||||
new_on_ground = True
|
||||
|
||||
x = new_x
|
||||
y = new_y
|
||||
z = new_z
|
||||
on_ground = new_on_ground
|
||||
|
||||
vy -= GRAVITY
|
||||
|
|
@ -125,10 +269,12 @@ def simulate_jump(
|
|||
|
||||
if on_ground:
|
||||
vx *= ground_friction
|
||||
vz *= ground_friction
|
||||
else:
|
||||
vx *= FRICTION_MULTIPLIER
|
||||
vz *= FRICTION_MULTIPLIER
|
||||
|
||||
trajectory.append(TickState(tick, x, y, vx, vy, on_ground))
|
||||
trajectory.append(TickState(tick, x, y, z, vx, vy, vz, on_ground))
|
||||
|
||||
if jumped and on_ground:
|
||||
break
|
||||
|
|
@ -142,6 +288,11 @@ def get_landing(
|
|||
landing_x_start: float = 0.0,
|
||||
momentum_ticks: int = 12,
|
||||
ceiling_y: Optional[float] = None,
|
||||
landing_width: Optional[float] = None,
|
||||
yaw_degrees: float = 0.0,
|
||||
strafe_input: float = 0.0,
|
||||
wall_z: Optional[float] = None,
|
||||
start_z: float = 0.0,
|
||||
) -> Optional[tuple[float, float]]:
|
||||
trajectory = simulate_jump(
|
||||
sprint=sprint,
|
||||
|
|
@ -149,6 +300,11 @@ def get_landing(
|
|||
ceiling_y=ceiling_y,
|
||||
landing_y=target_y,
|
||||
landing_x_start=landing_x_start,
|
||||
landing_width=landing_width,
|
||||
yaw_degrees=yaw_degrees,
|
||||
strafe_input=strafe_input,
|
||||
wall_z=wall_z,
|
||||
start_z=start_z,
|
||||
)
|
||||
was_air = False
|
||||
for state in trajectory:
|
||||
|
|
@ -189,7 +345,7 @@ def can_reach_gap(
|
|||
if dy > 1.252:
|
||||
return False, None, 0.0
|
||||
|
||||
needed_x = 0.5 + gap_blocks + HALF_WIDTH
|
||||
needed_x = 0.5 + gap_blocks - HALF_WIDTH
|
||||
landing_platform_start = 0.5 + gap_blocks
|
||||
|
||||
if gap_blocks == 0 and dy > 0:
|
||||
|
|
@ -200,6 +356,7 @@ def can_reach_gap(
|
|||
target_y=dy,
|
||||
landing_x_start=landing_platform_start,
|
||||
momentum_ticks=momentum_ticks,
|
||||
landing_width=TARGET_BLOCK_WIDTH,
|
||||
)
|
||||
if result is None:
|
||||
return False, None, needed_x
|
||||
|
|
@ -212,3 +369,74 @@ def can_reach_gap(
|
|||
return False, landing_x, needed_x
|
||||
|
||||
return True, landing_x, needed_x
|
||||
|
||||
|
||||
SIDE_WALL_YAW_SWEEP = [0.0, 3.0, 5.0, 8.0, 10.0]
|
||||
|
||||
|
||||
def can_reach_gap_with_side_wall(
|
||||
gap_blocks: int,
|
||||
dy: float,
|
||||
wall_offset: int,
|
||||
sprint: bool = True,
|
||||
momentum_ticks: int = 12,
|
||||
) -> tuple[bool, Optional[float], float]:
|
||||
"""Check gap reachability with a side wall parallel to the jump direction.
|
||||
|
||||
wall_offset=0 means the wall is flush with the platform edge (wall at z=1.0
|
||||
for a 1-wide platform centered at z=0.5). wall_offset=1 means one air block
|
||||
between the platform edge and the wall face.
|
||||
|
||||
Sweeps yaw angles from 0 to 10 degrees toward the wall to find the
|
||||
worst-case trajectory. Uses the most pessimistic result: if any realistic
|
||||
yaw angle causes a failure, the case is marked unreachable or gets a
|
||||
reduced margin. This models the real-world constraint where MCC's
|
||||
pathfinder can't guarantee perfect yaw alignment.
|
||||
"""
|
||||
if dy > 1.252:
|
||||
return False, None, 0.0
|
||||
|
||||
wall_z = 1.0 + wall_offset
|
||||
start_z = 0.5
|
||||
|
||||
clearance = wall_z - (start_z + HALF_WIDTH)
|
||||
if clearance < 0:
|
||||
return False, None, 0.0
|
||||
|
||||
needed_x = 0.5 + gap_blocks - HALF_WIDTH
|
||||
landing_platform_start = 0.5 + gap_blocks
|
||||
if gap_blocks == 0 and dy > 0:
|
||||
landing_platform_start = 0.5
|
||||
|
||||
worst_ok = True
|
||||
worst_landing_x: Optional[float] = None
|
||||
worst_margin: Optional[float] = None
|
||||
|
||||
for yaw in SIDE_WALL_YAW_SWEEP:
|
||||
result = get_landing(
|
||||
sprint=sprint,
|
||||
target_y=dy,
|
||||
landing_x_start=landing_platform_start,
|
||||
momentum_ticks=momentum_ticks,
|
||||
landing_width=TARGET_BLOCK_WIDTH,
|
||||
yaw_degrees=yaw,
|
||||
wall_z=wall_z,
|
||||
start_z=start_z,
|
||||
)
|
||||
|
||||
if result is None:
|
||||
return False, worst_landing_x, needed_x
|
||||
|
||||
landing_x, landing_y = result
|
||||
if abs(landing_y - dy) > 0.01:
|
||||
return False, landing_x, needed_x
|
||||
|
||||
if gap_blocks > 0 and landing_x < needed_x:
|
||||
return False, landing_x, needed_x
|
||||
|
||||
margin = landing_x - needed_x
|
||||
if worst_margin is None or margin < worst_margin:
|
||||
worst_margin = margin
|
||||
worst_landing_x = landing_x
|
||||
|
||||
return True, worst_landing_x, needed_x
|
||||
|
|
|
|||
|
|
@ -3,12 +3,18 @@ import json
|
|||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
|
||||
from tools.pathing_theory.models import CanonicalLiveCase, TheoryCase
|
||||
from tools.pathing_theory.capabilities import format_momentum_capability_lines
|
||||
from tools.pathing_theory.models import (
|
||||
CanonicalLiveCase,
|
||||
MomentumCapabilityBand,
|
||||
TheoryCase,
|
||||
)
|
||||
|
||||
|
||||
def write_theory_artifacts(
|
||||
cases: list[TheoryCase],
|
||||
canonical_cases: list[CanonicalLiveCase],
|
||||
capability_bands: list[MomentumCapabilityBand],
|
||||
output_dir: Path,
|
||||
) -> None:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
|
@ -17,6 +23,8 @@ def write_theory_artifacts(
|
|||
csv_path = output_dir / "theory-matrix.csv"
|
||||
md_path = output_dir / "theory-matrix.md"
|
||||
canonical_path = output_dir / "canonical-live-cases.json"
|
||||
capability_path = output_dir / "momentum-capabilities.json"
|
||||
capability_md_path = output_dir / "momentum-capabilities.md"
|
||||
|
||||
json_path.write_text(
|
||||
json.dumps([asdict(case) for case in cases], indent=2) + "\n",
|
||||
|
|
@ -26,6 +34,10 @@ def write_theory_artifacts(
|
|||
json.dumps([asdict(case) for case in canonical_cases], indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
capability_path.write_text(
|
||||
json.dumps([asdict(band) for band in capability_bands], indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with csv_path.open("w", encoding="utf-8", newline="") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=list(asdict(cases[0]).keys()))
|
||||
|
|
@ -38,8 +50,8 @@ def write_theory_artifacts(
|
|||
"",
|
||||
"## Canonical live coverage",
|
||||
"",
|
||||
"This file is generated from `tools/sim_jump_reach.py` and is the first-wave authority",
|
||||
"for theory-aligned linear, neo, and headhitter live suites.",
|
||||
"This file is generated from `tools/pathing_theory/simulator.py` and is the first-wave authority",
|
||||
"for theory-aligned linear, neo, headhitter, and sidewall live suites.",
|
||||
"",
|
||||
"| family | subfamily | movement_mode | case_id | expected_reachable | margin |",
|
||||
"| --- | --- | --- | --- | --- | --- |",
|
||||
|
|
@ -51,3 +63,29 @@ def write_theory_artifacts(
|
|||
)
|
||||
|
||||
md_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
capability_lines = [
|
||||
"# Momentum Capabilities",
|
||||
"",
|
||||
"This file compresses the full theory matrix into `mm` breakpoint bands that can",
|
||||
"be consumed directly by the planner.",
|
||||
"",
|
||||
"| family | subfamily | movement_mode | qualifiers | mm_range | reach |",
|
||||
"| --- | --- | --- | --- | --- | --- |",
|
||||
]
|
||||
for band, line in zip(capability_bands, format_momentum_capability_lines(capability_bands)):
|
||||
qualifiers: list[str] = []
|
||||
if band.delta_y is not None:
|
||||
qualifiers.append(f"dy={band.delta_y}")
|
||||
if band.ceiling_height is not None:
|
||||
qualifiers.append(f"ceil={band.ceiling_height}")
|
||||
if band.wall_offset is not None:
|
||||
qualifiers.append(f"wo={band.wall_offset}")
|
||||
capability_lines.append(
|
||||
f"| {band.family} | {band.subfamily} | {band.movement_mode} | "
|
||||
f"{', '.join(qualifiers) if qualifiers else '-'} | "
|
||||
f"{band.min_mm}..{band.max_mm} | "
|
||||
f"{line.split(' | ')[-1]} |"
|
||||
)
|
||||
|
||||
capability_md_path.write_text("\n".join(capability_lines) + "\n", encoding="utf-8")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
from tools.pathing_theory.models import TheoryCase
|
||||
from tools.pathing_theory.primitives import PLAYER_WIDTH, can_reach_gap, get_apex, get_landing
|
||||
from tools.pathing_theory.primitives import (
|
||||
PLAYER_WIDTH,
|
||||
TARGET_BLOCK_WIDTH,
|
||||
can_reach_gap,
|
||||
can_reach_gap_with_side_wall,
|
||||
get_apex,
|
||||
get_landing,
|
||||
)
|
||||
|
||||
MAX_MOMENTUM_TICKS = 12
|
||||
|
||||
|
||||
def _float_token(value: float) -> str:
|
||||
|
|
@ -9,111 +18,169 @@ def _float_token(value: float) -> str:
|
|||
def build_theory_cases() -> list[TheoryCase]:
|
||||
cases: list[TheoryCase] = []
|
||||
|
||||
for sprint, movement_mode, momentum_ticks in [
|
||||
(False, "walk", 12),
|
||||
(True, "sprint", 0),
|
||||
(True, "sprint", 12),
|
||||
for sprint, movement_mode in [
|
||||
(False, "walk"),
|
||||
(True, "sprint"),
|
||||
]:
|
||||
for gap in range(0, 8):
|
||||
for delta_y in [0.0, 1.0, -1.0, -2.0]:
|
||||
ok, landing_x, needed_x = can_reach_gap(
|
||||
gap_blocks=gap,
|
||||
dy=delta_y,
|
||||
sprint=sprint,
|
||||
momentum_ticks=momentum_ticks,
|
||||
)
|
||||
apex_y, _ = get_apex(sprint=sprint, momentum_ticks=momentum_ticks)
|
||||
subfamily = (
|
||||
"flat"
|
||||
if delta_y == 0.0
|
||||
else "ascend"
|
||||
if delta_y > 0.0
|
||||
else "descend"
|
||||
)
|
||||
for momentum_ticks in range(0, MAX_MOMENTUM_TICKS + 1):
|
||||
apex_y, _ = get_apex(sprint=sprint, momentum_ticks=momentum_ticks)
|
||||
for gap in range(0, 8):
|
||||
for delta_y in [0.0, 1.0, -1.0, -2.0]:
|
||||
ok, landing_x, needed_x = can_reach_gap(
|
||||
gap_blocks=gap,
|
||||
dy=delta_y,
|
||||
sprint=sprint,
|
||||
momentum_ticks=momentum_ticks,
|
||||
)
|
||||
subfamily = (
|
||||
"flat"
|
||||
if delta_y == 0.0
|
||||
else "ascend"
|
||||
if delta_y > 0.0
|
||||
else "descend"
|
||||
)
|
||||
cases.append(
|
||||
TheoryCase(
|
||||
case_id=(
|
||||
f"linear-{subfamily}-{movement_mode}-mm{momentum_ticks}"
|
||||
f"-gap{gap}-dy{_float_token(delta_y)}"
|
||||
),
|
||||
family="linear",
|
||||
subfamily=subfamily,
|
||||
movement_mode=movement_mode,
|
||||
momentum_ticks=momentum_ticks,
|
||||
gap_blocks=gap,
|
||||
delta_y=delta_y,
|
||||
ceiling_height=None,
|
||||
wall_width=None,
|
||||
wall_offset=None,
|
||||
expected_reachable=ok,
|
||||
landing_x=landing_x,
|
||||
apex_y=apex_y,
|
||||
margin=None if landing_x is None else landing_x - needed_x,
|
||||
)
|
||||
)
|
||||
|
||||
for sprint, movement_mode in [
|
||||
(False, "walk"),
|
||||
(True, "sprint"),
|
||||
]:
|
||||
for momentum_ticks in range(0, MAX_MOMENTUM_TICKS + 1):
|
||||
apex_y, _ = get_apex(sprint=sprint, momentum_ticks=momentum_ticks)
|
||||
landing = get_landing(
|
||||
sprint=sprint,
|
||||
target_y=0.0,
|
||||
landing_x_start=0.0,
|
||||
momentum_ticks=momentum_ticks,
|
||||
)
|
||||
for wall_width in [1, 2, 3, 4]:
|
||||
landing_x = None if landing is None else landing[0]
|
||||
needed_x = wall_width + PLAYER_WIDTH
|
||||
margin = None if landing_x is None else landing_x - needed_x
|
||||
cases.append(
|
||||
TheoryCase(
|
||||
case_id=(
|
||||
f"linear-{subfamily}-{movement_mode}-mm{momentum_ticks}"
|
||||
f"-gap{gap}-dy{_float_token(delta_y)}"
|
||||
),
|
||||
family="linear",
|
||||
subfamily=subfamily,
|
||||
case_id=f"neo-neo-{movement_mode}-mm{momentum_ticks}-wall{wall_width}",
|
||||
family="neo",
|
||||
subfamily="neo",
|
||||
movement_mode=movement_mode,
|
||||
momentum_ticks=momentum_ticks,
|
||||
gap_blocks=gap,
|
||||
delta_y=delta_y,
|
||||
gap_blocks=None,
|
||||
delta_y=0.0,
|
||||
ceiling_height=None,
|
||||
wall_width=None,
|
||||
expected_reachable=ok,
|
||||
wall_width=wall_width,
|
||||
wall_offset=None,
|
||||
expected_reachable=margin is not None and margin >= 0.0,
|
||||
landing_x=landing_x,
|
||||
apex_y=apex_y,
|
||||
margin=None if landing_x is None else landing_x - needed_x,
|
||||
margin=margin,
|
||||
)
|
||||
)
|
||||
|
||||
landing = get_landing(
|
||||
sprint=True,
|
||||
target_y=0.0,
|
||||
landing_x_start=0.0,
|
||||
momentum_ticks=12,
|
||||
)
|
||||
for wall_width in [1, 2, 3, 4]:
|
||||
landing_x = None if landing is None else landing[0]
|
||||
needed_x = wall_width + PLAYER_WIDTH
|
||||
margin = None if landing_x is None else landing_x - needed_x
|
||||
cases.append(
|
||||
TheoryCase(
|
||||
case_id=f"neo-neo-sprint-mm12-wall{wall_width}",
|
||||
family="neo",
|
||||
subfamily="neo",
|
||||
movement_mode="sprint",
|
||||
momentum_ticks=12,
|
||||
gap_blocks=None,
|
||||
delta_y=0.0,
|
||||
ceiling_height=None,
|
||||
wall_width=wall_width,
|
||||
expected_reachable=margin is not None and margin >= 0.0,
|
||||
landing_x=landing_x,
|
||||
apex_y=get_apex(sprint=True, momentum_ticks=12)[0],
|
||||
margin=margin,
|
||||
)
|
||||
)
|
||||
|
||||
for ceiling_height in [4.0, 3.0, 2.5, 2.0, 1.8125]:
|
||||
for gap in [1, 2, 3, 4]:
|
||||
landing = get_landing(
|
||||
for momentum_ticks in range(0, MAX_MOMENTUM_TICKS + 1):
|
||||
for ceiling_height in [4.0, 3.0, 2.5, 2.0, 1.8125]:
|
||||
apex_y, _ = get_apex(
|
||||
sprint=True,
|
||||
target_y=0.0,
|
||||
landing_x_start=0.5 + gap,
|
||||
momentum_ticks=12,
|
||||
momentum_ticks=momentum_ticks,
|
||||
ceiling_y=ceiling_height,
|
||||
)
|
||||
landing_x = None if landing is None else landing[0]
|
||||
needed_x = 0.5 + gap + (PLAYER_WIDTH / 2.0)
|
||||
margin = None if landing_x is None else landing_x - needed_x
|
||||
cases.append(
|
||||
TheoryCase(
|
||||
case_id=(
|
||||
f"ceiling-headhitter-sprint-mm12-gap{gap}"
|
||||
f"-ceil{str(ceiling_height).replace('.', 'p')}"
|
||||
),
|
||||
family="ceiling",
|
||||
subfamily="headhitter",
|
||||
movement_mode="sprint",
|
||||
momentum_ticks=12,
|
||||
gap_blocks=gap,
|
||||
delta_y=0.0,
|
||||
ceiling_height=ceiling_height,
|
||||
wall_width=None,
|
||||
expected_reachable=margin is not None and margin >= 0.0,
|
||||
landing_x=landing_x,
|
||||
apex_y=get_apex(
|
||||
sprint=True,
|
||||
momentum_ticks=12,
|
||||
ceiling_y=ceiling_height,
|
||||
)[0],
|
||||
margin=margin,
|
||||
for gap in [1, 2, 3, 4]:
|
||||
landing = get_landing(
|
||||
sprint=True,
|
||||
target_y=0.0,
|
||||
landing_x_start=0.5 + gap,
|
||||
momentum_ticks=momentum_ticks,
|
||||
ceiling_y=ceiling_height,
|
||||
landing_width=TARGET_BLOCK_WIDTH,
|
||||
)
|
||||
)
|
||||
landing_x = None if landing is None else landing[0]
|
||||
needed_x = 0.5 + gap - (PLAYER_WIDTH / 2.0)
|
||||
margin = None if landing_x is None else landing_x - needed_x
|
||||
cases.append(
|
||||
TheoryCase(
|
||||
case_id=(
|
||||
f"ceiling-headhitter-sprint-mm{momentum_ticks}-gap{gap}"
|
||||
f"-ceil{str(ceiling_height).replace('.', 'p')}"
|
||||
),
|
||||
family="ceiling",
|
||||
subfamily="headhitter",
|
||||
movement_mode="sprint",
|
||||
momentum_ticks=momentum_ticks,
|
||||
gap_blocks=gap,
|
||||
delta_y=0.0,
|
||||
ceiling_height=ceiling_height,
|
||||
wall_width=None,
|
||||
wall_offset=None,
|
||||
expected_reachable=margin is not None and margin >= 0.0,
|
||||
landing_x=landing_x,
|
||||
apex_y=apex_y,
|
||||
margin=margin,
|
||||
)
|
||||
)
|
||||
|
||||
# --- Side-wall jump family ---
|
||||
for sprint, movement_mode in [
|
||||
(False, "walk"),
|
||||
(True, "sprint"),
|
||||
]:
|
||||
for momentum_ticks in range(0, MAX_MOMENTUM_TICKS + 1):
|
||||
apex_y, _ = get_apex(sprint=sprint, momentum_ticks=momentum_ticks)
|
||||
for wall_offset in [0, 1]:
|
||||
for gap in range(0, 8):
|
||||
for delta_y in [0.0, 1.0, -1.0, -2.0]:
|
||||
ok, landing_x, needed_x = can_reach_gap_with_side_wall(
|
||||
gap_blocks=gap,
|
||||
dy=delta_y,
|
||||
wall_offset=wall_offset,
|
||||
sprint=sprint,
|
||||
momentum_ticks=momentum_ticks,
|
||||
)
|
||||
subfamily = (
|
||||
"flat"
|
||||
if delta_y == 0.0
|
||||
else "ascend"
|
||||
if delta_y > 0.0
|
||||
else "descend"
|
||||
)
|
||||
cases.append(
|
||||
TheoryCase(
|
||||
case_id=(
|
||||
f"sidewall-{subfamily}-{movement_mode}-mm{momentum_ticks}"
|
||||
f"-gap{gap}-dy{_float_token(delta_y)}-wo{wall_offset}"
|
||||
),
|
||||
family="sidewall",
|
||||
subfamily=subfamily,
|
||||
movement_mode=movement_mode,
|
||||
momentum_ticks=momentum_ticks,
|
||||
gap_blocks=gap,
|
||||
delta_y=delta_y,
|
||||
ceiling_height=None,
|
||||
wall_width=None,
|
||||
wall_offset=wall_offset,
|
||||
expected_reachable=ok,
|
||||
landing_x=landing_x,
|
||||
apex_y=apex_y,
|
||||
margin=None if landing_x is None else landing_x - needed_x,
|
||||
)
|
||||
)
|
||||
|
||||
return cases
|
||||
|
|
|
|||
|
|
@ -23,10 +23,15 @@ import sys
|
|||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
from tools.pathing_theory.capabilities import (
|
||||
build_momentum_capability_bands,
|
||||
format_momentum_capability_lines,
|
||||
)
|
||||
from tools.pathing_theory.canonical import build_canonical_live_cases
|
||||
from tools.pathing_theory.primitives import (
|
||||
PLAYER_WIDTH,
|
||||
can_reach_gap,
|
||||
can_reach_gap_with_side_wall,
|
||||
get_apex,
|
||||
get_landing,
|
||||
simulate_jump,
|
||||
|
|
@ -179,30 +184,67 @@ def analyze_all(verbose: bool = False) -> list[dict]:
|
|||
else:
|
||||
print(f" {ceil:>7.4f}b {'N/A':>12}")
|
||||
|
||||
# --- Part 6: Verbose ---
|
||||
# --- Part 6: Side-wall jumps ---
|
||||
print(f"\n[6] Side-Wall Jump Feasibility (Sprint, 12t momentum)")
|
||||
print(f" Wall parallel to jump direction, flush with platform edge (wo=0)")
|
||||
print(f" Yaw sweep 0-10 deg, worst-case used.")
|
||||
print()
|
||||
|
||||
dy_values_sw = [1.0, 0.0, -1.0, -2.0]
|
||||
header_sw = f" {'Gap':>4}"
|
||||
for dy in dy_values_sw:
|
||||
sign = "+" if dy > 0 else ""
|
||||
header_sw += f" {sign}{dy:>5.1f}(L) {sign}{dy:>5.1f}(W)"
|
||||
print(header_sw)
|
||||
print(f" {'----':>4}" + " ---------- ----------" * len(dy_values_sw))
|
||||
|
||||
for gap in range(0, 7):
|
||||
row = f" {gap:>4}"
|
||||
for dy in dy_values_sw:
|
||||
ok_lin, _, _ = can_reach_gap(gap, dy, sprint=True, momentum_ticks=12)
|
||||
ok_sw, _, _ = can_reach_gap_with_side_wall(
|
||||
gap, dy, wall_offset=0, sprint=True, momentum_ticks=12,
|
||||
)
|
||||
lin_str = "YES" if ok_lin else "no"
|
||||
sw_str = "YES" if ok_sw else "no"
|
||||
marker = " " if ok_lin == ok_sw else "*"
|
||||
row += f" {lin_str:>6} {sw_str:>6}{marker}"
|
||||
print(row)
|
||||
|
||||
print()
|
||||
print(" L=linear (no wall), W=wall (wo=0), *=reachability differs")
|
||||
|
||||
# --- Part 7: Verbose ---
|
||||
if verbose:
|
||||
for label, sp in [("Sprint", True), ("Walk", False)]:
|
||||
print(f"\n[V] {label} Jump Trajectory (12t momentum, flat)")
|
||||
print(f" {'Tick':>4} {'X':>10} {'Y':>10} {'VX':>10} {'VY':>10} {'Gnd':>5}")
|
||||
print(f" {'Tick':>4} {'X':>10} {'Y':>10} {'Z':>10} {'VX':>10} {'VY':>10} {'VZ':>10} {'Gnd':>5}")
|
||||
traj = simulate_jump(sprint=sp, momentum_ticks=12, landing_y=0.0)
|
||||
for s in traj:
|
||||
g = "G" if s.on_ground else ""
|
||||
print(f" {s.tick:>4} {s.x:>10.4f} {s.y:>10.4f} "
|
||||
f"{s.vx:>10.6f} {s.vy:>10.6f} {g:>5}")
|
||||
print(f" {s.tick:>4} {s.x:>10.4f} {s.y:>10.4f} {s.z:>10.4f} "
|
||||
f"{s.vx:>10.6f} {s.vy:>10.6f} {s.vz:>10.6f} {g:>5}")
|
||||
|
||||
# +1 ascending sprint jump
|
||||
print(f"\n[V] Sprint +1 Ascending Trajectory (12t mm, gap=1)")
|
||||
print(f" {'Tick':>4} {'X':>10} {'Y':>10} {'VX':>10} {'VY':>10} {'Gnd':>5}")
|
||||
traj = simulate_jump(sprint=True, momentum_ticks=12,
|
||||
landing_y=1.0, landing_x_start=1.5)
|
||||
print(f"\n[V] Sprint flat with side wall (12t mm, yaw=10, wo=0)")
|
||||
print(f" {'Tick':>4} {'X':>10} {'Y':>10} {'Z':>10} {'VX':>10} {'VY':>10} {'VZ':>10} {'Gnd':>5}")
|
||||
traj = simulate_jump(sprint=True, momentum_ticks=12, landing_y=0.0,
|
||||
landing_x_start=4.5, landing_width=1.0,
|
||||
yaw_degrees=10.0, wall_z=1.0, start_z=0.5)
|
||||
for s in traj:
|
||||
g = "G" if s.on_ground else ""
|
||||
print(f" {s.tick:>4} {s.x:>10.4f} {s.y:>10.4f} "
|
||||
f"{s.vx:>10.6f} {s.vy:>10.6f} {g:>5}")
|
||||
print(f" {s.tick:>4} {s.x:>10.4f} {s.y:>10.4f} {s.z:>10.4f} "
|
||||
f"{s.vx:>10.6f} {s.vy:>10.6f} {s.vz:>10.6f} {g:>5}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def list_momentum_capabilities() -> None:
|
||||
cases = build_theory_cases()
|
||||
bands = build_momentum_capability_bands(cases)
|
||||
for line in format_momentum_capability_lines(bands):
|
||||
print(line)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Minecraft jump reachability simulator (Java 1.14+)")
|
||||
|
|
@ -212,15 +254,27 @@ def main():
|
|||
help="Export results to CSV file")
|
||||
parser.add_argument("--write-artifacts", type=str, default=None,
|
||||
help="Write tracked theory artifacts to a directory")
|
||||
parser.add_argument("--list-capabilities", action="store_true",
|
||||
help="List compressed mm breakpoint capabilities")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.write_artifacts:
|
||||
cases = build_theory_cases()
|
||||
canonical_cases = build_canonical_live_cases(cases)
|
||||
write_theory_artifacts(cases, canonical_cases, Path(args.write_artifacts))
|
||||
capability_bands = build_momentum_capability_bands(cases)
|
||||
write_theory_artifacts(
|
||||
cases,
|
||||
canonical_cases,
|
||||
capability_bands,
|
||||
Path(args.write_artifacts),
|
||||
)
|
||||
print(f"Wrote theory artifacts to {args.write_artifacts}")
|
||||
return
|
||||
|
||||
if args.list_capabilities:
|
||||
list_momentum_capabilities()
|
||||
return
|
||||
|
||||
results = analyze_all(verbose=args.verbose)
|
||||
|
||||
if args.csv and results:
|
||||
|
|
|
|||
869
tools/test-parkour.py
Normal file
869
tools/test-parkour.py
Normal file
|
|
@ -0,0 +1,869 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Full-coverage parkour test suite for MCC pathfinding.
|
||||
|
||||
Reads momentum-capabilities.json to derive a test matrix, builds multi-segment
|
||||
jump courses via RCON, and verifies MCC can navigate (or correctly reject) each
|
||||
one. Stops testing larger gaps once the first failure is seen per group.
|
||||
|
||||
Usage:
|
||||
source tools/mcc-env.sh
|
||||
python3 tools/test-parkour.py [OPTIONS]
|
||||
|
||||
Options:
|
||||
--list-cases Print test matrix and exit
|
||||
--dry-run Build courses only, do not navigate
|
||||
--filter PATTERN Hierarchical filter (see examples below)
|
||||
--username NAME MCC username (default: MCCBot)
|
||||
--rcon-port PORT RCON port (default: 25575)
|
||||
--rcon-password PASS RCON password (default: test123)
|
||||
--wait SECONDS Seconds to wait per navigation (default: 15)
|
||||
--results PATH Write JSONL results to PATH
|
||||
|
||||
Filter examples:
|
||||
--filter linear All linear tests
|
||||
--filter linear/flat Only linear flat (dy=0)
|
||||
--filter linear/ascend Only linear ascend (dy>0)
|
||||
--filter linear/descend/dy-1 Linear descend, dy=-1 only
|
||||
--filter neo All neo tests
|
||||
--filter ceiling All ceiling tests
|
||||
--filter ceiling/headhitter/ceil2.5 Ceiling with height 2.5
|
||||
--filter linear-flat-gap4 Exact case_id match
|
||||
|
||||
Multiple filters: --filter linear,neo
|
||||
|
||||
Note: sidewall family is excluded by default (identical max_reach to linear,
|
||||
wall does not affect A* block-level pathfinding).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
CAPABILITIES_PATH = REPO_ROOT / "tools" / "pathing_data" / "momentum-capabilities.json"
|
||||
|
||||
SEGMENTS = 3
|
||||
CLEAR_MARGIN = 7
|
||||
LINEAR_RUNWAY = 4
|
||||
SIDEWALL_RUNWAY = 2
|
||||
|
||||
CEILING_HEIGHTS_TO_TEST = [2.0, 2.5, 4.0]
|
||||
|
||||
# Families whose A* max_reach is identical to linear (wall doesn't affect
|
||||
# pathfinder block-level decisions). Excluded from the default matrix.
|
||||
SKIP_FAMILIES = {"sidewall"}
|
||||
|
||||
NEO_LANDING_GAP = 3 # blocks between wall-end and next wall-start (1 air + platform + 1 air)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RCON client
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class RconClient:
|
||||
def __init__(self, host: str = "localhost", port: int = 25575, password: str = "test123"):
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._password = password
|
||||
self._sock: Optional[socket.socket] = None
|
||||
|
||||
def connect(self) -> None:
|
||||
self._sock = socket.socket()
|
||||
self._sock.settimeout(5)
|
||||
self._sock.connect((self._host, self._port))
|
||||
self._send(1, 3, self._password)
|
||||
resp = self._recv()
|
||||
rid = struct.unpack("<i", resp[:4])[0]
|
||||
if rid == -1:
|
||||
raise RuntimeError("RCON auth failed")
|
||||
|
||||
def command(self, cmd: str) -> str:
|
||||
if self._sock is None:
|
||||
self.connect()
|
||||
self._send(2, 2, cmd)
|
||||
resp = self._recv()
|
||||
return resp[8:-2].decode(errors="replace")
|
||||
|
||||
def close(self) -> None:
|
||||
if self._sock:
|
||||
self._sock.close()
|
||||
self._sock = None
|
||||
|
||||
def _send(self, req_id: int, pkt_type: int, body: str) -> None:
|
||||
encoded = body.encode()
|
||||
header = struct.pack("<iii", 10 + len(encoded), req_id, pkt_type)
|
||||
assert self._sock is not None
|
||||
self._sock.send(header + encoded + b"\x00\x00")
|
||||
|
||||
def _recv(self) -> bytes:
|
||||
assert self._sock is not None
|
||||
length_data = b""
|
||||
while len(length_data) < 4:
|
||||
length_data += self._sock.recv(4 - len(length_data))
|
||||
length = struct.unpack("<i", length_data)[0]
|
||||
data = b""
|
||||
while len(data) < length:
|
||||
data += self._sock.recv(length - len(data))
|
||||
return data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MCC command interface
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class MccClient:
|
||||
def __init__(self, session: str):
|
||||
self.session = session
|
||||
session_root = Path(os.environ.get("TMPDIR", "/tmp")) / "mcc-debug" / session
|
||||
self.input_file = session_root / "mcc_input.txt"
|
||||
self.log_file = session_root / "mcc-debug.log"
|
||||
|
||||
def send(self, command: str) -> None:
|
||||
self.input_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self.input_file.open("a") as f:
|
||||
f.write(command + "\n")
|
||||
|
||||
def clear_log(self) -> None:
|
||||
with self.log_file.open("w") as f:
|
||||
f.truncate(0)
|
||||
|
||||
def read_log(self) -> str:
|
||||
if not self.log_file.exists():
|
||||
return ""
|
||||
return self.log_file.read_text(errors="replace")
|
||||
|
||||
def strip_ansi(self, text: str) -> str:
|
||||
return re.sub(r"\x1b\[[0-9;]*m", "", text)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test matrix generation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class TestCase:
|
||||
case_id: str
|
||||
family: str
|
||||
subfamily: str
|
||||
gap_or_wall: int
|
||||
delta_y: float
|
||||
ceiling_height: Optional[float]
|
||||
wall_offset: Optional[int]
|
||||
expected: str # "pass" or "reject"
|
||||
|
||||
def group_key(self) -> tuple:
|
||||
"""Key for stop-at-first-failure grouping."""
|
||||
return (self.family, self.subfamily, self.delta_y,
|
||||
self.ceiling_height, self.wall_offset)
|
||||
|
||||
def label(self) -> str:
|
||||
return self.case_id
|
||||
|
||||
|
||||
def load_capabilities(path: Path) -> list[dict]:
|
||||
with path.open() as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def derive_test_matrix(caps: list[dict]) -> list[TestCase]:
|
||||
"""Derive the test matrix from momentum capabilities.
|
||||
|
||||
For each unique (family, subfamily, qualifiers) combination, finds the
|
||||
global max_reach across all movement modes and momentum ranges, then
|
||||
generates gap values from 0 to max_reach+1. The max_reach+1 case is the
|
||||
sole expected-reject case per group.
|
||||
"""
|
||||
grouped: dict[tuple, int] = {}
|
||||
|
||||
for band in caps:
|
||||
family = band["family"]
|
||||
subfamily = band["subfamily"]
|
||||
metric = band["capability_metric"]
|
||||
reach = band["max_reach"]
|
||||
if reach is None:
|
||||
continue
|
||||
|
||||
dy = band.get("delta_y")
|
||||
ceil = band.get("ceiling_height")
|
||||
wo = band.get("wall_offset")
|
||||
|
||||
if family == "ceiling":
|
||||
if ceil not in CEILING_HEIGHTS_TO_TEST:
|
||||
continue
|
||||
|
||||
if family in SKIP_FAMILIES:
|
||||
continue
|
||||
|
||||
key = (family, subfamily, dy, ceil, wo, metric)
|
||||
if key not in grouped or reach > grouped[key]:
|
||||
grouped[key] = reach
|
||||
|
||||
cases: list[TestCase] = []
|
||||
for key, max_reach in sorted(grouped.items()):
|
||||
family, subfamily, dy, ceil, wo, metric = key
|
||||
|
||||
for value in range(0, max_reach + 2):
|
||||
expected = "pass" if value <= max_reach else "reject"
|
||||
|
||||
qualifier_parts = []
|
||||
if dy is not None and dy != 0.0:
|
||||
qualifier_parts.append(f"dy{dy:+.0f}")
|
||||
if ceil is not None:
|
||||
qualifier_parts.append(f"ceil{ceil}")
|
||||
if wo is not None:
|
||||
qualifier_parts.append(f"wo{wo}")
|
||||
|
||||
qualifier_str = "-".join(qualifier_parts) if qualifier_parts else ""
|
||||
value_label = f"gap{value}" if metric == "gap_blocks" else f"wall{value}"
|
||||
parts = [family, subfamily, value_label]
|
||||
if qualifier_str:
|
||||
parts.append(qualifier_str)
|
||||
case_id = "-".join(parts)
|
||||
|
||||
cases.append(TestCase(
|
||||
case_id=case_id,
|
||||
family=family,
|
||||
subfamily=subfamily,
|
||||
gap_or_wall=value,
|
||||
delta_y=dy if dy is not None else 0.0,
|
||||
ceiling_height=ceil,
|
||||
wall_offset=wo,
|
||||
expected=expected,
|
||||
))
|
||||
|
||||
return cases
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Filtering
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def matches_filter(case: TestCase, pattern: str) -> bool:
|
||||
"""Check if a test case matches a hierarchical filter pattern.
|
||||
|
||||
Supports:
|
||||
- Exact case_id match: "linear-flat-gap4"
|
||||
- Family: "linear"
|
||||
- Family/subfamily: "linear/flat"
|
||||
- With qualifiers: "linear/descend/dy-1", "sidewall/flat/wo0",
|
||||
"ceiling/headhitter/ceil2.5"
|
||||
"""
|
||||
if case.case_id == pattern:
|
||||
return True
|
||||
|
||||
parts = pattern.split("/")
|
||||
|
||||
if parts[0] != case.family:
|
||||
return False
|
||||
if len(parts) == 1:
|
||||
return True
|
||||
|
||||
if parts[1] != case.subfamily:
|
||||
return False
|
||||
if len(parts) == 2:
|
||||
return True
|
||||
|
||||
for qualifier in parts[2:]:
|
||||
q_lower = qualifier.lower()
|
||||
matched = False
|
||||
|
||||
if q_lower.startswith("dy"):
|
||||
try:
|
||||
target_dy = float(q_lower[2:])
|
||||
if case.delta_y == target_dy:
|
||||
matched = True
|
||||
except ValueError:
|
||||
pass
|
||||
elif q_lower.startswith("ceil"):
|
||||
try:
|
||||
target_ceil = float(q_lower[4:])
|
||||
if case.ceiling_height == target_ceil:
|
||||
matched = True
|
||||
except ValueError:
|
||||
pass
|
||||
elif q_lower.startswith("wo"):
|
||||
try:
|
||||
target_wo = int(q_lower[2:])
|
||||
if case.wall_offset == target_wo:
|
||||
matched = True
|
||||
except ValueError:
|
||||
pass
|
||||
elif q_lower.startswith("gap"):
|
||||
try:
|
||||
target_gap = int(q_lower[3:])
|
||||
if case.gap_or_wall == target_gap:
|
||||
matched = True
|
||||
except ValueError:
|
||||
pass
|
||||
elif q_lower.startswith("wall"):
|
||||
try:
|
||||
target_wall = int(q_lower[4:])
|
||||
if case.gap_or_wall == target_wall:
|
||||
matched = True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not matched:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def apply_filters(cases: list[TestCase], filter_str: str) -> list[TestCase]:
|
||||
"""Apply comma-separated filter patterns to the case list."""
|
||||
patterns = [p.strip() for p in filter_str.split(",")]
|
||||
return [c for c in cases if any(matches_filter(c, p) for p in patterns)]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# World building
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class CourseLayout:
|
||||
start_x: int
|
||||
start_y: int
|
||||
start_z: int
|
||||
end_x: int
|
||||
end_y: int
|
||||
end_z: int
|
||||
clear_min: tuple[int, int, int]
|
||||
clear_max: tuple[int, int, int]
|
||||
|
||||
|
||||
class WorldBuilder:
|
||||
def __init__(self, rcon: RconClient, base_x: int = 100, base_y: int = 80):
|
||||
self.rcon = rcon
|
||||
self.base_x = base_x
|
||||
self.base_y = base_y
|
||||
self._z_cursor = 100
|
||||
|
||||
def allocate_z(self, width: int = 1) -> int:
|
||||
z = self._z_cursor
|
||||
self._z_cursor += width + 2 * CLEAR_MARGIN + 5
|
||||
return z
|
||||
|
||||
def clear_area(self, x1: int, y1: int, z1: int, x2: int, y2: int, z2: int) -> None:
|
||||
dx = x2 - x1
|
||||
dz = z2 - z1
|
||||
# MC fill command has a 32768-block limit per call; chunk if needed
|
||||
chunk_size = 48
|
||||
for cx in range(x1, x2 + 1, chunk_size):
|
||||
for cz in range(z1, z2 + 1, chunk_size):
|
||||
ex = min(cx + chunk_size - 1, x2)
|
||||
ez = min(cz + chunk_size - 1, z2)
|
||||
self.rcon.command(f"fill {cx} {y1} {cz} {ex} {y2} {ez} air")
|
||||
|
||||
def set_block(self, x: int, y: int, z: int, block: str = "stone") -> None:
|
||||
self.rcon.command(f"setblock {x} {y} {z} {block}")
|
||||
|
||||
def fill_blocks(self, x1: int, y1: int, z1: int, x2: int, y2: int, z2: int,
|
||||
block: str = "stone") -> None:
|
||||
self.rcon.command(f"fill {x1} {y1} {z1} {x2} {y2} {z2} {block}")
|
||||
|
||||
def build_linear_route(self, case: TestCase) -> CourseLayout:
|
||||
gap = case.gap_or_wall
|
||||
dy = case.delta_y
|
||||
bx = self.base_x
|
||||
by = self.base_y
|
||||
bz = self.allocate_z()
|
||||
floor_y = by - 1
|
||||
|
||||
platform_stride = gap + 1
|
||||
total_x = LINEAR_RUNWAY + SEGMENTS * platform_stride + 2
|
||||
|
||||
max_dy_extent = int(abs(dy) * SEGMENTS) + 2
|
||||
x_min = bx - CLEAR_MARGIN
|
||||
x_max = bx + total_x + CLEAR_MARGIN
|
||||
y_min = min(floor_y, floor_y + int(dy * SEGMENTS)) - CLEAR_MARGIN
|
||||
y_max = max(floor_y, floor_y + int(dy * SEGMENTS)) + CLEAR_MARGIN
|
||||
z_min = bz - CLEAR_MARGIN
|
||||
z_max = bz + CLEAR_MARGIN
|
||||
|
||||
self.clear_area(x_min, y_min, z_min, x_max, y_max, z_max)
|
||||
|
||||
for rx in range(LINEAR_RUNWAY):
|
||||
self.set_block(bx + rx, floor_y, bz)
|
||||
|
||||
last_x = bx + LINEAR_RUNWAY - 1
|
||||
last_y = floor_y
|
||||
|
||||
for seg in range(SEGMENTS):
|
||||
plat_x = last_x + gap + 1
|
||||
plat_y = last_y + int(dy)
|
||||
self.set_block(plat_x, plat_y, bz)
|
||||
last_x = plat_x
|
||||
last_y = plat_y
|
||||
|
||||
return CourseLayout(
|
||||
start_x=bx, start_y=by, start_z=bz,
|
||||
end_x=last_x, end_y=last_y + 1, end_z=bz,
|
||||
clear_min=(x_min, y_min, z_min),
|
||||
clear_max=(x_max, y_max, z_max),
|
||||
)
|
||||
|
||||
def build_neo_route(self, case: TestCase) -> CourseLayout:
|
||||
"""Neo jump: player must jump around a wall to reach the next platform.
|
||||
|
||||
Layout (top view, each segment):
|
||||
[Runway/Platform at Z=cur_z]
|
||||
[Wall: 1 block in X, wall_width blocks in Z, 4 blocks tall]
|
||||
[1 block air gap]
|
||||
[Landing platform]
|
||||
[1 block air gap]
|
||||
[Next wall...]
|
||||
|
||||
The wall runs along Z starting from the current Z. The player
|
||||
jumps around the wall edge in the +Z direction to reach the landing.
|
||||
"""
|
||||
wall_width = case.gap_or_wall
|
||||
bx = self.base_x
|
||||
by = self.base_y
|
||||
bz = self.allocate_z(width=(wall_width + NEO_LANDING_GAP) * SEGMENTS + 10)
|
||||
floor_y = by - 1
|
||||
|
||||
z_extent = SEGMENTS * (wall_width + NEO_LANDING_GAP) + 10
|
||||
total_x = LINEAR_RUNWAY + SEGMENTS * 2 + 5
|
||||
|
||||
x_min = bx - CLEAR_MARGIN
|
||||
x_max = bx + total_x + CLEAR_MARGIN
|
||||
y_min = floor_y - CLEAR_MARGIN
|
||||
y_max = floor_y + CLEAR_MARGIN
|
||||
z_min = bz - CLEAR_MARGIN
|
||||
z_max = bz + z_extent + CLEAR_MARGIN
|
||||
|
||||
self.clear_area(x_min, y_min, z_min, x_max, y_max, z_max)
|
||||
|
||||
# Runway
|
||||
for rx in range(LINEAR_RUNWAY):
|
||||
self.set_block(bx + rx, floor_y, bz)
|
||||
|
||||
seg_x = bx + LINEAR_RUNWAY - 1
|
||||
cur_z = bz
|
||||
|
||||
for seg in range(SEGMENTS):
|
||||
wall_x = seg_x + 1
|
||||
|
||||
if wall_width > 0:
|
||||
wall_z_start = cur_z
|
||||
wall_z_end = cur_z + wall_width - 1
|
||||
self.fill_blocks(wall_x, floor_y, wall_z_start,
|
||||
wall_x, floor_y + 3, wall_z_end)
|
||||
|
||||
# Landing: 1 block of air, then platform, then 1 block of air
|
||||
landing_z = cur_z + wall_width + 1 # 1 air gap after wall
|
||||
self.set_block(wall_x + 1, floor_y, landing_z)
|
||||
|
||||
seg_x = wall_x + 1
|
||||
cur_z = landing_z + 2 # 1 air gap after landing before next wall
|
||||
|
||||
end_x = seg_x
|
||||
end_z = cur_z - 2 # last landing position
|
||||
|
||||
return CourseLayout(
|
||||
start_x=bx, start_y=by, start_z=bz,
|
||||
end_x=end_x, end_y=by, end_z=end_z,
|
||||
clear_min=(x_min, y_min, z_min),
|
||||
clear_max=(x_max, y_max, z_max),
|
||||
)
|
||||
|
||||
def build_sidewall_route(self, case: TestCase) -> CourseLayout:
|
||||
"""Sidewall jump: platforms along a massive wall face.
|
||||
|
||||
The wall is directly behind the platforms (Z+1), tall and thick,
|
||||
constraining backward movement. Player jumps between 1x1 platforms
|
||||
that are at different X offsets and Y heights along the wall.
|
||||
|
||||
Layout (side view, looking from -Z toward +Z / toward the wall):
|
||||
|
||||
[=== MASSIVE WALL (Z=bz+1 to bz+6, full height) ===]
|
||||
| |
|
||||
| [P3] at X+2*stride, Y+2*dy |
|
||||
| |
|
||||
| [P2] at X+stride, Y+dy |
|
||||
| |
|
||||
| [Start/Runway] at X, Y |
|
||||
[====================================================]
|
||||
(open air below/in front)
|
||||
|
||||
Wall_offset controls distance from platform to wall:
|
||||
wo=0: wall at Z=bz+1 (directly behind)
|
||||
wo=1: wall at Z=bz+2 (1 block gap)
|
||||
"""
|
||||
gap = case.gap_or_wall
|
||||
dy = case.delta_y
|
||||
wo = case.wall_offset if case.wall_offset is not None else 0
|
||||
bx = self.base_x
|
||||
by = self.base_y
|
||||
bz = self.allocate_z(width=8 + wo)
|
||||
floor_y = by - 1
|
||||
|
||||
platform_stride = gap + 1
|
||||
total_x = SIDEWALL_RUNWAY + SEGMENTS * platform_stride + 2
|
||||
|
||||
x_min = bx - CLEAR_MARGIN
|
||||
x_max = bx + total_x + CLEAR_MARGIN
|
||||
y_min = min(floor_y, floor_y + int(dy * SEGMENTS)) - CLEAR_MARGIN
|
||||
y_max = max(floor_y, floor_y + int(dy * SEGMENTS)) + CLEAR_MARGIN
|
||||
z_min = bz - CLEAR_MARGIN
|
||||
z_max = bz + 8 + wo + CLEAR_MARGIN
|
||||
|
||||
self.clear_area(x_min, y_min, z_min, x_max, y_max, z_max)
|
||||
|
||||
# Runway (shorter than linear -- 2 blocks like the reference image)
|
||||
for rx in range(SIDEWALL_RUNWAY):
|
||||
self.set_block(bx + rx, floor_y, bz)
|
||||
|
||||
last_x = bx + SIDEWALL_RUNWAY - 1
|
||||
last_y = floor_y
|
||||
|
||||
for seg in range(SEGMENTS):
|
||||
plat_x = last_x + gap + 1
|
||||
plat_y = last_y + int(dy)
|
||||
self.set_block(plat_x, plat_y, bz)
|
||||
last_x = plat_x
|
||||
last_y = plat_y
|
||||
|
||||
# Massive wall behind the platforms
|
||||
wall_z_start = bz + 1 + wo
|
||||
wall_z_end = bz + 6 + wo # 6 blocks thick
|
||||
wall_y_low = min(floor_y, last_y) - 2
|
||||
wall_y_high = max(floor_y, last_y) + 5
|
||||
wall_x_start = bx - 1
|
||||
wall_x_end = last_x + 1
|
||||
self.fill_blocks(wall_x_start, wall_y_low, wall_z_start,
|
||||
wall_x_end, wall_y_high, wall_z_end)
|
||||
|
||||
return CourseLayout(
|
||||
start_x=bx, start_y=by, start_z=bz,
|
||||
end_x=last_x, end_y=last_y + 1, end_z=bz,
|
||||
clear_min=(x_min, y_min, z_min),
|
||||
clear_max=(x_max, y_max, z_max),
|
||||
)
|
||||
|
||||
def build_ceiling_route(self, case: TestCase) -> CourseLayout:
|
||||
gap = case.gap_or_wall
|
||||
ceil_height = case.ceiling_height or 4.0
|
||||
bx = self.base_x
|
||||
by = self.base_y
|
||||
bz = self.allocate_z()
|
||||
floor_y = by - 1
|
||||
|
||||
platform_stride = gap + 1
|
||||
total_x = LINEAR_RUNWAY + SEGMENTS * platform_stride + 2
|
||||
|
||||
x_min = bx - CLEAR_MARGIN
|
||||
x_max = bx + total_x + CLEAR_MARGIN
|
||||
ceil_y = floor_y + int(ceil_height) + 1
|
||||
y_min = floor_y - CLEAR_MARGIN
|
||||
y_max = ceil_y + CLEAR_MARGIN
|
||||
z_min = bz - CLEAR_MARGIN
|
||||
z_max = bz + CLEAR_MARGIN
|
||||
|
||||
self.clear_area(x_min, y_min, z_min, x_max, y_max, z_max)
|
||||
|
||||
for rx in range(LINEAR_RUNWAY):
|
||||
self.set_block(bx + rx, floor_y, bz)
|
||||
|
||||
last_x = bx + LINEAR_RUNWAY - 1
|
||||
for seg in range(SEGMENTS):
|
||||
plat_x = last_x + gap + 1
|
||||
self.set_block(plat_x, floor_y, bz)
|
||||
last_x = plat_x
|
||||
|
||||
ceil_block_y = floor_y + math.ceil(ceil_height)
|
||||
self.fill_blocks(bx - 1, ceil_block_y, bz - 1, last_x + 1, ceil_block_y, bz + 1)
|
||||
|
||||
return CourseLayout(
|
||||
start_x=bx, start_y=by, start_z=bz,
|
||||
end_x=last_x, end_y=by, end_z=bz,
|
||||
clear_min=(x_min, y_min, z_min),
|
||||
clear_max=(x_max, y_max, z_max),
|
||||
)
|
||||
|
||||
def build(self, case: TestCase) -> CourseLayout:
|
||||
if case.family == "linear":
|
||||
return self.build_linear_route(case)
|
||||
if case.family == "neo":
|
||||
return self.build_neo_route(case)
|
||||
if case.family == "sidewall":
|
||||
return self.build_sidewall_route(case)
|
||||
if case.family == "ceiling":
|
||||
return self.build_ceiling_route(case)
|
||||
raise ValueError(f"Unknown family: {case.family}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test execution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class TestResult:
|
||||
case: TestCase
|
||||
outcome: str # "pass", "reject", "fail", "invalid_live_case"
|
||||
matched_expected: bool
|
||||
log_excerpt: str = ""
|
||||
|
||||
|
||||
def resolve_session() -> str:
|
||||
explicit = os.environ.get("SESSION", "")
|
||||
if explicit:
|
||||
return explicit
|
||||
repo = os.environ.get("MCC_REPO_ROOT", "")
|
||||
if repo:
|
||||
return Path(repo).name
|
||||
return Path.cwd().name
|
||||
|
||||
|
||||
def run_single_test(
|
||||
case: TestCase,
|
||||
layout: CourseLayout,
|
||||
rcon: RconClient,
|
||||
mcc: MccClient,
|
||||
username: str,
|
||||
wait_seconds: int = 15,
|
||||
) -> TestResult:
|
||||
rcon.command(f"gamemode creative {username}")
|
||||
time.sleep(0.3)
|
||||
rcon.command(f"tp {username} {layout.start_x}.5 {layout.start_y} {layout.start_z}.5")
|
||||
time.sleep(1)
|
||||
rcon.command(f"gamemode survival {username}")
|
||||
time.sleep(0.5)
|
||||
|
||||
mcc.clear_log()
|
||||
time.sleep(0.3)
|
||||
|
||||
mcc.send(f"goto {layout.end_x} {layout.end_y} {layout.end_z}")
|
||||
time.sleep(wait_seconds)
|
||||
|
||||
raw_log = mcc.read_log()
|
||||
log = mcc.strip_ansi(raw_log)
|
||||
all_lines = log.splitlines()
|
||||
|
||||
a_star_lines = [l for l in all_lines if "[A*]" in l][:3]
|
||||
path_mgr_lines = [l for l in all_lines if "[PathMgr]" in l]
|
||||
path_exec_lines = [l for l in all_lines if "[PathExec]" in l]
|
||||
move_lines = [l for l in all_lines if "FileInput" in l or "path" in l.lower()
|
||||
or "move" in l.lower() or "navigate" in l.lower()]
|
||||
|
||||
outcome = "invalid_live_case"
|
||||
full_text = log.lower()
|
||||
mgr_text = "\n".join(path_mgr_lines)
|
||||
astar_text = "\n".join(a_star_lines)
|
||||
exec_text = "\n".join(path_exec_lines)
|
||||
|
||||
if "navigation complete" in full_text:
|
||||
outcome = "pass"
|
||||
elif "complete" in mgr_text.lower():
|
||||
outcome = "pass"
|
||||
elif "failed to compute a safe path" in full_text:
|
||||
outcome = "reject"
|
||||
elif "not a reachable" in full_text:
|
||||
outcome = "reject"
|
||||
elif "no path" in full_text:
|
||||
outcome = "reject"
|
||||
elif "Failed" in astar_text:
|
||||
outcome = "reject"
|
||||
elif "Replan failed" in mgr_text or "Giving up" in mgr_text:
|
||||
outcome = "fail"
|
||||
elif "FAILED" in exec_text:
|
||||
outcome = "fail"
|
||||
elif "failed" in full_text:
|
||||
outcome = "fail"
|
||||
|
||||
excerpt_lines = []
|
||||
if a_star_lines:
|
||||
excerpt_lines.append(f" A*: {a_star_lines[0]}")
|
||||
if path_mgr_lines:
|
||||
excerpt_lines.append(f" Mgr: {path_mgr_lines[-1]}")
|
||||
relevant = [l for l in all_lines if "path" in l.lower() or "move" in l.lower()
|
||||
or "navigate" in l.lower() or "A*" in l]
|
||||
if not excerpt_lines and relevant:
|
||||
excerpt_lines.append(f" Log: {relevant[0]}")
|
||||
|
||||
return TestResult(
|
||||
case=case,
|
||||
outcome=outcome,
|
||||
matched_expected=(outcome == case.expected),
|
||||
log_excerpt="\n".join(excerpt_lines),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stop-at-first-failure logic
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def should_skip(case: TestCase, failed_groups: set[tuple]) -> bool:
|
||||
"""Skip this case if its group already had a failure at a smaller gap."""
|
||||
return case.group_key() in failed_groups
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Full-coverage parkour test suite",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__.split("Usage:")[0],
|
||||
)
|
||||
parser.add_argument("--list-cases", action="store_true",
|
||||
help="Print test matrix and exit")
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="Build worlds but don't run MCC navigation")
|
||||
parser.add_argument("--filter", type=str, default=None,
|
||||
help="Comma-separated hierarchical filters")
|
||||
parser.add_argument("--rcon-port", type=int, default=25575)
|
||||
parser.add_argument("--rcon-password", type=str, default="test123")
|
||||
parser.add_argument("--username", type=str, default="MCCBot")
|
||||
parser.add_argument("--wait", type=int, default=15,
|
||||
help="Seconds to wait for navigation per test")
|
||||
parser.add_argument("--results", type=str, default=None,
|
||||
help="Path for JSONL results output")
|
||||
args = parser.parse_args()
|
||||
|
||||
caps = load_capabilities(CAPABILITIES_PATH)
|
||||
all_cases = derive_test_matrix(caps)
|
||||
|
||||
if args.filter:
|
||||
all_cases = apply_filters(all_cases, args.filter)
|
||||
|
||||
if args.list_cases:
|
||||
pass_count = sum(1 for c in all_cases if c.expected == "pass")
|
||||
reject_count = sum(1 for c in all_cases if c.expected == "reject")
|
||||
print(f"Total: {len(all_cases)} cases ({pass_count} pass, {reject_count} reject)")
|
||||
|
||||
current_group = ""
|
||||
for c in all_cases:
|
||||
group = f"{c.family}/{c.subfamily}"
|
||||
if group != current_group:
|
||||
print(f"\n {group}:")
|
||||
current_group = group
|
||||
marker = "PASS" if c.expected == "pass" else "REJECT"
|
||||
quals = []
|
||||
if c.delta_y != 0.0:
|
||||
quals.append(f"dy={c.delta_y:+.0f}")
|
||||
if c.ceiling_height is not None:
|
||||
quals.append(f"ceil={c.ceiling_height}")
|
||||
if c.wall_offset is not None:
|
||||
quals.append(f"wo={c.wall_offset}")
|
||||
q = f" ({', '.join(quals)})" if quals else ""
|
||||
metric = "gap" if c.family != "neo" else "wall"
|
||||
print(f" {c.case_id:<50} {metric}={c.gap_or_wall} [{marker}]{q}")
|
||||
return
|
||||
|
||||
rcon = RconClient(port=args.rcon_port, password=args.rcon_password)
|
||||
rcon.connect()
|
||||
|
||||
rcon.command("difficulty peaceful")
|
||||
rcon.command("gamerule doMobSpawning false")
|
||||
rcon.command("time set day")
|
||||
|
||||
builder = WorldBuilder(rcon)
|
||||
|
||||
if args.dry_run:
|
||||
print("=== DRY RUN: building worlds only ===")
|
||||
for case in all_cases:
|
||||
layout = builder.build(case)
|
||||
print(f" {case.case_id}: start=({layout.start_x},{layout.start_y},{layout.start_z})"
|
||||
f" end=({layout.end_x},{layout.end_y},{layout.end_z})")
|
||||
rcon.close()
|
||||
print(f"\nBuilt {len(all_cases)} courses.")
|
||||
return
|
||||
|
||||
session = resolve_session()
|
||||
mcc = MccClient(session)
|
||||
|
||||
results_path = Path(args.results) if args.results else None
|
||||
if results_path:
|
||||
results_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print("=" * 60)
|
||||
print(" MCC Full-Coverage Parkour Test Suite")
|
||||
print("=" * 60)
|
||||
print(f" Cases: {len(all_cases)}")
|
||||
print(f" Username: {args.username}")
|
||||
print(f" Session: {session}")
|
||||
print(f" Wait: {args.wait}s per test")
|
||||
print()
|
||||
|
||||
results: list[TestResult] = []
|
||||
failed_groups: set[tuple] = set()
|
||||
skipped = 0
|
||||
|
||||
for i, case in enumerate(all_cases, 1):
|
||||
if should_skip(case, failed_groups):
|
||||
skipped += 1
|
||||
print(f" [{i}/{len(all_cases)}] {case.case_id} -- SKIPPED (group already failed)")
|
||||
continue
|
||||
|
||||
print(f"\n--- [{i}/{len(all_cases)}] {case.case_id} (expect: {case.expected}) ---")
|
||||
|
||||
layout = builder.build(case)
|
||||
print(f" Route: ({layout.start_x},{layout.start_y},{layout.start_z}) -> "
|
||||
f"({layout.end_x},{layout.end_y},{layout.end_z})")
|
||||
|
||||
result = run_single_test(
|
||||
case, layout, rcon, mcc, args.username, args.wait,
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
status = "OK" if result.matched_expected else "MISMATCH"
|
||||
print(f" Outcome: {result.outcome} [{status}]")
|
||||
if result.log_excerpt:
|
||||
print(result.log_excerpt)
|
||||
|
||||
# Stop-at-first-failure: only trigger on definitive navigation
|
||||
# failures (reject/fail), not on setup issues (invalid_live_case).
|
||||
if result.outcome in ("reject", "fail") and case.expected == "pass":
|
||||
failed_groups.add(case.group_key())
|
||||
print(f" >> Group failed at {case.family}/{case.subfamily} "
|
||||
f"gap/wall={case.gap_or_wall} -- skipping larger values")
|
||||
|
||||
if results_path:
|
||||
with results_path.open("a") as f:
|
||||
f.write(json.dumps({
|
||||
"case_id": case.case_id,
|
||||
"family": case.family,
|
||||
"subfamily": case.subfamily,
|
||||
"gap_or_wall": case.gap_or_wall,
|
||||
"expected": case.expected,
|
||||
"outcome": result.outcome,
|
||||
"matched": result.matched_expected,
|
||||
}) + "\n")
|
||||
|
||||
rcon.close()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(" SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
passed = [r for r in results if r.matched_expected]
|
||||
failed = [r for r in results if not r.matched_expected]
|
||||
|
||||
print(f"\n {len(passed)}/{len(results)} matched expectations")
|
||||
if skipped:
|
||||
print(f" {skipped} cases skipped (stop-at-first-failure)")
|
||||
|
||||
if failed:
|
||||
print(f"\n MISMATCHES ({len(failed)}):")
|
||||
for r in failed:
|
||||
print(f" {r.case.case_id}: expected={r.case.expected} got={r.outcome}")
|
||||
|
||||
sys.exit(0 if not failed else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,81 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
# Automated parkour jump test for MCC pathfinding
|
||||
# Usage: source tools/mcc-env.sh && bash tools/test-parkour.sh
|
||||
# Full-coverage parkour test suite for MCC pathfinding
|
||||
# Thin wrapper around test-parkour.py
|
||||
# Usage: source tools/mcc-env.sh && bash tools/test-parkour.sh [args...]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
source "$REPO_ROOT/tools/pathing_live_common.sh"
|
||||
|
||||
MANIFEST="$REPO_ROOT/tools/pathing_data/canonical-live-cases.json"
|
||||
RESULTS_FILE="${RESULTS_FILE:-/tmp/mcc-debug/pathing-live-results.jsonl}"
|
||||
LOG="/tmp/mcc-debug/mcc-debug.log"
|
||||
RESULTS=""
|
||||
TEST_NUM=0
|
||||
LAST_RESULT="invalid_live_case"
|
||||
|
||||
if [[ "${1:-}" == "--list-cases" ]]; then
|
||||
manifest_cases_for_query "$MANIFEST" "linear"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$RESULTS_FILE")"
|
||||
: > "$RESULTS_FILE"
|
||||
|
||||
run_manifest_case() {
|
||||
local case_id="$1"
|
||||
local case_json
|
||||
case_json="$(manifest_case_json "$MANIFEST" "$case_id")"
|
||||
|
||||
read -r world_recipe start_x start_y start_z goal_x goal_y goal_z < <(
|
||||
python3 - "$case_json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
row = json.loads(sys.argv[1])
|
||||
print(
|
||||
row["world_recipe_id"],
|
||||
row["start"]["x"],
|
||||
row["start"]["y"],
|
||||
row["start"]["z"],
|
||||
row["goal"]["x"],
|
||||
row["goal"]["y"],
|
||||
row["goal"]["z"],
|
||||
)
|
||||
PY
|
||||
)
|
||||
|
||||
local landing_block_y=$(( ${goal_y%.*} - 1 ))
|
||||
|
||||
case "$world_recipe" in
|
||||
linear-flat|linear-ascend|linear-descend)
|
||||
mc-rcon "fill 95 80 95 115 90 105 air" >/dev/null
|
||||
mc-rcon "fill 95 79 95 115 79 105 air" >/dev/null
|
||||
mc-rcon "setblock 100 79 100 stone" >/dev/null
|
||||
mc-rcon "setblock ${goal_x%.*} ${landing_block_y} ${goal_z%.*} stone" >/dev/null
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported world recipe for test-parkour.sh: $world_recipe" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
run_test "$case_id" "${start_x%.*}" "${start_y%.*}" "${start_z%.*}" "${goal_x%.*}" "${goal_y%.*}" "${goal_z%.*}"
|
||||
record_live_result "$RESULTS_FILE" "$case_json" "$LAST_RESULT" "$LOG"
|
||||
}
|
||||
|
||||
echo "========================================"
|
||||
echo " MCC Parkour Jump Test Suite"
|
||||
echo "========================================"
|
||||
|
||||
while IFS= read -r case_id; do
|
||||
run_manifest_case "$case_id"
|
||||
done < <(manifest_cases_for_query "$MANIFEST" "linear")
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " SUMMARY"
|
||||
echo "========================================"
|
||||
echo -e "$RESULTS"
|
||||
exec python3 "$SCRIPT_DIR/test-parkour.py" "$@"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
|
||||
VERSION="${1:-1.21.11-Vanilla}"
|
||||
SESSION="mcc-pathing-jump-combos"
|
||||
USERNAME="MCCBot"
|
||||
SESSION="${SESSION:-mcc-pathing-jump-combos}"
|
||||
USERNAME="${USERNAME:-MCCBot}"
|
||||
|
||||
SESSION_ROOT="$(_mcc_session_root "$SESSION")"
|
||||
LOG="$(_mcc_session_log_file "$SESSION")"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
|
||||
VERSION="${1:-1.21.11-Vanilla}"
|
||||
SESSION="mcc-pathing-long-routes"
|
||||
USERNAME="MCCBot"
|
||||
SESSION="${SESSION:-mcc-pathing-long-routes}"
|
||||
USERNAME="${USERNAME:-MCCBot}"
|
||||
|
||||
SESSION_ROOT="$(_mcc_session_root "$SESSION")"
|
||||
LOG="$(_mcc_session_log_file "$SESSION")"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
|
||||
VERSION="${1:-1.21.11-Vanilla}"
|
||||
SESSION="mcc-pathing-template"
|
||||
USERNAME="MCCBot"
|
||||
SESSION="${SESSION:-mcc-pathing-template}"
|
||||
USERNAME="${USERNAME:-MCCBot}"
|
||||
|
||||
SESSION_ROOT="$(_mcc_session_root "$SESSION")"
|
||||
LOG="$(_mcc_session_log_file "$SESSION")"
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
source "$REPO_ROOT/tools/pathing_live_common.sh"
|
||||
|
||||
SESSION="${SESSION:-$(_mcc_resolve_session)}"
|
||||
USERNAME="${USERNAME:-MCCBot}"
|
||||
MANIFEST="$REPO_ROOT/tools/pathing_data/canonical-live-cases.json"
|
||||
RESULTS_FILE="${RESULTS_FILE:-/tmp/mcc-debug/pathing-live-results.jsonl}"
|
||||
LOG="/tmp/mcc-debug/mcc-debug.log"
|
||||
RESULTS_FILE="${RESULTS_FILE:-$(_mcc_session_root "$SESSION")/pathing-live-results.jsonl}"
|
||||
LOG="$(_mcc_session_log_file "$SESSION")"
|
||||
RESULTS=""
|
||||
TEST_NUM=0
|
||||
LAST_RESULT="invalid_live_case"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|||
source "$REPO_ROOT/tools/mcc-env.sh"
|
||||
|
||||
VERSION="${1:-1.21.11-Vanilla}"
|
||||
SESSION="mcc-brake-test"
|
||||
USERNAME="MCCBot"
|
||||
SESSION="${SESSION:-mcc-brake-test}"
|
||||
USERNAME="${USERNAME:-MCCBot}"
|
||||
|
||||
SESSION_ROOT="$(_mcc_session_root "$SESSION")"
|
||||
LOG="$(_mcc_session_log_file "$SESSION")"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import tempfile
|
|||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from tools.pathing_theory.capabilities import build_momentum_capability_bands
|
||||
from tools.pathing_theory.canonical import build_canonical_live_cases
|
||||
from tools.pathing_theory.renderers import write_theory_artifacts
|
||||
from tools.pathing_theory.simulator import build_theory_cases
|
||||
|
|
@ -16,8 +17,8 @@ class CanonicalPathingCaseTests(unittest.TestCase):
|
|||
self.assertTrue(all(case.movement_mode == "sprint" for case in canonical_cases))
|
||||
self.assertTrue(all(case.momentum_ticks == 12 for case in canonical_cases))
|
||||
self.assertIn("linear:flat:sprint:easy", bucket_ids)
|
||||
self.assertIn("linear:flat:sprint:boundary", bucket_ids)
|
||||
self.assertIn("linear:flat:sprint:reject", bucket_ids)
|
||||
self.assertIn("linear:descend:sprint:boundary", bucket_ids)
|
||||
self.assertIn("neo:neo:sprint:boundary", bucket_ids)
|
||||
self.assertIn("ceiling:headhitter:sprint:boundary", bucket_ids)
|
||||
|
||||
|
|
@ -26,17 +27,26 @@ class CanonicalPathingCaseTests(unittest.TestCase):
|
|||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
output_dir = Path(temp_dir)
|
||||
write_theory_artifacts(cases, build_canonical_live_cases(cases), output_dir)
|
||||
write_theory_artifacts(
|
||||
cases,
|
||||
build_canonical_live_cases(cases),
|
||||
build_momentum_capability_bands(cases),
|
||||
output_dir,
|
||||
)
|
||||
|
||||
json_path = output_dir / "theory-matrix.json"
|
||||
csv_path = output_dir / "theory-matrix.csv"
|
||||
md_path = output_dir / "theory-matrix.md"
|
||||
canonical_path = output_dir / "canonical-live-cases.json"
|
||||
capability_path = output_dir / "momentum-capabilities.json"
|
||||
capability_md_path = output_dir / "momentum-capabilities.md"
|
||||
|
||||
self.assertTrue(json_path.exists())
|
||||
self.assertTrue(csv_path.exists())
|
||||
self.assertTrue(md_path.exists())
|
||||
self.assertTrue(canonical_path.exists())
|
||||
self.assertTrue(capability_path.exists())
|
||||
self.assertTrue(capability_md_path.exists())
|
||||
|
||||
exported_cases = json.loads(json_path.read_text())
|
||||
self.assertEqual(len(cases), len(exported_cases))
|
||||
|
|
|
|||
71
tools/tests/test_pathing_capabilities.py
Normal file
71
tools/tests/test_pathing_capabilities.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import subprocess
|
||||
import unittest
|
||||
|
||||
from tools.pathing_theory.capabilities import build_momentum_capability_bands
|
||||
from tools.pathing_theory.simulator import build_theory_cases
|
||||
|
||||
|
||||
class PathingCapabilityTests(unittest.TestCase):
|
||||
def test_linear_descend_sprint_dy_minus2_compresses_into_mm_breakpoints(self) -> None:
|
||||
bands = build_momentum_capability_bands(build_theory_cases())
|
||||
rows = [
|
||||
band
|
||||
for band in bands
|
||||
if band.family == "linear"
|
||||
and band.subfamily == "descend"
|
||||
and band.movement_mode == "sprint"
|
||||
and band.delta_y == -2.0
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
[(band.min_mm, band.max_mm, band.max_reach) for band in rows],
|
||||
[(0, 0, 4), (1, 12, 5)],
|
||||
)
|
||||
|
||||
def test_linear_ascend_walk_dy_plus1_compresses_into_mm_breakpoints(self) -> None:
|
||||
bands = build_momentum_capability_bands(build_theory_cases())
|
||||
rows = [
|
||||
band
|
||||
for band in bands
|
||||
if band.family == "linear"
|
||||
and band.subfamily == "ascend"
|
||||
and band.movement_mode == "walk"
|
||||
and band.delta_y == 1.0
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
[(band.min_mm, band.max_mm, band.max_reach) for band in rows],
|
||||
[(0, 0, 1), (1, 12, 2)],
|
||||
)
|
||||
|
||||
def test_ceiling_sprint_height_2p5_has_late_mm_breakpoint(self) -> None:
|
||||
bands = build_momentum_capability_bands(build_theory_cases())
|
||||
rows = [
|
||||
band
|
||||
for band in bands
|
||||
if band.family == "ceiling"
|
||||
and band.subfamily == "headhitter"
|
||||
and band.movement_mode == "sprint"
|
||||
and band.ceiling_height == 2.5
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
[(band.min_mm, band.max_mm, band.max_reach) for band in rows],
|
||||
[(0, 7, 2), (8, 12, 3)],
|
||||
)
|
||||
|
||||
def test_sim_jump_reach_lists_momentum_capabilities(self) -> None:
|
||||
result = subprocess.run(
|
||||
["python3", "tools/sim_jump_reach.py", "--list-capabilities"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("linear | descend | sprint | dy=-2.0 | mm=0..0 | max_gap=4", result.stdout)
|
||||
self.assertIn("linear | ascend | walk | dy=1.0 | mm=1..12 | max_gap=2", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -3,19 +3,51 @@ import unittest
|
|||
|
||||
|
||||
class PathingLiveScriptTests(unittest.TestCase):
|
||||
def test_test_parkour_lists_linear_canonical_cases(self) -> None:
|
||||
def test_test_parkour_lists_all_families(self) -> None:
|
||||
result = subprocess.run(
|
||||
["bash", "tools/test-parkour.sh", "--list-cases"],
|
||||
["python3", "tools/test-parkour.py", "--list-cases"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("linear-flat-sprint-mm12-gap5-dy0p0", result.stdout)
|
||||
self.assertIn("linear-ascend-sprint-mm12-gap2-dy1p0", result.stdout)
|
||||
self.assertNotIn("linear-flat-walk-mm12-gap5-dy0p0", result.stdout)
|
||||
self.assertNotIn("linear-flat-sprint-mm0-gap3-dy0p0", result.stdout)
|
||||
self.assertIn("linear/flat", result.stdout)
|
||||
self.assertIn("linear/ascend", result.stdout)
|
||||
self.assertIn("linear/descend", result.stdout)
|
||||
self.assertIn("neo/neo", result.stdout)
|
||||
self.assertIn("ceiling/headhitter", result.stdout)
|
||||
self.assertIn("sidewall/flat", result.stdout)
|
||||
self.assertIn("sidewall/ascend", result.stdout)
|
||||
self.assertIn("sidewall/descend", result.stdout)
|
||||
|
||||
def test_test_parkour_linear_has_reject_at_max_plus_one(self) -> None:
|
||||
result = subprocess.run(
|
||||
["python3", "tools/test-parkour.py", "--list-cases", "--family", "linear"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("linear-flat-gap4", result.stdout)
|
||||
self.assertIn("linear-flat-gap5", result.stdout)
|
||||
self.assertIn("[PASS]", result.stdout)
|
||||
self.assertIn("[REJECT]", result.stdout)
|
||||
|
||||
def test_test_parkour_neo_covers_wall_range(self) -> None:
|
||||
result = subprocess.run(
|
||||
["python3", "tools/test-parkour.py", "--list-cases", "--family", "neo"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
for w in range(5):
|
||||
self.assertIn(f"neo-neo-wall{w}", result.stdout)
|
||||
self.assertIn("neo-neo-wall5", result.stdout)
|
||||
self.assertIn("[REJECT]", result.stdout)
|
||||
|
||||
def test_test_pathing_theory_neo_ceiling_lists_theory_cases(self) -> None:
|
||||
result = subprocess.run(
|
||||
|
|
@ -27,7 +59,7 @@ class PathingLiveScriptTests(unittest.TestCase):
|
|||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("neo-neo-sprint-mm12-wall1", result.stdout)
|
||||
self.assertIn("ceiling-headhitter-sprint-mm12-gap3-ceil2p0", result.stdout)
|
||||
self.assertIn("ceiling-headhitter-sprint-mm12-gap3-ceil2p5", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from tools.pathing_theory.primitives import (
|
||||
can_reach_gap,
|
||||
can_reach_gap_with_side_wall,
|
||||
get_landing,
|
||||
)
|
||||
from tools.pathing_theory.simulator import build_theory_cases
|
||||
|
||||
|
||||
|
|
@ -14,14 +19,103 @@ class PathingTheoryMatrixTests(unittest.TestCase):
|
|||
self.assertIn(("linear", "descend"), families)
|
||||
self.assertIn(("neo", "neo"), families)
|
||||
self.assertIn(("ceiling", "headhitter"), families)
|
||||
self.assertIn(("sidewall", "flat"), families)
|
||||
self.assertIn(("sidewall", "ascend"), families)
|
||||
self.assertIn(("sidewall", "descend"), families)
|
||||
|
||||
linear_boundary = next(
|
||||
case
|
||||
for case in cases
|
||||
if case.case_id == "linear-flat-sprint-mm12-gap5-dy0p0"
|
||||
)
|
||||
self.assertTrue(linear_boundary.expected_reachable)
|
||||
self.assertGreater(linear_boundary.margin, 0.0)
|
||||
self.assertFalse(linear_boundary.expected_reachable)
|
||||
self.assertTrue(
|
||||
linear_boundary.margin is None or linear_boundary.margin < 0.0
|
||||
)
|
||||
|
||||
def test_walk_momentum_does_not_turn_gap4_ascend_into_reachable(self) -> None:
|
||||
ok, landing_x, needed_x = can_reach_gap(
|
||||
gap_blocks=4,
|
||||
dy=1.0,
|
||||
sprint=False,
|
||||
momentum_ticks=12,
|
||||
)
|
||||
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(landing_x is None or landing_x < needed_x)
|
||||
|
||||
def test_walk_momentum_can_reach_gap2_ascend_with_edge_takeoff_support(self) -> None:
|
||||
ok, landing_x, needed_x = can_reach_gap(
|
||||
gap_blocks=2,
|
||||
dy=1.0,
|
||||
sprint=False,
|
||||
momentum_ticks=12,
|
||||
)
|
||||
|
||||
self.assertTrue(ok)
|
||||
self.assertIsNotNone(landing_x)
|
||||
self.assertGreaterEqual(landing_x, needed_x)
|
||||
|
||||
def test_walk_gap3_ascend_does_not_snap_up_after_falling_below_platform(self) -> None:
|
||||
landing = get_landing(
|
||||
sprint=False,
|
||||
target_y=1.0,
|
||||
landing_x_start=3.5,
|
||||
momentum_ticks=12,
|
||||
)
|
||||
|
||||
self.assertIsNone(landing)
|
||||
|
||||
def test_sprint_with_run_up_still_treats_flat_gap5_as_unreachable(self) -> None:
|
||||
ok, landing_x, needed_x = can_reach_gap(
|
||||
gap_blocks=5,
|
||||
dy=0.0,
|
||||
sprint=True,
|
||||
momentum_ticks=12,
|
||||
)
|
||||
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(landing_x is None or landing_x < needed_x)
|
||||
|
||||
def test_sidewall_wo0_margin_is_less_or_equal_to_linear(self) -> None:
|
||||
"""Side wall should never make a jump easier than the open-air linear case."""
|
||||
cases = build_theory_cases()
|
||||
linear_by_key: dict[tuple, float | None] = {}
|
||||
for c in cases:
|
||||
if c.family == "linear":
|
||||
key = (c.subfamily, c.movement_mode, c.momentum_ticks,
|
||||
c.gap_blocks, c.delta_y)
|
||||
linear_by_key[key] = c.margin
|
||||
|
||||
for c in cases:
|
||||
if c.family != "sidewall" or c.wall_offset != 0:
|
||||
continue
|
||||
key = (c.subfamily, c.movement_mode, c.momentum_ticks,
|
||||
c.gap_blocks, c.delta_y)
|
||||
lin_margin = linear_by_key.get(key)
|
||||
if lin_margin is None or c.margin is None:
|
||||
continue
|
||||
self.assertLessEqual(
|
||||
c.margin, lin_margin + 1e-9,
|
||||
f"sidewall margin {c.margin} > linear margin {lin_margin} "
|
||||
f"for {c.case_id}",
|
||||
)
|
||||
|
||||
def test_sidewall_walk_descend_gap3_mm0_is_unreachable_wo0(self) -> None:
|
||||
"""The tight walk descend dy=-2 gap3 mm0 should flip to unreachable with wall."""
|
||||
ok, _, _ = can_reach_gap_with_side_wall(
|
||||
gap_blocks=3, dy=-2.0, wall_offset=0, sprint=False, momentum_ticks=0,
|
||||
)
|
||||
self.assertFalse(ok)
|
||||
|
||||
def test_sidewall_sprint_flat_gap4_mm12_is_still_reachable_wo0(self) -> None:
|
||||
"""Sprint flat gap4 with plenty of margin should survive the wall penalty."""
|
||||
ok, landing_x, needed_x = can_reach_gap_with_side_wall(
|
||||
gap_blocks=4, dy=0.0, wall_offset=0, sprint=True, momentum_ticks=12,
|
||||
)
|
||||
self.assertTrue(ok)
|
||||
self.assertIsNotNone(landing_x)
|
||||
self.assertGreater(landing_x, needed_x)
|
||||
|
||||
def test_theory_markdown_mentions_canonical_live_coverage(self) -> None:
|
||||
markdown = Path("tools/pathing_data/theory-matrix.md").read_text(encoding="utf-8")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue