diff --git a/app/play/[level]/page.tsx b/app/play/[level]/page.tsx
index ff1be6e..8d57c47 100644
--- a/app/play/[level]/page.tsx
+++ b/app/play/[level]/page.tsx
@@ -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";
@@ -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 (
@@ -98,7 +105,7 @@ export default function PlayLevelPage({
setPlayerMap(newMap);
};
- const handlePlacementModeChange = (mode: "player" | "block") => {
+ const handlePlacementModeChange = async (mode: "player" | "block") => {
setPlacementMode(mode);
};
@@ -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 = () => {
@@ -230,6 +244,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 dcc04fa..db80928 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,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();
+ 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 (
@@ -44,12 +74,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/app/visualizer.tsx b/app/visualizer.tsx
index 78926bb..6ecdb5d 100644
--- a/app/visualizer.tsx
+++ b/app/visualizer.tsx
@@ -19,7 +19,7 @@ export function Visualizer({
controls?: boolean;
cellSize?: string;
map: string[][];
- onSimulationEnd?: (isWin: boolean) => void;
+ onSimulationEnd?: (isWin: boolean) => Promise;
}) {
const simulator = useRef(null);
const interval = useRef(null);
@@ -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;
@@ -52,7 +52,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 2808ecd..0b5050f 100644
--- a/convex/_generated/api.d.ts
+++ b/convex/_generated/api.d.ts
@@ -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";
@@ -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;
diff --git a/convex/playerresults.ts b/convex/playerresults.ts
new file mode 100644
index 0000000..ab3c2a3
--- /dev/null
+++ b/convex/playerresults.ts
@@ -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,
+ );
+
+ 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,
+ });
+ }
+ },
+});
diff --git a/convex/schema.ts b/convex/schema.ts
index 6ec248c..5d5a4f7 100644
--- a/convex/schema.ts
+++ b/convex/schema.ts
@@ -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"]),
});
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);
+ },
+});