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:
@@ -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)))
|
||||
|
||||
Reference in New Issue
Block a user