Skip to content

Commit ceb3dbb

Browse files
committed
Add path consistency tests and graph pruning improvements
Introduce extensive path consistency tests for scenarios with different graph connectivities and blocked nodes using D* Lite. Add graph pruning logic to exclude nodes marked as blocked and enhance debugging utilities for path validation.
1 parent 19daf8a commit ceb3dbb

File tree

11 files changed

+638
-166
lines changed

11 files changed

+638
-166
lines changed

common/src/main/kotlin/com/lambda/command/commands/PathCommand.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.lambda.brigadier.optional
2727
import com.lambda.brigadier.required
2828
import com.lambda.command.LambdaCommand
2929
import com.lambda.module.modules.movement.Pathfinder
30+
import com.lambda.pathing.move.MoveFinder
3031
import com.lambda.util.Communication.info
3132
import com.lambda.util.extension.CommandBuilder
3233
import com.lambda.util.world.fastVectorOf
@@ -62,8 +63,9 @@ object PathCommand : LambdaCommand(
6263
val v = fastVectorOf(x().value(), y().value(), z().value())
6364
val pruneGraph = if (prune != null) {
6465
prune().value()
65-
} else true
66-
Pathfinder.dStar.invalidate(v, pruneGraph = pruneGraph)
66+
} else false
67+
Pathfinder.dStar.invalidate(v, pruneGraph)
68+
MoveFinder.clear(v)
6769
Pathfinder.needsUpdate = true
6870
this@PathCommand.info("Invalidated ${v.string}")
6971
}
@@ -107,7 +109,7 @@ object PathCommand : LambdaCommand(
107109
required(integer("Z", -30000000, 30000000)) { z ->
108110
execute {
109111
val v = fastVectorOf(x().value(), y().value(), z().value())
110-
this@PathCommand.info("Successors: ${Pathfinder.graph.successors[v]?.keys?.joinToString { it.string }}")
112+
this@PathCommand.info("Successors: ${Pathfinder.graph.successors[v]?.entries?.joinToString { "${it.key.string}: ${it.value}" }}")
111113
}
112114
}
113115
}
@@ -120,7 +122,7 @@ object PathCommand : LambdaCommand(
120122
required(integer("Z", -30000000, 30000000)) { z ->
121123
execute {
122124
val v = fastVectorOf(x().value(), y().value(), z().value())
123-
this@PathCommand.info("Predecessors: ${Pathfinder.graph.predecessors[v]?.keys?.joinToString { it.string }}")
125+
this@PathCommand.info("Predecessors: ${Pathfinder.graph.predecessors[v]?.entries?.joinToString { "${it.key.string}: ${it.value}" }}")
124126
}
125127
}
126128
}

common/src/main/kotlin/com/lambda/module/modules/movement/Pathfinder.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.lambda.event.events.MovementEvent
2222
import com.lambda.event.events.RenderEvent
2323
import com.lambda.event.events.RotationEvent
2424
import com.lambda.event.events.TickEvent
25+
import com.lambda.event.events.WorldEvent
2526
import com.lambda.event.listener.SafeListener.Companion.listen
2627
import com.lambda.graphics.gl.Matrices
2728
import com.lambda.graphics.renderer.esp.builders.buildFilled
@@ -46,6 +47,7 @@ import com.lambda.pathing.move.TraverseMove
4647
import com.lambda.threading.runSafe
4748
import com.lambda.threading.runSafeConcurrent
4849
import com.lambda.util.Communication.info
50+
import com.lambda.util.Formatting.asString
4951
import com.lambda.util.Formatting.string
5052
import com.lambda.util.math.setAlpha
5153
import com.lambda.util.player.MovementUtils.buildMovementInput
@@ -129,11 +131,13 @@ object Pathfinder : Module(
129131
// info("${isPathClear(playerPos, targetPos)}")
130132
}
131133

132-
// listen<WorldEvent.BlockUpdate.Client> {
133-
// val pos = it.pos.toFastVec()
134-
// graph.markDirty(pos)
135-
// info("Updated block at ${it.pos} to ${it.newState.block.name.string} rescheduled D*Lite.")
136-
// }
134+
listen<WorldEvent.BlockUpdate.Client> {
135+
val pos = it.pos.toFastVec()
136+
MoveFinder.clear(pos)
137+
dStar.invalidate(pos, pathing.pruneGraph)
138+
needsUpdate = true
139+
info("Updated block at ${it.pos.asString()} to ${it.newState.block.name.string} rescheduled D*Lite.")
140+
}
137141

