From 31834dac8413d1febb35f2a24618a87d62b12092 Mon Sep 17 00:00:00 2001 From: Ashutoshbind15 Date: Thu, 17 Oct 2024 10:34:06 +0530 Subject: [PATCH 1/2] basic logic to save user gameplay --- app/play/[level]/page.tsx | 17 +++++-- app/visualizer.tsx | 6 +-- convex/_generated/api.d.ts | 2 + convex/playerresults.ts | 101 +++++++++++++++++++++++++++++++++++++ convex/schema.ts | 10 ++++ convex/users.ts | 11 ++++ 6 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 convex/playerresults.ts diff --git a/app/play/[level]/page.tsx b/app/play/[level]/page.tsx index a9535c3..a1087c8 100644 --- a/app/play/[level]/page.tsx +++ b/app/play/[level]/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { useQuery } from "convex/react"; +import { useMutation, useQuery } from "convex/react"; import { api } from "@/convex/_generated/api"; import { Button } from "@/components/ui/button"; import { Visualizer } from "../../visualizer"; @@ -24,6 +24,9 @@ export default function PlayLevelPage({ ); const [blockCount, setBlockCount] = useState(0); + const userResultMutation = useMutation(api.playerresults.updateUserResult); + const user = useQuery(api.users.getUserOrNull); + if (!map) { return
Loading...
; } @@ -68,7 +71,7 @@ export default function PlayLevelPage({ setPlayerMap(newMap); }; - const handlePlacementModeChange = (mode: "player" | "block") => { + const handlePlacementModeChange = async (mode: "player" | "block") => { setPlacementMode(mode); }; @@ -83,8 +86,16 @@ 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({ + userId: user?._id, + mapId: map._id, + hasWon: isWin, + placedGrid: playerMap, + }); + } }; const mapWidth = diff --git a/app/visualizer.tsx b/app/visualizer.tsx index 06a2dbe..38b2438 100644 --- a/app/visualizer.tsx +++ b/app/visualizer.tsx @@ -16,7 +16,7 @@ export function Visualizer({ autoStart?: boolean; controls?: boolean; map: string[][]; - onSimulationEnd?: (isWin: boolean) => void; + onSimulationEnd?: (isWin: boolean) => Promise; }) { const simulator = useRef(null); const interval = useRef(null); @@ -32,7 +32,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; @@ -49,7 +49,7 @@ export function Visualizer({ setIsRunning(false); if (onSimulationEnd) { - onSimulationEnd(!simulator.current!.getPlayer().dead()); + await onSimulationEnd(!simulator.current!.getPlayer().dead()); } return; diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 7918494..913c6fc 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -22,6 +22,7 @@ import type * as http from "../http.js"; import type * as init from "../init.js"; import type * as leaderboard from "../leaderboard.js"; import type * as maps from "../maps.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"; @@ -42,6 +43,7 @@ declare const fullApi: ApiFromModules<{ init: typeof init; leaderboard: typeof leaderboard; maps: typeof maps; + playerresults: typeof playerresults; results: typeof results; scores: typeof scores; users: typeof users; diff --git a/convex/playerresults.ts b/convex/playerresults.ts new file mode 100644 index 0000000..f1639ef --- /dev/null +++ b/convex/playerresults.ts @@ -0,0 +1,101 @@ +import { v } from "convex/values"; +import { mutation, query } from "./_generated/server"; + +export const getUserMapStatus = query({ + args: { + userId: v.id("users"), + mapId: v.id("maps"), + }, + handler: async ({ db }, { userId, mapId }) => { + const res = await db + .query("userResults") + .withIndex("by_mapId_userId", (q) => + q.eq("mapId", mapId).eq("userId", userId), + ) + .collect(); + return res[0].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, + ); + + return mapWinCounts; + }, +}); + +export const getPlayerRecordsForAMap = query({ + args: { + userId: v.id("users"), + mapId: v.id("maps"), + }, + handler: async ({ db }, { userId, mapId }) => { + const res = await db + .query("userResults") + .withIndex("by_mapId_userId", (q) => + q.eq("mapId", mapId).eq("userId", userId), + ) + .collect(); + + const resPopulated = Promise.all( + (res[0].attempts ?? []).map((attemptId) => db.get(attemptId)), + ); + + return { + ...res[0], + attempts: resPopulated, + }; + }, +}); + +export const updateUserResult = mutation({ + args: { + userId: v.id("users"), + mapId: v.id("maps"), + hasWon: v.boolean(), + placedGrid: v.array(v.array(v.string())), + }, + handler: async (ctx, { userId, mapId, hasWon, placedGrid }) => { + 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, + }); + } + }, +}); diff --git a/convex/schema.ts b/convex/schema.ts index 7f950f1..769fe1a 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -43,4 +43,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"]), }); diff --git a/convex/users.ts b/convex/users.ts index bf62374..e991574 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -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); + }, +}); From 2ee4027b6abaa6779d38cb4bbda77131a09f346f Mon Sep 17 00:00:00 2001 From: Ashutoshbind15 Date: Thu, 17 Oct 2024 20:47:27 +0530 Subject: [PATCH 2/2] add the client side code for user tries, players won before and some improvements in corresponding queries and mutations --- app/play/[level]/page.tsx | 32 +++++++++++++++-- app/play/page.tsx | 61 +++++++++++++++++++++++++++++--- convex/_generated/api.d.ts | 4 +-- convex/playerresults.ts | 72 +++++++++++++++++++++++++++----------- 4 files changed, 141 insertions(+), 28 deletions(-) diff --git a/app/play/[level]/page.tsx b/app/play/[level]/page.tsx index 5075f49..e158f3d 100644 --- a/app/play/[level]/page.tsx +++ b/app/play/[level]/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { useMutation, useQuery, useAction } from "convex/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"; @@ -43,6 +43,10 @@ export default function PlayLevelPage({ const userResultMutation = useMutation(api.playerresults.updateUserResult); const user = useQuery(api.users.getUserOrNull); + const tries = useQuery(api.playerresults.getPlayerRecordsForAMap, { + mapId: map?._id, + }); + if (!map) { return
Loading...
; } @@ -106,7 +110,6 @@ export default function PlayLevelPage({ setGameResult(isWin ? "WON" : "LOST"); if (user && user._id) { await userResultMutation({ - userId: user?._id, mapId: map._id, hasWon: isWin, placedGrid: playerMap, @@ -252,6 +255,31 @@ export default function PlayLevelPage({ )} + + + {tries && tries.attempts && tries.attempts.length > 0 && ( + <> +
Tries
+
+ {tries.attempts.map((attempt) => ( +
+ {attempt?.grid && } +
+ {attempt?.didWin ? "You Survived!" : "You Died!"} +
+
+ ))} +
+ + )} +
) : (
diff --git a/app/play/page.tsx b/app/play/page.tsx index 2b2e21e..ee9d604 100644 --- a/app/play/page.tsx +++ b/app/play/page.tsx @@ -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 { @@ -12,9 +12,39 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { useEffect, useState } from "react"; 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(); + const ctr = new Map(); + + 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
Loading...
; @@ -33,12 +63,35 @@ export default function PlayPage() { - + - + + + +
+ {resMap.has(map._id) ? ( +
+ {resMap.get(map._id) ? "Beaten" : "Unbeaten"} +
+ ) : ( +
Unplayed
+ )} +
+ Won by {countMap.has(map._id) ? countMap.get(map._id) : 0}{" "} + Players +
+
+
+ + +
+ Won by {countMap.has(map._id) ? countMap.get(map._id) : 0}{" "} + Players +
+
))} diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 6d595a1..0b5050f 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -24,8 +24,8 @@ import type * as http from "../http.js"; import type * as init from "../init.js"; import type * as leaderboard from "../leaderboard.js"; import type * as maps from "../maps.js"; -import type * as playerresults from "../playerresults.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"; @@ -48,8 +48,8 @@ declare const fullApi: ApiFromModules<{ init: typeof init; leaderboard: typeof leaderboard; maps: typeof maps; - playerresults: typeof playerresults; models: typeof models; + playerresults: typeof playerresults; results: typeof results; scores: typeof scores; users: typeof users; diff --git a/convex/playerresults.ts b/convex/playerresults.ts index f1639ef..ab3c2a3 100644 --- a/convex/playerresults.ts +++ b/convex/playerresults.ts @@ -1,19 +1,25 @@ import { v } from "convex/values"; import { mutation, query } from "./_generated/server"; +import { getAuthUserId } from "@convex-dev/auth/server"; export const getUserMapStatus = query({ - args: { - userId: v.id("users"), - mapId: v.id("maps"), - }, - handler: async ({ db }, { userId, mapId }) => { - const res = await db + handler: async (ctx) => { + const userId = await getAuthUserId(ctx); + + if (!userId) { + return []; + } + const res = await ctx.db .query("userResults") - .withIndex("by_mapId_userId", (q) => - q.eq("mapId", mapId).eq("userId", userId), - ) + .filter((q) => q.eq(q.field("userId"), userId)) .collect(); - return res[0].hasWon; + + return res.map((r) => { + return { + mapId: r.mapId, + hasWon: r.hasWon, + }; + }); }, }); @@ -36,42 +42,68 @@ export const getMapsWins = query({ {} as Record, ); - return mapWinCounts; + const res = []; + + for (const [mapId, count] of Object.entries(mapWinCounts)) { + res.push({ mapId, count }); + } + + return res; }, }); export const getPlayerRecordsForAMap = query({ args: { - userId: v.id("users"), - mapId: v.id("maps"), + mapId: v.optional(v.id("maps")), }, - handler: async ({ db }, { userId, mapId }) => { - const res = await db + 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) => db.get(attemptId)), + (res[0].attempts ?? []).map((attemptId) => ctx.db.get(attemptId)), ); + const resolvedAttempts = await resPopulated; + return { - ...res[0], - attempts: resPopulated, + hasWon: res[0].hasWon, + attempts: resolvedAttempts, }; }, }); export const updateUserResult = mutation({ args: { - userId: v.id("users"), mapId: v.id("maps"), hasWon: v.boolean(), placedGrid: v.array(v.array(v.string())), }, - handler: async (ctx, { userId, mapId, hasWon, placedGrid }) => { + 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) =>