tictactoe / index.html
LULDev's picture
undefined - Initial Deployment
08dc4e3 verified
<!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">
<!-- Game Header -->
<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>
<!-- Lobby Section -->
<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">
<!-- Create Game -->
<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>
<!-- Join Game -->
<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>
<!-- Game ID Display -->
<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>
<!-- Waiting Room -->
<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>
<!-- Game Section -->
<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>
<!-- Game Board -->
<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">
<!-- Cells will be generated dynamically -->
</div>
<!-- Results Overlay -->
<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>
<!-- Game Controls -->
<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 -->
<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>
// DOM Elements
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');
// Game state
let gameId = null;
let player = null;
let isCurrentTurn = false;
let gameBoardState = Array(9).fill(null);
// Initialize Socket.io connection
const socket = io("https://tic-tac-toe-socketio.adaptable.app/", { // Replace with your Socket.io server URL
transports: ['websocket']
});
// Event Listeners
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.io Event Handlers
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) {
// Transition to game screen after a short delay
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);
// Update player names
playerOneName.textContent = gameData.players[0].name;
playerTwoName.textContent = gameData.players[1]?.name || 'Waiting...';
playerCount.textContent = gameData.players.length;
// Set player symbols
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 two players are present, start game
if (gameData.players.length === 2) {
lobbySection.classList.add('hidden');
gameSection.classList.remove('hidden');
waitingRoom.classList.add('hidden');
updateBoard(gameData.board);
updateGameState(gameData);
} else {
// Show waiting room
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);
});
// Game Functions
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);
// Update status
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');
}
}
// Mark winning cells if any
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');
// Remove winning cell classes
const cells = document.querySelectorAll('.grid-cell');
cells.forEach(cell => cell.classList.remove('winning-cell'));
}
function resetGame() {
// Reset UI
lobbySection.classList.remove('hidden');
gameSection.classList.add('hidden');
gameOver.classList.add('hidden');
gameIdDisplay.classList.add('hidden');
waitingRoom.classList.add('hidden');
// Reset game data
gameId = null;
player = null;
isCurrentTurn = false;
gameBoardState = Array(9).fill(null);
// Clean up
document.getElementById('game-id').value = '';
gameBoard.innerHTML = '';
// Leave current game
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) {
// Create notification element
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);
// Remove after delay
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);
}
}
// Create board when page loads
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();
// Check for game ID in URL parameters
function checkUrlParams() {
const params = new URLSearchParams(window.location.search);
if (params.has('game')) {
const gameId = params.get('game');
gameIdInput.value = gameId;
// Auto-fill game ID field but don't join automatically
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>