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/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 85ae729..93852f5 100644 --- a/convex/init.ts +++ b/convex/init.ts @@ -1,10 +1,9 @@ +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); }, }); diff --git a/convex/maps.ts b/convex/maps.ts index 40a4465..ff2f9ad 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, + }), + ), + ); }, }); @@ -139,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"); diff --git a/convex/models.ts b/convex/models.ts new file mode 100644 index 0000000..1f58997 --- /dev/null +++ b/convex/models.ts @@ -0,0 +1,49 @@ +import { AI_MODELS } from "./constants"; +import { api } from "./_generated/api"; +import { internalMutation, query } from "./_generated/server"; + +export const runActiveModelsGames = 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 }), + ), + ); + }, +}); + +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/results.ts b/convex/results.ts index c66db40..a82f516 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); @@ -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, + }); }, }); diff --git a/convex/schema.ts b/convex/schema.ts index 7f950f1..04eaf23 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(), @@ -31,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(),