Jofthomas's picture
Jofthomas HF staff
bulk2
90cbf22
raw
history blame
4.83 kB
import { ConvexError, v } from 'convex/values';
import { DatabaseReader, MutationCtx, internalAction, mutation, query } from '../_generated/server';
import { insertInput } from './insertInput';
import { Game } from './game';
import { internal } from '../_generated/api';
import { sleep } from '../util/sleep';
import { Id } from '../_generated/dataModel';
import { ENGINE_ACTION_DURATION } from '../constants';
export async function createEngine(ctx: MutationCtx) {
const now = Date.now();
const engineId = await ctx.db.insert('engines', {
currentTime: now,
generationNumber: 0,
running: true,
});
return engineId;
}
async function loadWorldStatus(db: DatabaseReader, worldId: Id<'worlds'>) {
const worldStatus = await db
.query('worldStatus')
.withIndex('worldId', (q) => q.eq('worldId', worldId))
.unique();
if (!worldStatus) {
throw new Error(`No engine found for world ${worldId}`);
}
return worldStatus;
}
export async function startEngine(ctx: MutationCtx, worldId: Id<'worlds'>) {
const { engineId } = await loadWorldStatus(ctx.db, worldId);
const engine = await ctx.db.get(engineId);
if (!engine) {
throw new Error(`Invalid engine ID: ${engineId}`);
}
if (engine.running) {
throw new Error(`Engine ${engineId} isn't currently stopped`);
}
const now = Date.now();
const generationNumber = engine.generationNumber + 1;
await ctx.db.patch(engineId, {
// Forcibly advance time to the present. This does mean we'll skip
// simulating the time the engine was stopped, but we don't want
// to have to simulate a potentially large stopped window and send
// it down to clients.
lastStepTs: engine.currentTime,
currentTime: now,
running: true,
generationNumber,
});
await ctx.scheduler.runAfter(0, internal.aiTown.main.runStep, {
worldId: worldId,
generationNumber,
maxDuration: ENGINE_ACTION_DURATION,
});
}
export async function kickEngine(ctx: MutationCtx, worldId: Id<'worlds'>) {
const { engineId } = await loadWorldStatus(ctx.db, worldId);
const engine = await ctx.db.get(engineId);
if (!engine) {
throw new Error(`Invalid engine ID: ${engineId}`);
}
if (!engine.running) {
throw new Error(`Engine ${engineId} isn't currently running`);
}
const generationNumber = engine.generationNumber + 1;
await ctx.db.patch(engineId, { generationNumber });
await ctx.scheduler.runAfter(0, internal.aiTown.main.runStep, {
worldId: worldId,
generationNumber,
maxDuration: ENGINE_ACTION_DURATION,
});
}
export async function stopEngine(ctx: MutationCtx, worldId: Id<'worlds'>) {
const { engineId } = await loadWorldStatus(ctx.db, worldId);
const engine = await ctx.db.get(engineId);
if (!engine) {
throw new Error(`Invalid engine ID: ${engineId}`);
}
if (!engine.running) {
throw new Error(`Engine ${engineId} isn't currently running`);
}
await ctx.db.patch(engineId, { running: false });
}
export const runStep = internalAction({
args: {
worldId: v.id('worlds'),
generationNumber: v.number(),
maxDuration: v.number(),
},
handler: async (ctx, args) => {
try {
const { engine, gameState } = await ctx.runQuery(internal.aiTown.game.loadWorld, {
worldId: args.worldId,
generationNumber: args.generationNumber,
});
const game = new Game(engine, args.worldId, gameState);
let now = Date.now();
const deadline = now + args.maxDuration;
while (now < deadline) {
await game.runStep(ctx, now);
const sleepUntil = Math.min(now + game.stepDuration, deadline);
await sleep(sleepUntil - now);
now = Date.now();
}
await ctx.scheduler.runAfter(0, internal.aiTown.main.runStep, {
worldId: args.worldId,
generationNumber: game.engine.generationNumber,
maxDuration: args.maxDuration,
});
} catch (e: unknown) {
if (e instanceof ConvexError) {
if (e.data.kind === 'engineNotRunning') {
console.debug(`Engine is not running: ${e.message}`);
return;
}
if (e.data.kind === 'generationNumber') {
console.debug(`Generation number mismatch: ${e.message}`);
return;
}
}
throw e;
}
},
});
export const sendInput = mutation({
args: {
worldId: v.id('worlds'),
name: v.string(),
args: v.any(),
},
handler: async (ctx, args) => {
return await insertInput(ctx, args.worldId, args.name as any, args.args);
},
});
export const inputStatus = query({
args: {
inputId: v.id('inputs'),
},
handler: async (ctx, args) => {
const input = await ctx.db.get(args.inputId);
if (!input) {
throw new Error(`Invalid input ID: ${args.inputId}`);
}
return input.returnValue ?? null;
},
});