Initial commit: original tic-tac-toe game
Existing 2-player hotseat game with name entry, emoji avatars, scoreboard, sound effects, and celebration animations.
This commit is contained in:
commit
4b7d42dc13
5 changed files with 1388 additions and 0 deletions
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# OS / editor cruft
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.swp
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Test / tooling artifacts
|
||||||
|
/screenshots/
|
||||||
|
*.log
|
||||||
7
README.md
Normal file
7
README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Kara's ultimate tic tac toe
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Design an ultimate browser based tic tac toe game with the following features:
|
||||||
|
|
||||||
|
|
||||||
153
index.html
Normal file
153
index.html
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>✨ Ultimate Tic-Tac-Toe ✨</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="stars"></div>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Player Name Input Screen -->
|
||||||
|
<div id="nameScreen" class="screen active">
|
||||||
|
<div class="name-card">
|
||||||
|
<h1 class="title">✨ Ultimate Tic-Tac-Toe ✨</h1>
|
||||||
|
<div class="sparkle-divider"></div>
|
||||||
|
|
||||||
|
<div class="name-input-group">
|
||||||
|
<label for="player1Name">
|
||||||
|
<span class="player-icon">🌟</span>
|
||||||
|
Player 1 Name
|
||||||
|
</label>
|
||||||
|
<input type="text" id="player1Name" placeholder="Enter your name..." maxlength="15">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="name-input-group">
|
||||||
|
<label for="player2Name">
|
||||||
|
<span class="player-icon">💫</span>
|
||||||
|
Player 2 Name
|
||||||
|
</label>
|
||||||
|
<input type="text" id="player2Name" placeholder="Enter your name..." maxlength="15">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="avatar-section">
|
||||||
|
<h3 class="avatar-title">Choose Your Avatars!</h3>
|
||||||
|
<div class="avatar-selection">
|
||||||
|
<div class="player-avatar-group">
|
||||||
|
<label>Player 1</label>
|
||||||
|
<div class="avatar-options" id="player1Avatars">
|
||||||
|
<div class="avatar-option selected" data-avatar="⭐">⭐</div>
|
||||||
|
<div class="avatar-option" data-avatar="🦄">🦄</div>
|
||||||
|
<div class="avatar-option" data-avatar="🌸">🌸</div>
|
||||||
|
<div class="avatar-option" data-avatar="🎀">🎀</div>
|
||||||
|
<div class="avatar-option" data-avatar="🦋">🦋</div>
|
||||||
|
<div class="avatar-option" data-avatar="🌈">🌈</div>
|
||||||
|
<div class="avatar-option" data-avatar="🎨">🎨</div>
|
||||||
|
<div class="avatar-option" data-avatar="🎪">🎪</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="player-avatar-group">
|
||||||
|
<label>Player 2</label>
|
||||||
|
<div class="avatar-options" id="player2Avatars">
|
||||||
|
<div class="avatar-option" data-avatar="⭐">⭐</div>
|
||||||
|
<div class="avatar-option" data-avatar="🦄">🦄</div>
|
||||||
|
<div class="avatar-option selected" data-avatar="🌸">🌸</div>
|
||||||
|
<div class="avatar-option" data-avatar="🎀">🎀</div>
|
||||||
|
<div class="avatar-option" data-avatar="🦋">🦋</div>
|
||||||
|
<div class="avatar-option" data-avatar="🌈">🌈</div>
|
||||||
|
<div class="avatar-option" data-avatar="🎨">🎨</div>
|
||||||
|
<div class="avatar-option" data-avatar="🎪">🎪</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="startBtn" class="start-btn">
|
||||||
|
Start Playing! 🎮
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Game Screen -->
|
||||||
|
<div id="gameScreen" class="screen">
|
||||||
|
<div class="game-card">
|
||||||
|
<h1 class="game-title">✨ Tic-Tac-Toe ✨</h1>
|
||||||
|
|
||||||
|
<div class="game-layout">
|
||||||
|
<!-- Left Side: Game Area -->
|
||||||
|
<div class="game-area">
|
||||||
|
<div class="player-info">
|
||||||
|
<div class="player-display player1-display active">
|
||||||
|
<span class="player-symbol" id="player1Symbol">X</span>
|
||||||
|
<span class="player-name" id="player1Display"></span>
|
||||||
|
<div class="player-glow"></div>
|
||||||
|
</div>
|
||||||
|
<div class="vs">VS</div>
|
||||||
|
<div class="player-display player2-display">
|
||||||
|
<span class="player-symbol" id="player2Symbol">O</span>
|
||||||
|
<span class="player-name" id="player2Display"></span>
|
||||||
|
<div class="player-glow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="game-board" id="gameBoard">
|
||||||
|
<div class="cell" data-cell></div>
|
||||||
|
<div class="cell" data-cell></div>
|
||||||
|
<div class="cell" data-cell></div>
|
||||||
|
<div class="cell" data-cell></div>
|
||||||
|
<div class="cell" data-cell></div>
|
||||||
|
<div class="cell" data-cell></div>
|
||||||
|
<div class="cell" data-cell></div>
|
||||||
|
<div class="cell" data-cell></div>
|
||||||
|
<div class="cell" data-cell></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="game-status" id="gameStatus"></div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button id="resetBtn" class="game-btn">🔄 New Game</button>
|
||||||
|
<button id="changeNamesBtn" class="game-btn">👥 Change Names</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Side: Score Board -->
|
||||||
|
<div class="score-sidebar">
|
||||||
|
<h2 class="score-title">Scoreboard</h2>
|
||||||
|
<div class="score-item player1-score">
|
||||||
|
<span class="score-avatar" id="score1Avatar">⭐</span>
|
||||||
|
<span class="score-name" id="score1Name">Player 1</span>
|
||||||
|
<span class="score-value" id="score1">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="score-divider">🏆</div>
|
||||||
|
<div class="score-item player2-score">
|
||||||
|
<span class="score-avatar" id="score2Avatar">🌸</span>
|
||||||
|
<span class="score-name" id="score2Name">Player 2</span>
|
||||||
|
<span class="score-value" id="score2">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Winner Screen -->
|
||||||
|
<div id="winnerScreen" class="screen">
|
||||||
|
<div class="winner-card">
|
||||||
|
<div class="confetti"></div>
|
||||||
|
<h1 class="winner-title">🎉 Winner! 🎉</h1>
|
||||||
|
<div class="trophy" id="winnerAvatar">🏆</div>
|
||||||
|
<p class="winner-name" id="winnerName"></p>
|
||||||
|
<div class="fireworks">
|
||||||
|
<div class="firework"></div>
|
||||||
|
<div class="firework"></div>
|
||||||
|
<div class="firework"></div>
|
||||||
|
</div>
|
||||||
|
<button id="playAgainBtn" class="start-btn">Play Again! 🎮</button>
|
||||||
|
<button id="changeNamesBtn2" class="game-btn">Change Names</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
386
script.js
Normal file
386
script.js
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
// Game State
|
||||||
|
let currentPlayer = 'X';
|
||||||
|
let gameBoard = ['', '', '', '', '', '', '', '', ''];
|
||||||
|
let gameActive = true;
|
||||||
|
let player1Name = '';
|
||||||
|
let player2Name = '';
|
||||||
|
let player1Avatar = '⭐';
|
||||||
|
let player2Avatar = '🌸';
|
||||||
|
let player1Score = 0;
|
||||||
|
let player2Score = 0;
|
||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
const nameScreen = document.getElementById('nameScreen');
|
||||||
|
const gameScreen = document.getElementById('gameScreen');
|
||||||
|
const winnerScreen = document.getElementById('winnerScreen');
|
||||||
|
|
||||||
|
const player1Input = document.getElementById('player1Name');
|
||||||
|
const player2Input = document.getElementById('player2Name');
|
||||||
|
const startBtn = document.getElementById('startBtn');
|
||||||
|
|
||||||
|
const player1Display = document.getElementById('player1Display');
|
||||||
|
const player2Display = document.getElementById('player2Display');
|
||||||
|
const gameStatus = document.getElementById('gameStatus');
|
||||||
|
const gameBoardElement = document.getElementById('gameBoard');
|
||||||
|
const cells = document.querySelectorAll('[data-cell]');
|
||||||
|
|
||||||
|
const resetBtn = document.getElementById('resetBtn');
|
||||||
|
const changeNamesBtn = document.getElementById('changeNamesBtn');
|
||||||
|
const changeNamesBtn2 = document.getElementById('changeNamesBtn2');
|
||||||
|
const playAgainBtn = document.getElementById('playAgainBtn');
|
||||||
|
|
||||||
|
const winnerName = document.getElementById('winnerName');
|
||||||
|
const winnerAvatar = document.getElementById('winnerAvatar');
|
||||||
|
|
||||||
|
// Avatar elements
|
||||||
|
const player1AvatarOptions = document.getElementById('player1Avatars');
|
||||||
|
const player2AvatarOptions = document.getElementById('player2Avatars');
|
||||||
|
|
||||||
|
// Score elements
|
||||||
|
const score1Element = document.getElementById('score1');
|
||||||
|
const score2Element = document.getElementById('score2');
|
||||||
|
const score1Name = document.getElementById('score1Name');
|
||||||
|
const score2Name = document.getElementById('score2Name');
|
||||||
|
const score1Avatar = document.getElementById('score1Avatar');
|
||||||
|
const score2Avatar = document.getElementById('score2Avatar');
|
||||||
|
|
||||||
|
// Symbol elements
|
||||||
|
const player1SymbolElement = document.getElementById('player1Symbol');
|
||||||
|
const player2SymbolElement = document.getElementById('player2Symbol');
|
||||||
|
|
||||||
|
// Winning Combinations
|
||||||
|
const winningCombinations = [
|
||||||
|
[0, 1, 2],
|
||||||
|
[3, 4, 5],
|
||||||
|
[6, 7, 8],
|
||||||
|
[0, 3, 6],
|
||||||
|
[1, 4, 7],
|
||||||
|
[2, 5, 8],
|
||||||
|
[0, 4, 8],
|
||||||
|
[2, 4, 6]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
startBtn.addEventListener('click', startGame);
|
||||||
|
resetBtn.addEventListener('click', resetGame);
|
||||||
|
changeNamesBtn.addEventListener('click', showNameScreen);
|
||||||
|
changeNamesBtn2.addEventListener('click', showNameScreen);
|
||||||
|
playAgainBtn.addEventListener('click', resetGame);
|
||||||
|
|
||||||
|
cells.forEach(cell => {
|
||||||
|
cell.addEventListener('click', handleCellClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Avatar selection listeners
|
||||||
|
player1AvatarOptions.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('avatar-option')) {
|
||||||
|
player1AvatarOptions.querySelectorAll('.avatar-option').forEach(opt => {
|
||||||
|
opt.classList.remove('selected');
|
||||||
|
});
|
||||||
|
e.target.classList.add('selected');
|
||||||
|
player1Avatar = e.target.dataset.avatar;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
player2AvatarOptions.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('avatar-option')) {
|
||||||
|
player2AvatarOptions.querySelectorAll('.avatar-option').forEach(opt => {
|
||||||
|
opt.classList.remove('selected');
|
||||||
|
});
|
||||||
|
e.target.classList.add('selected');
|
||||||
|
player2Avatar = e.target.dataset.avatar;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow Enter key to start game
|
||||||
|
player1Input.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
player2Input.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
player2Input.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
startGame();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
function startGame() {
|
||||||
|
// Get player names or use defaults
|
||||||
|
player1Name = player1Input.value.trim() || 'Player 1';
|
||||||
|
player2Name = player2Input.value.trim() || 'Player 2';
|
||||||
|
|
||||||
|
// Randomly choose starting player
|
||||||
|
currentPlayer = Math.random() < 0.5 ? 'X' : 'O';
|
||||||
|
|
||||||
|
// Update displays
|
||||||
|
player1Display.textContent = player1Name;
|
||||||
|
player2Display.textContent = player2Name;
|
||||||
|
|
||||||
|
// Update symbols with avatars
|
||||||
|
player1SymbolElement.textContent = player1Avatar;
|
||||||
|
player2SymbolElement.textContent = player2Avatar;
|
||||||
|
|
||||||
|
// Update score board
|
||||||
|
score1Name.textContent = player1Name;
|
||||||
|
score2Name.textContent = player2Name;
|
||||||
|
score1Avatar.textContent = player1Avatar;
|
||||||
|
score2Avatar.textContent = player2Avatar;
|
||||||
|
score1Element.textContent = player1Score;
|
||||||
|
score2Element.textContent = player2Score;
|
||||||
|
|
||||||
|
// Switch to game screen
|
||||||
|
showGameScreen();
|
||||||
|
updateStatus();
|
||||||
|
updatePlayerHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNameScreen() {
|
||||||
|
nameScreen.classList.add('active');
|
||||||
|
gameScreen.classList.remove('active');
|
||||||
|
winnerScreen.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showGameScreen() {
|
||||||
|
nameScreen.classList.remove('active');
|
||||||
|
gameScreen.classList.add('active');
|
||||||
|
winnerScreen.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWinnerScreen() {
|
||||||
|
nameScreen.classList.remove('active');
|
||||||
|
gameScreen.classList.remove('active');
|
||||||
|
winnerScreen.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCellClick(e) {
|
||||||
|
const cell = e.target;
|
||||||
|
const cellIndex = Array.from(cells).indexOf(cell);
|
||||||
|
|
||||||
|
// Check if cell is already filled or game is not active
|
||||||
|
if (gameBoard[cellIndex] !== '' || !gameActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make move
|
||||||
|
makeMove(cell, cellIndex);
|
||||||
|
|
||||||
|
// Check for win or draw
|
||||||
|
checkResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeMove(cell, index) {
|
||||||
|
gameBoard[index] = currentPlayer;
|
||||||
|
|
||||||
|
// Use avatar instead of X/O
|
||||||
|
const avatar = currentPlayer === 'X' ? player1Avatar : player2Avatar;
|
||||||
|
cell.textContent = avatar;
|
||||||
|
cell.classList.add(currentPlayer.toLowerCase());
|
||||||
|
|
||||||
|
// Create particle burst effect
|
||||||
|
createParticleBurst(cell, avatar);
|
||||||
|
|
||||||
|
// Play move sound
|
||||||
|
playMoveSound(currentPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkResult() {
|
||||||
|
let roundWon = false;
|
||||||
|
let winningCombo = null;
|
||||||
|
|
||||||
|
// Check all winning combinations
|
||||||
|
for (let i = 0; i < winningCombinations.length; i++) {
|
||||||
|
const [a, b, c] = winningCombinations[i];
|
||||||
|
if (gameBoard[a] && gameBoard[a] === gameBoard[b] && gameBoard[a] === gameBoard[c]) {
|
||||||
|
roundWon = true;
|
||||||
|
winningCombo = [a, b, c];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roundWon) {
|
||||||
|
// Highlight winning cells
|
||||||
|
winningCombo.forEach(index => {
|
||||||
|
cells[index].classList.add('winning');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determine winner
|
||||||
|
const winner = currentPlayer === 'X' ? player1Name : player2Name;
|
||||||
|
const winnerAvatarSymbol = currentPlayer === 'X' ? player1Avatar : player2Avatar;
|
||||||
|
gameActive = false;
|
||||||
|
|
||||||
|
// Update score
|
||||||
|
if (currentPlayer === 'X') {
|
||||||
|
player1Score++;
|
||||||
|
score1Element.textContent = player1Score;
|
||||||
|
score1Element.classList.add('score-update');
|
||||||
|
setTimeout(() => score1Element.classList.remove('score-update'), 500);
|
||||||
|
} else {
|
||||||
|
player2Score++;
|
||||||
|
score2Element.textContent = player2Score;
|
||||||
|
score2Element.classList.add('score-update');
|
||||||
|
setTimeout(() => score2Element.classList.remove('score-update'), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show winner screen after animation
|
||||||
|
setTimeout(() => {
|
||||||
|
winnerName.textContent = winner;
|
||||||
|
winnerAvatar.textContent = winnerAvatarSymbol;
|
||||||
|
showWinnerScreen();
|
||||||
|
playWinSound();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for draw
|
||||||
|
if (!gameBoard.includes('')) {
|
||||||
|
gameActive = false;
|
||||||
|
gameStatus.textContent = "It's a Draw! 🤝";
|
||||||
|
setTimeout(() => {
|
||||||
|
resetGame();
|
||||||
|
}, 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch player
|
||||||
|
currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
|
||||||
|
updateStatus();
|
||||||
|
updatePlayerHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus() {
|
||||||
|
const currentPlayerName = currentPlayer === 'X' ? player1Name : player2Name;
|
||||||
|
gameStatus.textContent = `${currentPlayerName}'s Turn (${currentPlayer})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePlayerHighlight() {
|
||||||
|
const player1Div = document.querySelector('.player1-display');
|
||||||
|
const player2Div = document.querySelector('.player2-display');
|
||||||
|
|
||||||
|
if (currentPlayer === 'X') {
|
||||||
|
player1Div.classList.add('active');
|
||||||
|
player2Div.classList.remove('active');
|
||||||
|
} else {
|
||||||
|
player1Div.classList.remove('active');
|
||||||
|
player2Div.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetGame() {
|
||||||
|
// Reset game state
|
||||||
|
currentPlayer = Math.random() < 0.5 ? 'X' : 'O';
|
||||||
|
gameBoard = ['', '', '', '', '', '', '', '', ''];
|
||||||
|
gameActive = true;
|
||||||
|
|
||||||
|
// Clear cells
|
||||||
|
cells.forEach(cell => {
|
||||||
|
cell.textContent = '';
|
||||||
|
cell.classList.remove('x', 'o', 'winning');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update display
|
||||||
|
updateStatus();
|
||||||
|
updatePlayerHighlight();
|
||||||
|
|
||||||
|
// Show game screen
|
||||||
|
showGameScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
function playWinSound() {
|
||||||
|
// Create a simple celebratory sound using Web Audio API
|
||||||
|
try {
|
||||||
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
|
||||||
|
// Play a series of ascending notes
|
||||||
|
const notes = [523.25, 587.33, 659.25, 783.99]; // C, D, E, G
|
||||||
|
|
||||||
|
notes.forEach((frequency, index) => {
|
||||||
|
const oscillator = audioContext.createOscillator();
|
||||||
|
const gainNode = audioContext.createGain();
|
||||||
|
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(audioContext.destination);
|
||||||
|
|
||||||
|
oscillator.frequency.value = frequency;
|
||||||
|
oscillator.type = 'sine';
|
||||||
|
|
||||||
|
const startTime = audioContext.currentTime + (index * 0.15);
|
||||||
|
const stopTime = startTime + 0.3;
|
||||||
|
|
||||||
|
gainNode.gain.setValueAtTime(0.3, startTime);
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, stopTime);
|
||||||
|
|
||||||
|
oscillator.start(startTime);
|
||||||
|
oscillator.stop(stopTime);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Audio not supported');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playMoveSound(player) {
|
||||||
|
// Create different sound for each player
|
||||||
|
try {
|
||||||
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
const oscillator = audioContext.createOscillator();
|
||||||
|
const gainNode = audioContext.createGain();
|
||||||
|
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(audioContext.destination);
|
||||||
|
|
||||||
|
// Different frequencies for different players
|
||||||
|
oscillator.frequency.value = player === 'X' ? 523.25 : 659.25; // C or E
|
||||||
|
oscillator.type = 'sine';
|
||||||
|
|
||||||
|
const startTime = audioContext.currentTime;
|
||||||
|
const stopTime = startTime + 0.1;
|
||||||
|
|
||||||
|
gainNode.gain.setValueAtTime(0.2, startTime);
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.01, stopTime);
|
||||||
|
|
||||||
|
oscillator.start(startTime);
|
||||||
|
oscillator.stop(stopTime);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Audio not supported');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createParticleBurst(cell, emoji) {
|
||||||
|
// Get cell position
|
||||||
|
const rect = cell.getBoundingClientRect();
|
||||||
|
const centerX = rect.left + rect.width / 2;
|
||||||
|
const centerY = rect.top + rect.height / 2;
|
||||||
|
|
||||||
|
// Create particles
|
||||||
|
const particleCount = 8;
|
||||||
|
const particles = ['✨', '💫', '⭐', '🌟', emoji];
|
||||||
|
|
||||||
|
for (let i = 0; i < particleCount; i++) {
|
||||||
|
const particle = document.createElement('div');
|
||||||
|
particle.className = 'particle';
|
||||||
|
particle.textContent = particles[Math.floor(Math.random() * particles.length)];
|
||||||
|
|
||||||
|
// Random direction
|
||||||
|
const angle = (Math.PI * 2 * i) / particleCount;
|
||||||
|
const distance = 80 + Math.random() * 40;
|
||||||
|
const tx = Math.cos(angle) * distance;
|
||||||
|
const ty = Math.sin(angle) * distance;
|
||||||
|
|
||||||
|
particle.style.left = centerX + 'px';
|
||||||
|
particle.style.top = centerY + 'px';
|
||||||
|
particle.style.setProperty('--tx', tx + 'px');
|
||||||
|
particle.style.setProperty('--ty', ty + 'px');
|
||||||
|
|
||||||
|
document.body.appendChild(particle);
|
||||||
|
|
||||||
|
// Remove particle after animation
|
||||||
|
setTimeout(() => {
|
||||||
|
particle.remove();
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize player highlight
|
||||||
|
updatePlayerHighlight();
|
||||||
832
style.css
Normal file
832
style.css
Normal file
|
|
@ -0,0 +1,832 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 25%, #f093fb 50%, #4facfe 75%, #667eea 100%);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
animation: gradientShift 15s ease infinite;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
100% { background-position: 0% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated Stars Background */
|
||||||
|
.stars {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars::before,
|
||||||
|
.stars::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: white;
|
||||||
|
box-shadow:
|
||||||
|
100px 200px white, 300px 100px white, 500px 300px white,
|
||||||
|
700px 150px white, 200px 400px white, 600px 450px white,
|
||||||
|
150px 50px white, 450px 250px white, 350px 500px white,
|
||||||
|
800px 400px white, 50px 300px white, 550px 50px white,
|
||||||
|
250px 150px rgba(255,255,255,0.5), 650px 350px rgba(255,255,255,0.5),
|
||||||
|
400px 100px rgba(255,255,255,0.5), 100px 450px rgba(255,255,255,0.5);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: twinkle 3s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stars::after {
|
||||||
|
animation-delay: 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes twinkle {
|
||||||
|
0%, 100% { opacity: 0.3; transform: scale(1); }
|
||||||
|
50% { opacity: 1; transform: scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Screens */
|
||||||
|
.screen {
|
||||||
|
display: none;
|
||||||
|
animation: fadeIn 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: scale(0.9); }
|
||||||
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Name Input Screen */
|
||||||
|
.name-card {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 30px 30px;
|
||||||
|
box-shadow:
|
||||||
|
0 20px 60px rgba(0, 0, 0, 0.3),
|
||||||
|
0 0 40px rgba(246, 79, 254, 0.5),
|
||||||
|
inset 0 0 60px rgba(255, 255, 255, 0.5);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.8);
|
||||||
|
max-height: 95vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
.name-card::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-card::-webkit-scrollbar-track {
|
||||||
|
background: rgba(240, 147, 251, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-card::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(135deg, #f093fb, #4facfe);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-card::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: linear-gradient(135deg, #f5576c, #f093fb);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0px); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2.2em;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(45deg, #f093fb, #f5576c, #4facfe, #f093fb);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: gradientText 3s ease infinite;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 0 30px rgba(245, 87, 108, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientText {
|
||||||
|
0%, 100% { background-position: 0% 50%; }
|
||||||
|
50% { background-position: 100% 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.sparkle-divider {
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, transparent, #f093fb, #4facfe, #f093fb, transparent);
|
||||||
|
margin: 15px 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
animation: sparkleMove 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkleMove {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-input-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-input-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #764ba2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-icon {
|
||||||
|
font-size: 1.5em;
|
||||||
|
animation: bounce 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(-5px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-input-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px 20px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
border: 3px solid #f093fb;
|
||||||
|
border-radius: 15px;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-input-group input:focus {
|
||||||
|
border-color: #4facfe;
|
||||||
|
box-shadow: 0 0 20px rgba(79, 172, 254, 0.5);
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Avatar Selection */
|
||||||
|
.avatar-section {
|
||||||
|
margin: 25px 0 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.3em;
|
||||||
|
color: #764ba2;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-selection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-avatar-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #764ba2;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-option {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
font-size: 1.8em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: white;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-option:hover {
|
||||||
|
transform: scale(1.15);
|
||||||
|
border-color: #4facfe;
|
||||||
|
box-shadow: 0 0 15px rgba(79, 172, 254, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-option.selected {
|
||||||
|
border-color: #f093fb;
|
||||||
|
background: linear-gradient(135deg, rgba(240, 147, 251, 0.2), rgba(79, 172, 254, 0.2));
|
||||||
|
box-shadow: 0 0 20px rgba(240, 147, 251, 0.6);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-option.selected:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: linear-gradient(135deg, #f093fb, #f5576c);
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 10px 30px rgba(245, 87, 108, 0.4);
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn:hover {
|
||||||
|
transform: translateY(-5px) scale(1.05);
|
||||||
|
box-shadow: 0 15px 40px rgba(245, 87, 108, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-btn:active {
|
||||||
|
transform: translateY(-2px) scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Game Screen */
|
||||||
|
.game-card {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow:
|
||||||
|
0 20px 60px rgba(0, 0, 0, 0.3),
|
||||||
|
0 0 40px rgba(246, 79, 254, 0.5),
|
||||||
|
inset 0 0 60px rgba(255, 255, 255, 0.5);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-title {
|
||||||
|
font-size: 2.5em;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(45deg, #f093fb, #f5576c, #4facfe);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: gradientText 3s ease infinite;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Game Layout - Horizontal Split */
|
||||||
|
.game-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-area {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Score Sidebar */
|
||||||
|
.score-sidebar {
|
||||||
|
width: 220px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(240, 147, 251, 0.15), rgba(79, 172, 254, 0.15));
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 25px 20px;
|
||||||
|
border: 2px solid rgba(240, 147, 251, 0.3);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.4em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #764ba2;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-avatar {
|
||||||
|
font-size: 3em;
|
||||||
|
animation: bounce 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-name {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #764ba2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-value {
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #f5576c;
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
border: 3px solid #f093fb;
|
||||||
|
min-width: 70px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 5px 15px rgba(240, 147, 251, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-divider {
|
||||||
|
font-size: 2.5em;
|
||||||
|
text-align: center;
|
||||||
|
animation: rotate 3s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-item.player1-score .score-value {
|
||||||
|
color: #f5576c;
|
||||||
|
border-color: #f5576c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-item.player2-score .score-value {
|
||||||
|
color: #4facfe;
|
||||||
|
border-color: #4facfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Player Info */
|
||||||
|
.player-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-display {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 25px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 3px solid transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-display.active {
|
||||||
|
border-color: #f093fb;
|
||||||
|
box-shadow: 0 0 30px rgba(240, 147, 251, 0.6);
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-glow {
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: radial-gradient(circle, rgba(240, 147, 251, 0.3), transparent 70%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-display.active .player-glow {
|
||||||
|
opacity: 1;
|
||||||
|
animation: rotate 3s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-symbol {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player1-display .player-symbol {
|
||||||
|
color: #f5576c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player2-display .player-symbol {
|
||||||
|
color: #4facfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-name {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #764ba2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #764ba2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Game Board */
|
||||||
|
.game-board {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 15px;
|
||||||
|
margin: 30px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, rgba(240, 147, 251, 0.2), rgba(79, 172, 254, 0.2));
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: white;
|
||||||
|
border: 4px solid #f093fb;
|
||||||
|
border-radius: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 4em;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(45deg, transparent, rgba(240, 147, 251, 0.3), transparent);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell:hover:not(.x):not(.o)::before {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell:hover:not(.x):not(.o) {
|
||||||
|
transform: scale(1.1);
|
||||||
|
border-color: #4facfe;
|
||||||
|
box-shadow: 0 0 20px rgba(79, 172, 254, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.x,
|
||||||
|
.cell.o {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.x {
|
||||||
|
color: #f5576c;
|
||||||
|
animation: cellAppear 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.o {
|
||||||
|
color: #4facfe;
|
||||||
|
animation: cellAppear 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cellAppear {
|
||||||
|
from {
|
||||||
|
transform: scale(0) rotate(180deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1) rotate(0deg);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell.winning {
|
||||||
|
animation: winningCell 0.6s ease infinite;
|
||||||
|
background: linear-gradient(135deg, #f093fb, #4facfe);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes winningCell {
|
||||||
|
0%, 100% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Game Status */
|
||||||
|
.game-status {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #764ba2;
|
||||||
|
min-height: 40px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: bold;
|
||||||
|
border: 3px solid #f093fb;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: white;
|
||||||
|
color: #764ba2;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-btn:hover {
|
||||||
|
background: linear-gradient(135deg, #f093fb, #f5576c);
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 10px 20px rgba(240, 147, 251, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Winner Screen */
|
||||||
|
.winner-card {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 30px;
|
||||||
|
padding: 60px 40px;
|
||||||
|
box-shadow:
|
||||||
|
0 20px 60px rgba(0, 0, 0, 0.3),
|
||||||
|
0 0 40px rgba(246, 79, 254, 0.5),
|
||||||
|
inset 0 0 60px rgba(255, 255, 255, 0.5);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.8);
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winner-title {
|
||||||
|
font-size: 3em;
|
||||||
|
background: linear-gradient(45deg, #f093fb, #f5576c, #4facfe);
|
||||||
|
background-size: 200% 200%;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: gradientText 3s ease infinite;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trophy {
|
||||||
|
font-size: 8em;
|
||||||
|
animation: trophyBounce 1s ease-in-out infinite;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes trophyBounce {
|
||||||
|
0%, 100% { transform: translateY(0) rotate(-5deg); }
|
||||||
|
50% { transform: translateY(-20px) rotate(5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.winner-name {
|
||||||
|
font-size: 2.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #764ba2;
|
||||||
|
margin: 30px 0;
|
||||||
|
text-shadow: 0 0 20px rgba(240, 147, 251, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Confetti */
|
||||||
|
.confetti {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confetti::before,
|
||||||
|
.confetti::after {
|
||||||
|
content: '🎉 ✨ 🌟 💫 🎊 ⭐';
|
||||||
|
position: absolute;
|
||||||
|
font-size: 2em;
|
||||||
|
animation: confettiFall 3s linear infinite;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confetti::after {
|
||||||
|
animation-delay: 1.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes confettiFall {
|
||||||
|
0% {
|
||||||
|
top: -10%;
|
||||||
|
opacity: 1;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 110%;
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fireworks */
|
||||||
|
.fireworks {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: fireworkExplode 2s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework:nth-child(1) {
|
||||||
|
top: 20%;
|
||||||
|
left: 20%;
|
||||||
|
background: #f093fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework:nth-child(2) {
|
||||||
|
top: 30%;
|
||||||
|
right: 20%;
|
||||||
|
background: #4facfe;
|
||||||
|
animation-delay: 0.7s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firework:nth-child(3) {
|
||||||
|
bottom: 20%;
|
||||||
|
left: 50%;
|
||||||
|
background: #f5576c;
|
||||||
|
animation-delay: 1.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fireworkExplode {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 0 currentColor,
|
||||||
|
0 0 0 0 currentColor,
|
||||||
|
0 0 0 0 currentColor,
|
||||||
|
0 0 0 0 currentColor;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(2);
|
||||||
|
opacity: 0;
|
||||||
|
box-shadow:
|
||||||
|
0 -50px 0 5px currentColor,
|
||||||
|
50px 0 0 5px currentColor,
|
||||||
|
0 50px 0 5px currentColor,
|
||||||
|
-50px 0 0 5px currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Particle Effects */
|
||||||
|
.particle {
|
||||||
|
position: fixed;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 1.5em;
|
||||||
|
z-index: 9999;
|
||||||
|
animation: particleBurst 0.8s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes particleBurst {
|
||||||
|
0% {
|
||||||
|
transform: translate(0, 0) scale(1) rotate(0deg);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(var(--tx), var(--ty)) scale(0) rotate(360deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Score Update Animation */
|
||||||
|
.score-value.score-update {
|
||||||
|
animation: scorePopAnimation 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scorePopAnimation {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.5); box-shadow: 0 0 30px currentColor; }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.game-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-sidebar {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-item {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.title {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-title {
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-info {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue