diff --git a/convex/constants.ts b/convex/constants.ts index f3009e7..299421d 100644 --- a/convex/constants.ts +++ b/convex/constants.ts @@ -1,12 +1,12 @@ export const AI_MODELS = [ - { - model: "gpt-4o", - name: "OpenAI - 4o Mini", - }, { model: "gemini-1.5-pro", name: "Gemini - 1.5 Pro", }, + { + model: "gpt-4o", + name: "OpenAI - 4o Mini", + }, ]; export const AI_MODEL_IDS = AI_MODELS.map((model) => model.model); diff --git a/convex/gemini.ts b/convex/gemini.ts deleted file mode 100644 index a83df75..0000000 --- a/convex/gemini.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai"; -import { action, internalAction } from "./_generated/server"; -import { v } from "convex/values"; -import { api, internal } from "./_generated/api"; -import { Doc } from "./_generated/dataModel"; -import { ZombieSurvival } from "../simulators/zombie-survival"; - -const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY as string); - -const schema = { - description: "Game Round Results", - type: SchemaType.OBJECT, - properties: { - map: { - type: SchemaType.ARRAY, - items: { - type: SchemaType.ARRAY, - items: { - type: SchemaType.STRING, - }, - }, - description: "The resulting map after the placements", - }, - reasoning: { - type: SchemaType.STRING, - description: "The reasoning behind the move", - }, - playerCoordinates: { - type: SchemaType.ARRAY, - items: { - type: SchemaType.NUMBER, - }, - description: "The player's coordinates", - }, - boxCoordinates: { - type: SchemaType.ARRAY, - items: { - type: SchemaType.ARRAY, - items: { - type: SchemaType.NUMBER, - }, - }, - description: "The box coordinates", - }, - }, - required: ["map", "reasoning", "playerCoordinates", "boxCoordinates"], -}; - -const model = genAI.getGenerativeModel({ - model: "gemini-1.5-pro", - generationConfig: { - responseMimeType: "application/json", - responseSchema: schema, - }, -}); - -type playMapActionResponse = { - map: string[][]; - reasoning: string; - playerCoordinates: number[]; - boxCoordinates: number[][]; -}; - -export const playMapAction = internalAction({ - args: { - level: v.number(), - gameId: v.id("games"), - modelId: v.string(), - }, - handler: async (ctx, args) => { - const resultId = await ctx.runMutation( - internal.results.createInitialResult, - { - gameId: args.gameId, - level: args.level, - }, - ); - - const map: Doc<"maps"> | null = (await ctx.runQuery( - api.maps.getMapByLevel, - { - level: args.level, - }, - )) as any; - - if (!map) { - throw new Error("Map not found"); - } - - if (process.env.MOCK_GEMINI === "true") { - const existingMap = [...map.grid.map((row) => [...row])]; - existingMap[0][0] = "P"; - existingMap[0][1] = "B"; - existingMap[0][2] = "B"; - return { - map: existingMap, - reasoning: "This is a mock response", - playerCoordinates: [0, 0], - boxCoordinates: [], - }; - } - - const result = await model.generateContent( - `You're given a 2d grid of nums such that. - " " represents an empty space. - "Z" represents a zombie. Zombies move one Manhattan step every turn and aim to reach the player. - "R" represents rocks, which players can shoot over but zombies cannot pass through or break. - "P" represents the player, who cannot move. The player's goal is to shoot and kill zombies before they reach them. - "B" represents blocks that can be placed before the round begins to hinder the zombies. You can place up to two blocks on the map. - - Your goal is to place the player ("P") and two blocks ("B") in locations that maximize the player's survival by delaying the zombies' approach. - You can shoot any zombie regardless of where it is on the grid. - Returning a 2d grid with the player and blocks placed in the optimal locations, with the coordinates player ("P") and the blocks ("B"), also provide reasoning for the choices. - - You can't replace rocks R or zombies Z with blocks. If there is no room to place a block, do not place any. - - Grid: ${JSON.stringify(map)}`, - ); - - // todo: check if the response is valid acc to types and the player and box coordinates are valid, - // as sometimes the model returns a state that's erroring out in the simulator - - const parsedResponse = JSON.parse( - result.response.text(), - ) as playMapActionResponse; - - const game = new ZombieSurvival(parsedResponse.map); - while (!game.finished()) { - game.step(); - } - const isWin = !game.getPlayer().dead(); - - await ctx.runMutation(internal.results.updateResult, { - resultId, - isWin, - reasoning: parsedResponse.reasoning, - map: parsedResponse.map, - }); - }, -}); diff --git a/models/gemini-1.5-pro.ts b/models/gemini-1.5-pro.ts new file mode 100644 index 0000000..7cc9fcc --- /dev/null +++ b/models/gemini-1.5-pro.ts @@ -0,0 +1,87 @@ +import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai"; +import { type ModelResult } from "."; + +const schema = { + description: "Game Round Results", + type: SchemaType.OBJECT, + properties: { + map: { + type: SchemaType.ARRAY, + items: { + type: SchemaType.ARRAY, + items: { + type: SchemaType.STRING, + }, + }, + description: "The resulting map after the placements", + }, + reasoning: { + type: SchemaType.STRING, + description: "The reasoning behind the move", + }, + playerCoordinates: { + type: SchemaType.ARRAY, + items: { + type: SchemaType.NUMBER, + }, + description: "The player's coordinates", + }, + boxCoordinates: { + type: SchemaType.ARRAY, + items: { + type: SchemaType.ARRAY, + items: { + type: SchemaType.NUMBER, + }, + }, + description: "The box coordinates", + }, + }, + required: ["map", "reasoning", "playerCoordinates", "boxCoordinates"], +}; + +interface GeminiResponse { + map: string[][]; + reasoning: string; + playerCoordinates: number[]; + boxCoordinates: number[][]; +} + +export async function gemini15pro(map: string[][]): Promise { + const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); + + const model = genAI.getGenerativeModel({ + model: "gemini-1.5-pro", + generationConfig: { + responseMimeType: "application/json", + responseSchema: schema, + }, + }); + + const result = await model.generateContent( + `You're given a 2d grid of nums such that. + " " represents an empty space. + "Z" represents a zombie. Zombies move one Manhattan step every turn and aim to reach the player. + "R" represents rocks, which players can shoot over but zombies cannot pass through or break. + "P" represents the player, who cannot move. The player's goal is to shoot and kill zombies before they reach them. + "B" represents blocks that can be placed before the round begins to hinder the zombies. You can place up to two blocks on the map. + + Your goal is to place the player ("P") and two blocks ("B") in locations that maximize the player's survival by delaying the zombies' approach. + You can shoot any zombie regardless of where it is on the grid. + Returning a 2d grid with the player and blocks placed in the optimal locations, with the coordinates player ("P") and the blocks ("B"), also provide reasoning for the choices. + + You can't replace rocks R or zombies Z with blocks. If there is no room to place a block, do not place any. + + Grid: ${JSON.stringify(map)}`, + ); + + // todo: check if the response is valid acc to types and the player and box coordinates are valid, + // as sometimes the model returns a state that's erroring out in the simulator + + const parsedResponse = JSON.parse(result.response.text()) as GeminiResponse; + + return { + solution: parsedResponse.map, + reasoning: parsedResponse.reasoning, + }; +} diff --git a/models/index.ts b/models/index.ts index 0393670..0b994f7 100644 --- a/models/index.ts +++ b/models/index.ts @@ -1,3 +1,4 @@ +import { gemini15pro } from "./gemini-1.5-pro"; import { gpt4o } from "./gpt-4o"; export interface ModelResult { @@ -10,6 +11,9 @@ export async function runModel( map: string[][], ): Promise { switch (modelId) { + case "gemini-1.5-pro": { + return gemini15pro(map); + } case "gpt-4o": { return gpt4o(map); }