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, }; }