From f3266eac5d8c06dfae8b5de5e70dfd1482f35d0c Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 23:30:30 +0300 Subject: [PATCH 1/6] Create models table --- app/page.tsx | 29 ++++++++++++++-------- convex/_generated/api.d.ts | 2 ++ convex/init.ts | 8 +++--- convex/models.ts | 51 ++++++++++++++++++++++++++++++++++++++ convex/schema.ts | 5 ++++ 5 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 convex/models.ts diff --git a/app/page.tsx b/app/page.tsx index 1de3032..6a3f149 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,8 +1,8 @@ "use client"; -import { api } from "@/convex/_generated/api"; -import { useAction, useMutation } from "convex/react"; -import React, { useState } from "react"; +import React from "react"; +import { useQuery, useMutation } from "convex/react"; +import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Select, @@ -11,14 +11,20 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { AI_MODELS } from "@/convex/constants"; -import { useRouter } from "next/navigation"; +import { api } from "@/convex/_generated/api"; export default function MainPage() { + const models = useQuery(api.models.getActiveModels); const startNewGame = useMutation(api.games.startNewGame); - const [model, setModel] = useState(AI_MODELS[0].model); + const [model, setModel] = React.useState(""); const router = useRouter(); + React.useEffect(() => { + if (models !== undefined && models.length !== 0) { + setModel(models[0].slug); + } + }, [models]); + const handleClick = async () => { await startNewGame({ modelId: model, @@ -37,11 +43,12 @@ export default function MainPage() { - {AI_MODELS.map((model) => ( - - {model.name} - - ))} + {models !== undefined && + models.map((model) => ( + + {model.name} + + ))} diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 7918494..29ab2df 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -22,6 +22,7 @@ import type * as http from "../http.js"; import type * as init from "../init.js"; import type * as leaderboard from "../leaderboard.js"; import type * as maps from "../maps.js"; +import type * as models from "../models.js"; import type * as results from "../results.js"; import type * as scores from "../scores.js"; import type * as users from "../users.js"; @@ -42,6 +43,7 @@ declare const fullApi: ApiFromModules<{ init: typeof init; leaderboard: typeof leaderboard; maps: typeof maps; + models: typeof models; results: typeof results; scores: typeof scores; users: typeof users; diff --git a/convex/init.ts b/convex/init.ts index 85ae729..658b8e7 100644 --- a/convex/init.ts +++ b/convex/init.ts @@ -1,10 +1,10 @@ +import { internal } from "./_generated/api"; import { internalMutation } from "./_generated/server"; -import { seedMaps } from "./maps"; export default internalMutation({ handler: async (ctx) => { - const maps = await ctx.db.query("maps").first(); - if (maps) return; - await seedMaps(ctx, {}); + await ctx.runMutation(internal.maps.seedMaps); + await ctx.runMutation(internal.models.seedModels); + await ctx.runMutation(internal.models.scheduleModelsGames); }, }); diff --git a/convex/models.ts b/convex/models.ts new file mode 100644 index 0000000..57c6933 --- /dev/null +++ b/convex/models.ts @@ -0,0 +1,51 @@ +import { AI_MODELS } from "./constants"; +import { api, internal } from "./_generated/api"; +import { internalMutation, query } from "./_generated/server"; + +export const scheduleModelsGames = internalMutation({ + handler: async (ctx) => { + const models = await ctx.runQuery(api.models.getActiveModels); + + await Promise.all( + models.map((model) => + ctx.runMutation(api.games.startNewGame, { modelId: model.slug }), + ), + ); + + await ctx.scheduler.runAfter(300_000, internal.models.scheduleModelsGames); + }, +}); + +export const seedModels = internalMutation({ + handler: async (ctx) => { + const models = await ctx.db.query("models").collect(); + const promises = []; + + for (const model of AI_MODELS) { + const existingModel = models.find((it) => it.slug === model.model); + + if (existingModel !== undefined) { + continue; + } + + promises.push( + ctx.db.insert("models", { + slug: model.model, + name: model.name, + active: true, + }), + ); + } + + await Promise.all(promises); + }, +}); + +export const getActiveModels = query({ + handler: async (ctx) => { + return await ctx.db + .query("models") + .withIndex("by_active", (q) => q.eq("active", true)) + .collect(); + }, +}); diff --git a/convex/schema.ts b/convex/schema.ts index 7f950f1..45f7ee9 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -19,6 +19,11 @@ export default defineSchema({ modelId: v.string(), score: v.number(), }).index("by_modelId", ["modelId"]), + models: defineTable({ + slug: v.string(), + active: v.boolean(), + name: v.string(), + }).index("by_active", ["active"]), results: defineTable({ gameId: v.id("games"), level: v.number(), From 20712055d82ec1f43b23f76e0409aca8f9eab155 Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 23:30:55 +0300 Subject: [PATCH 2/6] Fix map seeder --- convex/maps.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/convex/maps.ts b/convex/maps.ts index 40a4465..da55c23 100644 --- a/convex/maps.ts +++ b/convex/maps.ts @@ -91,19 +91,20 @@ const LEVELS = [ export const seedMaps = internalMutation({ handler: async (ctx) => { - // delete all existing maps - const maps = await ctx.db.query("maps").collect(); + const firstMap = await ctx.db.query("maps").first(); - for (const map of maps) { - await ctx.db.delete(map._id); + if (firstMap) { + return; } - LEVELS.forEach((map, idx) => { - ctx.db.insert("maps", { - level: idx + 1, - grid: map.grid, - }); - }); + await Promise.all( + LEVELS.map((map, idx) => + ctx.db.insert("maps", { + level: idx + 1, + grid: map.grid, + }), + ), + ); }, }); From b4484b0202f97c1af95171bf8d0a48a537e7efca Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 23:31:35 +0300 Subject: [PATCH 3/6] Use index when filtering results --- convex/results.ts | 2 +- convex/schema.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/convex/results.ts b/convex/results.ts index c66db40..03abeeb 100644 --- a/convex/results.ts +++ b/convex/results.ts @@ -22,7 +22,7 @@ export const getLastCompletedResults = query({ handler: async ({ db }) => { const results = await db .query("results") - .filter((q) => q.eq(q.field("status"), "completed")) + .withIndex("by_status", (q) => q.eq("status", "completed")) .order("desc") .take(20); diff --git a/convex/schema.ts b/convex/schema.ts index 45f7ee9..04eaf23 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -36,7 +36,9 @@ export default defineSchema({ v.literal("completed"), v.literal("failed"), ), - }).index("by_gameId_level", ["gameId", "level"]), + }) + .index("by_gameId_level", ["gameId", "level"]) + .index("by_status", ["status"]), globalrankings: defineTable({ modelId: v.string(), wins: v.number(), From 88929ab5e538d8bfb07479224cd393a9b8f7af57 Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 23:32:04 +0300 Subject: [PATCH 4/6] Run next results even on failure --- convex/results.ts | 60 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/convex/results.ts b/convex/results.ts index 03abeeb..a82f516 100644 --- a/convex/results.ts +++ b/convex/results.ts @@ -68,6 +68,10 @@ export const failResult = internalMutation({ error: args.error, status: "failed", }); + + await ctx.runMutation(internal.results.scheduleNextPlay, { + resultId: args.resultId, + }); }, }); @@ -94,10 +98,6 @@ export const updateResult = internalMutation({ const game = await ctx.db.get(result.gameId); - const maps = await ctx.db.query("maps").collect(); - - const lastLevel = maps.reduce((max, map) => Math.max(max, map.level), 0); - if (!game) { throw new Error("Game not found"); } @@ -114,20 +114,48 @@ export const updateResult = internalMutation({ }); } - if (result.level < lastLevel) { - const map = await ctx.runQuery(api.maps.getMapByLevel, { - level: result.level + 1, - }); + await ctx.runMutation(internal.results.scheduleNextPlay, { + resultId: args.resultId, + }); + }, +}); - if (!map) { - throw new Error("Next map not found"); - } +export const scheduleNextPlay = internalMutation({ + args: { + resultId: v.id("results"), + }, + handler: async (ctx, args) => { + const result = await ctx.db.get(args.resultId); - await ctx.scheduler.runAfter(0, internal.maps.playMapAction, { - gameId: result.gameId, - modelId: game.modelId, - level: result.level + 1, - }); + if (!result) { + throw new Error("Result not found"); } + + const maps = await ctx.db.query("maps").collect(); + const lastLevel = maps.reduce((max, map) => Math.max(max, map.level), 0); + + if (result.level >= lastLevel) { + return; + } + + const map = await ctx.runQuery(api.maps.getMapByLevel, { + level: result.level + 1, + }); + + const game = await ctx.db.get(result.gameId); + + if (!game) { + throw new Error("Game not found"); + } + + if (!map) { + throw new Error("Next map not found"); + } + + await ctx.scheduler.runAfter(0, internal.maps.playMapAction, { + gameId: result.gameId, + modelId: game.modelId, + level: result.level + 1, + }); }, }); From 00916fdb3a7b52e795b4825313eb584399f4bbda Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 23:35:38 +0300 Subject: [PATCH 5/6] Don't use `any` in unnecessary place --- convex/maps.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/convex/maps.ts b/convex/maps.ts index da55c23..ff2f9ad 100644 --- a/convex/maps.ts +++ b/convex/maps.ts @@ -140,12 +140,9 @@ export const playMapAction = internalAction({ }, ); - const map: Doc<"maps"> | null = (await ctx.runQuery( - api.maps.getMapByLevel, - { - level: args.level, - }, - )) as any; + const map = await ctx.runQuery(api.maps.getMapByLevel, { + level: args.level, + }); if (!map) { throw new Error("Map not found"); From 7176232559ffdef0cbb98730ad971f7067bdccb8 Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 23:53:22 +0300 Subject: [PATCH 6/6] Use cron.ts instead of manual scheduling --- convex/cron.ts | 10 ++++++++++ convex/init.ts | 1 - convex/models.ts | 6 ++---- 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 convex/cron.ts diff --git a/convex/cron.ts b/convex/cron.ts new file mode 100644 index 0000000..1999d95 --- /dev/null +++ b/convex/cron.ts @@ -0,0 +1,10 @@ +import { cronJobs } from "convex/server"; +import { internal } from "./_generated/api"; + +const crons = cronJobs(); + +crons.interval( + "run games for all active models", + { minutes: 5 }, + internal.models.runActiveModelsGames, +); diff --git a/convex/init.ts b/convex/init.ts index 658b8e7..93852f5 100644 --- a/convex/init.ts +++ b/convex/init.ts @@ -5,6 +5,5 @@ export default internalMutation({ handler: async (ctx) => { await ctx.runMutation(internal.maps.seedMaps); await ctx.runMutation(internal.models.seedModels); - await ctx.runMutation(internal.models.scheduleModelsGames); }, }); diff --git a/convex/models.ts b/convex/models.ts index 57c6933..1f58997 100644 --- a/convex/models.ts +++ b/convex/models.ts @@ -1,8 +1,8 @@ import { AI_MODELS } from "./constants"; -import { api, internal } from "./_generated/api"; +import { api } from "./_generated/api"; import { internalMutation, query } from "./_generated/server"; -export const scheduleModelsGames = internalMutation({ +export const runActiveModelsGames = internalMutation({ handler: async (ctx) => { const models = await ctx.runQuery(api.models.getActiveModels); @@ -11,8 +11,6 @@ export const scheduleModelsGames = internalMutation({ ctx.runMutation(api.games.startNewGame, { modelId: model.slug }), ), ); - - await ctx.scheduler.runAfter(300_000, internal.models.scheduleModelsGames); }, });