feat(minecraft): add map() query DSL for ASCII spatial awareness and terrain visualization

Adds `query.map(options?)` to Brain's query runtime, returning ASCII top-down or cross-section maps with configurable radius (1-32), entity/elevation overlays, and yLevel slicing. Symbols include ground, stone, water, lava, trees, ores, chests, players, and mobs. Enables spatial navigation, resource finding, and underground exploration.
This commit is contained in:
Rin
2026-02-18 11:14:45 +08:00
committed by Neko Ayaka
parent 122be40782
commit 2022c733b3
4 changed files with 775 additions and 0 deletions
@@ -0,0 +1,220 @@
import { Vec3 } from 'vec3'
import { describe, expect, it } from 'vitest'
import { renderMap } from './map-renderer'
// ─── Helpers ────────────────────────────────────────────────────────
function createBotStub(options: {
position?: Vec3
blocks?: Map<string, { name: string }>
entities?: Record<number, any>
} = {}) {
const pos = options.position ?? new Vec3(0, 64, 0)
const blocks = options.blocks ?? new Map()
const entities = options.entities ?? {}
// Add bot entity to entities
const botEntity = {
id: 99,
position: pos,
type: 'player',
username: 'Bot',
name: 'player',
}
entities[99] = botEntity
return {
entity: botEntity,
entities,
blockAt: (queryPos: Vec3) => {
const key = `${Math.floor(queryPos.x)},${Math.floor(queryPos.y)},${Math.floor(queryPos.z)}`
const found = blocks.get(key)
if (found)
return { name: found.name, position: new Vec3(Math.floor(queryPos.x), Math.floor(queryPos.y), Math.floor(queryPos.z)), diggable: true, boundingBox: 'block' }
return { name: 'air', position: new Vec3(Math.floor(queryPos.x), Math.floor(queryPos.y), Math.floor(queryPos.z)), diggable: false, boundingBox: 'empty' }
},
} as any
}
function buildBlockMap(entries: Array<[number, number, number, string]>): Map<string, { name: string }> {
const map = new Map<string, { name: string }>()
for (const [x, y, z, name] of entries) {
map.set(`${x},${y},${z}`, { name })
}
return map
}
// ─── Tests ──────────────────────────────────────────────────────────
describe('map-renderer', () => {
describe('renderMap top-down', () => {
it('calls blockAt with Vec3-compatible positions', () => {
const pos = new Vec3(0, 64, 0)
const bot = {
entity: {
id: 99,
position: pos,
type: 'player',
username: 'Bot',
name: 'player',
},
entities: {},
blockAt: (queryPos: Vec3) => {
queryPos.floored()
return { name: 'air', position: pos, diggable: false, boundingBox: 'empty' }
},
} as any
expect(() => renderMap(bot, { radius: 1, showEntities: false, showElevation: false })).not.toThrow()
})
it('renders a basic map with the bot at center', () => {
const bot = createBotStub()
const result = renderMap(bot, { radius: 3, showElevation: false })
expect(result.view).toBe('top-down')
expect(result.center).toEqual({ x: 0, y: 64, z: 0 })
expect(result.radius).toBe(3)
expect(result.map).toContain('@') // Bot marker
})
it('classifies ground blocks correctly', () => {
const blocks = buildBlockMap([
[0, 64, 0, 'grass_block'],
[1, 64, 0, 'grass_block'],
[-1, 64, 0, 'dirt'],
])
const bot = createBotStub({ blocks })
const result = renderMap(bot, { radius: 2, showEntities: false, showElevation: false })
// Ground blocks should appear as '.'
expect(result.map).toContain('.')
expect(result.legend).toContain('grass/dirt')
})
it('shows water blocks as ~', () => {
const blocks = buildBlockMap([
[2, 64, 0, 'water'],
[3, 64, 0, 'water'],
])
const bot = createBotStub({ blocks })
const result = renderMap(bot, { radius: 4, showEntities: false, showElevation: false })
expect(result.map).toContain('~')
expect(result.legend).toContain('water')
})
it('shows tree trunks as T', () => {
const blocks = buildBlockMap([
[3, 64, 0, 'oak_log'],
[3, 65, 0, 'oak_log'],
[3, 66, 0, 'oak_log'],
[3, 67, 0, 'oak_leaves'],
])
const bot = createBotStub({ blocks })
const result = renderMap(bot, { radius: 4, showEntities: false, showElevation: false })
// Top-down view should show the topmost block — leaves on top of the log column
// But the log at surface level should be visible if it's the surface block
expect(result.legend).toMatch(/tree trunk|leaves/)
})
it('shows ores as $', () => {
const blocks = buildBlockMap([
[1, 64, 0, 'iron_ore'],
[2, 64, 0, 'diamond_ore'],
])
const bot = createBotStub({ blocks })
const result = renderMap(bot, { radius: 3, showEntities: false, showElevation: false })
expect(result.map).toContain('$')
expect(result.legend).toContain('ore')
})
it('shows interactive blocks as !', () => {
const blocks = buildBlockMap([
[1, 64, 0, 'crafting_table'],
[-1, 64, 0, 'chest'],
])
const bot = createBotStub({ blocks })
const result = renderMap(bot, { radius: 2, showEntities: false, showElevation: false })
expect(result.map).toContain('!')
expect(result.legend).toContain('chest/furnace/table')
})
it('overlays entities on the map', () => {
const entities: Record<number, any> = {
1: { id: 1, name: 'zombie', type: 'mob', position: new Vec3(2, 64, 0) },
2: { id: 2, name: 'cow', type: 'mob', position: new Vec3(-1, 64, 1) },
3: { id: 3, name: 'player', type: 'player', username: 'Alex', position: new Vec3(0, 64, -2) },
}
const bot = createBotStub({ entities })
const result = renderMap(bot, { radius: 4, showElevation: false })
expect(result.map).toContain('M') // hostile mob
expect(result.map).toContain('A') // passive mob (animal)
expect(result.map).toContain('P') // player
expect(result.map).toContain('@') // self
expect(result.map).toContain('zombie')
expect(result.map).toContain('Alex')
})
it('respects radius clamping', () => {
const bot = createBotStub()
const result = renderMap(bot, { radius: 100 })
expect(result.radius).toBe(32) // MAX_RADIUS
const result2 = renderMap(bot, { radius: 0 })
expect(result2.radius).toBe(1) // min 1
})
it('shows compass directions', () => {
const bot = createBotStub()
const result = renderMap(bot, { radius: 3 })
expect(result.map).toContain('N(-Z)')
expect(result.map).toContain('S(+Z)')
expect(result.map).toContain('W(-X)')
expect(result.map).toContain('E(+X)')
})
})
describe('renderMap cross-section', () => {
it('renders a cross-section at the bot Y level', () => {
const blocks = buildBlockMap([
[0, 63, 0, 'stone'],
[0, 64, 0, 'grass_block'],
[1, 63, 0, 'stone'],
[1, 64, 0, 'grass_block'],
[-1, 63, 0, 'stone'],
[-1, 64, 0, 'dirt'],
])
const bot = createBotStub({ blocks })
const result = renderMap(bot, { radius: 3, view: 'cross-section' })
expect(result.view).toBe('cross-section')
expect(result.map).toContain('Cross-section')
expect(result.map).toContain('@') // Bot marker
})
})
describe('legend', () => {
it('only includes categories present on the map', () => {
// Map with only ground and water
const blocks = buildBlockMap([
[0, 64, 0, 'grass_block'],
[1, 64, 0, 'water'],
])
const bot = createBotStub({ blocks })
const result = renderMap(bot, { radius: 2, showEntities: false, showElevation: false })
expect(result.legend).toContain('grass/dirt')
expect(result.legend).toContain('water')
// Should NOT contain categories not on the map
expect(result.legend).not.toContain('lava')
expect(result.legend).not.toContain('ore')
})
})
})
@@ -0,0 +1,543 @@
import type { Bot } from 'mineflayer'
import { Vec3 } from 'vec3'
// ─── Types ───────────────────────────────────────────────────────────
export type MapViewType = 'top-down' | 'cross-section'
export interface MapOptions {
/** Radius in blocks from center (default: 16, max: 32) */
radius?: number
/** View type (default: 'top-down') */
view?: MapViewType
/** Whether to show entities on the map (default: true) */
showEntities?: boolean
/** Whether to show elevation numbers (default: true) */
showElevation?: boolean
/** Y level for cross-section view (default: bot's Y) */
yLevel?: number
}
export interface MapResult {
/** The rendered ASCII map string */
map: string
/** Legend explaining the symbols used */
legend: string
/** Center position of the map */
center: { x: number, y: number, z: number }
/** Radius used */
radius: number
/** View type used */
view: MapViewType
}
// ─── Block Category Classification ──────────────────────────────────
//
// We collapse hundreds of block names into a small set of semantic
// categories. Each category gets a single ASCII symbol. The goal is
// maximum information density with minimum noise.
type BlockCategory
= | 'air'
| 'ground'
| 'stone'
| 'sand'
| 'water'
| 'lava'
| 'log'
| 'leaves'
| 'ore'
| 'crop'
| 'path'
| 'wood_structure'
| 'stone_structure'
| 'interactive'
| 'danger'
| 'snow'
| 'ice'
| 'glass'
| 'unknown'
// NOTICE: Symbol choices are intentionally single-character ASCII that
// are visually distinct in monospace fonts and semantically suggestive.
const CATEGORY_SYMBOLS: Record<BlockCategory, string> = {
air: ' ',
ground: '.',
stone: '#',
sand: ':',
water: '~',
lava: '%',
log: 'T',
leaves: '*',
ore: '$',
crop: ';',
path: '_',
wood_structure: '=',
stone_structure: 'B',
interactive: '!',
danger: 'X',
snow: '\'',
ice: '-',
glass: 'o',
unknown: '?',
}
const CATEGORY_LEGEND: Record<BlockCategory, string> = {
air: 'air/void',
ground: 'grass/dirt',
stone: 'stone/rock',
sand: 'sand/gravel',
water: 'water',
lava: 'lava',
log: 'tree trunk',
leaves: 'leaves',
ore: 'ore',
crop: 'crops/farmland',
path: 'path/road',
wood_structure: 'wood building',
stone_structure: 'stone building',
interactive: 'chest/furnace/table',
danger: 'danger (cactus/fire/magma)',
snow: 'snow',
ice: 'ice',
glass: 'glass',
unknown: 'unknown',
}
// Entity symbols placed on top of terrain
const ENTITY_SYMBOLS = {
self: '@',
player: 'P',
hostile: 'M',
passive: 'A',
neutral: 'N',
item: 'i',
} as const
// ─── Block Name → Category Mapping ──────────────────────────────────
const BLOCK_PATTERNS: Array<[RegExp | string[], BlockCategory]> = [
// Interactive (highest priority — these are actionable)
[['crafting_table', 'furnace', 'blast_furnace', 'smoker', 'chest', 'trapped_chest', 'ender_chest', 'barrel', 'anvil', 'enchanting_table', 'brewing_stand', 'grindstone', 'stonecutter', 'loom', 'cartography_table', 'smithing_table', 'composter', 'lectern', 'bed'], 'interactive'],
// Danger
[['lava', 'flowing_lava'], 'lava'],
[['cactus', 'fire', 'soul_fire', 'magma_block', 'sweet_berry_bush', 'wither_rose', 'pointed_dripstone'], 'danger'],
// Water
[['water', 'flowing_water', 'bubble_column', 'kelp', 'kelp_plant', 'seagrass', 'tall_seagrass'], 'water'],
// Ores
[/_(ore|_ore)$/, 'ore'],
[['ancient_debris', 'raw_iron_block', 'raw_gold_block', 'raw_copper_block'], 'ore'],
// Trees
[/_log$/, 'log'],
[['mushroom_stem'], 'log'],
[/_leaves$/, 'leaves'],
[['mangrove_roots', 'muddy_mangrove_roots'], 'log'],
// Crops / farming
[['farmland', 'wheat', 'carrots', 'potatoes', 'beetroots', 'melon', 'pumpkin', 'melon_stem', 'pumpkin_stem', 'sugar_cane', 'bamboo', 'cocoa', 'nether_wart', 'sweet_berries', 'cave_vines', 'cave_vines_plant'], 'crop'],
// Paths
[['dirt_path', 'grass_path'], 'path'],
[/_slab$/, 'path'],
// Wood structures (planks, fences, doors, stairs)
[/_planks$/, 'wood_structure'],
[/_fence$/, 'wood_structure'],
[/_door$/, 'wood_structure'],
[/_stairs$/, 'wood_structure'],
[/_trapdoor$/, 'wood_structure'],
[/_wall$/, 'stone_structure'],
// Stone structures
[/_bricks?$/, 'stone_structure'],
[['cobblestone', 'mossy_cobblestone', 'smooth_stone', 'polished_andesite', 'polished_diorite', 'polished_granite', 'cut_sandstone', 'smooth_sandstone'], 'stone_structure'],
// Ice / snow
[['snow', 'snow_block', 'powder_snow'], 'snow'],
[['ice', 'packed_ice', 'blue_ice', 'frosted_ice'], 'ice'],
// Glass
[/glass/, 'glass'],
// Sand / gravel
[['sand', 'red_sand', 'gravel', 'soul_sand', 'soul_soil', 'clay'], 'sand'],
// Stone (natural)
[['stone', 'deepslate', 'andesite', 'diorite', 'granite', 'tuff', 'calcite', 'dripstone_block', 'basalt', 'smooth_basalt', 'blackstone', 'netherrack', 'end_stone', 'obsidian', 'crying_obsidian', 'bedrock', 'terracotta'], 'stone'],
[/_terracotta$/, 'stone'],
// Ground (dirt, grass, etc.)
[['grass_block', 'dirt', 'coarse_dirt', 'rooted_dirt', 'podzol', 'mycelium', 'mud', 'packed_mud', 'moss_block', 'muddy_mangrove_roots'], 'ground'],
]
function classifyBlock(blockName: string): BlockCategory {
if (blockName === 'air' || blockName === 'cave_air' || blockName === 'void_air')
return 'air'
for (const [pattern, category] of BLOCK_PATTERNS) {
if (pattern instanceof RegExp) {
if (pattern.test(blockName))
return category
}
else if (pattern.includes(blockName)) {
return category
}
}
return 'unknown'
}
// ─── Entity Classification ──────────────────────────────────────────
const HOSTILE_MOBS = new Set([
'zombie',
'skeleton',
'creeper',
'spider',
'cave_spider',
'enderman',
'witch',
'slime',
'magma_cube',
'blaze',
'ghast',
'wither_skeleton',
'phantom',
'drowned',
'husk',
'stray',
'pillager',
'vindicator',
'ravager',
'vex',
'evoker',
'guardian',
'elder_guardian',
'hoglin',
'piglin_brute',
'warden',
'breeze',
])
const PASSIVE_MOBS = new Set([
'cow',
'sheep',
'pig',
'chicken',
'horse',
'donkey',
'mule',
'rabbit',
'cat',
'ocelot',
'parrot',
'fox',
'turtle',
'axolotl',
'glow_squid',
'squid',
'bat',
'cod',
'salmon',
'tropical_fish',
'pufferfish',
'mooshroom',
'strider',
'frog',
'tadpole',
'allay',
'sniffer',
'camel',
'armadillo',
'villager',
'wandering_trader',
])
function classifyEntity(entityName: string): keyof typeof ENTITY_SYMBOLS {
if (HOSTILE_MOBS.has(entityName))
return 'hostile'
if (PASSIVE_MOBS.has(entityName))
return 'passive'
return 'neutral'
}
// ─── Surface Finding ────────────────────────────────────────────────
/**
* Find the topmost non-air block at (x, z) by scanning downward from
* a reasonable height. Returns the block name and Y level, or null if
* the column is entirely air (unloaded chunk).
*/
function findSurfaceBlock(
bot: Bot,
x: number,
z: number,
startY: number,
): { name: string, y: number } | null {
// Scan from startY + 16 down to startY - 32 to handle hills and valleys
const top = Math.min(startY + 16, 319)
const bottom = Math.max(startY - 48, -64)
for (let y = top; y >= bottom; y--) {
const block = bot.blockAt(new Vec3(x, y, z))
if (block && block.name !== 'air' && block.name !== 'cave_air' && block.name !== 'void_air') {
return { name: block.name, y }
}
}
return null
}
// ─── Renderers ──────────────────────────────────────────────────────
function renderTopDown(bot: Bot, options: Required<MapOptions>): MapResult {
const center = bot.entity.position
const cx = Math.floor(center.x)
const cy = Math.floor(center.y)
const cz = Math.floor(center.z)
const r = options.radius
// Build the grid: each cell is [symbol, elevation_delta]
const size = r * 2 + 1
const grid: string[][] = Array.from({ length: size }, () => Array.from({ length: size }, () => ' '))
const elevations: (number | null)[][] = Array.from({ length: size }, () => Array.from({ length: size }, () => null))
const usedCategories = new Set<BlockCategory>()
for (let dz = -r; dz <= r; dz++) {
for (let dx = -r; dx <= r; dx++) {
const wx = cx + dx
const wz = cz + dz
const gx = dx + r
const gz = dz + r
const surface = findSurfaceBlock(bot, wx, wz, cy)
if (!surface) {
grid[gz][gx] = ' '
continue
}
const category = classifyBlock(surface.name)
usedCategories.add(category)
grid[gz][gx] = CATEGORY_SYMBOLS[category]
elevations[gz][gx] = surface.y - cy
}
}
// Overlay entities
const entityOverlays: Array<{ gx: number, gz: number, symbol: string, label: string }> = []
if (options.showEntities) {
// Bot itself
grid[r][r] = ENTITY_SYMBOLS.self
entityOverlays.push({ gx: r, gz: r, symbol: ENTITY_SYMBOLS.self, label: 'You' })
// Other entities
for (const entity of Object.values(bot.entities)) {
if (entity === bot.entity)
continue
const ex = Math.floor(entity.position.x) - cx
const ez = Math.floor(entity.position.z) - cz
if (Math.abs(ex) > r || Math.abs(ez) > r)
continue
const gx = ex + r
const gz = ez + r
if (entity.type === 'player') {
grid[gz][gx] = ENTITY_SYMBOLS.player
entityOverlays.push({ gx, gz, symbol: ENTITY_SYMBOLS.player, label: entity.username ?? 'player' })
}
else if (entity.type === 'mob') {
const kind = classifyEntity(entity.name ?? '')
grid[gz][gx] = ENTITY_SYMBOLS[kind]
entityOverlays.push({ gx, gz, symbol: ENTITY_SYMBOLS[kind], label: entity.name ?? 'mob' })
}
else if (entity.type === 'object' && entity.name === 'item') {
grid[gz][gx] = ENTITY_SYMBOLS.item
}
}
}
// Build the map string
const lines: string[] = []
// Header with compass and coordinates
lines.push(` Top-down view centered at (${cx}, ${cy}, ${cz}) | radius: ${r}`)
lines.push(` N(-Z)`)
lines.push(` |`)
// Column header (X axis markers at edges)
const xLeft = cx - r
const xRight = cx + r
lines.push(`W(-X)${'─'.repeat(size + 2)}E(+X) [${xLeft}..${xRight}]`)
for (let gz = 0; gz < size; gz++) {
const row = grid[gz].join('')
// Add elevation markers on the right side for every 4th row
if (options.showElevation && gz % 4 === 0) {
const elevSamples = elevations[gz]
.filter((e): e is number => e !== null)
if (elevSamples.length > 0) {
const minE = Math.min(...elevSamples)
const maxE = Math.max(...elevSamples)
lines.push(` |${row}| dy:${minE > 0 ? '+' : ''}${minE}..${maxE > 0 ? '+' : ''}${maxE}`)
}
else {
lines.push(` |${row}|`)
}
}
else {
lines.push(` |${row}|`)
}
}
lines.push(` ${'─'.repeat(size + 2)}`)
lines.push(` |`)
lines.push(` S(+Z) [Z: ${cz - r}..${cz + r}]`)
// Entity list
if (entityOverlays.length > 1) {
lines.push('')
lines.push('Entities:')
for (const e of entityOverlays) {
if (e.symbol === ENTITY_SYMBOLS.self)
continue
const dx = e.gx - r
const dz = e.gz - r
lines.push(` ${e.symbol} ${e.label} (${dx > 0 ? '+' : ''}${dx}, ${dz > 0 ? '+' : ''}${dz})`)
}
}
// Legend (only show categories that appear on this map)
const legendParts: string[] = []
for (const cat of usedCategories) {
if (cat === 'air')
continue
legendParts.push(`${CATEGORY_SYMBOLS[cat]}=${CATEGORY_LEGEND[cat]}`)
}
// Always include entity symbols if entities are shown
if (options.showEntities) {
legendParts.push(`${ENTITY_SYMBOLS.self}=you`)
legendParts.push(`${ENTITY_SYMBOLS.player}=player`)
legendParts.push(`${ENTITY_SYMBOLS.hostile}=hostile mob`)
legendParts.push(`${ENTITY_SYMBOLS.passive}=animal`)
}
const legend = legendParts.join(' ')
return {
map: lines.join('\n'),
legend,
center: { x: cx, y: cy, z: cz },
radius: r,
view: 'top-down',
}
}
function renderCrossSection(bot: Bot, options: Required<MapOptions>): MapResult {
const center = bot.entity.position
const cx = Math.floor(center.x)
const cy = options.yLevel
const cz = Math.floor(center.z)
const r = options.radius
// Cross-section: X horizontal, Y vertical, at fixed Z = cz
const width = r * 2 + 1
const height = r * 2 + 1
const yTop = cy + r
const yBottom = cy - r
const grid: string[][] = Array.from({ length: height }, () => Array.from({ length: width }, () => ' '))
const usedCategories = new Set<BlockCategory>()
for (let dy = -r; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
const wx = cx + dx
const wy = cy - dy // Flip Y so top of grid = higher Y
const gx = dx + r
const gy = dy + r
const block = bot.blockAt(new Vec3(wx, wy, cz))
if (!block) {
grid[gy][gx] = ' '
continue
}
const category = classifyBlock(block.name)
usedCategories.add(category)
grid[gy][gx] = CATEGORY_SYMBOLS[category]
}
}
// Mark bot position if visible
const botDx = 0
const botDy = cy - Math.floor(center.y)
if (Math.abs(botDx) <= r && Math.abs(botDy) <= r) {
grid[botDy + r][botDx + r] = ENTITY_SYMBOLS.self
}
const lines: string[] = []
lines.push(` Cross-section at Z=${cz} centered at (${cx}, ${cy}) | radius: ${r}`)
lines.push(` Y=${yTop}`)
for (let gy = 0; gy < height; gy++) {
const row = grid[gy].join('')
const worldY = yTop - gy
if (gy % 4 === 0) {
lines.push(`${String(worldY).padStart(4)}|${row}|`)
}
else {
lines.push(` |${row}|`)
}
}
lines.push(` Y=${yBottom}`)
lines.push(` [X: ${cx - r}..${cx + r}]`)
// Legend
const legendParts: string[] = []
for (const cat of usedCategories) {
if (cat === 'air')
continue
legendParts.push(`${CATEGORY_SYMBOLS[cat]}=${CATEGORY_LEGEND[cat]}`)
}
legendParts.push(`${ENTITY_SYMBOLS.self}=you`)
return {
map: lines.join('\n'),
legend: legendParts.join(' '),
center: { x: cx, y: cy, z: cz },
radius: r,
view: 'cross-section',
}
}
// ─── Public API ─────────────────────────────────────────────────────
const DEFAULT_RADIUS = 16
const MAX_RADIUS = 32
export function renderMap(bot: Bot, options: MapOptions = {}): MapResult {
const resolved: Required<MapOptions> = {
radius: Math.min(Math.max(options.radius ?? DEFAULT_RADIUS, 1), MAX_RADIUS),
view: options.view ?? 'top-down',
showEntities: options.showEntities ?? true,
showElevation: options.showElevation ?? true,
yLevel: options.yLevel ?? Math.floor(bot.entity.position.y),
}
switch (resolved.view) {
case 'cross-section':
return renderCrossSection(bot, resolved)
case 'top-down':
default:
return renderTopDown(bot, resolved)
}
}
@@ -67,6 +67,12 @@ Core query entrypoints:
- `query.entities()`: nearby entities with chain methods (`within`, `limit`, `whereType`, `names`, `first`, `list`)
- `query.inventory()`: inventory stacks (`whereName`, `names`, `countByName`, `count`, `has`, `summary`, `list`)
- `query.craftable()`: craftable item names (supports `uniq`, `whereIncludes`, `list`)
- `query.gaze(options?)`: where nearby players are looking (`playerName`, `lookPoint`, `hitBlock`)
- `query.map(options?)`: ASCII top-down or cross-section map of surroundings. Returns `{ map, legend, center, radius, view }`.
- Options: `{ radius?: number (1-32, default 16), view?: "top-down" | "cross-section", showEntities?: boolean, showElevation?: boolean, yLevel?: number }`
- Symbols: `.`=ground `#`=stone `~`=water `%`=lava `T`=tree trunk `$`=ore `!`=chest/furnace/table `@`=you `P`=player `M`=hostile `A`=animal
- Use `query.map()` for spatial awareness — finding trees, water, ores, structures, and navigating terrain.
- Use `query.map({ view: "cross-section" })` to see underground layers (caves, ore veins, elevation).
Composable patterns:
- `const ores = query.blocks().within(24).isOre().names().uniq().list()`
@@ -79,6 +85,8 @@ Composable patterns:
- `const invSummary = query.inventory().summary(); invSummary`
- `const invLine = query.inventory().summary().map(({ name, count }) => `${count} ${name}`).join(", "); invLine`
- `const craftableTools = query.craftable().whereIncludes("pickaxe").uniq().list()`
- `const area = query.map({ radius: 16 }); area.map` — top-down ASCII map of surroundings
- `const underground = query.map({ view: "cross-section", radius: 8 }); underground.map` — vertical slice showing caves/ores
Inventory summary shape reminder:
- `query.inventory().summary()` returns an **array** of `{ name, count }`.
@@ -8,6 +8,7 @@ import { inspect } from 'node:util'
import { Vec3 } from 'vec3'
import { computeNearbyPlayerGaze } from '../perception/gaze'
import { renderMap } from './map-renderer'
import * as world from '../../skills/world'
@@ -447,5 +448,8 @@ export function createQueryRuntime(mineflayer: Mineflayer) {
nearbyDistance: options?.range ?? 16,
})
},
map: (options?: { radius?: number, view?: 'top-down' | 'cross-section', showEntities?: boolean, showElevation?: boolean, yLevel?: number }) => {
return renderMap(mineflayer.bot, options)
},
}
}