From cb2c990d9d0f0978b3188a0adc11b1bd59f27c9b Mon Sep 17 00:00:00 2001 From: POSTI-25 Date: Mon, 9 Feb 2026 03:13:47 +0530 Subject: [PATCH 1/9] /assisstant and /memory routes --- app/api/assistant/route.ts | 103 ++++++++++++++++++++ app/api/memory/route.ts | 195 +++++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 app/api/assistant/route.ts create mode 100644 app/api/memory/route.ts diff --git a/app/api/assistant/route.ts b/app/api/assistant/route.ts new file mode 100644 index 0000000..516e204 --- /dev/null +++ b/app/api/assistant/route.ts @@ -0,0 +1,103 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { BackboardClient } from 'backboard-sdk'; + +const bb = new BackboardClient({ + apiKey: process.env.BACKBOARD_API_KEY!, +}); + +// Type for non-streamed message responses +type BBMessageResponse = { + content: string; + status: string; + toolCalls: Array<{ id: string; function: { name: string; parsed_arguments: any } }> | null; + runId?: string; + threadId: string; + memoryOperationId?: string; +}; + +// Mock/Placeholder for database operations +const mockMatchData = { + getMatch: (matchId: string) => ({ + id: matchId, + threadId: null as string | null // Replace with real DB lookup + }), + updateMatchThread: (matchId: string, threadId: string) => { + console.log(`Mock DB: Saved thread ${threadId} for match ${matchId}`); + } +}; + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { matchId, code, language, userId } = body; + + if (!matchId || !code || !language || !userId) { + return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + } + + // 1. Get or Create Assistant (Logic simplified) + // In production, store ASSISTANT_ID in an env var to avoid re-creating + const assistant = await bb.createAssistant({ + name: "Compile-n-Conquer GM", + system_prompt: "You are a competitive coding Game Master. Analyze complexity and efficiency.", + tools: [{ + type: "function", + function: { + name: "analyze_code_complexity", + description: "Analyze Big O complexity", + parameters: { + type: "object", + properties: { + complexity: { type: "string" }, + score: { type: "number" } + } + } + } + }] + }); + + // 2. Thread Management + const match = mockMatchData.getMatch(matchId); + let threadId: string = match.threadId ?? ''; + + if (!threadId) { + const thread = await bb.createThread(assistant.assistantId); + threadId = thread.threadId; + mockMatchData.updateMatchThread(matchId, threadId); + } + + // 3. Send Message & Handle Interaction + let response = await bb.addMessage(threadId, { + content: `User ${userId} submitted ${language} code:\n\`\`\`\n${code}\n\`\`\``, + llm_provider: "openai", + model_name: "gpt-4o", + stream: false + }) as BBMessageResponse; + + // 4. Handle Tool Calls (If LLM triggers the analyzer) + if (response.status === "REQUIRES_ACTION" && response.toolCalls) { + const toolOutputs = response.toolCalls.map((tc) => ({ + tool_call_id: tc.id, + output: JSON.stringify({ complexity: "O(n)", score: 85 }) // Mock analysis logic + })); + + const toolResponse = await bb.submitToolOutputs( + threadId, + response.runId!, + toolOutputs, + false + ) as BBMessageResponse; + response = toolResponse; + } + + return NextResponse.json({ + success: true, + evaluation: response.content, + threadId + }); + + } catch (error: any) { + console.error('API Error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/memory/route.ts b/app/api/memory/route.ts new file mode 100644 index 0000000..49c5fab --- /dev/null +++ b/app/api/memory/route.ts @@ -0,0 +1,195 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { BackboardClient } from 'backboard-sdk'; + +// Type for non-streamed message responses +type BBMessageResponse = { + content: string; + status: string; + threadId: string; + memoryOperationId?: string; +}; + +// Mock/Placeholder for user database operations (replace with actual Prisma calls later) +const mockUserData = { + getUserProfile: (userId: string) => ({ + id: userId, + username: `user_${userId}`, + pastPerformance: "Student at VIT, intermediate competitive programmer", + commonErrors: [ + "Off-by-one errors in array indexing", + "Inefficient nested loops leading to O(n²) complexity", + "Missing edge case handling for empty inputs" + ], + preferredAlgorithms: [ + "Dynamic Programming", + "Binary Search", + "Two Pointers technique" + ], + skillLevel: "intermediate", + totalMatches: 15, + winRate: 0.67, + averageComplexity: "O(n log n)", + learningGoals: [ + "Improve graph algorithm understanding", + "Master advanced DP patterns", + "Reduce time complexity in solutions" + ] + }), + updateUserMemorySession: (userId: string, memoryData: any) => { + console.log(`Mock: Updated memory session for user ${userId}`, memoryData); + return { success: true, lastUpdated: new Date().toISOString() }; + } +}; + +export async function POST(req: NextRequest) { + try { + // Parse request body + const body = await req.json(); + const { userId, action, memoryContext } = body; + + // Validate required fields + if (!userId || !action) { + return NextResponse.json( + { error: 'Missing required fields: userId, action' }, + { status: 400 } + ); + } + + // Validate action type + if (!['update', 'initialize', 'retrieve'].includes(action)) { + return NextResponse.json( + { error: 'Invalid action. Must be: update, initialize, or retrieve' }, + { status: 400 } + ); + } + + if (!process.env.BACKBOARD_API_KEY) { + return NextResponse.json( + { error: 'Backboard API key not configured' }, + { status: 500 } + ); + } + + // Initialize Backboard client + const client = new BackboardClient({ + apiKey: process.env.BACKBOARD_API_KEY + }); + + // Create or retrieve assistant for memory management + const assistant = await client.createAssistant({ + name: "Compile-n-Conquer Player Profile Manager", + system_prompt: `You are the Player Profile Manager for Compile-n-Conquer. + You maintain persistent memory about each player's: + - Coding patterns and preferred algorithms + - Common mistakes and learning areas + - Performance history and skill progression + - Personalized coaching recommendations + + Use this information to provide personalized feedback and adaptive difficulty. + Always maintain context about the player's journey and growth areas.` + }); + + // Mock: Get user profile data (replace with actual Prisma call) + const userProfile = mockUserData.getUserProfile(userId); + + let result; + + switch (action) { + case 'initialize': + case 'update': + // Create a thread for memory management + const thread = await client.createThread(assistant.assistantId); + + // Prepare memory context + const contextToStore = memoryContext || { + userProfile, + timestamp: new Date().toISOString(), + sessionType: 'profile_update' + }; + + // Build comprehensive memory message + const memoryMessage = ` +Player Profile Update for User ${userId}: + +**Background:** ${userProfile.pastPerformance} + +**Skill Assessment:** +- Level: ${userProfile.skillLevel} +- Total Matches: ${userProfile.totalMatches} +- Win Rate: ${(userProfile.winRate * 100).toFixed(1)}% +- Average Complexity: ${userProfile.averageComplexity} + +**Common Challenges:** +${userProfile.commonErrors.map(error => `- ${error}`).join('\n')} + +**Strengths & Preferences:** +${userProfile.preferredAlgorithms.map(algo => `- ${algo}`).join('\n')} + +**Learning Goals:** +${userProfile.learningGoals.map(goal => `- ${goal}`).join('\n')} + +Remember this context for personalized coaching and adaptive difficulty in future interactions. +`; + + // Store memory with Backboard's memory feature + const memoryResponse = await client.addMessage(thread.threadId, { + content: memoryMessage, + memory: "Auto", // Enable automatic memory saving + stream: false + }) as BBMessageResponse; + + // Mock: Update local database with memory session info + const updateResult = mockUserData.updateUserMemorySession(userId, { + threadId: thread.threadId, + memoryOperationId: memoryResponse.memoryOperationId, + contextStored: contextToStore + }); + + result = { + action: 'updated', + userId, + threadId: thread.threadId, + memoryOperationId: memoryResponse.memoryOperationId, + userProfile, + updateResult + }; + break; + + case 'retrieve': + // Create a new thread to test memory recall + const retrieveThread = await client.createThread(assistant.assistantId); + + // Query what the assistant remembers about this user + const recallResponse = await client.addMessage(retrieveThread.threadId, { + content: `What do you remember about user ${userId}? Provide a comprehensive summary of their profile, strengths, weaknesses, and learning journey.`, + memory: "Auto", // Search and retrieve saved memories + stream: false + }) as BBMessageResponse; + + result = { + action: 'retrieved', + userId, + threadId: retrieveThread.threadId, + memorizedProfile: recallResponse.content, + currentProfile: userProfile + }; + break; + } + + return NextResponse.json({ + success: true, + ...result, + timestamp: new Date().toISOString() + }); + + } catch (error) { + console.error('Memory API Error:', error); + return NextResponse.json( + { + error: 'Failed to process memory operation', + details: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} \ No newline at end of file From 0baea99af286c0e16753b3865668806b5de14324 Mon Sep 17 00:00:00 2001 From: POSTI-25 Date: Mon, 9 Feb 2026 16:03:46 +0530 Subject: [PATCH 2/9] backboard architecture --- ARCHITECTURE.md | 316 +++++++++++ app/arena/page.tsx | 69 +++ app/auth/page.tsx | 155 ++++++ app/globals.css | 122 ++++- app/layout.tsx | 41 +- app/page.tsx | 76 +-- app/profile/page.tsx | 186 +++++++ backbaord/evaluation.ts | 583 ++++++++++++++++++++ backbaord/index.ts | 9 + backbaord/memory.ts | 204 +++++++ backbaord/mock-test.ts | 869 ++++++++++++++++++++++++++++++ backbaord/testing.ts | 39 ++ backbaord/tools.ts | 223 ++++++++ backbaord/types.ts | 174 ++++++ components/arena/code-editor.tsx | 257 +++++++++ components/arena/points-panel.tsx | 157 ++++++ components/arena/progress-bar.tsx | 109 ++++ components/arena/users-panel.tsx | 110 ++++ components/landing/features.tsx | 81 +++ components/landing/footer.tsx | 40 ++ components/landing/game-modes.tsx | 126 +++++ components/landing/hero.tsx | 124 +++++ components/landing/navbar.tsx | 95 ++++ components/theme-provider.tsx | 11 + components/ui/accordion.tsx | 58 ++ components/ui/alert-dialog.tsx | 141 +++++ components/ui/alert.tsx | 59 ++ components/ui/aspect-ratio.tsx | 7 + components/ui/avatar.tsx | 50 ++ components/ui/badge.tsx | 37 ++ components/ui/breadcrumb.tsx | 115 ++++ components/ui/button.tsx | 57 ++ components/ui/calendar.tsx | 66 +++ components/ui/card.tsx | 79 +++ components/ui/carousel.tsx | 262 +++++++++ components/ui/chart.tsx | 365 +++++++++++++ components/ui/checkbox.tsx | 30 ++ components/ui/collapsible.tsx | 11 + components/ui/command.tsx | 153 ++++++ components/ui/context-menu.tsx | 200 +++++++ components/ui/dialog.tsx | 122 +++++ components/ui/drawer.tsx | 118 ++++ components/ui/dropdown-menu.tsx | 200 +++++++ components/ui/form.tsx | 178 ++++++ components/ui/hover-card.tsx | 29 + components/ui/input-otp.tsx | 71 +++ components/ui/input.tsx | 22 + components/ui/label.tsx | 26 + components/ui/menubar.tsx | 236 ++++++++ components/ui/navigation-menu.tsx | 128 +++++ components/ui/pagination.tsx | 117 ++++ components/ui/popover.tsx | 31 ++ components/ui/progress.tsx | 28 + components/ui/radio-group.tsx | 44 ++ components/ui/resizable.tsx | 45 ++ components/ui/scroll-area.tsx | 48 ++ components/ui/select.tsx | 160 ++++++ components/ui/separator.tsx | 31 ++ components/ui/sheet.tsx | 141 +++++ components/ui/sidebar.tsx | 771 ++++++++++++++++++++++++++ components/ui/skeleton.tsx | 15 + components/ui/slider.tsx | 28 + components/ui/sonner.tsx | 31 ++ components/ui/switch.tsx | 29 + components/ui/table.tsx | 117 ++++ components/ui/tabs.tsx | 55 ++ components/ui/textarea.tsx | 22 + components/ui/toast.tsx | 129 +++++ components/ui/toaster.tsx | 35 ++ components/ui/toggle-group.tsx | 61 +++ components/ui/toggle.tsx | 45 ++ components/ui/tooltip.tsx | 30 ++ components/ui/use-mobile.tsx | 19 + components/ui/use-toast.ts | 191 +++++++ hooks/use-mobile.tsx | 19 + hooks/use-toast.ts | 191 +++++++ lib/utils.ts | 6 + package-lock.json | 227 +++++++- package.json | 7 +- tailwind.config.ts | 89 +++ 80 files changed, 9640 insertions(+), 118 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 app/arena/page.tsx create mode 100644 app/auth/page.tsx create mode 100644 app/profile/page.tsx create mode 100644 backbaord/evaluation.ts create mode 100644 backbaord/index.ts create mode 100644 backbaord/memory.ts create mode 100644 backbaord/mock-test.ts create mode 100644 backbaord/testing.ts create mode 100644 backbaord/tools.ts create mode 100644 backbaord/types.ts create mode 100644 components/arena/code-editor.tsx create mode 100644 components/arena/points-panel.tsx create mode 100644 components/arena/progress-bar.tsx create mode 100644 components/arena/users-panel.tsx create mode 100644 components/landing/features.tsx create mode 100644 components/landing/footer.tsx create mode 100644 components/landing/game-modes.tsx create mode 100644 components/landing/hero.tsx create mode 100644 components/landing/navbar.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/aspect-ratio.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/calendar.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/chart.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/context-menu.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/drawer.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input-otp.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/menubar.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/pagination.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/resizable.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/toggle-group.tsx create mode 100644 components/ui/toggle.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 components/ui/use-mobile.tsx create mode 100644 components/ui/use-toast.ts create mode 100644 hooks/use-mobile.tsx create mode 100644 hooks/use-toast.ts create mode 100644 lib/utils.ts create mode 100644 tailwind.config.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..0481c87 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,316 @@ +# Compile-n-Conquer — Evaluation Pipeline Architecture + +## Overview + +A competitive coding platform where users compete in rounds by solving coding questions. Submissions are evaluated by an LLM pipeline (via Backboard.io) across multiple metrics, scored with configurable weights, and ranked with tiebreaker logic. + +--- + +## Core Concepts + +| Term | Definition | +|---|---| +| **Round** | A competitive session containing N questions | +| **Submission** | A single user's code answer to one question | +| **Evaluation Thread** | A dedicated Backboard.io LLM thread for one scoring metric | +| **Memory** | Persistent Backboard.io memory tracking user skill level over time | + +--- + +## Input Format + +All submissions for a round are batched into a structured payload: + +```json +{ + "roundId": "round_1", + "questions": [ + { "id": "q1", "title": "Two Sum", "description": "...", "difficulty": "medium" }, + { "id": "q2", "title": "...", "description": "...", "difficulty": "..." }, + { "id": "q3", "title": "...", "description": "...", "difficulty": "..." } + ], + "submissions": [ + { + "userId": "user_1", + "responses": [ + { "questionId": "q1", "code": "...", "language": "python", "submittedAt": "ISO-timestamp" }, + { "questionId": "q2", "code": "...", "language": "cpp", "submittedAt": "ISO-timestamp" }, + { "questionId": "q3", "code": "...", "language": "javascript", "submittedAt": "ISO-timestamp" } + ] + }, + { + "userId": "user_2", + "responses": [/* ... */] + }, + { + "userId": "user_3", + "responses": [/* ... */] + } + ] +} +``` + +--- + +## Evaluation Pipeline (4 LLM Threads via Backboard.io) + +### Thread 1 — Time Complexity Analysis + +**Purpose:** Determine the Big-O time complexity for every submission. + +**Backboard Tool Definition:** +``` +name: "analyze_time_complexity" +description: "Analyze and return the Big-O time complexity of the submitted code relative to the problem it solves. Return the complexity class and a numeric efficiency score (1-100) where 100 is optimal." +parameters: + - code (string, required): The submitted source code + - language (string, required): Programming language + - questionContext (string, required): The problem statement for context +output: + - complexity (string): e.g. "O(n)", "O(n log n)", "O(n²)" + - score (number 1-100): Efficiency rating + - reasoning (string): Brief justification +``` + +**Output Format:** +```json +{ + "roundId": "round_1", + "metric": "time_complexity", + "results": { + "user_1": [ + { "questionId": "q1", "complexity": "O(n)", "score": 92, "reasoning": "..." }, + { "questionId": "q2", "complexity": "O(n log n)", "score": 85, "reasoning": "..." }, + { "questionId": "q3", "complexity": "O(n²)", "score": 40, "reasoning": "..." } + ], + "user_2": [/* ... */], + "user_3": [/* ... */] + } +} +``` + +--- + +### Thread 2 — Space Complexity Analysis + +**Purpose:** Determine the Big-O space complexity for every submission. + +**Backboard Tool Definition:** +``` +name: "analyze_space_complexity" +description: "Analyze and return the Big-O space complexity of the submitted code. Consider auxiliary space used (not input space). Return the complexity class and a numeric efficiency score (1-100)." +parameters: + - code (string, required): The submitted source code + - language (string, required): Programming language + - questionContext (string, required): The problem statement for context +output: + - complexity (string): e.g. "O(1)", "O(n)", "O(n²)" + - score (number 1-100): Efficiency rating + - reasoning (string): Brief justification +``` + +**Output Format:** Same structure as Thread 1, with `"metric": "space_complexity"`. + +--- + +### Thread 3 — Originality & Context Relevance Analysis + +**Purpose:** Detect likely GPT-generated / copy-pasted code and evaluate how relevant the solution is to the actual question. + +**Scoring Criteria:** +- **Comment density:** Excessive/formulaic comments → likely AI-generated (penalize) +- **Code style consistency:** Unusual variable naming patterns, overly verbose docstrings +- **Relevance:** Does the code actually solve the given problem, or is it tangential? +- **Pattern detection:** Boilerplate patterns common in LLM outputs + +**Backboard Tool Definition:** +``` +name: "analyze_originality" +description: "Analyze code for originality signals. Check comment density (high = likely AI-generated), code style patterns, and relevance to the problem. Return an originality score (1-10) and a relevance score (1-10)." +parameters: + - code (string, required): The submitted source code + - language (string, required): Programming language + - questionContext (string, required): The problem statement +output: + - originalityScore (number 1-10): 10 = clearly human-written, 1 = clearly AI-generated + - relevanceScore (number 1-10): 10 = perfectly solves the problem, 1 = completely unrelated + - commentDensity (number): Comments-to-code ratio percentage + - flags (string[]): e.g. ["excessive_comments", "boilerplate_docstrings", "generic_variable_names"] + - reasoning (string): Brief justification +``` + +**Output Format:** +```json +{ + "roundId": "round_1", + "metric": "originality", + "results": { + "user_1": [ + { + "questionId": "q1", + "originalityScore": 8, + "relevanceScore": 9, + "commentDensity": 12.5, + "flags": [], + "reasoning": "..." + } + ] + } +} +``` + +--- + +### Thread 4 — Final Ranking (Aggregation Thread) + +**Purpose:** Consume the outputs of Threads 1-3, apply weights, integrate test-case pass rates from DB, and produce final rankings. + +**Backboard Tool Definition:** +``` +name: "compute_final_ranking" +description: "Given all metric scores for all users in a round, compute weighted final scores and produce a ranking. Apply the provided weights. In case of a tie, use the tiebreaker metric (time taken)." +parameters: + - timeComplexityResults (object, required): Output from Thread 1 + - spaceComplexityResults (object, required): Output from Thread 2 + - originalityResults (object, required): Output from Thread 3 + - testCaseResults (object, required): { userId: { passed: N, total: M } } from DB + - weights (object, required): { timeComplexity, spaceComplexity, originality, testCases } + - tiebreakerData (object, required): { userId: timeTakenMs } for each user +output: + - rankings (array): Ordered list of { userId, rank, finalScore, breakdown } +``` + +--- + +## Scoring Formula + +``` +finalScore = (TC_score × W_tc) + (SC_score × W_sc) + (originality_score × W_orig) + (testCase_score × W_test) +``` + +**Default Weights:** +| Metric | Weight | Max Raw Score | Normalized To | +|---|---|---|---| +| Time Complexity | 25% | 100 per question | 0-25 | +| Space Complexity | 20% | 100 per question | 0-20 | +| Originality + Relevance | 15% | 10 per question | 0-15 | +| Test Cases Passed | 40% | 100% pass rate | 0-40 | + +**Total Max Score:** 100 per question, averaged across all questions in the round. + +**Tiebreaker:** If two users have the same final score, the user who submitted in less total time ranks higher. + +--- + +## Memory System (`/memory`) + +Tracks persistent user profiles across rounds for adaptive difficulty and skill classification. + +**Skill Tiers:** +| Tier | Criteria | +|---|---| +| Beginner | Avg score < 30, struggles with basic DS | +| Intermediate | Avg score 30-60, consistent O(n²) or better | +| Advanced | Avg score 60-85, uses optimal approaches often | +| Pro | Avg score 85+, consistently optimal TC/SC | + +**Memory stores:** +- Historical round scores and progression trend +- Common error patterns (off-by-one, TLE, MLE) +- Preferred algorithms and languages +- Skill tier + promotion/demotion history +- Personalized coaching recommendations + +**Promotion Logic:** +- 3 consecutive rounds with avg score in next tier → promote +- 2 consecutive rounds below current tier threshold → demote +- Memory is queried at round start to set adaptive difficulty + +--- + +## API Endpoints + +### `POST /api/assistant` — Evaluation Pipeline + +**Request:** +```json +{ + "roundId": "round_1", + "action": "evaluate", + "submissions": [/* full round submissions payload */], + "weights": { "timeComplexity": 0.25, "spaceComplexity": 0.20, "originality": 0.15, "testCases": 0.40 }, + "testCaseResults": { "user_1": { "passed": 8, "total": 10 }, ... }, + "tiebreakerData": { "user_1": 145000, ... } +} +``` + +**Response:** +```json +{ + "success": true, + "roundId": "round_1", + "evaluationId": "eval_xxx", + "rankings": [ + { + "rank": 1, + "userId": "user_2", + "finalScore": 87.5, + "breakdown": { + "timeComplexity": { "avgScore": 90, "weighted": 22.5 }, + "spaceComplexity": { "avgScore": 85, "weighted": 17.0 }, + "originality": { "avgScore": 8.5, "weighted": 12.75 }, + "testCases": { "passRate": 0.9, "weighted": 36.0 } + }, + "timeTakenMs": 120000 + } + ], + "threadIds": { + "timeComplexity": "thread_tc_xxx", + "spaceComplexity": "thread_sc_xxx", + "originality": "thread_orig_xxx", + "ranking": "thread_rank_xxx" + } +} +``` + +### `POST /api/memory` — Player Profile Memory + +**Actions:** `initialize`, `update`, `retrieve`, `classify` + +--- + +## Execution Flow Diagram + +``` +Round Ends → Collect All Submissions + │ + ├──→ Thread 1: Time Complexity Analysis ──→ Store TC Results + ├──→ Thread 2: Space Complexity Analysis ──→ Store SC Results + ├──→ Thread 3: Originality Analysis ────────→ Store Orig Results + │ (Threads 1-3 run in parallel) + │ + ▼ + Thread 4: Aggregation & Ranking + │ + ├── Fetch test case pass rates from DB + ├── Apply scoring weights + ├── Handle tiebreakers (time taken) + │ + ▼ + Final Rankings Published + │ + ▼ + /memory: Update user profiles & skill tiers +``` + +--- + +## Mock Testing Strategy + +All evaluation logic is testable without live Backboard API calls: + +1. **Unit tests for scoring math** — Weight application, normalization, tiebreaker logic +2. **Mock LLM responses** — Predefined tool call outputs for each thread +3. **End-to-end pipeline test** — Full round with 3 mock users, 3 questions, verify ranking order +4. **Edge cases** — Tied scores, empty submissions, single user, all-zero scores +5. **Memory classification test** — Verify tier promotion/demotion with historical mock data diff --git a/app/arena/page.tsx b/app/arena/page.tsx new file mode 100644 index 0000000..ca6d747 --- /dev/null +++ b/app/arena/page.tsx @@ -0,0 +1,69 @@ +"use client" + +import { ProgressBar } from "@/components/arena/progress-bar" +import { CodeEditor } from "@/components/arena/code-editor" +import { UsersPanel } from "@/components/arena/users-panel" +import { PointsPanel } from "@/components/arena/points-panel" +import { Terminal, ArrowLeft } from "lucide-react" +import Link from "next/link" + +export default function ArenaPage() { + return ( +
+ {/* Top bar */} +
+
+ + + +
+ + + CnC Arena + +
+
+
+
+
+ + Live + +
+ + Mode: Code from Scratch + + + Y + +
+
+ + {/* Progress bar section */} +
+ +
+ + {/* Main content area */} +
+ {/* Code editor - center/left */} +
+ +
+ + {/* Right sidebar: Users + Points */} +
+ + +
+
+
+ ) +} diff --git a/app/auth/page.tsx b/app/auth/page.tsx new file mode 100644 index 0000000..d9b2a96 --- /dev/null +++ b/app/auth/page.tsx @@ -0,0 +1,155 @@ +"use client" + +import { Terminal, ArrowLeft } from "lucide-react" +import Link from "next/link" +import { useState } from "react" + +export default function AuthPage() { + const [isLoading, setIsLoading] = useState(false) + + const handleGoogleSignIn = () => { + setIsLoading(true) + // Mock: In production, redirect to Supabase Google OAuth + setTimeout(() => { + setIsLoading(false) + }, 2000) + } + + return ( +
+ {/* Grid background */} +
+ + {/* Scanline */} +
+
+
+ +
+ {/* Back link */} + + + Back to Home + + + {/* Auth card */} +
+ {/* Header */} +
+
+ +
+
+

+ {"Access Terminal"} +

+

+ {"// authenticate to enter the arena"} +

+
+
+ + {/* Terminal-style status */} +
+
+ {"$"} + {"system.auth --status"} +
+
+ {">"} + {"Awaiting credentials..."} + {"_"} +
+
+ + {/* Google Sign In button */} + + + {/* Divider */} +
+
+ {"or"} +
+
+ + {/* Username/Email mock fields */} +
+
+ + +
+
+ + +
+ +
+ + {/* Footer text */} +

+ {"New operative? "} + + {"Request Access"} + +

+
+
+
+ ) +} diff --git a/app/globals.css b/app/globals.css index a2dc41e..8f13be6 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,114 @@ @import "tailwindcss"; -:root { - --background: #ffffff; - --foreground: #171717; +@theme { + --color-background: hsl(120 5% 3%); + --color-foreground: hsl(120 100% 85%); + --color-card: hsl(120 5% 5%); + --color-card-foreground: hsl(120 100% 85%); + --color-popover: hsl(120 5% 5%); + --color-popover-foreground: hsl(120 100% 85%); + --color-primary: hsl(120 100% 50%); + --color-primary-foreground: hsl(120 5% 3%); + --color-secondary: hsl(120 5% 10%); + --color-secondary-foreground: hsl(120 100% 75%); + --color-muted: hsl(120 3% 12%); + --color-muted-foreground: hsl(120 10% 50%); + --color-accent: hsl(120 100% 50%); + --color-accent-foreground: hsl(120 5% 3%); + --color-destructive: hsl(0 80% 50%); + --color-destructive-foreground: hsl(0 0% 98%); + --color-border: hsl(120 15% 15%); + --color-input: hsl(120 10% 12%); + --color-ring: hsl(120 100% 50%); + --color-chart-1: hsl(120 100% 50%); + --color-chart-2: hsl(120 80% 40%); + --color-chart-3: hsl(120 60% 30%); + --color-chart-4: hsl(45 100% 50%); + --color-chart-5: hsl(0 80% 50%); + --color-sidebar: hsl(120 5% 5%); + --color-sidebar-foreground: hsl(120 100% 85%); + --color-sidebar-primary: hsl(120 100% 50%); + --color-sidebar-primary-foreground: hsl(120 5% 3%); + --color-sidebar-accent: hsl(120 5% 10%); + --color-sidebar-accent-foreground: hsl(120 100% 85%); + --color-sidebar-border: hsl(120 15% 15%); + --color-sidebar-ring: hsl(120 100% 50%); + --radius-sm: calc(0.375rem - 4px); + --radius-md: calc(0.375rem - 2px); + --radius-lg: 0.375rem; + --radius-xl: calc(0.375rem + 4px); } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); +* { + border-color: var(--color-border); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +body { + background-color: var(--color-background); + color: var(--color-foreground); } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +/* Scanline effect */ +@keyframes scanline { + 0% { transform: translateY(-100%); } + 100% { transform: translateY(100vh); } +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 5px hsl(120 100% 50% / 0.3); } + 50% { box-shadow: 0 0 20px hsl(120 100% 50% / 0.6), 0 0 40px hsl(120 100% 50% / 0.2); } +} + +@keyframes typing { + from { width: 0; } + to { width: 100%; } +} + +@keyframes flicker { + 0%, 100% { opacity: 1; } + 92% { opacity: 1; } + 93% { opacity: 0.8; } + 94% { opacity: 1; } + 96% { opacity: 0.9; } + 97% { opacity: 1; } +} + +.animate-scanline { + animation: scanline 8s linear infinite; +} + +.animate-blink { + animation: blink 1s step-end infinite; +} + +.animate-pulse-glow { + animation: pulse-glow 2s ease-in-out infinite; +} + +.animate-flicker { + animation: flicker 4s linear infinite; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: hsl(120 5% 5%); +} + +::-webkit-scrollbar-thumb { + background: hsl(120 100% 50% / 0.3); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: hsl(120 100% 50% / 0.5); } diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..0741ec6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,34 +1,31 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; +import React from "react" +import type { Metadata, Viewport } from 'next' +import { JetBrains_Mono } from 'next/font/google' -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); +import './globals.css' -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); +const jetbrainsMono = JetBrains_Mono({ + subsets: ['latin'], + variable: '--font-geist-mono', +}) export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; + title: 'CompileNConquer - Competitive Coding Arena', + description: 'A multiplayer competitive coding game that blends problem-solving depth with real-time racing excitement.', +} + +export const viewport: Viewport = { + themeColor: '#00ff41', +} export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode }>) { return ( - - - {children} - + + {children} - ); + ) } diff --git a/app/page.tsx b/app/page.tsx index 295f8fd..69a2aaa 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,65 +1,17 @@ -import Image from "next/image"; +import { Navbar } from "@/components/landing/navbar" +import { Hero } from "@/components/landing/hero" +import { Features } from "@/components/landing/features" +import { GameModes } from "@/components/landing/game-modes" +import { Footer } from "@/components/landing/footer" -export default function Home() { +export default function Page() { return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
- ); +
+ + + + +
+
+ ) } diff --git a/app/profile/page.tsx b/app/profile/page.tsx new file mode 100644 index 0000000..2d0f980 --- /dev/null +++ b/app/profile/page.tsx @@ -0,0 +1,186 @@ +import { Terminal, ArrowLeft, Trophy, Zap, Code, Target, Clock, TrendingUp } from "lucide-react" +import Link from "next/link" + +const profile = { + name: "you", + rating: 1847, + rank: "#342", + title: "Code Warrior", + joined: "Jan 2026", + totalBattles: 128, + wins: 87, + losses: 41, + winRate: "68%", + totalXP: 24850, + streak: 7, +} + +const stats = [ + { label: "Battles", value: "128", icon: Target }, + { label: "Win Rate", value: "68%", icon: TrendingUp }, + { label: "XP", value: "24,850", icon: Zap }, + { label: "Streak", value: "7", icon: Trophy }, +] + +const recentMatches = [ + { opponent: "n3x_byte", result: "WIN", points: "+175", mode: "Scratch", date: "2 hrs ago" }, + { opponent: "c0d3_wr4ith", result: "LOSS", points: "-50", mode: "Debug", date: "5 hrs ago" }, + { opponent: "z3r0_day", result: "WIN", points: "+200", mode: "Reverse", date: "1 day ago" }, + { opponent: "algo_gh0st", result: "WIN", points: "+150", mode: "Scratch", date: "1 day ago" }, + { opponent: "byteB1aster", result: "WIN", points: "+180", mode: "Debug", date: "2 days ago" }, +] + +const skillBreakdown = [ + { skill: "DSA", level: 78, color: "hsl(120 100% 50%)" }, + { skill: "Clean Code", level: 85, color: "hsl(45 100% 50%)" }, + { skill: "Speed", level: 62, color: "hsl(200 100% 50%)" }, + { skill: "Debugging", level: 71, color: "hsl(0 80% 55%)" }, + { skill: "Efficiency", level: 90, color: "hsl(120 80% 60%)" }, +] + +export default function ProfilePage() { + return ( +
+ {/* Top bar */} +
+
+
+ + + +
+ + + Profile Terminal + +
+
+ + Enter Arena + +
+
+ +
+ {/* Profile header */} +
+
+ Y +
+
+
+

{profile.name}

+ + {profile.title} + +
+
+ ELO: {profile.rating} + Global Rank: {profile.rank} + Joined: {profile.joined} +
+
+
+ + {/* Stats grid */} +
+ {stats.map((stat) => ( +
+
+ + + {stat.label} + +
+ {stat.value} +
+ ))} +
+ +
+ {/* Skill breakdown */} +
+
+ + + Skill Breakdown + +
+
+ {skillBreakdown.map((s) => ( +
+
+ {s.skill} + {s.level}% +
+
+
+
+
+ ))} +
+
+ + {/* Recent matches */} +
+
+ + + Recent Matches + +
+
+ {recentMatches.map((match, i) => ( +
+
+ + {match.result} + +
+ + vs {match.opponent} + + {match.mode} +
+
+
+ + {match.points} XP + + {match.date} +
+
+ ))} +
+
+
+
+
+ ) +} diff --git a/backbaord/evaluation.ts b/backbaord/evaluation.ts new file mode 100644 index 0000000..8ecff07 --- /dev/null +++ b/backbaord/evaluation.ts @@ -0,0 +1,583 @@ +// ============================================================ +// Compile-n-Conquer — Evaluation Pipeline Engine +// ============================================================ +// Orchestrates the 4-thread LLM evaluation pipeline via Backboard.io + +import { BackboardClient } from "backboard-sdk"; +import { + RoundSubmissions, + TimeComplexityResults, + SpaceComplexityResults, + OriginalityResults, + ComplexityResult, + OriginalityResult, + TestCaseResults, + ScoringWeights, + TiebreakerData, + RankedUser, + RoundEvaluation, + ScoreBreakdown, + EvaluationConfig, + DEFAULT_EVAL_CONFIG, +} from "./types"; +import { + TIME_COMPLEXITY_TOOL, + SPACE_COMPLEXITY_TOOL, + ORIGINALITY_TOOL, + RANKING_TOOL, + SYSTEM_PROMPTS, +} from "./tools"; + +// Type for Backboard message responses +type BBMessageResponse = { + content: string; + status: string; + toolCalls: Array<{ + id: string; + function: { name: string; parsedArguments: any }; + }> | null; + runId?: string; + threadId: string; +}; + +// ---- Helper: Build the batch prompt for a metric thread ---- + +function buildBatchPrompt( + round: RoundSubmissions, + metricLabel: string +): string { + let prompt = `## Round: ${round.roundId} — ${metricLabel} Analysis\n\n`; + prompt += `### Questions in this round:\n`; + for (const q of round.questions) { + prompt += `- **${q.id}** (${q.difficulty}): ${q.title}\n ${q.description}\n\n`; + } + + prompt += `### Submissions:\n\n`; + for (const sub of round.submissions) { + prompt += `#### User: ${sub.userId}\n`; + for (const resp of sub.responses) { + const question = round.questions.find((q) => q.id === resp.questionId); + prompt += `**Question ${resp.questionId}** (${resp.language}):\n`; + prompt += "```\n" + resp.code + "\n```\n\n"; + prompt += `Call the tool for this submission with questionContext: "${question?.description || question?.title || resp.questionId}"\n\n`; + } + } + + prompt += `\nAnalyze ALL submissions above. Call the tool once per submission.\n`; + return prompt; +} + +// ---- Thread 1: Time Complexity ---- + +export async function evaluateTimeComplexity( + client: BackboardClient, + round: RoundSubmissions, + config: EvaluationConfig = DEFAULT_EVAL_CONFIG +): Promise<{ results: TimeComplexityResults; threadId: string }> { + const assistant = await client.createAssistant({ + name: "CnC Time Complexity Analyzer", + system_prompt: SYSTEM_PROMPTS.timeComplexity, + tools: [TIME_COMPLEXITY_TOOL], + }); + + const thread = await client.createThread(assistant.assistantId); + const prompt = buildBatchPrompt(round, "Time Complexity"); + + let response = (await client.addMessage(thread.threadId, { + content: prompt, + llm_provider: config.llmProvider, + model_name: config.modelName, + stream: false, + })) as BBMessageResponse; + + // Handle tool calls — collect all TC results + const results: Record = {}; + for (const sub of round.submissions) { + results[sub.userId] = []; + } + + while (response.status === "REQUIRES_ACTION" && response.toolCalls) { + const toolOutputs = []; + + for (const tc of response.toolCalls) { + if (tc.function.name === "analyze_time_complexity") { + const args = tc.function.parsedArguments; + + // The LLM analyzed it — we acknowledge and capture + // In production, we could run our own static analysis here too + const analysisResult: ComplexityResult = { + questionId: args.questionContext || "unknown", + complexity: args.complexity || "unknown", + score: args.score || 50, + reasoning: args.reasoning || "No reasoning provided", + }; + + toolOutputs.push({ + tool_call_id: tc.id, + output: JSON.stringify({ + status: "recorded", + ...analysisResult, + }), + }); + } + } + + response = (await client.submitToolOutputs( + thread.threadId, + response.runId!, + toolOutputs, + false + )) as BBMessageResponse; + } + + // Parse the final response content for structured results + const parsedResults = parseComplexityFromContent( + response.content, + round, + results + ); + + return { + results: { + roundId: round.roundId, + metric: "time_complexity", + results: parsedResults, + }, + threadId: thread.threadId, + }; +} + +// ---- Thread 2: Space Complexity ---- + +export async function evaluateSpaceComplexity( + client: BackboardClient, + round: RoundSubmissions, + config: EvaluationConfig = DEFAULT_EVAL_CONFIG +): Promise<{ results: SpaceComplexityResults; threadId: string }> { + const assistant = await client.createAssistant({ + name: "CnC Space Complexity Analyzer", + system_prompt: SYSTEM_PROMPTS.spaceComplexity, + tools: [SPACE_COMPLEXITY_TOOL], + }); + + const thread = await client.createThread(assistant.assistantId); + const prompt = buildBatchPrompt(round, "Space Complexity"); + + let response = (await client.addMessage(thread.threadId, { + content: prompt, + llm_provider: config.llmProvider, + model_name: config.modelName, + stream: false, + })) as BBMessageResponse; + + const results: Record = {}; + for (const sub of round.submissions) { + results[sub.userId] = []; + } + + while (response.status === "REQUIRES_ACTION" && response.toolCalls) { + const toolOutputs = []; + + for (const tc of response.toolCalls) { + if (tc.function.name === "analyze_space_complexity") { + const args = tc.function.parsedArguments; + const analysisResult: ComplexityResult = { + questionId: args.questionContext || "unknown", + complexity: args.complexity || "unknown", + score: args.score || 50, + reasoning: args.reasoning || "No reasoning provided", + }; + + toolOutputs.push({ + tool_call_id: tc.id, + output: JSON.stringify({ status: "recorded", ...analysisResult }), + }); + } + } + + response = (await client.submitToolOutputs( + thread.threadId, + response.runId!, + toolOutputs, + false + )) as BBMessageResponse; + } + + const parsedResults = parseComplexityFromContent( + response.content, + round, + results + ); + + return { + results: { + roundId: round.roundId, + metric: "space_complexity", + results: parsedResults, + }, + threadId: thread.threadId, + }; +} + +// ---- Thread 3: Originality & Relevance ---- + +export async function evaluateOriginality( + client: BackboardClient, + round: RoundSubmissions, + config: EvaluationConfig = DEFAULT_EVAL_CONFIG +): Promise<{ results: OriginalityResults; threadId: string }> { + const assistant = await client.createAssistant({ + name: "CnC Originality Analyzer", + system_prompt: SYSTEM_PROMPTS.originality, + tools: [ORIGINALITY_TOOL], + }); + + const thread = await client.createThread(assistant.assistantId); + const prompt = buildBatchPrompt(round, "Originality & Relevance"); + + let response = (await client.addMessage(thread.threadId, { + content: prompt, + llm_provider: config.llmProvider, + model_name: config.modelName, + stream: false, + })) as BBMessageResponse; + + const results: Record = {}; + for (const sub of round.submissions) { + results[sub.userId] = []; + } + + while (response.status === "REQUIRES_ACTION" && response.toolCalls) { + const toolOutputs = []; + + for (const tc of response.toolCalls) { + if (tc.function.name === "analyze_originality") { + const args = tc.function.parsedArguments; + const analysisResult: OriginalityResult = { + questionId: args.questionContext || "unknown", + originalityScore: args.originalityScore || 5, + relevanceScore: args.relevanceScore || 5, + commentDensity: args.commentDensity || 0, + flags: args.flags || [], + reasoning: args.reasoning || "No reasoning provided", + }; + + toolOutputs.push({ + tool_call_id: tc.id, + output: JSON.stringify({ status: "recorded", ...analysisResult }), + }); + } + } + + response = (await client.submitToolOutputs( + thread.threadId, + response.runId!, + toolOutputs, + false + )) as BBMessageResponse; + } + + const parsedResults = parseOriginalityFromContent( + response.content, + round, + results + ); + + return { + results: { + roundId: round.roundId, + metric: "originality", + results: parsedResults, + }, + threadId: thread.threadId, + }; +} + +// ---- Thread 4: Final Ranking ---- + +export async function computeRanking( + client: BackboardClient, + tcResults: TimeComplexityResults, + scResults: SpaceComplexityResults, + origResults: OriginalityResults, + testCaseResults: TestCaseResults, + weights: ScoringWeights, + tiebreakerData: TiebreakerData, + config: EvaluationConfig = DEFAULT_EVAL_CONFIG +): Promise<{ rankings: RankedUser[]; threadId: string }> { + const assistant = await client.createAssistant({ + name: "CnC Ranking Engine", + system_prompt: SYSTEM_PROMPTS.ranking, + tools: [RANKING_TOOL], + }); + + const thread = await client.createThread(assistant.assistantId); + + const prompt = `Compute the final ranking for round ${tcResults.roundId}. + +Use the compute_final_ranking tool with the following data: +- timeComplexityResults: ${JSON.stringify(tcResults)} +- spaceComplexityResults: ${JSON.stringify(scResults)} +- originalityResults: ${JSON.stringify(origResults)} +- testCaseResults: ${JSON.stringify(testCaseResults)} +- weights: ${JSON.stringify(weights)} +- tiebreakerData: ${JSON.stringify(tiebreakerData)} + +Produce a final ranking with full score breakdowns for each user.`; + + let response = (await client.addMessage(thread.threadId, { + content: prompt, + llm_provider: config.llmProvider, + model_name: config.modelName, + stream: false, + })) as BBMessageResponse; + + while (response.status === "REQUIRES_ACTION" && response.toolCalls) { + const toolOutputs = []; + + for (const tc of response.toolCalls) { + if (tc.function.name === "compute_final_ranking") { + // Let the LLM compute the ranking via tool — we can also verify locally + const localRanking = computeRankingLocally( + tcResults, + scResults, + origResults, + testCaseResults, + weights, + tiebreakerData + ); + + toolOutputs.push({ + tool_call_id: tc.id, + output: JSON.stringify({ rankings: localRanking }), + }); + } + } + + response = (await client.submitToolOutputs( + thread.threadId, + response.runId!, + toolOutputs, + false + )) as BBMessageResponse; + } + + // Use local computation as source of truth (LLM is for validation) + const rankings = computeRankingLocally( + tcResults, + scResults, + origResults, + testCaseResults, + weights, + tiebreakerData + ); + + return { rankings, threadId: thread.threadId }; +} + +// ---- Local Scoring Engine (deterministic, no LLM needed) ---- + +export function computeRankingLocally( + tcResults: TimeComplexityResults, + scResults: SpaceComplexityResults, + origResults: OriginalityResults, + testCaseResults: TestCaseResults, + weights: ScoringWeights, + tiebreakerData: TiebreakerData +): RankedUser[] { + const userIds = Object.keys(tcResults.results); + const scored: Array<{ + userId: string; + finalScore: number; + breakdown: ScoreBreakdown; + timeTakenMs: number; + }> = []; + + for (const userId of userIds) { + const tcScores = tcResults.results[userId] || []; + const scScores = scResults.results[userId] || []; + const origScores = origResults.results[userId] || []; + const testCase = testCaseResults[userId] || { passed: 0, total: 1 }; + + // Average TC score (out of 100) + const avgTcScore = + tcScores.length > 0 + ? tcScores.reduce((sum, r) => sum + r.score, 0) / tcScores.length + : 0; + + // Average SC score (out of 100) + const avgScScore = + scScores.length > 0 + ? scScores.reduce((sum, r) => sum + r.score, 0) / scScores.length + : 0; + + // Average originality score: combine originality (1-10) and relevance (1-10) → normalize to 100 + const avgOrigScore = + origScores.length > 0 + ? origScores.reduce( + (sum, r) => + sum + ((r.originalityScore + r.relevanceScore) / 2) * 10, + 0 + ) / origScores.length + : 0; + + // Test case pass rate → scale to 100 + const testCaseScore = + testCase.total > 0 ? (testCase.passed / testCase.total) * 100 : 0; + + // Weighted final score + const finalScore = + avgTcScore * weights.timeComplexity + + avgScScore * weights.spaceComplexity + + avgOrigScore * weights.originality + + testCaseScore * weights.testCases; + + scored.push({ + userId, + finalScore: Math.round(finalScore * 100) / 100, + breakdown: { + timeComplexity: { + avgScore: Math.round(avgTcScore * 100) / 100, + weighted: + Math.round(avgTcScore * weights.timeComplexity * 100) / 100, + }, + spaceComplexity: { + avgScore: Math.round(avgScScore * 100) / 100, + weighted: + Math.round(avgScScore * weights.spaceComplexity * 100) / 100, + }, + originality: { + avgScore: Math.round(avgOrigScore * 100) / 100, + weighted: + Math.round(avgOrigScore * weights.originality * 100) / 100, + }, + testCases: { + passRate: testCase.total > 0 ? testCase.passed / testCase.total : 0, + weighted: + Math.round(testCaseScore * weights.testCases * 100) / 100, + }, + }, + timeTakenMs: tiebreakerData[userId] || Infinity, + }); + } + + // Sort: highest score first, then lowest time (tiebreaker) + scored.sort((a, b) => { + if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore; + return a.timeTakenMs - b.timeTakenMs; // lower time wins + }); + + return scored.map((s, i) => ({ + rank: i + 1, + ...s, + })); +} + +// ---- Full Pipeline Orchestrator ---- + +export async function runEvaluationPipeline( + round: RoundSubmissions, + testCaseResults: TestCaseResults, + tiebreakerData: TiebreakerData, + weights: ScoringWeights, + config: EvaluationConfig = DEFAULT_EVAL_CONFIG +): Promise { + const client = new BackboardClient({ + apiKey: process.env.BACKBOARD_API_KEY!, + }); + + // Threads 1-3 run in parallel + const [tcResult, scResult, origResult] = await Promise.all([ + evaluateTimeComplexity(client, round, config), + evaluateSpaceComplexity(client, round, config), + evaluateOriginality(client, round, config), + ]); + + // Thread 4: aggregation (depends on 1-3) + const rankingResult = await computeRanking( + client, + tcResult.results, + scResult.results, + origResult.results, + testCaseResults, + weights, + tiebreakerData, + config + ); + + return { + roundId: round.roundId, + evaluationId: `eval_${round.roundId}_${Date.now()}`, + rankings: rankingResult.rankings, + threadIds: { + timeComplexity: tcResult.threadId, + spaceComplexity: scResult.threadId, + originality: origResult.threadId, + ranking: rankingResult.threadId, + }, + evaluatedAt: new Date().toISOString(), + }; +} + +// ---- Content Parsers (extract structured data from LLM final response) ---- + +function parseComplexityFromContent( + content: string, + round: RoundSubmissions, + fallback: Record +): Record { + // Try to parse JSON from LLM response + try { + const jsonMatch = content.match(/\{[\s\S]*\}/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + if (parsed.results) return parsed.results; + } + } catch { + // Fall through to fallback + } + + // If parsing fails, return whatever was collected from tool calls + // or generate defaults + for (const sub of round.submissions) { + if (!fallback[sub.userId] || fallback[sub.userId].length === 0) { + fallback[sub.userId] = sub.responses.map((r) => ({ + questionId: r.questionId, + complexity: "unknown", + score: 50, + reasoning: "Could not extract from LLM response", + })); + } + } + return fallback; +} + +function parseOriginalityFromContent( + content: string, + round: RoundSubmissions, + fallback: Record +): Record { + try { + const jsonMatch = content.match(/\{[\s\S]*\}/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + if (parsed.results) return parsed.results; + } + } catch { + // Fall through + } + + for (const sub of round.submissions) { + if (!fallback[sub.userId] || fallback[sub.userId].length === 0) { + fallback[sub.userId] = sub.responses.map((r) => ({ + questionId: r.questionId, + originalityScore: 5, + relevanceScore: 5, + commentDensity: 0, + flags: [], + reasoning: "Could not extract from LLM response", + })); + } + } + return fallback; +} diff --git a/backbaord/index.ts b/backbaord/index.ts new file mode 100644 index 0000000..3656907 --- /dev/null +++ b/backbaord/index.ts @@ -0,0 +1,9 @@ +// ============================================================ +// Compile-n-Conquer — Backboard Evaluation Pipeline +// ============================================================ +// Barrel export for all modules + +export * from "./types"; +export * from "./tools"; +export * from "./evaluation"; +export * from "./memory"; diff --git a/backbaord/memory.ts b/backbaord/memory.ts new file mode 100644 index 0000000..60b8b4c --- /dev/null +++ b/backbaord/memory.ts @@ -0,0 +1,204 @@ +// ============================================================ +// Compile-n-Conquer — Memory / Skill Tier Engine +// ============================================================ + +import { BackboardClient } from "backboard-sdk"; +import { + PlayerProfile, + SkillTier, + RoundScore, + TierChange, + EvaluationConfig, + DEFAULT_EVAL_CONFIG, +} from "./types"; +import { SYSTEM_PROMPTS } from "./tools"; + +// ---- Skill Tier Thresholds ---- + +const TIER_THRESHOLDS: Record = { + beginner: { min: 0, max: 30 }, + intermediate: { min: 30, max: 60 }, + advanced: { min: 60, max: 85 }, + pro: { min: 85, max: 100 }, +}; + +const PROMOTION_STREAK = 3; // consecutive rounds needed to promote +const DEMOTION_STREAK = 2; // consecutive rounds needed to demote + +// ---- Classify Tier from Score ---- + +export function classifyTier(avgScore: number): SkillTier { + if (avgScore >= TIER_THRESHOLDS.pro.min) return "pro"; + if (avgScore >= TIER_THRESHOLDS.advanced.min) return "advanced"; + if (avgScore >= TIER_THRESHOLDS.intermediate.min) return "intermediate"; + return "beginner"; +} + +// ---- Check Promotion / Demotion ---- + +export function evaluateTierChange( + currentTier: SkillTier, + roundHistory: RoundScore[] +): { newTier: SkillTier; changed: boolean; reason: string } { + if (roundHistory.length === 0) { + return { newTier: currentTier, changed: false, reason: "No round history" }; + } + + const tierOrder: SkillTier[] = ["beginner", "intermediate", "advanced", "pro"]; + const currentIdx = tierOrder.indexOf(currentTier); + + // Check promotion (last N rounds all qualify for next tier) + if (currentIdx < tierOrder.length - 1) { + const nextTier = tierOrder[currentIdx + 1]; + const nextThreshold = TIER_THRESHOLDS[nextTier].min; + const recentForPromotion = roundHistory.slice(-PROMOTION_STREAK); + + if ( + recentForPromotion.length >= PROMOTION_STREAK && + recentForPromotion.every((r) => r.finalScore >= nextThreshold) + ) { + return { + newTier: nextTier, + changed: true, + reason: `${PROMOTION_STREAK} consecutive rounds with avg score ≥ ${nextThreshold} → promoted to ${nextTier}`, + }; + } + } + + // Check demotion (last N rounds all fall below current tier) + if (currentIdx > 0) { + const currentThreshold = TIER_THRESHOLDS[currentTier].min; + const recentForDemotion = roundHistory.slice(-DEMOTION_STREAK); + + if ( + recentForDemotion.length >= DEMOTION_STREAK && + recentForDemotion.every((r) => r.finalScore < currentThreshold) + ) { + const prevTier = tierOrder[currentIdx - 1]; + return { + newTier: prevTier, + changed: true, + reason: `${DEMOTION_STREAK} consecutive rounds with avg score < ${currentThreshold} → demoted to ${prevTier}`, + }; + } + } + + return { newTier: currentTier, changed: false, reason: "No tier change" }; +} + +// ---- Update Player Profile After a Round ---- + +export function updatePlayerProfile( + profile: PlayerProfile, + roundScore: RoundScore +): PlayerProfile { + const updatedHistory = [...profile.roundHistory, roundScore]; + + // Recalculate avg score from last 5 rounds + const recentRounds = updatedHistory.slice(-5); + const avgScore = + recentRounds.reduce((sum, r) => sum + r.finalScore, 0) / + recentRounds.length; + + // Check tier change + const tierResult = evaluateTierChange(profile.skillTier, updatedHistory); + + const promotionHistory = [...profile.promotionHistory]; + if (tierResult.changed) { + promotionHistory.push({ + from: profile.skillTier, + to: tierResult.newTier, + reason: tierResult.reason, + changedAt: new Date().toISOString(), + }); + } + + return { + ...profile, + skillTier: tierResult.newTier, + avgScore: Math.round(avgScore * 100) / 100, + roundHistory: updatedHistory, + promotionHistory, + }; +} + +// ---- Store Memory via Backboard ---- + +export async function storePlayerMemory( + client: BackboardClient, + profile: PlayerProfile, + config: EvaluationConfig = DEFAULT_EVAL_CONFIG +): Promise<{ threadId: string; memoryOperationId?: string }> { + const assistant = await client.createAssistant({ + name: "CnC Player Memory Manager", + system_prompt: SYSTEM_PROMPTS.memory, + }); + + const thread = await client.createThread(assistant.assistantId); + + const memoryMessage = ` +Player Profile Update: + +User: ${profile.userId} +Current Tier: ${profile.skillTier} +Average Score: ${profile.avgScore} + +Round History (last 5): +${profile.roundHistory + .slice(-5) + .map((r) => `- Round ${r.roundId}: Score ${r.finalScore}, Rank #${r.rank}`) + .join("\n")} + +Common Errors: ${profile.commonErrors.join(", ") || "None recorded"} +Preferred Algorithms: ${profile.preferredAlgorithms.join(", ") || "None recorded"} + +Tier Changes: +${ + profile.promotionHistory.length > 0 + ? profile.promotionHistory + .slice(-3) + .map((tc) => `- ${tc.from} → ${tc.to}: ${tc.reason}`) + .join("\n") + : "No tier changes yet" +} + +Remember this profile for personalized coaching and adaptive difficulty. +`; + + const response = (await client.addMessage(thread.threadId, { + content: memoryMessage, + memory: "Auto", + stream: false, + })) as any; + + return { + threadId: thread.threadId, + memoryOperationId: response.memoryOperationId, + }; +} + +// ---- Retrieve Player Context from Memory ---- + +export async function retrievePlayerMemory( + client: BackboardClient, + userId: string, + config: EvaluationConfig = DEFAULT_EVAL_CONFIG +): Promise<{ content: string; threadId: string }> { + const assistant = await client.createAssistant({ + name: "CnC Player Memory Manager", + system_prompt: SYSTEM_PROMPTS.memory, + }); + + const thread = await client.createThread(assistant.assistantId); + + const response = (await client.addMessage(thread.threadId, { + content: `Retrieve everything you remember about user ${userId}. Provide their current skill tier, recent performance trends, areas for improvement, and personalized coaching recommendations.`, + memory: "Auto", + stream: false, + })) as any; + + return { + content: response.content, + threadId: thread.threadId, + }; +} diff --git a/backbaord/mock-test.ts b/backbaord/mock-test.ts new file mode 100644 index 0000000..5c92d59 --- /dev/null +++ b/backbaord/mock-test.ts @@ -0,0 +1,869 @@ +// ============================================================ +// Compile-n-Conquer — Mock Test Suite +// ============================================================ +// Run: npx tsx backbaord/mock-test.ts + +// NOTE: We import types directly and scoring/memory functions inline +// to avoid pulling in backboard-sdk (which has ESM export issues in tsx). +// In production, use the barrel export from ./index.ts + +import { + RoundSubmissions, + TimeComplexityResults, + SpaceComplexityResults, + OriginalityResults, + TestCaseResults, + ScoringWeights, + TiebreakerData, + DEFAULT_WEIGHTS, + PlayerProfile, + RoundScore, + RankedUser, + ScoreBreakdown, + SkillTier, +} from "./types"; + +// ---- Inline: computeRankingLocally (from evaluation.ts) ---- + +function computeRankingLocally( + tcResults: TimeComplexityResults, + scResults: SpaceComplexityResults, + origResults: OriginalityResults, + testCaseResults: TestCaseResults, + weights: ScoringWeights, + tiebreakerData: TiebreakerData +): RankedUser[] { + const userIds = Object.keys(tcResults.results); + const scored: Array<{ + userId: string; + finalScore: number; + breakdown: ScoreBreakdown; + timeTakenMs: number; + }> = []; + + for (const userId of userIds) { + const tcScores = tcResults.results[userId] || []; + const scScores = scResults.results[userId] || []; + const origScores = origResults.results[userId] || []; + const testCase = testCaseResults[userId] || { passed: 0, total: 1 }; + + const avgTcScore = + tcScores.length > 0 + ? tcScores.reduce((sum, r) => sum + r.score, 0) / tcScores.length + : 0; + const avgScScore = + scScores.length > 0 + ? scScores.reduce((sum, r) => sum + r.score, 0) / scScores.length + : 0; + const avgOrigScore = + origScores.length > 0 + ? origScores.reduce( + (sum, r) => + sum + ((r.originalityScore + r.relevanceScore) / 2) * 10, + 0 + ) / origScores.length + : 0; + const testCaseScore = + testCase.total > 0 ? (testCase.passed / testCase.total) * 100 : 0; + + const finalScore = + avgTcScore * weights.timeComplexity + + avgScScore * weights.spaceComplexity + + avgOrigScore * weights.originality + + testCaseScore * weights.testCases; + + scored.push({ + userId, + finalScore: Math.round(finalScore * 100) / 100, + breakdown: { + timeComplexity: { + avgScore: Math.round(avgTcScore * 100) / 100, + weighted: Math.round(avgTcScore * weights.timeComplexity * 100) / 100, + }, + spaceComplexity: { + avgScore: Math.round(avgScScore * 100) / 100, + weighted: Math.round(avgScScore * weights.spaceComplexity * 100) / 100, + }, + originality: { + avgScore: Math.round(avgOrigScore * 100) / 100, + weighted: Math.round(avgOrigScore * weights.originality * 100) / 100, + }, + testCases: { + passRate: testCase.total > 0 ? testCase.passed / testCase.total : 0, + weighted: Math.round(testCaseScore * weights.testCases * 100) / 100, + }, + }, + timeTakenMs: tiebreakerData[userId] || Infinity, + }); + } + + scored.sort((a, b) => { + if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore; + return a.timeTakenMs - b.timeTakenMs; + }); + + return scored.map((s, i) => ({ rank: i + 1, ...s })); +} + +// ---- Inline: Tier functions (from memory.ts) ---- + +const TIER_THRESHOLDS: Record = { + beginner: { min: 0, max: 30 }, + intermediate: { min: 30, max: 60 }, + advanced: { min: 60, max: 85 }, + pro: { min: 85, max: 100 }, +}; + +function classifyTier(avgScore: number): SkillTier { + if (avgScore >= TIER_THRESHOLDS.pro.min) return "pro"; + if (avgScore >= TIER_THRESHOLDS.advanced.min) return "advanced"; + if (avgScore >= TIER_THRESHOLDS.intermediate.min) return "intermediate"; + return "beginner"; +} + +function evaluateTierChange( + currentTier: SkillTier, + roundHistory: RoundScore[] +): { newTier: SkillTier; changed: boolean; reason: string } { + if (roundHistory.length === 0) { + return { newTier: currentTier, changed: false, reason: "No round history" }; + } + const tierOrder: SkillTier[] = ["beginner", "intermediate", "advanced", "pro"]; + const currentIdx = tierOrder.indexOf(currentTier); + + if (currentIdx < tierOrder.length - 1) { + const nextTier = tierOrder[currentIdx + 1]; + const nextThreshold = TIER_THRESHOLDS[nextTier].min; + const recentForPromotion = roundHistory.slice(-3); + if ( + recentForPromotion.length >= 3 && + recentForPromotion.every((r) => r.finalScore >= nextThreshold) + ) { + return { + newTier: nextTier, + changed: true, + reason: `3 consecutive rounds with avg score ≥ ${nextThreshold} → promoted to ${nextTier}`, + }; + } + } + + if (currentIdx > 0) { + const currentThreshold = TIER_THRESHOLDS[currentTier].min; + const recentForDemotion = roundHistory.slice(-2); + if ( + recentForDemotion.length >= 2 && + recentForDemotion.every((r) => r.finalScore < currentThreshold) + ) { + const prevTier = tierOrder[currentIdx - 1]; + return { + newTier: prevTier, + changed: true, + reason: `2 consecutive rounds with avg score < ${currentThreshold} → demoted to ${prevTier}`, + }; + } + } + + return { newTier: currentTier, changed: false, reason: "No tier change" }; +} + +function updatePlayerProfile( + profile: PlayerProfile, + roundScore: RoundScore +): PlayerProfile { + const updatedHistory = [...profile.roundHistory, roundScore]; + const recentRounds = updatedHistory.slice(-5); + const avgScore = + recentRounds.reduce((sum, r) => sum + r.finalScore, 0) / recentRounds.length; + const tierResult = evaluateTierChange(profile.skillTier, updatedHistory); + const promotionHistory = [...profile.promotionHistory]; + if (tierResult.changed) { + promotionHistory.push({ + from: profile.skillTier, + to: tierResult.newTier, + reason: tierResult.reason, + changedAt: new Date().toISOString(), + }); + } + return { + ...profile, + skillTier: tierResult.newTier, + avgScore: Math.round(avgScore * 100) / 100, + roundHistory: updatedHistory, + promotionHistory, + }; +} + +// ============================================================ +// TEST DATA — 3 users, 3 questions, 1 round +// ============================================================ + +const MOCK_ROUND: RoundSubmissions = { + roundId: "round_1", + questions: [ + { + id: "q1", + title: "Two Sum", + description: "Given an array of integers and a target, return indices of two numbers that add up to the target.", + difficulty: "easy", + }, + { + id: "q2", + title: "Longest Substring Without Repeating Characters", + description: "Find the length of the longest substring without repeating characters.", + difficulty: "medium", + }, + { + id: "q3", + title: "Merge K Sorted Lists", + description: "Merge k sorted linked lists into one sorted linked list.", + difficulty: "hard", + }, + ], + submissions: [ + { + userId: "user_1", + responses: [ + { + questionId: "q1", + language: "python", + submittedAt: "2026-02-09T10:05:00Z", + code: `def two_sum(nums, target): + seen = {} + for i, num in enumerate(nums): + comp = target - num + if comp in seen: + return [seen[comp], i] + seen[num] = i + return []`, + }, + { + questionId: "q2", + language: "python", + submittedAt: "2026-02-09T10:15:00Z", + code: `def length_of_longest_substring(s): + char_set = set() + l = 0 + result = 0 + for r in range(len(s)): + while s[r] in char_set: + char_set.remove(s[l]) + l += 1 + char_set.add(s[r]) + result = max(result, r - l + 1) + return result`, + }, + { + questionId: "q3", + language: "python", + submittedAt: "2026-02-09T10:30:00Z", + code: `import heapq +def merge_k_lists(lists): + heap = [] + for i, l in enumerate(lists): + if l: + heapq.heappush(heap, (l.val, i, l)) + dummy = ListNode(0) + curr = dummy + while heap: + val, i, node = heapq.heappop(heap) + curr.next = node + curr = curr.next + if node.next: + heapq.heappush(heap, (node.next.val, i, node.next)) + return dummy.next`, + }, + ], + }, + { + userId: "user_2", + responses: [ + { + questionId: "q1", + language: "cpp", + submittedAt: "2026-02-09T10:03:00Z", + code: `vector twoSum(vector& nums, int target) { + // This function finds two numbers that sum to target + // It uses a brute force approach to check all pairs + // Time complexity: O(n^2) + // Space complexity: O(1) + for (int i = 0; i < nums.size(); i++) { + // Iterate through each element + for (int j = i + 1; j < nums.size(); j++) { + // Check if the pair sums to target + if (nums[i] + nums[j] == target) { + // Return the indices + return {i, j}; + } + } + } + // Return empty if no pair found + return {}; +}`, + }, + { + questionId: "q2", + language: "cpp", + submittedAt: "2026-02-09T10:18:00Z", + code: `int lengthOfLongestSubstring(string s) { + unordered_set chars; + int l = 0, res = 0; + for (int r = 0; r < s.size(); r++) { + while (chars.count(s[r])) { + chars.erase(s[l]); + l++; + } + chars.insert(s[r]); + res = max(res, r - l + 1); + } + return res; +}`, + }, + { + questionId: "q3", + language: "cpp", + submittedAt: "2026-02-09T10:28:00Z", + code: `ListNode* mergeKLists(vector& lists) { + auto cmp = [](ListNode* a, ListNode* b) { return a->val > b->val; }; + priority_queue, decltype(cmp)> pq(cmp); + for (auto l : lists) if (l) pq.push(l); + ListNode dummy(0); + ListNode* curr = &dummy; + while (!pq.empty()) { + curr->next = pq.top(); pq.pop(); + curr = curr->next; + if (curr->next) pq.push(curr->next); + } + return dummy.next; +}`, + }, + ], + }, + { + userId: "user_3", + responses: [ + { + questionId: "q1", + language: "javascript", + submittedAt: "2026-02-09T10:08:00Z", + code: `/** + * @param {number[]} nums - Array of integers + * @param {number} target - Target sum + * @returns {number[]} - Indices of two numbers + * @description This function implements the Two Sum algorithm + * using a hash map for O(n) time complexity + */ +function twoSum(nums, target) { + // Create a hash map to store values and their indices + const map = new Map(); + // Iterate through each number in the array + for (let i = 0; i < nums.length; i++) { + // Calculate the complement + const complement = target - nums[i]; + // Check if complement exists in our map + if (map.has(complement)) { + // Return the pair of indices + return [map.get(complement), i]; + } + // Store current number and its index + map.set(nums[i], i); + } + // Return empty array if no solution found + return []; +}`, + }, + { + questionId: "q2", + language: "javascript", + submittedAt: "2026-02-09T10:22:00Z", + code: `function lengthOfLongestSubstring(s) { + let max = 0; + for (let i = 0; i < s.length; i++) { + let seen = new Set(); + for (let j = i; j < s.length; j++) { + if (seen.has(s[j])) break; + seen.add(s[j]); + max = Math.max(max, j - i + 1); + } + } + return max; +}`, + }, + { + questionId: "q3", + language: "javascript", + submittedAt: "2026-02-09T10:40:00Z", + code: `// didnt finish this one +function mergeKLists(lists) { + if (!lists.length) return null; + return lists[0]; // just returning first list lol +}`, + }, + ], + }, + ], +}; + +// ---- Mock LLM Outputs (what Threads 1-3 would return) ---- + +const MOCK_TC_RESULTS: TimeComplexityResults = { + roundId: "round_1", + metric: "time_complexity", + results: { + user_1: [ + { questionId: "q1", complexity: "O(n)", score: 95, reasoning: "Single-pass hash map approach" }, + { questionId: "q2", complexity: "O(n)", score: 92, reasoning: "Sliding window, each char processed at most twice" }, + { questionId: "q3", complexity: "O(N log k)", score: 90, reasoning: "Min-heap with k elements" }, + ], + user_2: [ + { questionId: "q1", complexity: "O(n²)", score: 40, reasoning: "Brute force nested loops" }, + { questionId: "q2", complexity: "O(n)", score: 92, reasoning: "Sliding window technique" }, + { questionId: "q3", complexity: "O(N log k)", score: 90, reasoning: "Priority queue approach" }, + ], + user_3: [ + { questionId: "q1", complexity: "O(n)", score: 95, reasoning: "Hash map single pass" }, + { questionId: "q2", complexity: "O(n²)", score: 35, reasoning: "Brute force nested loop approach" }, + { questionId: "q3", complexity: "O(1)", score: 5, reasoning: "No real solution, just returns first list" }, + ], + }, +}; + +const MOCK_SC_RESULTS: SpaceComplexityResults = { + roundId: "round_1", + metric: "space_complexity", + results: { + user_1: [ + { questionId: "q1", complexity: "O(n)", score: 80, reasoning: "Hash map stores up to n elements" }, + { questionId: "q2", complexity: "O(min(n,m))", score: 85, reasoning: "Set bounded by alphabet size" }, + { questionId: "q3", complexity: "O(k)", score: 90, reasoning: "Heap of size k" }, + ], + user_2: [ + { questionId: "q1", complexity: "O(1)", score: 100, reasoning: "No extra space" }, + { questionId: "q2", complexity: "O(min(n,m))", score: 85, reasoning: "Set for window chars" }, + { questionId: "q3", complexity: "O(k)", score: 90, reasoning: "Priority queue of size k" }, + ], + user_3: [ + { questionId: "q1", complexity: "O(n)", score: 80, reasoning: "Map stores values" }, + { questionId: "q2", complexity: "O(n)", score: 70, reasoning: "New set per iteration, up to n" }, + { questionId: "q3", complexity: "O(1)", score: 100, reasoning: "No extra space (but no real solution)" }, + ], + }, +}; + +const MOCK_ORIG_RESULTS: OriginalityResults = { + roundId: "round_1", + metric: "originality", + results: { + user_1: [ + { questionId: "q1", originalityScore: 9, relevanceScore: 10, commentDensity: 0, flags: [], reasoning: "Clean, human-style competitive code" }, + { questionId: "q2", originalityScore: 9, relevanceScore: 10, commentDensity: 0, flags: [], reasoning: "Idiomatic sliding window" }, + { questionId: "q3", originalityScore: 8, relevanceScore: 10, commentDensity: 0, flags: [], reasoning: "Standard heap approach, well-written" }, + ], + user_2: [ + { questionId: "q1", originalityScore: 3, relevanceScore: 9, commentDensity: 55, flags: ["excessive_comments", "boilerplate_docstrings"], reasoning: "Every line has an explanatory comment — strong GPT signal" }, + { questionId: "q2", originalityScore: 8, relevanceScore: 10, commentDensity: 0, flags: [], reasoning: "Clean competitive code" }, + { questionId: "q3", originalityScore: 8, relevanceScore: 10, commentDensity: 0, flags: [], reasoning: "Concise and idiomatic" }, + ], + user_3: [ + { questionId: "q1", originalityScore: 2, relevanceScore: 10, commentDensity: 62, flags: ["excessive_comments", "boilerplate_docstrings", "generic_variable_names"], reasoning: "JSDoc + line-by-line comments strongly suggest AI generation" }, + { questionId: "q2", originalityScore: 7, relevanceScore: 8, commentDensity: 0, flags: [], reasoning: "Human-style brute force" }, + { questionId: "q3", originalityScore: 6, relevanceScore: 1, commentDensity: 10, flags: [], reasoning: "Honest comment about not finishing, but doesn't solve the problem" }, + ], + }, +}; + +const MOCK_TEST_CASES: TestCaseResults = { + user_1: { passed: 28, total: 30 }, + user_2: { passed: 25, total: 30 }, + user_3: { passed: 15, total: 30 }, +}; + +const MOCK_TIEBREAKER: TiebreakerData = { + user_1: 1500000, // 25 min + user_2: 1500000, // 25 min (same as user_1 to test tiebreaker) + user_3: 1920000, // 32 min +}; + +// ============================================================ +// TEST RUNNER +// ============================================================ + +function assert(condition: boolean, message: string) { + if (condition) { + console.log(` ✅ PASS: ${message}`); + } else { + console.error(` ❌ FAIL: ${message}`); + process.exitCode = 1; + } +} + +function section(title: string) { + console.log(`\n${"=".repeat(60)}`); + console.log(` ${title}`); + console.log("=".repeat(60)); +} + +// ---- Test 1: Local Ranking Computation ---- + +function testRankingComputation() { + section("Test 1: Ranking Computation with Default Weights"); + + const rankings = computeRankingLocally( + MOCK_TC_RESULTS, + MOCK_SC_RESULTS, + MOCK_ORIG_RESULTS, + MOCK_TEST_CASES, + DEFAULT_WEIGHTS, + MOCK_TIEBREAKER + ); + + console.log("\n Rankings:"); + for (const r of rankings) { + console.log( + ` #${r.rank} ${r.userId} — Score: ${r.finalScore} | TC: ${r.breakdown.timeComplexity.weighted} | SC: ${r.breakdown.spaceComplexity.weighted} | Orig: ${r.breakdown.originality.weighted} | Tests: ${r.breakdown.testCases.weighted}` + ); + } + + assert(rankings.length === 3, "3 users ranked"); + assert(rankings[0].rank === 1, "Rank 1 assigned"); + assert(rankings[1].rank === 2, "Rank 2 assigned"); + assert(rankings[2].rank === 3, "Rank 3 assigned"); + assert(rankings[0].finalScore >= rankings[1].finalScore, "Rank 1 score ≥ Rank 2 score"); + assert(rankings[1].finalScore >= rankings[2].finalScore, "Rank 2 score ≥ Rank 3 score"); + assert(rankings[0].userId === "user_1", "User 1 should be ranked first (best overall)"); + assert(rankings[2].userId === "user_3", "User 3 should be ranked last (incomplete submission)"); + + return rankings; +} + +// ---- Test 2: Tiebreaker Logic ---- + +function testTiebreaker() { + section("Test 2: Tiebreaker — Same Scores, Different Times"); + + // Create identical scores for two users + const identicalTC: TimeComplexityResults = { + roundId: "round_tie", + metric: "time_complexity", + results: { + user_a: [{ questionId: "q1", complexity: "O(n)", score: 80, reasoning: "" }], + user_b: [{ questionId: "q1", complexity: "O(n)", score: 80, reasoning: "" }], + }, + }; + const identicalSC: SpaceComplexityResults = { + roundId: "round_tie", + metric: "space_complexity", + results: { + user_a: [{ questionId: "q1", complexity: "O(1)", score: 90, reasoning: "" }], + user_b: [{ questionId: "q1", complexity: "O(1)", score: 90, reasoning: "" }], + }, + }; + const identicalOrig: OriginalityResults = { + roundId: "round_tie", + metric: "originality", + results: { + user_a: [{ questionId: "q1", originalityScore: 8, relevanceScore: 9, commentDensity: 5, flags: [], reasoning: "" }], + user_b: [{ questionId: "q1", originalityScore: 8, relevanceScore: 9, commentDensity: 5, flags: [], reasoning: "" }], + }, + }; + const identicalTests: TestCaseResults = { + user_a: { passed: 9, total: 10 }, + user_b: { passed: 9, total: 10 }, + }; + + // user_b was faster + const tiebreaker: TiebreakerData = { + user_a: 300000, // 5 min + user_b: 180000, // 3 min — faster + }; + + const rankings = computeRankingLocally( + identicalTC, + identicalSC, + identicalOrig, + identicalTests, + DEFAULT_WEIGHTS, + tiebreaker + ); + + console.log("\n Tied Rankings:"); + for (const r of rankings) { + console.log(` #${r.rank} ${r.userId} — Score: ${r.finalScore} | Time: ${r.timeTakenMs}ms`); + } + + assert(rankings[0].finalScore === rankings[1].finalScore, "Scores are identical"); + assert(rankings[0].userId === "user_b", "Faster user (user_b) wins tiebreaker"); + assert(rankings[1].userId === "user_a", "Slower user (user_a) ranks second"); +} + +// ---- Test 3: Weight Sensitivity ---- + +function testWeightSensitivity() { + section("Test 3: Weight Sensitivity — Test Cases Dominate"); + + const heavyTestWeights: ScoringWeights = { + timeComplexity: 0.05, + spaceComplexity: 0.05, + originality: 0.05, + testCases: 0.85, // test cases heavily weighted + }; + + const rankings = computeRankingLocally( + MOCK_TC_RESULTS, + MOCK_SC_RESULTS, + MOCK_ORIG_RESULTS, + MOCK_TEST_CASES, + heavyTestWeights, + MOCK_TIEBREAKER + ); + + console.log("\n With heavy test-case weight (85%):"); + for (const r of rankings) { + console.log( + ` #${r.rank} ${r.userId} — Score: ${r.finalScore} | Tests: ${r.breakdown.testCases.weighted} (${(r.breakdown.testCases.passRate * 100).toFixed(0)}%)` + ); + } + + assert(rankings[0].userId === "user_1", "User 1 still first (highest test pass rate)"); + assert( + rankings[0].breakdown.testCases.weighted > rankings[0].breakdown.timeComplexity.weighted, + "Test case contribution dominates TC contribution" + ); +} + +// ---- Test 4: Edge Case — Single User ---- + +function testSingleUser() { + section("Test 4: Edge Case — Single User Round"); + + const singleTC: TimeComplexityResults = { + roundId: "round_solo", + metric: "time_complexity", + results: { + solo_user: [{ questionId: "q1", complexity: "O(n)", score: 70, reasoning: "" }], + }, + }; + const singleSC: SpaceComplexityResults = { + roundId: "round_solo", + metric: "space_complexity", + results: { + solo_user: [{ questionId: "q1", complexity: "O(1)", score: 100, reasoning: "" }], + }, + }; + const singleOrig: OriginalityResults = { + roundId: "round_solo", + metric: "originality", + results: { + solo_user: [{ questionId: "q1", originalityScore: 7, relevanceScore: 8, commentDensity: 8, flags: [], reasoning: "" }], + }, + }; + + const rankings = computeRankingLocally( + singleTC, + singleSC, + singleOrig, + { solo_user: { passed: 10, total: 10 } }, + DEFAULT_WEIGHTS, + { solo_user: 120000 } + ); + + assert(rankings.length === 1, "Single user in rankings"); + assert(rankings[0].rank === 1, "Solo user is rank 1"); + assert(rankings[0].finalScore > 0, "Score is positive"); + console.log(` Solo user score: ${rankings[0].finalScore}`); +} + +// ---- Test 5: Edge Case — Empty/Zero Submissions ---- + +function testZeroScores() { + section("Test 5: Edge Case — All Zero Scores"); + + const zeroTC: TimeComplexityResults = { + roundId: "round_zero", + metric: "time_complexity", + results: { + user_x: [{ questionId: "q1", complexity: "unknown", score: 0, reasoning: "No code" }], + }, + }; + const zeroSC: SpaceComplexityResults = { + roundId: "round_zero", + metric: "space_complexity", + results: { + user_x: [{ questionId: "q1", complexity: "unknown", score: 0, reasoning: "No code" }], + }, + }; + const zeroOrig: OriginalityResults = { + roundId: "round_zero", + metric: "originality", + results: { + user_x: [{ questionId: "q1", originalityScore: 0, relevanceScore: 0, commentDensity: 0, flags: [], reasoning: "Empty" }], + }, + }; + + const rankings = computeRankingLocally( + zeroTC, + zeroSC, + zeroOrig, + { user_x: { passed: 0, total: 10 } }, + DEFAULT_WEIGHTS, + { user_x: 600000 } + ); + + assert(rankings.length === 1, "User still ranked"); + assert(rankings[0].finalScore === 0, "Score is zero"); +} + +// ---- Test 6: Tier Classification ---- + +function testTierClassification() { + section("Test 6: Skill Tier Classification"); + + assert(classifyTier(15) === "beginner", "Score 15 → beginner"); + assert(classifyTier(29) === "beginner", "Score 29 → beginner"); + assert(classifyTier(30) === "intermediate", "Score 30 → intermediate"); + assert(classifyTier(59) === "intermediate", "Score 59 → intermediate"); + assert(classifyTier(60) === "advanced", "Score 60 → advanced"); + assert(classifyTier(84) === "advanced", "Score 84 → advanced"); + assert(classifyTier(85) === "pro", "Score 85 → pro"); + assert(classifyTier(100) === "pro", "Score 100 → pro"); +} + +// ---- Test 7: Tier Promotion ---- + +function testTierPromotion() { + section("Test 7: Tier Promotion Logic"); + + const history: RoundScore[] = [ + { roundId: "r1", finalScore: 65, rank: 1, evaluatedAt: "" }, + { roundId: "r2", finalScore: 70, rank: 1, evaluatedAt: "" }, + { roundId: "r3", finalScore: 72, rank: 2, evaluatedAt: "" }, + ]; + + const result = evaluateTierChange("intermediate", history); + assert(result.changed === true, "Should be promoted"); + assert(result.newTier === "advanced", "Promoted to advanced"); + console.log(` Reason: ${result.reason}`); +} + +// ---- Test 8: Tier Demotion ---- + +function testTierDemotion() { + section("Test 8: Tier Demotion Logic"); + + const history: RoundScore[] = [ + { roundId: "r1", finalScore: 88, rank: 1, evaluatedAt: "" }, + { roundId: "r2", finalScore: 50, rank: 3, evaluatedAt: "" }, + { roundId: "r3", finalScore: 55, rank: 3, evaluatedAt: "" }, + ]; + + const result = evaluateTierChange("advanced", history); + assert(result.changed === true, "Should be demoted"); + assert(result.newTier === "intermediate", "Demoted to intermediate"); + console.log(` Reason: ${result.reason}`); +} + +// ---- Test 9: No Tier Change ---- + +function testNoTierChange() { + section("Test 9: No Tier Change — Inconsistent Performance"); + + const history: RoundScore[] = [ + { roundId: "r1", finalScore: 70, rank: 1, evaluatedAt: "" }, + { roundId: "r2", finalScore: 50, rank: 2, evaluatedAt: "" }, // dips below + { roundId: "r3", finalScore: 75, rank: 1, evaluatedAt: "" }, + ]; + + const result = evaluateTierChange("intermediate", history); + assert(result.changed === false, "No change with inconsistent scores"); +} + +// ---- Test 10: Full Profile Update Flow ---- + +function testProfileUpdate() { + section("Test 10: Full Player Profile Update"); + + const profile: PlayerProfile = { + userId: "test_user", + skillTier: "intermediate", + avgScore: 55, + roundHistory: [ + { roundId: "r1", finalScore: 62, rank: 2, evaluatedAt: "" }, + { roundId: "r2", finalScore: 68, rank: 1, evaluatedAt: "" }, + ], + commonErrors: ["Off-by-one errors"], + preferredAlgorithms: ["Binary Search"], + promotionHistory: [], + }; + + const newRound: RoundScore = { + roundId: "r3", + finalScore: 71, + rank: 1, + evaluatedAt: new Date().toISOString(), + }; + + const updated = updatePlayerProfile(profile, newRound); + + console.log(` Before: ${profile.skillTier} (avg: ${profile.avgScore})`); + console.log(` After: ${updated.skillTier} (avg: ${updated.avgScore})`); + console.log(` Rounds: ${updated.roundHistory.length}`); + + assert(updated.roundHistory.length === 3, "Round added to history"); + assert(updated.skillTier === "advanced", "Promoted to advanced after 3 rounds ≥ 60"); + assert(updated.promotionHistory.length === 1, "Promotion recorded"); + console.log(` Promotion: ${updated.promotionHistory[0].reason}`); +} + +// ---- Test 11: Score Breakdown Integrity ---- + +function testScoreBreakdownIntegrity() { + section("Test 11: Score Breakdown Adds Up to Final Score"); + + const rankings = computeRankingLocally( + MOCK_TC_RESULTS, + MOCK_SC_RESULTS, + MOCK_ORIG_RESULTS, + MOCK_TEST_CASES, + DEFAULT_WEIGHTS, + MOCK_TIEBREAKER + ); + + for (const r of rankings) { + const sumOfWeighted = + r.breakdown.timeComplexity.weighted + + r.breakdown.spaceComplexity.weighted + + r.breakdown.originality.weighted + + r.breakdown.testCases.weighted; + + const diff = Math.abs(sumOfWeighted - r.finalScore); + assert( + diff < 0.02, // allow tiny floating point drift + `${r.userId}: breakdown sum (${sumOfWeighted.toFixed(2)}) ≈ finalScore (${r.finalScore})` + ); + } +} + +// ============================================================ +// RUN ALL TESTS +// ============================================================ + +console.log("\n🏟️ Compile-n-Conquer — Evaluation Pipeline Mock Tests\n"); + +testRankingComputation(); +testTiebreaker(); +testWeightSensitivity(); +testSingleUser(); +testZeroScores(); +testTierClassification(); +testTierPromotion(); +testTierDemotion(); +testNoTierChange(); +testProfileUpdate(); +testScoreBreakdownIntegrity(); + +section("SUMMARY"); +console.log( + process.exitCode + ? " ⚠️ Some tests FAILED. Check output above." + : " 🎉 All tests PASSED!" +); +console.log(""); diff --git a/backbaord/testing.ts b/backbaord/testing.ts new file mode 100644 index 0000000..e0d77d3 --- /dev/null +++ b/backbaord/testing.ts @@ -0,0 +1,39 @@ +// Install: npm install backboard-sdk +import { BackboardClient } from 'backboard-sdk'; + +async function main() { + const apiKey = process.env.BACKBOARD_API_KEY; + if (!apiKey) throw new Error('Missing BACKBOARD_API_KEY'); + + // Initialize the Backboard client (types are bundled with the package) + const client = new BackboardClient({ apiKey }); + + // Create an assistant + const assistant = await client.createAssistant({ + name: 'My First Assistant', + system_prompt: 'A helpful assistant' + }); + + // Create a thread + const thread = await client.createThread(assistant.assistantId); + + // Send a message and get the complete response + const response = await client.addMessage(thread.threadId, { + content: 'Hello! Tell me a fun fact about space.', + llm_provider: 'google', + model_name: 'gemini-2.5-flash', + stream: false + }); + + if ('content' in response) { + console.log(response.content); + } else { + let fullContent = ''; + for await (const chunk of response) { + fullContent += chunk; + } + console.log(fullContent); + } +} + +main().catch(console.error); \ No newline at end of file diff --git a/backbaord/tools.ts b/backbaord/tools.ts new file mode 100644 index 0000000..87bd1a4 --- /dev/null +++ b/backbaord/tools.ts @@ -0,0 +1,223 @@ +// ============================================================ +// Backboard Tool Definitions for the Evaluation Pipeline +// ============================================================ + +import { BackboardToolDef } from "./types"; + +// ---- Thread 1: Time Complexity Analysis ---- +export const TIME_COMPLEXITY_TOOL: BackboardToolDef = { + type: "function", + function: { + name: "analyze_time_complexity", + description: + "Analyze and return the Big-O time complexity of the submitted code relative to the problem it solves. Return the complexity class and a numeric efficiency score (1-100) where 100 is the optimal known solution.", + parameters: { + type: "object", + properties: { + code: { + type: "string", + description: "The submitted source code to analyze", + }, + language: { + type: "string", + description: "Programming language of the submission (python, cpp, javascript, java, etc.)", + }, + questionContext: { + type: "string", + description: "The problem statement / question description for context", + }, + }, + required: ["code", "language", "questionContext"], + }, + }, +}; + +// ---- Thread 2: Space Complexity Analysis ---- +export const SPACE_COMPLEXITY_TOOL: BackboardToolDef = { + type: "function", + function: { + name: "analyze_space_complexity", + description: + "Analyze and return the Big-O space complexity (auxiliary space only, excluding input) of the submitted code. Return the complexity class and a numeric efficiency score (1-100) where 100 is optimal.", + parameters: { + type: "object", + properties: { + code: { + type: "string", + description: "The submitted source code to analyze", + }, + language: { + type: "string", + description: "Programming language of the submission", + }, + questionContext: { + type: "string", + description: "The problem statement for context", + }, + }, + required: ["code", "language", "questionContext"], + }, + }, +}; + +// ---- Thread 3: Originality & Relevance Analysis ---- +export const ORIGINALITY_TOOL: BackboardToolDef = { + type: "function", + function: { + name: "analyze_originality", + description: + "Analyze code for originality signals. Check comment density (high comment-to-code ratio = likely AI-generated), code style patterns, boilerplate detection, and relevance to the problem. Return an originality score (1-10 where 10 is clearly human-written) and a relevance score (1-10 where 10 perfectly solves the problem).", + parameters: { + type: "object", + properties: { + code: { + type: "string", + description: "The submitted source code to analyze", + }, + language: { + type: "string", + description: "Programming language of the submission", + }, + questionContext: { + type: "string", + description: "The problem statement to check relevance against", + }, + }, + required: ["code", "language", "questionContext"], + }, + }, +}; + +// ---- Thread 4: Final Ranking Aggregation ---- +export const RANKING_TOOL: BackboardToolDef = { + type: "function", + function: { + name: "compute_final_ranking", + description: + "Given all metric scores for all users in a round, compute weighted final scores and produce a ranking. Apply the provided weights to normalize scores. In case of tied final scores, use the tiebreaker data (time taken in ms — lower is better) to determine rank order.", + parameters: { + type: "object", + properties: { + timeComplexityResults: { + type: "string", + description: "JSON string of time complexity results for all users", + }, + spaceComplexityResults: { + type: "string", + description: "JSON string of space complexity results for all users", + }, + originalityResults: { + type: "string", + description: "JSON string of originality/relevance results for all users", + }, + testCaseResults: { + type: "string", + description: "JSON string of { userId: { passed, total } } from the database", + }, + weights: { + type: "string", + description: "JSON string of scoring weights { timeComplexity, spaceComplexity, originality, testCases }", + }, + tiebreakerData: { + type: "string", + description: "JSON string of { userId: timeTakenMs } for tiebreaker resolution", + }, + }, + required: [ + "timeComplexityResults", + "spaceComplexityResults", + "originalityResults", + "testCaseResults", + "weights", + "tiebreakerData", + ], + }, + }, +}; + +// ---- System Prompts ---- + +export const SYSTEM_PROMPTS = { + timeComplexity: `You are an expert algorithm analyst for a competitive coding platform called Compile-n-Conquer. + +Your job: Analyze submitted code and determine its Big-O TIME COMPLEXITY. + +For each piece of code you receive: +1. Identify the dominant operations and loops +2. Determine the worst-case time complexity as a Big-O expression +3. Score the efficiency from 1-100 (100 = optimal known solution for the problem) +4. Provide brief reasoning + +You will receive batched submissions for a round. Analyze each one using the analyze_time_complexity tool. Be strict and accurate — this is a competitive setting. + +IMPORTANT: You MUST call the analyze_time_complexity tool for EVERY submission in the batch. Do not skip any.`, + + spaceComplexity: `You are an expert algorithm analyst for a competitive coding platform called Compile-n-Conquer. + +Your job: Analyze submitted code and determine its Big-O SPACE COMPLEXITY (auxiliary space only — do not count the input). + +For each piece of code you receive: +1. Identify all extra data structures, recursion depth, and allocations +2. Determine the worst-case auxiliary space complexity as a Big-O expression +3. Score the efficiency from 1-100 (100 = optimal space usage) +4. Provide brief reasoning + +You will receive batched submissions for a round. Analyze each one using the analyze_space_complexity tool. Be strict and accurate. + +IMPORTANT: You MUST call the analyze_space_complexity tool for EVERY submission in the batch. Do not skip any.`, + + originality: `You are a code originality and relevance analyst for a competitive coding platform called Compile-n-Conquer. + +Your job: Detect whether code was likely AI-generated (e.g. from ChatGPT/Copilot) and assess how relevant it is to the problem. + +Detection signals for AI-generated code: +- Excessive inline comments explaining obvious operations +- Over-structured docstrings with parameter/return descriptions for simple functions +- Boilerplate patterns (e.g. "# Initialize variables", "# Edge case handling") +- Unnaturally consistent and verbose naming conventions +- Generic solution patterns that don't match the specific problem constraints + +Scoring: +- originalityScore (1-10): 10 = clearly human-written competitive code, 1 = obviously pasted from AI +- relevanceScore (1-10): 10 = perfectly addresses the problem, 1 = completely unrelated code +- commentDensity: percentage of comment lines vs code lines +- flags: array of detected signals + +You will receive batched submissions. Analyze each one using the analyze_originality tool. + +IMPORTANT: You MUST call the analyze_originality tool for EVERY submission in the batch.`, + + ranking: `You are the ranking engine for Compile-n-Conquer. + +You receive pre-computed scores from three analysis threads (time complexity, space complexity, originality) plus test case pass rates from the database. + +Your job: +1. Apply the provided scoring weights to compute a final score for each user +2. Average scores across all questions in the round +3. Rank users from highest to lowest final score +4. If two users have the exact same final score, the one with LOWER timeTakenMs ranks higher + +Use the compute_final_ranking tool with all the data to produce the final ranking. + +Output a clear, structured ranking with full score breakdowns.`, + + memory: `You are the Player Profile Manager for Compile-n-Conquer, a competitive coding platform. + +You maintain persistent memory about each player: +- Skill tier: beginner / intermediate / advanced / pro +- Performance history: scores, ranks, trends +- Common error patterns and weak areas +- Preferred algorithms and languages +- Coaching recommendations + +Tier classification rules: +- Beginner: avg score < 30 across recent rounds +- Intermediate: avg score 30-60 +- Advanced: avg score 60-85 +- Pro: avg score 85+ + +Promotion: 3 consecutive rounds with avg score in the next tier → promote +Demotion: 2 consecutive rounds below current tier threshold → demote + +Always provide actionable coaching feedback based on the player's history.`, +}; diff --git a/backbaord/types.ts b/backbaord/types.ts new file mode 100644 index 0000000..f54cde7 --- /dev/null +++ b/backbaord/types.ts @@ -0,0 +1,174 @@ +// ============================================================ +// Compile-n-Conquer — Evaluation Pipeline Types +// ============================================================ + +// ---- Input Types ---- + +export interface Question { + id: string; + title: string; + description: string; + difficulty: "easy" | "medium" | "hard"; +} + +export interface UserResponse { + questionId: string; + code: string; + language: string; + submittedAt: string; // ISO timestamp +} + +export interface UserSubmission { + userId: string; + responses: UserResponse[]; +} + +export interface RoundSubmissions { + roundId: string; + questions: Question[]; + submissions: UserSubmission[]; +} + +// ---- Metric Result Types ---- + +export interface ComplexityResult { + questionId: string; + complexity: string; // e.g. "O(n)", "O(n log n)" + score: number; // 1-100 + reasoning: string; +} + +export interface OriginalityResult { + questionId: string; + originalityScore: number; // 1-10 + relevanceScore: number; // 1-10 + commentDensity: number; // percentage + flags: string[]; + reasoning: string; +} + +export interface MetricResults { + roundId: string; + metric: "time_complexity" | "space_complexity" | "originality"; + results: Record; // userId -> per-question results +} + +export type TimeComplexityResults = MetricResults; +export type SpaceComplexityResults = MetricResults; +export type OriginalityResults = MetricResults; + +// ---- Test Case Types ---- + +export interface TestCaseResult { + passed: number; + total: number; +} + +export type TestCaseResults = Record; // userId -> result + +// ---- Scoring & Ranking Types ---- + +export interface ScoringWeights { + timeComplexity: number; // e.g. 0.25 + spaceComplexity: number; // e.g. 0.20 + originality: number; // e.g. 0.15 + testCases: number; // e.g. 0.40 +} + +export const DEFAULT_WEIGHTS: ScoringWeights = { + timeComplexity: 0.25, + spaceComplexity: 0.20, + originality: 0.15, + testCases: 0.40, +}; + +export interface ScoreBreakdown { + timeComplexity: { avgScore: number; weighted: number }; + spaceComplexity: { avgScore: number; weighted: number }; + originality: { avgScore: number; weighted: number }; + testCases: { passRate: number; weighted: number }; +} + +export interface RankedUser { + rank: number; + userId: string; + finalScore: number; + breakdown: ScoreBreakdown; + timeTakenMs: number; +} + +export interface RoundEvaluation { + roundId: string; + evaluationId: string; + rankings: RankedUser[]; + threadIds: { + timeComplexity: string; + spaceComplexity: string; + originality: string; + ranking: string; + }; + evaluatedAt: string; +} + +// ---- Tiebreaker ---- + +export type TiebreakerData = Record; // userId -> timeTakenMs + +// ---- Memory / Skill Tier Types ---- + +export type SkillTier = "beginner" | "intermediate" | "advanced" | "pro"; + +export interface PlayerProfile { + userId: string; + skillTier: SkillTier; + avgScore: number; + roundHistory: RoundScore[]; + commonErrors: string[]; + preferredAlgorithms: string[]; + promotionHistory: TierChange[]; +} + +export interface RoundScore { + roundId: string; + finalScore: number; + rank: number; + evaluatedAt: string; +} + +export interface TierChange { + from: SkillTier; + to: SkillTier; + reason: string; + changedAt: string; +} + +// ---- Backboard Tool Definitions ---- + +export interface BackboardToolDef { + type: "function"; + function: { + name: string; + description: string; + parameters: { + type: "object"; + properties: Record; + required: string[]; + }; + }; +} + +// ---- Evaluation Pipeline Config ---- + +export interface EvaluationConfig { + llmProvider: string; + modelName: string; + weights: ScoringWeights; + maxRetries: number; +} + +export const DEFAULT_EVAL_CONFIG: EvaluationConfig = { + llmProvider: "google", + modelName: "gemini-2.5-flash", + weights: DEFAULT_WEIGHTS, + maxRetries: 2, +}; diff --git a/components/arena/code-editor.tsx b/components/arena/code-editor.tsx new file mode 100644 index 0000000..51e56a4 --- /dev/null +++ b/components/arena/code-editor.tsx @@ -0,0 +1,257 @@ +"use client" + +import { useState } from "react" +import { Play, RotateCcw, Send, ChevronDown } from "lucide-react" + +const defaultCode = `def two_sum(nums, target): + """ + Given an array of integers nums and + an integer target, return indices of + the two numbers that add up to target. + """ + seen = {} + for i, num in enumerate(nums): + complement = target - num + if complement in seen: + return [seen[complement], i] + seen[num] = i + return [] + +# Test +print(two_sum([2, 7, 11, 15], 9)) +` + +const problemStatement = { + title: "Two Sum", + difficulty: "Easy", + tag: "Q1 / 5", + description: + "Given an array of integers nums and an integer target, return the indices of the two numbers such that they add up to target. You may assume that each input would have exactly one solution.", + examples: [ + { input: "nums = [2,7,11,15], target = 9", output: "[0, 1]" }, + { input: "nums = [3,2,4], target = 6", output: "[1, 2]" }, + ], + constraints: [ + "2 <= nums.length <= 10^4", + "-10^9 <= nums[i] <= 10^9", + "Only one valid answer exists.", + ], +} + +export function CodeEditor() { + const [code, setCode] = useState(defaultCode) + const [activeTab, setActiveTab] = useState<"problem" | "output">("problem") + const [language, setLanguage] = useState("python") + const [showLangDropdown, setShowLangDropdown] = useState(false) + const [output, setOutput] = useState("") + + const handleRun = () => { + setActiveTab("output") + setOutput("Running...\n") + setTimeout(() => { + setOutput( + `$ python solution.py\n[0, 1]\n\n---\nTest Case 1: PASSED\nTest Case 2: PASSED\nTest Case 3: PASSED\n\nAll test cases passed.\nExecution time: 4ms\nMemory: 14.2 MB` + ) + }, 1200) + } + + const handleSubmit = () => { + setActiveTab("output") + setOutput("Submitting...\n") + setTimeout(() => { + setOutput( + `$ submit solution.py\n\n[ACCEPTED]\n\nRuntime: 4ms (faster than 95.2%)\nMemory: 14.2 MB (less than 88.1%)\n\nTime Complexity: O(n)\nSpace Complexity: O(n)\n\nCode Quality Score: 87/100\n - Naming: 9/10\n - Readability: 9/10\n - Efficiency: 10/10\n - Clean Code: 8.7/10\n\n+150 XP | +25 Speed Bonus | First Blood: +50 XP` + ) + }, 1500) + } + + return ( +
+ {/* Left panel: Problem + Output */} +
+ {/* Tabs */} +
+ + +
+ + {/* Content */} +
+ {activeTab === "problem" ? ( +
+
+ {problemStatement.tag} +

+ {problemStatement.title} +

+ + {problemStatement.difficulty} + +
+

+ {problemStatement.description} +

+
+ + {"Examples"} + + {problemStatement.examples.map((ex, i) => ( +
+
+ {"Input: "} + {ex.input} +
+
+ {"Output: "} + {ex.output} +
+
+ ))} +
+
+ + {"Constraints"} + +
    + {problemStatement.constraints.map((c) => ( +
  • + {">"} + {c} +
  • + ))} +
+
+
+ ) : ( +
+              {output || "No output yet. Run your code to see results."}
+            
+ )} +
+
+ + {/* Right panel: Code Editor */} +
+ {/* Toolbar */} +
+
+ + {showLangDropdown && ( +
+ {["python", "c++", "java", "javascript"].map((lang) => ( + + ))} +
+ )} +
+
+ + + +
+
+ + {/* Line numbers + textarea */} +
+ {/* Line numbers */} +
+ {code.split("\n").map((_, i) => ( + + {i + 1} + + ))} +
+ + {/* Code area */} +