Skip to content

Commit

Permalink
adding the ability to place blocks on manual play; improve prompting …
Browse files Browse the repository at this point in the history
…a bit, remove width / height from map (it can be computed via the array), working on simplier maps that get progressively harder.
  • Loading branch information
webdevcody committed Oct 16, 2024
1 parent bb45e47 commit e904b95
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 88 deletions.
4 changes: 3 additions & 1 deletion app/games/[gameId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export default function GamePage({ params }: { params: { gameId: string } }) {
<p className="ml-4">Game starting...</p>
</div>
) : (
results.map((result) => <Result result={result} key={result._id} />)
results.map((result) => (
<Result autoPlay={true} result={result} key={result._id} />
))
)}
</div>
</div>
Expand Down
10 changes: 8 additions & 2 deletions app/games/[gameId]/result.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand All @@ -24,8 +25,13 @@ export const Result = ({ result }: { result: Doc<"results"> }) => {

return (
<div className="flex items-center gap-8">
<div className="whitespace-nowrap">Level {map.level}</div>
<Visualizer map={result.map} />
<Link
href={`/play/${map.level}`}
className="whitespace-nowrap hover:underline cursor-pointer"
>
Level {map.level}
</Link>
<Visualizer map={result.map} autoStart={true} />
<div className="flex flex-col">
<div
className={`font-bold ${result.isWin ? "text-green-500" : "text-red-500"}`}
Expand Down
59 changes: 48 additions & 11 deletions app/play/[level]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export default function PlayLevelPage({
const [playerMap, setPlayerMap] = useState<string[][]>([]);
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 <div>Loading...</div>;
Expand All @@ -27,6 +31,7 @@ export default function PlayLevelPage({
function handleRetryClicked() {
setIsSimulating(false);
setGameResult(null);
setBlockCount(0);
if (map) {
setPlayerMap(map.grid);
}
Expand All @@ -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(
Expand All @@ -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 (
<div className="container mx-auto py-8">
<div className="flex justify-between items-center mb-6">
Expand All @@ -81,6 +102,22 @@ export default function PlayLevelPage({
<h1 className="text-3xl font-bold text-center">Level {level}</h1>
<div className="w-[100px]"></div> {/* Spacer for alignment */}
</div>
<div className="mb-4 flex justify-center gap-4">
<Button
onClick={() => handlePlacementModeChange("player")}
disabled={playerMap.some((row) => row.includes("P"))}
variant={placementMode === "player" ? "default" : "outline"}
>
Place Player
</Button>
<Button
onClick={() => handlePlacementModeChange("block")}
disabled={blockCount >= 2}
variant={placementMode === "block" ? "default" : "outline"}
>
Place Block ({2 - blockCount} left)
</Button>
</div>
<div className="mb-8 flex flex-col items-center">
<h2 className="text-xl font-semibold mb-4">
{isSimulating ? "Simulation Result" : "Place Your Player"}
Expand All @@ -106,8 +143,8 @@ export default function PlayLevelPage({
<div
className="absolute inset-0 grid"
style={{
gridTemplateColumns: `repeat(${map.width}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${map.height}, minmax(0, 1fr))`,
gridTemplateColumns: `repeat(${mapWidth}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${mapHeight}, minmax(0, 1fr))`,
}}
>
{(playerMap.length > 0 ? playerMap : map.grid).map((row, y) =>
Expand Down
150 changes: 84 additions & 66 deletions convex/maps.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
},
});
Expand Down
2 changes: 0 additions & 2 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
32 changes: 26 additions & 6 deletions models/gpt-4o.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())),
Expand All @@ -18,13 +17,16 @@ export async function gpt4o(map: string[][]): Promise<ModelResult> {
"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",
Expand All @@ -38,6 +40,7 @@ export async function gpt4o(map: string[][]): Promise<ModelResult> {
content: JSON.stringify(map),
},
],
temperature: 0.5,
response_format: zodResponseFormat(ResponseSchema, "game_map"),
});

Expand All @@ -49,8 +52,25 @@ export async function gpt4o(map: string[][]): Promise<ModelResult> {
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,
};
}

0 comments on commit e904b95

Please sign in to comment.