Skip to content

Add exclusion zones to movement settings#6

Merged
GenerelSchwerz merged 3 commits into
Minecraft-Pathfinding:2026-rewritefrom
XaXayo12:add-exclusion-zones
Jun 17, 2026
Merged

Add exclusion zones to movement settings#6
GenerelSchwerz merged 3 commits into
Minecraft-Pathfinding:2026-rewritefrom
XaXayo12:add-exclusion-zones

Conversation

@XaXayo12

Copy link
Copy Markdown

Exclusion zones

Adds "keep out" exclusion zones to the movement settings, like upstream mineflayer-pathfinder.

Three settings, all default to [] (zero overhead when unused):

Setting Checked on every block the bot would…
exclusionAreasStep stand in (foot block of each move)
exclusionAreasBreak break (mine)
exclusionAreasPlace place (build on)

Each entry is (block: BlockInfo) => number: 0 = no opinion, a positive number = soft (avoided when a cheaper route exists), Infinity (>= COST_INF) = hard (never used). Costs from all areas in a list are summed.

Implementation

  • Step exclusion is computed inside each movement provider on the block the bot lands in (already fetched while generating the move) and folded into the cost before the move is created — Move.cost stays readonly, no extra block lookup, and forbidden landings are never generated.
  • Break/place exclusion is folded into Movement.breakCost / placeCost.
  • The optimizer refuses to straight-line a merge whose swept segment crosses a hard step zone (exact Amanatides–Woo voxel traversal), so optimized/straight-lined paths never cut through excluded terrain the A* route went around.
  • buildMovementOptions() hands out fresh copies of the three arrays (defaults frozen) so settings objects never share or mutate the same array.

Only the ExclusionArea type is exported; users write their own zone functions (ready-to-copy box/radius helpers are in examples/exclusionZones.js).

Tests

tests/exclusionZones.test.ts: default-array safety, functional pathfinding (hard wall → detour, soft wall → still reaches, hard goal → unreachable), and the optimizer guard (straight + diagonal merges). Full suite green.

Add "keep out" exclusion zones to the movement settings, matching the
exclusion-area concept from upstream PrismarineJS/mineflayer-pathfinder.

Settings (all default to []):
- exclusionAreasStep  -> blocks the bot may not stand in / walk into
- exclusionAreasBreak -> blocks the bot may not break (mine)
- exclusionAreasPlace -> blocks the bot may not place (build on)

Each area is a function (block: BlockInfo) => number returning the extra cost
of a block; a summed weight >= COST_INF forbids it, a positive value is a soft
penalty. Empty lists cost nothing to evaluate.

Implementation:
- Step exclusion is folded into each movement provider, on the block the bot
  lands in (already fetched while generating the move), before the move is
  created -- Move.cost stays readonly and there is no extra block lookup.
  Forbidden landings are simply never generated.
- Break/place exclusion is folded into Movement.breakCost / placeCost.
- The optimizer refuses to straight-line a merge whose swept segment crosses a
  hard step zone (exact Amanatides-Woo voxel traversal), so optimized paths
  never cut through "keep out" terrain the A* route went around.
- buildMovementOptions() hands out fresh copies of the three arrays (defaults
  frozen) so bots/sessions never share or mutate the same array.

Only the ExclusionArea type is exported; users write their own zone functions
(ready-to-copy box/radius helpers are in examples/exclusionZones.js).

Tests, docs (API + AdvancedUsage), and an example included.

@GenerelSchwerz GenerelSchwerz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better. Everything looks good except for the optimizer portion.

