Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions docs/MovementCosts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<h1 align="center">Movement Costs</h1>

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)
- [The tick constants](#the-tick-constants)
- [What each move costs](#what-each-move-costs)
- [The heuristic (A\*'s guess)](#the-heuristic-as-guess)
- [Knobs you can tune](#knobs-you-can-tune)

## The big idea: cost = 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.

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
[`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). |
| `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_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_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). |
| `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:

| 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. |
| **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. |
| **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
`blocks × (sprinting ? SPRINT : WALK)`.

## The heuristic (A\*'s guess)

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)):

```
heuristic = straight_line_distance × COST_HEURISTIC
```

`COST_HEURISTIC` is the **walk** cost per block (4.633), not the cheaper
**sprint** cost (3.564). That choice is deliberate:

- 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.**

In short: same speed, much better paths.

## 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
them.

| Option | Default | What it does |
| --- | --- | --- |
| `liquidCost` | `WALK_ONE_IN_WATER - WALK_ONE_BLOCK` (≈ 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:

```js
bot.pathfinder.setMoveOptions({
digCost: 5, // mining looks 5x slower than it is
liquidCost: 40 // water is very costly per block
})
```
13 changes: 7 additions & 6 deletions src/mineflayer-specific/goals.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Vec3 } from 'vec3'
import { Goal as AGoal } from '../abstract'
import { Move } from './move'
import { COST_HEURISTIC } from './movements/costs'
import { World } from './world/worldInterface'
import { AABB } from '@nxg-org/mineflayer-util-plugin'
import { PlaceHandler } from './movements/interactionUtils'
Expand Down Expand Up @@ -282,7 +283,7 @@ export class GoalBlock extends Goal {
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) * (20 / 4.317) // from baritone.
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)
}
Expand All @@ -291,7 +292,7 @@ export class GoalBlock extends Goal {
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) * (20 / 4.317) // from baritone.
const distance = Math.sqrt(dx * dx + dz * dz + dy * dy) * COST_HEURISTIC
return distance
}

Expand Down Expand Up @@ -332,7 +333,7 @@ export class GoalNear extends Goal {
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) * (20 / 4.317) // from baritone.
return Math.sqrt(dx * dx + dz * dz + dy * dy) * COST_HEURISTIC
}

distHeuristic (node: Move): number {
Expand Down Expand Up @@ -363,7 +364,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) * (20 / 4.317) // from baritone.
return Math.sqrt(dx * dx + dz * dz) * COST_HEURISTIC
}

distHeuristic (node: Move): number {
Expand Down Expand Up @@ -418,7 +419,7 @@ export class GoalLookAt extends Goal {
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) * (20 / 4.317) // from baritone.
return Math.sqrt(dx * dx + dz * dz + dy * dy) * COST_HEURISTIC
}

distHeuristic (node: Move): number {
Expand Down Expand Up @@ -559,7 +560,7 @@ export class GoalFollowEntity extends GoalDynamic<'entityMoved', 'entityGone'> {
const dy = this.y - node.y
const dz = this.z - node.z

return Math.sqrt(dx * dx + dy * dy + dz * dz) * (20 / 4.317) // from baritone.
return Math.sqrt(dx * dx + dy * dy + dz * dz) * COST_HEURISTIC
}

distHeuristic (node: Move): number {
Expand Down
64 changes: 51 additions & 13 deletions src/mineflayer-specific/movements/costs.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,70 @@
// blatantly stolen from Baritone.

// export const WALK_ONE_BLOCK_COST = 20 / 4.317;
// export const WALK_ONE_IN_WATER_COST = 20 / 2.2;
// export const WALK_ONE_OVER_SOUL_SAND_COST = WALK_ONE_BLOCK_COST * 2;
// export const LADDER_UP_ONE_COST = 20 / 2.35;
// export const LADDER_DOWN_ONE_COST = 20 / 3.0;
// export const SNEAK_ONE_BLOCK_COST = 20 / 1.3;
// export const SPRINT_ONE_BLOCK_COST = 20 / 5.612;
// export const SPRINT_MULTIPLIER = SPRINT_ONE_BLOCK_COST / WALK_ONE_BLOCK_COST;
// export const WALK_OFF_BLOCK_COST = WALK_ONE_BLOCK_COST * 0.8;
// export const CENTER_AFTER_FALL_COST = WALK_ONE_BLOCK_COST - WALK_OFF_BLOCK_COST;
// export const COST_INF = 1000000;
// ===========================================================================
// 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
// unit (ticks) is the whole point of this file: the pathfinder can now compare
// "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.
// ===========================================================================

/** Ticks to WALK one block (player walks 4.317 blocks/s). */
export const WALK_ONE_BLOCK_COST = 4.633 as const
/** Ticks to swim/walk one block while IN water (much slower than on land). */
export const WALK_ONE_IN_WATER_COST = 9.091 as const
/** Ticks to walk one block on top of soul sand (it drags you). */
export const WALK_ONE_OVER_SOUL_SAND_COST = 9.266 as const
/** Ticks to climb UP one block on a ladder/vine. */
export const LADDER_UP_ONE_COST = 8.511 as const
/** Ticks to climb DOWN one block on a ladder/vine. */
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). */
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
/** Ticks to walk OFF the edge of a block (start of a drop; slightly cheaper than a full walk). */
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. */
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.
*
* 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.
*/
export const COST_HEURISTIC = 20 / 4.317

/**
* Lookup table: `FALL_N_BLOCKS_COST[n]` = ticks to fall `n` blocks straight down.
* Falling speeds up over time, so this is not linear (falling 2 blocks costs less
* than twice falling 1). Index it with a whole number of blocks.
*/
export const FALL_N_BLOCKS_COST = generateFallNBlocksCost()
export const FALL_1_25_BLOCKS_COST = distanceToTicks(1.25)
export const FALL_0_25_BLOCKS_COST = distanceToTicks(0.25)
/**
* Ticks for the upward arc of a normal jump. A jump lifts the bot ~1.25 blocks
* and it lands back ~0.25 blocks lower than the peak, so the "useful" climb costs
* the difference between those two fall times.
*/
export const JUMP_ONE_BLOCK_COST = FALL_1_25_BLOCKS_COST - FALL_0_25_BLOCKS_COST

export function distanceToTicks (distance: number): number {
Expand Down
52 changes: 38 additions & 14 deletions src/mineflayer-specific/movements/movement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,27 @@ import { BreakHandler, InteractHandler, PlaceHandler } from './interactionUtils'
import type { InteractType } from './interactionUtils'
import type { Block } from '../../types'
import { Vec3Properties } from '../../types'
import { COST_INF } from './costs'
import { COST_INF, SPRINT_ONE_BLOCK_COST, WALK_ONE_BLOCK_COST, WALK_ONE_IN_WATER_COST } from './costs'
import type { ExclusionArea } from './exclusionZones'

export interface MovementOptions {
allowDiagonalBridging: boolean
allowJumpSprint: boolean
allow1by1towers: boolean

// --- Cost knobs (all in GAME TICKS; see costs.ts) -----------------------
/** Extra ticks per block when moving through liquid (water is slow). */
liquidCost: number
/** Multiplier on a block's real mining time. 1 = use the true dig time as-is. */
digCost: number
forceLook: boolean
/** Extra ticks charged per jump, on top of the physical jump arc. 0 = trust the physics. */
jumpCost: number
/** Ticks charged for placing a block. Placing interrupts movement, so it is deliberately costly. */
placeCost: number
/** Ticks charged for killing horizontal velocity (e.g. stopping at the edge of a drop). */
velocityKillCost: number

forceLook: boolean
canOpenDoors: boolean
canDig: boolean
canPlace: boolean
Expand Down Expand Up @@ -65,10 +73,14 @@ export const DEFAULT_MOVEMENT_OPTS: MovementOptions = {
maxDropDown: 3,
infiniteLiquidDropdownDistance: true,
allowSprinting: true,
liquidCost: 3,
placeCost: 2,
// Extra time water adds over walking the same block on land (~4.46 ticks).
liquidCost: WALK_ONE_IN_WATER_COST - WALK_ONE_BLOCK_COST,
// Baritone's block-placement penalty: building a block to stand on is slow.
placeCost: 20,
// Keep a block's real mining time as-is (1x). Raise to make digging look worse.
digCost: 1,
jumpCost: 0.5,
// The jump arc already has a real tick cost (see JUMP_ONE_BLOCK_COST); add no fudge on top.
jumpCost: 0,
velocityKillCost: 2, // implement at a later date.
forceLook: true,
careAboutLookAlignment: true,
Expand Down Expand Up @@ -244,6 +256,16 @@ export abstract class Movement {
return block.physical ? 0 : COST_INF
}

/**
* Ticks to travel `blocks` blocks of flat ground.
*
* The bot sprints when allowed (the fastest way to move), otherwise it walks.
* Pass `1` for one straight block, `Math.SQRT2` for one diagonal block, etc.
*/
travelCost (blocks: number): number {
return blocks * (this.settings.allowSprinting ? SPRINT_ONE_BLOCK_COST : WALK_ONE_BLOCK_COST)
}

/** Extra cost of STANDING in this block (sum of every step exclusion area; 0 if none). */
exclusionStep (block: BlockInfo): number {
return sumExclusionAreas(this.settings.exclusionAreasStep, block)
Expand Down Expand Up @@ -334,22 +356,24 @@ export abstract class Movement {
return cost
}

/**
* Ticks to break (mine) `block` with the bot's best tool.
*
* `pathingUtil.digCost` returns the real dig time in MILLISECONDS, and the
* game runs at 20 ticks/s (50 ms/tick), so `ms / 50` is the dig time in ticks
* — the same unit as every other movement cost. `digCost` (default 1) is a
* multiplier so callers can make mining look cheaper or dearer.
*/
breakCost (block: BlockInfo): number {
if (block.block === null) return COST_INF // Don't know its type, but that's only replaceables so just return.

// const tool = this.bot.pathfinder.bestHarvestTool(block)

const digTime = this.bot.pathingUtil.digCost(block.block)
// const tool = null as any;
// const enchants = (tool && tool.nbt) ? nbt.simplify(tool.nbt).Enchantments : []
// const effects = this.bot.entity.effects
// const digTime = block.block.digTime(tool ? tool.type : null, false, false, false, enchants, effects)
const laborCost = (1 + 3 * digTime / 1000) * this.settings.digCost
const digTimeMs = this.bot.pathingUtil.digCost(block.block)
const miningTicks = (digTimeMs / 50) * this.settings.digCost

// Add the break-exclusion penalty (0 unless the user configured "no mining" zones).
// If the block sits inside a forbidden zone this pushes the cost past COST_INF,
// which every caller treats as "do not break this block".
return laborCost + this.exclusionBreak(block)
return miningTicks + this.exclusionBreak(block)
}

safeOrPlace (block: BlockInfo, toPlace: PlaceHandler[], type: InteractType = 'solid'): number {
Expand Down
Loading
Loading