Skip to content

Commit

Permalink
adding a manual play mode
Browse files Browse the repository at this point in the history
  • Loading branch information
webdevcody committed Oct 16, 2024
1 parent e5ec742 commit 52471c2
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 28 deletions.
71 changes: 48 additions & 23 deletions app/games/[gameId]/visualizer.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,60 @@
import { Button } from "@/components/ui/button";
import { ZombieSurvival } from "@/simulators/zombie-survival";
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";

const REPLAY_SPEED = 600;

export function Visualizer({ map }: { map: string[][] }) {
export function Visualizer({
map,
autoStart = false,
onSimulationEnd,
}: {
map: string[][];
autoStart?: boolean;
onSimulationEnd?: (isWin: boolean) => void;
}) {
const simulator = useRef<ZombieSurvival | null>(null);
const interval = useRef<NodeJS.Timeout | null>(null);
const [isRunning, setIsRunning] = useState(false);
const [mapState, setMapState] = useState(map);
const [needsReset, setNeedsReset] = useState(false);

const startSimulation = () => {
setNeedsReset(true);
const clonedMap = JSON.parse(JSON.stringify(map));
simulator.current = new ZombieSurvival(clonedMap);
setMapState(simulator.current!.getState());
setIsRunning(true);
interval.current = setInterval(() => {
if (simulator.current!.finished()) {
clearInterval(interval.current!);
interval.current = null;
setIsRunning(false);
if (onSimulationEnd) {
console.log("here");
onSimulationEnd(!simulator.current!.getPlayer().dead());
}
return;
}
simulator.current!.step();
setMapState(simulator.current!.getState());
}, REPLAY_SPEED);
};

useEffect(() => {
if (autoStart) {
startSimulation();
}
}, [autoStart]);

useEffect(() => {
return () => {
if (interval.current) {
clearInterval(interval.current);
}
};
}, []);

return (
<div>
{mapState.map((row, y) => (
Expand All @@ -26,27 +70,8 @@ export function Visualizer({ map }: { map: string[][] }) {
</div>
))}
<div className="flex gap-2 justify-center py-2">
<Button
onClick={() => {
setNeedsReset(true);
const clonedMap = JSON.parse(JSON.stringify(map));
simulator.current = new ZombieSurvival(clonedMap);
setMapState(simulator.current!.getState());
setIsRunning(true);
interval.current = setInterval(() => {
if (simulator.current!.finished()) {
clearInterval(interval.current!);
interval.current = null;
setIsRunning(false);
return;
}
simulator.current!.step();
setMapState(simulator.current!.getState());
}, REPLAY_SPEED);
}}
disabled={isRunning}
>
Play
<Button onClick={startSimulation} disabled={isRunning}>
Replay
</Button>
<Button
disabled={isRunning}
Expand Down
17 changes: 12 additions & 5 deletions app/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,16 @@ export default function Header() {
<span className="ml-2 text-xl font-bold">SurviveTheNight</span>
</Link>

<div className="flex items-center">
<nav className="flex items-center space-x-4">
<Link href="/">
<Button variant="ghost">Watch</Button>
</Link>
<Link href="/play">
<Button variant="ghost">Play</Button>
</Link>
</nav>

<div className="flex items-center space-x-4">
<span className="mr-2 text-sm">Synced using Convex</span>
<Link
href="https://www.convex.dev"
Expand All @@ -41,11 +50,9 @@ export default function Header() {
>
<Image src="/convex.svg" alt="Convex" width={24} height={24} />
</Link>
</div>

<div className="flex">
<div className="flex hover:bg-slate-500 mr-3 rounded-md px-1">
<ThemeToggle/>
<div className="flex hover:bg-slate-500 rounded-md px-1">
<ThemeToggle />
</div>
{!isAuthenticated ? (
<SignInWithGitHub />
Expand Down
134 changes: 134 additions & 0 deletions app/play/[level]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"use client";

import { useState } from "react";
import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
import { Button } from "@/components/ui/button";
import { Visualizer } from "@/app/games/[gameId]/visualizer";
import { Map } from "@/app/map";
import Link from "next/link";
import { ChevronLeftIcon } from "@radix-ui/react-icons";

export default function PlayLevelPage({
params,
}: {
params: { level: string };
}) {
const level = parseInt(params.level, 10);
const map = useQuery(api.maps.getMapByLevel, { level });
const [playerMap, setPlayerMap] = useState<string[][]>([]);
const [isSimulating, setIsSimulating] = useState(false);
const [gameResult, setGameResult] = useState<"WON" | "LOST" | null>(null);

if (!map) {
return <div>Loading...</div>;
}

function handleRetryClicked() {
setIsSimulating(false);
setGameResult(null);
if (map) {
setPlayerMap(map.grid);
}
}

const handleCellClick = (x: number, y: number) => {
if (isSimulating) return;

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] = " ";
}
}
}

// Place new player
if (newMap[y][x] === " ") {
newMap[y][x] = "P";
}

setPlayerMap(newMap);
};

const runSimulation = () => {
if (!playerMap.some((row) => row.includes("P"))) {
alert(
"Please place a player (P) on the map before running the simulation.",
);
return;
}
setIsSimulating(true);
setGameResult(null);
};

const handleSimulationEnd = (isWin: boolean) => {
setGameResult(isWin ? "WON" : "LOST");
};

return (
<div className="container mx-auto py-8">
<div className="flex justify-between items-center mb-6">
<Button variant="outline" asChild className="flex gap-2 items-center">
<Link href="/play" passHref>
<ChevronLeftIcon /> Back to Levels
</Link>
</Button>
<h1 className="text-3xl font-bold text-center">Level {level}</h1>
<div className="w-[100px]"></div> {/* Spacer for alignment */}
</div>
<div className="mb-8 flex flex-col items-center">
<h2 className="text-xl font-semibold mb-4">
{isSimulating ? "Simulation Result" : "Place Your Player"}
</h2>
{isSimulating ? (
<>
<Visualizer
map={playerMap}
autoStart={true}
onSimulationEnd={handleSimulationEnd}
/>
{gameResult && (
<div
className={`mt-4 text-2xl font-bold ${gameResult === "WON" ? "text-green-500" : "text-red-500"}`}
>
{gameResult === "WON" ? "You Survived!" : "You Died!"}
</div>
)}
</>
) : (
<div className="relative">
<Map map={playerMap.length > 0 ? playerMap : map.grid} />
<div
className="absolute inset-0 grid"
style={{
gridTemplateColumns: `repeat(${map.width}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${map.height}, minmax(0, 1fr))`,
}}
>
{(playerMap.length > 0 ? playerMap : map.grid).map((row, y) =>
row.map((_, x) => (
<div
key={`${x}-${y}`}
className="cursor-pointer"
onClick={() => handleCellClick(x, y)}
/>
)),
)}
</div>
</div>
)}
</div>
<div className="flex justify-center">
{!isSimulating && (
<Button onClick={runSimulation}>Run Simulation</Button>
)}
{isSimulating && <Button onClick={handleRetryClicked}>Retry</Button>}
</div>
</div>
);
}
37 changes: 37 additions & 0 deletions app/play/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
import { Map } from "@/app/map";
import { Button } from "@/components/ui/button";
import Link from "next/link";

export default function PlayPage() {
const maps = useQuery(api.maps.getMaps);

if (!maps) {
return <div>Loading...</div>;
}

return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-6 text-center">Choose a Level</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{maps.map((map) => (
<div
key={map._id}
className="border rounded-lg p-4 flex flex-col items-center justify-between h-full"
>
<div className="text-center">
<h2 className="text-xl font-semibold mb-4">Level {map.level}</h2>
<Map map={map.grid} />
</div>
<Link href={`/play/${map.level}`} passHref className="mt-auto pt-4">
<Button>Play</Button>
</Link>
</div>
))}
</div>
</div>
);
}

0 comments on commit 52471c2

Please sign in to comment.