Spaces:
Sleeping
Sleeping
File size: 4,827 Bytes
90cbf22 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
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;
},
});
|