← Public packages
@kentcdodds/world-builder
Build a shared 3D voxel world with friends and your AI agents, live.
src/lib/agent.ts
75 lines · 2.3 KB · TypeScriptimport { LIMITS } from '../shared/world-rules.ts'
import { agentPlayer, getCaller } from './identity.ts'
import {
WorldError,
applyBlockOps,
ownedBuild,
parseBlockOp,
requireUuid,
sleep,
type BlockOp,
} from './world-ops.ts'
export async function requireAgent(agentName: unknown) {
const caller = await getCaller()
if (!caller) throw new WorldError('Sign in to Kody to build.', 401)
return { caller, player: agentPlayer(caller, agentName) }
}
/**
* Stream block writes for a build in small batches so every viewer sees the
* structure appear block-by-block.
*/
export async function streamBuildOps(input: {
buildId: unknown
blocks: unknown
kind: 'put' | 'del'
batchSize?: unknown
delayMs?: unknown
}) {
const caller = await getCaller()
if (!caller) throw new WorldError('Sign in to Kody to build.', 401)
const buildId = requireUuid(input.buildId, 'buildId')
const build = await ownedBuild(buildId, caller.username)
if (build.status === 'done' || build.status === 'failed') {
throw new WorldError('That build is finished. Start a new build.', 409)
}
if (!Array.isArray(input.blocks)) throw new WorldError('blocks must be an array of {x, y, z, material}.')
if (input.blocks.length > LIMITS.maxAgentBlocksPerCall) {
throw new WorldError(
`At most ${LIMITS.maxAgentBlocksPerCall} blocks per call. Split the build into several calls.`,
)
}
const ops: Array<BlockOp> = []
let rejected = 0
for (const raw of input.blocks) {
const op = parseBlockOp(raw, input.kind)
if (op) ops.push(op)
else rejected++
}
const batchSize = clampInt(input.batchSize, 1, 60, 24)
const delayMs = clampInt(input.delayMs, 0, 1000, 150)
let placed = 0
let removed = 0
let batches = 0
for (let i = 0; i < ops.length; i += batchSize) {
const chunk = ops.slice(i, i + batchSize)
const out = await applyBlockOps({
player: build.player,
worldId: build.worldId,
ops: chunk,
buildId: input.kind === 'put' ? buildId : null,
})
placed += out.placed
removed += out.removed
batches++
if (delayMs && i + batchSize < ops.length) await sleep(delayMs)
}
return { buildId, placed, removed, rejected, batches }
}
function clampInt(value: unknown, min: number, max: number, fallback: number) {
const n = Number(value)
if (!Number.isFinite(n)) return fallback
return Math.max(min, Math.min(max, Math.round(n)))
}