fix: world and query

This commit is contained in:
RainbowBird
2025-01-07 00:41:15 +08:00
parent 18e460ae50
commit 2575f45306
5 changed files with 150 additions and 329 deletions
+19 -66
View File
@@ -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<string>
// 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<string, number>
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
@@ -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<string, string> {
const status = new Map<string, string>()
const pos = ctx.bot.entity.position
+124 -255
View File
@@ -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<string, number> {
return getInventoryStacks(ctx).reduce((counts, item) => {
counts[item.name] = (counts[item.name] || 0) + item.count
return counts
}, {} as Record<string, number>)
}
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<boolean> {
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
}
+1 -1
View File
@@ -28,7 +28,7 @@ async function main() {
registerComponent('command', createCommandComponent)
})
initAgent()
initAgent(ctx)
process.on('SIGINT', () => {
cleanup()
+2 -7
View File
@@ -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