Skip to content

Commit

Permalink
Merge pull request #81 from webdevcody/feat-manual-playground
Browse files Browse the repository at this point in the history
Ability to play as user in Playground
  • Loading branch information
webdevcody authored Oct 18, 2024
2 parents 52c93de + 972bb99 commit 0ed5ae8
Show file tree
Hide file tree
Showing 14 changed files with 407 additions and 176 deletions.
4 changes: 2 additions & 2 deletions app/games/[gameId]/result.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { Doc } from "@/convex/_generated/dataModel";
import { api } from "@/convex/_generated/api";
import { useQuery } from "convex/react";
import { ResultStatus } from "@/components/ResultStatus";
import { MapStatus } from "@/components/MapStatus";
import { Visualizer } from "@/components/Visualizer";
import Link from "next/link";

Expand Down Expand Up @@ -38,7 +38,7 @@ export const Result = ({ result }: { result: Doc<"results"> }) => {
)}

<div className="flex flex-col">
<ResultStatus result={result} />
<MapStatus map={result.map} />
{result.reasoning !== "" && <p>{result.reasoning}</p>}
</div>
</div>
Expand Down
14 changes: 6 additions & 8 deletions app/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export default function Header() {
<Link href="/leaderboard">
<Button variant="ghost">Leaderboard</Button>
</Link>
<Link href="/playground">
<Button variant="ghost">Playground</Button>
</Link>
{isAuthenticated && (
<Link href="/maps">
<Button variant="ghost">Submit Map</Button>
Expand All @@ -56,14 +59,9 @@ export default function Header() {
</Link>
)}
{flags?.showTestPage && (
<>
<Link href="/playground">
<Button variant="ghost">Playground</Button>
</Link>
<Link href="/test">
<Button variant="ghost">Test</Button>
</Link>
</>
<Link href="/test">
<Button variant="ghost">Test</Button>
</Link>
)}
</nav>

Expand Down
5 changes: 2 additions & 3 deletions app/play/[level]/test-mode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { AI_MODELS } from "@/convex/constants";
import { errorMessage } from "@/lib/utils";

interface TestModeProps {
level: number;
Expand Down Expand Up @@ -51,9 +52,7 @@ export default function TestMode({ level, map }: TestModeProps) {
setAiReasoning(result.reasoning);
} catch (error) {
console.error("Error testing AI model:", error);
setAiError(
error instanceof Error ? error.message : "An unexpected error occurred",
);
setAiError(errorMessage(error));
} finally {
setIsSimulating(false);
}
Expand Down
205 changes: 164 additions & 41 deletions app/playground/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,63 @@
"use client";

import React from "react";
import { useAction } from "convex/react";
import { CircleAlertIcon } from "lucide-react";
import { useAction, useMutation, useQuery } from "convex/react";
import { Button } from "@/components/ui/button";
import { CopyMapButton } from "@/components/CopyMapButton";
import { MapBuilder } from "@/components/MapBuilder";
import { MapStatus } from "@/components/MapStatus";
import { ModelSelector } from "@/components/ModelSelector";
import { Visualizer } from "@/components/Visualizer";
import { ZombieSurvival } from "@/simulators/zombie-survival";
import { api } from "@/convex/_generated/api";
import { errorMessage } from "@/lib/utils";
import { useToast } from "@/components/ui/use-toast";

export default function PlaygroundPage() {
const isAdmin = useQuery(api.users.isAdmin);
const publishMap = useMutation(api.maps.publishMap);
const testMap = useAction(api.maps.testMap);
const { toast } = useToast();
const [map, setMap] = React.useState<string[][]>([]);
const [model, setModel] = React.useState("");
const [error, setError] = React.useState<string | null>(null);
const [solution, setSolution] = React.useState<string[][] | null>(null);
const [reasoning, setReasoning] = React.useState<string | null>(null);
const [publishing, setPublishing] = React.useState(false);
const [simulating, setSimulating] = React.useState(false);
const [userPlaying, setUserPlaying] = React.useState(false);
const [userSolution, setUserSolution] = React.useState<string[][]>([]);
const [visualizingUserSolution, setVisualizingUserSolution] =
React.useState(false);

async function handlePublish() {
if (!ZombieSurvival.mapHasZombies(map)) {
alert("Add some zombies to the map first");
return;
}

if (!confirm("Are you sure?")) {
return;
}

setPublishing(true);

try {
await publishMap({ map });

toast({
description: "Map published successfully!",
});
} catch (error) {
toast({
description: errorMessage(error),
variant: "destructive",
});
} finally {
setPublishing(false);
}
}

async function handleSimulate() {
setError(null);
Expand Down Expand Up @@ -59,62 +99,145 @@ export default function PlaygroundPage() {
function handleEdit() {
setSolution(null);
setReasoning(null);
setUserPlaying(false);
setVisualizingUserSolution(false);
}

function handleUserPlay() {
if (!ZombieSurvival.mapHasZombies(map)) {
alert("Add player to the map first");
return;
}

setUserPlaying(true);
setUserSolution(map);
}

function handleVisualize() {
if (!ZombieSurvival.mapHasPlayer(userSolution)) {
alert("Add player to the map first");
return;
}

if (ZombieSurvival.mapHasMultiplePlayers(userSolution)) {
alert("Map has multiple players");
return;
}

setVisualizingUserSolution(true);
}

function handleStopVisualization() {
setVisualizingUserSolution(false);
}

const visualizing = solution !== null || visualizingUserSolution;

return (
<div className="container mx-auto min-h-screen flex flex-col items-center py-12 gap-8">
<h1 className="text-4xl font-bold mb-8">Playground</h1>
<h1 className="text-4xl font-bold">Playground</h1>

<div>
<div className="flex gap-8">
{solution !== null && (
<div className="flex flex-col">
<Visualizer
autoReplay
autoStart
controls={false}
map={solution}
/>
</div>
)}
{solution === null && (
<div className="flex flex-col gap-2 min-w-[700px]">
<div className="flex flex-col gap-0">
<div className="flex w-full gap-8">
<div className="flex flex-col gap-2 grow w-full">
<div className="flex justify-between">
<div className="flex flex-col gap-0">
<div className="flex gap-2">
<p>
Map ({map.length}x{map[0]?.length ?? 0})
</p>
<p className="text-xs">* Click on a cell to see magic</p>
</div>
<div className="flex justify-center">
<MapBuilder
disabled={simulating}
onChange={handleChangeMap}
value={map}
/>
<CopyMapButton map={map} />
</div>
<p className="text-xs">* Click on a cell to place entity</p>
</div>
)}
<div className="flex flex-col gap-4 max-w-[400px]">
<div className="flex flex-col gap-2 w-fit">
<p>Model (~$0.002)</p>
<ModelSelector onChange={handleChangeModel} value={model} />
{isAdmin && (
<Button
disabled={model === "" || simulating}
onClick={solution === null ? handleSimulate : handleEdit}
className="gap-1"
disabled={publishing}
onClick={handlePublish}
type="button"
variant="destructive"
>
<CircleAlertIcon size={16} />
<span>{publishing ? "Publishing..." : "Publish Map"}</span>
</Button>
)}
</div>
<div
className={`flex justify-center ${visualizing ? "pt-[28px]" : ""}`}
>
{visualizing && (
<Visualizer
autoReplay
autoStart
controls={false}
map={visualizingUserSolution ? userSolution : solution!}
/>
)}
{!visualizing && (
<MapBuilder
disabled={simulating}
onChange={userPlaying ? setUserSolution : handleChangeMap}
play={userPlaying}
value={userPlaying ? userSolution : map}
/>
)}
</div>
</div>
<div className="flex flex-col gap-4 shrink-0 w-[400px]">
<div className="flex flex-col gap-2 w-fit">
{isAdmin && (
<>
<p>Model (~$0.002)</p>
<ModelSelector onChange={handleChangeModel} value={model} />
{!userPlaying && solution === null && (
<Button
className="w-full"
disabled={model === "" || simulating}
onClick={handleSimulate}
type="button"
>
{simulating ? "Simulating..." : "Play With AI"}
</Button>
)}
{(solution !== null || userPlaying) && (
<Button
className="w-full"
disabled={model === "" || simulating}
onClick={handleEdit}
type="button"
>
{simulating ? "Simulating..." : "Edit"}
</Button>
)}
</>
)}
{solution === null && !simulating && !userPlaying && (
<Button className="w-full" onClick={handleUserPlay} type="button">
Play Yourself
</Button>
)}
{userPlaying && (
<Button
className="w-full"
onClick={
visualizingUserSolution
? handleStopVisualization
: handleVisualize
}
type="button"
>
{simulating
? "Simulating..."
: solution === null
? "Play"
: "Edit"}
{visualizingUserSolution ? "Stop" : "Visualize"}
</Button>
<CopyMapButton map={map} />
</div>
{error !== null && <p className="text-sm text-red-500">{error}</p>}
{reasoning !== null && <p className="text-sm">{reasoning}</p>}
)}
</div>
{error !== null && <p className="text-sm text-red-500">{error}</p>}
{visualizingUserSolution && <MapStatus map={userSolution} />}
{reasoning !== null && (
<div className="flex flex-col gap-0">
<MapStatus map={solution!} />
<p className="text-sm">{reasoning}</p>
</div>
)}
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions app/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ConvexAuthNextjsProvider } from "@convex-dev/auth/nextjs";
import { ConvexReactClient } from "convex/react";
import { ThemeProvider } from "next-themes";
import PlausibleProvider from "next-plausible";
import { Toaster } from "@/components/ui/toaster";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);

Expand All @@ -17,6 +18,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
<ThemeProvider attribute="class">
<ConvexAuthNextjsProvider client={convex}>
{children}
<Toaster />
</ConvexAuthNextjsProvider>
</ThemeProvider>
</PlausibleProvider>
Expand Down
4 changes: 2 additions & 2 deletions app/result.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { ResultStatus } from "@/components/ResultStatus";
import { MapStatus } from "@/components/MapStatus";
import { type ResultWithGame } from "@/convex/results";
import { Visualizer } from "@/components/Visualizer";
import { format } from "date-fns";
Expand Down Expand Up @@ -51,7 +51,7 @@ export default function Result({ result }: { result: ResultWithGame }) {
{result.status === "inProgress" ? (
"Started"
) : (
<ResultStatus result={result} />
<MapStatus map={result.map} />
)}{" "}
at {format(new Date(result._creationTime), "h:mma")}
</div>
Expand Down
33 changes: 29 additions & 4 deletions components/CopyMapButton.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
"use client";

import React from "react";
import { Button } from "./ui/button";
import { ClipboardCopyIcon, SmileIcon } from "lucide-react";

export function CopyMapButton({ map }: { map: string[][] }) {
const timeout = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const [copied, setCopied] = React.useState(false);

async function handleClick() {
await navigator.clipboard.writeText(JSON.stringify(map));
setCopied(true);

timeout.current = setTimeout(() => {
timeout.current = null;
setCopied(false);
}, 2000);
}

React.useEffect(() => {
return () => {
if (timeout.current !== null) {
clearTimeout(timeout.current);
}
};
}, []);

return (
<Button onClick={handleClick} type="button">
Copy As Code
</Button>
<button
className={
copied
? "cursor-default"
: "enabled:hover:scale-125 transition disabled:opacity-50"
}
onClick={copied ? undefined : handleClick}
type="button"
>
{copied ? <SmileIcon size={16} /> : <ClipboardCopyIcon size={16} />}
</button>
);
}
Loading

0 comments on commit 0ed5ae8

Please sign in to comment.