diff --git a/client/private/Advanced/Advance.js b/client/private/Advanced/Advance.js index a8767db..05bd83e 100644 --- a/client/private/Advanced/Advance.js +++ b/client/private/Advanced/Advance.js @@ -1,8 +1 @@ -function transferProblem(elem) { - const parent = elem.parentElement; - const title = parent.querySelector('h3').textContent; - const description = parent.querySelector('p').textContent; - localStorage.setItem('problemTitle', title); - localStorage.setItem('problemDescription', description); - window.location.href = window.location.origin + "/private/CoderPage/coder.html"; -} \ No newline at end of file +// Mobile menu toggle functionality \ No newline at end of file diff --git a/client/private/Arena/modules/ArenaCore.js b/client/private/Arena/modules/ArenaCore.js index c499c7e..d4a1544 100644 --- a/client/private/Arena/modules/ArenaCore.js +++ b/client/private/Arena/modules/ArenaCore.js @@ -125,7 +125,7 @@ class ArenaCore { async loadArenaStats() { try { - const response = await fetch('/api/arena/stats'); + const response = await fetch('/api/arena/system-stats'); if (response.ok) { const { stats } = await response.json(); this.uiManager.updateArenaStats(stats); diff --git a/client/private/Arena/modules/ArenaUIManager.js b/client/private/Arena/modules/ArenaUIManager.js index b9050ee..57a54ae 100644 --- a/client/private/Arena/modules/ArenaUIManager.js +++ b/client/private/Arena/modules/ArenaUIManager.js @@ -70,9 +70,9 @@ class ArenaUIManager { } updateArenaStats(stats) { - document.getElementById('onlineUsers').textContent = stats.onlineUsers; - document.getElementById('activeMatches').textContent = stats.activeMatches; - document.getElementById('totalMatches').textContent = stats.totalMatches; + document.getElementById('onlineUsers').textContent = stats.onlinePlayersCount || 0; + document.getElementById('activeMatches').textContent = stats.activeMatchesCount || 0; + document.getElementById('totalMatches').textContent = stats.totalMatchesCount || 0; } updatePlayerStats(stats) { diff --git a/client/private/CoderPage/coder.js b/client/private/CoderPage/coder.js index 6f7a4ad..2747e3c 100644 --- a/client/private/CoderPage/coder.js +++ b/client/private/CoderPage/coder.js @@ -12,7 +12,6 @@ window.addEventListener('load', async () => { // Mobile menu toggle functionality const menuToggle = document.getElementById('menu-toggle'); -const closeBtn = document.getElementById('close-btn'); const sidebar = document.getElementById('sidebar'); const outputBox = document.getElementById('output-box'); @@ -262,6 +261,7 @@ function displayProblem(problem) { }, 500); // Small delay to ensure DOM is ready } +// eslint-disable-next-line no-unused-vars async function runCode(btn) { btn.textContent = 'Running...'; btn.disabled = true; @@ -328,6 +328,7 @@ async function runCode(btn) { } // Submit solution function +// eslint-disable-next-line no-unused-vars async function submitSolution(btn) { if (!currentProblem) { alert('No problem loaded. Please try refreshing the page.'); @@ -365,18 +366,29 @@ async function submitSolution(btn) { if (data.success) { displaySubmissionResults(data.submission); - // AI assistance for failed test cases (Easy problems only) - if (aiAssistanceInstance && currentProblem && currentProblem.difficulty === 'easy' && - data.submission.status !== 'accepted' && data.submission.testResults) { + // Handle successful test completion - disable AI assistance if all tests pass + if (data.submission.status === 'accepted') { + console.log('🎉 All tests passed! Disabling AI assistance...'); - // Analyze failed test cases with AI - aiAssistanceInstance.analyzeTestCaseFailure({ - status: data.submission.status, - testCasesPassed: data.submission.testCasesPassed, - totalTestCases: data.submission.totalTestCases, - testResults: data.submission.testResults.filter(test => !test.passed), - feedback: data.submission.feedback - }); + // Disable AI assistance since the problem is solved correctly + if (aiAssistanceInstance) { + aiAssistanceInstance.disableForSuccess(); + console.log('✅ AI assistance disabled - problem solved successfully!'); + } + } else { + // AI assistance for failed test cases (Easy problems only) + if (aiAssistanceInstance && currentProblem && currentProblem.difficulty === 'easy' && + data.submission.testResults) { + + // Analyze failed test cases with AI + aiAssistanceInstance.analyzeTestCaseFailure({ + status: data.submission.status, + testCasesPassed: data.submission.testCasesPassed, + totalTestCases: data.submission.totalTestCases, + testResults: data.submission.testResults.filter(test => !test.passed), + feedback: data.submission.feedback + }); + } } // Handle different solve scenarios @@ -836,6 +848,8 @@ function displaySubmissionResults(submission) { outputBox.className = isAccepted ? 'output-success' : 'output-error'; } +// Language change function +// eslint-disable-next-line no-unused-vars function changeLang(list) { const selectedLang = list.value; diff --git a/client/private/Easy/beginer.js b/client/private/Easy/beginer.js index 7b9d32c..4928fca 100644 --- a/client/private/Easy/beginer.js +++ b/client/private/Easy/beginer.js @@ -1,6 +1,5 @@ // Mobile menu toggle functionality const menuToggle = document.getElementById('menu-toggle'); -const closeBtn = document.getElementById('close-btn'); const sidebar = document.getElementById('sidebar'); menuToggle.addEventListener('click', (e) => { @@ -9,11 +8,6 @@ menuToggle.addEventListener('click', (e) => { document.body.classList.toggle('no-scroll'); }); -closeBtn.addEventListener('click', () => { - sidebar.classList.remove('active'); - document.body.classList.remove('no-scroll'); -}); - // Close sidebar when clicking anywhere except the sidebar itself document.addEventListener('click', (e) => { // Check if the click is outside the sidebar and the menu toggle button @@ -28,11 +22,4 @@ sidebar.addEventListener('click', (e) => { e.stopPropagation(); }); -function transferProblem(elem) { - const parent = elem.parentElement; - const title = parent.querySelector('h3').textContent; - const description = parent.querySelector('p').textContent; - localStorage.setItem('problemTitle', title); - localStorage.setItem('problemDescription', description); - window.location.href = window.location.origin + "/private/CoderPage/coder.html"; -} \ No newline at end of file +// Mobile menu toggle functionality \ No newline at end of file diff --git a/client/private/HomePage/codigo.html b/client/private/HomePage/codigo.html index ac5ad78..4203194 100644 --- a/client/private/HomePage/codigo.html +++ b/client/private/HomePage/codigo.html @@ -9,6 +9,12 @@ + + +
@@ -29,7 +35,7 @@ - Logout + @@ -75,7 +81,7 @@",
- "explanation": ""
- }
- ]
-}
-
-Focus on:
-- Code completion
-- Method suggestions
-- Variable suggestions
-- Common patterns for this context
-- Problem-specific hints (without giving away the solution)
-`;
-
- const result = await this.model.generateContent(prompt);
- const responseText = result.response.text();
-
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- const suggestions = JSON.parse(jsonMatch[0]);
- return {
- success: true,
- suggestions: suggestions.suggestions || []
- };
- }
-
- return { success: true, suggestions: [] };
-
- } catch (error) {
- console.error('Contextual help error:', error);
- return { success: false, error: error.message, suggestions: [] };
- }
- }
-
- /**
- * Analyze test case failures and provide specific fixes
- * @param {string} code - User's code
- * @param {string} language - Programming language
- * @param {Object} problem - Problem context
- * @param {Object} testResults - Test results with failures
- * @returns {Object} Analysis of test failures with suggested fixes
- */
- async analyzeTestCaseFailure(code, language, problem, testResults) {
- if (!this.isEnabled) {
- return { failedTests: [] };
- }
-
- try {
- const prompt = `
-You are an AI debugging assistant helping a beginner fix failing test cases.
-
-PROBLEM: ${problem?.title || 'Coding Challenge'}
-DESCRIPTION: ${problem?.description || 'No description'}
-
-USER'S CODE:
-\`\`\`${language}
-${code}
-\`\`\`
-
-FAILED TEST RESULTS:
-${JSON.stringify(testResults, null, 2)}
-
-Analyze why the tests are failing and provide specific, actionable fixes.
-
-Return JSON:
-{
- "analysis": "Brief explanation of what's going wrong",
- "failedTests": [
- {
- "name": "",
- "expected": "",
- "actual": "",
- "issue": "",
- "suggestion": "",
- "codeChange": "",
- "line":
- }
- ],
- "commonIssues": [
- ""
- ],
- "nextSteps": [
- ""
- ]
-}
-
-Be specific about:
-- Exact code changes needed
-- Line numbers where changes should be made
-- Logic errors vs syntax errors
-- Edge cases not handled
-- Off-by-one errors
-- Input/output format issues
-`;
-
- const result = await this.model.generateContent(prompt);
- const responseText = result.response.text();
-
- const jsonMatch = responseText.match(/\{[\s\S]*\}/);
- if (jsonMatch) {
- const analysis = JSON.parse(jsonMatch[0]);
- return {
- success: true,
- analysis: analysis
- };
- }
-
- return {
- success: true,
- analysis: { failedTests: [], commonIssues: [], nextSteps: [] }
- };
-
- } catch (error) {
- console.error('Test failure analysis error:', error);
- return {
- success: false,
- error: error.message,
- analysis: { failedTests: [] }
- };
- }
- }
-
- /**
- * Format AI response to ensure consistency
- */
- formatResponse(response) {
- return {
- suggestions: response.suggestions || [],
- errors: response.errors || [],
- warnings: response.warnings || [],
- lineSpecific: response.lineSpecific || []
- };
- }
-
- /**
- * Extract suggestions from plain text response (fallback)
- */
- extractSuggestionsFromText(text, currentLine) {
- const suggestions = [];
- const errors = [];
- const warnings = [];
-
- // Simple pattern matching for common suggestions
- if (text.toLowerCase().includes('syntax error')) {
- errors.push({
- line: currentLine || 0,
- type: 'error',
- message: 'Syntax error detected',
- fix: 'Check for missing semicolons, brackets, or quotes'
- });
- }
-
- if (text.toLowerCase().includes('variable') && text.toLowerCase().includes('not defined')) {
- errors.push({
- line: currentLine || 0,
- type: 'error',
- message: 'Variable not defined',
- fix: 'Make sure to declare the variable before using it'
- });
- }
-
- return { suggestions, errors, warnings, lineSpecific: [] };
- }
-
- /**
- * Get language-specific tips for beginners
- */
- getLanguageTips(language) {
- const tips = {
- javascript: [
- "Use 'let' or 'const' instead of 'var' for better scope control",
- "Remember to end statements with semicolons",
- "Use === instead of == for strict equality",
- "Don't forget to handle async operations with await or .then()"
- ],
- python: [
- "Use proper indentation (4 spaces) for code blocks",
- "Remember that Python is case-sensitive",
- "Use descriptive variable names",
- "Don't forget colons after if, for, while, and function definitions"
- ],
- 'c++': [
- "Include necessary headers like #include ",
- "Don't forget to return 0 in main function",
- "Use proper variable types (int, string, etc.)",
- "Remember semicolons after statements"
- ],
- java: [
- "Every Java program needs a main method",
- "Class names should start with uppercase",
- "Don't forget semicolons after statements",
- "Use proper access modifiers (public, private)"
- ]
- };
-
- return tips[language] || [];
- }
-}
-
-export { AIAssistanceService };
diff --git a/server/arenaDatabase.js b/server/arenaDatabase.js
index aa4e1f6..ad6d7c7 100644
--- a/server/arenaDatabase.js
+++ b/server/arenaDatabase.js
@@ -67,34 +67,14 @@ const arenaQueueSchema = new mongoose.Schema({
status: { type: String, enum: ['waiting', 'matched'], default: 'waiting' }
});
-// Arena Player Stats Schema
-const arenaPlayerStatsSchema = new mongoose.Schema({
- userId: { type: String, required: true, unique: true },
- username: { type: String, required: true },
- totalMatches: { type: Number, default: 0 },
- wins: { type: Number, default: 0 },
- losses: { type: Number, default: 0 },
- draws: { type: Number, default: 0 },
- totalScore: { type: Number, default: 0 },
- totalBonusPoints: { type: Number, default: 0 },
- averageScore: { type: Number, default: 0 },
- winRate: { type: Number, default: 0 },
- questionsCompleted: { type: Number, default: 0 },
- fastestSolve: { type: Number }, // in seconds
- easyWins: { type: Number, default: 0 },
- mediumWins: { type: Number, default: 0 },
- hardWins: { type: Number, default: 0 },
- currentStreak: { type: Number, default: 0 },
- bestStreak: { type: Number, default: 0 },
- lastMatchAt: { type: Date },
- createdAt: { type: Date, default: Date.now },
- updatedAt: { type: Date, default: Date.now }
-});
-
class ArenaDBHandler {
static ArenaMatch = mongoose.model("ArenaMatch", arenaMatchSchema);
static ArenaQueue = mongoose.model("ArenaQueue", arenaQueueSchema);
- static ArenaPlayerStats = mongoose.model("ArenaPlayerStats", arenaPlayerStatsSchema);
+ // ArenaPlayerStats is now managed by UserStatsService
+
+ constructor(userStatsService) {
+ this.userStatsService = userStatsService;
+ }
// Matchmaking: Join queue
async joinQueue(userId, username, difficulty) {
@@ -497,47 +477,44 @@ class ArenaDBHandler {
}
}
- // Update player statistics
+ // Update player statistics using unified UserStatsService
async updatePlayerStats(match) {
try {
- const player1Stats = await this.getOrCreatePlayerStats(match.player1.userId, match.player1.username);
- const player2Stats = await this.getOrCreatePlayerStats(match.player2.userId, match.player2.username);
-
- // Update both players' stats
- for (const [player, stats] of [[match.player1, player1Stats], [match.player2, player2Stats]]) {
- stats.totalMatches += 1;
- stats.totalScore += player.score;
- stats.totalBonusPoints += player.bonusPoints;
- stats.questionsCompleted += player.questionsCompleted;
- stats.lastMatchAt = match.endedAt;
-
- if (match.winner === player.userId) {
- stats.wins += 1;
- stats.currentStreak += 1;
- stats.bestStreak = Math.max(stats.bestStreak, stats.currentStreak);
-
- // Increment difficulty-specific wins
- switch (player.selectedDifficulty) {
- case 'easy': stats.easyWins += 1; break;
- case 'medium': stats.mediumWins += 1; break;
- case 'hard': stats.hardWins += 1; break;
- }
- } else if (match.winner) {
- stats.losses += 1;
- stats.currentStreak = 0;
- } else {
- stats.draws += 1;
- }
-
- // Calculate averages
- stats.averageScore = stats.totalScore / stats.totalMatches;
- stats.winRate = (stats.wins / stats.totalMatches) * 100;
- stats.updatedAt = new Date();
+ console.log(`📊 [ArenaDBHandler] Updating arena stats for match ${match.matchId}:`);
+ console.log(` Winner: ${match.winner || 'Draw'}`);
+ console.log(` Player1: ${match.player1.username} (Score: ${match.player1.score || 0})`);
+ console.log(` Player2: ${match.player2.username} (Score: ${match.player2.score || 0})`);
+
+ // Prepare match data for UserStatsService
+ const matchData = {
+ player1: {
+ userId: match.player1.userId,
+ username: match.player1.username,
+ score: match.player1.score,
+ bonusPoints: match.player1.bonusPoints,
+ questionsCompleted: match.player1.questionsCompleted,
+ selectedDifficulty: match.player1.selectedDifficulty
+ },
+ player2: {
+ userId: match.player2.userId,
+ username: match.player2.username,
+ score: match.player2.score,
+ bonusPoints: match.player2.bonusPoints,
+ questionsCompleted: match.player2.questionsCompleted,
+ selectedDifficulty: match.player2.selectedDifficulty
+ },
+ winner: match.winner,
+ endedAt: match.endedAt
+ };
- await stats.save();
- }
+ // Update arena stats through unified service
+ const updates = await this.userStatsService.updateArenaStats(matchData);
+
+ console.log(`✅ [ArenaDBHandler] Arena stats updated for ${updates.length} players`);
+ return updates;
} catch (error) {
- console.error('[ArenaDBHandler] updatePlayerStats error:', error);
+ console.error(`❌ [ArenaDBHandler] updatePlayerStats error for match ${match.matchId}:`, error);
+ throw error;
}
}
@@ -595,51 +572,55 @@ class ArenaDBHandler {
return Math.max(0, question.timeLimit - elapsed);
}
- // Get player stats
+ // Get player stats using unified service
async getPlayerStats(userId) {
try {
- const stats = await ArenaDBHandler.ArenaPlayerStats.findOne({ userId });
- return stats || {
- totalMatches: 0,
- wins: 0,
- losses: 0,
- draws: 0,
- winRate: 0,
- averageScore: 0,
- currentStreak: 0,
- bestStreak: 0
- };
+ return await this.userStatsService.getArenaStats(userId);
} catch (error) {
console.error('[ArenaDBHandler] getPlayerStats error:', error);
throw error;
}
}
- // Get leaderboard
+ // Get leaderboard using unified service
async getArenaLeaderboard(limit = 20) {
try {
- const leaderboard = await ArenaDBHandler.ArenaPlayerStats
- .find({ totalMatches: { $gt: 0 } })
- .sort({ winRate: -1, totalScore: -1 })
- .limit(limit);
-
- return leaderboard.map((player, index) => ({
- rank: index + 1,
- username: player.username,
- totalMatches: player.totalMatches,
- wins: player.wins,
- losses: player.losses,
- draws: player.draws,
- winRate: Math.round(player.winRate * 100) / 100,
- averageScore: Math.round(player.averageScore * 100) / 100,
- currentStreak: player.currentStreak,
- bestStreak: player.bestStreak
- }));
+ return await this.userStatsService.getArenaLeaderboard(limit);
} catch (error) {
console.error('[ArenaDBHandler] getArenaLeaderboard error:', error);
return [];
}
}
+
+ // Abandon a match due to timeout or player disconnect
+ async abandonMatch(matchId, reason = 'timeout') {
+ try {
+ const match = await ArenaDBHandler.ArenaMatch.findOne({ matchId });
+ if (!match) {
+ throw new Error(`Match ${matchId} not found`);
+ }
+
+ // Don't abandon already completed matches
+ if (match.status === 'completed') {
+ console.log(`Match ${matchId} already completed, skipping abandon`);
+ return match;
+ }
+
+ // Mark as abandoned
+ match.status = 'abandoned';
+ match.endedAt = new Date();
+ match.totalDuration = (match.endedAt - match.startedAt) / 1000;
+ match.endReason = reason;
+
+ await match.save();
+
+ console.log(`Match ${matchId} abandoned due to: ${reason}`);
+ return match;
+ } catch (error) {
+ console.error('[ArenaDBHandler] abandonMatch error:', error);
+ throw error;
+ }
+ }
}
export { ArenaDBHandler };
diff --git a/server/arenaSocket.js b/server/arenaSocket.js
index 235978d..aa9a227 100644
--- a/server/arenaSocket.js
+++ b/server/arenaSocket.js
@@ -184,7 +184,46 @@ class ArenaSocketHandler {
await this.arenaDB.leaveQueue(socket.userId);
// Handle match abandonment if in active match
- // (You might want to pause the match or mark as abandoned)
+ for (const [matchId, matchData] of this.activeMatches.entries()) {
+ if (matchData.player1 === socket.userId || matchData.player2 === socket.userId) {
+ console.log(`🚫 [Arena] Player ${socket.userId} disconnected from active match ${matchId}`);
+
+ // Clear timeout for this match since it's being abandoned
+ if (matchData.timeout) {
+ clearTimeout(matchData.timeout);
+ }
+
+ // Mark match as abandoned in database
+ try {
+ await this.arenaDB.abandonMatch(matchId, socket.userId);
+ console.log(`🏃 [Arena] Match ${matchId} abandoned due to player disconnect`);
+ } catch (error) {
+ console.error(`❌ [Arena] Error abandoning match ${matchId}:`, error);
+ }
+
+ // Clean up match data
+ this.activeMatches.delete(matchId);
+ this.playerProgress.delete(matchData.player1);
+ this.playerProgress.delete(matchData.player2);
+
+ // Notify the other player if still connected
+ const otherPlayerId = matchData.player1 === socket.userId ? matchData.player2 : matchData.player1;
+ const otherSocket = this.findSocketByUserId(otherPlayerId);
+
+ if (otherSocket) {
+ otherSocket.emit('arena:opponent-disconnected', {
+ message: 'Your opponent disconnected. You win by forfeit!',
+ matchId: matchId,
+ winner: otherPlayerId
+ });
+ otherSocket.leave(`match:${matchId}`);
+ }
+
+ // Broadcast updated stats
+ await this.broadcastArenaStats();
+ break;
+ }
+ }
}
});
});
@@ -229,13 +268,23 @@ class ArenaSocketHandler {
player1Socket.emit('arena:match-found', matchData1);
player2Socket.emit('arena:match-found', matchData2);
- // Store active match
+ // Store active match with timeout
this.activeMatches.set(match.matchId, {
player1: match.player1.userId,
player2: match.player2.userId,
- currentQuestionIndex: 0
+ currentQuestionIndex: 0,
+ startTime: new Date()
});
+ // Set 30-minute match timeout
+ const matchTimeout = setTimeout(async () => {
+ console.log(`⏰ [Arena] Match ${match.matchId} timed out after 30 minutes`);
+ await this.endMatchOnTimeout(match.matchId);
+ }, 30 * 60 * 1000); // 30 minutes in milliseconds
+
+ // Store timeout reference for cleanup
+ this.activeMatches.get(match.matchId).timeout = matchTimeout;
+
console.log('🎮 [Arena] Match setup complete!');
// Broadcast updated arena stats to all clients
@@ -854,6 +903,12 @@ class ArenaSocketHandler {
this.playerProgress.delete(match.player1.userId);
this.playerProgress.delete(match.player2.userId);
+ // Clear match timeout if it exists
+ const activeMatch = this.activeMatches.get(matchId);
+ if (activeMatch && activeMatch.timeout) {
+ clearTimeout(activeMatch.timeout);
+ }
+
// Remove from active matches
this.activeMatches.delete(matchId);
@@ -870,6 +925,99 @@ class ArenaSocketHandler {
}
}
+ // Handle match timeout (30 minutes)
+ async endMatchOnTimeout(matchId) {
+ try {
+ console.log(`⏰ [Arena] Ending match ${matchId} due to timeout`);
+
+ // Get match data
+ const match = await this.arenaDB.getMatch(matchId);
+ if (!match) {
+ console.error(`❌ [Arena] Match ${matchId} not found for timeout`);
+ return;
+ }
+
+ // If match is already completed, nothing to do
+ if (match.status === 'completed') {
+ console.log(`✅ [Arena] Match ${matchId} already completed, ignoring timeout`);
+ return;
+ }
+
+ // Get current scores for both players
+ const player1Progress = this.playerProgress.get(match.player1.userId) || {
+ currentQuestionIndex: 0,
+ correctAnswers: 0,
+ totalScore: 0
+ };
+ const player2Progress = this.playerProgress.get(match.player2.userId) || {
+ currentQuestionIndex: 0,
+ correctAnswers: 0,
+ totalScore: 0
+ };
+
+ // Determine winner based on current scores
+ let winner = null;
+ if (player1Progress.totalScore > player2Progress.totalScore) {
+ winner = match.player1.userId;
+ } else if (player2Progress.totalScore > player1Progress.totalScore) {
+ winner = match.player2.userId;
+ }
+ // If scores are equal, it's a draw (winner remains null)
+
+ // End the match with timeout reason
+ await this.arenaDB.endMatch(matchId, {
+ winner,
+ player1FinalScore: player1Progress.totalScore,
+ player2FinalScore: player2Progress.totalScore,
+ player1QuestionsCompleted: player1Progress.correctAnswers,
+ player2QuestionsCompleted: player2Progress.correctAnswers,
+ endReason: 'timeout',
+ totalDuration: 30 * 60 * 1000 // 30 minutes in milliseconds
+ });
+
+ // Notify both players about the timeout
+ const player1Socket = this.findSocketByUserId(match.player1.userId);
+ const player2Socket = this.findSocketByUserId(match.player2.userId);
+
+ const timeoutMessage = {
+ type: 'timeout',
+ winner,
+ reason: 'Match ended due to 30-minute time limit',
+ finalScores: {
+ [match.player1.userId]: player1Progress.totalScore,
+ [match.player2.userId]: player2Progress.totalScore
+ },
+ questionsCompleted: {
+ [match.player1.userId]: player1Progress.correctAnswers,
+ [match.player2.userId]: player2Progress.correctAnswers
+ }
+ };
+
+ if (player1Socket) {
+ player1Socket.emit('arena:match-ended', timeoutMessage);
+ player1Socket.leave(`match:${matchId}`);
+ }
+
+ if (player2Socket) {
+ player2Socket.emit('arena:match-ended', timeoutMessage);
+ player2Socket.leave(`match:${matchId}`);
+ }
+
+ // Clean up match data
+ this.playerProgress.delete(match.player1.userId);
+ this.playerProgress.delete(match.player2.userId);
+ this.activeMatches.delete(matchId);
+
+ // Broadcast updated stats
+ await this.broadcastArenaStats();
+
+ console.log(`✅ [Arena] Match ${matchId} ended due to timeout. Winner: ${winner || 'Draw'}`);
+
+ } catch (error) {
+ console.error(`❌ [Arena] Error ending match ${matchId} on timeout:`, error);
+ }
+ }
+
// Find socket by user ID
findSocketByUserId(userId) {
for (const [socketId, socket] of this.io.sockets.sockets) {
diff --git a/server/database.js b/server/database.js
index 1ceb62a..ff6f120 100644
--- a/server/database.js
+++ b/server/database.js
@@ -29,9 +29,24 @@ class UserDBHandler {
problemsSolved: { type: Number, default: 0 },
easyCount: { type: Number, default: 0 },
mediumCount: { type: Number, default: 0 },
- hardCount: { type: Number, default: 0 }
+ hardCount: { type: Number, default: 0 },
+ realWorldCount: { type: Number, default: 0 }
});
+ // Add indexes for leaderboard performance
+ static {
+ // Compound index for leaderboard sorting (rank DESC, problemsSolved DESC, streak_count DESC)
+ UserDBHandler.userSchema.index({ rank: -1, problemsSolved: -1, streak_count: -1 });
+
+ // Index for userID lookups
+ UserDBHandler.userSchema.index({ userID: 1 });
+
+ // Individual indexes for rank position calculations
+ UserDBHandler.userSchema.index({ rank: -1 });
+ UserDBHandler.userSchema.index({ problemsSolved: -1 });
+ UserDBHandler.userSchema.index({ streak_count: -1 });
+ }
+
static Users = mongoose.model("Users", UserDBHandler.userSchema);
async middleware_userAuth(req, res, next) {
@@ -89,36 +104,7 @@ class UserDBHandler {
}
/**
- * Get leaderboard data
- * @param {Request} req The request object
- * @param {Response} res The response object
- */
- async endpoint_getLeaderboard(req, res) {
- try {
- const limit = parseInt(req.query.limit) || 50;
- const leaderboard = await this.getLeaderboard(limit);
-
- // Add ranking positions
- const rankedLeaderboard = leaderboard.map((user, index) => ({
- ...user.toObject(),
- position: index + 1
- }));
-
- res.json({
- success: true,
- leaderboard: rankedLeaderboard,
- totalUsers: rankedLeaderboard.length
- });
- } catch (error) {
- console.error('Leaderboard endpoint error:', error);
- res.status(500).json({
- success: false,
- error: 'Failed to fetch leaderboard'
- });
- }
- }
-
- /**
+ * Migrate existing users to include new stats fields
* Requires the req object to contain the param[optional] set to make sure the rank is set and not incremented
* @param {Request} req The request object
* @param {Response} res The response object
@@ -147,35 +133,6 @@ class UserDBHandler {
}
}
- /**
- * Requires the req object to contain the param[optional] set to make sure the rank is set and not incremented
- * @param {Request} req The request object
- * @param {Response} res The response object
- */
- async endpoint_incrementStreakCount(req, res) {
- if (req.params.value) {
- try {
- this.incrementStreakCount(req.auth.userId, req.params.value, req.params.set === "true");
- res.json({ message: 'ok' });
- } catch (err) {
- res.json({ error: err.toString() });
- return;
- }
- }
- else {
- res.json({ error: "Request parameter value missing" });
- }
- }
-
- async incrementStreakCount(_userId, value, set = false) {
- if (set) {
- await UserDBHandler.Users.updateOne({ userID: _userId }, { $set: { streak_count: value } });
- }
- else {
- await UserDBHandler.Users.updateOne({ userID: _userId }, { $inc: { streak_count: value } });
- }
- }
-
/**
* Requires the req object to contain the param[optional] set to make sure the rank is set and not incremented
* @param {Request} req The request object
@@ -220,209 +177,6 @@ class UserDBHandler {
}
}
- /**
- * Get leaderboard data
- * @param {number} limit Number of users to return (default: 50)
- * @returns {Array} Array of user objects sorted by rank
- */
- async getLeaderboard(limit = 50) {
- try {
- return await UserDBHandler.Users
- .find({}, {
- name: 1,
- userID: 1,
- rank: 1,
- contests_count: 1,
- streak_count: 1,
- problemsSolved: 1,
- easyCount: 1,
- mediumCount: 1,
- hardCount: 1,
- lastSolvedDate: 1,
- _id: 0
- })
- .sort({
- problemsSolved: -1,
- hardCount: -1,
- mediumCount: -1,
- easyCount: -1,
- lastSolvedDate: -1
- })
- .limit(limit);
- } catch (error) {
- console.error('[UserDBHandler Error] getLeaderboard failed:', error);
- throw error;
- }
- }
-
- /**
- * Get user statistics
- * @param {string} userId User ID
- * @returns {Object} User stats
- */
- async getUserStats(userId) {
- try {
- return await UserDBHandler.Users.findOne(
- { userID: userId },
- {
- rank: 1,
- contests_count: 1,
- streak_count: 1,
- name: 1,
- problemsSolved: 1,
- easyCount: 1,
- mediumCount: 1,
- hardCount: 1,
- lastSolvedDate: 1,
- _id: 0
- }
- );
- } catch (error) {
- console.error('[UserDBHandler Error] getUserStats failed:', error);
- throw error;
- }
- }
-
- /**
- * Update user statistics when a problem is successfully solved
- * @param {string} userId User ID
- * @param {string} difficulty Problem difficulty (easy, medium, hard)
- * @param {string} category Problem category
- * @returns {Object} Updated user stats
- */
- async updateUserOnProblemSolved(userId, difficulty, category) {
- try {
- // Calculate rank points based on difficulty
- const rankPoints = {
- 'easy': 10,
- 'medium': 25,
- 'hard': 50
- };
-
- const points = rankPoints[difficulty] || 10;
-
- // Prepare update object
- const updateObj = {
- $inc: {
- rank: points,
- problemsSolved: 1
- },
- $set: {
- lastSolvedDate: new Date()
- }
- };
-
- // Increment difficulty-specific counter
- if (difficulty === 'easy') {
- updateObj.$inc.easyCount = 1;
- } else if (difficulty === 'medium') {
- updateObj.$inc.mediumCount = 1;
- } else if (difficulty === 'hard') {
- updateObj.$inc.hardCount = 1;
- }
-
- // Update user data
- await UserDBHandler.Users.updateOne(
- { userID: userId },
- updateObj
- );
-
- // Handle streak separately
- await this.checkAndUpdateStreak(userId);
-
- // Return updated user stats
- return await this.getUserStats(userId);
- } catch (error) {
- console.error('[UserDBHandler Error] updateUserOnProblemSolved failed:', error);
- throw error;
- }
- }
-
- /**
- * API endpoint to update user stats on problem solve
- * @param {Request} req
- * @param {Response} res
- */
- async endpoint_updateOnProblemSolved(req, res) {
- try {
- const { difficulty, category } = req.body;
- const userId = req.auth?.userId;
-
- if (!userId) {
- return res.status(401).json({ success: false, error: 'Authentication required' });
- }
-
- if (!difficulty) {
- return res.status(400).json({ success: false, error: 'Difficulty is required' });
- }
-
- const updatedStats = await this.updateUserOnProblemSolved(userId, difficulty, category);
-
- res.json({
- success: true,
- message: 'Stats updated successfully',
- stats: updatedStats
- });
- } catch (error) {
- console.error('Update stats error:', error);
- res.status(500).json({ success: false, error: 'Failed to update stats' });
- }
- }
-
- /**
- * Check and update daily streak
- * @param {string} userId User ID
- */
- async checkAndUpdateStreak(userId) {
- try {
- const user = await UserDBHandler.Users.findOne({ userID: userId });
- if (!user) return;
-
- const today = new Date();
- const lastSolved = user.lastSolvedDate ? new Date(user.lastSolvedDate) : null;
-
- if (lastSolved) {
- const daysDiff = Math.floor((today - lastSolved) / (1000 * 60 * 60 * 24));
-
- if (daysDiff === 1) {
- // Continue streak
- await UserDBHandler.Users.updateOne(
- { userID: userId },
- {
- $inc: { streak_count: 1 },
- $set: { lastSolvedDate: today }
- }
- );
- } else if (daysDiff > 1) {
- // Reset streak
- await UserDBHandler.Users.updateOne(
- { userID: userId },
- {
- $set: {
- streak_count: 1,
- lastSolvedDate: today
- }
- }
- );
- }
- // If daysDiff === 0, user already solved today, don't update streak
- } else {
- // First time solving
- await UserDBHandler.Users.updateOne(
- { userID: userId },
- {
- $set: {
- streak_count: 1,
- lastSolvedDate: today
- }
- }
- );
- }
- } catch (error) {
- console.error('[UserDBHandler Error] checkAndUpdateStreak failed:', error);
- }
- }
-
/**
* Migrate existing users to include new stats fields
*/
diff --git a/server/index.js b/server/index.js
index 5246565..ca86248 100644
--- a/server/index.js
+++ b/server/index.js
@@ -8,11 +8,12 @@ import path from "path";
import { createServer } from "http";
import { MongooseConnect, UserDBHandler } from "./database.js";
+import { UserStatsService } from "./userStatsService.js";
import { CodeRunner } from "./codeRun.js";
import { ProblemDBHandler } from "./problemDatabase.js";
import { ArenaDBHandler } from "./arenaDatabase.js";
import { ArenaSocketHandler } from "./arenaSocket.js";
-import { AIAssistanceService } from "./aiAssistance.js";
+
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
@@ -29,6 +30,9 @@ const server = createServer(app);
// Initialize Arena Socket Handler
const arenaSocketHandler = new ArenaSocketHandler(server);
+// Initialize User Stats Service with arena socket handler for real-time stats
+const userStatsService = new UserStatsService(arenaSocketHandler);
+
// CORS middleware
app.use(cors({
origin: process.env.FRONTEND_URL || '*',
@@ -51,13 +55,10 @@ app.use(session({
const codeRunnerHandler = new CodeRunner();
// problem database handler -------
-const problemDBHandler = new ProblemDBHandler();
+const problemDBHandler = new ProblemDBHandler(userStatsService);
// arena database handler ----------
-const arenaDBHandler = new ArenaDBHandler();
-
-// AI assistance handler -----------
-const aiAssistanceService = new AIAssistanceService();
+const arenaDBHandler = new ArenaDBHandler(userStatsService);
// user database handler -------------
MongooseConnect.connect(process.env.MONGO_DB_URL);
@@ -66,15 +67,27 @@ const uDBHandler = new UserDBHandler();
// Run migration for existing users
uDBHandler.migrateUsersStats().catch(console.error);
+// Run migration for real-world stats
+userStatsService.migrateUsersForRealWorld().catch(console.error);
+
+// Start periodic 30-minute match timeout enforcement (every 5 minutes)
+setInterval(async () => {
+ try {
+ const cleanedCount = await userStatsService.cleanupStaleMatches();
+ if (cleanedCount > 0) {
+ console.log(`🔧 [Server] Periodic cleanup: ended ${cleanedCount} matches that exceeded 30-minute limit`);
+ }
+ } catch (error) {
+ console.error('❌ [Server] Periodic match cleanup error:', error);
+ }
+}, 5 * 60 * 1000); // Every 5 minutes
+
// authentication -----------------
app.use(clerk.clerkMiddleware());
// base entry point of server
app.use('/public', express.static('client/public'));
-// Serve test file (remove in production)
-app.use('/test-stats.html', express.static('test-stats.html'));
-
app.use('/private', clerk.requireAuth({ signInUrl: process.env.CLERK_SIGN_IN_URL, signUpUrl: process.env.CLERK_SIGN_UP_URL }),
uDBHandler.middleware_userAuth.bind(uDBHandler),
express.static('client/private'));
@@ -86,7 +99,7 @@ app.use('/api', express.json()); // Add JSON parsing middleware
// Re-enable essential endpoints
app.get('/api/incrank/:value/:set', clerk.requireAuth(), uDBHandler.endpoint_incrementUserRank.bind(uDBHandler));
-app.get('/api/incstreak/:value/:set', clerk.requireAuth(), uDBHandler.endpoint_incrementStreakCount.bind(uDBHandler));
+app.get('/api/incstreak/:value/:set', clerk.requireAuth(), userStatsService.endpoint_incrementStreakCount.bind(userStatsService));
app.get('/api/inccontest/:value/:set', clerk.requireAuth(), uDBHandler.endpoint_incrementContestsCount.bind(uDBHandler));
// Health check endpoint
@@ -115,31 +128,21 @@ app.get('/api/user/solved-problems',
// User stats endpoints
app.get('/api/userdata', clerk.requireAuth(), uDBHandler.endpoint_userData.bind(uDBHandler));
-app.get('/api/leaderboard', uDBHandler.endpoint_getLeaderboard.bind(uDBHandler));
+
+// New unified stats and leaderboard endpoints
+app.get('/api/user/stats', clerk.requireAuth(), userStatsService.endpoint_getUserStats.bind(userStatsService));
+app.get('/api/user/rank-position', clerk.requireAuth(), userStatsService.endpoint_getUserRankPosition.bind(userStatsService));
+app.get('/api/leaderboard', userStatsService.endpoint_getLeaderboard.bind(userStatsService));
app.post('/api/user/problem-solved',
express.json(),
clerk.requireAuth(),
- uDBHandler.endpoint_updateOnProblemSolved.bind(uDBHandler)
+ userStatsService.endpoint_updateOnProblemSolved.bind(userStatsService)
);
-// Arena endpoints
-app.get('/api/arena/stats', async (req, res) => {
- try {
- const stats = await arenaSocketHandler.getArenaStats();
- res.json({ success: true, stats });
- } catch (error) {
- res.status(500).json({ success: false, error: 'Failed to get arena stats' });
- }
-});
-
-app.get('/api/arena/leaderboard', async (req, res) => {
- try {
- const leaderboard = await arenaDBHandler.getArenaLeaderboard();
- res.json({ success: true, leaderboard });
- } catch (error) {
- res.status(500).json({ success: false, error: 'Failed to get arena leaderboard' });
- }
-});
+// Arena endpoints - now unified through UserStatsService
+app.get('/api/arena/stats', clerk.requireAuth(), userStatsService.endpoint_getArenaStats.bind(userStatsService));
+app.get('/api/arena/leaderboard', userStatsService.endpoint_getArenaLeaderboard.bind(userStatsService));
+app.get('/api/arena/system-stats', userStatsService.endpoint_getArenaSystemStats.bind(userStatsService));
app.get('/api/arena/player-stats/:userId', async (req, res) => {
try {
@@ -153,156 +156,6 @@ app.get('/api/arena/player-stats/:userId', async (req, res) => {
app.post('/api/run/:lang', express.json(), codeRunnerHandler.endpoint.bind(codeRunnerHandler));
-// AI Assistance endpoints
-app.post('/api/ai/analyze-code', express.json({ limit: '10mb' }), async (req, res) => {
- try {
- const { code, language, problem, currentLine } = req.body;
-
- if (!code || !language) {
- return res.status(400).json({
- success: false,
- error: 'Code and language are required'
- });
- }
-
- const analysis = await aiAssistanceService.analyzeCodeLine(code, language, problem, currentLine);
- res.json({ success: true, analysis });
- } catch (error) {
- console.error('AI Analysis Error:', error);
- res.status(500).json({
- success: false,
- error: 'AI analysis failed'
- });
- }
-});
-
-app.post('/api/ai/code-completion', express.json(), async (req, res) => {
- try {
- const { code, language, cursorLine, cursorColumn } = req.body;
-
- if (!code || !language || cursorLine === undefined || cursorColumn === undefined) {
- return res.status(400).json({
- success: false,
- error: 'Code, language, and cursor position are required'
- });
- }
-
- const completions = await aiAssistanceService.getCodeCompletion(code, language, cursorLine, cursorColumn);
- res.json({ success: true, completions });
- } catch (error) {
- console.error('Code Completion Error:', error);
- res.status(500).json({
- success: false,
- error: 'Code completion failed'
- });
- }
-});
-
-app.post('/api/ai/suggest-fix', express.json(), async (req, res) => {
- try {
- const { code, language, errorMessage } = req.body;
-
- if (!code || !language || !errorMessage) {
- return res.status(400).json({
- success: false,
- error: 'Code, language, and error message are required'
- });
- }
-
- const fixSuggestion = await aiAssistanceService.suggestFix(code, language, errorMessage);
- res.json({ success: true, fixSuggestion });
- } catch (error) {
- console.error('Fix Suggestion Error:', error);
- res.status(500).json({
- success: false,
- error: 'Fix suggestion failed'
- });
- }
-});
-
-app.get('/api/ai/language-tips/:language', async (req, res) => {
- try {
- const { language } = req.params;
- const tips = aiAssistanceService.getLanguageTips(language);
- res.json({ success: true, tips });
- } catch (error) {
- console.error('Language Tips Error:', error);
- res.status(500).json({
- success: false,
- error: 'Failed to get language tips'
- });
- }
-});
-
-// Enhanced AI Assistance endpoints for real-time per-line help
-app.post('/api/ai/real-time-analysis', express.json({ limit: '10mb' }), async (req, res) => {
- try {
- const { code, currentLine, currentLineText, language, problem } = req.body;
-
- if (!code || currentLine === undefined || !language) {
- return res.status(400).json({
- success: false,
- error: 'Code, currentLine, and language are required'
- });
- }
-
- const analysis = await aiAssistanceService.performRealTimeAnalysis(
- code, currentLine, currentLineText, language, problem
- );
- res.json(analysis);
- } catch (error) {
- console.error('Real-time Analysis Error:', error);
- res.status(500).json({
- success: false,
- error: 'Real-time analysis failed'
- });
- }
-});
-
-app.post('/api/ai/contextual-help', express.json(), async (req, res) => {
- try {
- const { line, token, cursor, language, problem } = req.body;
-
- if (!line || !language || !cursor) {
- return res.status(400).json({
- success: false,
- error: 'Line, language, and cursor position are required'
- });
- }
-
- const help = await aiAssistanceService.getContextualHelp(line, token, cursor, language, problem);
- res.json(help);
- } catch (error) {
- console.error('Contextual Help Error:', error);
- res.status(500).json({
- success: false,
- error: 'Contextual help failed'
- });
- }
-});
-
-app.post('/api/ai/analyze-test-failure', express.json({ limit: '10mb' }), async (req, res) => {
- try {
- const { code, language, problem, testResults } = req.body;
-
- if (!code || !language || !testResults) {
- return res.status(400).json({
- success: false,
- error: 'Code, language, and test results are required'
- });
- }
-
- const analysis = await aiAssistanceService.analyzeTestCaseFailure(code, language, problem, testResults);
- res.json(analysis);
- } catch (error) {
- console.error('Test Failure Analysis Error:', error);
- res.status(500).json({
- success: false,
- error: 'Test failure analysis failed'
- });
- }
-});
-
// main redirect
app.get('/', (req, res) => {
res.redirect('/public/LandingPage/');
diff --git a/server/problemDatabase.js b/server/problemDatabase.js
index 020e316..581b983 100644
--- a/server/problemDatabase.js
+++ b/server/problemDatabase.js
@@ -6,7 +6,7 @@ const problemSchema = new mongoose.Schema({
problemId: { type: String, unique: true, required: true },
title: { type: String, required: true },
description: { type: String, required: true },
- difficulty: { type: String, enum: ['easy', 'medium', 'hard'], required: true },
+ difficulty: { type: String, enum: ['easy', 'medium', 'hard', 'real-world'], required: true },
category: { type: String, required: true },
tags: [String],
constraints: String,
@@ -63,6 +63,10 @@ class ProblemDBHandler {
static Submissions = mongoose.model("Submissions", submissionSchema);
static UserProblemSolved = mongoose.model("UserProblemSolved", userProblemSolvedSchema);
+ constructor(userStatsService) {
+ this.userStatsService = userStatsService;
+ }
+
// Get all problems with filtering
async getProblems(filters = {}) {
try {
@@ -306,6 +310,8 @@ class ProblemDBHandler {
problemId,
code,
language,
+ difficulty: problem.difficulty,
+ category: problem.category,
...validationResult
});
@@ -315,6 +321,16 @@ class ProblemDBHandler {
if (validationResult.status === 'accepted') {
if (!alreadySolved) {
+ // Ensure difficulty and category are not undefined
+ if (!problem.difficulty || !problem.category) {
+ console.error('[SUBMISSION ERROR] Problem missing difficulty or category:', {
+ problemId: problem.problemId,
+ difficulty: problem.difficulty,
+ category: problem.category
+ });
+ throw new Error(`Problem ${problemId} has missing difficulty or category`);
+ }
+
// Mark as solved and update stats only for first-time solve
isFirstSolve = await this.markProblemAsSolved(
userId,
@@ -326,9 +342,8 @@ class ProblemDBHandler {
if (isFirstSolve) {
try {
- // Update user stats for first-time solve
- const userDBHandler = new UserDBHandler();
- updatedStats = await userDBHandler.updateUserOnProblemSolved(
+ // Update user stats for first-time solve using new stats service
+ updatedStats = await this.userStatsService.updateUserOnProblemSolved(
userId,
problem.difficulty,
problem.category
@@ -539,23 +554,6 @@ class ProblemDBHandler {
}
);
- // If solution is accepted, track in userProblemSolved
- if (submissionData.status === 'accepted') {
- await ProblemDBHandler.UserProblemSolved.updateOne(
- { userId: submissionData.userId, problemId: submissionData.problemId },
- {
- $setOnInsert: {
- difficulty: submissionData.difficulty,
- category: submissionData.category,
- firstSolvedAt: new Date(),
- bestSubmissionId: submissionId
- },
- $inc: { totalAttempts: 1 }
- },
- { upsert: true }
- );
- }
-
return submission;
} catch (error) {
console.error('Create submission error:', error);
diff --git a/server/seedDatabase.js b/server/seedDatabase.js
index e2fd642..dda7c15 100644
--- a/server/seedDatabase.js
+++ b/server/seedDatabase.js
@@ -1,6 +1,7 @@
-const mongoose = require("mongoose");
-const { ProblemDBHandler } = require("./problemDatabase.js");
-require('dotenv').config();
+import mongoose from "mongoose";
+import { ProblemDBHandler } from "./problemDatabase.js";
+import dotenv from 'dotenv';
+dotenv.config();
// Initial problem data - extracted from all HTML files
const initialProblems = [
@@ -411,9 +412,9 @@ const initialProblems = [
problemId: "tic-tac-toe",
title: "Tic Tac Toe Game",
description: "Implement a two-player Tic Tac Toe game with a console or GUI interface.",
- difficulty: "easy",
+ difficulty: "real-world",
category: "games",
- tags: ["games", "logic", "arrays"],
+ tags: ["games", "logic", "arrays", "real-world"],
constraints: "3x3 grid, two players, check win conditions",
examples: [
{
@@ -431,9 +432,9 @@ const initialProblems = [
problemId: "hangman-game",
title: "Hangman Game",
description: "Create a word-guessing game where players try to guess a hidden word letter by letter.",
- difficulty: "easy",
+ difficulty: "real-world",
category: "games",
- tags: ["games", "strings", "logic"],
+ tags: ["games", "strings", "logic", "real-world"],
constraints: "Word length: 3-10 characters, maximum 6 wrong guesses",
examples: [
{
@@ -451,9 +452,9 @@ const initialProblems = [
problemId: "number-guessing",
title: "Number Guessing Game",
description: "Develop a game where the player guesses a randomly generated number within a limited range.",
- difficulty: "easy",
+ difficulty: "real-world",
category: "games",
- tags: ["games", "random", "loops"],
+ tags: ["games", "random", "loops", "real-world"],
constraints: "Number range: 1-100, maximum 7 attempts",
examples: [
{
@@ -473,9 +474,9 @@ const initialProblems = [
problemId: "todo-list",
title: "To-Do List Application",
description: "Develop a to-do list app with features to add, delete, and mark tasks as complete.",
- difficulty: "easy",
+ difficulty: "real-world",
category: "web",
- tags: ["web", "crud", "javascript"],
+ tags: ["web", "crud", "javascript", "real-world"],
constraints: "Support CRUD operations, data persistence",
examples: [
{
@@ -493,9 +494,9 @@ const initialProblems = [
problemId: "weather-app",
title: "Weather App",
description: "Create an application that fetches and displays weather data from a public API.",
- difficulty: "medium",
+ difficulty: "real-world",
category: "web",
- tags: ["web", "api", "json"],
+ tags: ["web", "api", "json", "real-world"],
constraints: "Use a weather API, handle API errors, display temperature and conditions",
examples: [
{
@@ -513,9 +514,9 @@ const initialProblems = [
problemId: "expense-tracker",
title: "Expense Tracker",
description: "Build an app where users can log expenses, view charts, and manage their budget.",
- difficulty: "medium",
+ difficulty: "real-world",
category: "web",
- tags: ["web", "finance", "data-visualization"],
+ tags: ["web", "finance", "data-visualization", "real-world"],
constraints: "Categories for expenses, date tracking, budget limits",
examples: [
{
@@ -535,9 +536,9 @@ const initialProblems = [
problemId: "sentiment-analysis",
title: "Sentiment Analysis Tool",
description: "Analyze text (e.g., tweets) to detect positive/negative mood using natural language processing.",
- difficulty: "hard",
+ difficulty: "real-world",
category: "ai",
- tags: ["ai", "nlp", "machine-learning"],
+ tags: ["ai", "nlp", "machine-learning", "real-world"],
constraints: "Classify text as positive, negative, or neutral",
examples: [
{
@@ -556,9 +557,9 @@ const initialProblems = [
problemId: "spam-classifier",
title: "Spam Message Classifier",
description: "Train a model to classify messages as spam or not using machine learning techniques.",
- difficulty: "medium",
+ difficulty: "real-world",
category: "ai",
- tags: ["ai", "classification", "text-processing"],
+ tags: ["ai", "classification", "text-processing", "real-world"],
constraints: "Binary classification: spam or not spam",
examples: [
{
@@ -653,8 +654,8 @@ async function seedDatabase() {
}
// Run the seeding function
-if (require.main === module) {
+if (import.meta.url === `file://${process.argv[1]}`) {
seedDatabase();
}
-module.exports = { seedDatabase, initialProblems };
+export { seedDatabase, initialProblems };
diff --git a/server/userStatsService.js b/server/userStatsService.js
new file mode 100644
index 0000000..ee4025d
--- /dev/null
+++ b/server/userStatsService.js
@@ -0,0 +1,891 @@
+import { UserDBHandler } from './database.js';
+import mongoose from 'mongoose';
+
+// Arena Player Stats Schema - Import the schema here for consistency
+const arenaPlayerStatsSchema = new mongoose.Schema({
+ userId: { type: String, required: true, unique: true },
+ username: { type: String, required: true },
+ totalMatches: { type: Number, default: 0 },
+ wins: { type: Number, default: 0 },
+ losses: { type: Number, default: 0 },
+ draws: { type: Number, default: 0 },
+ totalScore: { type: Number, default: 0 },
+ totalBonusPoints: { type: Number, default: 0 },
+ averageScore: { type: Number, default: 0 },
+ winRate: { type: Number, default: 0 },
+ questionsCompleted: { type: Number, default: 0 },
+ fastestSolve: { type: Number }, // in seconds
+ easyWins: { type: Number, default: 0 },
+ mediumWins: { type: Number, default: 0 },
+ hardWins: { type: Number, default: 0 },
+ currentStreak: { type: Number, default: 0 },
+ bestStreak: { type: Number, default: 0 },
+ lastMatchAt: { type: Date },
+ createdAt: { type: Date, default: Date.now },
+ updatedAt: { type: Date, default: Date.now }
+});
+
+/**
+ * Common User Statistics Service
+ * Handles user stats logic for both homepage and leaderboard
+ */
+class UserStatsService {
+
+ constructor(arenaSocketHandler = null) {
+ // Initialize ArenaPlayerStats model (check if already exists to avoid conflicts)
+ try {
+ this.ArenaPlayerStats = mongoose.model("ArenaPlayerStats");
+ } catch (error) {
+ // Model doesn't exist, create it
+ this.ArenaPlayerStats = mongoose.model("ArenaPlayerStats", arenaPlayerStatsSchema);
+ }
+
+ // Store reference to arena socket handler for system stats
+ this.arenaSocketHandler = arenaSocketHandler;
+ }
+
+ /**
+ * Get comprehensive user statistics
+ * @param {string} userId - User ID
+ * @returns {Object} User stats object
+ */
+ async getUserStats(userId) {
+ try {
+ const user = await UserDBHandler.Users.findOne({ userID: userId });
+
+ if (!user) {
+ return {
+ problemsSolved: 0,
+ easyCount: 0,
+ mediumCount: 0,
+ hardCount: 0,
+ realWorldCount: 0,
+ rank: 0,
+ streak_count: 0
+ };
+ }
+
+ return {
+ problemsSolved: user.problemsSolved || 0,
+ easyCount: user.easyCount || 0,
+ mediumCount: user.mediumCount || 0,
+ hardCount: user.hardCount || 0,
+ realWorldCount: user.realWorldCount || 0,
+ rank: user.rank || 0,
+ streak_count: user.streak_count || 0,
+ lastSolvedDate: user.lastSolvedDate
+ };
+ } catch (error) {
+ console.error('[UserStatsService Error] Failed to get user stats:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Get leaderboard with user statistics and correct ranking positions
+ * @param {number} limit - Number of users to return
+ * @returns {Array} Array of user objects with stats and rank positions
+ */
+ async getLeaderboard(limit = 50) {
+ try {
+ const users = await UserDBHandler.Users.find({})
+ .sort({
+ rank: -1,
+ problemsSolved: -1,
+ streak_count: -1,
+ userID: 1 // Add userID as a tiebreaker for consistent ordering
+ })
+ .select('userID name problemsSolved easyCount mediumCount hardCount realWorldCount rank streak_count lastSolvedDate');
+
+ // Calculate rank positions with proper tie handling
+ const leaderboardWithRanks = [];
+ let currentRank = 1;
+
+ for (let i = 0; i < users.length; i++) {
+ const user = users[i];
+
+ // If this is not the first user, check if stats are different from previous user
+ if (i > 0) {
+ const prevUser = users[i - 1];
+ if (user.rank !== prevUser.rank ||
+ user.problemsSolved !== prevUser.problemsSolved ||
+ user.streak_count !== prevUser.streak_count) {
+ currentRank = i + 1; // Update rank position only when stats differ
+ }
+ // If stats are the same, keep the same rank as previous user
+ }
+
+ leaderboardWithRanks.push({
+ userID: user.userID,
+ name: user.name,
+ problemsSolved: user.problemsSolved || 0,
+ easyCount: user.easyCount || 0,
+ mediumCount: user.mediumCount || 0,
+ hardCount: user.hardCount || 0,
+ realWorldCount: user.realWorldCount || 0,
+ rank: user.rank || 0, // Keep original rank points for reference
+ rankPosition: currentRank, // Proper rank position with tie handling
+ streak_count: user.streak_count || 0,
+ lastSolvedDate: user.lastSolvedDate
+ });
+ }
+
+ return leaderboardWithRanks.slice(0, limit);
+ } catch (error) {
+ console.error('[UserStatsService Error] Failed to get leaderboard:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Update user stats when a problem is solved
+ * @param {string} userId - User ID
+ * @param {string} difficulty - Problem difficulty
+ * @param {string} category - Problem category
+ * @returns {Object} Updated user stats
+ */
+ async updateUserOnProblemSolved(userId, difficulty, category) {
+ try {
+ const rankPoints = {
+ 'easy': 10,
+ 'medium': 25,
+ 'hard': 50,
+ 'real-world': 30 // Real-world projects get medium-high points
+ };
+
+ const points = rankPoints[difficulty] || 10;
+
+ // Prepare update object
+ const updateObj = {
+ $inc: {
+ rank: points,
+ problemsSolved: 1
+ },
+ $set: {
+ lastSolvedDate: new Date()
+ }
+ };
+
+ // Increment difficulty-specific counter
+ if (difficulty === 'easy') {
+ updateObj.$inc.easyCount = 1;
+ } else if (difficulty === 'medium') {
+ updateObj.$inc.mediumCount = 1;
+ } else if (difficulty === 'hard') {
+ updateObj.$inc.hardCount = 1;
+ } else if (difficulty === 'real-world') {
+ updateObj.$inc.realWorldCount = 1;
+ }
+
+ // Update user data
+ await UserDBHandler.Users.updateOne(
+ { userID: userId },
+ updateObj,
+ { upsert: true }
+ );
+
+ // Handle streak separately
+ await this.checkAndUpdateStreak(userId);
+
+ // Return updated user stats
+ return await this.getUserStats(userId);
+ } catch (error) {
+ console.error('[UserStatsService Error] updateUserOnProblemSolved failed:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Check and update daily streak
+ * @param {string} userId User ID
+ */
+ async checkAndUpdateStreak(userId) {
+ try {
+ const user = await UserDBHandler.Users.findOne({ userID: userId });
+ if (!user) return;
+
+ const today = new Date();
+ const lastSolved = user.lastSolvedDate ? new Date(user.lastSolvedDate) : null;
+
+ if (lastSolved) {
+ const daysDiff = Math.floor((today - lastSolved) / (1000 * 60 * 60 * 24));
+
+ if (daysDiff === 1) {
+ // Continue streak
+ await UserDBHandler.Users.updateOne(
+ { userID: userId },
+ {
+ $inc: { streak_count: 1 },
+ $set: { lastSolvedDate: today }
+ }
+ );
+ } else if (daysDiff > 1) {
+ // Reset streak
+ await UserDBHandler.Users.updateOne(
+ { userID: userId },
+ {
+ $set: {
+ streak_count: 1,
+ lastSolvedDate: today
+ }
+ }
+ );
+ }
+ // If daysDiff === 0, user already solved today, don't update streak
+ } else {
+ // First time solving
+ await UserDBHandler.Users.updateOne(
+ { userID: userId },
+ {
+ $set: {
+ streak_count: 1,
+ lastSolvedDate: today
+ }
+ }
+ );
+ }
+ } catch (error) {
+ console.error('[UserStatsService Error] checkAndUpdateStreak failed:', error);
+ }
+ }
+
+ /**
+ * Increment or set streak count (for compatibility with existing endpoint)
+ * @param {string} userId User ID
+ * @param {number} value Value to increment or set
+ * @param {boolean} set Whether to set (true) or increment (false)
+ */
+ async incrementStreakCount(userId, value, set = false) {
+ try {
+ if (set) {
+ await UserDBHandler.Users.updateOne({ userID: userId }, { $set: { streak_count: value } });
+ } else {
+ await UserDBHandler.Users.updateOne({ userID: userId }, { $inc: { streak_count: value } });
+ }
+ } catch (error) {
+ console.error('[UserStatsService Error] incrementStreakCount failed:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * API endpoint to get user stats
+ */
+ async endpoint_getUserStats(req, res) {
+ try {
+ const userId = req.auth?.userId;
+ if (!userId) {
+ return res.status(401).json({ success: false, error: 'Authentication required' });
+ }
+
+ const stats = await this.getCombinedUserStats(userId);
+ res.json({ success: true, stats });
+ } catch (error) {
+ console.error('[UserStatsService] endpoint_getUserStats error:', error);
+ res.status(500).json({ success: false, error: 'Failed to get user stats' });
+ }
+ }
+
+ /**
+ * API endpoint to get arena stats only
+ */
+ async endpoint_getArenaStats(req, res) {
+ try {
+ const userId = req.auth?.userId;
+ if (!userId) {
+ return res.status(401).json({ success: false, error: 'Authentication required' });
+ }
+
+ const arenaStats = await this.getArenaStats(userId);
+ res.json({ success: true, stats: arenaStats });
+ } catch (error) {
+ console.error('[UserStatsService] endpoint_getArenaStats error:', error);
+ res.status(500).json({ success: false, error: 'Failed to get arena stats' });
+ }
+ }
+
+ /**
+ * API endpoint to get leaderboard
+ */
+ async endpoint_getLeaderboard(req, res) {
+ try {
+ const leaderboard = await this.getLeaderboard();
+ res.json({ success: true, leaderboard });
+ } catch (error) {
+ console.error('[UserStatsService] endpoint_getLeaderboard error:', error);
+ res.status(500).json({ success: false, error: 'Failed to get leaderboard' });
+ }
+ }
+
+ /**
+ * API endpoint to get arena leaderboard
+ */
+ async endpoint_getArenaLeaderboard(req, res) {
+ try {
+ const arenaLeaderboard = await this.getArenaLeaderboard();
+ res.json({ success: true, leaderboard: arenaLeaderboard });
+ } catch (error) {
+ console.error('[UserStatsService] endpoint_getArenaLeaderboard error:', error);
+ res.status(500).json({ success: false, error: 'Failed to get arena leaderboard' });
+ }
+ }
+
+ /**
+ * API endpoint to update problem solved stats
+ */
+ async endpoint_updateOnProblemSolved(req, res) {
+ try {
+ const { difficulty, category } = req.body;
+ const userId = req.auth?.userId;
+
+ if (!userId) {
+ return res.status(401).json({ success: false, error: 'Authentication required' });
+ }
+
+ if (!difficulty || !category) {
+ return res.status(400).json({ success: false, error: 'Difficulty and category are required' });
+ }
+
+ const updatedStats = await this.updateUserOnProblemSolved(userId, difficulty, category);
+ res.json({ success: true, stats: updatedStats });
+ } catch (error) {
+ console.error('[UserStatsService] endpoint_updateOnProblemSolved error:', error);
+ res.status(500).json({ success: false, error: 'Failed to update user stats' });
+ }
+ }
+
+ /**
+ * API endpoint to increment streak count
+ */
+ async endpoint_incrementStreakCount(req, res) {
+ try {
+ const { value, set } = req.params;
+ const userId = req.auth?.userId;
+
+ if (!userId) {
+ return res.status(401).json({ success: false, error: 'Authentication required' });
+ }
+
+ const updateValue = parseInt(value) || 0;
+ const updateObj = set === '1' ?
+ { $set: { streak_count: updateValue } } :
+ { $inc: { streak_count: updateValue } };
+
+ await UserDBHandler.Users.updateOne(
+ { userID: userId },
+ updateObj
+ );
+
+ const updatedStats = await this.getUserStats(userId);
+ res.json({ success: true, stats: updatedStats });
+ } catch (error) {
+ console.error('[UserStatsService] endpoint_incrementStreakCount error:', error);
+ res.status(500).json({ success: false, error: 'Failed to update streak count' });
+ }
+ }
+
+ /**
+ * Migrate existing users to include realWorldCount field
+ */
+ async migrateUsersForRealWorld() {
+ try {
+ const result = await UserDBHandler.Users.updateMany(
+ { realWorldCount: { $exists: false } },
+ { $set: { realWorldCount: 0 } }
+ );
+
+ console.log(`[UserStatsService] Migrated ${result.modifiedCount} users with realWorldCount field`);
+ return result.modifiedCount;
+ } catch (error) {
+ console.error('[UserStatsService Error] Migration failed:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Get arena statistics for a user
+ * @param {string} userId - User ID
+ * @returns {Object} Arena stats object
+ */
+ async getArenaStats(userId) {
+ try {
+ const arenaStats = await this.ArenaPlayerStats.findOne({ userId });
+
+ if (!arenaStats) {
+ return {
+ totalMatches: 0,
+ wins: 0,
+ losses: 0,
+ draws: 0,
+ winRate: 0,
+ averageScore: 0,
+ totalScore: 0,
+ questionsCompleted: 0,
+ currentStreak: 0,
+ bestStreak: 0,
+ easyWins: 0,
+ mediumWins: 0,
+ hardWins: 0,
+ lastMatchAt: null
+ };
+ }
+
+ return {
+ totalMatches: arenaStats.totalMatches || 0,
+ wins: arenaStats.wins || 0,
+ losses: arenaStats.losses || 0,
+ draws: arenaStats.draws || 0,
+ winRate: arenaStats.winRate || 0,
+ averageScore: arenaStats.averageScore || 0,
+ totalScore: arenaStats.totalScore || 0,
+ questionsCompleted: arenaStats.questionsCompleted || 0,
+ currentStreak: arenaStats.currentStreak || 0,
+ bestStreak: arenaStats.bestStreak || 0,
+ easyWins: arenaStats.easyWins || 0,
+ mediumWins: arenaStats.mediumWins || 0,
+ hardWins: arenaStats.hardWins || 0,
+ lastMatchAt: arenaStats.lastMatchAt
+ };
+ } catch (error) {
+ console.error('[UserStatsService Error] Failed to get arena stats:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Get combined user stats (problems + arena) including rank position
+ * @param {string} userId - User ID
+ * @returns {Object} Combined stats object with rank position
+ */
+ async getCombinedUserStats(userId) {
+ try {
+ const [problemStatsWithRank, arenaStats] = await Promise.all([
+ this.getUserStatsWithRankPosition(userId),
+ this.getArenaStats(userId)
+ ]);
+
+ return {
+ // Problem solving stats
+ problemsSolved: problemStatsWithRank.problemsSolved,
+ easyCount: problemStatsWithRank.easyCount,
+ mediumCount: problemStatsWithRank.mediumCount,
+ hardCount: problemStatsWithRank.hardCount,
+ realWorldCount: problemStatsWithRank.realWorldCount,
+ rank: problemStatsWithRank.rank, // Accumulated rank points
+ rankPosition: problemStatsWithRank.rankPosition, // Actual leaderboard position
+ streak_count: problemStatsWithRank.streak_count,
+ lastSolvedDate: problemStatsWithRank.lastSolvedDate,
+
+ // Arena stats
+ arena: arenaStats
+ };
+ } catch (error) {
+ console.error('[UserStatsService Error] Failed to get combined stats:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Update arena stats when a match is completed
+ * @param {Object} matchData - Match data with player results
+ * @returns {Object} Updated arena stats for both players
+ */
+ async updateArenaStats(matchData) {
+ try {
+ const { player1, player2, winner, endedAt } = matchData;
+
+ const updates = [];
+
+ // Update both players
+ for (const player of [player1, player2]) {
+ const isWinner = winner === player.userId;
+ const isLoser = winner && winner !== player.userId;
+ const isDraw = !winner;
+
+ // Ensure player has username - get from Users collection if needed
+ let username = player.username;
+ if (!username) {
+ const user = await UserDBHandler.Users.findOne({ userID: player.userId });
+ username = user?.name || `User_${player.userId.slice(-8)}`;
+ }
+
+ const updateObj = {
+ $inc: {
+ totalMatches: 1,
+ totalScore: player.score || 0,
+ totalBonusPoints: player.bonusPoints || 0,
+ questionsCompleted: player.questionsCompleted || 0
+ },
+ $set: {
+ username: username, // Ensure username is always set
+ lastMatchAt: endedAt || new Date(),
+ updatedAt: new Date()
+ }
+ };
+
+ // Handle win/loss/draw
+ if (isWinner) {
+ updateObj.$inc.wins = 1;
+ updateObj.$inc.currentStreak = 1;
+
+ // Increment difficulty-specific wins
+ const difficultyWinField = `${player.selectedDifficulty}Wins`;
+ updateObj.$inc[difficultyWinField] = 1;
+ } else if (isLoser) {
+ updateObj.$inc.losses = 1;
+ updateObj.$set.currentStreak = 0;
+ } else if (isDraw) {
+ updateObj.$inc.draws = 1;
+ }
+
+ // Update player stats
+ const result = await this.ArenaPlayerStats.updateOne(
+ { userId: player.userId },
+ updateObj,
+ { upsert: true }
+ );
+
+ // Calculate derived stats
+ await this.updateArenaDerivedStats(player.userId);
+
+ updates.push({
+ userId: player.userId,
+ updated: result.modifiedCount > 0 || result.upsertedCount > 0
+ });
+ }
+
+ return updates;
+ } catch (error) {
+ console.error('[UserStatsService Error] updateArenaStats failed:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Update derived arena stats (averages, win rate, best streak)
+ * @param {string} userId - User ID
+ */
+ async updateArenaDerivedStats(userId) {
+ try {
+ const stats = await this.ArenaPlayerStats.findOne({ userId });
+ if (!stats) return;
+
+ const updates = {};
+
+ // Calculate averages
+ if (stats.totalMatches > 0) {
+ updates.averageScore = Math.round((stats.totalScore / stats.totalMatches) * 100) / 100;
+ updates.winRate = Math.round((stats.wins / stats.totalMatches) * 10000) / 100; // 2 decimal places
+ }
+
+ // Update best streak
+ if (stats.currentStreak > stats.bestStreak) {
+ updates.bestStreak = stats.currentStreak;
+ }
+
+ // Apply updates if any
+ if (Object.keys(updates).length > 0) {
+ await this.ArenaPlayerStats.updateOne(
+ { userId },
+ { $set: updates }
+ );
+ }
+ } catch (error) {
+ console.error('[UserStatsService Error] updateArenaDerivedStats failed:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Get arena leaderboard
+ * @param {number} limit - Number of users to return
+ * @returns {Array} Array of arena stats sorted by wins
+ */
+ async getArenaLeaderboard(limit = 50) {
+ try {
+ const leaderboard = await this.ArenaPlayerStats.find({})
+ .sort({ wins: -1, winRate: -1, totalScore: -1 })
+ .limit(limit)
+ .select('userId username totalMatches wins losses draws winRate averageScore currentStreak bestStreak');
+
+ return leaderboard.map(stats => ({
+ userId: stats.userId,
+ username: stats.username,
+ totalMatches: stats.totalMatches,
+ wins: stats.wins,
+ losses: stats.losses,
+ draws: stats.draws,
+ winRate: stats.winRate,
+ averageScore: stats.averageScore,
+ currentStreak: stats.currentStreak,
+ bestStreak: stats.bestStreak
+ }));
+ } catch (error) {
+ console.error('[UserStatsService Error] Failed to get arena leaderboard:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Clean up stale arena matches and enforce 30-minute time limit
+ * This prevents zombie matches and enforces the core arena rule: all matches end in 30 minutes max
+ */
+ async cleanupStaleMatches() {
+ try {
+ const ArenaMatch = mongoose.model("ArenaMatch");
+ const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000);
+
+ // Find all matches that are still active but started more than 30 minutes ago
+ const staleMatches = await ArenaMatch.find({
+ status: { $in: ['waiting', 'in_progress'] },
+ startedAt: { $lt: thirtyMinutesAgo }
+ });
+
+ let cleanedCount = 0;
+
+ for (const match of staleMatches) {
+ try {
+ // Calculate final scores based on current progress
+ const player1Score = match.player1.score || 0;
+ const player2Score = match.player2.score || 0;
+
+ // Determine winner based on scores at timeout
+ let winner = null;
+ if (player1Score > player2Score) {
+ winner = match.player1.userId;
+ } else if (player2Score > player1Score) {
+ winner = match.player2.userId;
+ }
+ // If equal scores, it's a draw (winner stays null)
+
+ // End the match due to timeout
+ const updateResult = await ArenaMatch.updateOne(
+ { matchId: match.matchId },
+ {
+ $set: {
+ status: 'completed',
+ endedAt: new Date(),
+ endReason: 'timeout',
+ totalDuration: 30 * 60, // 30 minutes in seconds
+ winner: winner,
+ 'player1.finalScore': player1Score,
+ 'player2.finalScore': player2Score,
+ updatedAt: new Date()
+ }
+ }
+ );
+
+ if (updateResult.modifiedCount > 0) {
+ cleanedCount++;
+
+ // Update player arena stats for both players
+ const matchData = {
+ player1: {
+ userId: match.player1.userId,
+ username: match.player1.username,
+ selectedDifficulty: match.player1.selectedDifficulty,
+ score: player1Score,
+ questionsCompleted: match.player1.questionsCompleted || 0,
+ bonusPoints: 0
+ },
+ player2: {
+ userId: match.player2.userId,
+ username: match.player2.username,
+ selectedDifficulty: match.player2.selectedDifficulty,
+ score: player2Score,
+ questionsCompleted: match.player2.questionsCompleted || 0,
+ bonusPoints: 0
+ },
+ winner: winner,
+ endedAt: new Date()
+ };
+
+ // Update arena stats
+ await this.updateArenaStats(matchData);
+
+ console.log(`✅ [UserStatsService] Match ${match.matchId} ended due to 30-minute timeout. Winner: ${winner || 'Draw'}`);
+ }
+ } catch (matchError) {
+ console.error(`❌ [UserStatsService] Error ending match ${match.matchId}:`, matchError);
+ }
+ }
+
+ if (cleanedCount > 0) {
+ console.log(`[UserStatsService] Cleaned up ${cleanedCount} matches that exceeded 30-minute limit`);
+ }
+
+ return cleanedCount;
+ } catch (error) {
+ console.error('[UserStatsService Error] Failed to cleanup stale matches:', error);
+ return 0;
+ }
+ }
+
+ /**
+ * Get arena system statistics with automatic 30-minute timeout enforcement
+ * @returns {Object} Arena system stats
+ */
+ async getArenaSystemStats() {
+ try {
+ // Always run cleanup to enforce 30-minute rule (30% chance to reduce server load)
+ if (Math.random() < 0.3) {
+ await this.cleanupStaleMatches();
+ }
+
+ // If we have arena socket handler, use its real-time stats
+ if (this.arenaSocketHandler) {
+ const arenaStats = await this.arenaSocketHandler.getArenaStats();
+ return {
+ onlinePlayersCount: arenaStats.onlineUsers || 0,
+ activeMatchesCount: arenaStats.activeMatches || 0,
+ totalMatchesCount: arenaStats.totalMatches || 0
+ };
+ }
+
+ // Fallback to database queries if no socket handler
+ const ArenaQueue = mongoose.model("ArenaQueue");
+ const onlinePlayersCount = await ArenaQueue.countDocuments({});
+
+ // Get active matches count (only in_progress, to match ArenaSocketHandler)
+ const ArenaMatch = mongoose.model("ArenaMatch");
+ const activeMatchesCount = await ArenaMatch.countDocuments({
+ status: 'in_progress'
+ });
+
+ // Get total completed matches
+ const totalMatchesCount = await ArenaMatch.countDocuments({
+ status: 'completed'
+ });
+
+ return {
+ onlinePlayersCount,
+ activeMatchesCount,
+ totalMatchesCount
+ };
+ } catch (error) {
+ console.error('[UserStatsService Error] Failed to get arena system stats:', error);
+ return {
+ onlinePlayersCount: 0,
+ activeMatchesCount: 0,
+ totalMatchesCount: 0
+ };
+ }
+ }
+
+ /**
+ * API endpoint to get arena system stats
+ */
+ async endpoint_getArenaSystemStats(req, res) {
+ try {
+ const stats = await this.getArenaSystemStats();
+ res.json({ success: true, stats });
+ } catch (error) {
+ console.error('[UserStatsService] endpoint_getArenaSystemStats error:', error);
+ res.status(500).json({ success: false, error: 'Failed to get arena system stats' });
+ }
+ }
+
+ /**
+ * Get user's rank position in the leaderboard
+ * @param {string} userId - User ID
+ * @returns {number} User's rank position (1-based)
+ */
+ async getUserRankPosition(userId) {
+ try {
+ // First get the target user's stats
+ const targetUser = await UserDBHandler.Users.findOne({ userID: userId })
+ .select('rank problemsSolved streak_count');
+
+ if (!targetUser) {
+ return 0; // User not found
+ }
+
+ const targetRank = targetUser.rank || 0;
+ const targetProblems = targetUser.problemsSolved || 0;
+ const targetStreak = targetUser.streak_count || 0;
+
+ // Get all users with better stats (ordered the same way as leaderboard)
+ const betterUsers = await UserDBHandler.Users.find({
+ $or: [
+ // Users with higher rank points
+ { rank: { $gt: targetRank } },
+
+ // Users with same rank points but more problems solved
+ {
+ rank: targetRank,
+ problemsSolved: { $gt: targetProblems }
+ },
+
+ // Users with same rank points and problems solved but higher streak
+ {
+ rank: targetRank,
+ problemsSolved: targetProblems,
+ streak_count: { $gt: targetStreak }
+ }
+ ]
+ }).sort({
+ rank: -1,
+ problemsSolved: -1,
+ streak_count: -1,
+ userID: 1
+ });
+
+ // Count users with identical stats (ties) that come before this user (by userID)
+ const tiedUsers = await UserDBHandler.Users.find({
+ rank: targetRank,
+ problemsSolved: targetProblems,
+ streak_count: targetStreak,
+ userID: { $lt: userId } // Users with lexicographically smaller userID come first
+ }).countDocuments();
+
+ // Return 1-based position (1st place = 1, 2nd place = 2, etc.)
+ return betterUsers.length + tiedUsers + 1;
+ } catch (error) {
+ console.error('[UserStatsService Error] Failed to get user rank position:', error);
+ return 0;
+ }
+ }
+
+ /**
+ * Get comprehensive user statistics including leaderboard rank position
+ * @param {string} userId - User ID
+ * @returns {Object} User stats object with rank position
+ */
+ async getUserStatsWithRankPosition(userId) {
+ try {
+ const [basicStats, rankPosition] = await Promise.all([
+ this.getUserStats(userId),
+ this.getUserRankPosition(userId)
+ ]);
+
+ return {
+ ...basicStats,
+ rankPosition: rankPosition // Actual position in leaderboard (1st, 2nd, 3rd, etc.)
+ };
+ } catch (error) {
+ console.error('[UserStatsService Error] Failed to get user stats with rank position:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * API endpoint to get user's rank position in leaderboard
+ */
+ async endpoint_getUserRankPosition(req, res) {
+ try {
+ const userId = req.auth?.userId;
+ if (!userId) {
+ return res.status(401).json({ success: false, error: 'Authentication required' });
+ }
+
+ const rankPosition = await this.getUserRankPosition(userId);
+ res.json({ success: true, rankPosition });
+ } catch (error) {
+ console.error('[UserStatsService] endpoint_getUserRankPosition error:', error);
+ res.status(500).json({ success: false, error: 'Failed to get user rank position' });
+ }
+ }
+}
+
+// Export the class and let the main server create the instance with dependencies
+export { UserStatsService };