From c0d6864d70e96750b18ca0d0267913023e88e3bf Mon Sep 17 00:00:00 2001 From: Cody Seibert Date: Mon, 21 Oct 2024 00:09:03 -0400 Subject: [PATCH] improve playground --- app/{maps => admin}/review/page.tsx | 0 app/header.tsx | 63 ++++--- app/maps/page.tsx | 171 ------------------ app/playground/page.tsx | 268 ++++++++++++++++++---------- convex/maps.ts | 17 +- convex/users.ts | 17 ++ 6 files changed, 227 insertions(+), 309 deletions(-) rename app/{maps => admin}/review/page.tsx (100%) delete mode 100644 app/maps/page.tsx diff --git a/app/maps/review/page.tsx b/app/admin/review/page.tsx similarity index 100% rename from app/maps/review/page.tsx rename to app/admin/review/page.tsx diff --git a/app/header.tsx b/app/header.tsx index 62516af..c46b119 100644 --- a/app/header.tsx +++ b/app/header.tsx @@ -8,6 +8,13 @@ import Image from "next/image"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { api } from "@/convex/_generated/api"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { ChevronDownIcon } from "@radix-ui/react-icons"; const ThemeToggle = dynamic( async () => (await import("@/components/ThemeToggle")).ThemeToggle, @@ -43,34 +50,36 @@ export default function Header() { diff --git a/app/maps/page.tsx b/app/maps/page.tsx deleted file mode 100644 index 442c6ff..0000000 --- a/app/maps/page.tsx +++ /dev/null @@ -1,171 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useMutation } from "convex/react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { api } from "@/convex/_generated/api"; - -export default function AddMapPage() { - const [height, setHeight] = useState(1); - const [width, setWidth] = useState(1); - const [map, setMap] = useState([]); - const [isSubmitted, setIsSubmitted] = useState(false); - const [isZombie, setIsZombie] = useState(false); - const createMap = useMutation(api.maps.addMap); - useEffect(() => { - generateMap(); - }, [height, width]); - - const generateMap = () => { - const newMap = Array.from({ length: height }, () => - Array.from({ length: width }, () => " "), - ); - newMap.forEach((row, y) => { - row.forEach((cell, x) => { - newMap[y][x] = " "; - }); - }); - setMap(newMap); - }; - const checkValidMap = () => { - var flag = false; - map.forEach((row, y) => { - row.forEach((cell, x) => { - if (map[y][x] === " ") { - flag = true; - } - }); - }); - if (!flag) { - alert("No place left to place the player!"); - } else { - createMap({ grid: map }); - alert("Map saved"); - } - }; - const setCell = (y: number, x: number, z: boolean) => { - var cell = " "; - if (z === true) { - if (map[y][x] === "Z") { - cell = " "; - } else { - cell = "Z"; - } - } else { - if (map[y][x] === "B") { - cell = " "; - } else { - cell = "R"; - } - } - const newMap = map.map((row) => [...row]); - newMap[y][x] = cell; - setMap(newMap); - }; - return ( -
-

Submit Map

- - {isSubmitted ? ( -
-
- - -
-
- {map.map((row, y) => ( -
- {row.map((cell, x) => ( -
- -
- ))} -
- ))} -
-
- ) : ( -
-

Enter the height of the map:

- { - setHeight(+e.target.value); - }} - /> -

Enter the width of the map:

- { - setWidth(+e.target.value); - }} - /> -
- {map.map((row, y) => ( -
- {row.map((cell, x) => ( -
- {cell} -
- ))} -
- ))} -
-
- )} -
- {isSubmitted ? ( -
- - -
- ) : ( - - )} -
-
- ); -} diff --git a/app/playground/page.tsx b/app/playground/page.tsx index 6f57ad7..363c7ea 100644 --- a/app/playground/page.tsx +++ b/app/playground/page.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { useAction, useMutation, useQuery } from "convex/react"; -import { CircleAlertIcon, EraserIcon } from "lucide-react"; +import { CircleAlertIcon, EraserIcon, SendIcon, UploadIcon, ChevronLeft } from "lucide-react"; import { CopyMapButton } from "@/components/CopyMapButton"; import { MapBuilder } from "@/components/MapBuilder"; import { MapStatus } from "@/components/MapStatus"; @@ -13,15 +13,23 @@ import { useToast } from "@/components/ui/use-toast"; import { api } from "@/convex/_generated/api"; import { errorMessage } from "@/lib/utils"; import { ZombieSurvival } from "@/simulators/zombie-survival"; +import { Card } from "@/components/ui/card"; +import { Map } from "@/components/Map"; const STORAGE_MAP_KEY = "playground-map"; export default function PlaygroundPage() { const isAdmin = useQuery(api.users.isAdmin); - const publishMap = useMutation(api.maps.publishMap); + const submitMap = useMutation(api.maps.submitMap); const testMap = useAction(api.maps.testMap); const { toast } = useToast(); - const [map, setMap] = React.useState([[" "]]); + const [map, setMap] = React.useState([ + [" ", " ", " ", " ", " "], + [" ", " ", " ", " ", " "], + [" ", " ", " ", " ", " "], + [" ", " ", " ", " ", " "], + [" ", " ", " ", " ", " "], + ]); const [model, setModel] = React.useState(""); const [error, setError] = React.useState(null); const [solution, setSolution] = React.useState(null); @@ -46,7 +54,7 @@ export default function PlaygroundPage() { setPublishing(true); try { - await publishMap({ map }); + await submitMap({ map }); toast({ description: "Map published successfully!", @@ -104,6 +112,13 @@ export default function PlaygroundPage() { setReasoning(null); setUserPlaying(false); setVisualizingUserSolution(false); + + // Remove players and blocks from the map + const cleanedMap = map.map(row => + row.map(cell => (cell === "P" || cell === "B") ? " " : cell) + ); + setMap(cleanedMap); + window.localStorage.setItem(STORAGE_MAP_KEY, JSON.stringify(cleanedMap)); } function handleReset() { @@ -157,118 +172,173 @@ export default function PlaygroundPage() { const visualizing = solution !== null || visualizingUserSolution; return ( -
+

Playground

-
-
-
-
-
-

- Map ({ZombieSurvival.boardWidth(map)}x - {ZombieSurvival.boardHeight(map)}) -

- - -
-

* Click on a cell to place entity

-
- {isAdmin && ( - - )} -
-
- {visualizing && ( - +
+ {/* Left side: Grid */} +
+ + {!visualizing && !userPlaying && ( +

+ Click on the board to place or remove units. Use the buttons below to switch between unit types. +

)} - {!visualizing && ( - + {!visualizing && userPlaying && ( +

+ Place a player (P) and blocks (B) on the board to create your escape route. Click to toggle between empty, player, and block. +

)} -
+
+ {visualizing && ( + + )} + {!visualizing && ( +
+ +
+ {(userPlaying ? userSolution : map).map((row, y) => + row.map((cell, x) => ( +
{ + const newMap = userPlaying + ? [...userSolution] + : [...map]; + if (userPlaying) { + // Count existing players and blocks + const playerCount = newMap.flat().filter(c => c === "P").length; + const blockCount = newMap.flat().filter(c => c === "B").length; + + // Toggle logic for play mode + if (cell === " ") { + if (playerCount === 0) { + newMap[y][x] = "P"; + } else if (blockCount < 2) { + newMap[y][x] = "B"; + } + } else if (cell === "P") { + newMap[y][x] = " "; + } else if (cell === "B") { + newMap[y][x] = " "; + } + userPlaying + ? setUserSolution(newMap) + : handleChangeMap(newMap); + } else { + // Toggle between empty, zombie, and rock for edit mode + if (cell === " ") newMap[y][x] = "Z"; + else if (cell === "Z") newMap[y][x] = "R"; + else newMap[y][x] = " "; + } + userPlaying + ? setUserSolution(newMap) + : handleChangeMap(newMap); + }} + /> + )) + )} +
+
+ )} +
+
-
-
- {isAdmin && ( + + {/* Right side: Action Panel */} + +
+ {userPlaying || solution !== null ? ( <> -

Model (~$0.002)

- - {!userPlaying && solution === null && ( + + {userPlaying && ( )} + ) : ( + <> + + + + )} - {(solution !== null || userPlaying) && ( - - )} - {solution === null && !simulating && !userPlaying && ( - + + {error !== null &&

{error}

} + {visualizingUserSolution && } + {reasoning !== null && ( +
+ +

{reasoning}

+
)} - {userPlaying && ( - + + {isAdmin && !userPlaying && solution === null && ( + <> +
+
+

Model (~$0.002)

+ + +
+ )}
- {error !== null &&

{error}

} - {visualizingUserSolution && } - {reasoning !== null && ( -
- -

{reasoning}

-
- )} -
+
); diff --git a/convex/maps.ts b/convex/maps.ts index eb8a42d..003dc04 100644 --- a/convex/maps.ts +++ b/convex/maps.ts @@ -11,7 +11,7 @@ import { query, } from "./_generated/server"; import { Prompt } from "./prompts"; -import { adminMutationBuilder } from "./users"; +import { adminMutationBuilder, authenticatedMutation } from "./users"; const LEVELS = [ { @@ -150,23 +150,16 @@ export const addMap = mutation({ }, }); -export const publishMap = adminMutationBuilder({ +export const submitMap = authenticatedMutation({ args: { map: v.array(v.array(v.string())), }, handler: async (ctx, args) => { - const maps = await ctx.db - .query("maps") - .filter((q) => q.neq("level", undefined)) - .collect(); - - const lastLevel = maps.sort((a, b) => b.level! - a.level!)[0].level!; - await ctx.db.insert("maps", { grid: args.map, - level: lastLevel + 1, - submittedBy: ctx.admin.id, - isReviewed: true, + level: undefined, + submittedBy: ctx.userId, + isReviewed: false, }); }, }); diff --git a/convex/users.ts b/convex/users.ts index a14c505..30dbeb7 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -5,6 +5,7 @@ import { customQuery, } from "convex-helpers/server/customFunctions"; import { mutation, query } from "./_generated/server"; +import { ConvexError } from "convex/values"; export const viewer = query({ args: {}, @@ -99,3 +100,19 @@ export const adminMutationBuilder = customMutation( }; }), ); + + +export const authenticatedMutation = customMutation( + mutation, + customCtx(async (ctx) => { + const userId = await getAuthUserId(ctx); + + if (userId === null) { + throw new ConvexError("You must be signed in to perform this action"); + } + + return { + userId, + }; + }), +);