From 98ae881fe4d7a00a3fc9f26786aa9d4f2e82873e Mon Sep 17 00:00:00 2001 From: Lee Danilek Date: Thu, 31 Aug 2023 17:05:15 -0400 Subject: [PATCH] refactor walking into mutation (#23) refactor the walking and agent completion code so walkToTarget can be called from multiple different mutations. And the agent completion code can be a single mutation instead of a function called from an action, which makes it one atomic mutation with a large read set instead of two. this will come in handy if we want to call these functions from other mutations, especially when we add human interaction. --- convex/agent.ts | 22 +----- convex/engine.ts | 46 ++++++++---- convex/journal.ts | 183 +++++++++++++++++++++++++--------------------- 3 files changed, 131 insertions(+), 120 deletions(-) diff --git a/convex/agent.ts b/convex/agent.ts index 65916c5c..59d5e91e 100644 --- a/convex/agent.ts +++ b/convex/agent.ts @@ -292,28 +292,10 @@ function handleDone(ctx: ActionCtx, noSchedule?: boolean): DoneFn { const doIt: DoneFn = async (agentId, activity) => { // console.debug('handleDone: ', agentId, activity); if (!agentId) return; - let walkResult; - switch (activity.type) { - case 'walk': - walkResult = await ctx.runMutation(internal.journal.walk, { - agentId, - ignore: activity.ignore, - }); - break; - case 'continue': - walkResult = await ctx.runQuery(internal.journal.nextCollision, { - agentId, - ignore: activity.ignore, - }); - break; - default: - const _exhaustiveCheck: never = activity; - throw new Error(`Unhandled activity: ${JSON.stringify(activity)}`); - } await ctx.runMutation(internal.engine.agentDone, { agentId, - otherAgentIds: walkResult.nextCollision?.agentIds, - wakeTs: walkResult.nextCollision?.ts ?? walkResult.targetEndTs, + activity: activity.type, + ignore: activity.ignore, noSchedule, }); }; diff --git a/convex/engine.ts b/convex/engine.ts index c8c721f3..2f965926 100644 --- a/convex/engine.ts +++ b/convex/engine.ts @@ -8,6 +8,7 @@ import { } from './_generated/server'; import { TICK_DEBOUNCE, WORLD_IDLE_THRESHOLD } from './config'; import { asyncMap, pruneNull } from './lib/utils'; +import { getRandomPosition, nextCollision, walkToTarget } from './journal'; export const tick = internalMutation({ args: { worldId: v.id('worlds'), noSchedule: v.optional(v.boolean()) }, @@ -73,32 +74,47 @@ async function getRecentHeartbeat(db: DatabaseReader) { export const agentDone = internalMutation({ args: { agentId: v.id('agents'), - otherAgentIds: v.optional(v.array(v.id('agents'))), - wakeTs: v.number(), + activity: v.union(v.literal('walk'), v.literal('continue')), + ignore: v.array(v.id('players')), noSchedule: v.optional(v.boolean()), }, - handler: async (ctx, args) => { - const agentDoc = await ctx.db.get(args.agentId); - if (!agentDoc) throw new Error(`Agent ${args.agentId} not found`); + handler: async (ctx, { noSchedule, agentId, activity, ignore }) => { + if (!agentId) { + return; + } + const agentDoc = (await ctx.db.get(agentId))!; + const playerId = agentDoc.playerId; + const worldId = agentDoc.worldId; + let walkResult; + switch (activity) { + case 'walk': + const world = (await ctx.db.get(worldId))!; + const map = (await ctx.db.get(world.mapId))!; + const targetPosition = getRandomPosition(map); + walkResult = await walkToTarget(ctx, playerId, worldId, ignore, targetPosition); + break; + case 'continue': + walkResult = await nextCollision(ctx.db, worldId, playerId, ignore); + break; + default: + const _exhaustiveCheck: never = activity; + throw new Error(`Unhandled activity: ${JSON.stringify(activity)}`); + } + if (!agentDoc) throw new Error(`Agent ${agentId} not found`); if (!agentDoc.thinking) { throw new Error('Agent was not thinking: did you call agentDone twice for the same agent?'); } - const nextWakeTs = Math.ceil(args.wakeTs / TICK_DEBOUNCE) * TICK_DEBOUNCE; - await ctx.db.replace(args.agentId, { + const wakeTs = walkResult.nextCollision?.ts ?? walkResult.targetEndTs; + const nextWakeTs = Math.ceil(wakeTs / TICK_DEBOUNCE) * TICK_DEBOUNCE; + await ctx.db.replace(agentId, { playerId: agentDoc.playerId, worldId: agentDoc.worldId, thinking: false, lastWakeTs: agentDoc.nextWakeTs, nextWakeTs, - alsoWake: args.otherAgentIds, - scheduled: await enqueueAgentWake( - ctx, - args.agentId, - agentDoc.worldId, - nextWakeTs, - args.noSchedule, - ), + alsoWake: walkResult.nextCollision?.agentIds, + scheduled: await enqueueAgentWake(ctx, agentId, agentDoc.worldId, nextWakeTs, noSchedule), }); }, }); diff --git a/convex/journal.ts b/convex/journal.ts index 16632643..1487f0fc 100644 --- a/convex/journal.ts +++ b/convex/journal.ts @@ -1,6 +1,6 @@ import { v } from 'convex/values'; import { Doc, Id } from './_generated/dataModel'; -import { DatabaseReader, internalMutation, internalQuery } from './_generated/server'; +import { DatabaseReader, MutationCtx, internalMutation, internalQuery } from './_generated/server'; import { Position, EntryOfType, @@ -268,97 +268,110 @@ export const walk = internalMutation({ const { playerId, worldId } = agentDoc; const world = (await ctx.db.get(worldId))!; const map = (await ctx.db.get(world.mapId))!; - const otherPlayers = await asyncMap( - (await getAllPlayers(ctx.db, worldId)).filter((p) => p._id !== playerId), - async (p) => ({ - ...p, - motion: await getLatestPlayerMotion(ctx.db, p._id), - }), - ); const targetPosition = target - ? getPoseFromMotion(await getLatestPlayerMotion(ctx.db, target), ts).position + ? roundPose(getPoseFromMotion(await getLatestPlayerMotion(ctx.db, target), ts)).position : getRandomPosition(map); - const ourMotion = await getLatestPlayerMotion(ctx.db, playerId); - const { route, distance } = findRoute( - map, - ourMotion, - otherPlayers.map(({ motion }) => motion), - targetPosition, - ts, - ); - if (distance === 0) { - if (ourMotion.type === 'walking') { - await ctx.db.insert('journal', { - playerId, - data: { - type: 'stopped', - pose: { - position: route[0], - orientation: calculateOrientation(route[0], targetPosition), - }, - reason: 'interrupted', - }, - }); - } - return { - targetEndTs: ts + STUCK_CHILL_TIME, - // TODO: detect collisions with other players running into us. - }; - } - const exclude = new Set([...ignore, playerId]); - const targetEndTs = ts + distance * TIME_PER_STEP; - let endOrientation: number | undefined; - if (manhattanDistance(targetPosition, route[route.length - 1]) > 0) { - endOrientation = calculateOrientation(route[route.length - 1], targetPosition); - } - await ctx.db.insert('journal', { - playerId, - data: { type: 'walking', route, ignore, startTs: ts, targetEndTs, endOrientation }, - }); - const collisions = findCollision( - route, - otherPlayers.filter((p) => !exclude.has(p._id)), - ts, - CLOSE_DISTANCE, - ); - return { - targetEndTs, - nextCollision: collisions && { - ts: collisions.distance * TIME_PER_STEP + ts, - agentIds: pruneNull(collisions.hits.map(({ agentId }) => agentId)), - }, - }; + return await walkToTarget(ctx, playerId, worldId, ignore, targetPosition); }, }); -export const nextCollision = internalQuery({ - args: { agentId: v.id('agents'), ignore: v.array(v.id('players')) }, - handler: async (ctx, { agentId, ignore }) => { - const ts = Date.now(); - const agentDoc = (await ctx.db.get(agentId))!; - const { playerId, worldId } = agentDoc; - const exclude = new Set([...ignore, playerId]); - const otherPlayers = await asyncMap( - (await getAllPlayers(ctx.db, worldId)).filter((p) => !exclude.has(p._id)), - async (p) => ({ ...p, motion: await getLatestPlayerMotion(ctx.db, p._id) }), - ); - const ourMotion = await getLatestPlayerMotion(ctx.db, playerId); - const nearby = getNearbyPlayers(ourMotion, otherPlayers); - nearby.forEach(({ _id: id }) => exclude.add(id)); - const othersNotNearby = otherPlayers.filter(({ _id }) => !exclude.has(_id)); - const route = getRemainingPathFromMotion(ourMotion, ts); - const distance = getRouteDistance(route); - const targetEndTs = ts + distance * TIME_PER_STEP; - const collisions = findCollision(route, othersNotNearby, ts, CLOSE_DISTANCE); +export const walkToTarget = async ( + ctx: MutationCtx, + playerId: Id<'players'>, + worldId: Id<'worlds'>, + ignore: Id<'players'>[], + targetPosition: Position, +) => { + const ts = Date.now(); + const world = (await ctx.db.get(worldId))!; + const map = (await ctx.db.get(world.mapId))!; + const otherPlayers = await asyncMap( + (await getAllPlayers(ctx.db, worldId)).filter((p) => p._id !== playerId), + async (p) => ({ + ...p, + motion: await getLatestPlayerMotion(ctx.db, p._id), + }), + ); + const ourMotion = await getLatestPlayerMotion(ctx.db, playerId); + const { route, distance } = findRoute( + map, + ourMotion, + otherPlayers.map(({ motion }) => motion), + targetPosition, + ts, + ); + if (distance === 0) { + if (ourMotion.type === 'walking') { + await ctx.db.insert('journal', { + playerId, + data: { + type: 'stopped', + pose: { + position: route[0], + orientation: calculateOrientation(route[0], targetPosition), + }, + reason: 'interrupted', + }, + }); + } return { - targetEndTs, - nextCollision: collisions && { - ts: collisions.distance * TIME_PER_STEP + ts, - agentIds: pruneNull(collisions.hits.map(({ agentId }) => agentId)), - }, + targetEndTs: ts + STUCK_CHILL_TIME, + // TODO: detect collisions with other players running into us. }; - }, -}); + } + const exclude = new Set([...ignore, playerId]); + const targetEndTs = ts + distance * TIME_PER_STEP; + let endOrientation: number | undefined; + if (manhattanDistance(targetPosition, route[route.length - 1]) > 0) { + endOrientation = calculateOrientation(route[route.length - 1], targetPosition); + } + await ctx.db.insert('journal', { + playerId, + data: { type: 'walking', route, ignore, startTs: ts, targetEndTs, endOrientation }, + }); + const collisions = findCollision( + route, + otherPlayers.filter((p) => !exclude.has(p._id)), + ts, + CLOSE_DISTANCE, + ); + return { + targetEndTs, + nextCollision: collisions && { + ts: collisions.distance * TIME_PER_STEP + ts, + agentIds: pruneNull(collisions.hits.map(({ agentId }) => agentId)), + }, + }; +}; + +export const nextCollision = async ( + db: DatabaseReader, + worldId: Id<'worlds'>, + playerId: Id<'players'>, + ignore: Id<'players'>[], +) => { + const ts = Date.now(); + const exclude = new Set([...ignore, playerId]); + const otherPlayers = await asyncMap( + (await getAllPlayers(db, worldId)).filter((p) => !exclude.has(p._id)), + async (p) => ({ ...p, motion: await getLatestPlayerMotion(db, p._id) }), + ); + const ourMotion = await getLatestPlayerMotion(db, playerId); + const nearby = getNearbyPlayers(ourMotion, otherPlayers); + nearby.forEach(({ _id: id }) => exclude.add(id)); + const othersNotNearby = otherPlayers.filter(({ _id }) => !exclude.has(_id)); + const route = getRemainingPathFromMotion(ourMotion, ts); + const distance = getRouteDistance(route); + const targetEndTs = ts + distance * TIME_PER_STEP; + const collisions = findCollision(route, othersNotNearby, ts, CLOSE_DISTANCE); + return { + targetEndTs, + nextCollision: collisions && { + ts: collisions.distance * TIME_PER_STEP + ts, + agentIds: pruneNull(collisions.hits.map(({ agentId }) => agentId)), + }, + }; +}; export function getRandomPosition(map: Doc<'maps'>): Position { let pos;