From bd349ee80eaf05f136a41b7ad9f00c8321ee59ef Mon Sep 17 00:00:00 2001
From: XaXayo12 <257781493+XaXayo12@users.noreply.github.com>
Date: Thu, 18 Jun 2026 22:27:28 +0200
Subject: [PATCH 1/3] docs+heuristic: credit Baritone, adopt its octile+Y
heuristic shape
Two follow-ups from review:
1. Specific Baritone credit. The cost constants and the heuristic come from
Baritone (Leijurv & contributors, https://github.com/cabaletta/baritone,
LGPL-3.0). Named the exact sources (ActionCosts, GoalXZ, GoalYLevel) in
costs.ts, goals.ts, docs/MovementCosts.md and a README Credits section.
2. Heuristic, compared to Baritone. Baritone splits the estimate into a
horizontal "octile" part (GoalXZ: diagonal run + straight run) and a vertical
part (GoalYLevel: jump up / fall down), and scales by the SPRINT cost so it is
admissible (optimal paths). Adopted that SHAPE (heuristicXZ + heuristicY in
goals.ts) instead of the rough 3D-Euclidean distance.
Kept the WALK weight rather than Baritone's sprint weight, for a measured
reason: this A* uses a plain binary heap with no f-tie-breaker, so the
admissible sprint weight floods an equal-cost plateau on open ground (a flat
150x40 goal: ~4300 nodes visited vs ~150 with the walk weight, same path).
Baritone avoids that with tie-breaking + short segments we do not have. Walk
weight keeps the fast beeline; node counts are identical to before.
Verified: build + ts-standard clean, 31/31 tests pass, deterministic bench shows
identical visited/generated node counts to the previous heuristic.
---
README.md | 10 ++
docs/MovementCosts.md | 127 ++++++++++++---------
src/mineflayer-specific/goals.ts | 78 +++++++++----
src/mineflayer-specific/movements/costs.ts | 48 +++++---
4 files changed, 169 insertions(+), 94 deletions(-)
diff --git a/README.md b/README.md
index 2fa0214..a9f1fe9 100644
--- a/README.md
+++ b/README.md
@@ -46,4 +46,14 @@ Examples will be added as the project undergoes more development.
| --- | --- |
| [API](https://github.com/GenerelSchwerz/minecraft-pathfinding/blob/main/docs/API.md) | The documentation with the available methods and properties. |
| [Advanced Usage](https://github.com/GenerelSchwerz/minecraft-pathfinding/blob/main/docs/AdvancedUsage.md) | The documentation with the advanced usage of the pathfinder, including the customization of goals and movements. |
+| [Movement Costs](https://github.com/GenerelSchwerz/minecraft-pathfinding/blob/main/docs/MovementCosts.md) | How move costs and the A\* heuristic work (tick-based, credited to Baritone). |
| [Examples](https://github.com/GenerelSchwerz/minecraft-pathfinding/tree/main/examples) | The folder with the examples. |
+
+
Credits
+
+-----
+
+The movement **cost constants** and the A\* **heuristic shape** are taken from
+[Baritone](https://github.com/cabaletta/baritone), the Minecraft pathfinding mod
+by Leijurv and contributors (LGPL-3.0). See its `ActionCosts`, `GoalXZ` and
+`GoalYLevel`. Details, and where we deliberately differ, are in [Movement Costs](https://github.com/GenerelSchwerz/minecraft-pathfinding/blob/main/docs/MovementCosts.md).
diff --git a/docs/MovementCosts.md b/docs/MovementCosts.md
index 77d4ee1..658cb47 100644
--- a/docs/MovementCosts.md
+++ b/docs/MovementCosts.md
@@ -1,119 +1,142 @@
Movement Costs
-How the pathfinder decides which way to go: every possible move is given a
+How the pathfinder decides which way to go. Every possible move is given a
**cost**, and A\* looks for the route with the smallest total cost.
This page explains what that cost means, where the numbers come from, and the
few knobs you can turn.
-- [The big idea: cost = time](#the-big-idea-cost--time)
+- [Cost means time](#cost-means-time)
- [The tick constants](#the-tick-constants)
- [What each move costs](#what-each-move-costs)
-- [The heuristic (A\*'s guess)](#the-heuristic-as-guess)
+- [The heuristic](#the-heuristic)
- [Knobs you can tune](#knobs-you-can-tune)
-## The big idea: cost = time
+## Cost means time
A cost is **how long the bot is busy doing the move, measured in game ticks**.
Minecraft runs at **20 ticks per second**, so one tick is 1/20 of a second.
-Lower cost = faster = better.
+Lower cost means faster, which means better.
Everything is measured in the same unit (ticks), which is the whole point. When
-walking, jumping, falling, and digging are all expressed as *time*, the
-pathfinder can compare them honestly. It can answer questions like "is it faster
-to dig straight through this hill, or walk around it?" — because both options are
-just numbers of ticks.
-
-> Before this, moves used made-up numbers (walk = `1`, diagonal = `1.41`, a jump
-> was `+0.5`, a 3-block drop was `+1.5`, ...). Those numbers had no real-world
-> meaning, so the bot sometimes preferred routes that *looked* cheap but were
-> actually slow.
-
-The numbers come from [Baritone](https://github.com/cabaletta/baritone), a
-mature Minecraft pathfinder. They are hardcoded estimates (`20 / speed`) rather
-than a live physics simulation — close enough to reality, and very fast to
-compute. They live in
+walking, jumping, falling and digging are all expressed as *time*, the pathfinder
+can compare them honestly. It can answer a question like "is it faster to dig
+straight through this hill or walk around it", because both options are just
+numbers of ticks.
+
+> Before this, moves used made-up numbers (walk was `1`, diagonal `1.41`, a jump
+> `+0.5`, a 3-block drop `+1.5`). Those numbers had no real-world meaning, so the
+> bot sometimes preferred routes that *looked* cheap but were actually slow.
+
+### Credit
+
+These cost numbers, and the heuristic shape further down, come from
+[**Baritone**](https://github.com/cabaletta/baritone), the Minecraft pathfinding
+mod by **Leijurv and contributors** (licensed **LGPL-3.0**). The constants come
+from its `ActionCosts` interface
+(`src/main/java/baritone/pathing/movement/ActionCosts.java`), and the heuristic
+from its `GoalXZ` and `GoalYLevel`. They are hardcoded estimates (`20 / speed`)
+rather than a live physics simulation, so they are close to reality and very
+fast to compute. They live in
[`src/mineflayer-specific/movements/costs.ts`](../src/mineflayer-specific/movements/costs.ts).
## The tick constants
| Constant | Ticks | Meaning |
| --- | --- | --- |
-| `SPRINT_ONE_BLOCK_COST` | 3.564 | Sprint one block (fastest ground travel). |
+| `SPRINT_ONE_BLOCK_COST` | 3.564 | Sprint one block, the fastest ground travel. |
| `WALK_ONE_BLOCK_COST` | 4.633 | Walk one block. |
| `WALK_OFF_BLOCK_COST` | 3.706 | Step off the edge of a block (start of a drop). |
| `CENTER_AFTER_FALL_COST` | 0.927 | Re-center after landing. |
| `JUMP_ONE_BLOCK_COST` | 3.163 | The upward arc of a jump. |
-| `LADDER_UP_ONE_COST` | 8.511 | Climb up one block on a ladder/vine. |
+| `LADDER_UP_ONE_COST` | 8.511 | Climb up one block on a ladder or vine. |
| `LADDER_DOWN_ONE_COST` | 6.667 | Climb down one block. |
-| `WALK_ONE_IN_WATER_COST` | 9.091 | Swim/walk one block while in water. |
+| `WALK_ONE_IN_WATER_COST` | 9.091 | Swim or walk one block while in water. |
| `WALK_ONE_OVER_SOUL_SAND_COST` | 9.266 | Walk one block over soul sand. |
| `SNEAK_ONE_BLOCK_COST` | 15.385 | Sneak one block. |
-| `FALL_N_BLOCKS_COST[n]` | table | Ticks to fall `n` blocks (not linear — you speed up). |
+| `FALL_N_BLOCKS_COST[n]` | table | Ticks to fall `n` blocks. Not linear, because you speed up as you fall. |
| `COST_INF` | 1,000,000 | "Impossible." Any move this expensive is thrown away. |
## What each move costs
Each movement provider in
[`movementProviders.ts`](../src/mineflayer-specific/movements/movementProviders.ts)
-adds up the ticks for the thing it does:
+adds up the ticks for the thing it does.
| Move | Base cost (ticks) |
| --- | --- |
-| **Forward** | `travelCost(1)` — sprint/walk one block. |
-| **Diagonal** | `travelCost(√2)` — a diagonal block is √2 blocks of travel. |
-| **ForwardJump** (up 1) | `max(JUMP, WALK)` — you rise and move at once, so it's the slower of the two, not the sum. |
+| **Forward** | `travelCost(1)`, sprint or walk one block. |
+| **Diagonal** | `travelCost(√2)`, because a diagonal block is √2 blocks of travel. |
+| **ForwardJump** (up 1) | `max(JUMP, WALK)`. You rise and move at the same time, so it is the slower of the two, not the sum. |
| **ForwardDropDown** | `WALK_OFF_BLOCK_COST + FALL_N_BLOCKS_COST[height]`. |
-| **StraightDown** | `FALL_N_BLOCKS_COST[height]` — pure falling, no sideways travel. |
-| **StraightUp** | ladder → `LADDER_UP_ONE_COST`; otherwise `JUMP_ONE_BLOCK_COST` + a place cost to tower up. |
-| **ParkourForward** | `JUMP_ONE_BLOCK_COST + travelCost(gap)` — the jump plus sprinting across the gap. |
+| **StraightDown** | `FALL_N_BLOCKS_COST[height]`, pure falling with no sideways travel. |
+| **StraightUp** | On a ladder, `LADDER_UP_ONE_COST`. Otherwise `JUMP_ONE_BLOCK_COST` plus a place cost to tower up. |
+| **ParkourForward** | `JUMP_ONE_BLOCK_COST + travelCost(gap)`, the jump plus sprinting across the gap. |
| **ParkourDiagonal** | `JUMP_ONE_BLOCK_COST + travelCost(gap)`. |
On top of the base cost a move also pays for any blocks it must **break**
-(`breakCost`, the block's real mining time in ticks) or **place**
-(`placeCost`). `travelCost(blocks)` is a small helper on `Movement` that returns
+(`breakCost`, the block's real mining time in ticks) or **place** (`placeCost`).
+`travelCost(blocks)` is a small helper on `Movement` that returns
`blocks × (sprinting ? SPRINT : WALK)`.
-## The heuristic (A\*'s guess)
+## The heuristic
A\* also needs to *guess* the remaining cost from any point to the goal. That
-guess is the **heuristic** (in [`goals.ts`](../src/mineflayer-specific/goals.ts)):
+guess is the **heuristic** (in [`goals.ts`](../src/mineflayer-specific/goals.ts)).
+Its shape is Baritone's (`GoalXZ` and `GoalYLevel`). It splits the guess into a
+horizontal part and a vertical part instead of one 3D straight-line distance,
+because the bot moves that way. It travels along the 8 compass directions on the
+ground, and up or down is a separate jump or fall.
```
-heuristic = straight_line_distance × COST_HEURISTIC
-```
+heuristic = heuristicXZ(dx, dz) + heuristicY(dy)
-`COST_HEURISTIC` is the **walk** cost per block (4.633), not the cheaper
-**sprint** cost (3.564). That choice is deliberate:
+heuristicXZ split into a diagonal run (shorter axis, √2 per block) and a
+ straight run (the rest), then times COST_HEURISTIC (octile)
+heuristicY going up costs dy × JUMP_ONE_BLOCK_COST
+ going down costs dy × FALL_N_BLOCKS_COST[2] / 2
+```
-- A\* finds the *truly* shortest route only when its guess never costs more than
- the cheapest real move (sprinting). Guessing with the slightly-higher walk
- cost makes A\* a lightly **"weighted"** search: it leans harder toward the
- goal and explores far fewer dead-ends, so it stays **fast**.
-- The price is that returned paths can be up to `walk / sprint = 4.633 / 3.564
- ≈ 1.3×` the perfect optimum. That is a small, bounded amount.
-- Back when move costs were made-up numbers (~1 per block) but the heuristic was
- already ~4.633 per block, that same ratio was ~**4.6×** — paths could be far
- from optimal. Putting move costs in ticks shrinks the bound from ~4.6× to
- ~1.3× **without making the search any slower.**
+### Walk weight vs sprint weight (where we differ from Baritone)
-In short: same speed, much better paths.
+`COST_HEURISTIC` is the **walk** cost per block (4.633), not the cheaper
+**sprint** cost (3.564). **Baritone uses sprint** (its `costHeuristic` default is
+about 3.563), which never over-estimates the cheapest move, so Baritone's A\* is
+guaranteed to return the optimal path. We deliberately use walk instead, and the
+reason is measured, not guessed.
+
+- This A\* uses a plain binary heap with **no f-tie-breaker**. With the admissible
+ sprint weight, open ground fills with a plateau of equal-cost nodes and the
+ search floods through them. On a flat `150×40` goal that was about **4300 nodes
+ visited**. The walk weight visits about **150** for the same path, roughly
+ **28×** less work. Baritone dodges this with tie-breaking and short path
+ segments that this implementation does not have.
+- Using walk runs a lightly **weighted** A\*. It leans toward the goal and
+ beelines, which is fast. The price is that paths can be up to about `1.3×`
+ (`walk / sprint`) the optimum, which is small and bounded.
+- Before move costs were in ticks (about 1 per block) the heuristic was already
+ about 4.633, so that ratio was about **4.6×**. Tick costs already shrank it to
+ about **1.3×**.
+
+In short, we took Baritone's heuristic shape, which is a clear win, and kept a
+walk weight for speed because our A\* cannot afford the admissible version. Node
+counts stay as fast as before, with much better path quality.
## Knobs you can tune
Pass these in `moveSettings` (see [AdvancedUsage](./AdvancedUsage.md)). All are
-in ticks; the defaults are tuned to match reality, so you rarely need to change
+in ticks. The defaults are tuned to match reality, so you rarely need to change
them.
| Option | Default | What it does |
| --- | --- | --- |
-| `liquidCost` | `WALK_ONE_IN_WATER - WALK_ONE_BLOCK` (≈ 4.46) | Extra ticks per block for moving through water. |
+| `liquidCost` | `WALK_ONE_IN_WATER - WALK_ONE_BLOCK` (about 4.46) | Extra ticks per block for moving through water. |
| `placeCost` | `20` | Ticks charged for placing a block. Placing interrupts movement, so it is deliberately expensive (Baritone's value). |
| `digCost` | `1` | Multiplier on a block's real mining time. Raise it to make digging look worse and prefer going around. |
| `jumpCost` | `0` | Extra ticks per jump on top of the physical jump arc. Raise it to discourage jumpy paths. |
-Example — make the bot hate digging and never want to swim:
+An example that makes the bot hate digging and never want to swim:
```js
bot.pathfinder.setMoveOptions({
diff --git a/src/mineflayer-specific/goals.ts b/src/mineflayer-specific/goals.ts
index c6f6dd7..c913fd7 100644
--- a/src/mineflayer-specific/goals.ts
+++ b/src/mineflayer-specific/goals.ts
@@ -1,7 +1,7 @@
import { Vec3 } from 'vec3'
import { Goal as AGoal } from '../abstract'
import { Move } from './move'
-import { COST_HEURISTIC } from './movements/costs'
+import { COST_HEURISTIC, FALL_N_BLOCKS_COST, JUMP_ONE_BLOCK_COST } from './movements/costs'
import { World } from './world/worldInterface'
import { AABB } from '@nxg-org/mineflayer-util-plugin'
import { PlaceHandler } from './movements/interactionUtils'
@@ -11,6 +11,53 @@ import type { Block } from '../types'
import { BotEvents } from 'mineflayer'
import type { Entity } from 'prismarine-entity'
+// ===========================================================================
+// A* heuristic helpers.
+//
+// CREDIT: ported from Baritone (Leijurv & contributors,
+// https://github.com/cabaletta/baritone, LGPL-3.0) so our estimate has the same
+// shape as our real move costs (everything in game ticks):
+// GoalXZ.calculate -> heuristicXZ (horizontal, "octile" distance)
+// GoalYLevel.calculate -> heuristicY (vertical: jump up / fall down)
+//
+// Splitting horizontal and vertical (instead of one 3D straight-line distance)
+// matches how the bot actually moves: it can only travel along the 8 compass
+// directions on the ground, and up/down is a separate jump or fall.
+//
+// NOTE: Baritone scales this by the SPRINT cost (admissible). We scale by the
+// WALK cost instead. See COST_HEURISTIC in costs.ts for the measured reason
+// (our heap has no tie-breaker, so the admissible version explores ~28x more).
+// ===========================================================================
+
+/**
+ * Horizontal part of the heuristic (Baritone `GoalXZ.calculate`).
+ *
+ * A horizontal trip is made of a diagonal run (along the shorter axis, where
+ * one step covers both x and z) plus a straight run for whatever is left over.
+ * A diagonal block is `sqrt(2)` blocks of travel. The whole thing is scaled by
+ * {@link COST_HEURISTIC} ticks per block.
+ */
+function heuristicXZ (dx: number, dz: number): number {
+ const x = Math.abs(dx)
+ const z = Math.abs(dz)
+ const diagonal = Math.min(x, z)
+ const straight = Math.max(x, z) - diagonal
+ return (diagonal * Math.SQRT2 + straight) * COST_HEURISTIC
+}
+
+/**
+ * Vertical part of the heuristic (Baritone `GoalYLevel.calculate`).
+ *
+ * `dy` is `goalY - nodeY`: positive means the goal is above us (we must climb,
+ * roughly one jump per block) and negative means it is below us (we fall, which
+ * is cheaper per block, about half of a 2-block fall).
+ */
+function heuristicY (dy: number): number {
+ if (dy > 0) return dy * JUMP_ONE_BLOCK_COST
+ if (dy < 0) return -dy * (FALL_N_BLOCKS_COST[2] / 2)
+ return 0
+}
+
/**
* The abstract goal definition used by the pathfinder.
*/
@@ -279,13 +326,8 @@ export class GoalBlock extends Goal {
}
heuristic (node: Move): number {
- // return 0;
- const dx = this.x - node.x
- const dy = this.y - node.y
- const dz = this.z - node.z
- return Math.sqrt(dx * dx + dz * dz + dy * dy) * COST_HEURISTIC
- // return (Math.sqrt(dx * dx + dz * dz) + Math.abs(dy))
- // return distanceXZ(dx, dz) + Math.abs(dy)
+ // Baritone-style: horizontal (octile) + vertical (jump up / fall down).
+ return heuristicXZ(this.x - node.x, this.z - node.z) + heuristicY(this.y - node.y)
}
distHeuristic (node: Move): number {
@@ -330,10 +372,7 @@ export class GoalNear extends Goal {
}
heuristic (node: Move): number {
- const dx = this.x - node.x
- const dy = this.y - node.y
- const dz = this.z - node.z
- return Math.sqrt(dx * dx + dz * dz + dy * dy) * COST_HEURISTIC
+ return heuristicXZ(this.x - node.x, this.z - node.z) + heuristicY(this.y - node.y)
}
distHeuristic (node: Move): number {
@@ -362,9 +401,7 @@ export class GoalNearXZ extends Goal {
}
heuristic (node: Move): number {
- const dx = this.x - node.x
- const dz = this.z - node.z
- return Math.sqrt(dx * dx + dz * dz) * COST_HEURISTIC
+ return heuristicXZ(this.x - node.x, this.z - node.z)
}
distHeuristic (node: Move): number {
@@ -416,10 +453,7 @@ export class GoalLookAt extends Goal {
}
heuristic (node: Move): number {
- const dx = this.x - node.x
- const dy = this.y - (node.y + this.eyeHeight) // eye level
- const dz = this.z - node.z
- return Math.sqrt(dx * dx + dz * dz + dy * dy) * COST_HEURISTIC
+ return heuristicXZ(this.x - node.x, this.z - node.z) + heuristicY(this.y - (node.y + this.eyeHeight))
}
distHeuristic (node: Move): number {
@@ -556,11 +590,7 @@ export class GoalFollowEntity extends GoalDynamic<'entityMoved', 'entityGone'> {
}
heuristic (node: Move): number {
- const dx = this.x - node.x
- const dy = this.y - node.y
- const dz = this.z - node.z
-
- return Math.sqrt(dx * dx + dy * dy + dz * dz) * COST_HEURISTIC
+ return heuristicXZ(this.x - node.x, this.z - node.z) + heuristicY(this.y - node.y)
}
distHeuristic (node: Move): number {
diff --git a/src/mineflayer-specific/movements/costs.ts b/src/mineflayer-specific/movements/costs.ts
index ae52b04..2030bd4 100644
--- a/src/mineflayer-specific/movements/costs.ts
+++ b/src/mineflayer-specific/movements/costs.ts
@@ -1,5 +1,5 @@
// ===========================================================================
-// Movement cost constants — every value below is measured in GAME TICKS.
+// Movement cost constants. Every value below is measured in GAME TICKS.
//
// Minecraft runs at 20 ticks per second, so "ticks" is just "how long the bot
// is busy doing this thing". Lower = faster. Putting every cost in the same
@@ -7,11 +7,15 @@
// "walk around" vs "dig through" vs "jump up" honestly, because they are all
// expressed as time, not as made-up numbers.
//
-// The numbers are Baritone's hardcoded estimates. Each one is `20 / speed`,
-// where `speed` is how many blocks per second the bot covers doing that action
-// (e.g. the player walks at 4.317 blocks/s, so walking one block ~ 20 / 4.317
-// ~ 4.633 ticks). They are "correct enough" — close to in-game reality without
-// running a physics simulation for every candidate move.
+// CREDIT: these constants and formulas are taken from Baritone, the Minecraft
+// pathfinding mod by Leijurv and contributors (https://github.com/cabaletta/baritone,
+// licensed LGPL-3.0). Specifically the `ActionCosts` interface
+// (src/main/java/baritone/pathing/movement/ActionCosts.java) and the fall-cost
+// helpers around it. Each value is `20 / speed`, where `speed` is how many blocks
+// per second the bot covers doing that action (e.g. the player walks at
+// 4.317 blocks/s, so walking one block ~ 20 / 4.317 ~ 4.633 ticks). They are
+// "correct enough", close to in-game reality without running a physics
+// simulation for every candidate move.
// ===========================================================================
/** Ticks to WALK one block (player walks 4.317 blocks/s). */
@@ -26,7 +30,7 @@ export const LADDER_UP_ONE_COST = 8.511 as const
export const LADDER_DOWN_ONE_COST = 6.667 as const
/** Ticks to sneak one block (slowest way to move). */
export const SNEAK_ONE_BLOCK_COST = 15.385 as const
-/** Ticks to SPRINT one block (player sprints 5.612 blocks/s — the fastest ground travel). */
+/** Ticks to SPRINT one block (player sprints 5.612 blocks/s, the fastest ground travel). */
export const SPRINT_ONE_BLOCK_COST = 3.564 as const
/** Sprinting is this fraction of the time of walking (sprint / walk). */
export const SPRINT_MULTIPLIER = 0.769 as const
@@ -34,23 +38,31 @@ export const SPRINT_MULTIPLIER = 0.769 as const
export const WALK_OFF_BLOCK_COST = 3.706 as const
/** Ticks to re-center on the block you just dropped onto. */
export const CENTER_AFTER_FALL_COST = 0.927 as const
-/** "Infinitely expensive" — any move costing this much or more is treated as impossible. */
+/** "Infinitely expensive". Any move costing this much or more is treated as impossible. */
export const COST_INF = 1000000
/**
* Per-block weight used by the goal HEURISTIC (A*'s estimate of remaining cost),
- * also measured in ticks. It is deliberately the WALK cost, not the cheaper
- * SPRINT cost.
+ * in ticks. It is deliberately the WALK cost, not the cheaper SPRINT cost.
*
- * Why walk and not sprint? A* stays optimal only when the heuristic never
- * over-estimates the *cheapest* real cost (sprinting). By estimating with the
- * slightly-higher walk cost we run a lightly "weighted" A*: it leans toward the
- * goal and explores far fewer dead-ends (fast), while the paths it returns are
- * at most ~1.3x the true optimum (walk / sprint = 4.633 / 3.564). Before this
- * file's costs were in ticks, the same heuristic was ~4.6x the per-move cost,
- * so that bound used to be ~4.6x — this is a large quality win at no speed cost.
+ * Baritone uses the SPRINT cost here (its `costHeuristic` default is ~3.563),
+ * which never over-estimates the cheapest real move, so its A* is guaranteed to
+ * return the optimal path. We intentionally differ, and here is the measured
+ * reason: this A* uses a plain binary heap with NO f-tie-breaker, so an
+ * admissible (sprint) heuristic leaves a huge plateau of equal-`f` nodes on open
+ * ground and floods through them. Benchmarked on a flat 150x40 goal, the sprint
+ * heuristic visited ~4300 nodes; the walk weight below visited ~150, the same
+ * path, ~28x less work. Baritone avoids the plateau with tie-breaking and short
+ * path segments that this implementation does not have.
+ *
+ * Using the walk cost runs a lightly "weighted" A*: it leans toward the goal and
+ * beelines (fast), and returned paths are at most ~1.3x the true optimum
+ * (walk / sprint = 4.633 / 3.564). Before move costs were in ticks the same
+ * heuristic was ~4.6x the per-move cost, so that bound was ~4.6x. Tick costs
+ * already shrank it to ~1.3x. The heuristic SHAPE (split horizontal "octile" +
+ * vertical jump/fall) is Baritone's; see `heuristicXZ`/`heuristicY` in goals.ts.
*/
-export const COST_HEURISTIC = 20 / 4.317
+export const COST_HEURISTIC = WALK_ONE_BLOCK_COST
/**
* Lookup table: `FALL_N_BLOCKS_COST[n]` = ticks to fall `n` blocks straight down.
From 7fc1298f17e9216eab1e20c1950f73e26d92137b Mon Sep 17 00:00:00 2001
From: XaXayo12 <257781493+XaXayo12@users.noreply.github.com>
Date: Fri, 19 Jun 2026 17:55:49 +0200
Subject: [PATCH 2/3] fix(goals): GoalLookAt.isEnd gates on real distance, not
the tick heuristic
Review catch (chatgpt-codex): isEnd() used heuristic() as a distance gate
(`dist > this.distance + 3`). Now that heuristic() is the octile+vertical TICK
estimate, a normal close look (e.g. standing diagonally adjacent to a floor
block, ~1.3 blocks away) scores ~7.6 ticks and is rejected before the raycast;
the old Euclidean-scaled value was ~6.1 and passed. Use distHeuristic() (real
block distance) for the gate, which is what `this.distance` is measured in.
Affects GoalLookAt and its subclasses GoalMineBlock / GoalPlaceBlock.
---
src/mineflayer-specific/goals.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/mineflayer-specific/goals.ts b/src/mineflayer-specific/goals.ts
index c913fd7..19a282f 100644
--- a/src/mineflayer-specific/goals.ts
+++ b/src/mineflayer-specific/goals.ts
@@ -468,7 +468,10 @@ export class GoalLookAt extends Goal {
* TODO: account for entity collision (prismarine-world currently does not support this).
*/
isEnd (node: Move): boolean {
- const dist = this.heuristic(node)
+ // Gate on real block distance, not the tick heuristic. heuristic() is now an
+ // octile+vertical TICK estimate (e.g. ~7.6 for a 1.3-block diagonal look),
+ // which would wrongly fail this `> distance + 3` check before the raycast.
+ const dist = this.distHeuristic(node)
if (dist > this.distance + 3) return false
const pos = new Vec3(node.x, node.y + this.eyeHeight, node.z)
From 1bd3e26e6961466f6f7f95f08494e99288e0f83d Mon Sep 17 00:00:00 2001
From: XaXayo12 <257781493+XaXayo12@users.noreply.github.com>
Date: Sat, 20 Jun 2026 10:48:08 +0200
Subject: [PATCH 3/3] fix(goals): GoalBlock.distHeuristic returns raw blocks,
not ticks
Review catch (deep review): GoalBlock.distHeuristic multiplied by COST_HEURISTIC
(tick units) while every other goal's distHeuristic returns raw block distance
(GoalNear, GoalNearXZ, GoalLookAt, GoalFollowEntity). The only magnitude consumer
is PartialPathProducer.maxPathLength = min(partialPathLength, goal.distHeuristic(start)),
compared against result.path.length (a move COUNT). So GoalBlock's inflated value
(~4.6x) let a GoalBlock partial segment grow ~4.6x longer than the same-distance
GoalNear/GoalNearXZ segment before being cut. Pre-existing inconsistency surfaced
while reviewing the heuristic changes; drop the factor so all goals agree.
Verified: build + ts-standard clean, 31/31 tests pass.
---
src/mineflayer-specific/goals.ts | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/mineflayer-specific/goals.ts b/src/mineflayer-specific/goals.ts
index 19a282f..f05db73 100644
--- a/src/mineflayer-specific/goals.ts
+++ b/src/mineflayer-specific/goals.ts
@@ -331,11 +331,14 @@ export class GoalBlock extends Goal {
}
distHeuristic (node: Move): number {
+ // Raw block distance (NOT ticks). The only consumer, PartialPathProducer's
+ // maxPathLength, compares this to a move COUNT, so it must match every other
+ // goal's distHeuristic (which all return raw blocks). The stray COST_HEURISTIC
+ // here inflated GoalBlock's partial-segment cap ~4.6x vs other goal types.
const dx = this.x - node.x
const dy = this.y - node.y
const dz = this.z - node.z
- const distance = Math.sqrt(dx * dx + dz * dz + dy * dy) * COST_HEURISTIC
- return distance
+ return Math.sqrt(dx * dx + dz * dz + dy * dy)
}
isEnd (node: Move): boolean {