import { v } from 'convex/values'; import { internal } from './_generated/api'; import { DatabaseReader, MutationCtx, mutation } from './_generated/server'; import { Descriptions } from '../data/characters'; import * as map from '../data/gentle'; import { insertInput } from './aiTown/insertInput'; import { Id } from './_generated/dataModel'; import { createEngine } from './aiTown/main'; import { ENGINE_ACTION_DURATION, MAX_NPC } from './constants'; import { assertApiKey } from './util/llm'; const init = mutation({ args: { numAgents: v.optional(v.number()), }, handler: async (ctx, args) => { assertApiKey(); const { worldStatus, engine } = await getOrCreateDefaultWorld(ctx); if (worldStatus.status !== 'running') { console.warn( `Engine ${engine._id} is not active! Run "npx convex run testing:resume" to restart it.`, ); return; } const shouldCreate = await shouldCreateAgents( ctx.db, worldStatus.worldId, worldStatus.engineId, ); if (shouldCreate) { const toCreate = args.numAgents !== undefined ? args.numAgents : MAX_NPC; //Descriptions.length; for (let i = 0; i < toCreate; i++) { await insertInput(ctx, worldStatus.worldId, 'createAgent', { descriptionIndex: i % Descriptions.length, type: 'villager', }); } } }, }); export default init; async function getOrCreateDefaultWorld(ctx: MutationCtx) { const now = Date.now(); let worldStatus = await ctx.db .query('worldStatus') .filter((q) => q.eq(q.field('isDefault'), true)) .unique(); if (worldStatus) { const engine = (await ctx.db.get(worldStatus.engineId))!; return { worldStatus, engine }; } const engineId = await createEngine(ctx); const engine = (await ctx.db.get(engineId))!; const worldId = await ctx.db.insert('worlds', { nextId: 0, agents: [], conversations: [], players: [], playersInit: [], // initialize game cycle counter gameCycle: { currentTime: 0, cycleState: 'LobbyState', cycleIndex: -1, cycleNumber:0, }, gameVotes: [], llmVotes: [] }); const worldStatusId = await ctx.db.insert('worldStatus', { engineId: engineId, isDefault: true, lastViewed: now, status: 'running', worldId: worldId, }); worldStatus = (await ctx.db.get(worldStatusId))!; await ctx.db.insert('maps', { worldId, width: map.mapwidth, height: map.mapheight, tileSetUrl: map.tilesetpath, tileSetDimX: map.tilesetpxw, tileSetDimY: map.tilesetpxh, tileDim: map.tiledim, bgTiles: map.bgtiles, objectTiles: map.objmap, decorTiles: map.decors, bgTilesN: map.bgtilesN, objectTilesN: map.objmapN, decorTilesN: map.decorsN, animatedSprites: map.animatedsprites, }); await ctx.scheduler.runAfter(0, internal.aiTown.main.runStep, { worldId, generationNumber: engine.generationNumber, maxDuration: ENGINE_ACTION_DURATION, }); return { worldStatus, engine }; } async function shouldCreateAgents( db: DatabaseReader, worldId: Id<'worlds'>, engineId: Id<'engines'>, ) { const world = await db.get(worldId); if (!world) { throw new Error(`Invalid world ID: ${worldId}`); } if (world.agents.length > 0) { return false; } const unactionedJoinInputs = await db .query('inputs') .withIndex('byInputNumber', (q) => q.eq('engineId', engineId)) .order('asc') .filter((q) => q.eq(q.field('name'), 'createAgent')) .filter((q) => q.eq(q.field('returnValue'), undefined)) .collect(); if (unactionedJoinInputs.length > 0) { return false; } return true; }