From 89ac5ec5cefd18f2dea24d4bb84eddb91a91db1d Mon Sep 17 00:00:00 2001 From: Ashutoshbind15 Date: Thu, 17 Oct 2024 00:08:39 +0530 Subject: [PATCH] add a leaderboard for the model simulations --- app/header.tsx | 3 + app/leaderboard/page.tsx | 147 +++++++++++++++++++++++++++++++++++++ components/ui/table.tsx | 120 ++++++++++++++++++++++++++++++ components/ui/tabs.tsx | 55 ++++++++++++++ convex/_generated/api.d.ts | 2 + convex/leaderboard.ts | 86 ++++++++++++++++++++++ convex/maps.ts | 6 ++ convex/results.ts | 6 ++ convex/schema.ts | 11 +++ package.json | 1 + 10 files changed, 437 insertions(+) create mode 100644 app/leaderboard/page.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 convex/leaderboard.ts diff --git a/app/header.tsx b/app/header.tsx index 876038e..d23676e 100644 --- a/app/header.tsx +++ b/app/header.tsx @@ -39,6 +39,9 @@ export default function Header() { + + +
diff --git a/app/leaderboard/page.tsx b/app/leaderboard/page.tsx new file mode 100644 index 0000000..5ca9dbc --- /dev/null +++ b/app/leaderboard/page.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { api } from "@/convex/_generated/api"; +import { useQuery } from "convex/react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +// Define the types for the data +interface Ranking { + _id: string; + modelId: string; + level?: string; + wins: number; + losses: number; +} + +interface Stats { + wins: number; + losses: number; + total: number; + ratio: number; +} + +const LeaderBoard = () => { + const globalRanking = useQuery(api.leaderboard.getGlobalRankings) as + | Ranking[] + | undefined; + const levelRanking = useQuery(api.leaderboard.getLevelRankings) as + | Ranking[] + | undefined; + + // Transform the levelRanking data into a pivot table structure + const pivotLevelData = (levelRanking: Ranking[] | undefined) => { + const levels: Record> = {}; + + levelRanking?.forEach((item) => { + if (!levels[item.level!]) { + levels[item.level!] = {}; + } + + levels[item.level!][item.modelId] = { + wins: item.wins, + losses: item.losses, + total: item.wins + item.losses, + ratio: item.wins / (item.wins + item.losses), + }; + }); + + return levels; + }; + + const pivotedLevelData = pivotLevelData(levelRanking); + + // Get all unique model IDs to dynamically create columns + const allModels = Array.from( + new Set(levelRanking?.map((item) => item.modelId)), + ); + + return ( +
+
Leaderboard
+ + + + Global Rankings + Map based Rankings + + + {/* Global Rankings Table */} + + The global model realtime tally. + + + Model ID + Number of Wins + Number of Losses + Total Games + Ratio + + + + {globalRanking?.map((item) => ( + + {item.modelId} + {item.wins} + {item.losses} + {item.wins + item.losses} + + {(item.wins / (item.wins + item.losses)).toFixed(2)} + + + ))} + +
+
+ + {/* Map-based Rankings Pivoted Table */} + + The models map-based tally per level. + + + Level + {/* Dynamically render column headers for each model */} + {allModels.map((modelId) => ( + {modelId} + ))} + + + + {/* Render rows for each level */} + {Object.entries(pivotedLevelData).map(([level, models]) => ( + + {level} + {/* Render stats for each model in the columns */} + {allModels.map((modelId) => { + const stats = models[modelId] || { + wins: 0, + losses: 0, + total: 0, + ratio: 0, + }; + return ( + + Wins: {stats.wins}, Losses: {stats.losses}, Games:{" "} + {stats.total}, Ratio: {stats.ratio.toFixed(2)} + + ); + })} + + ))} + +
+
+
+
+ ); +}; + +export default LeaderBoard; diff --git a/components/ui/table.tsx b/components/ui/table.tsx new file mode 100644 index 0000000..c0df655 --- /dev/null +++ b/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx new file mode 100644 index 0000000..0f4caeb --- /dev/null +++ b/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index a4e3ff9..7918494 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -20,6 +20,7 @@ import type * as constants from "../constants.js"; import type * as games from "../games.js"; 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 results from "../results.js"; import type * as scores from "../scores.js"; @@ -39,6 +40,7 @@ declare const fullApi: ApiFromModules<{ games: typeof games; http: typeof http; init: typeof init; + leaderboard: typeof leaderboard; maps: typeof maps; results: typeof results; scores: typeof scores; diff --git a/convex/leaderboard.ts b/convex/leaderboard.ts new file mode 100644 index 0000000..5f35ca3 --- /dev/null +++ b/convex/leaderboard.ts @@ -0,0 +1,86 @@ +import { v } from "convex/values"; +import { internalMutation, query } from "./_generated/server"; + +export const getGlobalRankings = query({ + handler: async ({ db }) => { + const res = await db.query("globalrankings").collect(); + // Sort the results by wins/losses ratio + + const sortedResults = res.sort((a, b) => { + if (a.wins / (a.wins + a.losses) > b.wins / (b.wins + b.losses)) { + return -1; + } + if (a.wins / (a.wins + a.losses) < b.wins / (b.wins + b.losses)) { + return 1; + } + return 0; + }); + + return sortedResults; + }, +}); + +export const getLevelRankings = query({ + handler: async ({ db }) => { + const res = await db.query("levelrankings").collect(); + + const sortedResults = res.sort((a, b) => { + if (a.level < b.level) { + return -1; + } + if (a.level > b.level) { + return 1; + } + return 0; + }); + + return sortedResults; + }, +}); + +export const updateRankings = internalMutation({ + args: { + modelId: v.string(), + level: v.number(), + isWin: v.boolean(), + }, + handler: async (ctx, args) => { + const globalRanking = await ctx.db + .query("globalrankings") + .withIndex("by_modelId", (q) => q.eq("modelId", args.modelId)) + .collect(); + const levelRanking = await ctx.db + .query("levelrankings") + .withIndex("by_modelId_level", (q) => + q.eq("modelId", args.modelId).eq("level", args.level), + ) + .collect(); + + if (globalRanking.length === 0) { + await ctx.db.insert("globalrankings", { + modelId: args.modelId, + wins: args.isWin ? 1 : 0, + losses: args.isWin ? 0 : 1, + }); + } else { + await ctx.db.patch(globalRanking[0]._id, { + wins: globalRanking[0].wins + (args.isWin ? 1 : 0), + losses: globalRanking[0].losses + (args.isWin ? 0 : 1), + }); + } + + if (levelRanking.length === 0) { + await ctx.db.insert("levelrankings", { + modelId: args.modelId, + level: args.level, + wins: args.isWin ? 1 : 0, + losses: args.isWin ? 0 : 1, + }); + } else { + await ctx.db.patch(levelRanking[0]._id, { + wins: levelRanking[0].wins + (args.isWin ? 1 : 0), + losses: levelRanking[0].losses + (args.isWin ? 0 : 1), + }); + } + }, +}); diff --git a/convex/maps.ts b/convex/maps.ts index d9d2b4c..f814c8c 100644 --- a/convex/maps.ts +++ b/convex/maps.ts @@ -170,6 +170,12 @@ export const playMapAction = internalAction({ resultId, error: errorMessage, }); + + await ctx.runMutation(internal.leaderboard.updateRankings, { + modelId: args.modelId, + level: args.level, + isWin: false, + }); } }, }); diff --git a/convex/results.ts b/convex/results.ts index 630dedc..6ac8d44 100644 --- a/convex/results.ts +++ b/convex/results.ts @@ -81,6 +81,12 @@ export const updateResult = internalMutation({ throw new Error("Game not found"); } + await ctx.runMutation(internal.leaderboard.updateRankings, { + modelId: game.modelId, + level: result.level, + isWin: args.isWin, + }); + if (args.isWin) { await ctx.runMutation(internal.scores.incrementScore, { modelId: game.modelId, diff --git a/convex/schema.ts b/convex/schema.ts index 4083f79..e9ee6d3 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -34,4 +34,15 @@ export default defineSchema({ v.literal("failed"), ), }).index("by_gameId_level", ["gameId", "level"]), + globalrankings: defineTable({ + modelId: v.string(), + wins: v.number(), + losses: v.number(), + }).index("by_modelId", ["modelId"]), + levelrankings: defineTable({ + modelId: v.string(), + level: v.number(), + wins: v.number(), + losses: v.number(), + }).index("by_modelId_level", ["modelId", "level"]), }); diff --git a/package.json b/package.json index b257df4..15f5054 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0",