-
Notifications
You must be signed in to change notification settings - Fork 33
Add Battle Arena: ELO-based competitive programming with 1v1/3v3 modes #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…routes, and UI components Co-authored-by: Hackeries <123194124+Hackeries@users.noreply.github.com>
Co-authored-by: Hackeries <123194124+Hackeries@users.noreply.github.com>
Co-authored-by: Hackeries <123194124+Hackeries@users.noreply.github.com>
Co-authored-by: Hackeries <123194124+Hackeries@users.noreply.github.com>
Co-authored-by: Hackeries <123194124+Hackeries@users.noreply.github.com>
Co-authored-by: Hackeries <123194124+Hackeries@users.noreply.github.com>
…nknown' Co-authored-by: Hackeries <123194124+Hackeries@users.noreply.github.com>
|
@copilot have you added to the sidebar?? |
Co-authored-by: Hackeries <123194124+Hackeries@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements Battle Arena, a competitive programming feature that enables real-time 1v1 problem-solving matches with ELO-based rankings, tier progression (Bronze through Master), and a monetization model separating free users (3 daily unranked matches) from Pro users (unlimited ranked matches). The implementation includes a complete database schema with 7 tables, matchmaking APIs, ELO calculation utilities, and responsive UI components.
Key Changes:
- Database migration adds arena infrastructure with RLS policies, helper functions for tier calculation and access control, and automatic rating initialization for existing users
- Backend APIs implement ELO-based matchmaking with dynamic ranges, leaderboard ranking, and match history tracking
- Frontend components provide lobby, leaderboard, and result screens with tier badges, streak tracking, and Pro/Free tier differentiation
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 27 comments.
Show a summary per file
| File | Description |
|---|---|
| types/arena.ts | Type definitions for all arena entities, game state, and ELO system with tier constants and utility functions |
| supabase/migrations/003_battle_arena.sql | Complete database schema with 7 tables, RLS policies, indexes, helper functions, and initial data seeding |
| supabase/migrations/README_ARENA.md | Migration guide with setup instructions, verification queries, and troubleshooting steps |
| lib/arena/elo.ts | ELO rating calculation utilities with K-factor adaptation, tier progression logic, and matchmaking range determination |
| docs/BATTLE_ARENA.md | Technical documentation covering architecture, API endpoints, components, and game mechanics |
| components/arena/result-screen.tsx | Post-match results UI showing ELO changes, tier promotions, titles earned, and problem breakdowns |
| components/arena/index.ts | Component exports for arena features |
| components/arena/arena-lobby.tsx | Main lobby UI for match type/mode selection, player stats display, and Pro/Free tier messaging |
| components/arena/arena-leaderboard.tsx | Leaderboard display with top 50 rankings, tier badges, and win rate statistics |
| app/arena/page.tsx | Arena page entry point with authentication, subscription checking, and rating initialization |
| app/api/arena/matchmaking/route.ts | Matchmaking endpoint with ELO-based pairing, problem selection, and match creation logic |
| app/api/arena/leaderboard/route.ts | Leaderboard API with flexible profile field mapping and ELO-based ranking |
| app/api/arena/history/route.ts | Match history endpoint returning user's past matches with pagination |
Comments suppressed due to low confidence (1)
supabase/migrations/003_battle_arena.sql:309
- The RLS policy 'arena_ratings_update_system' allows any authenticated user to update any arena rating record. The policy uses
USING (true)which means there are no restrictions on who can update ratings. This is a significant security vulnerability as users could manipulate their own or others' ELO ratings, match statistics, and win streaks. The comment indicates this is for "System/Edge Functions" but RLS policies don't distinguish between regular users and edge functions when both use authenticated connections. Consider using a service role key for system operations or implementing a more restrictive policy.
CREATE POLICY "arena_ratings_update_system"
ON arena_ratings FOR UPDATE
USING (true); -- System/Edge Functions update ratings
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| CREATE OR REPLACE FUNCTION get_tier_from_elo(elo INTEGER) | ||
| RETURNS VARCHAR(20) | ||
| LANGUAGE plpgsql | ||
| IMMUTABLE | ||
| AS $$ | ||
| BEGIN | ||
| IF elo < 1000 THEN | ||
| RETURN 'bronze'; | ||
| ELSIF elo < 1200 THEN | ||
| RETURN 'silver'; | ||
| ELSIF elo < 1400 THEN | ||
| RETURN 'gold'; | ||
| ELSIF elo < 1600 THEN | ||
| RETURN 'platinum'; | ||
| ELSIF elo < 1800 THEN | ||
| RETURN 'diamond'; | ||
| ELSE | ||
| RETURN 'master'; | ||
| END IF; | ||
| END; | ||
| $$; |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect usage of IMMUTABLE function classification. The get_tier_from_elo function is marked as IMMUTABLE, which tells PostgreSQL that the function always returns the same result for the same input and has no side effects. While this is technically true for this function, it's worth noting that if the tier thresholds ever need to be changed (e.g., rebalancing tiers), this function would need to be recreated or altered. Consider using STABLE instead if there's any chance the tier boundaries might be configuration-driven in the future, though IMMUTABLE is acceptable for hardcoded logic.
| // Try to find an existing waiting match | ||
| const { data: waitingMatches, error: matchError } = await supabase | ||
| .from('arena_matches') | ||
| .select('*, arena_ratings!inner(elo_1v1)') | ||
| .eq('match_type', '1v1') | ||
| .eq('mode', mode) | ||
| .eq('state', 'waiting') | ||
| .is('player2_id', null) | ||
| .neq('player1_id', user.id) | ||
| .gte('arena_ratings.elo_1v1', minElo) | ||
| .lte('arena_ratings.elo_1v1', maxElo) | ||
| .limit(1); | ||
|
|
||
| if (!matchError && waitingMatches && waitingMatches.length > 0) { | ||
| const match = waitingMatches[0]; | ||
|
|
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type safety issue with arena_ratings join syntax. The query on line 100 uses arena_ratings!inner(elo_1v1) which is Supabase PostgREST syntax for joining tables. However, there's no explicit foreign key defined between arena_matches.player1_id and arena_ratings.user_id in the schema. While the relationship exists logically, Supabase's automatic foreign key detection might not work without an explicit FOREIGN KEY constraint. This could cause the query to fail at runtime. Verify that this join syntax works, or restructure to use explicit filtering.
| // Try to find an existing waiting match | |
| const { data: waitingMatches, error: matchError } = await supabase | |
| .from('arena_matches') | |
| .select('*, arena_ratings!inner(elo_1v1)') | |
| .eq('match_type', '1v1') | |
| .eq('mode', mode) | |
| .eq('state', 'waiting') | |
| .is('player2_id', null) | |
| .neq('player1_id', user.id) | |
| .gte('arena_ratings.elo_1v1', minElo) | |
| .lte('arena_ratings.elo_1v1', maxElo) | |
| .limit(1); | |
| if (!matchError && waitingMatches && waitingMatches.length > 0) { | |
| const match = waitingMatches[0]; | |
| // Try to find an existing waiting match (without relying on implicit joins) | |
| const { data: waitingMatchesRaw, error: matchError } = await supabase | |
| .from('arena_matches') | |
| .select('*') | |
| .eq('match_type', '1v1') | |
| .eq('mode', mode) | |
| .eq('state', 'waiting') | |
| .is('player2_id', null) | |
| .neq('player1_id', user.id) | |
| .limit(10); | |
| let match = null as (typeof waitingMatchesRaw extends Array<infer T> ? T : null) | null; | |
| if (!matchError && waitingMatchesRaw && waitingMatchesRaw.length > 0) { | |
| // Explicitly filter by opponent ELO range using arena_ratings | |
| for (const candidate of waitingMatchesRaw) { | |
| // Ensure we have a player1_id to look up | |
| if (!candidate || !('player1_id' in candidate) || !candidate.player1_id) { | |
| continue; | |
| } | |
| const { data: rating, error: ratingError } = await supabase | |
| .from('arena_ratings') | |
| .select('elo_1v1') | |
| .eq('user_id', candidate.player1_id) | |
| .single(); | |
| if (!ratingError && rating && rating.elo_1v1 >= minElo && rating.elo_1v1 <= maxElo) { | |
| match = candidate; | |
| break; | |
| } | |
| } | |
| } | |
| if (match) { |
| WITH CHECK (true); -- Matchmaking system creates matches | ||
|
|
||
| CREATE POLICY "arena_matches_update_system" | ||
| ON arena_matches FOR UPDATE | ||
| USING (true); -- System updates match state |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar security issue with arena_matches RLS policies. The 'arena_matches_update_system' policy allows any authenticated user to update any match record using USING (true). This means users could potentially manipulate match states, winner IDs, ELO changes, and final scores. Since matches affect rating calculations and competitive integrity, this is a critical security vulnerability. System operations should use service role keys or implement policies that restrict updates based on specific conditions or roles.
| WITH CHECK (true); -- Matchmaking system creates matches | |
| CREATE POLICY "arena_matches_update_system" | |
| ON arena_matches FOR UPDATE | |
| USING (true); -- System updates match state | |
| WITH CHECK (auth.role() = 'service_role'); -- Matchmaking system creates matches | |
| CREATE POLICY "arena_matches_update_system" | |
| ON arena_matches FOR UPDATE | |
| USING (auth.role() = 'service_role'); -- System updates match state |
| CREATE POLICY "arena_players_update_own" | ||
| ON arena_players FOR UPDATE | ||
| USING (user_id = auth.uid()); |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The arena_players update policy allows users to update their own player records, but this could be exploited. A user could modify their own problems_solved, score, solve_times, or even activity_status to gain unfair advantages. For example, they could artificially inflate their score or mark problems as solved without actually solving them. Player state updates should be restricted to specific allowed fields (like last_seen_at) or should only be performable by backend systems using elevated privileges.
| CREATE POLICY "arena_players_update_own" | |
| ON arena_players FOR UPDATE | |
| USING (user_id = auth.uid()); | |
| -- Arena Players updates are restricted to backend systems using elevated privileges | |
| -- (service-role keys bypass RLS). End-users cannot directly update arena_players rows. | |
| CREATE POLICY "arena_players_update_system" | |
| ON arena_players FOR UPDATE | |
| USING (false) | |
| WITH CHECK (false); |
| current_problem_id VARCHAR(100), -- Currently working on | ||
| problems_solved JSONB DEFAULT '[]', -- Array of solved problem IDs | ||
| problems_attempted JSONB DEFAULT '[]', -- Array of attempted problem IDs | ||
|
|
||
| -- Fog of Progress status | ||
| activity_status VARCHAR(20) DEFAULT 'idle', -- idle, attempting, close, solved | ||
| last_activity_at TIMESTAMPTZ DEFAULT NOW(), | ||
|
|
||
| -- Locks (for 3v3) | ||
| locked_problem_id VARCHAR(100), -- Problem this player has locked | ||
| locked_at TIMESTAMPTZ, |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type inconsistency in problem IDs. The arena_players table uses VARCHAR(100) for current_problem_id and locked_problem_id, while the problems table likely uses UUID for problem IDs (standard practice). The match creation code uses problemIds: await selectMatchProblems(...) which returns string[], and the selectMatchProblems function maps p.id from the problems table. If problem IDs are UUIDs in the database but are being stored as VARCHAR, this creates a type mismatch. Consider using UUID type consistently or verifying that problem IDs are actually strings in the problems table schema.
| @@ -0,0 +1,287 @@ | |||
| 'use client'; | |||
|
|
|||
| import { useState, useEffect } from 'react'; | |||
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import useEffect.
| import { useState, useEffect } from 'react'; | |
| import { useState } from 'react'; |
| import { Button } from '@/components/ui/button'; | ||
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; | ||
| import { Badge } from '@/components/ui/badge'; | ||
| import { Progress } from '@/components/ui/progress'; |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import Progress.
| import { Progress } from '@/components/ui/progress'; |
|
|
||
| CREATE POLICY "arena_ratings_update_system" | ||
| ON arena_ratings FOR UPDATE | ||
| USING (true); -- System/Edge Functions update ratings |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The RLS policy arena_ratings_update_system uses USING (true), which means any role with UPDATE privileges (including regular authenticated clients in a typical Supabase setup) can modify any row in arena_ratings. An attacker can directly call UPDATE arena_ratings from the client to arbitrarily change their own or other users’ ELO, tiers, streaks, and titles, completely undermining leaderboard integrity. Restrict this update policy to a privileged context (for example, service-role or a dedicated claim) or to the owning user_id, and route all rating changes through trusted server-side logic instead of allowing arbitrary client writes.
| USING (true); -- System/Edge Functions update ratings | |
| USING (auth.role() = 'service_role') -- Restrict updates to service-role context | |
| WITH CHECK (auth.role() = 'service_role'); |
| CREATE POLICY "arena_teams_insert_system" | ||
| ON arena_teams FOR INSERT | ||
| WITH CHECK (true); -- Edge Functions create teams | ||
|
|
||
| -- Arena Matches: Public read for match discovery, system write | ||
| ALTER TABLE arena_matches ENABLE ROW LEVEL SECURITY; | ||
|
|
||
| CREATE POLICY "arena_matches_select_all" | ||
| ON arena_matches FOR SELECT | ||
| USING (true); | ||
|
|
||
| CREATE POLICY "arena_matches_insert_system" | ||
| ON arena_matches FOR INSERT | ||
| WITH CHECK (true); -- Matchmaking system creates matches | ||
|
|
||
| CREATE POLICY "arena_matches_update_system" | ||
| ON arena_matches FOR UPDATE | ||
| USING (true); -- System updates match state |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The arena_teams_insert_system, arena_matches_insert_system, and arena_matches_update_system policies all use WITH CHECK (true) / USING (true), so any authenticated client with basic table privileges can create or modify teams and matches directly. This lets an attacker forge matches (e.g., set themselves as winner, change mode, problem_ids, or state) and manipulate team compositions outside of the intended matchmaking Edge Functions, corrupting the authoritative game state. Tighten these policies so that only trusted backend roles (e.g., auth.role() = 'service_role' or a dedicated system claim) can insert/update these rows, preventing arbitrary client-side tampering with core arena match data.
| CREATE POLICY "arena_players_insert_system" | ||
| ON arena_players FOR INSERT | ||
| WITH CHECK (true); | ||
|
|
||
| CREATE POLICY "arena_players_update_own" | ||
| ON arena_players FOR UPDATE | ||
| USING (user_id = auth.uid()); | ||
|
|
||
| -- Arena Events: Can view events for matches they're in | ||
| ALTER TABLE arena_events ENABLE ROW LEVEL SECURITY; | ||
|
|
||
| CREATE POLICY "arena_events_select_own_match" | ||
| ON arena_events FOR SELECT | ||
| USING ( | ||
| EXISTS ( | ||
| SELECT 1 FROM arena_players ap | ||
| WHERE ap.match_id = arena_events.match_id | ||
| AND ap.user_id = auth.uid() | ||
| ) | ||
| ); | ||
|
|
||
| CREATE POLICY "arena_events_insert_system" | ||
| ON arena_events FOR INSERT | ||
| WITH CHECK (true); | ||
|
|
||
| -- Arena Match History: Users can only see their own history | ||
| ALTER TABLE arena_match_history ENABLE ROW LEVEL SECURITY; | ||
|
|
||
| CREATE POLICY "arena_match_history_select_own" | ||
| ON arena_match_history FOR SELECT | ||
| USING (auth.uid() = user_id); | ||
|
|
||
| CREATE POLICY "arena_match_history_insert_system" | ||
| ON arena_match_history FOR INSERT | ||
| WITH CHECK (true); |
Copilot
AI
Dec 22, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because arena_players_insert_system, arena_events_insert_system, and arena_match_history_insert_system all use WITH CHECK (true), any authenticated client can insert rows into arena_players, arena_events, and arena_match_history without restriction. Combined with the arena_players_select_own_match / arena_events_select_own_match policies, a malicious user can insert themselves into any existing match_id to gain read access to all player and event records for that match, and can also fabricate match-history rows for arbitrary user_ids, breaking both privacy and integrity of arena data. Restrict these insert policies to a privileged role or tie them to auth.uid() as appropriate so that only trusted server-side code can create authoritative player, event, and history records for a match.
Added Battlle Arena (3v3 , 1v1)