| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Multiplayer Tic-Tac-Toe</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script> |
| <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet"> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <script> |
| tailwind.config = { |
| theme: { |
| extend: { |
| colors: { |
| primary: '#6366f1', |
| secondary: '#818cf8', |
| accent: '#a5b4fc', |
| dark: '#1e293b', |
| light: '#f1f5f9', |
| 'board-bg': '#ede9fe' |
| }, |
| fontFamily: { |
| sans: ['Nunito', 'sans-serif'] |
| }, |
| animation: { |
| 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', |
| 'bounce-slow': 'bounce 2s infinite' |
| } |
| } |
| } |
| } |
| </script> |
| <style> |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(-10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| .fade-in { |
| animation: fadeIn 0.5s ease-out forwards; |
| } |
| |
| .grid-cell:hover { |
| transform: scale(1.05); |
| transition: transform 0.3s ease; |
| } |
| |
| .winning-cell { |
| animation: pulse 2s infinite, bounce 1s infinite; |
| } |
| |
| #game-container { |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15), 0 15px 30px rgba(99, 102, 241, 0.1); |
| } |
| |
| .chip-animation { |
| position: absolute; |
| background-color: rgba(255, 255, 255, 0.6); |
| border-radius: 50%; |
| animation: expand 1s forwards; |
| opacity: 0; |
| } |
| |
| @keyframes expand { |
| 0% { transform: scale(0.1); opacity: 1; } |
| 100% { transform: scale(5); opacity: 0; } |
| } |
| |
| .status-banner { |
| transition: all 0.3s ease; |
| } |
| |
| .confetti { |
| position: absolute; |
| width: 10px; |
| height: 10px; |
| opacity: 0.7; |
| animation: fall 3s ease-in-out forwards; |
| } |
| |
| @keyframes fall { |
| 0% { transform: translateY(-10vh) rotate(0deg); opacity: 1; } |
| 100% { transform: translateY(100vh) rotate(360deg); opacity: 0; } |
| } |
| </style> |
| </head> |
| <body class="bg-gradient-to-br from-indigo-50 to-purple-100 min-h-screen flex flex-col items-center justify-center p-4 font-sans"> |
| <div id="game-container" class="w-full max-w-2xl bg-white rounded-2xl overflow-hidden fade-in"> |
| |
| <div class="bg-gradient-to-r from-primary to-secondary text-white py-5 px-6 text-center"> |
| <h1 class="text-3xl md:text-4xl font-bold flex items-center justify-center gap-2"> |
| <i class="fas fa-gamepad"></i> |
| <span>Multiplayer Tic-Tac-Toe</span> |
| </h1> |
| <p class="mt-2 opacity-90">Play against friends in real-time!</p> |
| </div> |
| |
| |
| <div id="lobby" class="p-6"> |
| <div class="text-center mb-8"> |
| <i class="fas fa-users text-6xl text-primary mb-4"></i> |
| <h2 class="text-2xl font-bold text-gray-800">Join or Create a Game</h2> |
| <p class="text-gray-600 mt-2">Share the Game ID with a friend to play together</p> |
| </div> |
| |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> |
| |
| <div class="bg-light rounded-xl p-6 border border-accent"> |
| <div class="flex items-center gap-3 mb-4"> |
| <div class="bg-primary rounded-lg p-3"> |
| <i class="fas fa-plus-circle text-white text-xl"></i> |
| </div> |
| <h3 class="font-bold text-xl text-gray-800">Create New Game</h3> |
| </div> |
| <p class="text-gray-700 mb-4">Create a new game and invite a friend to play with you.</p> |
| <button id="create-game" class="w-full bg-primary hover:bg-primary/90 text-white py-3 rounded-lg font-semibold transition flex items-center justify-center gap-2"> |
| <i class="fas fa-chess-board"></i> Create Game |
| </button> |
| </div> |
| |
| |
| <div class="bg-light rounded-xl p-6 border border-accent"> |
| <div class="flex items-center gap-3 mb-4"> |
| <div class="bg-secondary rounded-lg p-3"> |
| <i class="fas fa-user-plus text-white text-xl"></i> |
| </div> |
| <h3 class="font-bold text-xl text-gray-800">Join Existing Game</h3> |
| </div> |
| <div class="space-y-4"> |
| <div> |
| <label class="block text-gray-700 font-medium mb-2" for="game-id">Enter Game ID</label> |
| <input type="text" id="game-id" placeholder="Game ID" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary focus:border-transparent"> |
| </div> |
| <button id="join-game" class="w-full bg-secondary hover:bg-secondary/90 text-white py-3 rounded-lg font-semibold transition flex items-center justify-center gap-2"> |
| <i class="fas fa-sign-in-alt"></i> Join Game |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="game-id-display" class="hidden bg-light rounded-xl p-4 border border-accent"> |
| <div class="flex flex-col items-center text-center"> |
| <p class="font-bold text-lg text-gray-800 mb-2">Your Game ID:</p> |
| <div class="flex items-center gap-2"> |
| <p id="game-code" class="font-mono text-2xl font-bold bg-white py-2 px-6 rounded-lg border border-primary text-primary"></p> |
| <button id="copy-id" class="bg-accent text-primary p-2 rounded-lg hover:bg-accent/80 transition"> |
| <i class="fas fa-copy"></i> |
| </button> |
| </div> |
| <p class="text-gray-600 mt-3 flex items-center gap-2"> |
| <i class="fas fa-info-circle text-primary"></i> |
| <span>Share this code with your friend to join the game</span> |
| </p> |
| </div> |
| </div> |
| |
| |
| <div id="waiting-room" class="hidden mt-8 bg-board-bg rounded-xl p-8 text-center border border-accent"> |
| <div class="flex flex-col items-center"> |
| <div class="relative mb-6"> |
| <div class="w-24 h-24 rounded-full bg-accent flex items-center justify-center"> |
| <i class="fas fa-user-clock text-5xl text-primary"></i> |
| </div> |
| <div class="absolute -top-2 -right-2 w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center animate-bounce-slow"> |
| <span id="player-count">1</span> |
| </div> |
| </div> |
| <h3 class="text-xl font-bold text-gray-800 mb-3">Waiting for players...</h3> |
| <div class="w-full max-w-md bg-white rounded-lg p-4 mx-auto"> |
| <div class="flex items-center justify-center gap-4 mb-4"> |
| <div class="bg-primary/10 px-4 py-2 rounded-lg flex items-center gap-2"> |
| <i class="fas fa-user text-primary"></i> |
| <span id="player-name" class="font-semibold">You</span> |
| </div> |
| <span class="font-bold text-gray-600">vs</span> |
| <div class="bg-gray-100 px-4 py-2 rounded-lg flex items-center gap-2"> |
| <i class="fas fa-user-clock text-gray-500"></i> |
| <span class="text-gray-500">Waiting...</span> |
| </div> |
| </div> |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> |
| <div class="bg-primary h-2.5 rounded-full w-1/3 animate-pulse-slow"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div id="game-section" class="hidden p-6"> |
| <div id="game-status" class="status-banner bg-gradient-to-r from-purple-600 to-indigo-700 text-white py-3 px-6 rounded-lg flex items-center justify-between mb-6"> |
| <div class="flex items-center gap-2"> |
| <i class="fas fa-info-circle"></i> |
| <span id="status-text">Your turn! (X)</span> |
| </div> |
| <div id="players"> |
| <span id="player-symbol" class="font-bold">X</span> |
| <span class="mx-1">|</span> |
| <span id="game-id-label"></span> |
| </div> |
| </div> |
| |
| |
| <div class="relative bg-board-bg rounded-2xl p-4 overflow-hidden"> |
| <div id="game-board" class="grid grid-cols-3 gap-4 max-w-lg mx-auto my-4"> |
| |
| </div> |
| |
| |
| <div id="game-over" class="hidden absolute inset-0 bg-black/80 flex items-center justify-center rounded-2xl"> |
| <div class="bg-white p-8 rounded-xl text-center"> |
| <h3 id="result-text" class="text-2xl font-bold mb-4">X Wins!</h3> |
| <div class="flex flex-wrap gap-3 justify-center"> |
| <button id="play-again" class="bg-primary hover:bg-primary/90 text-white py-3 px-6 rounded-lg font-semibold transition"> |
| <i class="fas fa-redo mr-2"></i> Play Again |
| </button> |
| <button id="new-game" class="bg-secondary hover:bg-secondary/90 text-white py-3 px-6 rounded-lg font-semibold transition"> |
| <i class="fas fa-plus mr-2"></i> New Game |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="flex flex-col sm:flex-row gap-4 mt-8"> |
| <div class="flex gap-2"> |
| <button id="copy-link" class="bg-light hover:bg-accent text-primary px-4 py-2 rounded-lg transition flex items-center gap-2"> |
| <i class="fas fa-link"></i> Copy Invite Link |
| </button> |
| <button id="leave-game" class="bg-light hover:bg-rose-500/10 text-rose-500 px-4 py-2 rounded-lg transition flex items-center gap-2"> |
| <i class="fas fa-sign-out-alt"></i> Leave Game |
| </button> |
| </div> |
| <div class="ml-auto flex gap-2"> |
| <div id="player-one" class="bg-light px-4 py-2 rounded-lg flex items-center gap-2"> |
| <i class="fas fa-user text-primary"></i> |
| <span>X: <span id="player-one-name">Player 1</span></span> |
| </div> |
| <div id="player-two" class="bg-light px-4 py-2 rounded-lg flex items-center gap-2"> |
| <i class="fas fa-user text-secondary"></i> |
| <span>O: <span id="player-two-name">Player 2</span></span> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <footer class="mt-8 text-center text-gray-600"> |
| <p>Created with <i class="fas fa-heart text-red-500"></i> & Socket.io</p> |
| </footer> |
|
|
| <script> |
| |
| const lobbySection = document.getElementById('lobby'); |
| const gameSection = document.getElementById('game-section'); |
| const gameBoard = document.getElementById('game-board'); |
| const waitingRoom = document.getElementById('waiting-room'); |
| const gameIdDisplay = document.getElementById('game-id-display'); |
| const gameIdInput = document.getElementById('game-id'); |
| const gameIdLabel = document.getElementById('game-id-label'); |
| const gameOver = document.getElementById('game-over'); |
| const resultText = document.getElementById('result-text'); |
| const statusText = document.getElementById('status-text'); |
| const playerSymbol = document.getElementById('player-symbol'); |
| const playerOneName = document.getElementById('player-one-name'); |
| const playerTwoName = document.getElementById('player-two-name'); |
| const playerCount = document.getElementById('player-count'); |
| |
| |
| let gameId = null; |
| let player = null; |
| let isCurrentTurn = false; |
| let gameBoardState = Array(9).fill(null); |
| |
| |
| const socket = io("https://tic-tac-toe-socketio.adaptable.app/", { |
| transports: ['websocket'] |
| }); |
| |
| |
| document.getElementById('create-game').addEventListener('click', createGame); |
| document.getElementById('join-game').addEventListener('click', joinGame); |
| document.getElementById('copy-id').addEventListener('click', copyGameId); |
| document.getElementById('play-again').addEventListener('click', restartGame); |
| document.getElementById('new-game').addEventListener('click', resetGame); |
| document.getElementById('leave-game').addEventListener('click', leaveGame); |
| document.getElementById('copy-link').addEventListener('click', copyInviteLink); |
| |
| |
| socket.on('connect', () => { |
| console.log('Connected to server with socket id:', socket.id); |
| }); |
| |
| socket.on('gameCreated', (id) => { |
| gameId = id; |
| document.getElementById('game-code').textContent = id; |
| gameIdDisplay.classList.remove('hidden'); |
| waitingRoom.classList.remove('hidden'); |
| }); |
| |
| socket.on('playerJoined', (players) => { |
| playerCount.textContent = players.length; |
| if (players.length === 2) { |
| |
| setTimeout(() => { |
| lobbySection.classList.add('hidden'); |
| gameSection.classList.remove('hidden'); |
| waitingRoom.classList.add('hidden'); |
| }, 1000); |
| } |
| }); |
| |
| socket.on('playerLeft', (players) => { |
| playerCount.textContent = players.length; |
| }); |
| |
| socket.on('gameJoined', (gameData) => { |
| gameId = gameData.id; |
| player = gameData.players.find(p => p.id === socket.id); |
| |
| |
| playerOneName.textContent = gameData.players[0].name; |
| playerTwoName.textContent = gameData.players[1]?.name || 'Waiting...'; |
| playerCount.textContent = gameData.players.length; |
| |
| |
| if (player.id === gameData.players[0].id) { |
| playerSymbol.textContent = 'X'; |
| playerSymbol.classList.add('text-primary'); |
| } else if (player.id === gameData.players[1]?.id) { |
| playerSymbol.textContent = 'O'; |
| playerSymbol.classList.add('text-secondary'); |
| } |
| |
| |
| if (gameData.players.length === 2) { |
| lobbySection.classList.add('hidden'); |
| gameSection.classList.remove('hidden'); |
| waitingRoom.classList.add('hidden'); |
| updateBoard(gameData.board); |
| updateGameState(gameData); |
| } else { |
| |
| gameIdDisplay.classList.remove('hidden'); |
| waitingRoom.classList.remove('hidden'); |
| document.getElementById('game-code').textContent = gameId; |
| } |
| |
| gameIdLabel.textContent = `Game ID: ${gameId}`; |
| }); |
| |
| socket.on('updateBoard', (board) => { |
| updateBoard(board); |
| gameBoardState = board; |
| }); |
| |
| socket.on('updateGameState', (gameData) => { |
| updateGameState(gameData); |
| }); |
| |
| socket.on('gameOver', (result) => { |
| showGameResult(result); |
| createConfetti(); |
| }); |
| |
| socket.on('error', (message) => { |
| showNotification(message); |
| }); |
| |
| |
| function createGame() { |
| const playerName = `Player_${Math.floor(Math.random() * 1000)}`; |
| socket.emit('createGame', playerName); |
| } |
| |
| function joinGame() { |
| const id = gameIdInput.value.trim(); |
| if (!id) { |
| showNotification('Please enter a Game ID!'); |
| return; |
| } |
| |
| const playerName = `Player_${Math.floor(Math.random() * 1000)}`; |
| socket.emit('joinGame', { gameId: id, playerName }); |
| } |
| |
| function cellClick(index) { |
| if (isCurrentTurn && !gameBoardState[index]) { |
| const chipAnimation = document.createElement('div'); |
| chipAnimation.className = 'chip-animation'; |
| const cell = document.getElementById(`cell-${index}`); |
| cell.appendChild(chipAnimation); |
| |
| setTimeout(() => { |
| socket.emit('makeMove', { |
| gameId, |
| index, |
| symbol: player.symbol |
| }); |
| }, 300); |
| } |
| } |
| |
| function updateBoard(board) { |
| gameBoardState = [...board]; |
| gameBoard.innerHTML = ''; |
| |
| for (let i = 0; i < 9; i++) { |
| const cell = document.createElement('div'); |
| cell.id = `cell-${i}`; |
| cell.className = 'grid-cell aspect-square bg-white rounded-xl border-2 border-accent shadow-md flex items-center justify-center cursor-pointer hover:shadow-lg transition-all'; |
| cell.addEventListener('click', () => cellClick(i)); |
| |
| if (board[i]) { |
| const symbol = document.createElement('div'); |
| symbol.className = 'text-4xl font-bold'; |
| |
| if (board[i] === 'X') { |
| symbol.textContent = 'X'; |
| symbol.classList.add('text-primary'); |
| } else { |
| symbol.textContent = 'O'; |
| symbol.classList.add('text-secondary'); |
| } |
| |
| cell.appendChild(symbol); |
| } |
| |
| gameBoard.appendChild(cell); |
| } |
| } |
| |
| function updateGameState(gameData) { |
| isCurrentTurn = gameData.currentPlayerId === socket.id; |
| player = gameData.players.find(p => p.id === socket.id); |
| |
| |
| if (gameData.winner) { |
| if (gameData.winner === 'draw') { |
| statusText.textContent = "It's a draw!"; |
| } else if (gameData.winner === player.symbol) { |
| statusText.textContent = "You won!"; |
| } else { |
| statusText.textContent = "You lost!"; |
| } |
| } else { |
| if (isCurrentTurn) { |
| statusText.textContent = `Your turn! (${player.symbol})`; |
| document.getElementById('game-status').classList.remove('bg-gradient-to-r', 'from-purple-600', 'to-indigo-700'); |
| document.getElementById('game-status').classList.add('bg-gradient-to-r', 'from-blue-600', 'to-indigo-700'); |
| } else { |
| statusText.textContent = `Waiting for opponent...`; |
| document.getElementById('game-status').classList.remove('bg-gradient-to-r', 'from-blue-600', 'to-indigo-700'); |
| document.getElementById('game-status').classList.add('bg-gradient-to-r', 'from-purple-600', 'to-indigo-700'); |
| } |
| } |
| |
| |
| if (gameData.winningCombo) { |
| gameData.winningCombo.forEach(index => { |
| const cell = document.getElementById(`cell-${index}`); |
| cell.classList.add('winning-cell'); |
| }); |
| } |
| } |
| |
| function showGameResult(result) { |
| gameOver.classList.remove('hidden'); |
| |
| if (result.winner === 'draw') { |
| resultText.textContent = "Game ended in a draw!"; |
| resultText.className = "text-2xl font-bold text-gray-800"; |
| } else { |
| resultText.textContent = `${result.winner} wins the game!`; |
| resultText.className = `text-2xl font-bold ${result.winner === 'X' ? 'text-primary' : 'text-secondary'}`; |
| } |
| } |
| |
| function restartGame() { |
| socket.emit('restartGame', gameId); |
| gameOver.classList.add('hidden'); |
| |
| |
| const cells = document.querySelectorAll('.grid-cell'); |
| cells.forEach(cell => cell.classList.remove('winning-cell')); |
| } |
| |
| function resetGame() { |
| |
| lobbySection.classList.remove('hidden'); |
| gameSection.classList.add('hidden'); |
| gameOver.classList.add('hidden'); |
| gameIdDisplay.classList.add('hidden'); |
| waitingRoom.classList.add('hidden'); |
| |
| |
| gameId = null; |
| player = null; |
| isCurrentTurn = false; |
| gameBoardState = Array(9).fill(null); |
| |
| |
| document.getElementById('game-id').value = ''; |
| gameBoard.innerHTML = ''; |
| |
| |
| socket.emit('leaveGame', gameId); |
| } |
| |
| function leaveGame() { |
| socket.emit('leaveGame', gameId); |
| resetGame(); |
| } |
| |
| function copyGameId() { |
| navigator.clipboard.writeText(gameId) |
| .then(() => showNotification('Game ID copied to clipboard!')) |
| .catch(err => showNotification('Failed to copy: ' + err)); |
| } |
| |
| function copyInviteLink() { |
| const link = `${window.location.origin}/?game=${gameId}`; |
| navigator.clipboard.writeText(link) |
| .then(() => showNotification('Invite link copied to clipboard!')) |
| .catch(err => showNotification('Failed to copy: ' + err)); |
| } |
| |
| function showNotification(message) { |
| |
| const notification = document.createElement('div'); |
| notification.className = 'fixed bottom-5 right-5 bg-dark text-white px-4 py-3 rounded-lg shadow-lg transform transition-all duration-300'; |
| notification.innerHTML = ` |
| <div class="flex items-center gap-2"> |
| <i class="fas fa-info-circle"></i> |
| <span>${message}</span> |
| </div> |
| `; |
| |
| document.body.appendChild(notification); |
| |
| |
| setTimeout(() => { |
| notification.style.transform = 'translateX(100%)'; |
| setTimeout(() => { |
| document.body.removeChild(notification); |
| }, 300); |
| }, 3000); |
| } |
| |
| function createConfetti() { |
| const colors = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899']; |
| const gameBoardRect = gameBoard.getBoundingClientRect(); |
| |
| for (let i = 0; i < 150; i++) { |
| const confetti = document.createElement('div'); |
| const colorIndex = Math.floor(Math.random() * colors.length); |
| |
| confetti.style.backgroundColor = colors[colorIndex]; |
| confetti.className = 'confetti'; |
| confetti.style.left = Math.random() * gameBoardRect.width + 'px'; |
| confetti.style.top = Math.random() * gameBoardRect.height + 'px'; |
| confetti.style.animationDuration = (Math.random() * 3 + 2) + 's'; |
| confetti.style.opacity = Math.random(); |
| confetti.style.width = Math.random() * 10 + 5 + 'px'; |
| confetti.style.height = confetti.style.width; |
| |
| gameBoard.appendChild(confetti); |
| |
| setTimeout(() => { |
| confetti.remove(); |
| }, 4000); |
| } |
| } |
| |
| |
| function initializeBoard() { |
| for (let i = 0; i < 9; i++) { |
| const cell = document.createElement('div'); |
| cell.id = `cell-${i}`; |
| cell.className = 'grid-cell aspect-square bg-white rounded-xl border-2 border-accent shadow-md flex items-center justify-center cursor-pointer hover:shadow-lg transition-all'; |
| cell.addEventListener('click', () => cellClick(i)); |
| gameBoard.appendChild(cell); |
| } |
| } |
| |
| initializeBoard(); |
| |
| |
| function checkUrlParams() { |
| const params = new URLSearchParams(window.location.search); |
| if (params.has('game')) { |
| const gameId = params.get('game'); |
| gameIdInput.value = gameId; |
| |
| showNotification("Game ID detected! Click Join Game to continue."); |
| } |
| } |
| |
| checkUrlParams(); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=LULDev/tictactoe" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |