Files
moeka-project/services/minecraft/src/cognitive/reflex/runtime.ts
T

130 lines
3.6 KiB
TypeScript

import type { Logg } from '@guiiai/logg'
import type { MineflayerWithAgents } from '../types'
import { ReflexContext } from './context'
import type { ReflexBehavior } from './types/behavior'
import { selectMode, type ReflexModeId } from './modes'
export class ReflexRuntime {
private readonly context = new ReflexContext()
private readonly behaviors: ReflexBehavior[] = []
private readonly runHistory = new Map<string, { lastRunAt: number }>()
private mode: ReflexModeId = 'idle'
private activeBehaviorId: string | null = null
private activeBehaviorUntil: number | null = null
public constructor(
private readonly deps: {
logger: Logg
},
) { }
public getContext(): ReflexContext {
return this.context
}
public getMode(): ReflexModeId {
return this.mode
}
public getActiveBehaviorId(): string | null {
return this.activeBehaviorId
}
public registerBehavior(behavior: ReflexBehavior): void {
this.behaviors.push(behavior)
}
public tick(bot: MineflayerWithAgents, deltaMs: number): string | null {
const now = Date.now()
this.context.updateNow(now)
// TODO: future refactor: update ReflexContext via world_update/self_update events instead of polling Mineflayer state.
this.context.updateSelf({
location: bot.bot.entity.position,
health: bot.bot.health,
food: bot.bot.food,
oxygen: bot.bot.oxygenLevel,
holding: bot.bot.heldItem?.name ?? null,
})
this.context.updateEnvironment({
time: bot.bot.time.isDay ? 'day' : 'night',
weather: bot.bot.isRaining ? 'rain' : 'clear',
nearbyPlayers: Object.keys(bot.bot.players)
.filter(p => p !== bot.bot.username)
.map(name => ({ name })),
})
this.mode = selectMode(this.context.getSnapshot())
if (this.activeBehaviorUntil && now < this.activeBehaviorUntil)
return null
this.activeBehaviorId = null
this.activeBehaviorUntil = null
const ctx = this.context.getSnapshot()
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))
continue
const score = behavior.score(ctx)
if (score <= 0)
continue
const history = this.runHistory.get(behavior.id)
const cooldownMs = behavior.cooldownMs ?? 0
if (history && cooldownMs > 0 && now - history.lastRunAt < cooldownMs)
continue
if (!best || score > best.score)
best = { behavior, score }
}
if (!best)
return null
this.activeBehaviorId = best.behavior.id
this.runHistory.set(best.behavior.id, { lastRunAt: now })
try {
const maybePromise = best.behavior.run({ bot, context: this.context })
if (maybePromise && typeof (maybePromise as any).then === 'function') {
this.activeBehaviorUntil = now + Math.max(deltaMs, 50)
void (maybePromise as Promise<void>).finally(() => {
// Behavior ends naturally; next tick can run a new one.
this.activeBehaviorUntil = null
this.activeBehaviorId = null
})
}
else {
// Synchronous behavior ends immediately.
this.activeBehaviorId = null
}
this.deps.logger.withFields({
mode: this.mode,
behavior: best.behavior.id,
score: best.score,
}).log('ReflexRuntime: selected')
return best.behavior.id
}
catch (err) {
this.deps.logger.withError(err as Error).error('ReflexRuntime: behavior failed')
this.activeBehaviorId = null
this.activeBehaviorUntil = null
return null
}
}
}