From 452a42f28f029329514ace243b07e165bec90bf3 Mon Sep 17 00:00:00 2001 From: Darlene Santes Date: Mon, 28 Jul 2025 15:58:49 -0500 Subject: [PATCH 01/12] Enhance user profile management: update profile method to include avatar settings and add new route for profile setup --- client/src/services/api/authService.js | 29 +++++++++++-------- server/models/user.js | 9 ++++++ server/routes/userRoutes.js | 40 ++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/client/src/services/api/authService.js b/client/src/services/api/authService.js index 3f944a0..35cd90a 100644 --- a/client/src/services/api/authService.js +++ b/client/src/services/api/authService.js @@ -77,18 +77,23 @@ class AuthService { // Update profile async updateProfile(profileData) { try { - const response = await api.put('/api/auth/profile', profileData); - const { user } = response.data; - - // Update stored user data - localStorage.setItem('codeclash_user', JSON.stringify(user)); - - return { success: true, user }; - } catch (error) { - return { - success: false, - error: error.response?.data?.message || 'Profile update failed' - }; + const user = this.getCurrentUser(); + if (!user) { + throw new Error('User not found'); + } + const response = await api.put('/api/user/profile', { + ...profileData, + googleId: user.googleId, + }); + + const {user: updatedUser} = response.data; + localStorage.setItem('codeclash_user', JSON.stringify(updatedUser)); + return { success: true, user: updatedUser }; + } catch (error) { + return { + success: false, + error: error.response?.data?.message || 'Failed to update profile' + }; } } diff --git a/server/models/user.js b/server/models/user.js index 58a7f3b..5715eaa 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -53,6 +53,15 @@ module.exports = (sequelize) => { allowNull: true // This will be filled in during profile setup // Possible values = improve problem solving, learn new algorithms, practice coding interviews, etc. }, + // User Avatar + avatarTheme: { + type: DataTypes.STRING, + allowNull: true // This will be filled in during profile setup + }, + avatarColor: { + type: DataTypes.STRING, + allowNull: true // This will be filled in during profile setup + }, setupComplete: { type: DataTypes.BOOLEAN, // Indicates whether the user has completed their profile setup defaultValue: false // Default to false when the user first signs up diff --git a/server/routes/userRoutes.js b/server/routes/userRoutes.js index 1ced72f..09daff7 100644 --- a/server/routes/userRoutes.js +++ b/server/routes/userRoutes.js @@ -13,23 +13,59 @@ const express = require("express"); const app = express(); const router = express.Router(); app.use(express.json()); +const { User } = require('../models'); const { findOrCreateUser, updateUserPreferences, updateMatchStats, getUserStats } = require('../controllers/userController'); // Route to handle Google OAuth login router.post('/auth/google', async (req, res) => { const { googleId } = req.body; - console.log("Received Google ID:", googleId); // πŸ‘ˆ add this + console.log("Received Google ID:", googleId); try { const user = await findOrCreateUser(googleId); res.json(user); } catch (err) { - console.error("πŸ”₯ Error in /auth/google:", err); // πŸ‘ˆ add this + console.error("πŸ”₯ Error in /auth/google:", err); res.status(500).json({ error: 'Failed to find or create user' }); } }); +// route to set profile up +router.put('/profile', async (req, res) => { + try { + const { + googleId, + avatarTheme, + avatarColor, + favoriteLanguages, + skillLevel, + goals, + setupComplete + } = req.body; + console.log('Updating user profile with data:', req.body); // debugging log + + const user = await User.findOne({ where: { googleId } }); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + await user.update({ + avatarTheme, + avatarColor, + favoriteLanguages, + skillLevel, + goals, + setupComplete + }); + + console.log('User profile updated successfully'); + res.json({ user }); + } catch (error) { + console.error('Error updating user profile:', error); + res.status(500).json({ error: 'Failed to update profile' }); + } +}); + // Route to update user preferences router.put('/update-preferences/:id', async (req, res) => { From 1118580cfee26d97dc1ff223019803d1dd8adfbd Mon Sep 17 00:00:00 2001 From: pavelnavarro Date: Mon, 28 Jul 2025 15:25:36 -0600 Subject: [PATCH 02/12] fixed full 1v1 match flow with room creation, join logic, and start_game handling --- client/src/app.jsx | 9 +++ client/src/pages/LandingPage.jsx | 11 +++- client/src/pages/TestRoomPage.jsx | 98 +++++++++++++++++++++++++++++++ server/app.js | 4 ++ server/sockets/matchSocket.js | 77 +++++++++++++++++++++++- 5 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 client/src/pages/TestRoomPage.jsx diff --git a/client/src/app.jsx b/client/src/app.jsx index 416cd5c..afea8a3 100644 --- a/client/src/app.jsx +++ b/client/src/app.jsx @@ -8,6 +8,7 @@ import authService from './services/api/authService'; // FIXED: Capital S import './styles/global.css'; import GameRoom from './pages/GameRoom'; import MatchLobby from './pages/MatchLobby'; +import TestRoomPage from "./pages/TestRoomPage"; // Create context FIRST export const AppContext = React.createContext(); @@ -179,6 +180,14 @@ const App = () => { /> ); + case 'test-room': + return ( + + ); + default: return ( { Watch Demo - + + {/* Add Test Room Button */} + + {/* End test room button */} + {/* Quick Stats */}
diff --git a/client/src/pages/TestRoomPage.jsx b/client/src/pages/TestRoomPage.jsx new file mode 100644 index 0000000..0e97741 --- /dev/null +++ b/client/src/pages/TestRoomPage.jsx @@ -0,0 +1,98 @@ +import React, { useEffect, useState, useContext } from "react"; +import GameSocket from "../sockets/socket"; +import { AppContext } from "../app"; // πŸ‘ˆ Import context + +const TestRoomPage = () => { + const [roomCode, setRoomCode] = useState(""); + const [createdCode, setCreatedCode] = useState(""); + const { user } = useContext(AppContext); // πŸ‘ˆ Get actual logged-in user + + useEffect(() => { + console.log("🟒 Mounting TestRoomPage"); + console.log("=== SOCKET CONNECTION DEBUG ==="); + console.log("Connecting to: http://localhost:3001"); + console.log("User data:", user); + + GameSocket.connect(user); + + const onConnect = () => { + console.log("Socket is ready, setting up event listeners..."); + + GameSocket.socket.on("room_created", ({ roomCode }) => { + console.log("ROOM CREATED:", roomCode); + setCreatedCode(roomCode); + }); + + GameSocket.socket.on("room_joined", ({ roomCode, playerCount }) => { + console.log("JOINED ROOM:", roomCode, "Players:", playerCount); + }); + + GameSocket.socket.on("start_game", ({ problem }) => { + console.log("START GAME:", problem); + }); + + GameSocket.socket.on("room_error", ({ message }) => { + console.error("ROOM ERROR:", message); + }); + }; + + GameSocket.on("connection_status", ({ status }) => { + if (status === "connected") { + onConnect(); + } + }); + + return () => { + GameSocket.disconnect(); + }; + }, [user]); + + const handleCreateRoom = () => { + console.log("πŸ”˜ handleCreateRoom clicked"); + GameSocket.createRoom({ + difficulty: "easy", + creator: user + }); + }; + + const handleJoinRoom = () => { + GameSocket.joinRoom(roomCode, user); + }; + + return ( +
+

Code Clash Test Room

+ + + + {createdCode && ( +

+ Your room code: {createdCode} +

+ )} + +
+ setRoomCode(e.target.value.toUpperCase())} + className="px-2 py-1 text-black rounded" + /> + +
+
+ ); +}; + +export default TestRoomPage; diff --git a/server/app.js b/server/app.js index 2d62fc4..66ea181 100644 --- a/server/app.js +++ b/server/app.js @@ -3,6 +3,7 @@ const express = require("express"); const http = require("http"); const cors = require("cors"); const { Server } = require("socket.io"); +const matchSocketHandler = require("./sockets/matchSocket"); require('dotenv').config(); @@ -51,6 +52,9 @@ const io = new Server(server, { io.on("connection", (socket) => { console.log(`User connected: ${socket.id}`); + + matchSocketHandler(socket, io); + socket.on("disconnect", () => { console.log(`User disconnected: ${socket.id}`); }); diff --git a/server/sockets/matchSocket.js b/server/sockets/matchSocket.js index 264f855..400c251 100644 --- a/server/sockets/matchSocket.js +++ b/server/sockets/matchSocket.js @@ -1,12 +1,17 @@ // Store matches in memory (matchCode -> [sockets]) const { runJudge0 } = require("../utils/judge0Helper"); -const matchRooms = {}; -const matchData = {}; const { buildJsWrappedCode, formatTestCasesAsStdin } = require("../utils/wrappers"); const problems = require("../data/problems.json"); +const matchRooms = {}; +const matchData = {}; + +//Utility: generate random room codes +function generateRoomCode() { + return Math.random().toString(36).substring(2, 8).toUpperCase(); +} async function runAllTestCases(code, languageId, testCases) { const results = []; @@ -39,6 +44,74 @@ function matchSocketHandler(socket, io) { //sends welcome message to new users socket.emit("server_message", "Welcome to Code Clash!"); + //Create Room (Private) + socket.on("create_room", ({ difficulty, creator }) => { + console.log("πŸ”₯ Received create_room from:", creator?.username); + + let roomCode; + + do { + roomCode = generateRoomCode(); + } while (matchRooms[roomCode]); + + matchRooms[roomCode] = [socket]; + matchData[roomCode] = { + difficulty, + players: [creator], + createdAt: Date.now() + }; + + socket.join(roomCode); + socket.data.username = creator.username; + socket.data.userId = creator.id; + + console.log(`${creator.username} created room ${roomCode}`); + socket.emit("room_created", { roomCode }); + }); + + // βœ… Join Room + socket.on("join_room", ({ roomCode, player }) => { + const room = matchRooms[roomCode]; + + if (!room || room.length >= 2) { + socket.emit("room_error", { message: "Room not found or full" }); + return; + } + + matchRooms[roomCode].push(socket); + matchData[roomCode].players.push(player); + socket.join(roomCode); + socket.data.username = player.username; + socket.data.userId = player.id; + + console.log(`${player.username} joined room ${roomCode}`); + + io.to(roomCode).emit("room_joined", { + roomCode, + playerCount: matchRooms[roomCode].length + }); + + // Start game when 2 players are in + if (matchRooms[roomCode].length === 2) { + const difficulty = matchData[roomCode].difficulty || 'easy'; + const problem = problems.find(p => p.difficulty === difficulty) || problems[0]; + + matchData[roomCode].problem = problem; + + console.log("πŸ”₯ [matchSocket] BOTH PLAYERS JOINED β€” starting game in room", roomCode); + console.log("πŸ“¦ [matchSocket] Problem being sent:", problem.id, problem.title); + + io.to(roomCode).emit("start_game", { + problem + }); + } + }); + + // πŸ§ͺ Test messaging + socket.on("client_message", (msg) => { + console.log(`Message from client ${socket.id}:`, msg); + }); + //User wants to join a match socket.on ("join_match",({ matchCode, username}) => { socket.data.username = username; // Store username on socket From bc3e780a15767ac4af4640eaca6ca21d4d5e5194 Mon Sep 17 00:00:00 2001 From: pavelnavarro Date: Mon, 28 Jul 2025 21:16:29 -0600 Subject: [PATCH 03/12] removed mock data from gameroom and added problem display --- client/src/app.jsx | 14 +- client/src/pages/GameRoom.jsx | 101 ++--- client/src/pages/LandingPage.jsx | 11 +- client/src/pages/MatchLobby.jsx | 98 ++-- client/src/sockets/socket.js | 6 + package-lock.json | 1 + server/data/problems.json | 750 +++++++++++++++++++++++++++++++ server/package.json | 9 +- server/routes/gameRoutes.js | 10 +- server/sockets/matchSocket.js | 108 ++--- 10 files changed, 938 insertions(+), 170 deletions(-) diff --git a/client/src/app.jsx b/client/src/app.jsx index afea8a3..464ce43 100644 --- a/client/src/app.jsx +++ b/client/src/app.jsx @@ -17,6 +17,7 @@ const App = () => { const [currentPage, setCurrentPage] = useState('landing'); const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [roomData, setRoomData] = useState(null); // Initialize app with proper auth service useEffect(() => { @@ -55,8 +56,11 @@ const App = () => { }, []); // Navigation function - const navigate = (page) => { - console.log('Navigating to:', page); + const navigate = (page,payload) => { + console.log('Navigating to:', page, payload); + if (page === 'game-room') { + setRoomData(payload); + } setCurrentPage(page); }; @@ -110,7 +114,9 @@ const App = () => { navigate, handleLogin, handleLogout, - currentPage + currentPage, + roomData, + setRoomData }; // Page routing @@ -167,7 +173,7 @@ const App = () => { ); diff --git a/client/src/pages/GameRoom.jsx b/client/src/pages/GameRoom.jsx index 4158796..4f13669 100644 --- a/client/src/pages/GameRoom.jsx +++ b/client/src/pages/GameRoom.jsx @@ -39,31 +39,7 @@ const GameRoom = ({ navigate, user, roomData }) => { const progressTimeoutRef = useRef(null); // Sample problem (will come from server) - const [problem, setProblem] = useState({ - title: "Two Sum", - difficulty: "Easy", - description: `Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target. - -You may assume that each input would have exactly one solution, and you may not use the same element twice. - -You can return the answer in any order.`, - examples: [ - { - input: "nums = [2,7,11,15], target = 9", - output: "[0,1]", - explanation: "Because nums[0] + nums[1] == 9, we return [0, 1]." - }, - { - input: "nums = [3,2,4], target = 6", - output: "[1,2]" - } - ], - constraints: [ - "2 ≀ nums.length ≀ 104", - "-109 ≀ nums[i] ≀ 109", - "-109 ≀ target ≀ 109" - ] - }); + const [problem, setProblem] = useState(null); // Starter code const starterCode = { @@ -93,24 +69,6 @@ You can return the answer in any order.`, gameSocket.connect(user); } - // Join match if we have roomData with matchCode (William's logic) - if (roomData?.matchCode) { - console.log('Joining match with code:', roomData.matchCode); - gameSocket.socket.emit("join_match", { - matchCode: roomData.matchCode, - username: user?.username || 'Anonymous' - }); - } - - // Join room if we have roomData with roomCode - if (roomData?.roomCode) { - console.log('Joining room with code:', roomData.roomCode); - gameSocket.joinRoom(roomData.roomCode, { - id: user?.id, - username: user?.username - }); - } - /** * Socket Event Handlers */ @@ -133,10 +91,12 @@ You can return the answer in any order.`, // William's start_game event const handleStartGame = (data) => { - console.log('Game starting with problem:', data); + console.log('πŸ”₯ [GameRoom] start_game payload:', data); setGameState('active'); if (data.problem) { + console.log('πŸ“¦ [GameRoom] setting problem:', data.problem); setProblem(data.problem); + setCode(data.problem.functionSignature[language] || ''); } setTimeLeft(data.timeLimit || 600); setChatMessages([{ @@ -307,6 +267,18 @@ You can return the answer in any order.`, }; }, [user, roomData]); + useEffect(() => { + if (roomData?.problem) { + console.log('πŸ’‘ [GameRoom] init from navigation:', roomData.problem); + setProblem(roomData.problem); + setGameState('active'); + setCode(roomData.problem.functionSignature[language] || ''); + if (roomData.timeLimit) { + setTimeLeft(roomData.timeLimit); + } + } +}, [roomData, language]); + /** * Timer effect - handles countdown */ @@ -327,14 +299,15 @@ You can return the answer in any order.`, } }, [gameState, timeLeft]); - /** - * Initialize code when language changes - */ + //Restart de code every time you change language useEffect(() => { - setCode(starterCode[language] || ''); - }, [language]); + if (problem) { + setCode(problem.functionSignature[language] || ''); + } + }, [language, problem]); + - /** + /* * Handle typing indicators and progress updates */ const handleCodeChange = useCallback((newCode) => { @@ -494,7 +467,18 @@ You can return the answer in any order.`, /** * Render problem panel */ - const renderProblemPanel = () => ( +const renderProblemPanel = () => { + // show loading until we find problem + if (!problem) { + return ( +
+

Waiting for problem…

+
+ ); + } + + // once we get it, we renderize it + return (
@@ -508,7 +492,7 @@ You can return the answer in any order.`,

{problem.description}

- {problem.examples.map((example, index) => ( + {problem.examples?.map((example, index) => (
Example {index + 1}:
@@ -524,15 +508,16 @@ You can return the answer in any order.`,
Constraints:
-
    - {problem.constraints.map((constraint, index) => ( -
  • β€’ {constraint}
  • - ))} -
+
    + {problem.constraints?.map((constraint, index) => ( +
  • {constraint}
  • + ))} +
+
-
); +}; /** * Render code editor panel diff --git a/client/src/pages/LandingPage.jsx b/client/src/pages/LandingPage.jsx index a0895bc..4148d48 100644 --- a/client/src/pages/LandingPage.jsx +++ b/client/src/pages/LandingPage.jsx @@ -223,16 +223,7 @@ const CodeClashLanding = ({ navigate, user }) => { Watch Demo
- - {/* Add Test Room Button */} - - {/* End test room button */} - + {/* Quick Stats */}
diff --git a/client/src/pages/MatchLobby.jsx b/client/src/pages/MatchLobby.jsx index e903462..3ec755d 100644 --- a/client/src/pages/MatchLobby.jsx +++ b/client/src/pages/MatchLobby.jsx @@ -25,6 +25,10 @@ const MatchLobby = ({ navigate, user, mode = 'quick' }) => { const [success, setSuccess] = useState(''); const [connectionStatus, setConnectionStatus] = useState('disconnected'); const [playersInQueue, setPlayersInQueue] = useState(0); + + const [language, setLanguage] = useState('python'); // python by defect + const [category, setCategory] = useState(''); // empty means any + const topics = ['arrays','linked list','graphs','trees']; // Available difficulty levels for problems const difficulties = [ @@ -93,12 +97,7 @@ const MatchLobby = ({ navigate, user, mode = 'quick' }) => { const handleMatchFound = (data) => { console.log('Match found:', data); setIsSearching(false); - setSuccess('Match found! Starting game...'); - - // Navigate to game room after brief success message - setTimeout(() => { - navigate('game-room', { matchData: data }); - }, 1500); + setSuccess('Match found! Waiting for both players…'); }; const handleMatchCancelled = (data) => { @@ -124,12 +123,14 @@ const MatchLobby = ({ navigate, user, mode = 'quick' }) => { console.log('Room joined:', data); setSuccess('Room joined! Waiting for opponent...'); - // If room has 2 players, start the game - if (data.playerCount >= 2) { - setTimeout(() => { - navigate('game-room', { roomData: data }); - }, 1000); - } + }; + + const handleStartGame = (gameData) => { + console.log('πŸ”₯ [MatchLobby] start_game received:', gameData); + navigate('game-room', { + roomCode, + ...gameData + }); }; const handleRoomError = (data) => { @@ -137,10 +138,6 @@ const MatchLobby = ({ navigate, user, mode = 'quick' }) => { setError(data.message || 'Room error occurred.'); }; - const handleGameStarted = (data) => { - console.log('Game started:', data); - navigate('game-room', { gameData: data }); - }; const handleError = (data) => { console.error('Socket error:', data); @@ -154,8 +151,9 @@ const MatchLobby = ({ navigate, user, mode = 'quick' }) => { gameSocket.on('queue_update', handleQueueUpdate); gameSocket.on('room_created', handleRoomCreated); gameSocket.on('room_joined', handleRoomJoined); + gameSocket.on('start_game', handleStartGame); gameSocket.on('room_error', handleRoomError); - gameSocket.on('game_started', handleGameStarted); + //gameSocket.on('game_started', handleGameStarted); gameSocket.on('error', handleError); // Cleanup function - removes event listeners when component unmounts @@ -168,8 +166,9 @@ const MatchLobby = ({ navigate, user, mode = 'quick' }) => { gameSocket.off('queue_update', handleQueueUpdate); gameSocket.off('room_created', handleRoomCreated); gameSocket.off('room_joined', handleRoomJoined); + gameSocket.off('start_game', handleStartGame); gameSocket.off('room_error', handleRoomError); - gameSocket.off('game_started', handleGameStarted); + //gameSocket.off('game_started', handleGameStarted); gameSocket.off('error', handleError); // Leave any active matchmaking when component unmounts @@ -200,23 +199,23 @@ const MatchLobby = ({ navigate, user, mode = 'quick' }) => { * Create a private room with selected difficulty */ const generateRoomCode = () => { - // Check connection before attempting to create room - if (!gameSocket.isSocketConnected()) { - setError('Please wait for connection to establish.'); - return; - } - - console.log('Creating room with difficulty:', difficulty); - - // Use GameSocket class method to create room - gameSocket.createRoom({ - difficulty: difficulty, - creator: { - id: user?.id, - username: user?.username + if (!gameSocket.isSocketConnected()) { + setError('Please wait for connection to establish.'); + return; } - }); - }; + + console.log('Creating room with:', { difficulty, language, category }); + + gameSocket.createRoom({ + difficulty, + language, + category, + creator: { + id: user.id, + username: user.username + } + }); + }; /** * Copy room code to clipboard for sharing @@ -434,6 +433,37 @@ const MatchLobby = ({ navigate, user, mode = 'quick' }) => {
+ {/* Topic Selection */} +
+

Choose Topic

+
+ {topics.map(t => ( + + ))} + {/* BotΓ³n para "Any" */} + +
+
+
- {/* Test Results */} - {testResults.length > 0 && ( -
-
Test Results:
-
- {testResults.map((result) => ( -
- {result.passed ? ( - - ) : ( - - )} - - Test {result.id}: {result.input} - - {!result.passed && ( - - Expected {result.expected}, got {result.actual} - - )} -
- ))} -
+{/* Test Results */} +{testResults.length > 0 && ( +
+
Test Results:
+
+ {testResults.map(r => ( +
+ {r.passed ? ( + + ) : ( + + )} + {/* AquΓ­ mostramos la lΓ­nea completa del servidor */} + {r.text}
- )} + ))} +
+ {/* Resumen igual al estilo que prefieres */} +
+ Result: {testResults.filter(r => r.passed).length}/{testResults.length} tests passed.{" "} + {testResults.every(r => r.passed) + ? All passed πŸŽ‰ + : Some failed βœ—} +
+
+)} +
); diff --git a/client/src/pages/MatchLobby.jsx b/client/src/pages/MatchLobby.jsx index 3ec755d..8be9fe1 100644 --- a/client/src/pages/MatchLobby.jsx +++ b/client/src/pages/MatchLobby.jsx @@ -121,15 +121,17 @@ const MatchLobby = ({ navigate, user, mode = 'quick' }) => { const handleRoomJoined = (data) => { console.log('Room joined:', data); + setRoomCode(data.roomCode); setSuccess('Room joined! Waiting for opponent...'); }; - const handleStartGame = (gameData) => { - console.log('πŸ”₯ [MatchLobby] start_game received:', gameData); + const handleStartGame = ({ roomCode: rc, problem, timeLimit }) => { + console.log('πŸ”₯ [MatchLobby] start_game received:', { rc, problem, timeLimit }); navigate('game-room', { - roomCode, - ...gameData + roomCode: rc, + problem, + timeLimit: timeLimit || 600 }); }; diff --git a/client/src/sockets/socket.js b/client/src/sockets/socket.js index 2ca80bb..7f16468 100644 --- a/client/src/sockets/socket.js +++ b/client/src/sockets/socket.js @@ -108,6 +108,7 @@ class GameSocket { console.log('πŸ”” [GameSocket] raw start_game:', data); this.emit('start_game', data); }); + this.socket.on('opponent_joined', (data) => { console.log('Opponent joined:', data); this.emit('opponent_joined', data); @@ -267,8 +268,9 @@ class GameSocket { return; } - console.log('Submitting solution'); + console.log('Submitting solution', solutionData); this.socket.emit('submit_solution', { + roomCode: solutionData.roomCode, code: solutionData.code, language: solutionData.language, submissionTime: Date.now() diff --git a/server/sockets/matchSocket.js b/server/sockets/matchSocket.js index b7edfe8..ca0f27a 100644 --- a/server/sockets/matchSocket.js +++ b/server/sockets/matchSocket.js @@ -3,10 +3,17 @@ const axios = require('axios'); const { runJudge0 } = require("../utils/judge0Helper"); const { buildJsWrappedCode, - formatTestCasesAsStdin + formatTestCasesAsStdin, + buildPythonWrapper } = require("../utils/wrappers"); const matchRooms = {}; const matchData = {}; +const LANGUAGE_ID = { + javascript: 63, + python: 71, + java: 62, + cpp: 54 +}; //Utility: generate random room codes function generateRoomCode() { @@ -64,6 +71,7 @@ function matchSocketHandler(socket, io) { }; socket.join(roomCode); + socket.data.roomCode = roomCode; socket.data.username = creator.username; socket.data.userId = creator.id; @@ -83,6 +91,7 @@ function matchSocketHandler(socket, io) { matchRooms[roomCode].push(socket); matchData[roomCode].players.push(player); socket.join(roomCode); + socket.data.roomCode = roomCode; socket.data.username = player.username; socket.data.userId = player.id; @@ -110,7 +119,9 @@ function matchSocketHandler(socket, io) { ); io.to(roomCode).emit("start_game", { - problem + roomCode, + problem, + timeLimit: 600 }); } }); @@ -146,57 +157,179 @@ function matchSocketHandler(socket, io) { console.log("πŸ“¦ [matchSocket] Problem sent:", problem.id, problem.title); io.to(matchCode).emit("start_game", { - problem + roomCode: matchCode, + problem, + timeLimit: 600 }); } }); // Listen for a submission from the client - socket.on("submit_code", async ({ matchCode, code, languageId }) => { - try { - const problem = matchData[matchCode]?.problem; - if (!problem) throw new Error("Problem not found"); +// Inside your matchSocketHandler(socket, io), replace your existing handler with this: +socket.on("submit_solution", async ({ roomCode: matchCode, code, language }) => { + try { + console.log(`πŸ›°οΈ [Server] submit_solution received in room: ${matchCode}, language: ${language}`); + + // 1. Look up the problem data for this room + const problem = matchData[matchCode]?.problem; + if (!problem) { + socket.emit("submission_result", { passed: false, error: "Problem not found" }); + return; + } + + // 2. Map languages to Judge0 IDs + const languageIds = { + javascript: 63, + python: 71, + java: 62, + cpp: 54 + }; + const languageId = languageIds[language] || 63; + + // ── Branch by language ── + if (language === "javascript") { + // JavaScript: use your bulk‐wrapper + const wrapperType = problem.category === "linked list" + ? "linkedlist" + : problem.category === "binary tree" + ? "binarytree" + : problem.category === "graph" + ? "graph" + : "array"; + + const wrappedCode = buildJsWrappedCode(code, problem.testCases, wrapperType); + const stdin = formatTestCasesAsStdin(problem.testCases, wrapperType); + console.log("=== DEBUG: generated stdin ===\n" + stdin + "\n=== END DEBUG ==="); + + const result = await runJudge0(wrappedCode, languageId, "", stdin); + const rawBase64 = result.stdout || result.stderr || ""; + const decoded = Buffer.from(rawBase64, "base64").toString("utf-8"); + console.log("βœ… Decoded Output:\n", decoded); + + const lines = decoded.split("\n").filter(l => l.trim().length); + const allPassed = lines.every(line => line.startsWith("βœ…")); + + io.to(matchCode).emit("submission_result", { + output: decoded, + allPassed, + winner: allPassed ? socket.data.username : null + }); - const wrappedCode = buildJsWrappedCode(code, problem.testCases); - const stdin = formatTestCasesAsStdin(problem.testCases); + if (allPassed) { + socket.to(matchCode).emit("lock_editor"); + } - const result = await runJudge0(wrappedCode, languageId, "", stdin); - const output = (result.stdout || result.stderr || "").trim(); + } else if (language === "python") { + // 1) Build & run the Python wrapper + const wrappedCode = buildPythonWrapper(code, problem.testCases); + // Only send the actual input lines (ignore expectedOutputs) + const stdin = problem.testCases + .map(tc => tc.input.trim()) + .join('\n\n'); + console.log("=== DEBUG PYTHON stdin ===\n" + stdin + "\n=== END DEBUG ==="); + + const result = await runJudge0(wrappedCode, languageId, "", stdin); + const decoded = Buffer.from(result.stdout || result.stderr || "", "base64") + .toString("utf8") + .trim(); + console.log("βœ… Python decoded:\n", decoded); + + // 2) Break into lines and compare + const lines = decoded.split("\n").filter(l => l.trim().length); + const results = problem.testCases.map((tc, i) => { + let actual; + try { + actual = JSON.parse(lines[i]); + } catch { + actual = lines[i]; + } + // see if any of the allowed expected outputs matches + const passed = tc.expectedOutputs.some(exp => { + try { + return exp === JSON.stringify(actual); + } catch { + return false; + } + }); + return { + idx: i + 1, + passed, + input: tc.input.trim(), + expected: tc.expectedOutputs, + actual + }; + }); - const passedAll = !output.includes("❌"); + const allPassed = results.every(r => r.passed); - if (passedAll) { - io.to(matchCode).emit("match_over", { - winner: socket.data.username, - output - }); - socket.to(matchCode).emit("lock_editor"); + // 3) Emit exactly the same format you have for JS + const formatted = results.map((r, i) => { + if (r.passed) { + return `βœ… Test ${i + 1}: Passed`; } else { - socket.emit("submission_result", { - passed: false, - output - }); + return [ + `❌ Test ${i + 1}: Failed`, + ` Input: ${r.input}`, + ` Expected: ${JSON.stringify(r.expected)}`, + ` Got: ${JSON.stringify(r.actual)}` + ].join("\n"); } - } catch (err) { - socket.emit("submission_result", { - passed: false, - error: err.message - }); + }).join("\n\n"); + + io.to(matchCode).emit("submission_result", { + output: formatted, + allPassed, + winner: allPassed ? socket.data.username : null + }); + + if (allPassed) { + socket.to(matchCode).emit("lock_editor"); } - }); - - // Disconnect cleanup - socket.on("disconnect", () => { - console.log(`User disconnected: ${socket.id}`); - - for (const [code, sockets] of Object.entries(matchRooms)) { - matchRooms[code] = sockets.filter(s => s.id !== socket.id); - if (matchRooms[code].length === 0) { - delete matchRooms[code]; - } +} + else if (language === "java") { + // fallback: run each test case one by one + const results = await runAllTestCases(code, languageId, problem.testCases); + const allPassed = results.every(r => r.passed); + + const formatted = results.map((r, i) => { + if (r.passed) { + return `βœ… Test ${i + 1}: Passed`; + } else { + return [ + `❌ Test ${i + 1}: Failed`, + ` Input: ${r.input}`, + ` Expected: ${JSON.stringify(r.expectedOutput || r.expectedOutputs)}`, + ` Got: ${JSON.stringify(r.actual)}` + ].join("\n"); } + }).join("\n\n"); + + io.to(matchCode).emit("submission_result", { + output: formatted, + allPassed, + winner: allPassed ? socket.data.username : null + }); + + if (allPassed) { + socket.to(matchCode).emit("lock_editor"); + } + } + else { + socket.emit("submission_result", { + passed: false, + error: `Unsupported language: ${language}` + }); + } + } catch (err) { + console.error("❌ submit_solution error:", err); + socket.emit("submission_result", { + passed: false, + error: err.message }); + } +}); + } diff --git a/server/utils/wrappers.js b/server/utils/wrappers.js index ea3b95c..86c0964 100644 --- a/server/utils/wrappers.js +++ b/server/utils/wrappers.js @@ -1,43 +1,85 @@ function _buildArrayWrapper(userCode, testCases) { - const testRunner = ` -function arraysEqual(a, b) { - return Array.isArray(a) && Array.isArray(b) && - a.length === b.length && - a.every((val, index) => val === b[index]); -} + // 1. gets name and number of params + const match = userCode.match(/function\s+([^(]+)\s*\(([^)]*)\)/); + const fnName = match ? match[1] : "solutionFunc"; + const params = match[2] + .split(',') + .map(p => p.trim()) + .filter(p => p); + // 2. decide runner based on params + let testRunner; + if (params.length === 1) { + // just a param + testRunner = ` function runTests() { - const input = require('fs').readFileSync('/dev/stdin', 'utf8'); - const lines = input.trim().split('\\n\\n'); - lines.forEach((block, index) => { - const [numsLine, targetLine, ...expectedLines] = block.trim().split('\\n'); - const nums = JSON.parse(numsLine); - const target = JSON.parse(targetLine); - const expectedList = expectedLines.map(line => JSON.parse(line)); - let result, passed = false; - + const input = require('fs').readFileSync('/dev/stdin','utf8').trim(); + const blocks = input.split('\\n\\n'); + blocks.forEach((block, idx) => { + const [inp, ...expLines] = block.split('\\n'); + let result; try { - result = twoSum(nums, target); - passed = expectedList.some(expected => arraysEqual(result, expected)); + const arg = JSON.parse(inp); + result = ${fnName}(arg); } catch (e) { - console.log(\`❌ Test \${index + 1}: Error - \${e.message}\`); + console.log(\`❌ Test \${idx+1}: Error - \${e.message}\`); + return; + } + const expected = expLines.map(l => JSON.parse(l)); + if (expected.includes(result)) { + console.log(\`βœ… Test \${idx+1}: Passed\`); + } else { + console.log(\`❌ Test \${idx+1}: Failed\\n Input: \${inp}\\n Expected: \${JSON.stringify(expected)}\\n Got: \${JSON.stringify(result)}\`); + } + }); +} +runTests(); +`; + } else { + // two params (ej. twoSum(nums, target)) + testRunner = ` +function arraysEqual(a, b) { + return Array.isArray(a) && a.length === b.length && a.every((v,i) => v === b[i]); +} +function runTests() { + const input = require('fs').readFileSync('/dev/stdin','utf8').trim(); + const blocks = input.split('\\n\\n'); + blocks.forEach((block, idx) => { + const [aLine, bLine, ...expLines] = block.split('\\n'); + const arg1 = JSON.parse(aLine), arg2 = JSON.parse(bLine); + const expected = expLines.map(l => JSON.parse(l)); + let res, passed=false; + try { + res = ${fnName}(arg1, arg2); + passed = expected.some(e => arraysEqual(res, e)); + } catch(e) { + console.log(\`❌ Test \${idx+1}: Error - \${e.message}\`); return; } - if (passed) { - console.log(\`βœ… Test \${index + 1}: Passed\`); + console.log(\`βœ… Test \${idx+1}: Passed\`); } else { - console.log(\`❌ Test \${index + 1}: Failed\\n Input: \${numsLine}, \${targetLine}\\n Expected: \${JSON.stringify(expectedList)}\\n Got: \${JSON.stringify(result)}\`); + console.log(\`❌ Test \${idx+1}: Failed\\n Input: \${aLine}, \${bLine}\\n Expected: \${JSON.stringify(expected)}\\n Got: \${JSON.stringify(res)}\`); } }); } - runTests(); `; + } + + // 3. returns whole code return `${userCode}\n${testRunner}`; } + function _buildLinkedListWrapper(userCode, testCases) { + // 1. Extract the user’s function name dynamically + const match = userCode.match(/function\s+([^(]+)\s*\(/); + const fnName = match ? match[1] : "addTwoNumbers"; + + // 2. Build the test runner that transforms input arrays into linked lists, + // invokes the user’s function, converts the result back to an array, + // and compares it against expected output const testRunner = ` class ListNode { constructor(val = 0, next = null) { @@ -66,46 +108,50 @@ function linkedListToArray(head) { } function arraysEqual(a, b) { - return Array.isArray(a) && Array.isArray(b) && - a.length === b.length && - a.every((val, index) => val === b[index]); + return Array.isArray(a) && a.length === b.length && a.every((v,i) => v === b[i]); } function runTests() { - const input = require('fs').readFileSync('/dev/stdin', 'utf8'); - const blocks = input.trim().split('\\n\\n'); - - blocks.forEach((block, index) => { - const [inputLine, expectedLine] = block.trim().split('\\n'); - const inputArr = JSON.parse(inputLine); - const expected = JSON.parse(expectedLine); - - let resultArr = []; + const input = require('fs').readFileSync('/dev/stdin','utf8').trim(); + const blocks = input.split('\\n\\n'); + blocks.forEach((block, idx) => { + const [l1Line, l2Line, expectedLine] = block.split('\\n'); + let arr1, arr2, expected, resultArr; try { - const l1 = arrayToLinkedList(inputArr[0]); - const l2 = arrayToLinkedList(inputArr[1]); - const result = addTwoNumbers(l1, l2); - resultArr = linkedListToArray(result); + arr1 = JSON.parse(l1Line); + arr2 = JSON.parse(l2Line); + expected = JSON.parse(expectedLine); + const l1 = arrayToLinkedList(arr1); + const l2 = arrayToLinkedList(arr2); + const res = ${fnName}(l1, l2); + resultArr = linkedListToArray(res); } catch (e) { - console.log(\`❌ Test \${index + 1}: Error - \${e.message}\`); + console.log(\`❌ Test \${idx+1}: Error - \${e.message}\`); return; } - - const passed = arraysEqual(resultArr, expected); - if (passed) { - console.log(\`βœ… Test \${index + 1}: Passed\`); + if (arraysEqual(resultArr, expected)) { + console.log(\`βœ… Test \${idx+1}: Passed\`); } else { - console.log(\`❌ Test \${index + 1}: Failed\\n Input: \${JSON.stringify(inputArr)}\\n Expected: \${JSON.stringify(expected)}\\n Got: \${JSON.stringify(resultArr)}\`); + console.log(\`❌ Test \${idx+1}: Failed\\n Input: \${l1Line}, \${l2Line}\\n Expected: \${JSON.stringify(expected)}\\n Got: \${JSON.stringify(resultArr)}\`); } }); } - runTests(); `; + + // 3. Return the combined user code plus test runner return `${userCode}\n${testRunner}`; } function _buildBinaryTreeWrapper(userCode, testCases) { + // 1. Extract the user’s function name dynamically + const match = userCode.match(/function\s+([^(]+)\s*\(/); + const fnName = match ? match[1] : "inorderTraversal"; + + // 2. Build the test runner that: + // a) Parses the input array into a binary tree + // b) Calls the user’s function on the tree + // c) Compares the returned array against expected results const testRunner = ` class TreeNode { constructor(val, left = null, right = null) { @@ -137,84 +183,80 @@ function arrayToBinaryTree(arr) { } function arraysEqual(a, b) { - return JSON.stringify(a) === JSON.stringify(b); + return Array.isArray(a) && a.length === b.length && a.every((v,i) => v === b[i]); } function runTests() { - const input = require('fs').readFileSync('/dev/stdin', 'utf8'); - const blocks = input.trim().split('\\n\\n'); - - blocks.forEach((block, index) => { - const [inputLine, ...expectedLines] = block.trim().split('\\n'); - const inputArr = JSON.parse(inputLine); - const expectedList = expectedLines - .map(line => { - try { - return JSON.parse(line); - } catch { - return undefined; - } - }) - .filter(e => e !== undefined); - - let resultArr = []; + const input = require('fs').readFileSync('/dev/stdin','utf8').trim(); + const blocks = input.split('\\n\\n'); + blocks.forEach((block, idx) => { + const lines = block.split('\\n'); + let treeArr, expectedList, resultArr; try { - const root = arrayToBinaryTree(inputArr); - const result = inorderTraversal(root); - resultArr = Array.isArray(result) ? result : []; + treeArr = JSON.parse(lines[0]); + expectedList = lines.slice(1).map(l => JSON.parse(l)); + const root = arrayToBinaryTree(treeArr); + const res = ${fnName}(root); + resultArr = Array.isArray(res) ? res : []; } catch (e) { - console.log(\`❌ Test \${index + 1}: Error - \${e.message}\`); + console.log(\`❌ Test \${idx+1}: Error - \${e.message}\`); return; } - - const passed = expectedList.some(expected => JSON.stringify(resultArr) === JSON.stringify(expected)); - if (passed) { - console.log(\`βœ… Test \${index + 1}: Passed\`); + if (expectedList.some(exp => arraysEqual(resultArr, exp))) { + console.log(\`βœ… Test \${idx+1}: Passed\`); } else { - console.log(\`❌ Test \${index + 1}: Failed\\n Input: \${inputLine}\\n Expected: \${JSON.stringify(expectedList)}\\n Got: \${JSON.stringify(resultArr)}\`); + console.log(\`❌ Test \${idx+1}: Failed\\n Input: \${JSON.stringify(treeArr)}\\n Expected: \${JSON.stringify(expectedList)}\\n Got: \${JSON.stringify(resultArr)}\`); } }); } - runTests(); `; + + // 3. Return user code plus test runner return `${userCode}\n${testRunner}`; } + function _buildGraphWrapper(userCode, testCases) { + // 1. Dynamically extract the user’s function name + const match = userCode.match(/function\s+([^(]+)\s*\(/); + const fnName = match ? match[1] : "countComponents"; + + // 2. Build a test runner that: + // a) Reads n and edges from stdin + // b) Calls the user’s function with those arguments + // c) Compares the returned number against each expected output const testRunner = ` function runTests() { - const input = require('fs').readFileSync('/dev/stdin', 'utf8'); - const blocks = input.trim().split('\\n\\n'); - - blocks.forEach((block, index) => { - const [nLine, edgesLine, ...expectedLines] = block.trim().split('\\n'); - const n = JSON.parse(nLine); - const edges = JSON.parse(edgesLine); - const expectedList = expectedLines.map(line => JSON.parse(line)); - - let result; + const input = require('fs').readFileSync('/dev/stdin','utf8').trim(); + const blocks = input.split('\\n\\n'); + blocks.forEach((block, idx) => { + const [nLine, edgesLine, ...expLines] = block.split('\\n'); + let n, edges, expectedList, result; try { - result = countComponents(n, edges); // expects user-defined function + n = JSON.parse(nLine); + edges = JSON.parse(edgesLine); + expectedList = expLines.map(l => JSON.parse(l)); + result = ${fnName}(n, edges); } catch (e) { - console.log(\`❌ Test \${index + 1}: Error - \${e.message}\`); + console.log(\`❌ Test \${idx+1}: Error - \${e.message}\`); return; } - - const passed = expectedList.some(expected => result === expected); - if (passed) { - console.log(\`βœ… Test \${index + 1}: Passed\`); + if (expectedList.includes(result)) { + console.log(\`βœ… Test \${idx+1}: Passed\`); } else { - console.log(\`❌ Test \${index + 1}: Failed\\n Input: \${nLine}, \${edgesLine}\\n Expected: \${JSON.stringify(expectedList)}\\n Got: \${result}\`); + console.log(\`❌ Test \${idx+1}: Failed\\n Input: n=\${n}, edges=\${JSON.stringify(edges)}\\n Expected: \${JSON.stringify(expectedList)}\\n Got: \${result}\`); } }); } - runTests(); `; + + // 3. Return the combined user code plus the test runner return `${userCode}\n${testRunner}`; } + function buildJsWrappedCode(userCode, testCases, type = "array") { switch (type.toLowerCase()) { case "linkedlist": @@ -230,16 +272,62 @@ function buildJsWrappedCode(userCode, testCases, type = "array") { } function formatTestCasesAsStdin(testCases, type = "array") { - return testCases.map(({ input, expectedOutputs }) => { - const [part1, part2] = input.trim().split('\n'); - const inputLine = type === "linkedlist" ? `[${part1},${part2}]` : `${part1}\n${part2}`; - const expectedLines = expectedOutputs.map(out => `${out}`).join('\n'); - return `${inputLine}\n${expectedLines}`; - }).join('\n\n'); + return testCases + .map(({ input, expectedOutputs }) => { + const inputLines = input.trim().split('\n'); + const expectedLines = expectedOutputs || []; + return [...inputLines, ...expectedLines].join('\n'); + }) + .join('\n\n'); +} + + +function buildPythonWrapper(userCode /* string */, testCases /* array */) { + const match = userCode.match(/def\s+([^(]+)\s*\(/); + const fnName = match ? match[1] : "solution"; + + return ` +${userCode} + +import sys, json + +def run_tests(): + data = sys.stdin.read().strip() + if not data: + return + blocks = data.split("\\n\\n") + for idx, block in enumerate(blocks, start=1): + lines = block.split("\\n") + try: + args = [ json.loads(l) for l in lines if l.strip() ] + res = ${fnName}(*args) + output = json.dumps(res) + + expected = [ json.loads(l) for l in lines[len(args):] ] + passed = output in [ json.dumps(e) for e in expected ] + + if passed: + print(f"βœ… Test {idx}: Passed") + else: + print(f"❌ Test {idx}: Failed") + print(f" Input: {' | '.join(lines[:len(args)])}") + print(f" Expected: {expected}") + print(f" Got: {output}") + except Exception as e: + print(f"❌ Test {idx}: Error") + print(f" Input: {' | '.join(lines[:len(args)])}") + print(f" Error: {e}") + +if __name__ == "__main__": + run_tests() +`; } + + module.exports = { buildJsWrappedCode, - formatTestCasesAsStdin + formatTestCasesAsStdin, + buildPythonWrapper }; From a78846bd045bb7d058942a1c4a83b1e37263cb5c Mon Sep 17 00:00:00 2001 From: pavelnavarro Date: Tue, 29 Jul 2025 16:32:19 -0600 Subject: [PATCH 09/12] fixed python wrapper --- package-lock.json | 13 ++- server/data/problems.json | 211 +--------------------------------- server/sockets/matchSocket.js | 4 +- server/utils/wrappers.js | 24 ++-- 4 files changed, 26 insertions(+), 226 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6df2116..e44b55e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5729,9 +5729,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "funding": [ { "type": "opencollective", @@ -7156,9 +7156,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.191", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz", - "integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==", + "version": "1.5.192", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", + "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==", "license": "ISC" }, "node_modules/emittery": { @@ -18609,6 +18609,7 @@ "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", "license": "BSD-3-Clause", "dependencies": { "whatwg-url": "^7.0.0" diff --git a/server/data/problems.json b/server/data/problems.json index cf559d9..2b5aa60 100644 --- a/server/data/problems.json +++ b/server/data/problems.json @@ -167,47 +167,6 @@ { "input": "4\n[[0,1]]", "expectedOutputs": ["3"] } ] }, - { - "id": 6, - "title": "Best Time to Buy and Sell Stock", - "category": "arrays", - "difficulty": "easy", - "description": "You are given an array prices where prices[i] is the price of a given stock on the ith day. You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock. Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.", - "functionSignature": { - "javascript": "function maxProfit(prices) {\n // your code here\n}", - "python": "def max_profit(prices):\n # your code here\n pass", - "java": "public int maxProfit(int[] prices) {\n // your code here\n return 0;\n}", - "cpp": "int maxProfit(vector& prices) {\n // your code here\n return 0;\n}" - }, - "examples": [ - { - "input": "[7,1,5,3,6,4]", - "output": "5", - "explanation": "Buy at 1, sell at 6 for max profit 5." - }, - { - "input": "[7,6,4,3,1]", - "output": "0", - "explanation": "No profit possible." - } - ], - "constraints": [ - "1 <= prices.length <= 10^5", - "0 <= prices[i] <= 10^4" - ], - "testCases": [ - { "input": "[7,1,5,3,6,4]", "expectedOutputs": ["5"] }, - { "input": "[7,6,4,3,1]", "expectedOutputs": ["0"] }, - { "input": "[1,2]", "expectedOutputs": ["1"] }, - { "input": "[2,4,1]", "expectedOutputs": ["2"] }, - { "input": "[3,2,6,5,0,3]", "expectedOutputs": ["4"] }, - { "input": "[1]", "expectedOutputs": ["0"] }, - { "input": "[]", "expectedOutputs": ["0"] }, - { "input": "[2,1,2,1,0,1,2]", "expectedOutputs": ["2"] }, - { "input": "[1,4,2,11]", "expectedOutputs": ["10"] }, - { "input": "[5,4,3,2,1]", "expectedOutputs": ["0"] } - ] - }, { "id": 7, "title": "Reverse Linked List", @@ -290,47 +249,7 @@ { "input": "[1,2,2,null,3,3,null]", "expectedOutputs": ["true"] } ] }, -{ - "id": 9, - "title": "Contains Duplicate", - "category": "arrays", - "difficulty": "easy", - "description": "Given an integer array nums, return true if any value appears at least twice in the array, and return false if every element is distinct.", - "functionSignature": { - "javascript": "function containsDuplicate(nums) {\n // your code here\n}", - "python": "def contains_duplicate(nums):\n # your code here\n pass", - "java": "public boolean containsDuplicate(int[] nums) {\n // your code here\n return false;\n}", - "cpp": "bool containsDuplicate(vector& nums) {\n // your code here\n return false;\n}" - }, - "examples": [ - { - "input": "[1,2,3,1]", - "output": "true", - "explanation": "1 is repeated." - }, - { - "input": "[1,2,3,4]", - "output": "false", - "explanation": "All elements are unique." - } - ], - "constraints": [ - "1 <= nums.length <= 10^5", - "-10^9 <= nums[i] <= 10^9" - ], - "testCases": [ - { "input": "[1,2,3,1]", "expectedOutputs": ["true"] }, - { "input": "[1,2,3,4]", "expectedOutputs": ["false"] }, - { "input": "[1,1,1,3,3,4,3,2,4,2]", "expectedOutputs": ["true"] }, - { "input": "[]", "expectedOutputs": ["false"] }, - { "input": "[0]", "expectedOutputs": ["false"] }, - { "input": "[1,2,3,4,5,6,7,8,9,1]", "expectedOutputs": ["true"] }, - { "input": "[1,2,3,4,5]", "expectedOutputs": ["false"] }, - { "input": "[2,14,18,22,22]", "expectedOutputs": ["true"] }, - { "input": "[99]", "expectedOutputs": ["false"] }, - { "input": "[1,2,3,4,4]", "expectedOutputs": ["true"] } - ] -}, + { "id": 10, "title": "Merge Two Sorted Lists", @@ -831,48 +750,6 @@ { "input": "[5,6]\n[6,5]", "expectedOutputs": ["false"] } ] }, -{ - "id": 22, - "title": "Rotate Array", - "category": "arrays", - "difficulty": "easy", - "description": "Given an integer array nums, rotate the array to the right by k steps, where k is non-negative.", - "functionSignature": { - "javascript": "function rotate(nums, k) {\n // your code here\n}", - "python": "def rotate(nums, k):\n # your code here\n pass", - "java": "public void rotate(int[] nums, int k) {\n // your code here\n}", - "cpp": "void rotate(vector& nums, int k) {\n // your code here\n}" - }, - "examples": [ - { - "input": "[1,2,3,4,5,6,7], 3", - "output": "[5,6,7,1,2,3,4]", - "explanation": "Rotate right 3 times." - }, - { - "input": "[-1,-100,3,99], 2", - "output": "[3,99,-1,-100]", - "explanation": "Elements are shifted right." - } - ], - "constraints": [ - "1 <= nums.length <= 10^5", - "-2^31 <= nums[i] <= 2^31 - 1", - "0 <= k <= 10^5" - ], - "testCases": [ - { "input": "[1,2,3,4,5,6,7]\n3", "expectedOutputs": ["[5,6,7,1,2,3,4]"] }, - { "input": "[-1,-100,3,99]\n2", "expectedOutputs": ["[3,99,-1,-100]"] }, - { "input": "[1,2]\n3", "expectedOutputs": ["[2,1]"] }, - { "input": "[1]\n0", "expectedOutputs": ["[1]"] }, - { "input": "[1,2,3]\n0", "expectedOutputs": ["[1,2,3]"] }, - { "input": "[1,2,3,4]\n4", "expectedOutputs": ["[1,2,3,4]"] }, - { "input": "[1,2,3,4,5]\n5", "expectedOutputs": ["[1,2,3,4,5]"] }, - { "input": "[1,2,3,4,5,6]\n2", "expectedOutputs": ["[5,6,1,2,3,4]"] }, - { "input": "[1,2,3,4,5]\n3", "expectedOutputs": ["[3,4,5,1,2]"] }, - { "input": "[1,2,3,4,5,6,7,8]\n4", "expectedOutputs": ["[5,6,7,8,1,2,3,4]"] } - ] -}, { "id": 23, "title": "Merge k Sorted Lists", @@ -1466,49 +1343,6 @@ { "input": "[[2,3],[1,3],[1,2]]", "expectedOutputs": ["[[2,3],[1,3],[1,2]]"] } ] }, -{ - "id": 37, - "title": "Missing Number", - "category": "arrays", - "difficulty": "easy", - "description": "Given an array nums containing n distinct numbers in the range [0, n], return the only number in the range that is missing from the array.", - "functionSignature": { - "javascript": "function missingNumber(nums) {\n // your code here\n}", - "python": "def missing_number(nums):\n # your code here\n pass", - "java": "public int missingNumber(int[] nums) {\n // your code here\n return -1;\n}", - "cpp": "int missingNumber(vector& nums) {\n // your code here\n return -1;\n}" - }, - "examples": [ - { - "input": "[3,0,1]", - "output": "2", - "explanation": "All numbers from 0 to 3 except 2." - }, - { - "input": "[0,1]", - "output": "2", - "explanation": "Missing the last number in range." - } - ], - "constraints": [ - "n == nums.length", - "1 <= n <= 10^4", - "0 <= nums[i] <= n", - "All numbers are unique" - ], - "testCases": [ - { "input": "[3,0,1]", "expectedOutputs": ["2"] }, - { "input": "[0,1]", "expectedOutputs": ["2"] }, - { "input": "[9,6,4,2,3,5,7,0,1]", "expectedOutputs": ["8"] }, - { "input": "[0]", "expectedOutputs": ["1"] }, - { "input": "[1]", "expectedOutputs": ["0"] }, - { "input": "[4,2,1,0]", "expectedOutputs": ["3"] }, - { "input": "[2,0]", "expectedOutputs": ["1"] }, - { "input": "[1,2]", "expectedOutputs": ["0"] }, - { "input": "[0,2]", "expectedOutputs": ["1"] }, - { "input": "[0,1,3,4]", "expectedOutputs": ["2"] } - ] -}, { "id": 38, "title": "Intersection of Two Linked Lists", @@ -1638,49 +1472,6 @@ { "input": "3\n[[0,2],[1,2]]", "expectedOutputs": ["true"] } ] }, -{ - "id": 41, - "title": "Majority Element", - "category": "arrays", - "difficulty": "easy", - "description": "Given an array nums of size n, return the majority element. The majority element is the element that appears more than ⌊n / 2βŒ‹ times. You may assume that the majority element always exists in the array.", - "functionSignature": { - "javascript": "function majorityElement(nums) {\n // your code here\n}", - "python": "def majority_element(nums):\n # your code here\n pass", - "java": "public int majorityElement(int[] nums) {\n // your code here\n return -1;\n}", - "cpp": "int majorityElement(vector& nums) {\n // your code here\n return -1;\n}" - }, - "examples": [ - { - "input": "[3,2,3]", - "output": "3", - "explanation": "3 appears more than n/2 times." - }, - { - "input": "[2,2,1,1,1,2,2]", - "output": "2", - "explanation": "2 is the majority element." - } - ], - "constraints": [ - "n == nums.length", - "1 <= n <= 5 * 10^4", - "-10^9 <= nums[i] <= 10^9", - "The majority element always exists" - ], - "testCases": [ - { "input": "[3,2,3]", "expectedOutputs": ["3"] }, - { "input": "[2,2,1,1,1,2,2]", "expectedOutputs": ["2"] }, - { "input": "[1]", "expectedOutputs": ["1"] }, - { "input": "[1,1,1,2,3]", "expectedOutputs": ["1"] }, - { "input": "[4,4,5,4,6,4,4]", "expectedOutputs": ["4"] }, - { "input": "[9,9,9,1,2,3,9]", "expectedOutputs": ["9"] }, - { "input": "[2,2,2,2,2,2,2]", "expectedOutputs": ["2"] }, - { "input": "[7,7,5,7,5,5,7]", "expectedOutputs": ["7"] }, - { "input": "[0,0,0,1,1]", "expectedOutputs": ["0"] }, - { "input": "[10,10,10,20,30]", "expectedOutputs": ["10"] } - ] -}, { "id": 42, "title": "Linked List Cycle", diff --git a/server/sockets/matchSocket.js b/server/sockets/matchSocket.js index ca0f27a..8bc3992 100644 --- a/server/sockets/matchSocket.js +++ b/server/sockets/matchSocket.js @@ -224,9 +224,7 @@ socket.on("submit_solution", async ({ roomCode: matchCode, code, language }) => // 1) Build & run the Python wrapper const wrappedCode = buildPythonWrapper(code, problem.testCases); // Only send the actual input lines (ignore expectedOutputs) - const stdin = problem.testCases - .map(tc => tc.input.trim()) - .join('\n\n'); + const stdin = formatTestCasesAsStdin(problem.testCases, "array");; console.log("=== DEBUG PYTHON stdin ===\n" + stdin + "\n=== END DEBUG ==="); const result = await runJudge0(wrappedCode, languageId, "", stdin); diff --git a/server/utils/wrappers.js b/server/utils/wrappers.js index 86c0964..1429646 100644 --- a/server/utils/wrappers.js +++ b/server/utils/wrappers.js @@ -289,33 +289,43 @@ function buildPythonWrapper(userCode /* string */, testCases /* array */) { return ` ${userCode} -import sys, json +import sys, json, inspect def run_tests(): data = sys.stdin.read().strip() if not data: return blocks = data.split("\\n\\n") + for idx, block in enumerate(blocks, start=1): - lines = block.split("\\n") + lines = block.strip().split("\\n") + try: - args = [ json.loads(l) for l in lines if l.strip() ] + # Dynamically determine number of parameters + sig = inspect.signature(${fnName}) + num_args = len(sig.parameters) + + # First num_args lines are inputs + args = [json.loads(l) for l in lines[:num_args]] + expected_lines = lines[num_args:] + res = ${fnName}(*args) output = json.dumps(res) - expected = [ json.loads(l) for l in lines[len(args):] ] - passed = output in [ json.dumps(e) for e in expected ] + expected = [json.dumps(json.loads(l)) for l in expected_lines] + passed = output in expected if passed: print(f"βœ… Test {idx}: Passed") else: print(f"❌ Test {idx}: Failed") - print(f" Input: {' | '.join(lines[:len(args)])}") + print(f" Input: {' | '.join(lines[:num_args])}") print(f" Expected: {expected}") print(f" Got: {output}") + except Exception as e: print(f"❌ Test {idx}: Error") - print(f" Input: {' | '.join(lines[:len(args)])}") + print(f" Input: {' | '.join(lines[:num_args])}") print(f" Error: {e}") if __name__ == "__main__": From c9fab0ee772cb3679670f18e971b5543c0ca02c1 Mon Sep 17 00:00:00 2001 From: wiidu Date: Tue, 29 Jul 2025 18:55:58 -0500 Subject: [PATCH 10/12] Updated CORS headers --- server/app.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/server/app.js b/server/app.js index 585fcec..1fa5b6a 100644 --- a/server/app.js +++ b/server/app.js @@ -15,11 +15,15 @@ const gameRoutes = require("./routes/gameRoutes"); const app = express(); const server = http.createServer(app); +const allowedOrigins = [ + 'https://code-clash-tan.vercel.app/', + 'https://code-clash-89bb.onrender.com', + 'http://localhost:3000' +]; + // CORS app.use(cors({ - origin: process.env.NODE_ENV === 'production' ? - "https://code-clash-tan.vercel.app/" : - "http://localhost:3000", + origin: allowedOrigins, credentials: true })); From d78e408111cef0d6eef14a0791fab005b5e5024c Mon Sep 17 00:00:00 2001 From: wiidu Date: Tue, 29 Jul 2025 19:10:23 -0500 Subject: [PATCH 11/12] Added a slash oops --- server/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app.js b/server/app.js index 1fa5b6a..c26add4 100644 --- a/server/app.js +++ b/server/app.js @@ -16,7 +16,7 @@ const app = express(); const server = http.createServer(app); const allowedOrigins = [ - 'https://code-clash-tan.vercel.app/', + 'https://code-clash-tan.vercel.app', 'https://code-clash-89bb.onrender.com', 'http://localhost:3000' ]; From c9a265dcc466e615f5569511afa22ba5366f7430 Mon Sep 17 00:00:00 2001 From: Darlene Santes Date: Tue, 29 Jul 2025 20:40:59 -0500 Subject: [PATCH 12/12] Enhance GameRoom functionality to update user stats on game end --- client/src/app.jsx | 1 + client/src/pages/GameRoom.jsx | 27 ++++++++++- server/routes/userRoutes.js | 84 +++++++++++++++-------------------- 3 files changed, 63 insertions(+), 49 deletions(-) diff --git a/client/src/app.jsx b/client/src/app.jsx index 54e1045..d835ff5 100644 --- a/client/src/app.jsx +++ b/client/src/app.jsx @@ -186,6 +186,7 @@ const App = () => { ); diff --git a/client/src/pages/GameRoom.jsx b/client/src/pages/GameRoom.jsx index 1e00dac..bc281ec 100644 --- a/client/src/pages/GameRoom.jsx +++ b/client/src/pages/GameRoom.jsx @@ -183,10 +183,35 @@ const GameRoom = ({ navigate, user, roomData }) => { }; // Game end events - const handleGameEnded = (data) => { + const handleGameEnded = async (data) => { console.log('Game ended:', data); setGameState('completed'); setGameResult(data); + const isWin = data.winner === user.id; + // update user stats (wins/winstreak) + try { + const response = await fetch('/api/user/update-stats', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + googleId: user.googleId, + isWin + }) + }); + + const result = await response.json(); + if (!response.ok) { + console.error('Failed to update user stats:', result.error); + return; + } + console.log('User stats updated successfully:', result); + // Update local user state + setUser(result.user); + + } catch (error) { + console.error('Error updating user stats:', error); + } + }; // Chat events diff --git a/server/routes/userRoutes.js b/server/routes/userRoutes.js index 2771631..cb8de66 100644 --- a/server/routes/userRoutes.js +++ b/server/routes/userRoutes.js @@ -94,73 +94,61 @@ router.get('/stats/:id', async (req, res) => { // Route to update match stats (wins, matches played) router.put('/update-stats', async (req, res) => { - const {userId, isWin} = req.body; + const {googleId, isWin} = req.body; - // Update match stats based on the outcome of a match - try { - const updatedUser = await updateMatchStats(userId, isWin); - res.status(200).json(updatedUser); - } catch (error) { - console.error('Error updating match stats:', error); - res.status(500).json({ error: 'Internal server error' }); - } -}); - -// route to update user coins and rank after a game ends -/** - * This route updates the user's coins and rank based on the game result. - * input: { googleId: string, isWin: boolean } - * isWin is true if the user won the game, false if they lost. - * googleId is the user's unique identifier - * The rank is determined based on the user's coins - * result is json containing the user google ID, coins, and rank - */ -router.put('/update-coins-rank', async (req, res) => { - const { googleId, isWin } = req.body; // result can be 'true', 'false', draws can be treated as a loss - + // basic validation if (!googleId || typeof isWin !== 'boolean') { return res.status(400).json({ error: 'Invalid request data' }); } try { + //find user by googleId const user = await User.findOne({ where: { googleId } }); if (!user) { return res.status(404).json({ error: 'User not found' }); } - // update coins: - const coinsChange = isWin ? 100 : -50; // win gives 100 coins, loss deducts 50 coins - const newCoins = Math.max(0, user.coins + coinsChange); // ensure coins don't go below 0 - user.coins = newCoins; + // update match stats + if (isWin) { + user.wins += 1; + user.winStreak += 1; + } else { + user.winStreak = 0; + } + user.totalMatchesPlayed += 1; + + + // update rank + const coinChange = isWin ? 100 : -50; // 100 coins for win, -50 for loss + const newCoins = user.coins + coinChange; - // update rank based on coins - let rank; + user.coins = Math.max(newCoins, 0); // Ensure coins don't go negative const c = newCoins; - if (c >= 1350) rank = 'Legend'; - else if (c >= 1200) rank = 'Gold 3'; - else if (c >= 1050) rank = 'Gold 2'; - else if (c >= 900) rank = 'Gold 1'; - else if (c >= 750) rank = 'Silver 3'; - else if (c >= 600) rank = 'Silver 2'; - else if (c >= 450) rank = 'Silver 1'; - else if (c >= 300) rank = 'Bronze 3'; - else if (c >= 150) rank = 'Bronze 2'; - else rank = 'Bronze 1'; - - user.rank = rank; - - // save and respond + if (c >= 1350) user.rank = 'Legend'; + else if (c >= 1200) user.rank = 'Gold 3'; + else if (c >= 1050) user.rank = 'Gold 2'; + else if (c >= 900) user.rank = 'Gold 1'; + else if (c >= 750) user.rank = 'Silver 3'; + else if (c >= 600) user.rank = 'Silver 2'; + else if (c >= 450) user.rank = 'Silver 1'; + else if (c >= 300) user.rank = 'Bronze 3'; + else if (c >= 150) user.rank = 'Bronze 2'; + else user.rank = 'Bronze 1'; + await user.save(); - res.json({ - message: 'User coins and rank updated successfully', + res.status(200).json({ + message: 'User stats updated successfully', user: { googleId: user.googleId, - coins: user.coins, - rank: user.rank + wins: user.wins, + totalMatchesPlayed: user.totalMatchesPlayed, + winStreak: user.winStreak, + rank: user.rank, + coins: user.coins } }); } catch (error) { - console.error('Error updating user coins and rank:', error); + console.error('Error updating user stats:', error); res.status(500).json({ error: 'Internal server error' }); } });