Existing 2-player hotseat game with name entry, emoji avatars, scoreboard, sound effects, and celebration animations.
386 lines
12 KiB
JavaScript
386 lines
12 KiB
JavaScript
// 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();
|