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(),