tictactoe/script.js
Parley Hatch 4b7d42dc13 Initial commit: original tic-tac-toe game
Existing 2-player hotseat game with name entry, emoji avatars,
scoreboard, sound effects, and celebration animations.
2026-05-31 17:43:45 -06:00

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();