diff --git a/MinecraftClient.Tests/Pathing/Execution/PathPlanningContractTests.cs b/MinecraftClient.Tests/Pathing/Execution/PathPlanningContractTests.cs index ae59865d..1d718fef 100644 --- a/MinecraftClient.Tests/Pathing/Execution/PathPlanningContractTests.cs +++ b/MinecraftClient.Tests/Pathing/Execution/PathPlanningContractTests.cs @@ -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); + } } diff --git a/MinecraftClient.Tests/Pathing/Execution/PathTimingContractTests.cs b/MinecraftClient.Tests/Pathing/Execution/PathTimingContractTests.cs index 70f0e057..a7f417c3 100644 --- a/MinecraftClient.Tests/Pathing/Execution/PathTimingContractTests.cs +++ b/MinecraftClient.Tests/Pathing/Execution/PathTimingContractTests.cs @@ -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); + } } diff --git a/MinecraftClient.Tests/Pathing/Execution/PathingContractBootstrapTests.cs b/MinecraftClient.Tests/Pathing/Execution/PathingContractBootstrapTests.cs index 8842f84a..71fbfa9f 100644 --- a/MinecraftClient.Tests/Pathing/Execution/PathingContractBootstrapTests.cs +++ b/MinecraftClient.Tests/Pathing/Execution/PathingContractBootstrapTests.cs @@ -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))); + } } diff --git a/MinecraftClient.Tests/Pathing/Execution/Scenarios/PathingExecutionScenarioCatalog.cs b/MinecraftClient.Tests/Pathing/Execution/Scenarios/PathingExecutionScenarioCatalog.cs index b89511dc..520180b5 100644 --- a/MinecraftClient.Tests/Pathing/Execution/Scenarios/PathingExecutionScenarioCatalog.cs +++ b/MinecraftClient.Tests/Pathing/Execution/Scenarios/PathingExecutionScenarioCatalog.cs @@ -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; + } } diff --git a/MinecraftClient.Tests/Pathing/Execution/Support/PathingContractAssert.cs b/MinecraftClient.Tests/Pathing/Execution/Support/PathingContractAssert.cs index 1a97476c..f80a05a3 100644 --- a/MinecraftClient.Tests/Pathing/Execution/Support/PathingContractAssert.cs +++ b/MinecraftClient.Tests/Pathing/Execution/Support/PathingContractAssert.cs @@ -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++) { diff --git a/MinecraftClient.Tests/TestData/Pathing/pathing-planner-contracts.json b/MinecraftClient.Tests/TestData/Pathing/pathing-planner-contracts.json index 1106513c..ef519a79 100644 --- a/MinecraftClient.Tests/TestData/Pathing/pathing-planner-contracts.json +++ b/MinecraftClient.Tests/TestData/Pathing/pathing-planner-contracts.json @@ -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 } + } + ] } ] diff --git a/MinecraftClient.Tests/TestData/Pathing/pathing-timing-budgets.json b/MinecraftClient.Tests/TestData/Pathing/pathing-timing-budgets.json index 03d9e01e..d8adf3bb 100644 --- a/MinecraftClient.Tests/TestData/Pathing/pathing-timing-budgets.json +++ b/MinecraftClient.Tests/TestData/Pathing/pathing-timing-budgets.json @@ -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 } + ] } ]