| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Caro Game - Online Multiplayer</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> |
| <style> |
| .cell { |
| width: 40px; |
| height: 40px; |
| transition: all 0.2s; |
| } |
| .cell:hover:not(.occupied) { |
| background-color: rgba(0, 0, 0, 0.05); |
| } |
| .x-symbol { |
| color: #3b82f6; |
| } |
| .o-symbol { |
| color: #ef4444; |
| } |
| .winning-cell { |
| animation: pulse 1.5s infinite; |
| } |
| @keyframes pulse { |
| 0% { transform: scale(1); } |
| 50% { transform: scale(1.1); } |
| 100% { transform: scale(1); } |
| } |
| .board { |
| display: grid; |
| grid-template-columns: repeat(15, 1fr); |
| gap: 1px; |
| background-color: #333; |
| } |
| #connectionStatus { |
| position: fixed; |
| bottom: 20px; |
| right: 20px; |
| padding: 8px 12px; |
| border-radius: 20px; |
| font-size: 14px; |
| display: flex; |
| align-items: center; |
| } |
| .connecting { |
| background-color: #fbbf24; |
| color: #92400e; |
| } |
| .connected { |
| background-color: #10b981; |
| color: #064e3b; |
| } |
| .disconnected { |
| background-color: #ef4444; |
| color: #7f1d1d; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4"> |
| |
| <div id="mainMenu" class="max-w-md w-full bg-white rounded-xl shadow-xl overflow-hidden"> |
| <div class="bg-gradient-to-r from-blue-500 to-purple-600 p-6 text-white"> |
| <h1 class="text-3xl font-bold text-center"> |
| <i class="fas fa-gamepad mr-2"></i> Caro Game Online |
| </h1> |
| <p class="text-center opacity-90 mt-2">Play against friends in real-time!</p> |
| </div> |
|
|
| <div class="p-6"> |
| <div class="mb-6"> |
| <label for="playerName" class="block text-sm font-medium text-gray-700 mb-1">Your Name</label> |
| <input type="text" id="playerName" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Enter your name"> |
| </div> |
|
|
| <div class="mb-6"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Game Mode</label> |
| <div class="grid grid-cols-2 gap-4"> |
| <button id="createRoomBtn" class="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4 rounded-lg transition flex items-center justify-center"> |
| <i class="fas fa-plus-circle mr-2"></i> Create Room |
| </button> |
| <button id="joinRoomBtn" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition flex items-center justify-center"> |
| <i class="fas fa-sign-in-alt mr-2"></i> Join Room |
| </button> |
| </div> |
| </div> |
|
|
| <div id="joinRoomSection" class="hidden mb-6"> |
| <label for="roomId" class="block text-sm font-medium text-gray-700 mb-1">Room ID</label> |
| <div class="flex"> |
| <input type="text" id="roomId" class="flex-1 px-4 py-2 border border-gray-300 rounded-l-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Enter room ID"> |
| <button id="joinSpecificRoomBtn" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-r-lg transition"> |
| Join |
| </button> |
| </div> |
| </div> |
|
|
| <div id="roomInfo" class="hidden mb-6"> |
| <div class="bg-gray-50 p-4 rounded-lg border border-gray-200"> |
| <h3 class="font-semibold text-lg mb-2 flex items-center"> |
| <i class="fas fa-door-open text-blue-500 mr-2"></i> Room Info |
| </h3> |
| <div class="flex justify-between mt-2"> |
| <span class="text-gray-600">Room ID:</span> |
| <span id="displayRoomId" class="font-medium"></span> |
| </div> |
| <div class="flex justify-between mt-1"> |
| <span class="text-gray-600">Created:</span> |
| <span id="roomCreatedAt" class="text-sm text-gray-500"></span> |
| </div> |
| <div id="playersInRoom" class="mt-3 space-y-2"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| <div id="waitingForPlayers" class="hidden text-center py-4"> |
| <div class="animate-pulse flex flex-col items-center"> |
| <i class="fas fa-user-clock text-4xl text-blue-500 mb-2"></i> |
| <p class="text-gray-600">Waiting for another player to join...</p> |
| <div class="mt-4 bg-gray-100 rounded-lg p-3"> |
| <p class="text-sm font-medium">Share this room ID:</p> |
| <p id="shareRoomId" class="font-mono text-lg font-bold mt-1"></p> |
| <button id="copyRoomIdBtn" class="mt-2 bg-gray-200 hover:bg-gray-300 text-gray-800 text-sm font-medium py-1 px-3 rounded transition"> |
| <i class="fas fa-copy mr-1"></i> Copy to clipboard |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="gameScreen" class="max-w-4xl w-full bg-white rounded-xl shadow-xl overflow-hidden hidden"> |
| <div class="bg-gradient-to-r from-blue-500 to-purple-600 p-6 text-white"> |
| <div class="flex justify-between items-center"> |
| <h1 class="text-2xl font-bold"> |
| <i class="fas fa-gamepad mr-2"></i> Caro Online |
| </h1> |
| <div id="roomStatus" class="text-sm bg-white bg-opacity-20 px-3 py-1 rounded-full">Room: <span id="currentRoomId"></span></div> |
| </div> |
| </div> |
|
|
| <div class="p-6 flex flex-col md:flex-row gap-8"> |
| <div class="flex-1"> |
| <div class="flex justify-between items-center mb-6"> |
| <div class="player-card bg-blue-50 p-4 rounded-lg border-l-4 border-blue-500 flex-1 mr-2 transition-all duration-300" id="player1Card"> |
| <h3 class="font-semibold text-blue-700" id="player1Name">Player X</h3> |
| <p class="text-sm text-gray-600">You</p> |
| </div> |
| <div class="player-card bg-gray-100 p-4 rounded-lg border-l-4 border-gray-300 flex-1 ml-2 transition-all duration-300" id="player2Card"> |
| <h3 class="font-semibold text-gray-700" id="player2Name">Player O</h3> |
| <p class="text-sm text-gray-600">Waiting...</p> |
| </div> |
| </div> |
|
|
| <div class="board-container overflow-auto border rounded-lg shadow-inner bg-white p-2"> |
| <div class="board" id="board"></div> |
| </div> |
|
|
| <div class="mt-6 flex justify-between items-center"> |
| <button id="leaveRoomBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded-lg transition"> |
| <i class="fas fa-sign-out-alt mr-2"></i> Leave Room |
| </button> |
| <div id="gameStatus" class="text-lg font-medium text-gray-700"> |
| Setting up game... |
| </div> |
| <button id="gameResetBtn" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition"> |
| <i class="fas fa-redo mr-2"></i> Rematch |
| </button> |
| </div> |
| </div> |
|
|
| <div class="md:w-64 bg-gray-50 p-4 rounded-lg border border-gray-200"> |
| <h3 class="font-semibold text-lg mb-4 flex items-center"> |
| <i class="fas fa-comments text-purple-500 mr-2"></i> Chat |
| </h3> |
| <div class="chat-messages h-48 overflow-y-auto mb-3" id="chatMessages"> |
| <div class="text-center text-sm text-gray-500 py-4">No messages yet</div> |
| </div> |
| <div class="flex"> |
| <input type="text" id="chatInput" class="flex-1 px-3 py-2 border border-gray-300 rounded-l-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Type a message..."> |
| <button id="sendMessageBtn" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-r-lg transition"> |
| <i class="fas fa-paper-plane"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="winModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50"> |
| <div class="bg-white rounded-xl p-8 max-w-md w-full mx-4 text-center"> |
| <div class="text-6xl mb-4" id="winSymbol"></div> |
| <h2 class="text-2xl font-bold mb-2" id="winMessage"></h2> |
| <p class="text-gray-600 mb-6" id="winDescription"></p> |
| <div class="flex gap-4 justify-center"> |
| <button id="playAgainBtn" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-6 rounded-lg transition"> |
| Play Again |
| </button> |
| <button id="closeModalBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-6 rounded-lg transition"> |
| Close |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="connectionStatus" class="connecting"> |
| <i class="fas fa-circle-notch fa-spin mr-2"></i> |
| <span>Connecting...</span> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| const BOARD_SIZE = 15; |
| |
| |
| const mainMenu = document.getElementById('mainMenu'); |
| const gameScreen = document.getElementById('gameScreen'); |
| const playerNameInput = document.getElementById('playerName'); |
| const createRoomBtn = document.getElementById('createRoomBtn'); |
| const joinRoomBtn = document.getElementById('joinRoomBtn'); |
| const joinRoomSection = document.getElementById('joinRoomSection'); |
| const roomIdInput = document.getElementById('roomId'); |
| const joinSpecificRoomBtn = document.getElementById('joinSpecificRoomBtn'); |
| const roomInfo = document.getElementById('roomInfo'); |
| const displayRoomId = document.getElementById('displayRoomId'); |
| const playersInRoom = document.getElementById('playersInRoom'); |
| const waitingForPlayers = document.getElementById('waitingForPlayers'); |
| const shareRoomId = document.getElementById('shareRoomId'); |
| const copyRoomIdBtn = document.getElementById('copyRoomIdBtn'); |
| const board = document.getElementById('board'); |
| const gameStatus = document.getElementById('gameStatus'); |
| const leaveRoomBtn = document.getElementById('leaveRoomBtn'); |
| const gameResetBtn = document.getElementById('gameResetBtn'); |
| const player1Card = document.getElementById('player1Card'); |
| const player2Card = document.getElementById('player2Card'); |
| const player1Name = document.getElementById('player1Name'); |
| const player2Name = document.getElementById('player2Name'); |
| const winModal = document.getElementById('winModal'); |
| const winSymbol = document.getElementById('winSymbol'); |
| const winMessage = document.getElementById('winMessage'); |
| const winDescription = document.getElementById('winDescription'); |
| const playAgainBtn = document.getElementById('playAgainBtn'); |
| const closeModalBtn = document.getElementById('closeModalBtn'); |
| const chatMessages = document.getElementById('chatMessages'); |
| const chatInput = document.getElementById('chatInput'); |
| const sendMessageBtn = document.getElementById('sendMessageBtn'); |
| const connectionStatus = document.getElementById('connectionStatus'); |
| const currentRoomIdDisplay = document.getElementById('currentRoomId'); |
| |
| |
| let socket; |
| let currentPlayer = ''; |
| let gameBoard = []; |
| let gameActive = false; |
| let playerId = ''; |
| let roomId = ''; |
| let playerSymbol = ''; |
| let opponentName = ''; |
| let playerName = ''; |
| let isRoomCreator = false; |
| |
| |
| function initializeSocket() { |
| |
| const socketUrl = window.location.hostname === 'localhost' |
| ? 'http://localhost:3000' |
| : window.location.origin; |
| |
| socket = io(socketUrl, { |
| reconnection: true, |
| reconnectionAttempts: 5, |
| reconnectionDelay: 1000, |
| reconnectionDelayMax: 5000 |
| }); |
| |
| |
| socket.on('connect', () => { |
| console.log('Connected to server'); |
| connectionStatus.className = 'connected'; |
| connectionStatus.innerHTML = '<i class="fas fa-plug mr-2"></i><span>Connected</span>'; |
| }); |
| |
| socket.on('disconnect', () => { |
| console.log('Disconnected from server'); |
| connectionStatus.className = 'disconnected'; |
| connectionStatus.innerHTML = '<i class="fas fa-plug mr-2"></i><span>Disconnected</span>'; |
| |
| if (roomId) { |
| showError('Disconnected from server. Trying to reconnect...'); |
| } |
| }); |
| |
| socket.on('connect_error', (err) => { |
| console.log('Connection error:', err); |
| connectionStatus.className = 'disconnected'; |
| connectionStatus.innerHTML = '<i class="fas fa-exclamation-triangle mr-2"></i><span>Connection Error</span>'; |
| }); |
| |
| socket.on('reconnecting', (attempt) => { |
| console.log('Attempting to reconnect:', attempt); |
| connectionStatus.className = 'connecting'; |
| connectionStatus.innerHTML = `<i class="fas fa-circle-notch fa-spin mr-2"></i><span>Reconnecting (${attempt})...</span>`; |
| }); |
| |
| socket.on('reconnect', () => { |
| console.log('Reconnected to server'); |
| connectionStatus.className = 'connected'; |
| connectionStatus.innerHTML = '<i class="fas fa-plug mr-2"></i><span>Reconnected</span>'; |
| }); |
| |
| |
| socket.on('playerId', (id) => { |
| playerId = id; |
| console.log('Your player ID:', playerId); |
| }); |
| |
| socket.on('roomCreated', (data) => { |
| console.log('Room created:', data.roomId); |
| roomId = data.roomId; |
| isRoomCreator = true; |
| |
| displayRoomId.textContent = roomId; |
| shareRoomId.textContent = roomId; |
| currentRoomIdDisplay.textContent = roomId; |
| |
| |
| updatePlayerList([{ id: playerId, name: playerName }]); |
| |
| roomInfo.classList.remove('hidden'); |
| waitingForPlayers.classList.remove('hidden'); |
| joinRoomSection.classList.add('hidden'); |
| }); |
| |
| socket.on('roomJoined', (data) => { |
| console.log('Joined room:', data); |
| roomId = data.roomId; |
| currentRoomIdDisplay.textContent = roomId; |
| |
| |
| updatePlayerList(data.players); |
| |
| |
| if (data.players.length === 2) { |
| waitingForPlayers.classList.add('hidden'); |
| roomInfo.classList.remove('hidden'); |
| gameStatus.textContent = 'Game starting...'; |
| } else { |
| waitingForPlayers.classList.remove('hidden'); |
| roomInfo.classList.remove('hidden'); |
| } |
| |
| mainMenu.classList.add('hidden'); |
| gameScreen.classList.remove('hidden'); |
| }); |
| |
| socket.on('playerJoined', (data) => { |
| console.log('Player joined:', data.player.name); |
| updatePlayerList(data.players); |
| |
| |
| if (isRoomCreator && data.players.length === 2) { |
| socket.emit('startGame', { roomId }); |
| waitingForPlayers.classList.add('hidden'); |
| gameStatus.textContent = 'Game starting...'; |
| } |
| }); |
| |
| socket.on('playerLeft', (data) => { |
| console.log('Player left:', data.playerId); |
| updatePlayerList(data.players); |
| |
| if (gameActive) { |
| gameActive = false; |
| showError('Opponent disconnected. Waiting for reconnection...'); |
| } |
| }); |
| |
| socket.on('gameStarted', (data) => { |
| console.log('Game started:', data); |
| playerSymbol = data.playerSymbol; |
| opponentName = data.players.find(p => p.id !== playerId)?.name || 'Opponent'; |
| |
| |
| if (playerSymbol === 'X') { |
| player1Name.textContent = playerName; |
| player1Card.querySelector('p').textContent = 'You (X)'; |
| player2Name.textContent = opponentName; |
| player2Card.querySelector('p').textContent = 'Opponent (O)'; |
| currentPlayer = 'X'; |
| } else { |
| player1Name.textContent = opponentName; |
| player1Card.querySelector('p').textContent = 'Opponent (X)'; |
| player2Name.textContent = playerName; |
| player2Card.querySelector('p').textContent = 'You (O)'; |
| currentPlayer = 'X'; |
| } |
| |
| initializeBoard(); |
| gameActive = true; |
| updateTurnDisplay(); |
| }); |
| |
| socket.on('moveMade', (data) => { |
| console.log('Move received:', data); |
| const { row, col, player } = data; |
| |
| |
| gameBoard[row][col] = player; |
| const cell = document.querySelector(`[data-row="${row}"][data-col="${col}"]`); |
| cell.classList.add('occupied'); |
| |
| const symbol = document.createElement('div'); |
| symbol.classList.add('text-2xl', 'font-bold'); |
| symbol.textContent = player; |
| symbol.classList.add(player === 'X' ? 'x-symbol' : 'o-symbol'); |
| |
| cell.innerHTML = ''; |
| cell.appendChild(symbol); |
| |
| |
| currentPlayer = currentPlayer === 'X' ? 'O' : 'X'; |
| updateTurnDisplay(); |
| }); |
| |
| socket.on('gameOver', (data) => { |
| console.log('Game over:', data); |
| gameActive = false; |
| |
| |
| if (data.winningCells) { |
| data.winningCells.forEach(cell => { |
| const cellElement = document.querySelector(`[data-row="${cell.row}"][data-col="${cell.col}"]`); |
| cellElement.classList.add('winning-cell'); |
| }); |
| } |
| |
| |
| if (data.winner) { |
| const isWinner = (playerSymbol === data.winner); |
| winSymbol.innerHTML = data.winner === 'X' ? |
| '<i class="fas fa-times text-blue-500"></i>' : |
| '<i class="far fa-circle text-red-500"></i>'; |
| winMessage.textContent = isWinner ? 'You Won!' : 'You Lost!'; |
| winDescription.textContent = isWinner ? |
| `Congratulations! You defeated ${opponentName}.` : |
| `${opponentName} won this game. Better luck next time!`; |
| } else { |
| winSymbol.innerHTML = '<i class="fas fa-handshake text-yellow-500"></i>'; |
| winMessage.textContent = "It's a Draw!"; |
| winDescription.textContent = "The game has ended in a tie."; |
| } |
| |
| winModal.classList.remove('hidden'); |
| }); |
| |
| socket.on('rematchOffer', () => { |
| console.log('Received rematch offer'); |
| showMessage(`${opponentName} wants a rematch!`, 'purple'); |
| }); |
| |
| socket.on('rematchAccepted', () => { |
| console.log('Rematch accepted'); |
| showMessage(`${opponentName} accepted the rematch!`, 'purple'); |
| initializeBoard(); |
| gameActive = true; |
| updateTurnDisplay(); |
| }); |
| |
| socket.on('newGame', () => { |
| console.log('Starting new game'); |
| initializeBoard(); |
| gameActive = true; |
| updateTurnDisplay(); |
| }); |
| |
| socket.on('error', (error) => { |
| console.error('Error:', error); |
| showError(error.message); |
| }); |
| |
| socket.on('chatMessage', (message) => { |
| addChatMessage(message.sender, message.text, message.isSystem); |
| }); |
| } |
| |
| |
| function initializeBoard() { |
| board.innerHTML = ''; |
| board.style.gridTemplateColumns = `repeat(${BOARD_SIZE}, 1fr)`; |
| gameBoard = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill('')); |
| |
| for (let i = 0; i < BOARD_SIZE; i++) { |
| for (let j = 0; j < BOARD_SIZE; j++) { |
| const cell = document.createElement('div'); |
| cell.classList.add('cell', 'border', 'border-gray-200', 'flex', 'items-center', 'justify-center', 'cursor-pointer'); |
| cell.dataset.row = i; |
| cell.dataset.col = j; |
| |
| |
| if (i === 0) { |
| const colLabel = document.createElement('div'); |
| colLabel.classList.add('text-xs', 'text-gray-400', 'absolute', '-mt-6'); |
| colLabel.textContent = j + 1; |
| cell.appendChild(colLabel); |
| } |
| |
| if (j === 0) { |
| const rowLabel = document.createElement('div'); |
| rowLabel.classList.add('text-xs', 'text-gray-400', 'absolute', '-ml-6'); |
| rowLabel.textContent = i + 1; |
| cell.appendChild(rowLabel); |
| } |
| |
| cell.addEventListener('click', () => handleCellClick(i, j)); |
| board.appendChild(cell); |
| } |
| } |
| } |
| |
| |
| function handleCellClick(row, col) { |
| if (!gameActive || currentPlayer !== playerSymbol || gameBoard[row][col] !== '') return; |
| |
| |
| socket.emit('makeMove', { roomId, row, col, player: playerSymbol }); |
| |
| |
| gameBoard[row][col] = playerSymbol; |
| const cell = document.querySelector(`[data-row="${row}"][data-col="${col}"]`); |
| cell.classList.add('occupied'); |
| |
| const symbol = document.createElement('div'); |
| symbol.classList.add('text-2xl', 'font-bold'); |
| symbol.textContent = playerSymbol; |
| symbol.classList.add(playerSymbol === 'X' ? 'x-symbol' : 'o-symbol'); |
| |
| cell.innerHTML = ''; |
| cell.appendChild(symbol); |
| |
| |
| currentPlayer = currentPlayer === 'X' ? 'O' : 'X'; |
| updateTurnDisplay(); |
| } |
| |
| |
| function updateTurnDisplay() { |
| if (currentPlayer === playerSymbol) { |
| gameStatus.innerHTML = `<span class="font-semibold">Your turn</span> (${playerSymbol})`; |
| player1Card.classList.toggle('bg-blue-50', playerSymbol === 'X'); |
| player1Card.classList.toggle('border-blue-500', playerSymbol === 'X'); |
| player2Card.classList.toggle('bg-blue-50', playerSymbol === 'O'); |
| player2Card.classList.toggle('border-blue-500', playerSymbol === 'O'); |
| } else { |
| gameStatus.innerHTML = `<span class="font-semibold">${opponentName}'s turn</span> (${currentPlayer})`; |
| player1Card.classList.toggle('bg-blue-50', playerSymbol !== 'X'); |
| player1Card.classList.toggle('border-blue-500', playerSymbol !== 'X'); |
| player2Card.classList.toggle('bg-blue-50', playerSymbol !== 'O'); |
| player2Card.classList.toggle('border-blue-500', playerSymbol !== 'O'); |
| } |
| } |
| |
| |
| function updatePlayerList(players) { |
| playersInRoom.innerHTML = ''; |
| |
| if (players.length === 0) { |
| playersInRoom.innerHTML = '<div class="text-center text-gray-500 py-2">No players</div>'; |
| return; |
| } |
| |
| players.forEach(player => { |
| const playerElement = document.createElement('div'); |
| playerElement.classList.add('flex', 'items-center', 'py-1'); |
| |
| const icon = document.createElement('i'); |
| icon.classList.add('fas', player.id === playerId ? 'fa-user' : 'fa-user-friends', 'mr-2'); |
| icon.style.color = player.id === playerId ? '#3b82f6' : '#6b7280'; |
| |
| const name = document.createElement('span'); |
| name.textContent = player.name; |
| if (player.id === playerId) { |
| name.classList.add('font-medium', 'text-blue-600'); |
| } |
| |
| playerElement.appendChild(icon); |
| playerElement.appendChild(name); |
| playersInRoom.appendChild(playerElement); |
| }); |
| } |
| |
| |
| function showError(message) { |
| const existingError = document.getElementById('tempError'); |
| if (existingError) existingError.remove(); |
| |
| const errorElement = document.createElement('div'); |
| errorElement.id = 'tempError'; |
| errorElement.className = 'bg-red-100 border-l-4 border-red-500 text-red-700 p-3 mb-4 rounded'; |
| errorElement.role = 'alert'; |
| |
| const content = document.createElement('p'); |
| content.className = 'font-medium'; |
| content.innerHTML = `<i class="fas fa-exclamation-circle mr-2"></i> ${message}`; |
| |
| errorElement.appendChild(content); |
| |
| const firstChild = document.querySelector('.p-6 > div:first-child'); |
| if (firstChild) { |
| firstChild.parentNode.insertBefore(errorElement, firstChild); |
| } |
| |
| |
| setTimeout(() => { |
| errorElement.remove(); |
| }, 5000); |
| } |
| |
| |
| function showMessage(message, color = 'blue') { |
| const colorClasses = { |
| blue: 'bg-blue-100 border-blue-500 text-blue-700', |
| green: 'bg-green-100 border-green-500 text-green-700', |
| purple: 'bg-purple-100 border-purple-500 text-purple-700' |
| }; |
| |
| const existingMsg = document.getElementById('tempMessage'); |
| if (existingMsg) existingMsg.remove(); |
| |
| const msgElement = document.createElement('div'); |
| msgElement.id = 'tempMessage'; |
| msgElement.className = `${colorClasses[color]} border-l-4 p-3 mb-4 rounded`; |
| msgElement.role = 'alert'; |
| |
| const content = document.createElement('p'); |
| content.className = 'font-medium'; |
| content.innerHTML = `<i class="fas fa-info-circle mr-2"></i> ${message}`; |
| |
| msgElement.appendChild(content); |
| |
| const firstChild = document.querySelector('.p-6 > div:first-child'); |
| if (firstChild) { |
| firstChild.parentNode.insertBefore(msgElement, firstChild); |
| } |
| |
| |
| setTimeout(() => { |
| msgElement.remove(); |
| }, 5000); |
| } |
| |
| |
| function addChatMessage(sender, text, isSystem = false) { |
| if (chatMessages.firstChild?.classList?.contains('text-center')) { |
| chatMessages.innerHTML = ''; |
| } |
| |
| const messageElement = document.createElement('div'); |
| messageElement.className = 'mb-2'; |
| |
| if (isSystem) { |
| messageElement.innerHTML = ` |
| <div class="text-xs text-center text-gray-500 mb-1">${text}</div> |
| `; |
| } else { |
| const isCurrentUser = sender === playerName; |
| messageElement.innerHTML = ` |
| <div class="flex ${isCurrentUser ? 'justify-end' : 'justify-start'}"> |
| <div class="max-w-xs ${isCurrentUser ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800'} rounded-lg p-2"> |
| <div class="text-xs font-semibold ${isCurrentUser ? 'text-blue-100' : 'text-gray-600'}">${sender}</div> |
| <div class="text-sm">${text}</div> |
| </div> |
| </div> |
| `; |
| } |
| |
| chatMessages.appendChild(messageElement); |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| } |
| |
| |
| createRoomBtn.addEventListener('click', () => { |
| playerName = playerNameInput.value.trim(); |
| if (!playerName) { |
| showError('Please enter your name'); |
| return; |
| } |
| |
| socket.emit('createRoom', { playerName }); |
| }); |
| |
| joinRoomBtn.addEventListener('click', () => { |
| playerName = playerNameInput.value.trim(); |
| if (!playerName) { |
| showError('Please enter your name'); |
| return; |
| } |
| |
| joinRoomSection.classList.remove('hidden'); |
| }); |
| |
| joinSpecificRoomBtn.addEventListener('click', () => { |
| const roomId = roomIdInput.value.trim(); |
| if (!roomId) { |
| showError('Please enter a room ID'); |
| return; |
| } |
| |
| socket.emit('joinRoom', { roomId, playerName }); |
| }); |
| |
| copyRoomIdBtn.addEventListener('click', () => { |
| navigator.clipboard.writeText(roomId) |
| .then(() => { |
| const originalText = copyRoomIdBtn.innerHTML; |
| copyRoomIdBtn.innerHTML = '<i class="fas fa-check mr-1"></i> Copied!'; |
| setTimeout(() => { |
| copyRoomIdBtn.innerHTML = originalText; |
| }, 2000); |
| }) |
| .catch(err => { |
| console.error('Failed to copy:', err); |
| }); |
| }); |
| |
| leaveRoomBtn.addEventListener('click', () => { |
| socket.emit('leaveRoom', { roomId }); |
| mainMenu.classList.remove('hidden'); |
| gameScreen.classList.add('hidden'); |
| roomId = ''; |
| playerSymbol = ''; |
| opponentName = ''; |
| isRoomCreator = false; |
| }); |
| |
| gameResetBtn.addEventListener('click', () => { |
| if (!opponentName) { |
| showError('Wait for opponent to join before starting a rematch'); |
| return; |
| } |
| |
| socket.emit('offerRematch', { roomId }); |
| showMessage(`You offered a rematch to ${opponentName}`, 'purple'); |
| }); |
| |
| playAgainBtn.addEventListener('click', () => { |
| socket.emit('acceptRematch', { roomId }); |
| winModal.classList.add('hidden'); |
| }); |
| |
| closeModalBtn.addEventListener('click', () => { |
| winModal.classList.add('hidden'); |
| }); |
| |
| sendMessageBtn.addEventListener('click', sendChatMessage); |
| chatInput.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') { |
| sendChatMessage(); |
| } |
| }); |
| |
| function sendChatMessage() { |
| const message = chatInput.value.trim(); |
| if (!message || !roomId) return; |
| |
| socket.emit('sendChatMessage', { roomId, message, sender: playerName }); |
| addChatMessage(playerName, message); |
| chatInput.value = ''; |
| } |
| |
| |
| initializeSocket(); |
| }); |
| </script> |
| </body> |
| </html> |