Skip to content

feat: add multiplayer leaderboard with Convex backend#1

Draft
roomote-v0[bot] wants to merge 3 commits intomainfrom
feature/convex-leaderboard
Draft

feat: add multiplayer leaderboard with Convex backend#1
roomote-v0[bot] wants to merge 3 commits intomainfrom
feature/convex-leaderboard

Conversation

@roomote-v0
Copy link
Copy Markdown

@roomote-v0 roomote-v0 bot commented Feb 26, 2026

Opened by @roomote on behalf of John Herron

Adds a multiplayer leaderboard powered by Convex, allowing players to submit and view scores from Classic mode games.

Changes

Backend (Convex)

  • convex/schema.ts - Defines the leaderboard table with fields for playerName, tileset, longestCombo, isPerfectScore, and completedAt. Includes indexes for querying by tileset and by combo score.
  • convex/leaderboard.ts - Implements two functions:
    • getTopScores query - fetches top scores globally or filtered by tileset, sorted by longest combo (descending) then by completion time (ascending)
    • submitScore mutation - validates and inserts a new score entry (player name trimmed to 20 chars max)

Frontend

  • components/Leaderboard.tsx - New component showing a ranked leaderboard table with global/tileset filter tabs, rank indicators (1st/2nd/3rd highlighted), perfect score markers, and loading/empty states
  • components/VictoryModal.tsx - Updated to include a score submission form (name input + submit button) that appears after winning a Classic mode game. Player name is persisted in localStorage for convenience.
  • components/GameModeScreen.tsx - Added a "View Leaderboard" button below the mode selection cards
  • App.tsx - Wired leaderboard state, passes tilesetName/isClassicMode to VictoryModal, renders Leaderboard overlay
  • index.tsx - Wraps app with ConvexProvider when VITE_CONVEX_URL is set, falls back gracefully without it

Setup

  • Added convex dependency
  • Added vite-env.d.ts for Vite type support
  • Generated Convex types via npx convex dev --once

Setup Required

To activate the leaderboard, set VITE_CONVEX_URL in your environment (created automatically by npx convex dev).


View task on Roo Code Cloud

- Add Convex schema with leaderboard table (playerName, tileset, longestCombo, isPerfectScore, completedAt)
- Add Convex query (getTopScores) and mutation (submitScore) functions
- Create Leaderboard component with global/tileset filtering and top 20 scores
- Update VictoryModal with score submission form for Classic mode wins
- Add "View Leaderboard" button to GameModeScreen
- Wrap app with ConvexProvider (graceful fallback when VITE_CONVEX_URL not set)
- Add vite-env.d.ts for import.meta.env types
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
matchfield Ready Ready Preview, Comment Feb 26, 2026 8:10pm

@roomote-v0
Copy link
Copy Markdown
Author

roomote-v0 bot commented Feb 26, 2026

Rooviewer Clock   See task

Re-reviewed after 798f1c9 (leaderboard browser with settings discovery). The board selector UI is clean, but the 3 previously flagged issues remain unresolved and listBoards introduces a new scalability concern.

  • VictoryModal calls useMutation/useQuery without a ConvexProvider, crashing gameplay when Convex is not configured
  • Leaderboard calls useQuery without a ConvexProvider, crashing when "View Leaderboard" is clicked without Convex
  • submitScore mutation lacks server-side validation on longestCombo (accepts negative, fractional, Infinity, NaN)
  • listBoards does a full-table scan capped at 5000 rows; once exceeded, boards/counts/top combos become silently wrong
Previous reviews

Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues.

Comment thread components/VictoryModal.tsx Outdated
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import confetti from 'canvas-confetti';
import { useMutation } from 'convex/react';
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useMutation is called unconditionally here, but when VITE_CONVEX_URL is not set, index.tsx renders the app without a ConvexProvider. This will throw a "Could not find Convex client" error as soon as the component mounts during gameplay, crashing the app. The PR states the app "falls back gracefully" without the env var, but this hook call breaks that contract. You'd need to either conditionally render this component, or guard the hook usage (e.g., a wrapper component that checks for Convex availability).

Fix it with Roo Code or mention @roomote and request a fix.

@@ -0,0 +1,152 @@
import React, { useState } from "react";
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as VictoryModal - useQuery requires a ConvexProvider ancestor. When VITE_CONVEX_URL is not set and a user clicks "View Leaderboard" on the mode selection screen, this component mounts without a provider and crashes. Consider either hiding the leaderboard button when Convex is unavailable, or wrapping this component so the hook isn't called outside a provider.

Fix it with Roo Code or mention @roomote and request a fix.

Comment thread convex/leaderboard.ts
args: {
playerName: v.string(),
tileset: v.string(),
longestCombo: v.number(),
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

longestCombo is validated only as v.number(), so a malicious client can submit negative values, fractional numbers, Infinity, or NaN. Consider adding a check that the value is a non-negative integer (e.g., if (!Number.isInteger(args.longestCombo) || args.longestCombo < 0) throw ...) to prevent bogus leaderboard entries.

Fix it with Roo Code or mention @roomote and request a fix.

- Show leaderboard inline on the victory modal for both Classic and Custom modes
- Custom mode games get their own leaderboard per unique settings combination
- Settings hash only encodes diffs from defaults (future-proof, no versioning)
- Tileset removed from leaderboard grouping (cosmetic, separate menu already)
- Score submission available for all game modes (not just Classic)
- Leaderboard overlay filters by Classic vs current Custom settings hash
- Add listBoards query that aggregates all unique settings hashes with counts and top combos
- Leaderboard overlay now shows clickable tabs for every board that has scores
- Classic board always listed first, then sorted by popularity
- Each tab shows a tooltip with score count and top combo
- Pre-selects the relevant board based on current game context
Comment thread convex/leaderboard.ts
Comment on lines +39 to +41
const all = await ctx.db.query("leaderboard").take(5000);

const map = new Map<
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

listBoards does an unindexed full-table scan capped at 5000 rows. Since Convex queries are reactive, every score insertion triggers a re-scan for all connected clients. More importantly, once the table exceeds 5000 entries the results become silently wrong: boards with only newer entries disappear, counts are underreported, and topCombo values may miss the actual best score. Consider aggregating board metadata at write time (e.g. a separate boards table updated in submitScore) so this query reads a small, bounded dataset instead of scanning the full leaderboard.

Fix it with Roo Code or mention @roomote and request a fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant