## game.html ```html= <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Tic Tac Toe Game</title> <link rel="stylesheet" href="game.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js" integrity="sha512-zoJXRvW2gC8Z0Xo3lBbao5+AS3g6YWr5ztKqaicua11xHo+AvE1b0lT9ODgrHTmNUxeCw0Ry4BGRYZfXu70weg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js"></script> </head> <body> <div class="game-container"> <div class="players-info"> <span id="player1-info"></span> vs. <span id="player2-info"></span> </div> <table id="game-board"> <tr> <td data-row="0" data-col="0"></td> <td data-row="0" data-col="1"></td> <td data-row="0" data-col="2"></td> </tr> <tr> <td data-row="1" data-col="0"></td> <td data-row="1" data-col="1"></td> <td data-row="1" data-col="2"></td> </tr> <tr> <td data-row="2" data-col="0"></td> <td data-row="2" data-col="1"></td> <td data-row="2" data-col="2"></td> </tr> </table> <div id="game-info"></div> </div> <script src="game.js"></script> </body> </html> ``` ## game.css ```css= @import url('https://fonts.googleapis.com/css2?family=Rubik+Iso&family=Sedgwick+Ave+Display&display=swap'); body, html { margin: 0; padding: 0; border: none; outline: none; box-sizing: border-box; font-family: Arial, sans-serif; } .game-container { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; background-color: #36393f; color: #ffffff; } .players-info { margin-bottom: 20px; font-family: 'Sedgwick Ave Display', cursive; font-size: 28px; background-color: #2f3136; padding: 10px 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); color: #dcddde; } #player1-info, #player2-info { font-weight: normal; padding: 0 10px; } #game-info { font-family: 'Sedgwick Ave Display', cursive; font-size: 18px; margin-top: 16px; background-color: #2F313B; padding: 15px 25px; border-radius: 6px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); color: #dcddde; } #game-board td { width: 115px; height: 115px; border: 5px solid #40444b; text-align: center; vertical-align: middle; cursor: pointer; background-color: #1f2025; } #game-board td:hover { background-color: #2d2e35; transition: all 0.4s; } #game-board td img { width: 100%; height: 100%; object-fit: contain; display: block; /* Remove extra space */ cursor: not-allowed; } #game-board tr:first-child td { border-top: none; } #game-board tr:last-child td { border-bottom: none; } #game-board td:first-child { border-left: none; } #game-board td:last-child { border-right: none; } #game-controls { margin-top: 20px; } @keyframes epicAnimation { 0% { transform: scale(1); opacity: 0.5; } 50% { transform: scale(1.5); opacity: 1; } 100% { transform: scale(1); opacity: 0.5; } } #game-info.epic { animation: epicAnimation; animation-delay: 0.7s; animation-duration: 6s; color: gold; font-size: 24px; font-weight: bold; text-shadow: 2px 2px 4px rgba(0,0,0,0.5); } ``` ## game.js ```js= const socket = io.connect('http://localhost:3000'); const gameBoard = document.getElementById('game-board'); const player1Info = document.getElementById('player1-info'); const player2Info = document.getElementById('player2-info'); const gameInfo = document.getElementById('game-info'); let currentPlayer = 'X'; let gameStarted = false; let playerSymbol; let gameOver = false; // Extract roomId from the URL const urlParams = new URLSearchParams(window.location.search); const roomId = urlParams.get('roomId'); socket.emit('check-room-existence', { roomName: roomId }); let username = localStorage.getItem('username'); let clientId = localStorage.getItem('clientId'); if (username && clientId) { socket.emit('reconnect', { username: username, clientId: clientId, roomName: roomId }); } if (username) { console.log(`Emitting join-room with username: ${username} and room: ${roomId}`); socket.emit('join-room', { roomName: roomId, username: username }); } gameBoard.addEventListener('click', (e) => { if (gameOver) { alert("The game is over!"); return; } if (!gameStarted) { alert("The game hasn't started yet!"); return; } if (currentPlayer !== playerSymbol) { alert("It's not your turn!"); return; } const cell = e.target; if (cell.tagName === 'TD' && !cell.firstChild) { // Don't create and append the img here. Wait for the server's confirmation. socket.emit('make-move', { row: parseInt(cell.dataset.row), col: parseInt(cell.dataset.col), symbol: currentPlayer, roomName: roomId }); } else { alert("X or O already in this cell...") } }); // LISTENERS socket.on('game-started', () => { gameStarted = true; }); socket.on('player-symbol', (data) => { playerSymbol = data.symbol; }); socket.on('room-existence-response', (data) => { if (data.exists) { player1Info.textContent = data.player1; } else { alert("The room you're trying to join doesn't exist!"); window.location.href = 'index.html'; } }); socket.on('receive-move', (data) => { const cell = gameBoard.querySelector(`[data-row="${data.row}"][data-col="${data.col}"]`); const img = document.createElement('img'); img.src = `images/${data.symbol.toLowerCase()}.png`; cell.appendChild(img); currentPlayer = data.nextPlayer; updateTurnInfo(); }); socket.on('players-ready', (data) => { console.log('Received players-ready event:', data); player1Info.textContent = data.player1 || ''; player2Info.textContent = data.player2 || ''; updateTurnInfo(); }); socket.on('disconnect', (reason) => { console.log('Socket disconnected due to:', reason); }); socket.on('connect_error', (error) => { console.log('Socket connection error:', error); }); socket.on('room-error', (msg) => { alert("Room error: " + msg); }); socket.on('game-over', (result) => { gameOver = true; if (result === 'DRAW') { gameInfo.textContent = "DRAW!"; } else { gameInfo.textContent = `${result === 'X' ? player1Info.textContent.trim() : player2Info.textContent.trim()} WINS!!!`.toUpperCase(); } partyTime() gameInfo.classList.add('epic'); }); // functions function updateTurnInfo() { if (!player2Info.textContent.trim()) { // Check if player2's name is not set gameInfo.textContent = "Waiting for second player"; } else if (currentPlayer === 'X') { gameInfo.textContent = `${player1Info.textContent.trim()}'s turn (X)`; } else { gameInfo.textContent = `${player2Info.textContent.trim()}'s turn (O)`; } } function partyTime() { confetti({ spread: 180 }); // do this for 5 seconds var duration = 5 * 1000; var end = Date.now() + duration; (function frame() { // launch a few confetti from the left edge confetti({ particleCount: 7, angle: 60, spread: 55, origin: { x: 0 } }); // and launch a few from the right edge confetti({ particleCount: 7, angle: 120, spread: 55, origin: { x: 1 } }); // keep going until we are out of time if (Date.now() < end) { requestAnimationFrame(frame); } }()); } ``` ## Celi script.js (za index.html) ```js= // document.addEventListener('DOMContentLoaded', function() { // const lobbiesList = document.getElementById('lobbies-list'); // const sampleRooms = [ // { id: 'room-12345', name: "Alice's Room", status: 'IN_PROGRESS' }, // { id: 'room-67890', name: "Bob's Room", status: 'LOOKING_FOR_PLAYER' }, // { id: 'room-11223', name: "Charlie's Room", status: 'IN_PROGRESS' }, // ]; // sampleRooms.forEach(room => { // const a = document.createElement('a'); // a.href = `game.html?roomId=${room.id}`; // a.textContent = room.name + (room.status === 'IN_PROGRESS' ? ' (IN PROGRESS)' : ' (LOOKING FOR PLAYER)'); // lobbiesList.appendChild(a); // }); // }); const socket = io.connect('http://localhost:3000'); const usernameInput = document.getElementById('username-input'); const setUsernameBtn = document.getElementById('set-username-btn'); const createLobbyBtn = document.getElementById('create-lobby-btn'); const lobbiesList = document.getElementById('lobbies-list'); const usernameStatus = document.getElementById('username-status'); let username = localStorage.getItem('username'); let clientId = localStorage.getItem('clientId'); document.addEventListener('DOMContentLoaded', () => { if (username && clientId) { setUsernameStatus(true, username); usernameInput.value = username; socket.emit('reclaim-username', { username, clientId }); } else { setUsernameStatus(false, null); } socket.emit('get-rooms'); }); // Button click events setUsernameBtn.addEventListener('click', () => { const newUsername = usernameInput.value.trim(); let canChange = true; if (username) { canChange = confirm(`Are you sure you want to change your existing username "${username}"?`); } if (!canChange) { return; } if (newUsername == username) { alert("You are trying to change your username to the same name... can't do that"); return; } if (newUsername.length < 3) { alert("Username must be at least 3 characters long"); return; } if (newUsername.length > 7) { alert("Username must be max 7 characters long"); return; } const existingClientId = localStorage.getItem('clientId') || null; socket.emit('set-username', { username: newUsername, clientId: existingClientId }); }); createLobbyBtn.addEventListener('click', () => { checkIfSocketConnected(socket); if (username) { // Prompt the user to select an image const imageInput = document.createElement('input'); imageInput.type = 'file'; imageInput.accept = 'image/*'; imageInput.onchange = function() { // Upload the image const formData = new FormData(); formData.append('lobbyImage', imageInput.files[0]); fetch('http://localhost:3000/upload', { method: 'POST', body: formData }) .then(response => { if (!response.ok) { return response.json().then(data => { throw new Error(data.error); }); } return response.json(); }) .then(data => { socket.emit('create-room', { username: username, imageUrl: data.imageUrl }); }) .catch(error => { console.error('Error uploading image:', error); alert(error.message); // Display the error message to the user }); }; imageInput.click(); // This will open the file dialog } else { alert("Please set a username first."); } }); // Extra... toggleSpinner(false); // na zacetku izklopljen // pocakamo 0.5 sekund... let canShowSpinner = false; setTimeout(() => { canShowSpinner = true; }, 500); //// SOCKET LISTENERS socket.on('connect', () => { console.log('Socket connected!'); toggleSpinner(false); }); socket.on('disconnect', (reason) => { console.log('Socket disconnected due to:', reason); toggleSpinner(true); }); socket.on('reconnect', (attemptNumber) => { console.log('Socket reconnected on attempt:', attemptNumber); toggleSpinner(false); }); socket.on('room-error', (msg) => { alert("Room error: " + msg); }); socket.on('username-status', (data) => { if (data.isUnique) { username = data.username; // Use the username sent from the server localStorage.setItem('username', username); // Store the clientId in localStorage localStorage.setItem('clientId', data.clientId); setUsernameStatus(true, username); enableCreateLobbyBtn(); } else { alert(`Username ${username} is already in use or not available!`); } }); socket.on('update-rooms', (rooms) => { lobbiesList.innerHTML = ''; rooms.forEach(room => { const a = document.createElement('a'); a.href = `game.html?roomId=${room.id}`; a.textContent = room.player1 + "'s Room" + (room.status === 'IN_PROGRESS' ? ' (IN PROGRESS)' : ' (LOOKING FOR PLAYER)'); // Add the image next to the lobby name const img = document.createElement('img'); img.src = "http://localhost:3000" + room.imageUrl; img.alt = "[Cant load room pic]"; img.className = "lobbyPic" img.width = 42; a.prepend(img); // Add trash icon if the user is the owner of the lobby if (room.player1 === username) { const trashWrapper = document.createElement('div'); trashWrapper.className = "trash-wrapper"; const trashIcon = document.createElement('i'); trashIcon.className = "fa fa-trash"; trashIcon.style.color = "red"; trashIcon.addEventListener('click', (e) => { e.preventDefault(); socket.emit('delete-lobby', room.id); }); trashWrapper.appendChild(trashIcon); a.appendChild(trashWrapper); } lobbiesList.appendChild(a); }); }); //// FUNCTIONS function enableCreateLobbyBtn() { createLobbyBtn.classList.add('enabled'); createLobbyBtn.disabled = false; } function setUsernameStatus(isSet, currentUsername) { usernameStatus.textContent = isSet ? `Username set (${currentUsername})` : "Username not set"; usernameStatus.style.color = isSet ? "green" : "red"; // Set the tooltip for the createLobbyBtn createLobbyBtn.setAttribute('title', isSet ? "Be sure to have a lobby pic ready" : "Please set a username first"); } function checkIfSocketConnected(socket, customAlert = "") { let connected = socket.connected; if (!connected) { if (customAlert) { alert(customAlert) } else { alert("Connection not established / Connection lost with server's socket"); } } return connected; } function toggleSpinner(toggleON) { if(toggleON) { document.querySelector('.spinner-container').style.display = 'flex'; document.querySelector('.main-container').style.display = 'none'; } else { document.querySelector('.spinner-container').style.display = 'none'; document.querySelector('.main-container').style.display = 'flex'; } } ``` ## HTML za nas HOME page (index.html) ```html= <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Lobbies</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" type="text/css" media="screen" href="style.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js" integrity="sha512-zoJXRvW2gC8Z0Xo3lBbao5+AS3g6YWr5ztKqaicua11xHo+AvE1b0lT9ODgrHTmNUxeCw0Ry4BGRYZfXu70weg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> </head> <body> <div class="spinner-container"> <h2>Connecting to server</h2> <div class="spinner"></div> </div> <div class="main-container"> <div class="logo-container"> <img src="images/logo.png" alt="Manjka slika" id="site-logo"> </div> <div class="username-container"> <div id="username-status" class="username-status"></div> <div class="username-box"> <input type="text" id="username-input" placeholder="Enter username" autocomplete="off"> <button id="set-username-btn">SET</button> </div> <button id="create-lobby-btn">CREATE LOBBY</button> <h4 id="lobbiesListh4">Lobbies List</h4> <div id="lobbies-list"></div> </div> </div> <script src="script.js"></script> </body> </html> ``` ## CSS za lobbies index.html (poimenuj stil.css) ```css= body { font-family: Arial, sans-serif; background-color: #36393f; margin: 0; padding: 0; color: #ffffff; } .spinner-container { position: fixed; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background-color: rgba(0, 0, 0, 0.7); z-index: 1000; height: 100vh; } .spinner { border: 16px solid #f3f3f3; border-top: 16px solid #3498db; border-radius: 50%; width: 120px; height: 120px; animation: spin 2s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .main-container { /* display: none; hide it initially */ } .main-container { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; /*full height of the viewport*/ } .fa-trash { margin-left: 10px; transition: all 0.3s; border: 5px solid transparent; } .fa-trash:hover { background-color: rgba(255, 5, 5, 0.219); border-color: rgba(255, 5, 5, 0.219); border-radius: 5px; } .trash-wrapper { display: inline-block; } .trash-wrapper:hover .fa-trash { cursor: default !important; } /* Override the <a> tag's cursor when hovering over the trash icon */ a:hover .trash-wrapper:hover { cursor: default; } .logo-container { position: absolute; top: 60px; text-align: center; } #site-logo { width: 260px; height: auto; } .username-container { text-align: center; background-color: #2f3136; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); } .username-status { font-weight: bold; font-size: 14px; margin-bottom: 10px; } .username-box { display: inline-flex; align-items: center; margin-bottom: 10px; } #username-input { border: 2px solid #202225; padding: 10px; border-radius: 20px 0 0 20px; outline: none; width: 200px; background-color: #40444b; color: #ffffff; } #set-username-btn { border: 2px solid #202225; padding: 10px; border-radius: 0 20px 20px 0; background-color: #40444b; transition: background-color 0.3s; color: #ffffff; } #set-username-btn:hover { background-color: #50555b; } #set-username-btn:active { background-color: #30333b; } #create-lobby-btn { display: block; margin: 20px auto; padding: 15px 40px; border: none; border-radius: 30px; background-color: #202225; cursor: not-allowed; transition: background-color 0.3s; color: #ffffff; opacity: 0.4; } #create-lobby-btn.enabled { background-color: #7289da; cursor: pointer; opacity: 1; } #create-lobby-btn.enabled:hover { background-color: #5b6eae; } #lobbies-list { margin-top: 20px; max-height: 300px; overflow-y: scroll; overflow: auto; border: 1px solid #202225; border-radius: 10px; padding: 10px; background-color: #40444b; } a { text-decoration: none; color: #7289da; display: block; margin: 5px 0; padding: 5px; border-radius: 5px; transition: background-color 0.3s; } a:hover { background-color: #50555b; } .lobbyPic { border-radius: 50%; transition: border-radius 0.3s; vertical-align: middle; margin-right: 8px; } a:hover .lobbyPic { border-radius: 0%; } ``` # SERVER CODE (server.js) ### run with `npm start` (google what npm is) (before start ... do `npm i`) ```js const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const cors = require('cors'); var createError = require('http-errors'); const app = express(); const server = http.createServer(app); const multer = require('multer'); const rateLimit = require("express-rate-limit"); const fs = require('fs'); const { unlink } = require('fs'); const path = require('path'); app.use(express.static(path.join(__dirname, 'public'))); app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); const uploadsDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir); } app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); const bodyParser = require('body-parser'); app.use(bodyParser.json()); app.use(cors({ origin: "*", credentials: true })); // storage related // Setup multer storage const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, './uploads/') }, filename: function (req, file, cb) { cb(null, Date.now() + '-' + file.originalname) } }); const upload = multer({ storage: storage, limits: { fileSize: 5 * 1024 * 1024 // 5MB } }); // Rate limiting middleware const uploadLimiter = rateLimit({ windowMs: 5 * 1000, // 5 seconds max: 1, // 1 request per windowMs handler: function (req, res) { res.status(429).json({ error: "Too many images uploaded in the span of 5 seconds, please try again later." }); } }); app.use('/upload', uploadLimiter); // Endpoint to handle image uploads app.post('/upload', upload.single('lobbyImage'), (req, res, next) => { if (req.file) { res.json({ imageUrl: '/uploads/' + req.file.filename }); } else { next(new Error('Error uploading image.')); } }); app.get('/works', (req, res) => { res.send("Works.") }) // Error handling middleware app.use((err, req, res, next) => { if (err instanceof multer.MulterError) { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ error: 'Image size is too large. Please upload an image smaller than 5MB.' }); } return res.status(400).json({ error: 'Error uploading image.' }); } // Handle other errors here if needed res.status(500).json({ error: 'Internal Server Error.' }); }); /////////////// function setNewUsername(socket, newUsername, clientId) { // Only delete the old username if it's different from the new one if (socket.username && socket.username !== newUsername) { usernames.delete(socket.username); } usernames.add(newUsername); socket.username = newUsername; // Store the new username on the socket if (!clientId) { clientId = generateUUID(); } userMappings[newUsername] = { id: clientId, timestamp: Date.now() }; socket.emit('username-status', { isUnique: true, clientId, username: newUsername }); } function generateUUID() { // Simple UUID generator return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function checkWinner(board) { // Check rows, columns, and diagonals for a winner for (let i = 0; i < 3; i++) { if (board[i][0] && board[i][0] === board[i][1] && board[i][0] === board[i][2]) return board[i][0]; if (board[0][i] && board[0][i] === board[1][i] && board[0][i] === board[2][i]) return board[0][i]; } if (board[0][0] && board[0][0] === board[1][1] && board[0][0] === board[2][2]) return board[0][0]; if (board[0][2] && board[0][2] === board[1][1] && board[0][2] === board[2][0]) return board[0][2]; return null; } function getRooms() { return Object.entries(gameStates).map(([name, state]) => ({ id: name, player1: state.player1, name: state.player1 + "'s Room", status: state.player2 ? 'IN_PROGRESS' : 'LOOKING_FOR_PLAYER', imageUrl: state.imageUrl })); } /// SOCKET RELATED const io = socketIo(server, { cors: { origin: "*", methods: ["GET", "POST"], credentials: true } }); const gameStates = {}; const usernames = new Set(); const userRooms = {}; const userMappings = {}; // { username: { id: 'some-uuid', timestamp: Date.now() } } io.on('connection', (socket) => { console.log('A user connected'); socket.on('get-rooms', () => { let rooms = getRooms(); socket.emit('update-rooms', rooms); }); socket.on('set-username', (data) => { const newUsername = data.username; const clientId = data.clientId; if (usernames.has(newUsername)) { // If the username is already in use, check if the clientId matches const existingUser = userMappings[newUsername]; if (existingUser && existingUser.id === clientId && Date.now() - existingUser.timestamp <= 30000) { // If the clientId matches and the timestamp is within 30 seconds, allow reclaiming the username setNewUsername(socket, newUsername, clientId); } else { socket.emit('username-status', { isUnique: false }); } } else { setNewUsername(socket, newUsername, clientId); } }); socket.on('reconnect', (data) => { const { username, clientId, roomName } = data; const userMapping = userMappings[username]; if (userMapping && userMapping.id === clientId) { const gameState = gameStates[roomName]; if (gameState) { if (gameState.player1 === username) { gameState.player1Connected = true; // Set the connection flag for player1 socket.join(roomName); console.log(`${username} has rejoined room ${roomName}`); io.to(roomName).emit('players-ready', { roomName, player1: gameState.player1Connected ? gameState.player1 : null, player2: gameState.player2Connected ? gameState.player2 : null, currentPlayer: gameState.currentPlayer, }); socket.emit('player-symbol', { symbol: 'X' }); } else if (gameState.player2 === username) { gameState.player2Connected = true; // Set the connection flag for player2 socket.join(roomName); console.log(`${username} has rejoined room ${roomName}`); io.to(roomName).emit('players-ready', { roomName, player1: gameState.player1Connected ? gameState.player1 : null, player2: gameState.player2Connected ? gameState.player2 : null, currentPlayer: gameState.currentPlayer, }); socket.emit('player-symbol', { symbol: 'O' }); } } } }); socket.on('reclaim-username', (data) => { const usernameToReclaim = data.username; const clientId = data.clientId; const userMapping = userMappings[usernameToReclaim]; if (userMapping && userMapping.id === clientId) { setNewUsername(socket, usernameToReclaim, clientId); } else if (!usernames.has(usernameToReclaim)) { // If the username is not in use, set it as a new username setNewUsername(socket, usernameToReclaim, clientId); } else { socket.emit('username-status', { isUnique: false }); } }); socket.on('create-room', (data) => { const username = data.username; const imageUrl = data.imageUrl; if (!socket.username) { socket.emit('room-error', 'Please set a username first.'); return; } // Check if user already has a room if (userRooms[socket.username]) { console.log(`User ${socket.username} already has an active lobby: ${userRooms[socket.username]}`); socket.emit('room-error', 'You already have an active lobby.'); return; } const roomName = 'room-' + Math.random().toString(36).substr(2, 9); console.log(`Lobby ${roomName} created by ${socket.username}`); socket.join(roomName); console.log(`${username} has joined room ${roomName}`); const clientsInRoom = io.sockets.adapter.rooms.get(roomName); console.log(`Clients in room ${roomName}:`, [...clientsInRoom]); gameStates[roomName] = { player1: username, player1Connected: true, player2: null, player2Connected: false, board: Array(3).fill(null).map(() => Array(3).fill(null)), currentPlayer: 'X', imageUrl: imageUrl, gameInProgress: false }; userRooms[socket.username] = roomName; // Store the room against the user's name io.emit('update-rooms', getRooms()); }); socket.on('delete-lobby', (roomName) => { if (!gameStates[roomName]) { socket.emit('room-error', 'The lobby you are trying to delete does not exist.'); return; } if (gameStates[roomName].player1 !== socket.username) { socket.emit('room-error', 'You are not the owner of this lobby.'); return; } delete gameStates[roomName]; delete userRooms[socket.username]; // Ensure the user's lobby is deleted from userRooms console.log(`Lobby ${roomName} deleted by ${socket.username}`); io.emit('update-rooms', getRooms()); }); socket.on('join-room', (data) => { const roomName = data.roomName; const username = data.username; const gameState = gameStates[roomName]; if (gameState && gameState.gameInProgress) { socket.emit('room-error', 'Game is already in progress.'); return; } // Ensure the client joins the room socket.join(roomName); console.log(`Attempting to join room: ${roomName} with username: ${username}`); if (gameState) { console.log(`Current state of room ${roomName}:`, gameState); if (username !== gameState.player1 && !gameState.player2) { console.log(`Setting player2 as ${username}`); gameState.player2 = username; gameState.gameStarted = true; io.to(roomName).emit('game-started'); io.to(roomName).emit('players-ready', { roomName, player1: gameState.player1, player2: gameState.player2, currentPlayer: gameState.currentPlayer, }); socket.emit('player-symbol', { symbol: 'O' }); } else if (username === gameState.player1 && !gameState.player2) { console.log(`Player1 ${username} rejoining the room.`); io.to(roomName).emit('players-ready', { roomName, player1: gameState.player1, player2: gameState.player2, currentPlayer: gameState.currentPlayer, }); console.log(`Emitted players-ready event to room ${roomName}`); socket.emit('player-symbol', { symbol: 'X' }); } } else { console.log(`Room ${roomName} does not exist.`); } }); socket.on('check-room-existence', (data) => { const roomName = data.roomName; const gameState = gameStates[roomName]; if (gameState) { socket.emit('room-existence-response', { exists: true, player1: gameState.player1 }); } else { socket.emit('room-existence-response', { exists: false }); } }); socket.on('start-game', async (data) => { const roomName = data.roomName; const gameState = gameStates[roomName]; if (gameState) { gameState.gameInProgress = true; io.to(roomName).emit('game-started'); } }); socket.on('make-move', (move) => { const gameState = gameStates[move.roomName]; if (gameState && gameState.gameStarted && gameState.currentPlayer === move.symbol) { gameState.board[move.row][move.col] = move.symbol; gameState.currentPlayer = move.symbol === 'X' ? 'O' : 'X'; io.to(move.roomName).emit('receive-move', { row: move.row, col: move.col, symbol: move.symbol, nextPlayer: gameState.currentPlayer, }); const winner = checkWinner(gameState.board); let wasOver = false; if (winner) { io.to(move.roomName).emit('game-over', winner); wasOver = true; } else if (gameState.board.every(row => row.every(cell => cell))) { io.to(move.roomName).emit('game-over', 'DRAW'); wasOver = true; } if (wasOver) { delete userRooms[gameState.player1]; if (gameState.player2) { delete userRooms[gameState.player2]; } delete gameStates[move.roomName]; io.emit('update-rooms', getRooms()); } } }); socket.on('end-lobby', (roomName) => { const gameState = gameStates[roomName]; if (gameState) { // Delete the image associated with the room const imagePath = path.join(uploadsDir, path.basename(gameState.imageUrl)); unlink(imagePath, (err) => { if (err) { console.error('Error deleting the image:', err); } else { console.log('Image deleted successfully'); } }); delete userRooms[gameState.player1]; delete gameStates[roomName]; console.log(`Lobby ${roomName} deleted by ${socket.username}`); io.emit('update-rooms', getRooms()); socket.leave(roomName); } }); socket.on('rematch', (roomName) => { const gameState = gameStates[roomName]; if (gameState) { gameState.board = Array(3).fill(null).map(() => Array(3).fill(null)); gameState.currentPlayer = 'X'; io.to(roomName).emit('game-reset'); } }); socket.on('disconnect', () => { console.log('A user disconnected'); const roomName = userRooms[socket.username]; const gameState = gameStates[roomName]; if (gameState) { if (gameState.player1 === socket.username) { gameState.player1Connected = false; } else if (gameState.player2 === socket.username) { gameState.player2Connected = false; } } if (socket.username) { const userMapping = userMappings[socket.username]; if (userMapping) { userMapping.timestamp = Date.now(); } } }); }); //////////////// /// DB RELATED // const { // Pool // } = require('pg'); // const pool = new Pool({ // host: 'db', // Use service name defined in docker-compose.yml // port: 5432, // user: 'root', // password: 'rootpass', // database: 'tictactoe', // }); // Example query // pool.query('SELECT NOW()', (err, res) => { // console.log(err, res); // }); /// REST // POST endpoint to add game history // app.post('/add-game-history', (req, res) => { // // Example request body // const { // krogec, // krizec, // result, // koenc // } = req.body; // const query = 'INSERT INTO game_history (krogec, krizec, result, koenc) VALUES ($1, $2, $3, $4)'; // const values = [krogec, krizec, result, koenc]; // pool.query(query, values, (err) => { // if (err) { // console.error(err); // res.status(500).send('Error inserting data'); // } else { // res.status(200).send('Data inserted successfully'); // } // }); // }); // const createTableQuery = ` // CREATE TABLE IF NOT EXISTS game_history ( // id SERIAL PRIMARY KEY, // krogec VARCHAR(255), // krizec VARCHAR(255), // result VARCHAR(255), // koenc TIMESTAMP // ); // `; // pool.query(createTableQuery, (err) => { // if (err) { // console.error('Error creating table:', err); // } else { // console.log('Table game_history is ready'); // } // }); // catch 404 and forward to error handler app.use(function (req, res, next) { next(createError(404)); }); server.listen(3000, () => { console.log('Server running on http://localhost:3000'); }); ```