feat: add Phase 1 core pathfinding architecture

Implements the new Baritone-inspired A* pathfinding system:
- Core types: PathNode, PathResult, MoveResult, MoveType, ActionCosts
- BinaryHeapOpenSet min-heap for A* open set
- AStarPathFinder with timeout, cancellation, partial path support
- CalculationContext for thread-safe world state snapshots
- MoveHelper for block passability checks
- IGoal interface + GoalBlock, GoalXZ, GoalNear, GoalComposite
- IMove interface + MoveTraverse, MoveDiagonal, MoveAscend, MoveDescend, MoveClimb
- /pathfind command for testing the new pathfinder

Made-with: Cursor
This commit is contained in:
BruceChen 2026-04-11 01:48:10 +08:00
parent 3efe63c54d
commit 1abab20f17
23 changed files with 1222 additions and 0 deletions

View file

@ -0,0 +1,42 @@
using System;
namespace MinecraftClient.Pathing.Goals
{
public sealed class GoalBlock : IGoal
{
public int X { get; }
public int Y { get; }
public int Z { get; }
public GoalBlock(int x, int y, int z)
{
X = x;
Y = y;
Z = z;
}
public bool IsInGoal(int x, int y, int z)
=> x == X && y == Y && z == Z;
public double Heuristic(int x, int y, int z)
{
int dx = Math.Abs(x - X);
int dy = Math.Abs(y - Y);
int dz = Math.Abs(z - Z);
return DistanceHeuristic(dx, dy, dz);
}
internal static double DistanceHeuristic(int dx, int dy, int dz)
{
int horizontal = Math.Max(dx, dz);
int diagonal = Math.Min(dx, dz);
int straight = horizontal - diagonal;
double cost = diagonal * Core.ActionCosts.SprintOneBlock * Core.ActionCosts.DiagonalMultiplier
+ straight * Core.ActionCosts.SprintOneBlock
+ Math.Abs(dy) * Core.ActionCosts.SprintOneBlock;
return cost;
}
public override string ToString() => $"GoalBlock({X}, {Y}, {Z})";
}
}

View file

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
namespace MinecraftClient.Pathing.Goals
{
public sealed class GoalComposite : IGoal
{
private readonly IGoal[] _goals;
public GoalComposite(params IGoal[] goals)
{
ArgumentNullException.ThrowIfNull(goals);
_goals = goals;
}
public GoalComposite(IEnumerable<IGoal> goals)
{
ArgumentNullException.ThrowIfNull(goals);
_goals = goals is IGoal[] arr ? arr : [.. goals];
}
public bool IsInGoal(int x, int y, int z)
{
foreach (var g in _goals)
{
if (g.IsInGoal(x, y, z))
return true;
}
return false;
}
public double Heuristic(int x, int y, int z)
{
double min = double.MaxValue;
foreach (var g in _goals)
{
double h = g.Heuristic(x, y, z);
if (h < min) min = h;
}
return min;
}
public override string ToString() => $"GoalComposite({_goals.Length} goals)";
}
}

View file

@ -0,0 +1,42 @@
using System;
namespace MinecraftClient.Pathing.Goals
{
public sealed class GoalNear : IGoal
{
public int X { get; }
public int Y { get; }
public int Z { get; }
public int Range { get; }
private readonly int _rangeSq;
public GoalNear(int x, int y, int z, int range)
{
X = x;
Y = y;
Z = z;
Range = range;
_rangeSq = range * range;
}
public bool IsInGoal(int x, int y, int z)
{
int dx = x - X;
int dy = y - Y;
int dz = z - Z;
return dx * dx + dy * dy + dz * dz <= _rangeSq;
}
public double Heuristic(int x, int y, int z)
{
int dx = Math.Abs(x - X);
int dy = Math.Abs(y - Y);
int dz = Math.Abs(z - Z);
double h = GoalBlock.DistanceHeuristic(dx, dy, dz);
double reduction = Range * Core.ActionCosts.SprintOneBlock;
return Math.Max(0, h - reduction);
}
public override string ToString() => $"GoalNear({X}, {Y}, {Z}, range={Range})";
}
}

View file

@ -0,0 +1,28 @@
using System;
namespace MinecraftClient.Pathing.Goals
{
public sealed class GoalXZ : IGoal
{
public int X { get; }
public int Z { get; }
public GoalXZ(int x, int z)
{
X = x;
Z = z;
}
public bool IsInGoal(int x, int y, int z)
=> x == X && z == Z;
public double Heuristic(int x, int y, int z)
{
int dx = Math.Abs(x - X);
int dz = Math.Abs(z - Z);
return GoalBlock.DistanceHeuristic(dx, 0, dz);
}
public override string ToString() => $"GoalXZ({X}, {Z})";
}
}

View file

@ -0,0 +1,8 @@
namespace MinecraftClient.Pathing.Goals
{
public interface IGoal
{
bool IsInGoal(int x, int y, int z);
double Heuristic(int x, int y, int z);
}
}