test: add jump combo planner and timing contracts

This commit is contained in:
BruceChen 2026-04-13 17:08:32 +00:00
parent ba1dc32ccb
commit eb9d6ce356
7 changed files with 328 additions and 1 deletions

View file

@ -225,4 +225,21 @@ public sealed class PathPlanningContractTests
PathingContractAssert.PlannerMatches(contract, PathSegmentBuilder.FromPath(planResult.Path), planResult);
}
[Theory]
[InlineData("repeated-cardinal-parkour-chain")]
[InlineData("repeated-diagonal-parkour-chain")]
[InlineData("obstructed-parkour-l-turns")]
[InlineData("vertical-jump-mix")]
[InlineData("diagonal-vertical-mix")]
public void JumpCombo_PlannerMatchesContract(string scenarioId)
{
PathingExecutionScenario scenario = PathingExecutionScenarioCatalog.Get(scenarioId);
PathResult planResult = PathingScenarioRunner.PlanOnly(scenario);
PathingContractAssert.PlannerMatches(
PathingContractStore.LoadFromRepositoryRoot().GetPlanner(scenarioId),
PathSegmentBuilder.FromPath(planResult.Path),
planResult);
}
}

View file

@ -29,4 +29,19 @@ public sealed class PathTimingContractTests
PathingContractAssert.TimingMatches(budget, result);
}
[Theory]
[InlineData("repeated-cardinal-parkour-chain")]
[InlineData("repeated-diagonal-parkour-chain")]
[InlineData("obstructed-parkour-l-turns")]
[InlineData("vertical-jump-mix")]
[InlineData("diagonal-vertical-mix")]
public void JumpCombo_ExecutionStaysWithinBudget(string scenarioId)
{
PathingExecutionScenario scenario = PathingExecutionScenarioCatalog.Get(scenarioId);
PathingTimingBudget budget = PathingContractStore.LoadFromRepositoryRoot().GetTiming(scenarioId);
PathingScenarioResult result = PathingScenarioRunner.RunAccepted(scenario);
PathingContractAssert.TimingMatches(budget, result);
}
}

View file

@ -23,4 +23,20 @@ public sealed class PathingContractBootstrapTests
if (planResult.Status == PathStatus.Success)
_output.WriteLine(PathingContractBootstrapWriter.WriteTimingFragment(scenarioId, PathingScenarioRunner.RunAccepted(scenario)));
}
[Theory]
[InlineData("repeated-cardinal-parkour-chain")]
[InlineData("repeated-diagonal-parkour-chain")]
[InlineData("obstructed-parkour-l-turns")]
[InlineData("vertical-jump-mix")]
[InlineData("diagonal-vertical-mix")]
public void PrintJumpComboContractFragments(string scenarioId)
{
PathingExecutionScenario scenario = PathingExecutionScenarioCatalog.Get(scenarioId);
PathResult planResult = PathingScenarioRunner.PlanOnly(scenario);
_output.WriteLine(PathingContractBootstrapWriter.WritePlannerFragment(scenarioId, planResult));
if (planResult.Status == PathStatus.Success)
_output.WriteLine(PathingContractBootstrapWriter.WriteTimingFragment(scenarioId, PathingScenarioRunner.RunAccepted(scenario)));
}
}

View file

