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 {