feat(minecraft): semantic percetion, belief and state pulling
This commit is contained in:
@@ -56,11 +56,16 @@ export class Brain {
|
||||
public init(bot: MineflayerWithAgents): void {
|
||||
this.log('INFO', 'Brain: Initializing...')
|
||||
|
||||
// Unified Perception Signal Handler
|
||||
// this.deps.eventManager.on<PerceptionSignal>('perception', async (event) => {
|
||||
// this.log('INFO', `Brain: Received perception signal: ${event.payload.type} - ${event.payload.description}`)
|
||||
// await this.enqueueEvent(bot, event)
|
||||
// })
|
||||
// Perception Signal Handler - Only process chat messages for now
|
||||
this.deps.eventManager.on<PerceptionSignal>('perception', async (event) => {
|
||||
const signal = event.payload
|
||||
// Only handle chat messages in the deliberative layer
|
||||
if (signal.type !== 'chat_message')
|
||||
return
|
||||
|
||||
this.log('INFO', `Brain: Received chat: ${signal.description}`)
|
||||
await this.enqueueEvent(bot, event)
|
||||
})
|
||||
|
||||
// Listen to Task Execution Events (Action Feedback)
|
||||
this.deps.taskExecutor.on('action:completed', async ({ action, result }) => {
|
||||
|
||||
@@ -117,8 +117,12 @@ export function createAgentContainer(options: {
|
||||
}),
|
||||
|
||||
// Reflex Manager (Reactive Layer)
|
||||
reflexManager: asFunction(({ eventBus, logger }) =>
|
||||
new ReflexManager({ eventBus, logger }),
|
||||
reflexManager: asFunction(({ eventBus, perceptionPipeline, logger }) =>
|
||||
new ReflexManager({
|
||||
eventBus,
|
||||
perception: perceptionPipeline.getPerceptionAPI(),
|
||||
logger,
|
||||
}),
|
||||
).singleton(),
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import type { Logg } from '@guiiai/logg'
|
||||
|
||||
import type { Belief, EntityState, EntityView } from '../world/types'
|
||||
|
||||
import { BeliefEngine } from '../semantics/belief-engine'
|
||||
import { teabagPattern } from '../semantics/patterns/teabag'
|
||||
import { EntityStore } from '../world/entity-store'
|
||||
import { TemporalBuffer } from '../world/temporal-buffer'
|
||||
|
||||
/**
|
||||
* Unified perception API for upper layers
|
||||
* Provides entity queries and belief computations
|
||||
*/
|
||||
export class PerceptionAPI {
|
||||
private store: EntityStore
|
||||
private buffer: TemporalBuffer
|
||||
private engine: BeliefEngine
|
||||
|
||||
constructor(
|
||||
private readonly deps: {
|
||||
logger: Logg
|
||||
},
|
||||
) {
|
||||
this.deps.logger.log('PerceptionAPI: initialized')
|
||||
this.store = new EntityStore()
|
||||
this.buffer = new TemporalBuffer(5000) // 5s history
|
||||
this.engine = new BeliefEngine()
|
||||
|
||||
// Register default patterns
|
||||
this.engine.register(teabagPattern)
|
||||
}
|
||||
|
||||
// ============ Entity Updates (from Mineflayer) ============
|
||||
|
||||
/**
|
||||
* Update an entity's state (called by perception collector)
|
||||
*/
|
||||
updateEntity(id: string, partial: Partial<EntityState>): void {
|
||||
const changes = this.store.update(id, partial)
|
||||
this.buffer.recordAll(changes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an entity
|
||||
*/
|
||||
removeEntity(id: string): void {
|
||||
this.store.remove(id)
|
||||
this.buffer.clearEntity(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update self position (for distance calculations)
|
||||
*/
|
||||
updateSelfPosition(x: number, y: number, z: number): void {
|
||||
this.store.updateSelfPosition({ x, y, z } as any)
|
||||
}
|
||||
|
||||
// ============ Entity Queries ============
|
||||
|
||||
/**
|
||||
* Get all player entities with computed beliefs
|
||||
*/
|
||||
getPlayers(): EntityView[] {
|
||||
return this.store.getPlayers().map(e => this.buildEntityView(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific entity by ID
|
||||
*/
|
||||
getEntity(id: string): EntityView | null {
|
||||
const state = this.store.get(id)
|
||||
if (!state)
|
||||
return null
|
||||
return this.buildEntityView(state)
|
||||
}
|
||||
|
||||
// ============ Belief Queries ============
|
||||
|
||||
/**
|
||||
* Find entities with high confidence of a pattern
|
||||
*/
|
||||
entitiesWithBelief(pattern: string, minConfidence: number = 0.5): EntityView[] {
|
||||
return this.getPlayers().filter(e => (e.beliefs[pattern]?.confidence ?? 0) >= minConfidence)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the top belief for an entity
|
||||
*/
|
||||
getTopBelief(entityId: string): { pattern: string, belief: Belief } | null {
|
||||
const entity = this.getEntity(entityId)
|
||||
if (!entity)
|
||||
return null
|
||||
|
||||
let top: { pattern: string, belief: Belief } | null = null
|
||||
for (const [pattern, belief] of Object.entries(entity.beliefs)) {
|
||||
if (!top || belief.confidence > top.belief.confidence) {
|
||||
top = { pattern, belief }
|
||||
}
|
||||
}
|
||||
return top
|
||||
}
|
||||
|
||||
// ============ Pattern Management ============
|
||||
|
||||
/**
|
||||
* Register a custom pattern
|
||||
*/
|
||||
registerPattern(pattern: Parameters<BeliefEngine['register']>[0]): void {
|
||||
this.engine.register(pattern)
|
||||
}
|
||||
|
||||
// ============ Maintenance ============
|
||||
|
||||
/**
|
||||
* Prune old history entries
|
||||
*/
|
||||
prune(): void {
|
||||
this.buffer.prune()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all state
|
||||
*/
|
||||
clear(): void {
|
||||
this.store.clear()
|
||||
this.buffer.clear()
|
||||
}
|
||||
|
||||
// ============ Internal ============
|
||||
|
||||
private buildEntityView(state: EntityState): EntityView {
|
||||
const beliefs = this.engine.computeBeliefs(
|
||||
state.id,
|
||||
id => this.store.get(id),
|
||||
(id, since) => this.buffer.query(id, since),
|
||||
this.store.getSelfPosition(),
|
||||
)
|
||||
|
||||
return {
|
||||
id: state.id,
|
||||
name: state.name ?? state.id,
|
||||
type: state.type,
|
||||
state,
|
||||
beliefs,
|
||||
distanceToSelf: this.store.distanceToSelf(state.id) ?? Infinity,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,12 @@ import type { PerceptionStage } from './types/stage'
|
||||
import { DebugService } from '../../debug'
|
||||
import { createPerceptionFrameFromRawEvent } from './frame'
|
||||
import { MineflayerPerceptionCollector } from './mineflayer-perception-collector'
|
||||
import { PerceptionAPI } from './perception-api'
|
||||
import { SaliencyDetector } from './saliency-detector'
|
||||
|
||||
export class PerceptionPipeline {
|
||||
private readonly detector: SaliencyDetector
|
||||
private readonly perception: PerceptionAPI
|
||||
private collector: MineflayerPerceptionCollector | null = null
|
||||
private initialized = false
|
||||
|
||||
@@ -31,6 +33,8 @@ export class PerceptionPipeline {
|
||||
logger: Logg
|
||||
},
|
||||
) {
|
||||
this.perception = new PerceptionAPI({ logger: this.deps.logger })
|
||||
|
||||
this.detector = new SaliencyDetector({
|
||||
logger: this.deps.logger,
|
||||
onAttention: (signal) => {
|
||||
@@ -44,6 +48,31 @@ export class PerceptionPipeline {
|
||||
})
|
||||
|
||||
this.stages = [
|
||||
{
|
||||
name: 'entity_update',
|
||||
handle: (frame) => {
|
||||
if (frame.kind !== 'world_raw')
|
||||
return frame
|
||||
|
||||
const raw = frame.raw as RawPerceptionEvent
|
||||
|
||||
// Feed entity updates to PerceptionAPI
|
||||
if ('entityId' in raw && 'entityType' in raw) {
|
||||
const entityRaw = raw as RawPerceptionEvent & { entityId: string, entityType: string, displayName?: string, pos?: { x: number, y: number, z: number } }
|
||||
if (entityRaw.entityType === 'player') {
|
||||
this.perception.updateEntity(entityRaw.entityId, {
|
||||
id: entityRaw.entityId,
|
||||
type: 'player',
|
||||
name: entityRaw.displayName,
|
||||
position: entityRaw.pos as any,
|
||||
isSneaking: 'sneaking' in entityRaw ? (entityRaw as any).sneaking : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return frame
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'attention',
|
||||
handle: (frame) => {
|
||||
@@ -152,6 +181,13 @@ export class PerceptionPipeline {
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the PerceptionAPI for querying entity beliefs
|
||||
*/
|
||||
public getPerceptionAPI(): PerceptionAPI {
|
||||
return this.perception
|
||||
}
|
||||
|
||||
public ingest(frame: PerceptionFrame): void {
|
||||
if (!this.initialized)
|
||||
return
|
||||
|
||||
@@ -5,22 +5,24 @@ export const teabagBehavior: ReflexBehavior = {
|
||||
modes: ['social', 'idle'],
|
||||
cooldownMs: 5000,
|
||||
|
||||
when: (ctx) => {
|
||||
// Check if we recently received a teabag signal
|
||||
// Check if we recently received a teabag signal
|
||||
if (ctx.social.lastGesture === 'teabag') {
|
||||
const now = Date.now()
|
||||
const signalAge = now - (ctx.social.lastGestureAt || 0)
|
||||
|
||||
// Only respond if signal is fresh (< 2s)
|
||||
return signalAge < 2000
|
||||
}
|
||||
return false
|
||||
when: (_ctx, api) => {
|
||||
// Check if any player is teabagging with high confidence
|
||||
if (!api?.perception)
|
||||
return false
|
||||
const teabaggers = api.perception.entitiesWithBelief('teabag', 0.6)
|
||||
return teabaggers.length > 0
|
||||
},
|
||||
|
||||
score: () => {
|
||||
// Higher priority than LookAt (50)
|
||||
return 60
|
||||
score: (_ctx, api) => {
|
||||
// Higher priority than LookAt (50), scaled by confidence
|
||||
if (!api?.perception)
|
||||
return 0
|
||||
const teabaggers = api.perception.entitiesWithBelief('teabag', 0.6)
|
||||
if (teabaggers.length === 0)
|
||||
return 0
|
||||
// Use highest confidence as score boost
|
||||
const maxConfidence = Math.max(...teabaggers.map(e => e.beliefs.teabag?.confidence ?? 0))
|
||||
return 60 + (maxConfidence * 20)
|
||||
},
|
||||
|
||||
run: async ({ bot }) => {
|
||||
|
||||
@@ -43,7 +43,14 @@ describe('reflexManager', () => {
|
||||
} as any
|
||||
|
||||
const logger = makeLogger()
|
||||
const reflex = new ReflexManager({ eventBus, logger }) // Now accepts eventBus
|
||||
const perception = {
|
||||
getPlayers: vi.fn(() => []),
|
||||
getEntity: vi.fn(() => null),
|
||||
entitiesWithBelief: vi.fn(() => []),
|
||||
updateEntity: vi.fn(),
|
||||
updateSelfPosition: vi.fn(),
|
||||
} as any
|
||||
const reflex = new ReflexManager({ eventBus, perception, logger })
|
||||
|
||||
const bot = makeBot()
|
||||
reflex.init(bot)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Logg } from '@guiiai/logg'
|
||||
|
||||
import type { EventBus, TracedEvent } from '../os'
|
||||
import type { PerceptionAPI } from '../perception/perception-api'
|
||||
import type { PerceptionSignal } from '../perception/types/signals'
|
||||
import type { MineflayerWithAgents } from '../types'
|
||||
import type { ReflexContextState } from './context'
|
||||
@@ -19,6 +20,7 @@ export class ReflexManager {
|
||||
constructor(
|
||||
private readonly deps: {
|
||||
eventBus: EventBus
|
||||
perception: PerceptionAPI
|
||||
logger: Logg
|
||||
},
|
||||
) {
|
||||
@@ -86,7 +88,7 @@ export class ReflexManager {
|
||||
// For greeting behavior compatibility, we might need to map specific signals to social state.
|
||||
|
||||
// Trigger behavior selection
|
||||
this.runtime.tick(bot, 0)
|
||||
this.runtime.tick(bot, 0, this.deps.perception)
|
||||
|
||||
// Emit reflex state for observability
|
||||
DebugService.getInstance().emitReflexState({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Logg } from '@guiiai/logg'
|
||||
|
||||
import type { PerceptionAPI } from '../perception/perception-api'
|
||||
import type { MineflayerWithAgents } from '../types'
|
||||
import type { ReflexModeId } from './modes'
|
||||
import type { ReflexBehavior } from './types/behavior'
|
||||
@@ -38,7 +39,7 @@ export class ReflexRuntime {
|
||||
this.behaviors.push(behavior)
|
||||
}
|
||||
|
||||
public tick(bot: MineflayerWithAgents, deltaMs: number): string | null {
|
||||
public tick(bot: MineflayerWithAgents, deltaMs: number, perception: PerceptionAPI): string | null {
|
||||
const now = Date.now()
|
||||
|
||||
this.context.updateNow(now)
|
||||
@@ -73,16 +74,17 @@ export class ReflexRuntime {
|
||||
this.activeBehaviorUntil = null
|
||||
|
||||
const ctx = this.context.getSnapshot()
|
||||
const api = { bot, context: this.context, perception }
|
||||
|
||||
let best: { behavior: ReflexBehavior, score: number } | null = null
|
||||
for (const behavior of this.behaviors) {
|
||||
if (!behavior.modes.includes(this.mode))
|
||||
continue
|
||||
|
||||
if (!behavior.when(ctx))
|
||||
if (!behavior.when(ctx, api))
|
||||
continue
|
||||
|
||||
const score = behavior.score(ctx)
|
||||
const score = behavior.score(ctx, api)
|
||||
if (score <= 0)
|
||||
continue
|
||||
|
||||
@@ -102,7 +104,7 @@ export class ReflexRuntime {
|
||||
this.runHistory.set(best.behavior.id, { lastRunAt: now })
|
||||
|
||||
try {
|
||||
const maybePromise = best.behavior.run({ bot, context: this.context })
|
||||
const maybePromise = best.behavior.run(api)
|
||||
if (maybePromise && typeof (maybePromise as any).then === 'function') {
|
||||
this.activeBehaviorUntil = now + Math.max(deltaMs, 50)
|
||||
void (maybePromise as Promise<void>).finally(() => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PerceptionAPI } from '../../perception/perception-api'
|
||||
import type { MineflayerWithAgents } from '../../types'
|
||||
import type { ReflexContext } from '../context'
|
||||
import type { ReflexModeId } from '../modes'
|
||||
@@ -5,14 +6,15 @@ import type { ReflexModeId } from '../modes'
|
||||
export interface ReflexApi {
|
||||
bot: MineflayerWithAgents
|
||||
context: ReflexContext
|
||||
perception: PerceptionAPI
|
||||
}
|
||||
|
||||
export interface ReflexBehavior {
|
||||
id: string
|
||||
modes: ReflexModeId[]
|
||||
cooldownMs?: number
|
||||
when: (ctx: ReturnType<ReflexContext['getSnapshot']>) => boolean
|
||||
score: (ctx: ReturnType<ReflexContext['getSnapshot']>) => number
|
||||
when: (ctx: ReturnType<ReflexContext['getSnapshot']>, api?: ReflexApi) => boolean
|
||||
score: (ctx: ReturnType<ReflexContext['getSnapshot']>, api?: ReflexApi) => number
|
||||
run: (api: ReflexApi) => Promise<void> | void
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { Vec3 } from 'vec3'
|
||||
|
||||
import type { Belief, EntityState, PatternDefinition, StateChange } from '../world/types'
|
||||
|
||||
/**
|
||||
* Computes beliefs about entity behaviors based on registered patterns
|
||||
*/
|
||||
export class BeliefEngine {
|
||||
private patterns: Map<string, PatternDefinition> = new Map()
|
||||
|
||||
/**
|
||||
* Register a pattern
|
||||
*/
|
||||
register(pattern: PatternDefinition): void {
|
||||
this.patterns.set(pattern.id, pattern)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a pattern
|
||||
*/
|
||||
unregister(id: string): void {
|
||||
this.patterns.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered pattern IDs
|
||||
*/
|
||||
getPatternIds(): string[] {
|
||||
return Array.from(this.patterns.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute all beliefs for an entity
|
||||
*/
|
||||
computeBeliefs(
|
||||
entityId: string,
|
||||
getState: (id: string) => EntityState | null,
|
||||
getHistory: (id: string, since: number) => StateChange[],
|
||||
selfPosition: Vec3 | null,
|
||||
): Record<string, Belief> {
|
||||
const beliefs: Record<string, Belief> = {}
|
||||
|
||||
for (const [id, pattern] of this.patterns) {
|
||||
try {
|
||||
beliefs[id] = pattern.compute(entityId, getState, getHistory, selfPosition)
|
||||
}
|
||||
catch {
|
||||
// Pattern threw an error, treat as no belief
|
||||
beliefs[id] = { confidence: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
return beliefs
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a single belief for an entity
|
||||
*/
|
||||
computeBelief(
|
||||
patternId: string,
|
||||
entityId: string,
|
||||
getState: (id: string) => EntityState | null,
|
||||
getHistory: (id: string, since: number) => StateChange[],
|
||||
selfPosition: Vec3 | null,
|
||||
): Belief | null {
|
||||
const pattern = this.patterns.get(patternId)
|
||||
if (!pattern)
|
||||
return null
|
||||
|
||||
try {
|
||||
return pattern.compute(entityId, getState, getHistory, selfPosition)
|
||||
}
|
||||
catch {
|
||||
return { confidence: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { BeliefEngine } from './belief-engine'
|
||||
export { teabagPattern } from './patterns/teabag'
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { PatternDefinition } from '../../world/types'
|
||||
|
||||
/**
|
||||
* Teabag pattern: detects rapid crouching behavior
|
||||
*/
|
||||
export const teabagPattern: PatternDefinition = {
|
||||
id: 'teabag',
|
||||
category: 'social',
|
||||
description: 'Rapid crouching, typically a greeting or taunt',
|
||||
|
||||
compute(entityId, getState, getHistory, selfPosition) {
|
||||
// Get sneaking state changes in the last 2 seconds
|
||||
const since = Date.now() - 2000
|
||||
const changes = getHistory(entityId, since).filter(c => c.field === 'isSneaking')
|
||||
|
||||
// Need at least 4 toggles (2 full crouch cycles)
|
||||
if (changes.length < 4) {
|
||||
return { confidence: 0 }
|
||||
}
|
||||
|
||||
// Check distance if we have self position
|
||||
const entity = getState(entityId)
|
||||
if (entity && selfPosition) {
|
||||
const dx = entity.position.x - selfPosition.x
|
||||
const dy = entity.position.y - selfPosition.y
|
||||
const dz = entity.position.z - selfPosition.z
|
||||
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||
|
||||
// Too far away, reduce confidence
|
||||
if (distance > 15) {
|
||||
return { confidence: 0, data: { toggles: changes.length, distance } }
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate frequency (toggles per second)
|
||||
const duration = (changes[changes.length - 1].timestamp - changes[0].timestamp) / 1000
|
||||
const frequency = duration > 0 ? changes.length / duration : 0
|
||||
|
||||
// Confidence based on:
|
||||
// - Number of toggles (more = more confident, up to 8)
|
||||
// - Frequency (faster = more confident, up to 4 Hz)
|
||||
const countFactor = Math.min(1, changes.length / 8)
|
||||
const frequencyFactor = Math.min(1, frequency / 4)
|
||||
|
||||
const confidence = (countFactor * 0.4) + (frequencyFactor * 0.6)
|
||||
|
||||
return {
|
||||
confidence,
|
||||
data: {
|
||||
toggles: changes.length,
|
||||
frequency: Math.round(frequency * 100) / 100,
|
||||
duration: Math.round(duration * 1000),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { Vec3 } from 'vec3'
|
||||
|
||||
import type { EntityState, StateChange } from './types'
|
||||
|
||||
/**
|
||||
* Tracks the state of all known entities
|
||||
*/
|
||||
export class EntityStore {
|
||||
private entities: Map<string, EntityState> = new Map()
|
||||
private selfPosition: Vec3 | null = null
|
||||
|
||||
/**
|
||||
* Update or create an entity's state
|
||||
* Returns list of state changes that occurred
|
||||
*/
|
||||
update(id: string, partial: Partial<EntityState>): StateChange[] {
|
||||
const now = Date.now()
|
||||
const changes: StateChange[] = []
|
||||
const existing = this.entities.get(id)
|
||||
|
||||
if (!existing) {
|
||||
// New entity
|
||||
const newState: EntityState = {
|
||||
id,
|
||||
type: partial.type ?? 'player',
|
||||
name: partial.name,
|
||||
position: partial.position ?? { x: 0, y: 0, z: 0 } as Vec3,
|
||||
velocity: partial.velocity ?? { x: 0, y: 0, z: 0 } as Vec3,
|
||||
yaw: partial.yaw ?? 0,
|
||||
pitch: partial.pitch ?? 0,
|
||||
isSneaking: partial.isSneaking ?? false,
|
||||
isSprinting: partial.isSprinting ?? false,
|
||||
onGround: partial.onGround ?? true,
|
||||
firstSeen: now,
|
||||
lastUpdate: now,
|
||||
}
|
||||
this.entities.set(id, newState)
|
||||
return changes
|
||||
}
|
||||
|
||||
// Track changes to relevant fields
|
||||
const trackedFields: (keyof EntityState)[] = ['isSneaking', 'isSprinting', 'onGround']
|
||||
|
||||
for (const field of trackedFields) {
|
||||
if (field in partial && partial[field] !== existing[field]) {
|
||||
changes.push({
|
||||
entityId: id,
|
||||
field,
|
||||
from: existing[field],
|
||||
to: partial[field],
|
||||
timestamp: now,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
Object.assign(existing, partial, { lastUpdate: now })
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entity by ID
|
||||
*/
|
||||
get(id: string): EntityState | null {
|
||||
return this.entities.get(id) ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all player entities
|
||||
*/
|
||||
getPlayers(): EntityState[] {
|
||||
return Array.from(this.entities.values()).filter(e => e.type === 'player')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entity IDs
|
||||
*/
|
||||
getAllIds(): string[] {
|
||||
return Array.from(this.entities.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an entity
|
||||
*/
|
||||
remove(id: string): void {
|
||||
this.entities.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update self (bot) position for distance calculations
|
||||
*/
|
||||
updateSelfPosition(pos: Vec3): void {
|
||||
this.selfPosition = pos
|
||||
}
|
||||
|
||||
/**
|
||||
* Get self position
|
||||
*/
|
||||
getSelfPosition(): Vec3 | null {
|
||||
return this.selfPosition
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance from self to entity
|
||||
*/
|
||||
distanceToSelf(id: string): number | null {
|
||||
if (!this.selfPosition)
|
||||
return null
|
||||
const entity = this.entities.get(id)
|
||||
if (!entity)
|
||||
return null
|
||||
const dx = entity.position.x - this.selfPosition.x
|
||||
const dy = entity.position.y - this.selfPosition.y
|
||||
const dz = entity.position.z - this.selfPosition.z
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all entities
|
||||
*/
|
||||
clear(): void {
|
||||
this.entities.clear()
|
||||
this.selfPosition = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { EntityStore } from './entity-store'
|
||||
export { TemporalBuffer } from './temporal-buffer'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { StateChange } from './types'
|
||||
|
||||
/**
|
||||
* Rolling window buffer of state changes per entity
|
||||
*/
|
||||
export class TemporalBuffer {
|
||||
private buffer: Map<string, StateChange[]> = new Map()
|
||||
private maxAge: number
|
||||
|
||||
constructor(maxAgeMs: number = 5000) {
|
||||
this.maxAge = maxAgeMs
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a state change
|
||||
*/
|
||||
record(change: StateChange): void {
|
||||
const list = this.buffer.get(change.entityId) ?? []
|
||||
list.push(change)
|
||||
this.buffer.set(change.entityId, list)
|
||||
}
|
||||
|
||||
/**
|
||||
* Record multiple state changes
|
||||
*/
|
||||
recordAll(changes: StateChange[]): void {
|
||||
for (const change of changes) {
|
||||
this.record(change)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query changes for an entity since a given timestamp
|
||||
*/
|
||||
query(entityId: string, since: number): StateChange[] {
|
||||
const list = this.buffer.get(entityId) ?? []
|
||||
return list.filter(c => c.timestamp >= since)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query changes for an entity by field
|
||||
*/
|
||||
queryField(entityId: string, field: string, since: number): StateChange[] {
|
||||
return this.query(entityId, since).filter(c => c.field === field)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all changes for an entity (within max age)
|
||||
*/
|
||||
getAll(entityId: string): StateChange[] {
|
||||
const cutoff = Date.now() - this.maxAge
|
||||
return this.query(entityId, cutoff)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prune old entries from all buffers
|
||||
*/
|
||||
prune(): void {
|
||||
const cutoff = Date.now() - this.maxAge
|
||||
|
||||
for (const [entityId, list] of this.buffer.entries()) {
|
||||
const filtered = list.filter(c => c.timestamp >= cutoff)
|
||||
if (filtered.length === 0) {
|
||||
this.buffer.delete(entityId)
|
||||
}
|
||||
else {
|
||||
this.buffer.set(entityId, filtered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all history for an entity
|
||||
*/
|
||||
clearEntity(entityId: string): void {
|
||||
this.buffer.delete(entityId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all buffers
|
||||
*/
|
||||
clear(): void {
|
||||
this.buffer.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { Vec3 } from 'vec3'
|
||||
|
||||
/**
|
||||
* State of an entity at a point in time
|
||||
*/
|
||||
export interface EntityState {
|
||||
id: string
|
||||
type: 'player' | 'mob' | 'item'
|
||||
name?: string
|
||||
|
||||
// Position & Movement
|
||||
position: Vec3
|
||||
velocity: Vec3
|
||||
yaw: number
|
||||
pitch: number
|
||||
|
||||
// Status flags
|
||||
isSneaking: boolean
|
||||
isSprinting: boolean
|
||||
onGround: boolean
|
||||
|
||||
// Timestamps
|
||||
firstSeen: number
|
||||
lastUpdate: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A change in entity state
|
||||
*/
|
||||
export interface StateChange {
|
||||
entityId: string
|
||||
field: keyof EntityState
|
||||
from: unknown
|
||||
to: unknown
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Belief about an entity's behavior
|
||||
*/
|
||||
export interface Belief {
|
||||
confidence: number // 0-1
|
||||
data?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Definition of a pattern that computes beliefs
|
||||
*/
|
||||
export interface PatternDefinition {
|
||||
id: string
|
||||
category: 'social' | 'spatial' | 'threat' | 'neutral'
|
||||
description: string
|
||||
|
||||
compute: (
|
||||
entityId: string,
|
||||
getState: (id: string) => EntityState | null,
|
||||
getHistory: (id: string, since: number) => StateChange[],
|
||||
selfPosition: Vec3 | null,
|
||||
) => Belief
|
||||
}
|
||||
|
||||
/**
|
||||
* View of an entity for upper layers
|
||||
*/
|
||||
export interface EntityView {
|
||||
id: string
|
||||
name: string
|
||||
type: EntityState['type']
|
||||
state: EntityState
|
||||
beliefs: Record<string, Belief>
|
||||
distanceToSelf: number
|
||||
}
|
||||
Reference in New Issue
Block a user