Skip to content

Commit

Permalink
Merge pull request #67 from Ashutoshbind15/main
Browse files Browse the repository at this point in the history
Add the winstate, # of players who beat a map, and the user tries functionality
  • Loading branch information
webdevcody authored Oct 17, 2024
2 parents 570e20e + 306e94d commit 2f25397
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 11 deletions.
47 changes: 43 additions & 4 deletions app/play/[level]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState, useEffect } from "react";
import { useQuery } from "convex/react";
import { useEffect, useState } from "react";
import { useMutation, useQuery, useAction, Authenticated } from "convex/react";
import { api } from "@/convex/_generated/api";
import { Button } from "@/components/ui/button";
import { Visualizer } from "../../visualizer";
Expand Down Expand Up @@ -35,6 +35,13 @@ export default function PlayLevelPage({
}
}, [map]);

const userResultMutation = useMutation(api.playerresults.updateUserResult);
const user = useQuery(api.users.getUserOrNull);

const tries = useQuery(api.playerresults.getPlayerRecordsForAMap, {
mapId: map?._id,
});

if (!map) {
return (
<div className="container mx-auto min-h-screen flex flex-col items-center py-12 pb-24 gap-8">
Expand Down Expand Up @@ -98,7 +105,7 @@ export default function PlayLevelPage({
setPlayerMap(newMap);
};

const handlePlacementModeChange = (mode: "player" | "block") => {
const handlePlacementModeChange = async (mode: "player" | "block") => {
setPlacementMode(mode);
};

Expand All @@ -113,8 +120,15 @@ export default function PlayLevelPage({
setGameResult(null);
};

const handleSimulationEnd = (isWin: boolean) => {
const handleSimulationEnd = async (isWin: boolean) => {
setGameResult(isWin ? "WON" : "LOST");
if (user && user._id) {
await userResultMutation({
mapId: map._id,
hasWon: isWin,
placedGrid: playerMap,
});
}
};

const handleClearMap = () => {
Expand Down Expand Up @@ -230,6 +244,31 @@ export default function PlayLevelPage({
</Button>
)}
</div>

<Authenticated>
{tries && tries.attempts && tries.attempts.length > 0 && (
<>
<div className="font-semibold text-2xl mt-4">Tries</div>
<div className="flex flex-wrap items-center justify-around w-full">
{tries.attempts.map((attempt) => (
<div
key={attempt?._id}
className="flex flex-col gap-y-2 items-center"
>
{attempt?.grid && <Map map={attempt.grid} />}
<div
className={`mt-4 text-xl font-semibold ${
attempt?.didWin ? "text-green-500" : "text-red-500"
}`}
>
{attempt?.didWin ? "You Survived!" : "You Died!"}
</div>
</div>
))}
</div>
</>
)}
</Authenticated>
</>
) : (
<TestMode level={level} map={map.grid} />
Expand Down
61 changes: 57 additions & 4 deletions app/play/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client";

import { useQuery } from "convex/react";
import { Authenticated, Unauthenticated, useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
import { Map } from "@/app/map";
import { Map as GameMap } from "@/app/map";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import {
Expand All @@ -12,10 +12,40 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useEffect, useState } from "react";
import { Skeleton } from "@/components/ui/skeleton";

export default function PlayPage() {
const maps = useQuery(api.maps.getMaps);
const userMapResults = useQuery(api.playerresults.getUserMapStatus);
const mapCountResults = useQuery(api.playerresults.getMapsWins);

const [resMap, setResMap] = useState(new Map());
const [countMap, setCountMap] = useState(new Map());

useEffect(() => {
if (userMapResults && mapCountResults) {
const res = new Map<string, boolean>();
const ctr = new Map<string, number>();

for (const result of userMapResults as {
mapId: string;
hasWon: boolean;
}[]) {
res.set(result.mapId, result.hasWon);
}

for (const result of mapCountResults as {
mapId: string;
count: number;
}[]) {
ctr.set(result.mapId, result.count);
}

setResMap(res);
setCountMap(ctr);
}
}, [userMapResults, mapCountResults]);

if (!maps) {
return (
Expand Down Expand Up @@ -44,12 +74,35 @@ export default function PlayPage() {
</CardTitle>
</CardHeader>
<CardContent className="flex-grow flex items-center justify-center">
<Map map={map.grid} />
<GameMap map={map.grid} />
</CardContent>
<CardFooter className="flex justify-center">
<CardFooter className="flex justify-around px-3">
<Link href={`/play/${map.level}`} passHref>
<Button>Play</Button>
</Link>

<Authenticated>
<div className="flex flex-col items-center gap-y-2">
{resMap.has(map._id) ? (
<div className="ml-4">
{resMap.get(map._id) ? "Beaten" : "Unbeaten"}
</div>
) : (
<div className="ml-4">Unplayed</div>
)}
<div>
Won by {countMap.has(map._id) ? countMap.get(map._id) : 0}{" "}
Players
</div>
</div>
</Authenticated>

<Unauthenticated>
<div>
Won by {countMap.has(map._id) ? countMap.get(map._id) : 0}{" "}
Players
</div>
</Unauthenticated>
</CardFooter>
</Card>
))}
Expand Down
6 changes: 3 additions & 3 deletions app/visualizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function Visualizer({
controls?: boolean;
cellSize?: string;
map: string[][];
onSimulationEnd?: (isWin: boolean) => void;
onSimulationEnd?: (isWin: boolean) => Promise<void>;
}) {
const simulator = useRef<ZombieSurvival | null>(null);
const interval = useRef<NodeJS.Timeout | null>(null);
Expand All @@ -35,7 +35,7 @@ export function Visualizer({
setMapState(simulator.current!.getState());
setIsRunning(true);

interval.current = setInterval(() => {
interval.current = setInterval(async () => {
if (simulator.current!.finished()) {
clearInterval(interval.current!);
interval.current = null;
Expand All @@ -52,7 +52,7 @@ export function Visualizer({
setIsRunning(false);

if (onSimulationEnd) {
onSimulationEnd(!simulator.current!.getPlayer().dead());
await onSimulationEnd(!simulator.current!.getPlayer().dead());
}

return;
Expand Down
2 changes: 2 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type * as init from "../init.js";
import type * as leaderboard from "../leaderboard.js";
import type * as maps from "../maps.js";
import type * as models from "../models.js";
import type * as playerresults from "../playerresults.js";
import type * as results from "../results.js";
import type * as scores from "../scores.js";
import type * as users from "../users.js";
Expand All @@ -48,6 +49,7 @@ declare const fullApi: ApiFromModules<{
leaderboard: typeof leaderboard;
maps: typeof maps;
models: typeof models;
playerresults: typeof playerresults;
results: typeof results;
scores: typeof scores;
users: typeof users;
Expand Down
133 changes: 133 additions & 0 deletions convex/playerresults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { getAuthUserId } from "@convex-dev/auth/server";

export const getUserMapStatus = query({
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);

if (!userId) {
return [];
}
const res = await ctx.db
.query("userResults")
.filter((q) => q.eq(q.field("userId"), userId))
.collect();

return res.map((r) => {
return {
mapId: r.mapId,
hasWon: r.hasWon,
};
});
},
});

export const getMapsWins = query({
handler: async ({ db }) => {
const wonCounts = await db
.query("userResults")
.filter((q) => q.eq(q.field("hasWon"), true)) // Only users who have won
.collect();

// Format the results as a count per map
const mapWinCounts = wonCounts.reduce(
(counts, result) => {
const mapId = result.mapId;
if (mapId) {
counts[mapId] = (counts[mapId] || 0) + 1;
}
return counts;
},
{} as Record<string, number>,
);

const res = [];

for (const [mapId, count] of Object.entries(mapWinCounts)) {
res.push({ mapId, count });
}

return res;
},
});

export const getPlayerRecordsForAMap = query({
args: {
mapId: v.optional(v.id("maps")),
},
handler: async (ctx, { mapId }) => {
const userId = await getAuthUserId(ctx);

if (!userId) {
return null;
}

if (!mapId) {
return {};
}

const res = await ctx.db
.query("userResults")
.withIndex("by_mapId_userId", (q) =>
q.eq("mapId", mapId).eq("userId", userId),
)
.collect();

if (res.length === 0) {
return null;
}

const resPopulated = Promise.all(
(res[0].attempts ?? []).map((attemptId) => ctx.db.get(attemptId)),
);

const resolvedAttempts = await resPopulated;

return {
hasWon: res[0].hasWon,
attempts: resolvedAttempts,
};
},
});

export const updateUserResult = mutation({
args: {
mapId: v.id("maps"),
hasWon: v.boolean(),
placedGrid: v.array(v.array(v.string())),
},
handler: async (ctx, { mapId, hasWon, placedGrid }) => {
const userId = await getAuthUserId(ctx);

if (userId == null) {
throw new Error("Not signed in");
}

const res = await ctx.db
.query("userResults")
.withIndex("by_mapId_userId", (q) =>
q.eq("mapId", mapId).eq("userId", userId),
)
.collect();

const attemptId = await ctx.db.insert("attempts", {
grid: placedGrid,
didWin: hasWon,
});

if (res.length === 0) {
await ctx.db.insert("userResults", {
userId,
mapId,
attempts: [attemptId],
hasWon,
});
} else {
await ctx.db.patch(res[0]._id, {
attempts: [...res[0].attempts, attemptId],
hasWon: res[0].hasWon || hasWon,
});
}
},
});
10 changes: 10 additions & 0 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,14 @@ export default defineSchema({
wins: v.number(),
losses: v.number(),
}).index("by_modelId_level", ["modelId", "level"]),
attempts: defineTable({
grid: v.array(v.array(v.string())),
didWin: v.boolean(),
}),
userResults: defineTable({
userId: v.id("users"),
mapId: v.id("maps"),
attempts: v.array(v.id("attempts")),
hasWon: v.boolean(),
}).index("by_mapId_userId", ["mapId", "userId"]),
});
11 changes: 11 additions & 0 deletions convex/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,14 @@ export const viewer = query({
return user;
},
});

export const getUserOrNull = query({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (userId === null) {
return null;
}
return await ctx.db.get(userId);
},
});

0 comments on commit 2f25397

Please sign in to comment.