@ -43,6 +43,51 @@ internal static class PathingExecutionScenarioCatalog
StartYaw = 270f,
MaxExecutionTicks = 80
},
"repeated-cardinal-parkour-chain" => new PathingExecutionScenario
{
Id = scenarioId,
BuildWorld = BuildRepeatedCardinalParkourChain,
Start = new Location(580.5, 80, 580.5),
Goal = new GoalBlock(588, 80, 580),
StartYaw = 270f,
MaxExecutionTicks = 420
},
"repeated-diagonal-parkour-chain" => new PathingExecutionScenario
{
Id = scenarioId,
BuildWorld = BuildRepeatedDiagonalParkourChain,
Start = new Location(600.5, 80, 600.5),
Goal = new GoalBlock(606, 80, 606),
StartYaw = 315f,
MaxExecutionTicks = 420
},
"obstructed-parkour-l-turns" => new PathingExecutionScenario
{
Id = scenarioId,
BuildWorld = BuildObstructedParkourLTurns,
Start = new Location(620.5, 80, 620.5),
Goal = new GoalBlock(626, 80, 622),
StartYaw = 270f,
MaxExecutionTicks = 420
},
"vertical-jump-mix" => new PathingExecutionScenario
{
Id = scenarioId,
BuildWorld = BuildVerticalJumpMix,
Start = new Location(640.5, 80, 620.5),
Goal = new GoalBlock(648, 80, 620),
StartYaw = 270f,
MaxExecutionTicks = 420
},
"diagonal-vertical-mix" => new PathingExecutionScenario
{
Id = scenarioId,
BuildWorld = BuildDiagonalVerticalMix,
Start = new Location(680.5, 80, 620.5),
Goal = new GoalBlock(684, 80, 624),
StartYaw = 315f,
MaxExecutionTicks = 420
},
_ => throw new ArgumentOutOfRangeException(nameof(scenarioId), scenarioId, null)
};
@ -88,4 +133,70 @@ internal static class PathingExecutionScenarioCatalog
FlatWorldTestBuilder.SetSolid(world, 143, 80, 138);
return world;
}
private static World BuildRepeatedCardinalParkourChain()
{
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);
return world;
}
private static World BuildRepeatedDiagonalParkourChain()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 598, max: 608);
FlatWorldTestBuilder.ClearBox(world, 598, 79, 598, 608, 90, 608);
FlatWorldTestBuilder.SetSolid(world, 600, 79, 600);
FlatWorldTestBuilder.SetSolid(world, 602, 79, 602);
FlatWorldTestBuilder.SetSolid(world, 604, 79, 604);
FlatWorldTestBuilder.SetSolid(world, 606, 79, 606);
return world;
}
private static World BuildObstructedParkourLTurns()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 618, max: 628);
FlatWorldTestBuilder.ClearBox(world, 618, 79, 618, 628, 90, 624);
FlatWorldTestBuilder.SetSolid(world, 620, 79, 620);
FlatWorldTestBuilder.SetSolid(world, 622, 79, 620);
FlatWorldTestBuilder.SetSolid(world, 622, 79, 621);
FlatWorldTestBuilder.SetSolid(world, 624, 79, 621);
FlatWorldTestBuilder.SetSolid(world, 624, 79, 622);
FlatWorldTestBuilder.SetSolid(world, 626, 79, 622);
FlatWorldTestBuilder.SetSolid(world, 620, 80, 621);
FlatWorldTestBuilder.SetSolid(world, 620, 81, 621);
FlatWorldTestBuilder.SetSolid(world, 622, 80, 622);
FlatWorldTestBuilder.SetSolid(world, 622, 81, 622);
return world;
}
private static World BuildVerticalJumpMix()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 618, max: 650);
FlatWorldTestBuilder.ClearBox(world, 638, 79, 618, 650, 80, 622);
FlatWorldTestBuilder.ClearBox(world, 638, 81, 618, 650, 92, 622);
FlatWorldTestBuilder.SetSolid(world, 640, 79, 620);
FlatWorldTestBuilder.SetSolid(world, 642, 80, 620);
FlatWorldTestBuilder.SetSolid(world, 644, 79, 620);
FlatWorldTestBuilder.SetSolid(world, 646, 80, 620);
FlatWorldTestBuilder.SetSolid(world, 648, 79, 620);
return world;
}
private static World BuildDiagonalVerticalMix()
{
World world = FlatWorldTestBuilder.CreateStoneFloor(min: 618, max: 686);
FlatWorldTestBuilder.ClearBox(world, 678, 79, 618, 686, 80, 626);
FlatWorldTestBuilder.ClearBox(world, 678, 81, 618, 686, 92, 626);
FlatWorldTestBuilder.SetSolid(world, 680, 79, 620);
FlatWorldTestBuilder.SetSolid(world, 681, 80, 621);
FlatWorldTestBuilder.SetSolid(world, 682, 79, 622);
FlatWorldTestBuilder.SetSolid(world, 683, 80, 623);
FlatWorldTestBuilder.SetSolid(world, 684, 79, 624);
return world;
}
}

View file

