feat(minecraft): add query.self/snapshot helpers and inventory count/has/summary methods

Add query.self() returning one-shot self state (pos/health/food/heldItem/gameMode/isRaining/timeOfDay), add query.snapshot(range) returning combined self/inventory/nearby snapshot with counts/summary/emptySlots/totalStacks and blocks/entities/ores within range, add inventory.count(name)/has(name, atLeast)/summary() helpers for item queries, register query.self/snapshot as readonly functions in JavaScriptPlanner sandbox
This commit is contained in:
Rin
2026-02-18 11:14:40 +08:00
committed by Neko Ayaka
parent a9f95aeb53
commit 5642b1e204
3 changed files with 97 additions and 0 deletions
@@ -207,6 +207,8 @@ export class JavaScriptPlanner {
{ name: 'llmUserMessage', kind: 'string', readonly: true },
{ name: 'llmConversationHistory', kind: 'object', readonly: true },
{ name: 'query', kind: 'object', readonly: true },
{ name: 'query.self', kind: 'function', readonly: true },
{ name: 'query.snapshot', kind: 'function', readonly: true },
{ name: 'bot', kind: 'object', readonly: true },
{ name: 'mineflayer', kind: 'object', readonly: true },
{ name: 'mem', kind: 'object', readonly: false },
@@ -73,4 +73,29 @@ describe('query DSL', () => {
const pickaxes = query.craftable().whereIncludes('pickaxe').uniq().list()
expect(pickaxes).toEqual(['wooden_pickaxe', 'stone_pickaxe'])
})
it('supports inventory helper methods for count/has/summary', () => {
const query = createQueryRuntime(createMineflayerStub())
expect(query.inventory().count('bread')).toBe(5)
expect(query.inventory().has('bread', 5)).toBe(true)
expect(query.inventory().has('bread', 6)).toBe(false)
expect(query.inventory().summary()).toEqual([
{ name: 'cobblestone', count: 16 },
{ name: 'bread', count: 5 },
])
})
it('returns self and snapshot views for one-shot state reads', () => {
const query = createQueryRuntime(createMineflayerStub())
const self = query.self()
expect(self.pos).toEqual({ x: 0, y: 64, z: 0 })
expect(self.heldItem).toBeNull()
const snap = query.snapshot(12)
expect(snap.inventory.counts).toEqual({
bread: 5,
cobblestone: 16,
})
expect(snap.nearby.ores.map(b => b.name)).toEqual(['coal_ore', 'coal_ore', 'ancient_debris'])
})
})
@@ -31,6 +31,21 @@ interface InventoryRecord {
displayName?: string
}
interface InventorySummaryRecord {
name: string
count: number
}
interface SelfQueryRecord {
pos: { x: number, y: number, z: number }
health: number
food: number
heldItem: string | null
gameMode: string
isRaining: boolean
timeOfDay: number | null
}
type NamePredicate = (value: string) => boolean
class NameQueryChain {
@@ -199,6 +214,28 @@ class InventoryQueryChain {
}, {} as Record<string, number>)
}
public count(name: string): number {
const needle = name.toLowerCase()
return this.list()
.filter(item => item.name.toLowerCase() === needle)
.reduce((sum, item) => sum + item.count, 0)
}
public has(name: string, atLeast = 1): boolean {
return this.count(name) >= Math.max(1, Math.floor(atLeast))
}
public summary(): InventorySummaryRecord[] {
const counts = this.countByName()
return Object.entries(counts)
.map(([name, count]) => ({ name, count }))
.sort((a, b) => {
if (b.count !== a.count)
return b.count - a.count
return a.name.localeCompare(b.name)
})
}
public list(): InventoryRecord[] {
return this.mineflayer.bot.inventory
.items()
@@ -296,8 +333,41 @@ function clamp(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value))
}
function toSelfRecord(mineflayer: Mineflayer): SelfQueryRecord {
return {
pos: toPos(mineflayer.bot.entity.position),
health: mineflayer.bot.health,
food: mineflayer.bot.food,
heldItem: mineflayer.bot.heldItem?.name ?? null,
gameMode: mineflayer.bot.game?.gameMode ?? 'unknown',
isRaining: Boolean(mineflayer.bot.isRaining),
timeOfDay: typeof mineflayer.bot.time?.timeOfDay === 'number' ? mineflayer.bot.time.timeOfDay : null,
}
}
export function createQueryRuntime(mineflayer: Mineflayer) {
return {
self: () => toSelfRecord(mineflayer),
snapshot: (range = 16) => {
const normalizedRange = clamp(Math.floor(range), 1, 64)
const inventory = new InventoryQueryChain(mineflayer)
return {
self: toSelfRecord(mineflayer),
inventory: {
counts: inventory.countByName(),
summary: inventory.summary(),
emptySlots: typeof mineflayer.bot.inventory.emptySlotCount === 'function'
? mineflayer.bot.inventory.emptySlotCount()
: Math.max(0, 36 - mineflayer.bot.inventory.items().length),
totalStacks: mineflayer.bot.inventory.items().length,
},
nearby: {
blocks: new BlockQueryChain(mineflayer).within(normalizedRange).limit(20).list(),
entities: new EntityQueryChain(mineflayer).within(normalizedRange).limit(20).list(),
ores: new BlockQueryChain(mineflayer).within(normalizedRange).isOre().limit(20).list(),
},
}
},
blocks: () => new BlockQueryChain(mineflayer),
blockAt: ({ x, y, z }: { x: number, y: number, z: number }) => {
const block = mineflayer.bot.blockAt(new Vec3(Math.floor(x), Math.floor(y), Math.floor(z)))