From 73d5642ebddfdab0ab40b50e70d3ee7b958f534e Mon Sep 17 00:00:00 2001
From: XaXayo12 <257781493+XaXayo12@users.noreply.github.com>
Date: Thu, 18 Jun 2026 17:56:54 +0200
Subject: [PATCH] feat(costs): tick-based movement costs instead of arbitrary
numbers
Every movement provider used made-up costs (forward = 1, diagonal = sqrt2,
jump = +0.5, a 3-block drop = +1.5, break = 1 + 3*digTime/1000). They had no
shared unit, so A* could not honestly compare "walk around" vs "dig through"
vs "drop down". Now every cost is measured in game ticks (20/sec) using
Baritone's hardcoded estimates, which already lived (unused) in costs.ts.
- travelCost(blocks): sprint (or walk) time per block.
- Forward / Diagonal: travelCost(1) / travelCost(sqrt2).
- ForwardJump: max(jump arc, walk) - you rise and move at once.
- ForwardDropDown / StraightDown: walk-off + real FALL_N_BLOCKS_COST fall time.
- StraightUp: ladder speed when climbing, else jump + place.
- Parkour (forward + diagonal): jump + sprint time across the gap.
- breakCost: real mining time in ticks (ms / 50), not a made-up formula.
- movement.ts defaults: liquid / place / jump / dig costs are now tick-based.
The goal heuristic was already in ticks (walk cost per block). Kept it at the
walk cost (not the cheaper sprint cost) on purpose: A* stays a lightly weighted
search - same fast beeline - but returned paths are now <=1.3x optimal
(walk / sprint = 4.633 / 3.564) instead of the old ~4.6x (per-move cost was ~1
while the heuristic was ~4.633). Centralized that number as COST_HEURISTIC.
Verified: build + ts-standard clean, 31/31 tests pass. A deterministic flat
benchmark shows identical visited/generated node counts before and after (no
search regression); only path quality improves. Added docs/MovementCosts.md.
The hard-exclusion detour test asserted "detour > 21 moves"; with realistic
costs A* now finds the optimal detour around the wall using diagonals at the
same move count (it swings out to |z| >= 4, 0 nodes inside the zone), so that
proxy was wrong. The test now asserts that geometric fact directly.
---
docs/MovementCosts.md | 123 ++++++++++++++++++
src/mineflayer-specific/goals.ts | 13 +-
src/mineflayer-specific/movements/costs.ts | 64 +++++++--
src/mineflayer-specific/movements/movement.ts | 52 ++++++--
.../movements/movementProviders.ts | 54 +++++---
tests/exclusionZones.test.ts | 7 +-
6 files changed, 259 insertions(+), 54 deletions(-)
create mode 100644 docs/MovementCosts.md
diff --git a/docs/MovementCosts.md b/docs/MovementCosts.md
new file mode 100644
index 0000000..77d4ee1
--- /dev/null
+++ b/docs/MovementCosts.md
@@ -0,0 +1,123 @@
+
Movement Costs
+
+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
+})
+```
diff --git a/src/mineflayer-specific/goals.ts b/src/mineflayer-specific/goals.ts
index 5597941..c6f6dd7 100644
--- a/src/mineflayer-specific/goals.ts
+++ b/src/mineflayer-specific/goals.ts
@@ -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'
@@ -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)
}
@@ -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
}
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
diff --git a/src/mineflayer-specific/movements/costs.ts b/src/mineflayer-specific/movements/costs.ts
index dc2571a..ae52b04 100644
--- a/src/mineflayer-specific/movements/costs.ts
+++ b/src/mineflayer-specific/movements/costs.ts
@@ -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 {
diff --git a/src/mineflayer-specific/movements/movement.ts b/src/mineflayer-specific/movements/movement.ts
index ff43bd5..c7d471a 100644
--- a/src/mineflayer-specific/movements/movement.ts
+++ b/src/mineflayer-specific/movements/movement.ts
@@ -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
@@ -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,
@@ -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)
@@ -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 {
diff --git a/src/mineflayer-specific/movements/movementProviders.ts b/src/mineflayer-specific/movements/movementProviders.ts
index 6defc66..cd7eaef 100644
--- a/src/mineflayer-specific/movements/movementProviders.ts
+++ b/src/mineflayer-specific/movements/movementProviders.ts
@@ -6,10 +6,22 @@ import { BreakHandler, PlaceHandler } from './interactionUtils'
import { emptyVec } from '@nxg-org/mineflayer-physics-util/dist/physics/settings'
import { MovementProvider } from './movementProvider'
import { BlockInfo } from '../world/cacheWorld'
-import { COST_INF } from './costs'
+import {
+ COST_INF,
+ FALL_N_BLOCKS_COST,
+ JUMP_ONE_BLOCK_COST,
+ LADDER_UP_ONE_COST,
+ WALK_OFF_BLOCK_COST,
+ WALK_ONE_BLOCK_COST
+} from './costs'
const PARKOUR_DIAGONAL_3_3_TRAVEL = Math.sqrt(18) // 3 by 3 offset.
+// Small fixed tick penalty for the "forward" parkour landing (running across a
+// gap onto a same-height block). It is slower than a clean jump-and-drop, so it
+// pays a little extra. Kept as a placeholder until per-frame parkour timing exists.
+const PARKOUR_FORWARD_LANDING_PENALTY = 3
+
// technically, the offsets are slow. Yeah, I know.
// However, removing those breaks the code. So I won't fix that for the time being. -Gen
@@ -36,7 +48,7 @@ export class Forward extends MovementProvider {
getMoveForward (start: Move, dir: Vec3, neighbors: Move[]): void {
const pos = start.cachedVec
- let cost = 1 // move cost
+ let cost = this.travelCost(1) // sprint/walk one block forward
if (this.getBlockInfo(pos, 0, 0, 0).liquid) cost += this.settings.liquidCost
@@ -79,8 +91,6 @@ export class Forward extends MovementProvider {
export class Diagonal extends MovementProvider {
movementDirs = Movement.diagonalDirs
- static diagonalCost = Math.SQRT2 // sqrt(2)
-
provideMovements (start: Move, storage: Move[], goal: goals.Goal, closed: Set): void {
for (const dir of this.movementDirs) {
const off = start.cachedVec.plus(dir).floor()
@@ -90,7 +100,7 @@ export class Diagonal extends MovementProvider {
}
getMoveDiagonal (node: Move, dir: Vec3, neighbors: Move[], goal: goals.Goal): void {
- let cost = Diagonal.diagonalCost
+ let cost = this.travelCost(Math.SQRT2) // one diagonal block is sqrt(2) blocks of travel
const block0 = this.getBlockInfo(node, dir.x, 0, dir.z)
@@ -174,7 +184,9 @@ export class ForwardJump extends MovementProvider {
const blockH = this.getBlockInfo(pos, dir.x, 2, dir.z)
const blockC = this.getBlockInfo(pos, dir.x, 0, dir.z)
- let cost = 1 + this.settings.jumpCost // move cost (move+jump)
+ // Jumping up one block: you move and rise at the same time, so the cost is
+ // the slower of "walk one block" and "the jump arc", not their sum.
+ let cost = Math.max(JUMP_ONE_BLOCK_COST, WALK_ONE_BLOCK_COST) + this.settings.jumpCost
const block0 = this.getBlockInfo(pos, 0, 0, 0)
if (block0.liquid) cost += this.settings.liquidCost
@@ -290,7 +302,7 @@ export class ForwardDropDown extends DropDownProvider {
}
getMoveDropDown (node: Move, dir: Vec3, neighbors: Move[], closed: Set): void {
- let cost = 1 // move cost
+ let cost = WALK_OFF_BLOCK_COST // step off the ledge
const block0 = this.getBlockInfo(node, 0, 0, 0)
const blockLand = this.getLandingBlock(block0, node, dir)
@@ -301,8 +313,8 @@ export class ForwardDropDown extends DropDownProvider {
if (block0.liquid) cost += this.settings.liquidCost
- // drop cost
- cost += (node.y - blockLand.position.y) * 0.5
+ // ticks to fall the height we are dropping (whole blocks)
+ cost += FALL_N_BLOCKS_COST[node.y - blockLand.position.y]
const blockA = this.getBlockInfo(node, dir.x, 2, dir.z)
const blockB = this.getBlockInfo(node, dir.x, 1, dir.z)
@@ -342,7 +354,7 @@ export class StraightDown extends DropDownProvider {
}
getMoveDown (node: Move, neighbors: Move[], closed: Set): void {
- let cost = 1 // move cost
+ let cost = 0 // straight down has no horizontal travel; the fall below is the whole cost
const block0 = this.getBlockInfo(node, 0, 0, 0)
const blockLand = this.getLandingBlock(block0, node)
@@ -352,8 +364,8 @@ export class StraightDown extends DropDownProvider {
if (block0.liquid) cost += this.settings.liquidCost // dont go underwater
- // drop cost
- cost += (node.y - blockLand.position.y) * 0.5
+ // ticks to fall the height we are dropping (whole blocks)
+ cost += FALL_N_BLOCKS_COST[node.y - blockLand.position.y]
const block1 = this.getBlockInfo(node, 0, -1, 0)
@@ -381,12 +393,14 @@ export class StraightUp extends MovementProvider {
}
getMoveUp (node: Move, neighbors: Move[], closed: Set): void {
- let cost = this.settings.jumpCost // move cost
-
const block1 = this.getBlockInfo(node, 0, 0, 0)
if (block1.isInvalid) return // out of range.
+ // Climbing a ladder/vine has its own speed; otherwise we jump (and below we
+ // may also place a block to tower up, which adds its own place cost).
+ let cost = block1.climbable ? LADDER_UP_ONE_COST : JUMP_ONE_BLOCK_COST + this.settings.jumpCost
+
if (block1.liquid) cost += this.settings.liquidCost
// if (this.getNumEntitiesAt(node, 0, 0, 0) > 0) return // an entity (besides the player) is blocking the building area
@@ -450,7 +464,7 @@ export class ParkourForward extends MovementProvider {
return
}
- const cost0 = 1 + this.settings.jumpCost // move cost (move+jump)
+ const cost0 = JUMP_ONE_BLOCK_COST + this.settings.jumpCost // the jump itself
// Leaving entities at the ceiling level (along path) out for now because there are few cases where that will be important
// cost += this.getNumEntitiesAt(node, dir.x, 0, dir.z) * this.entityCost
@@ -464,7 +478,7 @@ export class ParkourForward extends MovementProvider {
const maxD = this.settings.allowSprinting ? 5 : 2
for (let d = 2; d <= maxD; d++) {
- let cost = cost0 + d * 0.5 // 0.5 per block forward
+ let cost = cost0 + this.travelCost(d) // jump plus sprinting across d blocks
const dx = dir.x * d
const dz = dir.z * d
@@ -495,7 +509,7 @@ export class ParkourForward extends MovementProvider {
floorCleared = floorCleared && !blockE.physical
} else if (flag1 && ceilingClear && blockB.walkthrough && blockC.walkthrough && blockD.physical) {
// Forward
- cost += 3 // potential slowdown (will fix later.)
+ cost += PARKOUR_FORWARD_LANDING_PENALTY
if ((cost += this.exclusionStep(blockC)) < COST_INF) {
neighbors.push(Move.fromPrevious(cost, blockC.position.offset(0.5, 0, 0.5), node, this))
}
@@ -550,7 +564,7 @@ export class ParkourDiagonal extends MovementProvider {
if (!this.getBlockInfo(node, dir.x, 1, 0).walkthrough) return
if (!this.getBlockInfo(node, 0, 1, dir.z).walkthrough) return
- const cost0 = Diagonal.diagonalCost + this.settings.jumpCost
+ const cost0 = JUMP_ONE_BLOCK_COST + this.settings.jumpCost // the jump itself
const maxD = this.settings.allowSprinting ? 4 : 2
// old behavior
@@ -586,7 +600,7 @@ export class ParkourDiagonal extends MovementProvider {
const dz = dir.z * zSteps
const travel = Math.sqrt(xSteps * xSteps + zSteps * zSteps)
- let cost = cost0 + 0.5 * Diagonal.diagonalCost * travel
+ let cost = cost0 + this.travelCost(travel) // jump plus sprinting across the gap
const majorIsX = xSteps > zSteps
const majorIsZ = zSteps > xSteps
const frontDx = dx - (majorIsX || xSteps === zSteps ? dir.x : 0)
@@ -629,7 +643,7 @@ export class ParkourDiagonal extends MovementProvider {
return true
}
} else if (flag1 && ceilingClear && blockB.walkthrough && blockC.walkthrough && blockD.physical && blockFrontC.walkthrough) {
- cost += 3
+ cost += PARKOUR_FORWARD_LANDING_PENALTY
if ((cost += this.exclusionStep(blockC)) < COST_INF) {
neighbors.push(Move.fromPrevious(cost, blockC.position.offset(0.5, 0, 0.5), node, this))
}
diff --git a/tests/exclusionZones.test.ts b/tests/exclusionZones.test.ts
index cc6462b..f483cd3 100644
--- a/tests/exclusionZones.test.ts
+++ b/tests/exclusionZones.test.ts
@@ -142,7 +142,12 @@ test('a hard step exclusion forces the bot to detour around a wall', async () =>
)
}
- assert.ok(result.path.length > 21, `expected a detour longer than 21 moves, got ${result.path.length}`)
+ // The wall blocks z in [-3, 3] for x in [8, 12], so any valid path MUST swing
+ // out to |z| >= 4 to get around it. We assert that geometric fact rather than
+ // "more than 21 moves": diagonal moves absorb the sideways excursion, so an
+ // optimal detour can keep the same move count (it just costs more real time).
+ const maxAbsZ = Math.max(...result.path.map((node) => Math.abs(node.z)))
+ assert.ok(maxAbsZ >= 4, `expected the path to swing out to |z| >= 4 around the wall, got max |z| = ${maxAbsZ}`)
} finally {
rig.stopPassivePhysics()
}