@ -32,7 +32,7 @@ internal static class PathingContractAssert
internal static void TimingMatches(PathingTimingBudget budget, PathingScenarioResult result)
{
if (!result.Completed)
throw new XunitException("navigation did not complete");
throw new XunitException($"navigation did not complete\n{Format(result, budget)}");
if (result.ReplanCount != 0)
throw new XunitException($"expected 0 replans, saw {result.ReplanCount}\n{Format(result, budget)}");
@ -53,6 +53,7 @@ internal static class PathingContractAssert
private static string Format(PathingScenarioResult result, PathingTimingBudget budget)
{
var sb = new StringBuilder();
sb.AppendLine($"completed={result.Completed} replans={result.ReplanCount}");
sb.AppendLine($"route actual={result.TotalTicks} expected={budget.ExpectedTotalTicks} max={budget.MaxTotalTicks}");
for (int i = 0; i < result.SegmentRuns.Count; i++)
{

View file

@ -91,5 +91,120 @@
"scenarioId": "rejected-3x1-invalid-goal",
"expectedStatus": "Failed",
"segments": []
},
{
"scenarioId": "repeated-cardinal-parkour-chain",
"expectedStatus": "Success",
"segments": [
{
"moveType": "Parkour",
"startBlock": { "x": 580, "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, "z": 580 }
},
{
"moveType": "Parkour",
"startBlock": { "x": 584, "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, "z": 580 }
}
]
},
{
"scenarioId": "repeated-diagonal-parkour-chain",
"expectedStatus": "Success",
"segments": [
{
"moveType": "Parkour",
"startBlock": { "x": 600, "y": 80, "z": 600 },
"endBlock": { "x": 602, "y": 80, "z": 602 }
},
{
"moveType": "Parkour",
"startBlock": { "x": 602, "y": 80, "z": 602 },
"endBlock": { "x": 604, "y": 80, "z": 604 }
},
{
"moveType": "Parkour",
"startBlock": { "x": 604, "y": 80, "z": 604 },
"endBlock": { "x": 606, "y": 80, "z": 606 }
}
]
},
{
"scenarioId": "obstructed-parkour-l-turns",
"expectedStatus": "Success",
"segments": [
{
"moveType": "Parkour",
"startBlock": { "x": 620, "y": 80, "z": 620 },
"endBlock": { "x": 622, "y": 80, "z": 620 }
},
{
"moveType": "Parkour",
"startBlock": { "x": 622, "y": 80, "z": 620 },
"endBlock": { "x": 624, "y": 80, "z": 621 }
},
{
"moveType": "Parkour",
"startBlock": { "x": 624, "y": 80, "z": 621 },
"endBlock": { "x": 626, "y": 80, "z": 622 }
}
]
},
{
"scenarioId": "vertical-jump-mix",
"expectedStatus": "Success",
"segments": [
{
"moveType": "Parkour",
"startBlock": { "x": 640, "y": 80, "z": 620 },
"endBlock": { "x": 642, "y": 81, "z": 620 }
},
{
"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, "z": 620 }
},
{
"moveType": "Descend",
"startBlock": { "x": 646, "y": 81, "z": 620 },
"endBlock": { "x": 648, "y": 80, "z": 620 }
}
]
},
{
"scenarioId": "diagonal-vertical-mix",
"expectedStatus": "Success",
"segments": [
{
"moveType": "Ascend",
"startBlock": { "x": 680, "y": 80, "z": 620 },
"endBlock": { "x": 681, "y": 81, "z": 621 }
},
{
"moveType": "Parkour",
"startBlock": { "x": 681, "y": 81, "z": 621 },
"endBlock": { "x": 683, "y": 81, "z": 623 }
},
{
"moveType": "Descend",
"startBlock": { "x": 683, "y": 81, "z": 623 },
"endBlock": { "x": 684, "y": 80, "z": 624 }
}
]
}
]

View file

@ -39,5 +39,57 @@
"expectedTotalTicks": 0,
"maxTotalTicks": 0,
"segments": []
},
{
"scenarioId": "repeated-cardinal-parkour-chain",
"expectedTotalTicks": 0,
"maxTotalTicks": 2,
"segments": [
{ "moveType": "Parkour", "expectedTicks": 61, "maxTicks": 74 },
{ "moveType": "Parkour", "expectedTicks": 61, "maxTicks": 74 },
{ "moveType": "Parkour", "expectedTicks": 61, "maxTicks": 74 },
{ "moveType": "Parkour", "expectedTicks": 27, "maxTicks": 33 }
]
},
{
"scenarioId": "repeated-diagonal-parkour-chain",
"expectedTotalTicks": 20,
"maxTotalTicks": 24,
"segments": [
{ "moveType": "Parkour", "expectedTicks": 61, "maxTicks": 74 },
{ "moveType": "Parkour", "expectedTicks": 61, "maxTicks": 74 },
{ "moveType": "Parkour", "expectedTicks": 20, "maxTicks": 24 }
]
},
{
"scenarioId": "obstructed-parkour-l-turns",
"expectedTotalTicks": 0,
"maxTotalTicks": 2,
"segments": [
{ "moveType": "Parkour", "expectedTicks": 27, "maxTicks": 33 },
{ "moveType": "Parkour", "expectedTicks": 27, "maxTicks": 33 },
{ "moveType": "Parkour", "expectedTicks": 27, "maxTicks": 33 }
]
},
{
"scenarioId": "vertical-jump-mix",
"expectedTotalTicks": 33,
"maxTotalTicks": 40,
"segments": [
{ "moveType": "Parkour", "expectedTicks": 13, "maxTicks": 16 },
{ "moveType": "Descend", "expectedTicks": 201, "maxTicks": 242 },
{ "moveType": "Parkour", "expectedTicks": 19, "maxTicks": 23 },
{ "moveType": "Descend", "expectedTicks": 14, "maxTicks": 17 }
]
},
{
"scenarioId": "diagonal-vertical-mix",
"expectedTotalTicks": 31,
"maxTotalTicks": 38,
"segments": [
{ "moveType": "Ascend", "expectedTicks": 81, "maxTicks": 98 },
{ "moveType": "Parkour", "expectedTicks": 16, "maxTicks": 20 },
{ "moveType": "Descend", "expectedTicks": 15, "maxTicks": 18 }
]
}
]