138142
listen<RotationEvent.StrafeInput> { event ->
139143
if (!pathing.moveAlongPath) return@listen

common/src/main/kotlin/com/lambda/pathing/PathingConfig.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.lambda.util.NamedEnum
2222

2323
interface PathingConfig {
2424
val algorithm: PathingAlgorithm
25+
val pruneGraph: Boolean
2526
val cutoffTimeout: Long
2627
val maxFallHeight: Double
2728
val mlg: Boolean

common/src/main/kotlin/com/lambda/pathing/PathingSettings.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class PathingSettings(
3131
private val page by c.setting("Pathing Page", Page.Pathfinding, "Current page", vis)
3232

3333
override val algorithm by c.setting("Algorithm", PathingConfig.PathingAlgorithm.A_STAR) { vis() && page == Page.Pathfinding }
34+
override val pruneGraph by c.setting("Prune Graph", true) { vis() && page == Page.Pathfinding && algorithm == PathingConfig.PathingAlgorithm.D_STAR_LITE }
3435
override val cutoffTimeout by c.setting("Cutoff Timeout", 500L, 1L..2000L, 10L, "Timeout of path calculation", " ms") { vis() && page == Page.Pathfinding }
3536
override val maxFallHeight by c.setting("Max Fall Height", 3.0, 0.0..30.0, 0.5) { vis() && page == Page.Pathfinding }
3637
override val mlg by c.setting("Do MLG", false) { vis() && page == Page.Pathfinding }

common/src/main/kotlin/com/lambda/pathing/dstar/DStarLite.kt

Lines changed: 30 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -144,139 +144,35 @@ class DStarLite(
144144
}
145145

146146
/**
147-
* Invalidates a node (e.g., it became an obstacle) and updates affected neighbors.
147+
* Invalidates a node and updates affected neighbors.
148148
* Also updates the neighbors of neighbors to ensure diagonal paths are correctly recalculated.
149149
* Optionally prunes the graph after invalidation to remove unnecessary nodes and edges.
150150
*
151151
* @param u The node to invalidate
152-
* @param pruneGraph Whether to prune the graph after invalidation
153152
*/
154-
fun invalidate(u: FastVector, pruneGraph: Boolean = true) {
155-
val newNodes = mutableSetOf<FastVector>()
156-
val affectedNeighbors = mutableSetOf<FastVector>()
157-
val pathNodes = mutableSetOf<FastVector>()
158-
val modifiedNodes = mutableSetOf<FastVector>()
159-
160-
// Add the invalidated node to the modified nodes
161-
modifiedNodes.add(u)
162-
163-
// First, collect all neighbors of the invalidated node
164-
val neighbors = graph.neighbors(u)
165-
affectedNeighbors.addAll(neighbors)
166-
modifiedNodes.addAll(neighbors)
167-
168-
// Set g and rhs values of the invalidated node to infinity
169-
setG(u, INF)
170-
setRHS(u, INF)
171-
updateVertex(u)
172-
173-
// Update edges between the invalidated node and its neighbors
174-
neighbors.forEach { v ->
175-
val current = graph.successors(v)
153+
fun invalidate(u: FastVector, prune: Boolean = false) {
154+
val modified = mutableSetOf(u)
155+
(graph.neighbors(u) + u).forEach { v ->
156+
val current = graph.neighbors(v)
176157
val updated = graph.nodeInitializer(v)
177-
val removed = current.filter { (w, _) -> w !in updated }
178-
179-
// Only add new nodes that are directly connected to the current path
180-
// This reduces unnecessary node generation
181-
updated.keys.filter { w -> w !in current.keys && w != u }.forEach {
182-
// Check if this node is likely to be on a new path
183-
if (g(v) < INF) {
184-
newNodes.add(it)
185-
modifiedNodes.add(it)
186-
}
187-
}
188-
189-
// Set the edge cost between u and v to infinity (blocked)
190-
updateEdge(u, v, INF)
191-
updateEdge(v, u, INF)
192-
193-
// Update removed and new edges for this neighbor
194-
removed.forEach { (w, _) ->
158+
val removed = current.filter { w -> w !in updated }
159+
removed.forEach { w ->
195160
updateEdge(v, w, INF)
196161
updateEdge(w, v, INF)
197-
modifiedNodes.add(w)
198162
}
199163
updated.forEach { (w, c) ->
200164
updateEdge(v, w, c)
201165
updateEdge(w, v, c)
202-
modifiedNodes.add(w)
203-
}
204-
}
205-
206-
// Now, update only the neighbors of neighbors that are likely to be on the new path
207-
// This is crucial when a node in a diagonal path is blocked
208-
neighbors.forEach { v ->
209-
// Only process neighbors that are likely to be on the path
210-
if (g(v) >= INF) return@forEach
211-
pathNodes.add(v)
212-
213-
// Get all neighbors of this neighbor (excluding the original invalidated node)
214-
// Only consider neighbors that are likely to be on the path
215-
val secondaryNeighbors = graph.neighbors(v).filter { it != u && g(it) < INF }
216-
217-
// For each secondary neighbor, reinitialize its edges
218-
secondaryNeighbors.forEach { w ->
219-
// Add to affected neighbors for later rhs update
220-
affectedNeighbors.add(w)
221-
pathNodes.add(w)
222-
modifiedNodes.add(w)
223-
224-
// Reinitialize edges for this secondary neighbor
225-
val currentW = graph.successors(w)
226-
val updatedW = graph.nodeInitializer(w)
227-
228-
// Update edges for this secondary neighbor
229-
// Only update edges to nodes that are likely to be on the path
230-
updatedW.forEach { (z, c) ->
231-
if (z != u) { // Don't create edges to the invalidated node
232-
updateEdge(w, z, c)
233-
updateEdge(z, w, c)
234-
modifiedNodes.add(z)
235-
236-
// If this node has a finite g-value, it's likely on the path
237-
if (g(z) < INF) {
238-
pathNodes.add(z)
239-
}
240-
}
241-
}
242-
243-
// Add any new nodes discovered, but only if they're likely to be on the path
244-
updatedW.keys.filter { z -> z !in currentW.keys && z != u }.forEach {
245-
// Check if this node is connected to a node on the path
246-
if (g(w) < INF) {
247-
newNodes.add(it)
248-
modifiedNodes.add(it)
249-
}
250-
}
251-
}
252-
}
253-
254-
// Ensure all edges to/from the invalidated node are set to infinity
255-
// First, get all current successors and predecessors of the invalidated node
256-
val currentSuccessors = graph.successors(u).keys.toSet()
257-
val currentPredecessors = graph.predecessors(u).keys.toSet()
258-
259-
// Set all edges to/from the invalidated node to infinity
260-
(currentSuccessors + currentPredecessors + graph.nodes).forEach { node ->
261-
if (node != u) {
262-
updateEdge(node, u, INF)
263-
updateEdge(u, node, INF)
264-
modifiedNodes.add(node)
265-
}
266-
}
267-
268-
// Update rhs values for all affected nodes
269-
(affectedNeighbors + newNodes).forEach { node ->
270-
if (node != goal) {
271-
setRHS(node, minSuccessorCost(node))
272-
updateVertex(node)
273166
}
167+
modified.addAll(removed + updated.keys + v)
274168
}
169+
if (prune) prune(modified)
170+
}
275171

276-
// Prune the graph if requested
277-
if (pruneGraph) {
278-
// Prune the graph, passing the modified nodes for targeted pruning
279-
graph.prune(modifiedNodes)
172+
private fun prune(modifiedNodes: Set<FastVector> = emptySet()) {
173+
graph.prune(modifiedNodes).forEach {
174+
gMap.remove(it)
175+
rhsMap.remove(it)
280176
}
281177
}
282178

@@ -289,14 +185,21 @@ class DStarLite(
289185
*/
290186
fun updateEdge(u: FastVector, v: FastVector, c: Double) {
291187
val cOld = graph.cost(u, v)
292-
if (cOld == c) return
293188
graph.setCost(u, v, c)
189+
// LOG.info("Setting edge ${u.string} -> ${v.string} to $c")
294190
if (cOld > c) {
295-
if (u != goal) setRHS(u, min(rhs(u), c + g(v)))
191+
if (u != goal) {
192+
setRHS(u, min(rhs(u), c + g(v)))
193+
// LOG.info("Setting RHS of ${u.string} to ${rhs(u)}")
194+
}
296195
} else if (rhs(u) == cOld + g(v)) {
297-
if (u != goal) setRHS(u, minSuccessorCost(u))
196+
if (u != goal) {
197+
setRHS(u, minSuccessorCost(u))
198+
// LOG.info("Setting RHS of ${u.string} to ${rhs(u)}")
199+
}
298200
}
299201
updateVertex(u)
202+
// LOG.info("Updated vertex ${u.string}")
300203
}
301204

302205
/**
@@ -309,38 +212,19 @@ class DStarLite(
309212
*/
310213
fun path(maxLength: Int = 10_000): List<FastVector> {
311214
val path = mutableListOf<FastVector>()
312-
if (start !in graph) return path.toList() // Start not even known
215+
if (start !in graph) return emptyList()
216+
if (rhs(start) == INF) return emptyList()
313217

314218
var current = start
315219
path.add(current)
316220

317221
var iterations = 0
318222
while (current != goal && iterations < maxLength) {
319223
iterations++
320-
val successors = graph.successors(current)
321-
if (successors.isEmpty()) break // Dead end
322-
323-
var bestNext: FastVector? = null
324-
var minCost = INF
325-
326-
// Find successor s' that minimizes c(current, s') + g(s')
327-
for ((succ, cost) in successors) {
328-
if (cost == INF) continue // Skip impassable edges explicitly
329-
val costPlusG = cost + g(succ)
330-
if (costPlusG < minCost) {
331-
minCost = costPlusG
332-
bestNext = succ
333-
}
334-
}
335-
336-
if (bestNext == null) break // No path found
337-
338-
current = bestNext
339-
if (current !in path) { // Avoid trivial cycles
340-
path.add(current)
341-
} else {
342-
break // Cycle detected
343-
}
224+
val cheapest = graph.successors(current)
225+
.minByOrNull { (succ, cost) -> cost + g(succ) } ?: break
226+
current = cheapest.key
227+
if (current !in path) path.add(current) else break
344228
}
345229
return path
346230
}

common/src/main/kotlin/com/lambda/pathing/dstar/LazyGraph.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,7 @@ class LazyGraph(
9999
*
100100
* @param modifiedNodes A set of nodes that have been modified and need to be checked for pruning
101101
*/
102-
fun prune(modifiedNodes: Set<FastVector> = emptySet()) {
103-
// Nodes to check for pruning
102+
fun prune(modifiedNodes: Set<FastVector> = emptySet()): Set<FastVector> {
104103
val nodesToCheck = if (modifiedNodes.isEmpty()) {
105104
// If no modified nodes specified, check all nodes
106105
nodes.toSet()
@@ -154,26 +153,27 @@ class LazyGraph(
154153

155154
// Remove nodes with only infinite connections
156155
nodesToRemove.forEach { removeNode(it) }
156+
return nodesToRemove
157157
}
158158

159159
/**
160160
* Returns the successors of a node without initializing it if it doesn't exist.
161161
* This is useful for debugging and testing.
162162
*/
163163
fun getSuccessorsWithoutInitializing(u: FastVector): Map<FastVector, Double> {
164-
return successors[u] ?: emptyMap()
164+
return successors[u]?.filter { it.value.isFinite() } ?: emptyMap()
165165
}
166166

167167
/**
168168
* Returns the predecessors of a node without initializing it if it doesn't exist.
169169
* This is useful for debugging and testing.
170170
*/
171171
fun getPredecessorsWithoutInitializing(u: FastVector): Map<FastVector, Double> {
172-
return predecessors[u] ?: emptyMap()
172+
return predecessors[u]?.filter { it.value.isFinite() } ?: emptyMap()
173173
}
174174

175175
fun edges(u: FastVector) = successors(u).entries + predecessors(u).entries
176-
fun neighbors(u: FastVector): Set<FastVector> = successors(u).keys + predecessors(u).keys
176+
fun neighbors(u: FastVector): Set<FastVector> = getSuccessorsWithoutInitializing(u).keys + getPredecessorsWithoutInitializing(u).keys
177177

178178
/** Returns the cost of the edge from u to v (or ∞ if none exists) */
179179
fun cost(u: FastVector, v: FastVector): Double = successors(u)[v] ?: Double.POSITIVE_INFINITY

common/src/main/kotlin/com/lambda/pathing/move/MoveFinder.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,15 @@ import kotlin.reflect.KFunction1
5151
object MoveFinder {
5252
private val nodeTypeCache = HashMap<FastVector, NodeType>()
5353

54-
fun SafeContext.moveOptions(origin: FastVector, heuristic: KFunction1<FastVector, Double>, config: PathingConfig) =
55-
EightWayDirection.entries.flatMap { direction ->
54+
fun SafeContext.moveOptions(origin: FastVector, heuristic: KFunction1<FastVector, Double>, config: PathingConfig): Set<Move> {
55+
val nodeType = findPathType(origin)
56+
if (nodeType == NodeType.BLOCKED) return emptySet()
57+
return EightWayDirection.entries.flatMap { direction ->
5658
(-1..1).mapNotNull { y ->
5759
getPathNode(heuristic, origin, direction, y, config)
5860
}
59-
}
61+
}.toSet()
62+
}
6063

6164
private fun SafeContext.getPathNode(
6265
heuristic: KFunction1<FastVector, Double>,
@@ -158,5 +161,6 @@ object MoveFinder {
158161
return blockPos.y.toDouble() + (if (voxelShape.isEmpty) 0.0 else voxelShape.getMax(Direction.Axis.Y))
159162
}
160163

164+
fun clear(u: FastVector) = nodeTypeCache.remove(u)
161165
fun clean() = nodeTypeCache.clear()
162166
}

0 commit comments

Comments
 (0)