Skip to content

Commit

Permalink
Add playground page
Browse files Browse the repository at this point in the history
  • Loading branch information
delasy committed Oct 17, 2024
1 parent a2b3b2d commit f97c3b5
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 23 deletions.
11 changes: 8 additions & 3 deletions app/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,14 @@ export default function Header() {
</Link>
)}
{flags?.showTestPage && (
<Link href="/test">
<Button variant="ghost">Test</Button>
</Link>
<>
<Link href="/playground">
<Button variant="ghost">Playground</Button>
</Link>
<Link href="/test">
<Button variant="ghost">Test</Button>
</Link>
</>
)}
</nav>

Expand Down
122 changes: 122 additions & 0 deletions app/playground/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"use client";

import React from "react";
import { useAction } from "convex/react";
import { Button } from "@/components/ui/button";
import { CopyMapButton } from "@/components/CopyMapButton";
import { MapBuilder } from "@/components/MapBuilder";
import { ModelSelector } from "@/components/ModelSelector";
import { Visualizer } from "@/components/Visualizer";
import { ZombieSurvival } from "@/simulators/zombie-survival";
import { api } from "@/convex/_generated/api";

export default function PlaygroundPage() {
const testMap = useAction(api.maps.testMap);
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 [simulating, setSimulating] = React.useState(false);

async function handleSimulate() {
setError(null);
setSolution(null);
setReasoning(null);

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

setSimulating(true);

const { error, solution, reasoning } = await testMap({
modelId: model,
map: map,
});

if (typeof error !== "undefined") {
setError(error);
} else {
setSolution(solution!);
setReasoning(reasoning!);
}

setSimulating(false);
}

function handleChangeMap(value: string[][]) {
setMap(value);
setError(null);
}

function handleChangeModel(value: string) {
setModel(value);
setError(null);
}

function handleEdit() {
setSolution(null);
setReasoning(null);
}

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>

<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">
<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}
/>
</div>
</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} />
<Button
disabled={model === "" || simulating}
onClick={solution === null ? handleSimulate : handleEdit}
type="button"
className="w-full"
>
{simulating
? "Simulating..."
: solution === null
? "Play"
: "Edit"}
</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>
</div>
</div>
</div>
);
}
16 changes: 16 additions & 0 deletions components/CopyMapButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"use client";

import React from "react";
import { Button } from "./ui/button";

export function CopyMapButton({ map }: { map: string[][] }) {
async function handleClick() {
await navigator.clipboard.writeText(JSON.stringify(map));
}

return (
<Button onClick={handleClick} type="button">
Copy As Code
</Button>
);
}
171 changes: 171 additions & 0 deletions components/MapBuilder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React from "react";
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
ChevronUpIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";

export function MapBuilder({
disabled,
onChange,
value,
}: {
disabled?: boolean;
onChange: (value: string[][]) => unknown;
value: string[][];
}) {
const map = value.length === 0 || value[0].length === 0 ? [[" "]] : value;

React.useEffect(() => {
if (value.length === 0 || value[0].length === 0) {
onChange([[" "]]);
}
}, [value]);

function handleClickCell(row: number, cell: number, initialValue: string) {
const newValue =
initialValue === " " ? "R" : initialValue === "R" ? "Z" : " ";

const newMap = [...map.map((row) => [...row])];
newMap[row][cell] = newValue;
onChange(newMap);
}

function handleIncreaseDown() {
onChange([...map.map((row) => [...row]), [...map[0].map(() => " ")]]);
}

function handleDecreaseDown() {
onChange([...map.slice(0, -1).map((row) => [...row])]);
}

function handleIncreaseLeft() {
onChange(map.map((row) => [" ", ...row]));
}

function handleDecreaseLeft() {
onChange(map.map((row) => [...row.slice(1)]));
}

function handleIncreaseRight() {
onChange(map.map((row) => [...row, " "]));
}

function handleDecreaseRight() {
onChange(map.map((row) => [...row.slice(0, -1)]));
}

function handleIncreaseUp() {
onChange([[...map[0].map(() => " ")], ...map.map((row) => [...row])]);
}

function handleDecreaseUp() {
onChange([...map.slice(1).map((row) => [...row])]);
}

const moreThanOneRow = map.length > 1;
const moreThanOneCell = map[0].length > 1;

const buttonClassName = cn("border border-white w-8 h-8 disabled:opacity-50");

const controlClassName = cn(
"h-8 absolute hover:scale-125 transition disabled:opacity-50",
);

return (
<div className="relative inline-flex flex-col p-7 w-fit">
<button
className={cn(controlClassName, "top-0 left-1/2 translate-x-[-100%]")}
disabled={disabled}
type="button"
onClick={handleIncreaseUp}
>
<ChevronUpIcon />
</button>
{moreThanOneRow && (
<button
className={cn(controlClassName, "top-0 left-1/2")}
disabled={disabled}
type="button"
onClick={handleDecreaseUp}
>
<ChevronDownIcon />
</button>
)}
<button
className={cn(controlClassName, "top-1/2 left-0 translate-y-[-100%]")}
disabled={disabled}
type="button"
onClick={handleIncreaseLeft}
>
<ChevronLeftIcon />
</button>
{moreThanOneCell && (
<button
className={cn(controlClassName, "top-1/2 left-0")}
disabled={disabled}
type="button"
onClick={handleDecreaseLeft}
>
<ChevronRightIcon />
</button>
)}
<button
className={cn(
controlClassName,
"bottom-0 left-1/2 translate-x-[-100%]",
)}
disabled={disabled}
type="button"
onClick={handleIncreaseDown}
>
<ChevronDownIcon />
</button>
{moreThanOneRow && (
<button
className={cn(controlClassName, "bottom-0 left-1/2")}
disabled={disabled}
type="button"
onClick={handleDecreaseDown}
>
<ChevronUpIcon />
</button>
)}
<button
className={cn(controlClassName, "top-1/2 right-0 translate-y-[-100%]")}
disabled={disabled}
type="button"
onClick={handleIncreaseRight}
>
<ChevronRightIcon />
</button>
{moreThanOneCell && (
<button
className={cn(controlClassName, "top-1/2 right-0")}
disabled={disabled}
type="button"
onClick={handleDecreaseRight}
>
<ChevronLeftIcon />
</button>
)}
{map.map((row, rowIdx) => (
<div key={rowIdx} className="inline-flex">
{row.map((cell, cellIdx) => (
<button
className={buttonClassName}
disabled={disabled === true}
key={`${rowIdx}.${cellIdx}`}
onClick={() => handleClickCell(rowIdx, cellIdx, cell)}
type="button"
>
{cell}
</button>
))}
</div>
))}
</div>
);
}
16 changes: 16 additions & 0 deletions convex/maps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@ export const seedMaps = internalMutation({
},
});

export const testMap = action({
args: {
modelId: v.string(),
map: v.array(v.array(v.string())),
},
handler: async (ctx, args) => {
const flags = await ctx.runQuery(api.flags.getFlags);

if (!flags.showTestPage) {
throw new Error("Test page is not enabled");
}

return await runModel(args.modelId, args.map);
},
});

export const getMaps = query({
args: {},
handler: async (ctx) => {
Expand Down
Loading

0 comments on commit f97c3b5

Please sign in to comment.