mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: add sidewall parkour planner support
This commit is contained in:
parent
724880f928
commit
e23037a897
5 changed files with 342 additions and 0 deletions
|
|
@ -207,6 +207,35 @@ public sealed class LivePathingRegressionTests
|
|||
Assert.Empty(PathSegmentBuilder.FromPath(result.Path));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SidewallParkourScenarioBuilder.AcceptedCases), MemberType = typeof(SidewallParkourScenarioBuilder))]
|
||||
public void AStar_SidewallAcceptedCases_PlanThroughAllThreeJumps(string scenarioId, int gap, int deltaY, int wallOffset)
|
||||
{
|
||||
PathingExecutionScenario scenario = SidewallParkourScenarioBuilder.Create(scenarioId, gap, deltaY, wallOffset);
|
||||
PathResult result = PathingScenarioRunner.PlanOnly(scenario);
|
||||
List<PathSegment> segments = PathSegmentBuilder.FromPath(result.Path);
|
||||
|
||||
Assert.Equal(PathStatus.Success, result.Status);
|
||||
Assert.Equal(3, segments.FindAll(segment => segment.MoveType == MoveType.Parkour).Count);
|
||||
Assert.All(
|
||||
segments.Where(segment => segment.MoveType == MoveType.Parkour),
|
||||
segment => Assert.Equal(ParkourProfile.Sidewall, segment.ParkourProfile));
|
||||
Assert.Equal(scenario.Goal.X + 0.5, segments[^1].End.X);
|
||||
Assert.Equal(scenario.Goal.Y, segments[^1].End.Y);
|
||||
Assert.Equal(scenario.Goal.Z + 0.5, segments[^1].End.Z);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(SidewallParkourScenarioBuilder.RejectedCases), MemberType = typeof(SidewallParkourScenarioBuilder))]
|
||||
public void AStar_SidewallRejectedCases_RejectBeforeExecution(string scenarioId, int gap, int deltaY, int wallOffset)
|
||||
{
|
||||
PathingExecutionScenario scenario = SidewallParkourScenarioBuilder.Create(scenarioId, gap, deltaY, wallOffset);
|
||||
PathResult result = PathingScenarioRunner.PlanOnly(scenario);
|
||||
|
||||
Assert.Equal(PathStatus.Failed, result.Status);
|
||||
Assert.Empty(PathSegmentBuilder.FromPath(result.Path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AStar_LiveCoordinateLinearDescendGap3DyMinus2_PlansThroughAllThreeJumps()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
using MinecraftClient.Pathing.Moves.Impl;
|
||||
using MinecraftClient.Tests.Pathing.Execution;
|
||||
using Xunit;
|
||||
|
||||
namespace MinecraftClient.Tests.Pathing.Moves;
|
||||
|
||||
public sealed class MoveSidewallParkourTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("sidewall-flat-gap2-wo0", 2, 0, 0)]
|
||||
[InlineData("sidewall-flat-gap3-wo1", 3, 0, 1)]
|
||||
[InlineData("sidewall-ascend-gap2-dy+1-wo0", 2, 1, 0)]
|
||||
[InlineData("sidewall-ascend-gap3-dy+1-wo1", 3, 1, 1)]
|
||||
[InlineData("sidewall-descend-gap2-dy-1-wo0", 2, -1, 0)]
|
||||
[InlineData("sidewall-descend-gap3-dy-1-wo1", 3, -1, 1)]
|
||||
[InlineData("sidewall-descend-gap2-dy-2-wo0", 2, -2, 0)]
|
||||
[InlineData("sidewall-descend-gap3-dy-2-wo1", 3, -2, 1)]
|
||||
public void Calculate_AcceptsTheoryAllowedCases(string scenarioId, int gap, int deltaY, int wallOffset)
|
||||
{
|
||||
World world = SidewallParkourScenarioBuilder.BuildWorld(gap, deltaY, wallOffset);
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var move = new MoveSidewallParkour(xOffset: -1, zOffset: gap, yDelta: deltaY);
|
||||
MoveResult result = default;
|
||||
|
||||
move.Calculate(ctx, 100, 80, 100, ref result);
|
||||
|
||||
Assert.False(result.IsImpossible, scenarioId);
|
||||
Assert.Equal(ParkourProfile.Sidewall, result.ParkourProfile);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("sidewall-flat-gap5-wo0", 5, 0, 0)]
|
||||
[InlineData("sidewall-flat-gap5-wo1", 5, 0, 1)]
|
||||
[InlineData("sidewall-ascend-gap4-dy+1-wo0", 4, 1, 0)]
|
||||
[InlineData("sidewall-ascend-gap4-dy+1-wo1", 4, 1, 1)]
|
||||
[InlineData("sidewall-descend-gap6-dy-1-wo0", 6, -1, 0)]
|
||||
[InlineData("sidewall-descend-gap6-dy-2-wo1", 6, -2, 1)]
|
||||
public void Calculate_RejectsTheoryForbiddenCases(string scenarioId, int gap, int deltaY, int wallOffset)
|
||||
{
|
||||
World world = SidewallParkourScenarioBuilder.BuildWorld(gap, deltaY, wallOffset);
|
||||
var ctx = new CalculationContext(world, allowParkour: true, allowParkourAscend: true);
|
||||
var move = new MoveSidewallParkour(xOffset: -1, zOffset: gap, yDelta: deltaY);
|
||||
MoveResult result = default;
|
||||
|
||||
move.Calculate(ctx, 100, 80, 100, ref result);
|
||||
|
||||
Assert.True(result.IsImpossible, scenarioId);
|
||||
}
|
||||
}
|
||||
|
|
@ -96,6 +96,30 @@ namespace MinecraftClient.Pathing.Core
|
|||
moves.Add(new MoveParkour(0, dz * dist, yDelta: -2));
|
||||
}
|
||||
|
||||
// Sidewall parkour: dominant-axis sprint jumps with a one-block lateral offset.
|
||||
foreach (int dx in offsets)
|
||||
{
|
||||
foreach (int dz in offsets)
|
||||
{
|
||||
foreach (int distance in new[] { 2, 3, 4, 5 })
|
||||
{
|
||||
moves.Add(new MoveSidewallParkour(dx, dz * distance));
|
||||
moves.Add(new MoveSidewallParkour(dx * distance, dz));
|
||||
|
||||
if (distance <= 3)
|
||||
{
|
||||
moves.Add(new MoveSidewallParkour(dx, dz * distance, yDelta: 1));
|
||||
moves.Add(new MoveSidewallParkour(dx * distance, dz, yDelta: 1));
|
||||
}
|
||||
|
||||
moves.Add(new MoveSidewallParkour(dx, dz * distance, yDelta: -1));
|
||||
moves.Add(new MoveSidewallParkour(dx * distance, dz, yDelta: -1));
|
||||
moves.Add(new MoveSidewallParkour(dx, dz * distance, yDelta: -2));
|
||||
moves.Add(new MoveSidewallParkour(dx * distance, dz, yDelta: -2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Diagonal parkour: sprint jumps at angles.
|
||||
// Only include combinations with actual distance <= ~3.2 blocks (conservative)
|
||||
foreach (int dx in offsets)
|
||||
|
|
|
|||
104
MinecraftClient/Pathing/Moves/Impl/MoveSidewallParkour.cs
Normal file
104
MinecraftClient/Pathing/Moves/Impl/MoveSidewallParkour.cs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Pathing.Core;
|
||||
|
||||
namespace MinecraftClient.Pathing.Moves.Impl
|
||||
{
|
||||
public sealed class MoveSidewallParkour : IMove
|
||||
{
|
||||
public MoveType Type => MoveType.Parkour;
|
||||
public int XOffset { get; }
|
||||
public int ZOffset { get; }
|
||||
public bool DynamicY => false;
|
||||
|
||||
private readonly int _yDelta;
|
||||
|
||||
public MoveSidewallParkour(int xOffset, int zOffset, int yDelta = 0)
|
||||
{
|
||||
XOffset = xOffset;
|
||||
ZOffset = zOffset;
|
||||
_yDelta = yDelta;
|
||||
}
|
||||
|
||||
public void Calculate(CalculationContext ctx, int x, int y, int z, ref MoveResult result)
|
||||
{
|
||||
if (!ctx.AllowParkour || !ctx.CanSprint)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_yDelta > 0 && !ctx.AllowParkourAscend)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_yDelta < 0 && -_yDelta > ctx.MaxFallHeight)
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.IsSidewallProfile(XOffset, ZOffset, _yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
Material standingOn = ctx.GetMaterial(x, y - 1, z);
|
||||
if (standingOn.CanBeClimbedOn())
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
Material atFeet = ctx.GetMaterial(x, y, z);
|
||||
if (atFeet.IsLiquid())
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourFeasibility.GetSidewallAxes(XOffset, ZOffset, out int forwardX, out int forwardZ, out int lateralX, out int lateralZ);
|
||||
|
||||
int destX = x + XOffset;
|
||||
int destY = y + _yDelta;
|
||||
int destZ = z + ZOffset;
|
||||
|
||||
if (!ctx.CanWalkThrough(x, y + 2, z))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasDominantAxisRunUp(ctx, x, y, z, forwardX, forwardZ, XOffset, ZOffset, _yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasSidewallArcClearance(ctx, x, y, z, forwardX, forwardZ, lateralX, lateralZ, XOffset, ZOffset, _yDelta))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ParkourFeasibility.HasSidewallLandingClearance(ctx, destX, destY, destZ, forwardX, forwardZ, lateralX, lateralZ))
|
||||
{
|
||||
result.SetImpossible();
|
||||
return;
|
||||
}
|
||||
|
||||
double horizDist = Math.Sqrt((double)(XOffset * XOffset + ZOffset * ZOffset));
|
||||
double cost = _yDelta switch
|
||||
{
|
||||
> 0 => horizDist * ctx.SprintCost + ctx.JumpPenalty * 2,
|
||||
< 0 => horizDist * ctx.SprintCost + ctx.JumpPenalty + ActionCosts.FallCost(-_yDelta),
|
||||
_ => horizDist * ctx.SprintCost + ctx.JumpPenalty,
|
||||
};
|
||||
|
||||
result.Set(destX, destY, destZ, cost, ParkourProfile.Sidewall);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,37 @@ namespace MinecraftClient.Pathing.Moves;
|
|||
|
||||
internal static class ParkourFeasibility
|
||||
{
|
||||
public static bool IsSidewallProfile(int xOffset, int zOffset, int yDelta)
|
||||
{
|
||||
int absX = Math.Abs(xOffset);
|
||||
int absZ = Math.Abs(zOffset);
|
||||
int major = Math.Max(absX, absZ);
|
||||
int minor = Math.Min(absX, absZ);
|
||||
|
||||
return minor == 1
|
||||
&& major >= 2
|
||||
&& major <= 5
|
||||
&& yDelta is >= -2 and <= 1;
|
||||
}
|
||||
|
||||
public static void GetSidewallAxes(int xOffset, int zOffset, out int forwardX, out int forwardZ, out int lateralX, out int lateralZ)
|
||||
{
|
||||
if (Math.Abs(xOffset) > Math.Abs(zOffset))
|
||||
{
|
||||
forwardX = Math.Sign(xOffset);
|
||||
forwardZ = 0;
|
||||
lateralX = 0;
|
||||
lateralZ = Math.Sign(zOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
forwardX = 0;
|
||||
forwardZ = Math.Sign(zOffset);
|
||||
lateralX = Math.Sign(xOffset);
|
||||
lateralZ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasRunUp(
|
||||
CalculationContext ctx,
|
||||
int x,
|
||||
|
|
@ -144,6 +175,109 @@ internal static class ParkourFeasibility
|
|||
return false;
|
||||
}
|
||||
|
||||
public static bool HasDominantAxisRunUp(
|
||||
CalculationContext ctx,
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
int forwardX,
|
||||
int forwardZ,
|
||||
int xOffset,
|
||||
int zOffset,
|
||||
int yDelta)
|
||||
{
|
||||
int major = Math.Max(Math.Abs(xOffset), Math.Abs(zOffset));
|
||||
int maxMajor = yDelta switch
|
||||
{
|
||||
> 0 => 3,
|
||||
< 0 => 5,
|
||||
_ => 4,
|
||||
};
|
||||
|
||||
if (major > maxMajor)
|
||||
return false;
|
||||
|
||||
bool carriedEntry = ctx.PreviousMoveType is MoveType.Parkour or MoveType.Descend;
|
||||
if (carriedEntry)
|
||||
return true;
|
||||
|
||||
for (int i = 1; i <= 2; i++)
|
||||
{
|
||||
int rx = x - (forwardX * i);
|
||||
int rz = z - (forwardZ * i);
|
||||
if (!ctx.CanWalkOn(rx, y - 1, rz) || !IsColumnPassable(ctx, rx, y, rz))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool HasSidewallArcClearance(
|
||||
CalculationContext ctx,
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
int forwardX,
|
||||
int forwardZ,
|
||||
int lateralX,
|
||||
int lateralZ,
|
||||
int xOffset,
|
||||
int zOffset,
|
||||
int yDelta)
|
||||
{
|
||||
int major = Math.Max(Math.Abs(xOffset), Math.Abs(zOffset));
|
||||
int insideWallDepth = 0;
|
||||
|
||||
for (int step = 0; step < 2; step++)
|
||||
{
|
||||
int wx = x + lateralX + (forwardX * step);
|
||||
int wz = z + lateralZ + (forwardZ * step);
|
||||
if (ctx.CanWalkThrough(wx, y, wz) && ctx.CanWalkThrough(wx, y + 1, wz))
|
||||
break;
|
||||
insideWallDepth++;
|
||||
}
|
||||
|
||||
if (insideWallDepth is < 1 or > 2)
|
||||
return false;
|
||||
|
||||
for (int step = 1; step <= major; step++)
|
||||
{
|
||||
int cx = x + (forwardX * step);
|
||||
int cz = z + (forwardZ * step);
|
||||
if (!IsColumnPassable(ctx, cx, y, cz))
|
||||
return false;
|
||||
}
|
||||
|
||||
int outsideX = x - lateralX;
|
||||
int outsideZ = z - lateralZ;
|
||||
return IsColumnPassable(ctx, outsideX, y, outsideZ);
|
||||
}
|
||||
|
||||
public static bool HasSidewallLandingClearance(
|
||||
CalculationContext ctx,
|
||||
int destX,
|
||||
int destY,
|
||||
int destZ,
|
||||
int forwardX,
|
||||
int forwardZ,
|
||||
int lateralX,
|
||||
int lateralZ)
|
||||
{
|
||||
if (!ctx.CanWalkOn(destX, destY - 1, destZ))
|
||||
return false;
|
||||
|
||||
if (!IsColumnPassable(ctx, destX, destY, destZ))
|
||||
return false;
|
||||
|
||||
if (!IsColumnPassable(ctx, destX + forwardX, destY, destZ + forwardZ))
|
||||
return false;
|
||||
|
||||
if (!IsColumnPassable(ctx, destX - lateralX, destY, destZ - lateralZ))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsColumnPassable(CalculationContext ctx, int x, int y, int z)
|
||||
{
|
||||
return ctx.CanWalkThrough(x, y, z)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue