diff --git a/services/minecraft/src/agents/query.ts b/services/minecraft/src/agents/query.ts index 57391df95..c64a78c95 100644 --- a/services/minecraft/src/agents/query.ts +++ b/services/minecraft/src/agents/query.ts @@ -1,32 +1,11 @@ -import type { Bot } from 'mineflayer' import type { BotContext } from '../composables/bot' import { z } from 'zod' -import { getStatus } from '../components/status' +import { getStatusToString } from '../components/status' +import * as world from '../composables/world' // Core types type QueryResult = string | Promise -// BotContext management -let ctx: QueryBotContext - -export function initQueryBotContext(BotContext: QueryBotContext): void { - ctx = BotContext -} - -interface QueryBotContext { - world: { - getBiomeName: (bot: Bot) => string - getNearbyPlayerNames: (bot: Bot) => string[] - getInventoryCounts: (bot: Bot) => Record - getNearbyBlockTypes: (bot: Bot) => string[] - getCraftableItems: (bot: Bot) => string[] - getNearbyEntityTypes: (bot: Bot) => string[] - } - convoManager: { - getInGameAgents: () => string[] - } -} - interface Query { readonly name: string readonly description: string @@ -51,9 +30,7 @@ function createStatsQuery(): Query { name: 'stats', description: 'Get your bot\'s location, health, hunger, and time of day.', schema: z.object({}), - perform: (ctx: BotContext) => (): string => { - return Array.from(getStatus(ctx).entries()).map(([key, value]) => `${key}: ${value}`).join('\n') - }, + perform: (ctx: BotContext) => (): string => getStatusToString(ctx), } } @@ -62,9 +39,9 @@ function createInventoryQuery(): Query { name: 'inventory', description: 'Get your bot\'s inventory.', schema: z.object({}), - perform: (agent: QueryAgentBotContext) => (): string => { - const { bot } = agent - const inventory = ctx.world.getInventoryCounts(bot) + perform: (ctx: BotContext) => (): string => { + const { bot } = ctx + const inventory = world.getInventoryCounts({ bot, botCtx: ctx }) const items = Object.entries(inventory) .map(([item, count]) => formatInventoryItem(item, count)) .join('') @@ -77,7 +54,7 @@ function createInventoryQuery(): Query { ].filter(Boolean).join('') return pad(`INVENTORY${items || ': Nothing'} -${agent.bot.game.gameMode === 'creative' ? '\n(You have infinite items in creative mode. You do not need to gather resources!!)' : ''} +${bot.game.gameMode === 'creative' ? '\n(You have infinite items in creative mode. You do not need to gather resources!!)' : ''} WEARING: ${wearing || 'Nothing'}`) }, } @@ -88,8 +65,8 @@ function createNearbyBlocksQuery(): Query { name: 'nearbyBlocks', description: 'Get the blocks near the bot.', schema: z.object({}), - perform: (agent: QueryAgentBotContext) => (): string => { - const blocks = ctx.world.getNearbyBlockTypes(agent.bot) + perform: (ctx: BotContext) => (): string => { + const blocks = world.getNearbyBlockTypes({ bot: ctx.bot, botCtx: ctx }) return pad(`NEARBY_BLOCKS${blocks.map(b => `\n- ${b}`).join('') || ': none'}`) }, } @@ -100,8 +77,8 @@ function createCraftableQuery(): Query { name: 'craftable', description: 'Get the craftable items with the bot\'s inventory.', schema: z.object({}), - perform: (agent: QueryAgentBotContext) => (): string => { - const craftable = ctx.world.getCraftableItems(agent.bot) + perform: (ctx: BotContext) => (): string => { + const craftable = world.getCraftableItems({ bot: ctx.bot, botCtx: ctx }) return pad(`CRAFTABLE_ITEMS${craftable.map(i => `\n- ${i}`).join('') || ': none'}`) }, } @@ -112,19 +89,16 @@ function createEntitiesQuery(): Query { name: 'entities', description: 'Get the nearby players and entities.', schema: z.object({}), - perform: (agent: QueryAgentBotContext) => (): string => { - const { bot } = agent - const players = ctx.world.getNearbyPlayerNames(bot) - .filter(p => !ctx.convoManager.getInGameAgents().includes(p)) - const bots = ctx.convoManager.getInGameAgents() - .filter(b => b !== agent.name) - const entities = ctx.world.getNearbyEntityTypes(bot) - .filter(e => e !== 'player' && e !== 'item') + perform: (ctx: BotContext) => (): string => { + const { bot } = ctx + const worldCtx = { bot, botCtx: ctx } + const players = world.getNearbyPlayerNames(worldCtx) + const entities = world.getNearbyEntityTypes(worldCtx) + .filter((e: string) => e !== 'player' && e !== 'item') const result = [ - ...players.map(p => `- Human player: ${p}`), - ...bots.map(b => `- Bot player: ${b}`), - ...entities.map(e => `- entities: ${e}`), + ...players.map((p: string) => `- Human player: ${p}`), + ...entities.map((e: string) => `- entities: ${e}`), ] return pad(`NEARBY_ENTITIES${result.length ? `\n${result.join('\n')}` : ': none'}`) @@ -132,25 +106,6 @@ function createEntitiesQuery(): Query { } } -function createModesQuery(): Query { - return { - name: 'modes', - description: 'Get all available modes and their docs and see which are on/off.', - schema: z.object({}), - perform: (agent: QueryAgentBotContext) => (): string => agent.bot.modes.getDocs(), - } -} - -function createSavedPlacesQuery(): Query { - return { - name: 'savedPlaces', - description: 'List all saved locations.', - schema: z.object({}), - perform: (agent: QueryAgentBotContext) => (): string => - `Saved place names: ${agent.memory_bank.getKeys()}`, - } -} - // Export query list export const queryList: readonly Query[] = [ createStatsQuery(), @@ -158,6 +113,4 @@ export const queryList: readonly Query[] = [ createNearbyBlocksQuery(), createCraftableQuery(), createEntitiesQuery(), - createModesQuery(), - createSavedPlacesQuery(), ] as const diff --git a/services/minecraft/src/components/status.ts b/services/minecraft/src/components/status.ts index 0f7f7486f..f83345403 100644 --- a/services/minecraft/src/components/status.ts +++ b/services/minecraft/src/components/status.ts @@ -2,6 +2,10 @@ import type { BotContext, ComponentLifecycle } from '../composables/bot' import { useLogg } from '@guiiai/logg' import { registerCommand } from '../composables/command' +export function getStatusToString(ctx: BotContext): string { + return Array.from(getStatus(ctx).entries()).map(([key, value]) => `${key}: ${value}`).join('\n') +} + export function getStatus(ctx: BotContext): Map { const status = new Map() const pos = ctx.bot.entity.position diff --git a/services/minecraft/src/composables/world.ts b/services/minecraft/src/composables/world.ts index 0e873b295..9a83f0339 100644 --- a/services/minecraft/src/composables/world.ts +++ b/services/minecraft/src/composables/world.ts @@ -1,305 +1,174 @@ +import type { Bot } from 'mineflayer' +import type { Block } from 'prismarine-block' +import type { Entity } from 'prismarine-entity' +import type { Item } from 'prismarine-item' +import type { Vec3 } from 'vec3' +import type { BotContext } from './bot' import pf from 'mineflayer-pathfinder' - import * as mc from '../utils/mcdata' -export function getNearestFreeSpace(bot, size = 1, distance = 8) { - /** - * Get the nearest empty space with solid blocks beneath it of the given size. - * @param {Bot} bot - The bot to get the nearest free space for. - * @param {number} size - The (size x size) of the space to find, default 1. - * @param {number} distance - The maximum distance to search, default 8. - * @returns {Vec3} - The south west corner position of the nearest free space. - * @example - * let position = world.getNearestFreeSpace(bot, 1, 8); - */ - const empty_pos = bot.findBlocks({ - matching: (block) => { - return block && block.name == 'air' - }, +interface WorldContext { + bot: Bot + botCtx: BotContext +} + +export function getNearestFreeSpace(ctx: WorldContext, size: number = 1, distance: number = 8): Vec3 | undefined { + const emptyPositions = ctx.bot.findBlocks({ + matching: (block: Block) => block?.name === 'air', maxDistance: distance, count: 1000, }) - for (let i = 0; i < empty_pos.length; i++) { - let empty = true + + return emptyPositions.find((pos) => { for (let x = 0; x < size; x++) { for (let z = 0; z < size; z++) { - const top = bot.blockAt(empty_pos[i].offset(x, 0, z)) - const bottom = bot.blockAt(empty_pos[i].offset(x, -1, z)) - if (!top || !top.name == 'air' || !bottom || bottom.drops.length == 0 || !bottom.diggable) { - empty = false - break + const top = ctx.bot.blockAt(pos.offset(x, 0, z)) + const bottom = ctx.bot.blockAt(pos.offset(x, -1, z)) + if (!top || top.name !== 'air' || !bottom?.drops?.length || !bottom.diggable) { + return false } } - if (!empty) - break } - if (empty) { - return empty_pos[i] - } - } + return true + }) } -export function getNearestBlocks(bot, block_types = null, distance = 16, count = 10000) { - /** - * Get a list of the nearest blocks of the given types. - * @param {Bot} bot - The bot to get the nearest block for. - * @param {string[]} block_types - The names of the blocks to search for. - * @param {number} distance - The maximum distance to search, default 16. - * @param {number} count - The maximum number of blocks to find, default 10000. - * @returns {Block[]} - The nearest blocks of the given type. - * @example - * let woodBlocks = world.getNearestBlocks(bot, ['oak_log', 'birch_log'], 16, 1); - */ - // if blocktypes is not a list, make it a list - let block_ids = [] - if (block_types === null) { - block_ids = mc.getAllBlockIds(['air']) - } - else { - if (!Array.isArray(block_types)) - block_types = [block_types] - for (const block_type of block_types) { - block_ids.push(mc.getBlockId(block_type)) - } - } +export function getNearestBlocks(ctx: WorldContext, blockTypes: string[] | string | null = null, distance: number = 16, count: number = 10000): Block[] { + const blockIds = blockTypes === null + ? mc.getAllBlockIds(['air']) + : (Array.isArray(blockTypes) ? blockTypes : [blockTypes]).map(mc.getBlockId) - const positions = bot.findBlocks({ matching: block_ids, maxDistance: distance, count }) - const blocks = [] - for (let i = 0; i < positions.length; i++) { - const block = bot.blockAt(positions[i]) - const distance = positions[i].distanceTo(bot.entity.position) - blocks.push({ block, distance }) - } - blocks.sort((a, b) => a.distance - b.distance) + const positions = ctx.bot.findBlocks({ matching: blockIds, maxDistance: distance, count }) - const res = [] - for (let i = 0; i < blocks.length; i++) { - res.push(blocks[i].block) - } - return res + return positions + .map((pos) => { + const block = ctx.bot.blockAt(pos) + const dist = pos.distanceTo(ctx.bot.entity.position) + return block ? { block, distance: dist } : null + }) + .filter((item): item is { block: Block, distance: number } => item !== null) + .sort((a, b) => a.distance - b.distance) + .map(item => item.block) } -export function getNearestBlock(bot, block_type, distance = 16) { - /** - * Get the nearest block of the given type. - * @param {Bot} bot - The bot to get the nearest block for. - * @param {string} block_type - The name of the block to search for. - * @param {number} distance - The maximum distance to search, default 16. - * @returns {Block} - The nearest block of the given type. - * @example - * let coalBlock = world.getNearestBlock(bot, 'coal_ore', 16); - */ - const blocks = getNearestBlocks(bot, block_type, distance, 1) - if (blocks.length > 0) { - return blocks[0] - } - return null +export function getNearestBlock(ctx: WorldContext, blockType: string, distance: number = 16): Block | null { + const blocks = getNearestBlocks(ctx, blockType, distance, 1) + return blocks[0] || null } -export function getNearbyEntities(bot, maxDistance = 16) { - const entities = [] - for (const entity of Object.values(bot.entities)) { - const distance = entity.position.distanceTo(bot.entity.position) - if (distance > maxDistance) - continue - entities.push({ entity, distance }) - } - entities.sort((a, b) => a.distance - b.distance) - const res = [] - for (let i = 0; i < entities.length; i++) { - res.push(entities[i].entity) - } - return res +export function getNearbyEntities(ctx: WorldContext, maxDistance: number = 16): Entity[] { + return Object.values(ctx.bot.entities) + .filter((entity): entity is Entity => + entity !== null + && entity.position.distanceTo(ctx.bot.entity.position) <= maxDistance, + ) + .sort((a, b) => + a.position.distanceTo(ctx.bot.entity.position) + - b.position.distanceTo(ctx.bot.entity.position), + ) } -export function getNearestEntityWhere(bot, predicate, maxDistance = 16) { - return bot.nearestEntity(entity => predicate(entity) && bot.entity.position.distanceTo(entity.position) < maxDistance) +export function getNearestEntityWhere(ctx: WorldContext, predicate: (entity: Entity) => boolean, maxDistance: number = 16): Entity | null { + return ctx.bot.nearestEntity(entity => + predicate(entity) + && ctx.bot.entity.position.distanceTo(entity.position) < maxDistance, + ) } -export function getNearbyPlayers(bot, maxDistance) { - if (maxDistance == null) - maxDistance = 16 - const players = [] - for (const entity of Object.values(bot.entities)) { - const distance = entity.position.distanceTo(bot.entity.position) - if (distance > maxDistance) - continue - if (entity.type == 'player' && entity.username != bot.username) { - players.push({ entity, distance }) - } - } - players.sort((a, b) => a.distance - b.distance) - const res = [] - for (let i = 0; i < players.length; i++) { - res.push(players[i].entity) - } - return res +export function getNearbyPlayers(ctx: WorldContext, maxDistance: number = 16): Entity[] { + return getNearbyEntities(ctx, maxDistance) + .filter(entity => + entity.type === 'player' + && entity.username !== ctx.bot.username, + ) } -export function getInventoryStacks(bot) { - const inventory = [] - for (const item of bot.inventory.items()) { - if (item != null) { - inventory.push(item) - } - } - return inventory +export function getInventoryStacks(ctx: WorldContext): Item[] { + return ctx.bot.inventory.items().filter((item): item is Item => item !== null) } -export function getInventoryCounts(bot) { - /** - * Get an object representing the bot's inventory. - * @param {Bot} bot - The bot to get the inventory for. - * @returns {object} - An object with item names as keys and counts as values. - * @example - * let inventory = world.getInventoryCounts(bot); - * let oakLogCount = inventory['oak_log']; - * let hasWoodenPickaxe = inventory['wooden_pickaxe'] > 0; - */ - const inventory = {} - for (const item of bot.inventory.items()) { - if (item != null) { - if (inventory[item.name] == null) { - inventory[item.name] = 0 - } - inventory[item.name] += item.count - } - } - return inventory +export function getInventoryCounts(ctx: WorldContext): Record { + return getInventoryStacks(ctx).reduce((counts, item) => { + counts[item.name] = (counts[item.name] || 0) + item.count + return counts + }, {} as Record) } -export function getCraftableItems(bot) { - /** - * Get a list of all items that can be crafted with the bot's current inventory. - * @param {Bot} bot - The bot to get the craftable items for. - * @returns {string[]} - A list of all items that can be crafted. - * @example - * let craftableItems = world.getCraftableItems(bot); - */ - let table = getNearestBlock(bot, 'crafting_table') - if (!table) { - for (const item of bot.inventory.items()) { - if (item != null && item.name === 'crafting_table') { - table = item - break - } - } - } - const res = [] - for (const item of mc.getAllItems()) { - const recipes = bot.recipesFor(item.id, null, 1, table) - if (recipes.length > 0) - res.push(item.name) - } - return res +export function getCraftableItems(ctx: WorldContext): string[] { + const table = getNearestBlock(ctx, 'crafting_table') + || getInventoryStacks(ctx).find(item => item.name === 'crafting_table') + + return mc.getAllItems() + .filter(item => ctx.bot.recipesFor(item.id, null, 1, table).length > 0) + .map(item => item.name) } -export function getPosition(bot) { - /** - * Get your position in the world (Note that y is vertical). - * @param {Bot} bot - The bot to get the position for. - * @returns {Vec3} - An object with x, y, and x attributes representing the position of the bot. - * @example - * let position = world.getPosition(bot); - * let x = position.x; - */ - return bot.entity.position +export function getPosition(ctx: WorldContext): Vec3 { + return ctx.bot.entity.position } -export function getNearbyEntityTypes(bot) { - /** - * Get a list of all nearby mob types. - * @param {Bot} bot - The bot to get nearby mobs for. - * @returns {string[]} - A list of all nearby mobs. - * @example - * let mobs = world.getNearbyEntityTypes(bot); - */ - const mobs = getNearbyEntities(bot, 16) - const found = [] - for (let i = 0; i < mobs.length; i++) { - if (!found.includes(mobs[i].name)) { - found.push(mobs[i].name) - } - } - return found +export function getNearbyEntityTypes(ctx: WorldContext): string[] { + return [...new Set( + getNearbyEntities(ctx, 16) + .map(mob => mob.name) + .filter((name): name is string => name !== undefined), + )] } -export function getNearbyPlayerNames(bot) { - /** - * Get a list of all nearby player names. - * @param {Bot} bot - The bot to get nearby players for. - * @returns {string[]} - A list of all nearby players. - * @example - * let players = world.getNearbyPlayerNames(bot); - */ - const players = getNearbyPlayers(bot, 64) - const found = [] - for (let i = 0; i < players.length; i++) { - if (!found.includes(players[i].username) && players[i].username != bot.username) { - found.push(players[i].username) - } - } - return found +export function getNearbyPlayerNames(ctx: WorldContext): string[] { + return [...new Set( + getNearbyPlayers(ctx, 64) + .map(player => player.username) + .filter((name): name is string => + name !== undefined + && name !== ctx.bot.username, + ), + )] } -export function getNearbyBlockTypes(bot, distance = 16) { - /** - * Get a list of all nearby block names. - * @param {Bot} bot - The bot to get nearby blocks for. - * @param {number} distance - The maximum distance to search, default 16. - * @returns {string[]} - A list of all nearby blocks. - * @example - * let blocks = world.getNearbyBlockTypes(bot); - */ - const blocks = getNearestBlocks(bot, null, distance) - const found = [] - for (let i = 0; i < blocks.length; i++) { - if (!found.includes(blocks[i].name)) { - found.push(blocks[i].name) - } - } - return found +export function getNearbyBlockTypes(ctx: WorldContext, distance: number = 16): string[] { + return [...new Set( + getNearestBlocks(ctx, null, distance) + .map(block => block.name), + )] } -export async function isClearPath(bot, target) { - /** - * Check if there is a path to the target that requires no digging or placing blocks. - * @param {Bot} bot - The bot to get the path for. - * @param {Entity} target - The target to path to. - * @returns {boolean} - True if there is a clear path, false otherwise. - */ - const movements = new pf.Movements(bot) +export async function isClearPath(ctx: WorldContext, target: Entity): Promise { + const movements = new pf.Movements(ctx.bot) movements.canDig = false movements.canPlaceOn = false - const goal = new pf.goals.GoalNear(target.position.x, target.position.y, target.position.z, 1) - const path = await bot.pathfinder.getPathTo(movements, goal, 100) + + const goal = new pf.goals.GoalNear( + target.position.x, + target.position.y, + target.position.z, + 1, + ) + + const path = await ctx.bot.pathfinder.getPathTo(movements, goal, 100) return path.status === 'success' } -export function shouldPlaceTorch(bot) { - if (!bot.modes.isOn('torch_placing') || bot.interrupt_code) +export function shouldPlaceTorch(ctx: WorldContext): boolean { + // if (!ctx.bot.modes.isOn('torch_placing') || ctx.bot.interrupt_code) { + // return false + // } + + const pos = getPosition(ctx) + const nearestTorch = getNearestBlock(ctx, 'torch', 6) + || getNearestBlock(ctx, 'wall_torch', 6) + + if (nearestTorch) { return false - const pos = getPosition(bot) - // TODO: check light level instead of nearby torches, block.light is broken - let nearest_torch = getNearestBlock(bot, 'torch', 6) - if (!nearest_torch) - nearest_torch = getNearestBlock(bot, 'wall_torch', 6) - if (!nearest_torch) { - const block = bot.blockAt(pos) - const has_torch = bot.inventory.items().find(item => item.name === 'torch') - return has_torch && block?.name === 'air' } - return false + + const block = ctx.bot.blockAt(pos) + const hasTorch = ctx.bot.inventory.items().some(item => item?.name === 'torch') + + return Boolean(hasTorch && block?.name === 'air') } -export function getBiomeName(bot) { - /** - * Get the name of the biome the bot is in. - * @param {Bot} bot - The bot to get the biome for. - * @returns {string} - The name of the biome. - * @example - * let biome = world.getBiomeName(bot); - */ - const biomeId = bot.world.getBiome(bot.entity.position) +export function getBiomeName(ctx: WorldContext): string { + const biomeId = ctx.bot.world.getBiome(ctx.bot.entity.position) return mc.getAllBiomes()[biomeId].name } diff --git a/services/minecraft/src/main.ts b/services/minecraft/src/main.ts index ef0ea2e7a..4ab245a74 100644 --- a/services/minecraft/src/main.ts +++ b/services/minecraft/src/main.ts @@ -28,7 +28,7 @@ async function main() { registerComponent('command', createCommandComponent) }) - initAgent() + initAgent(ctx) process.on('SIGINT', () => { cleanup() diff --git a/services/minecraft/src/prompts/agent.ts b/services/minecraft/src/prompts/agent.ts index 990340b2d..8c41887c0 100644 --- a/services/minecraft/src/prompts/agent.ts +++ b/services/minecraft/src/prompts/agent.ts @@ -1,4 +1,5 @@ import type { BotContext } from '../composables/bot' +import { getStatusToString } from '../components/status' export function basicSystemPrompt(botName: string): string { return `You are a playful Minecraft bot named ${botName} that can converse with players, see, move, @@ -40,13 +41,7 @@ task to do in Minecraft. My ultimate goal is to discover as many things as possi accomplish as many tasks as possible and become the best Minecraft player in the world. I will give you the following information: -${Array.from(ctx.status.entries()).map(([key, value]) => `${key}: ${value}`).join('\n')} - -Then you can choose some of the tools to use. Use the valid JS call function to call the tool. - -## For example: -### Get the stats -stats() +${getStatusToString(ctx)} ` return prompt