| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Neon Tic-Tac-Toe</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap" rel="stylesheet"> |
| <style> |
| |
| :root { |
| --bg-color: #0c0c24; |
| --grid-color: #8a2be2; |
| --x-color: #00ffff; |
| --o-color: #ff00ff; |
| --text-color: #f0f0f0; |
| --glow-blur: 15px; |
| --glow-spread: 5px; |
| } |
| |
| body { |
| background-color: var(--bg-color); |
| color: var(--text-color); |
| font-family: 'Orbitron', sans-serif; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| height: 100vh; |
| margin: 0; |
| overflow: hidden; |
| text-align: center; |
| } |
| |
| .container { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 20px; |
| padding: 10px; |
| } |
| |
| |
| #start-screen, #game-screen { |
| width: 100%; |
| max-width: 450px; |
| } |
| |
| #game-screen { |
| display: none; |
| } |
| |
| |
| .btn { |
| font-family: 'Orbitron', sans-serif; |
| font-size: 1.5rem; |
| padding: 15px 30px; |
| border: 2px solid var(--text-color); |
| border-radius: 10px; |
| background-color: transparent; |
| color: var(--text-color); |
| cursor: pointer; |
| transition: all 0.3s ease; |
| box-shadow: 0 0 5px var(--text-color), 0 0 10px var(--text-color) inset; |
| } |
| |
| .btn:hover { |
| background-color: var(--text-color); |
| color: var(--bg-color); |
| box-shadow: 0 0 10px var(--text-color), 0 0 20px var(--text-color), 0 0 30px var(--text-color); |
| } |
| |
| .btn.pulse { |
| animation: pulse-animation 1.5s infinite; |
| } |
| |
| @keyframes pulse-animation { |
| 0% { box-shadow: 0 0 5px var(--text-color), 0 0 10px var(--text-color) inset; } |
| 50% { box-shadow: 0 0 10px #fff, 0 0 20px #fff inset, 0 0 20px #fff; } |
| 100% { box-shadow: 0 0 5px var(--text-color), 0 0 10px var(--text-color) inset; } |
| } |
| |
| |
| #player-display { |
| display: flex; |
| justify-content: space-around; |
| width: 100%; |
| margin-bottom: 10px; |
| } |
| |
| .player-box { |
| padding: 10px 20px; |
| border-radius: 10px; |
| font-size: 2rem; |
| font-weight: bold; |
| transition: transform 0.3s; |
| } |
| |
| .player-box.player-O { |
| border: 2px solid var(--o-color); |
| box-shadow: 0 0 var(--glow-spread) var(--o-color), 0 0 var(--glow-blur) var(--o-color), 0 0 20px var(--o-color) inset; |
| color: var(--o-color); |
| } |
| |
| .player-box.player-X { |
| border: 2px solid var(--x-color); |
| box-shadow: 0 0 var(--glow-spread) var(--x-color), 0 0 var(--glow-blur) var(--x-color), 0 0 20px var(--x-color) inset; |
| color: var(--x-color); |
| } |
| |
| .player-box.active { |
| transform: scale(1.1); |
| } |
| |
| |
| #status-text { |
| font-size: 1.2rem; |
| min-height: 1.5rem; |
| margin-bottom: 10px; |
| } |
| |
| |
| #board-container { |
| position: relative; |
| } |
| |
| #game-board { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| grid-template-rows: repeat(3, 1fr); |
| width: 330px; |
| height: 330px; |
| margin: 0 auto; |
| border-radius: 10px; |
| box-shadow: 0 0 10px var(--grid-color), 0 0 20px var(--grid-color); |
| padding: 15px; |
| gap: 15px; |
| background-color: var(--grid-color); |
| } |
| |
| .cell { |
| background-color: var(--bg-color); |
| border-radius: 5px; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| font-size: 5rem; |
| font-weight: bold; |
| cursor: pointer; |
| transition: background-color 0.3s; |
| } |
| |
| .cell:hover { |
| background-color: #242444; |
| } |
| |
| .cell.X, .cell.O { |
| animation: pop-in 0.3s ease-out; |
| } |
| |
| .cell.X { |
| color: var(--x-color); |
| text-shadow: 0 0 var(--glow-spread) var(--x-color), 0 0 var(--glow-blur) var(--x-color); |
| } |
| |
| .cell.O { |
| color: var(--o-color); |
| text-shadow: 0 0 var(--glow-spread) var(--o-color), 0 0 var(--glow-blur) var(--o-color); |
| } |
| |
| @keyframes pop-in { |
| 0% { transform: scale(0.5); opacity: 0; } |
| 80% { transform: scale(1.1); opacity: 1; } |
| 100% { transform: scale(1); } |
| } |
| |
| |
| #winning-line { |
| position: absolute; |
| background-color: white; |
| height: 8px; |
| border-radius: 4px; |
| display: none; |
| transform-origin: left center; |
| z-index: 10; |
| } |
| |
| #winning-line.win-x { |
| background-color: var(--x-color); |
| box-shadow: 0 0 5px var(--x-color), 0 0 10px var(--x-color), 0 0 20px var(--x-color); |
| } |
| |
| #winning-line.win-o { |
| background-color: var(--o-color); |
| box-shadow: 0 0 5px var(--o-color), 0 0 10px var(--o-color), 0 0 20px var(--o-color); |
| } |
| |
| |
| .win-row-0 { top: 55px; left: 25px; width: 310px; } |
| .win-row-1 { top: 165px; left: 25px; width: 310px; } |
| .win-row-2 { top: 275px; left: 25px; width: 310px; } |
| .win-col-0 { top: 25px; left: 55px; width: 310px; transform: rotate(90deg); } |
| .win-col-1 { top: 25px; left: 165px; width: 310px; transform: rotate(90deg); } |
| .win-col-2 { top: 25px; left: 275px; width: 310px; transform: rotate(90deg); } |
| .win-diag-0 { top: 35px; left: 35px; width: 390px; transform: rotate(45deg); } |
| .win-diag-1 { top: 295px; left: 35px; width: 390px; transform: rotate(-45deg); } |
| |
| |
| |
| #post-game-controls { |
| display: none; |
| gap: 15px; |
| margin-top: 20px; |
| } |
| |
| |
| #chat-container { |
| width: 100%; |
| max-width: 450px; |
| margin-top: 20px; |
| border: 2px solid var(--grid-color); |
| border-radius: 10px; |
| padding: 10px; |
| box-shadow: 0 0 5px var(--grid-color), 0 0 10px var(--grid-color) inset; |
| } |
| |
| #chat-messages { |
| height: 120px; |
| overflow-y: auto; |
| margin-bottom: 10px; |
| display: flex; |
| flex-direction: column; |
| gap: 5px; |
| padding-right: 10px; |
| } |
| |
| #chat-messages::-webkit-scrollbar { width: 8px; } |
| #chat-messages::-webkit-scrollbar-track { background: transparent; } |
| #chat-messages::-webkit-scrollbar-thumb { background-color: var(--grid-color); border-radius: 10px; } |
| |
| .chat-message { |
| padding: 5px 10px; |
| border-radius: 8px; |
| max-width: 75%; |
| word-wrap: break-word; |
| } |
| |
| .chat-message.mine { |
| background-color: rgba(255, 0, 255, 0.2); |
| align-self: flex-end; |
| text-align: right; |
| border: 1px solid var(--o-color); |
| } |
| |
| .chat-message.opponent { |
| background-color: rgba(0, 255, 255, 0.2); |
| align-self: flex-start; |
| text-align: left; |
| border: 1px solid var(--x-color); |
| } |
| |
| #chat-input-container { |
| display: flex; |
| gap: 10px; |
| } |
| |
| #chat-input { |
| flex-grow: 1; |
| background: transparent; |
| border: 1px solid var(--text-color); |
| border-radius: 5px; |
| color: var(--text-color); |
| padding: 8px; |
| font-family: 'Orbitron', sans-serif; |
| } |
| |
| #send-chat-btn { |
| font-family: 'Orbitron', sans-serif; |
| font-size: 1rem; |
| padding: 8px 15px; |
| border: 1px solid var(--text-color); |
| border-radius: 5px; |
| background-color: transparent; |
| color: var(--text-color); |
| cursor: pointer; |
| } |
| #send-chat-btn:hover { background-color: var(--text-color); color: var(--bg-color); } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| |
| <div id="start-screen"> |
| <h1>Neon Tic-Tac-Toe</h1> |
| <button id="start-game-btn" class="btn">Start Game</button> |
| </div> |
|
|
| |
| <div id="game-screen"> |
| <div id="player-display"> |
| <div class="player-box player-X">X</div> |
| <div class="player-box player-O">O</div> |
| </div> |
| <p id="status-text">Connecting...</p> |
| |
| <div id="board-container"> |
| <div id="game-board"></div> |
| <div id="winning-line"></div> |
| </div> |
|
|
| <div id="post-game-controls"> |
| <button id="play-again-btn" class="btn">Play Again</button> |
| <button id="end-game-btn" class="btn">End Game</button> |
| </div> |
|
|
| <div id="chat-container"> |
| <div id="chat-messages"></div> |
| <form id="chat-form"> |
| <div id="chat-input-container"> |
| <input type="text" id="chat-input" placeholder="Say something..." autocomplete="off"> |
| <button type="submit" id="send-chat-btn">Send</button> |
| </div> |
| </form> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script> |
| <script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-database.js"></script> |
|
|
| <script> |
| |
| const firebaseConfig = { |
| apiKey: "AIzaSyABQ-uGB5POWEcwkq_TXqfCW0Bih8uGXmA", |
| authDomain: "tic-tac-257ab.firebaseapp.com", |
| databaseURL: "https://tic-tac-257ab-default-rtdb.firebaseio.com", |
| projectId: "tic-tac-257ab", |
| storageBucket: "tic-tac-257ab.firebasestorage.app", |
| messagingSenderId: "546807013314", |
| appId: "1:546807013314:web:dc768cb7de998874be805b" |
| }; |
| |
| |
| firebase.initializeApp(firebaseConfig); |
| const database = firebase.database(); |
| |
| |
| let playerId = null; |
| let playerSymbol = null; |
| let gameId = null; |
| let gameRef = null; |
| let chatRef = null; |
| |
| |
| const startScreen = document.getElementById('start-screen'); |
| const gameScreen = document.getElementById('game-screen'); |
| const startGameBtn = document.getElementById('start-game-btn'); |
| const statusText = document.getElementById('status-text'); |
| const gameBoard = document.getElementById('game-board'); |
| const playerDisplayX = document.querySelector('.player-box.player-X'); |
| const playerDisplayO = document.querySelector('.player-box.player-O'); |
| const postGameControls = document.getElementById('post-game-controls'); |
| const playAgainBtn = document.getElementById('play-again-btn'); |
| const endGameBtn = document.getElementById('end-game-btn'); |
| const winningLine = document.getElementById('winning-line'); |
| const chatForm = document.getElementById('chat-form'); |
| const chatInput = document.getElementById('chat-input'); |
| const chatMessages = document.getElementById('chat-messages'); |
| |
| |
| function generateId() { |
| return Math.random().toString(36).substr(2, 9); |
| } |
| |
| function showScreen(screenName) { |
| startScreen.style.display = 'none'; |
| gameScreen.style.display = 'none'; |
| if (screenName === 'start') { |
| startScreen.style.display = 'block'; |
| } else if (screenName === 'game') { |
| gameScreen.style.display = 'block'; |
| } |
| } |
| |
| |
| startGameBtn.addEventListener('click', () => { |
| playerId = generateId(); |
| startGameBtn.disabled = true; |
| startGameBtn.textContent = 'Searching...'; |
| showScreen('game'); |
| statusText.textContent = 'Searching for an opponent...'; |
| |
| const waitingPlayersRef = database.ref('waitingPlayers'); |
| |
| waitingPlayersRef.transaction(currentLobby => { |
| if (currentLobby === null) { |
| return { [playerId]: true }; |
| } else { |
| return null; |
| } |
| }, (error, committed, snapshot) => { |
| if (error) { |
| console.error("Transaction failed: ", error); |
| resetLocalGame(); |
| } else if (!committed) { |
| console.log("Transaction was not committed. Retrying matchmaking..."); |
| startGameBtn.disabled = false; |
| startGameBtn.click(); |
| } else { |
| const finalLobbyState = snapshot.val(); |
| if (finalLobbyState && finalLobbyState[playerId]) { |
| findGameCreatedForMe(); |
| } else { |
| const opponentId = Object.keys(snapshot.previous.val())[0]; |
| createGame(opponentId); |
| } |
| } |
| }); |
| }); |
| |
| function findGameCreatedForMe() { |
| statusText.textContent = "Waiting for an opponent..."; |
| const myWaitingRef = database.ref(`waitingPlayers/${playerId}`); |
| myWaitingRef.onDisconnect().remove(); |
| |
| database.ref('games').on('child_added', (gameSnapshot) => { |
| const gameData = gameSnapshot.val(); |
| if (gameData && gameData.players && gameData.players.X === playerId) { |
| myWaitingRef.onDisconnect().cancel(); |
| myWaitingRef.remove(); |
| database.ref('games').off('child_added'); |
| joinGame(gameSnapshot.key); |
| } |
| }); |
| } |
| |
| function createGame(opponentId) { |
| statusText.textContent = "Opponent found! Creating game..."; |
| gameId = `game_${opponentId}_${playerId}`; |
| |
| const newGame = { |
| players: { X: opponentId, O: playerId }, |
| board: Array(9).fill(null), |
| turn: 'X', |
| status: 'playing', |
| rematch: {} |
| }; |
| |
| database.ref(`games/${gameId}`).set(newGame).then(() => { |
| joinGame(gameId); |
| }); |
| } |
| |
| |
| function joinGame(id) { |
| console.log(`Player ${playerId} is joining game ${id}`); |
| gameId = id; |
| gameRef = database.ref(`games/${gameId}`); |
| |
| |
| createBoard(); |
| |
| |
| gameRef.on('value', handleGameUpdate); |
| } |
| |
| |
| function handleGameUpdate(snapshot) { |
| const gameData = snapshot.val(); |
| |
| |
| if (!gameData) { |
| console.log("Game data is null. Game has ended or been deleted."); |
| resetLocalGame(); |
| return; |
| } |
| console.log("Received game update:", gameData); |
| |
| |
| if (gameData.players.X === playerId) { |
| playerSymbol = 'X'; |
| } else if (gameData.players.O === playerId) { |
| playerSymbol = 'O'; |
| } else { |
| console.error("I am not a player in this game. My ID:", playerId, "Players:", gameData.players); |
| return; |
| } |
| |
| |
| gameRef.onDisconnect().update({ status: `${playerSymbol}_DISCONNECTED` }); |
| |
| |
| if (!chatRef) { |
| setupChat(); |
| } |
| |
| |
| updateBoard(gameData.board); |
| updateTurnIndicator(gameData.turn, gameData.status); |
| updateStatus(gameData); |
| handleRematch(gameData.rematch); |
| } |
| |
| |
| function createBoard() { |
| gameBoard.innerHTML = ''; |
| for (let i = 0; i < 9; i++) { |
| const cell = document.createElement('div'); |
| cell.classList.add('cell'); |
| cell.dataset.index = i; |
| cell.addEventListener('click', () => makeMove(i)); |
| gameBoard.appendChild(cell); |
| } |
| } |
| |
| function makeMove(index) { |
| |
| gameRef.once('value', snapshot => { |
| const gameData = snapshot.val(); |
| if (gameData.status === 'playing' && gameData.turn === playerSymbol && !gameData.board[index]) { |
| const newBoard = [...gameData.board]; |
| newBoard[index] = playerSymbol; |
| |
| const { status, winInfo } = checkForWinner(newBoard); |
| |
| const updatePayload = { |
| board: newBoard, |
| turn: playerSymbol === 'X' ? 'O' : 'X', |
| status: status, |
| winInfo: winInfo || null |
| }; |
| |
| gameRef.update(updatePayload).catch(error => { |
| console.error("DATABASE UPDATE FAILED:", error); |
| statusText.textContent = "Error: Could not make move."; |
| }); |
| } |
| }); |
| } |
| |
| function updateBoard(board) { |
| const cells = document.querySelectorAll('.cell'); |
| cells.forEach((cell, index) => { |
| const symbol = board[index]; |
| if (symbol && cell.textContent !== symbol) { |
| cell.textContent = symbol; |
| cell.classList.add(symbol); |
| } else if (!symbol) { |
| cell.textContent = ''; |
| cell.classList.remove('X', 'O'); |
| } |
| }); |
| } |
| |
| function updateTurnIndicator(turn, status) { |
| if (status !== 'playing') { |
| playerDisplayX.classList.remove('active'); |
| playerDisplayO.classList.remove('active'); |
| return; |
| } |
| if (turn === 'X') { |
| playerDisplayX.classList.add('active'); |
| playerDisplayO.classList.remove('active'); |
| } else { |
| playerDisplayO.classList.add('active'); |
| playerDisplayX.classList.remove('active'); |
| } |
| } |
| |
| function updateStatus(gameData) { |
| winningLine.style.display = 'none'; |
| winningLine.className = 'winning-line'; |
| postGameControls.style.display = 'none'; |
| |
| let message = ''; |
| let gameEnded = false; |
| |
| switch (gameData.status) { |
| case 'playing': |
| message = gameData.turn === playerSymbol ? 'Your Turn' : "Opponent's Turn"; |
| break; |
| case 'X_WINS': |
| message = gameData.players.X === playerId ? 'You Win!' : 'Opponent Wins!'; |
| showWinningLine(gameData.winInfo); |
| gameEnded = true; |
| break; |
| case 'O_WINS': |
| message = gameData.players.O === playerId ? 'You Win!' : 'Opponent Wins!'; |
| showWinningLine(gameData.winInfo); |
| gameEnded = true; |
| break; |
| case 'DRAW': |
| message = "It's a Draw!"; |
| gameEnded = true; |
| break; |
| case 'X_DISCONNECTED': |
| case 'O_DISCONNECTED': |
| message = 'Opponent Disconnected. You Win!'; |
| gameEnded = true; |
| break; |
| } |
| statusText.textContent = message; |
| |
| if (gameEnded) { |
| postGameControls.style.display = 'flex'; |
| } |
| } |
| |
| function checkForWinner(board) { |
| const winningCombos = [ |
| [0, 1, 2], [3, 4, 5], [6, 7, 8], |
| [0, 3, 6], [1, 4, 7], [2, 5, 8], |
| [0, 4, 8], [2, 4, 6] |
| ]; |
| const winningComboMap = { |
| 0: 'win-row-0', 1: 'win-row-1', 2: 'win-row-2', |
| 3: 'win-col-0', 4: 'win-col-1', 5: 'win-col-2', |
| 6: 'win-diag-0', 7: 'win-diag-1' |
| }; |
| |
| for (let i = 0; i < winningCombos.length; i++) { |
| const [a, b, c] = winningCombos[i]; |
| if (board[a] && board[a] === board[b] && board[a] === board[c]) { |
| return { status: `${board[a]}_WINS`, winInfo: { combo: winningComboMap[i], winner: board[a] } }; |
| } |
| } |
| |
| return board.every(cell => cell !== null) ? { status: 'DRAW', winInfo: null } : { status: 'playing', winInfo: null }; |
| } |
| |
| function showWinningLine(winInfo) { |
| if (!winInfo) return; |
| winningLine.classList.add(winInfo.combo); |
| winningLine.classList.add(winInfo.winner === 'X' ? 'win-x' : 'win-o'); |
| winningLine.style.display = 'block'; |
| } |
| |
| |
| playAgainBtn.addEventListener('click', () => { |
| playAgainBtn.textContent = 'Waiting...'; |
| playAgainBtn.disabled = true; |
| database.ref(`games/${gameId}/rematch/${playerId}`).set(true); |
| }); |
| |
| function handleRematch(rematchData) { |
| rematchData = rematchData || {}; |
| const opponentRequested = Object.keys(rematchData).some(id => id !== playerId); |
| |
| playAgainBtn.classList.remove('pulse'); |
| |
| if (rematchData[playerId]) { |
| playAgainBtn.textContent = 'Waiting...'; |
| playAgainBtn.disabled = true; |
| } else { |
| playAgainBtn.textContent = 'Play Again'; |
| playAgainBtn.disabled = false; |
| } |
| |
| if (opponentRequested && !rematchData[playerId]) { |
| playAgainBtn.classList.add('pulse'); |
| } |
| |
| if (Object.keys(rematchData).length === 2) { |
| if (playerSymbol === 'X') { |
| resetGameForRematch(); |
| } |
| } |
| } |
| |
| function resetGameForRematch() { |
| gameRef.update({ |
| board: Array(9).fill(null), |
| turn: 'X', |
| status: 'playing', |
| winInfo: null, |
| rematch: {} |
| }); |
| } |
| |
| endGameBtn.addEventListener('click', () => { |
| if (gameRef) { |
| gameRef.onDisconnect().cancel(); |
| gameRef.remove(); |
| } |
| }); |
| |
| function resetLocalGame() { |
| if (gameRef) { |
| gameRef.off('value', handleGameUpdate); |
| gameRef = null; |
| } |
| if(chatRef) { |
| chatRef.off(); |
| chatRef = null; |
| } |
| database.ref('games').off(); |
| |
| gameId = null; |
| playerSymbol = null; |
| |
| startGameBtn.disabled = false; |
| startGameBtn.textContent = 'Start Game'; |
| playAgainBtn.textContent = 'Play Again'; |
| playAgainBtn.disabled = false; |
| playAgainBtn.classList.remove('pulse'); |
| chatMessages.innerHTML = ''; |
| chatInput.value = ''; |
| |
| showScreen('start'); |
| } |
| |
| |
| function setupChat() { |
| chatMessages.innerHTML = ''; |
| chatRef = database.ref(`games/${gameId}/chat`); |
| chatRef.on('child_added', snapshot => { |
| const message = snapshot.val(); |
| displayChatMessage(message); |
| }); |
| } |
| |
| chatForm.addEventListener('submit', (e) => { |
| e.preventDefault(); |
| const text = chatInput.value.trim(); |
| if (text && chatRef) { |
| const message = { |
| senderId: playerId, |
| text: text, |
| timestamp: firebase.database.ServerValue.TIMESTAMP |
| }; |
| chatRef.push(message); |
| chatInput.value = ''; |
| } |
| }); |
| |
| function displayChatMessage(message) { |
| const messageEl = document.createElement('div'); |
| messageEl.classList.add('chat-message'); |
| messageEl.classList.add(message.senderId === playerId ? 'mine' : 'opponent'); |
| messageEl.textContent = message.text; |
| chatMessages.appendChild(messageEl); |
| chatMessages.scrollTop = chatMessages.scrollHeight; |
| } |
| |
| |
| showScreen('start'); |
| </script> |
| </body> |
| </html> |