Skip to content

Commit

Permalink
refactor walking into mutation (#23)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ldanilek authored Aug 31, 2023
1 parent 08c7850 commit 98ae881
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 120 deletions.
22 changes: 2 additions & 20 deletions convex/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
};
Expand Down
46 changes: 31 additions & 15 deletions convex/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) },
Expand Down Expand Up @@ -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),
});
},
});
Expand Down
183 changes: 98 additions & 85 deletions convex/journal.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 98ae881

Please sign in to comment.