From e904b959b1f9c342726cacb352bdde19b46422f5 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Wed, 16 Oct 2024 12:58:59 -0400 Subject: [PATCH 1/6] adding the ability to place blocks on manual play; improve prompting a bit, remove width / height from map (it can be computed via the array), working on simplier maps that get progressively harder. --- app/games/[gameId]/page.tsx | 4 +- app/games/[gameId]/result.tsx | 10 ++- app/play/[level]/page.tsx | 59 ++++++++++--- convex/maps.ts | 150 +++++++++++++++++++--------------- convex/schema.ts | 2 - models/gpt-4o.ts | 32 ++++++-- 6 files changed, 169 insertions(+), 88 deletions(-) diff --git a/app/games/[gameId]/page.tsx b/app/games/[gameId]/page.tsx index 0ffb499..08d341b 100644 --- a/app/games/[gameId]/page.tsx +++ b/app/games/[gameId]/page.tsx @@ -25,7 +25,9 @@ export default function GamePage({ params }: { params: { gameId: string } }) {

Game starting...

) : ( - results.map((result) => ) + results.map((result) => ( + + )) )} diff --git a/app/games/[gameId]/result.tsx b/app/games/[gameId]/result.tsx index ff39de0..a971b5c 100644 --- a/app/games/[gameId]/result.tsx +++ b/app/games/[gameId]/result.tsx @@ -4,6 +4,7 @@ import { Doc } from "@/convex/_generated/dataModel"; import { api } from "@/convex/_generated/api"; import { useQuery } from "convex/react"; import { Visualizer } from "./visualizer"; +import Link from "next/link"; export const Result = ({ result }: { result: Doc<"results"> }) => { const map = useQuery(api.maps.getMapByLevel, { @@ -24,8 +25,13 @@ export const Result = ({ result }: { result: Doc<"results"> }) => { return (
-
Level {map.level}
- + + Level {map.level} + +
([]); const [isSimulating, setIsSimulating] = useState(false); const [gameResult, setGameResult] = useState<"WON" | "LOST" | null>(null); + const [placementMode, setPlacementMode] = useState<"player" | "block">( + "player", + ); + const [blockCount, setBlockCount] = useState(0); if (!map) { return
Loading...
; @@ -27,6 +31,7 @@ export default function PlayLevelPage({ function handleRetryClicked() { setIsSimulating(false); setGameResult(null); + setBlockCount(0); if (map) { setPlayerMap(map.grid); } @@ -38,23 +43,35 @@ export default function PlayLevelPage({ const newMap = playerMap.length > 0 ? [...playerMap] : map.grid.map((row) => [...row]); - // Remove existing player if any - for (let i = 0; i < newMap.length; i++) { - for (let j = 0; j < newMap[i].length; j++) { - if (newMap[i][j] === "P") { - newMap[i][j] = " "; + if (placementMode === "player") { + // Remove existing player if any + for (let i = 0; i < newMap.length; i++) { + for (let j = 0; j < newMap[i].length; j++) { + if (newMap[i][j] === "P") { + newMap[i][j] = " "; + } } } - } - // Place new player - if (newMap[y][x] === " ") { - newMap[y][x] = "P"; + // Place new player + if (newMap[y][x] === " ") { + newMap[y][x] = "P"; + } + } else if (placementMode === "block" && blockCount < 2) { + // Place new block + if (newMap[y][x] === " ") { + newMap[y][x] = "B"; + setBlockCount(blockCount + 1); + } } setPlayerMap(newMap); }; + const handlePlacementModeChange = (mode: "player" | "block") => { + setPlacementMode(mode); + }; + const runSimulation = () => { if (!playerMap.some((row) => row.includes("P"))) { alert( @@ -70,6 +87,10 @@ export default function PlayLevelPage({ setGameResult(isWin ? "WON" : "LOST"); }; + const mapWidth = + playerMap.length > 0 ? playerMap[0].length : map.grid[0].length; + const mapHeight = playerMap.length > 0 ? playerMap.length : map.grid.length; + return (
@@ -81,6 +102,22 @@ export default function PlayLevelPage({

Level {level}

{/* Spacer for alignment */}
+
+ + +

{isSimulating ? "Simulation Result" : "Place Your Player"} @@ -106,8 +143,8 @@ export default function PlayLevelPage({
{(playerMap.length > 0 ? playerMap : map.grid).map((row, y) => diff --git a/convex/maps.ts b/convex/maps.ts index d9d2b4c..acaf803 100644 --- a/convex/maps.ts +++ b/convex/maps.ts @@ -1,90 +1,108 @@ -import OpenAI from "openai"; import { internalAction, internalMutation, query } from "./_generated/server"; import { v } from "convex/values"; -import { z } from "zod"; -import { zodResponseFormat } from "openai/helpers/zod"; import { Doc } from "./_generated/dataModel"; import { ZombieSurvival } from "../simulators/zombie-survival"; import { api, internal } from "./_generated/api"; import { runModel } from "../models"; -const MAPS = [ +const LEVELS = [ { - level: 1, grid: [ - ["Z", " ", " ", "R", " "], - [" ", "R", " ", " ", " "], - [" ", " ", " ", " ", " "], - [" ", " ", " ", " ", "Z"], - [" ", " ", " ", " ", " "], + ["Z", " "], + [" ", " "], ], - width: 5, - height: 5, }, { - level: 2, - grid: [ - [" ", " ", "R", " ", "Z"], - [" ", " ", " ", " ", " "], - [" ", " ", " ", "R", " "], - ["Z", " ", " ", " ", " "], - [" ", " ", " ", " ", " "], - ], - width: 5, - height: 5, - }, - { - level: 3, - grid: [ - ["Z", " ", " ", "R", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", " ", "Z", " "], - [" ", " ", " ", " ", " ", " ", " "], - [" ", "R", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", "R", " ", " "], - ], - width: 7, - height: 7, - }, - { - level: 4, - grid: [ - [" ", "Z", " ", " ", "R", " ", " "], - [" ", " ", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", "Z"], - [" ", " ", "R", " ", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", " "], - ], - width: 7, - height: 7, + grid: [["Z", " ", " ", " "]], }, { - level: 5, grid: [ - [" ", " ", " ", " ", "R", "Z", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], - ["Z", " ", " ", " ", " ", "R", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", "R", " ", " ", " ", " ", "Z", " "], - [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], - [" ", " ", "R", " ", " ", " ", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", "Z", " ", " ", " ", " "], - ["B", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], - [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], - ["Z", " ", " ", " ", "Z", " ", " ", " ", " ", " ", "Z"], + ["Z", " "], + [" ", " "], + [" ", " "], + ["Z", " "], ], - width: 11, - height: 11, }, + // { + // grid: [ + // ["Z", " ", " ", "R", " "], + // [" ", "R", " ", " ", " "], + // [" ", " ", " ", " ", " "], + // [" ", " ", " ", " ", "Z"], + // [" ", " ", " ", " ", " "], + // ], + // }, + // { + // grid: [ + // ["Z", " ", " ", "R", " "], + // [" ", "R", " ", " ", " "], + // [" ", " ", " ", " ", " "], + // [" ", " ", " ", " ", "Z"], + // [" ", " ", " ", " ", " "], + // ], + // }, + // { + // grid: [ + // [" ", " ", "R", " ", "Z"], + // [" ", " ", " ", " ", " "], + // [" ", " ", " ", "R", " "], + // ["Z", " ", " ", " ", " "], + // [" ", " ", " ", " ", " "], + // ], + // }, + // { + // grid: [ + // ["Z", " ", " ", "R", " ", " ", " "], + // [" ", " ", " ", " ", " ", " ", " "], + // [" ", " ", " ", " ", " ", " ", " "], + // [" ", " ", " ", " ", " ", "Z", " "], + // [" ", " ", " ", " ", " ", " ", " "], + // [" ", "R", " ", " ", " ", " ", " "], + // [" ", " ", " ", " ", "R", " ", " "], + // ], + // }, + // { + // grid: [ + // [" ", "Z", " ", " ", "R", " ", " "], + // [" ", " ", " ", " ", " ", " ", " "], + // [" ", " ", " ", " ", " ", " ", " "], + // [" ", " ", " ", " ", " ", " ", "Z"], + // [" ", " ", "R", " ", " ", " ", " "], + // [" ", " ", " ", " ", " ", " ", " "], + // [" ", " ", " ", " ", " ", " ", " "], + // ], + // }, + // { + // grid: [ + // [" ", " ", " ", " ", "R", "Z", " ", " ", " ", " ", " "], + // [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + // ["Z", " ", " ", " ", " ", "R", " ", " ", " ", " ", " "], + // [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + // [" ", " ", " ", " ", "R", " ", " ", " ", " ", "Z", " "], + // [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + // [" ", " ", "R", " ", " ", " ", " ", " ", " ", " ", " "], + // [" ", " ", " ", " ", " ", " ", "Z", " ", " ", " ", " "], + // ["B", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + // [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "], + // ["Z", " ", " ", " ", "Z", " ", " ", " ", " ", " ", "Z"], + // ], + // }, ]; export const seedMaps = internalMutation({ handler: async (ctx) => { - for (const map of MAPS) { - ctx.db.insert("maps", map); + // delete all existing maps + const maps = await ctx.db.query("maps").collect(); + + for (const map of maps) { + await ctx.db.delete(map._id); + } + + for (const [idx, map] of LEVELS.entries()) { + ctx.db.insert("maps", { + level: idx + 1, + grid: map.grid, + }); } }, }); diff --git a/convex/schema.ts b/convex/schema.ts index 4083f79..2741e80 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -14,8 +14,6 @@ export default defineSchema({ maps: defineTable({ level: v.number(), grid: v.array(v.array(v.string())), - width: v.number(), - height: v.number(), }).index("by_level", ["level"]), scores: defineTable({ modelId: v.string(), diff --git a/models/gpt-4o.ts b/models/gpt-4o.ts index a2f53e2..498c8eb 100644 --- a/models/gpt-4o.ts +++ b/models/gpt-4o.ts @@ -4,7 +4,6 @@ import { zodResponseFormat } from "openai/helpers/zod"; import { type ModelResult } from "."; const ResponseSchema = z.object({ - map: z.array(z.array(z.string())), reasoning: z.string(), playerCoordinates: z.array(z.number()), boxCoordinates: z.array(z.array(z.number())), @@ -18,13 +17,16 @@ export async function gpt4o(map: string[][]): Promise { "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. + "B" represents blocks that can be placed before the round begins to hinder the zombies. - 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. + Your goal is to place the player ("P") in a location which maximize the player's survival. + You must place two blocks ("B") in locations which maximize the player's survival. 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.`; + Zombies can only move horizontally or vertically, not diagonally. + You can't replace rocks R or zombies Z with blocks. + Players will always shoot at the closest zombie each turn. + If there is no room to place a block, do not place any.`; const completion = await openai.beta.chat.completions.parse({ model: "gpt-4o-2024-08-06", @@ -38,6 +40,7 @@ export async function gpt4o(map: string[][]): Promise { content: JSON.stringify(map), }, ], + temperature: 0.5, response_format: zodResponseFormat(ResponseSchema, "game_map"), }); @@ -49,8 +52,25 @@ export async function gpt4o(map: string[][]): Promise { throw new Error("Failed to run model GPT-4o"); } + const originalMap = JSON.parse(JSON.stringify(map)); + + const [playerRow, playerCol] = response.parsed.playerCoordinates; + if (originalMap[playerRow][playerCol] !== " ") { + throw new Error("Cannot place player in a non-empty space"); + } + originalMap[playerRow][playerCol] = "P"; + + for (const block of response.parsed.boxCoordinates) { + const [blockRow, blockCol] = block; + if (originalMap[blockRow][blockCol] !== " ") { + throw new Error("Cannot place block in a non-empty space"); + } + + originalMap[blockRow][blockCol] = "B"; + } + return { - solution: response.parsed.map, + solution: originalMap, reasoning: response.parsed.reasoning, }; } From 2f63e2552ca502dfdbd4cb766cd5d317dbba1e9a Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 20:35:18 +0300 Subject: [PATCH 2/6] Implement watch page --- app/games/[gameId]/page.tsx | 4 +- app/games/[gameId]/result.tsx | 9 ++-- app/header.tsx | 2 +- app/play/[level]/page.tsx | 2 +- app/result-status.tsx | 11 +++++ app/{games/[gameId] => }/visualizer.tsx | 62 +++++++++++++++++-------- app/watch/page.tsx | 21 +++++++++ app/watch/result.tsx | 19 ++++++++ convex/results.ts | 21 +++++++++ 9 files changed, 121 insertions(+), 30 deletions(-) create mode 100644 app/result-status.tsx rename app/{games/[gameId] => }/visualizer.tsx (65%) create mode 100644 app/watch/page.tsx create mode 100644 app/watch/result.tsx diff --git a/app/games/[gameId]/page.tsx b/app/games/[gameId]/page.tsx index 08d341b..0ffb499 100644 --- a/app/games/[gameId]/page.tsx +++ b/app/games/[gameId]/page.tsx @@ -25,9 +25,7 @@ export default function GamePage({ params }: { params: { gameId: string } }) {

Game starting...

) : ( - results.map((result) => ( - - )) + results.map((result) => ) )}

diff --git a/app/games/[gameId]/result.tsx b/app/games/[gameId]/result.tsx index a971b5c..8dbabc0 100644 --- a/app/games/[gameId]/result.tsx +++ b/app/games/[gameId]/result.tsx @@ -3,7 +3,8 @@ import { Doc } from "@/convex/_generated/dataModel"; import { api } from "@/convex/_generated/api"; import { useQuery } from "convex/react"; -import { Visualizer } from "./visualizer"; +import { ResultStatus } from "@/app/result-status"; +import { Visualizer } from "../../visualizer"; import Link from "next/link"; export const Result = ({ result }: { result: Doc<"results"> }) => { @@ -33,11 +34,7 @@ export const Result = ({ result }: { result: Doc<"results"> }) => {
-
- {result.isWin ? "Won" : "Lost"} -
+ {result.reasoning !== "" &&

{result.reasoning}

}
diff --git a/app/header.tsx b/app/header.tsx index 876038e..8bbb8d3 100644 --- a/app/header.tsx +++ b/app/header.tsx @@ -33,7 +33,7 @@ export default function Header() {
))} -
- - -
+ {controls && ( +
+ + +
+ )}
); } diff --git a/app/watch/page.tsx b/app/watch/page.tsx new file mode 100644 index 0000000..ca98e37 --- /dev/null +++ b/app/watch/page.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { useQuery } from "convex/react"; +import Result from "./result"; +import { api } from "@/convex/_generated/api"; + +export default function GamePage() { + const results = useQuery(api.results.getLastCompletedResults); + + if (results === undefined) { + return

Loading...

; + } + + return ( +
+ {results.map((result) => ( + + ))} +
+ ); +} diff --git a/app/watch/result.tsx b/app/watch/result.tsx new file mode 100644 index 0000000..df6d6ca --- /dev/null +++ b/app/watch/result.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { ResultStatus } from "../result-status"; +import { type ResultWithGame } from "@/convex/results"; +import { Visualizer } from "../visualizer"; + +export default function Result({ result }: { result: ResultWithGame }) { + return ( +
+ {result.game !== null && ( +
+ +

{result.game.modelId}

+
+ )} + +
+ ); +} diff --git a/convex/results.ts b/convex/results.ts index 630dedc..d9eb5cf 100644 --- a/convex/results.ts +++ b/convex/results.ts @@ -2,6 +2,10 @@ import { v } from "convex/values"; import { internalMutation, query } from "./_generated/server"; import { api, internal } from "./_generated/api"; +export type ResultWithGame = Awaited< + ReturnType +>[number]; + export const getResults = query({ args: { gameId: v.id("games") }, handler: async ({ db }, args) => { @@ -14,6 +18,23 @@ export const getResults = query({ }, }); +export const getLastCompletedResults = query({ + handler: async ({ db }) => { + const results = await db + .query("results") + .filter((q) => q.eq(q.field("status"), "completed")) + .order("desc") + .take(20); + + return Promise.all( + results.map(async (result) => ({ + ...result, + game: await db.get(result.gameId), + })), + ); + }, +}); + export const createInitialResult = internalMutation({ args: { gameId: v.id("games"), From 5fbae251232a0480587d18e8f1bdeaba289a2f27 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Wed, 16 Oct 2024 14:04:16 -0400 Subject: [PATCH 3/6] remove auto play --- app/games/[gameId]/page.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/games/[gameId]/page.tsx b/app/games/[gameId]/page.tsx index 08d341b..0ffb499 100644 --- a/app/games/[gameId]/page.tsx +++ b/app/games/[gameId]/page.tsx @@ -25,9 +25,7 @@ export default function GamePage({ params }: { params: { gameId: string } }) {

Game starting...

) : ( - results.map((result) => ( - - )) + results.map((result) => ) )} From 08f1f5897ede9260a53f2adbe1c97e4f64a72fc8 Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 21:14:05 +0300 Subject: [PATCH 4/6] Move prompt/checks to models/index.ts --- models/gemini-1.5-pro.ts | 28 +++++------------- models/gpt-4o.ts | 42 ++++----------------------- models/index.ts | 63 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 70 insertions(+), 63 deletions(-) diff --git a/models/gemini-1.5-pro.ts b/models/gemini-1.5-pro.ts index 7cc9fcc..0d6bba1 100644 --- a/models/gemini-1.5-pro.ts +++ b/models/gemini-1.5-pro.ts @@ -1,5 +1,5 @@ import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai"; -import { type ModelResult } from "."; +import { type ModelHandler } from "."; const schema = { description: "Game Round Results", @@ -41,13 +41,13 @@ const schema = { }; interface GeminiResponse { + boxCoordinates: number[][]; map: string[][]; - reasoning: string; playerCoordinates: number[]; - boxCoordinates: number[][]; + reasoning: string; } -export async function gemini15pro(map: string[][]): Promise { +export const gemini15pro: ModelHandler = async (prompt, map) => { const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); const model = genAI.getGenerativeModel({ @@ -59,20 +59,7 @@ export async function gemini15pro(map: string[][]): Promise { }); 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)}`, + `${prompt}\n\nGrid: ${JSON.stringify(map)}`, ); // todo: check if the response is valid acc to types and the player and box coordinates are valid, @@ -81,7 +68,8 @@ export async function gemini15pro(map: string[][]): Promise { const parsedResponse = JSON.parse(result.response.text()) as GeminiResponse; return { - solution: parsedResponse.map, + boxCoordinates: parsedResponse.boxCoordinates, + playerCoordinates: parsedResponse.playerCoordinates, reasoning: parsedResponse.reasoning, }; -} +}; diff --git a/models/gpt-4o.ts b/models/gpt-4o.ts index 498c8eb..632ae5b 100644 --- a/models/gpt-4o.ts +++ b/models/gpt-4o.ts @@ -1,7 +1,7 @@ import OpenAI from "openai"; import { z } from "zod"; import { zodResponseFormat } from "openai/helpers/zod"; -import { type ModelResult } from "."; +import { type ModelHandler } from "."; const ResponseSchema = z.object({ reasoning: z.string(), @@ -9,25 +9,9 @@ const ResponseSchema = z.object({ boxCoordinates: z.array(z.array(z.number())), }); -export async function gpt4o(map: string[][]): Promise { +export const gpt4o: ModelHandler = async (prompt, map) => { const openai = new OpenAI(); - const prompt = `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. - - Your goal is to place the player ("P") in a location which maximize the player's survival. - You must place two blocks ("B") in locations which maximize the player's survival. - 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. - Zombies can only move horizontally or vertically, not diagonally. - You can't replace rocks R or zombies Z with blocks. - Players will always shoot at the closest zombie each turn. - If there is no room to place a block, do not place any.`; - const completion = await openai.beta.chat.completions.parse({ model: "gpt-4o-2024-08-06", messages: [ @@ -52,25 +36,9 @@ export async function gpt4o(map: string[][]): Promise { throw new Error("Failed to run model GPT-4o"); } - const originalMap = JSON.parse(JSON.stringify(map)); - - const [playerRow, playerCol] = response.parsed.playerCoordinates; - if (originalMap[playerRow][playerCol] !== " ") { - throw new Error("Cannot place player in a non-empty space"); - } - originalMap[playerRow][playerCol] = "P"; - - for (const block of response.parsed.boxCoordinates) { - const [blockRow, blockCol] = block; - if (originalMap[blockRow][blockCol] !== " ") { - throw new Error("Cannot place block in a non-empty space"); - } - - originalMap[blockRow][blockCol] = "B"; - } - return { - solution: originalMap, + boxCoordinates: response.parsed.boxCoordinates, + playerCoordinates: response.parsed.playerCoordinates, reasoning: response.parsed.reasoning, }; -} +}; diff --git a/models/index.ts b/models/index.ts index 0b994f7..b8bd75d 100644 --- a/models/index.ts +++ b/models/index.ts @@ -1,24 +1,75 @@ import { gemini15pro } from "./gemini-1.5-pro"; import { gpt4o } from "./gpt-4o"; -export interface ModelResult { - solution: string[][]; +export type ModelHandler = ( + prompt: string, + map: string[][], +) => Promise<{ + boxCoordinates: number[][]; + playerCoordinates: number[]; reasoning: string; -} +}>; + +const prompt = `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. + +Your goal is to place the player ("P") in a location which maximize the player's survival. +You must place two blocks ("B") in locations which maximize the player's survival. +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. +Zombies can only move horizontally or vertically, not diagonally. +You can't replace rocks R or zombies Z with blocks. +Players will always shoot at the closest zombie each turn. +If there is no room to place a block, do not place any.`; export async function runModel( modelId: string, map: string[][], -): Promise { +): Promise<{ + solution: string[][]; + reasoning: string; +}> { + let result; + switch (modelId) { case "gemini-1.5-pro": { - return gemini15pro(map); + result = await gemini15pro(prompt, map); + break; } case "gpt-4o": { - return gpt4o(map); + result = await gpt4o(prompt, map); + break; } default: { throw new Error(`Tried running unknown model '${modelId}'`); } } + + const originalMap = JSON.parse(JSON.stringify(map)); + const [playerRow, playerCol] = result.playerCoordinates; + + if (originalMap[playerRow][playerCol] !== " ") { + throw new Error("Cannot place player in a non-empty space"); + } + + originalMap[playerRow][playerCol] = "P"; + + for (const block of result.boxCoordinates) { + const [blockRow, blockCol] = block; + + if (originalMap[blockRow][blockCol] !== " ") { + throw new Error("Cannot place block in a non-empty space"); + } + + originalMap[blockRow][blockCol] = "B"; + } + + return { + solution: originalMap, + reasoning: result.reasoning, + }; } From 87bf97bf93fb71500c5bd5a63974cb983d0870cc Mon Sep 17 00:00:00 2001 From: Aaron Delasy Date: Wed, 16 Oct 2024 21:16:06 +0300 Subject: [PATCH 5/6] Fix maps TS iterator build error --- convex/maps.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/convex/maps.ts b/convex/maps.ts index acaf803..a2ccce2 100644 --- a/convex/maps.ts +++ b/convex/maps.ts @@ -98,12 +98,12 @@ export const seedMaps = internalMutation({ await ctx.db.delete(map._id); } - for (const [idx, map] of LEVELS.entries()) { + LEVELS.forEach((map, idx) => { ctx.db.insert("maps", { level: idx + 1, grid: map.grid, }); - } + }); }, }); From ab0a7989fdf29f2fbcdbfc6dd780a72ef9b90cb2 Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Wed, 16 Oct 2024 14:24:56 -0400 Subject: [PATCH 6/6] improve styling on watch page --- app/watch/page.tsx | 11 +++++++---- app/watch/result.tsx | 11 ++++++++--- package.json | 1 + 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/watch/page.tsx b/app/watch/page.tsx index ca98e37..35bebd1 100644 --- a/app/watch/page.tsx +++ b/app/watch/page.tsx @@ -12,10 +12,13 @@ export default function GamePage() { } return ( -
- {results.map((result) => ( - - ))} +
+

Recent Games

+
+ {results.map((result) => ( + + ))} +
); } diff --git a/app/watch/result.tsx b/app/watch/result.tsx index df6d6ca..b7d542c 100644 --- a/app/watch/result.tsx +++ b/app/watch/result.tsx @@ -3,13 +3,18 @@ import { ResultStatus } from "../result-status"; import { type ResultWithGame } from "@/convex/results"; import { Visualizer } from "../visualizer"; +import { format } from "date-fns"; export default function Result({ result }: { result: ResultWithGame }) { return ( -
+
{result.game !== null && ( -
- +
+
+ +

at {format(new Date(result._creationTime), "h:mma")}

+
+

Level {result.level}

{result.game.modelId}

)} diff --git a/package.json b/package.json index b257df4..68ac54b 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "convex": "^1.16.0", + "date-fns": "^4.1.0", "lucide-react": "^0.453.0", "next": "14.2.5", "next-themes": "^0.3.0",