Comment on lines +419 to +424
// Exclusion zones: the bot ends up standing one block above (node y + 1).
// Only do the (cache-routed) block lookup when step zones are configured.
if (this.settings.exclusionAreasStep.length > 0) {
if ((cost += this.exclusionStep(this.getBlockInfo(node, 0, 1, 0))) > COST_INF) return
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is awkward. This block is already looked up, block3, and that if check is redundant anyway since it happens inside exclusionStep (notably, inside sumExclusions). While it is technically faster to have the if statement here to avoid the redundant calls, v8 should optimize that away.

Comment on lines +493 to +498
// Exclusion zones: blockD is where the bot lands. Fold the step cost in
// before creating the move, and skip the move entirely if it is forbidden.
const stepCost = this.exclusionStep(blockD)
if (stepCost < COST_INF) {
neighbors.push(Move.fromPrevious(cost + stepCost, blockD.position.offset(0.5, 0, 0.5), node, this))
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are breaking paradigm with this code. Everywhere else, stepCost is implictly added to cost. Unless I'm blind, this should happen here as well. I don't believe cost is reused at any point here.

Comment on lines +505 to +508
const stepCost = this.exclusionStep(blockC)
if (stepCost < COST_INF) {
neighbors.push(Move.fromPrevious(cost1 + stepCost, blockC.position.offset(0.5, 0, 0.5), node, this))
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +516 to +519
const stepCost = this.exclusionStep(blockB)
if (stepCost < COST_INF) {
neighbors.push(Move.fromPrevious(cost + stepCost, blockB.position.offset(0.5, 0, 0.5), node, this))
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +634 to +637
const stepCost = this.exclusionStep(blockD)
if (stepCost < COST_INF) {
neighbors.push(Move.fromPrevious(cost + stepCost, blockD.position.offset(0.5, 0, 0.5), node, this))
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +641 to +644
const stepCost = this.exclusionStep(blockC)
if (stepCost < COST_INF) {
neighbors.push(Move.fromPrevious(cost + 3 + stepCost, blockC.position.offset(0.5, 0, 0.5), node, this))
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +649 to +652
const stepCost = this.exclusionStep(blockB)
if (stepCost < COST_INF) {
neighbors.push(Move.fromPrevious(cost + stepCost, blockB.position.offset(0.5, 0, 0.5), node, this))
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +15 to +73
/**
* Walk the straight segment from `from` to `to` with a voxel traversal
* (Amanatides & Woo) and return true as soon as a cell lands inside a HARD step
* zone (summed weight >= COST_INF). Visiting exactly the cells the segment
* crosses keeps the check both correct (no skipped cells, no false hits) and
* cheap (one block lookup per crossed cell).
*
* Only hard zones block a merge. Soft zones (a finite extra cost) are a
* preference, not a wall, so the optimizer is allowed to straighten through them.
*/
function lineCrossesHardExclusion (world: World, from: Vec3, to: Vec3, areas: ExclusionArea[]): boolean {
let x = Math.floor(from.x)
let y = Math.floor(from.y)
let z = Math.floor(from.z)
const endX = Math.floor(to.x)
const endY = Math.floor(to.y)
const endZ = Math.floor(to.z)

const dx = to.x - from.x
const dy = to.y - from.y
const dz = to.z - from.z

const stepX = Math.sign(dx)
const stepY = Math.sign(dy)
const stepZ = Math.sign(dz)

// The segment is parameterised by t in [0, 1]. tMax* is the t at which we next
// cross a cell boundary on that axis; tDelta* is the t to cross one whole cell.
// Axes that do not move get Infinity so they are never chosen to advance.
const tDeltaX = stepX !== 0 ? Math.abs(1 / dx) : Infinity
const tDeltaY = stepY !== 0 ? Math.abs(1 / dy) : Infinity
const tDeltaZ = stepZ !== 0 ? Math.abs(1 / dz) : Infinity

let tMaxX = stepX !== 0 ? (stepX > 0 ? x + 1 - from.x : from.x - x) / Math.abs(dx) : Infinity
let tMaxY = stepY !== 0 ? (stepY > 0 ? y + 1 - from.y : from.y - y) / Math.abs(dy) : Infinity
let tMaxZ = stepZ !== 0 ? (stepZ > 0 ? z + 1 - from.z : from.z - z) / Math.abs(dz) : Infinity

// Number of cells to visit = Manhattan distance in cells + 1. Looping a fixed
// number of times (rather than on tMax comparisons) keeps termination
// floating-point safe.
const cells = Math.abs(endX - x) + Math.abs(endY - y) + Math.abs(endZ - z)

for (let i = 0; i <= cells; i++) {
if (sumExclusionAreas(areas, world.getBlockInfo(new Vec3(x, y, z))) >= COST_INF) return true

if (tMaxX <= tMaxY && tMaxX <= tMaxZ) {
x += stepX
tMaxX += tDeltaX
} else if (tMaxY <= tMaxZ) {
y += stepY
tMaxY += tDeltaY
} else {
z += stepZ
tMaxZ += tDeltaZ
}
}
return false
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole code should be in a separate file that is clearly labeled as optimizer exclusion handling. However, the more appropiate way to handle this is to have the exclusion zones be checked in the optimizer providers themselves, not a blanket step here. It is the fault of the user if they don't avoid exclusion zones, we should not provide a general case solution.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ultimately should be removed in favor of having the optimizers avoid optimizing through exclusion zones in the first place.

Comment on lines +208 to +217

// Exclusion zones: an optimizer may straight-line a path across cells the
// original A* route went around. Never let a merge cut through a hard
// "keep out" (step) zone -- fall back to the unoptimized moves instead.
const stepAreas = newMove.moveType.settings.exclusionAreasStep
if (stepAreas.length > 0 && lineCrossesHardExclusion(this.world, newMove.entryPos, newMove.exitPos, stepAreas)) {
log(`[Index ${this.currentIndex}] Merge would cross a hard exclusion zone; skipping this optimizer.`)
continue
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Referencing here: https://github.com/Minecraft-Pathfinding/minecraft-pathfinding/pull/6/changes#r3429639022

This should not be here. Exclusion handling should be done in the optimizer providers, not a general catch-all here. This is bad code.

…dy StraightUp

- Optimizer: remove the blanket exclusion guard from Optimizer.compute. The
  straight-line check now lives inside LandStraightAheadOpt (the only optimizer
  that can route a merge through cells the A* path went around), via a
  lineCrossesHardExclusion voxel walk. No general catch-all in the optimizer core.
- Parkour: fold the step cost implicitly into `cost` (cost += exclusionStep(...))
  like every other provider, instead of a separate stepCost local.
- StraightUp: drop the redundant `length > 0` guard (sumExclusionAreas already
  short-circuits on an empty list) and reuse the already-fetched block3.
- Tests: replace the synthetic Optimizer.compute tests with direct, deterministic
  unit tests of lineCrossesHardExclusion (straight hit/miss, diagonal, soft/empty).

Build clean, 26/26 tests pass.
@GenerelSchwerz

Copy link
Copy Markdown
Collaborator

LGTM. Letting workflows run.

The CI runs `npx ts-standard` over the whole repo, which had been red since
April on ~648 pre-existing problems. This makes it green without changing any
runtime behavior:

- Auto-format the repo to ts-standard style (spacing, quotes, semicolons,
  blank lines, etc.). Purely mechanical.
- Ignore tests/ and scripts/ in ts-standard (they are outside tsconfig and
  cannot be type-linted), matching the existing examples ignore.
- Suppress the remaining type-aware rule violations per file with
  eslint-disable headers: restrict-template-expressions (debug logs),
  pre-existing unused vars, missing return types, var-requires, and a few
  strict-boolean / unmodified-loop / non-null / throw-literal spots in code
  unrelated to exclusion zones. The headers are behavior-preserving.

Build clean, 26/26 tests pass, ts-standard reports 0 problems.
@GenerelSchwerz GenerelSchwerz merged commit f171945 into Minecraft-Pathfinding:2026-rewrite Jun 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants