tictactoe / index.html
offerpk3's picture
Update index.html
93e5800 verified
<!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>
/* --- Core Styles & Theme --- */
: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;
}
/* --- Screens --- */
#start-screen, #game-screen {
width: 100%;
max-width: 450px;
}
#game-screen {
display: none; /* Hidden by default */
}
/* --- Buttons & Inputs --- */
.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 --- */
#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);
}
/* --- Game Status --- */
#status-text {
font-size: 1.2rem;
min-height: 1.5rem;
margin-bottom: 10px;
}
/* --- Game Board --- */
#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; /* Creates the outer border */
gap: 15px; /* Creates the grid lines */
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 --- */
#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);
}
/* Winning line positions */
.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 --- */
#post-game-controls {
display: none;
gap: 15px;
margin-top: 20px;
}
/* --- Chat --- */
#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">
<!-- Start Screen -->
<div id="start-screen">
<h1>Neon Tic-Tac-Toe</h1>
<button id="start-game-btn" class="btn">Start Game</button>
</div>
<!-- Game Screen -->
<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>
<!-- Firebase SDK -->
<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>
// <<< PASTE YOUR FIREBASE CONFIGURATION OBJECT HERE >>>
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 Initialization ---
firebase.initializeApp(firebaseConfig);
const database = firebase.database();
// --- Global State ---
let playerId = null;
let playerSymbol = null;
let gameId = null;
let gameRef = null;
let chatRef = null;
// --- DOM Elements ---
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');
// --- Utility Functions ---
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';
}
}
// --- Game Setup and Matchmaking (Final Version with Transaction) ---
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);
});
}
// --- THE NEW, SIMPLIFIED GAME INITIALIZATION ---
function joinGame(id) {
console.log(`Player ${playerId} is joining game ${id}`);
gameId = id;
gameRef = database.ref(`games/${gameId}`);
// 1. Create the visual board ONCE.
createBoard();
// 2. Attach the ONE main listener that handles ALL game logic.
gameRef.on('value', handleGameUpdate);
}
// --- THE SINGLE SOURCE OF TRUTH FOR GAME STATE ---
function handleGameUpdate(snapshot) {
const gameData = snapshot.val();
// If game data is gone, the game ended.
if (!gameData) {
console.log("Game data is null. Game has ended or been deleted.");
resetLocalGame();
return;
}
console.log("Received game update:", gameData);
// Determine player symbol from fresh data EVERY time.
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; // Not a player in this game.
}
// Set the disconnect hook now that we know our symbol.
gameRef.onDisconnect().update({ status: `${playerSymbol}_DISCONNECTED` });
// Initialize chat if it hasn't been already.
if (!chatRef) {
setupChat();
}
// Update all UI components based on the fresh data.
updateBoard(gameData.board);
updateTurnIndicator(gameData.turn, gameData.status);
updateStatus(gameData);
handleRematch(gameData.rematch);
}
// --- UI and Game Logic Functions ---
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) {
// The check for playerSymbol will now always be correct.
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';
}
// --- Post-Game and Rematch ---
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); // Specifically remove the listener we added
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');
}
// --- Chat System ---
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;
}
// --- Initial Load ---
showScreen('start');
</script>
</body>
</html>