diff --git a/services/minecraft/src/cognitive/conscious/map-renderer.test.ts b/services/minecraft/src/cognitive/conscious/map-renderer.test.ts new file mode 100644 index 000000000..aa93f1e56 --- /dev/null +++ b/services/minecraft/src/cognitive/conscious/map-renderer.test.ts @@ -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 + entities?: Record +} = {}) { + 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 { + const map = new Map() + 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 = { + 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') + }) + }) +}) diff --git a/services/minecraft/src/cognitive/conscious/map-renderer.ts b/services/minecraft/src/cognitive/conscious/map-renderer.ts new file mode 100644 index 000000000..63ce6d54f --- /dev/null +++ b/services/minecraft/src/cognitive/conscious/map-renderer.ts @@ -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 = { + 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 = { + 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): 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() + + 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): 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() + + 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 = { + 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) + } +} diff --git a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md index df23312ff..d4a9c5c04 100644 --- a/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md +++ b/services/minecraft/src/cognitive/conscious/prompts/brain-prompt.md @@ -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 }`. diff --git a/services/minecraft/src/cognitive/conscious/query-dsl.ts b/services/minecraft/src/cognitive/conscious/query-dsl.ts index 0a155f754..86b6f1973 100644 --- a/services/minecraft/src/cognitive/conscious/query-dsl.ts +++ b/services/minecraft/src/cognitive/conscious/query-dsl.ts @@ -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) + }, } }