598 lines
No EOL
20 KiB
JavaScript
598 lines
No EOL
20 KiB
JavaScript
// Educational Tic Tac Toe Game
|
|
|
|
// Game variables
|
|
let currentPlayer = 'X';
|
|
let gameBoard = ['', '', '', '', '', '', '', '', ''];
|
|
let gameActive = true;
|
|
let scores = { X: 0, O: 0 };
|
|
let gameSettings = {
|
|
questionType: 'multiplechoice',
|
|
language: 'arabic',
|
|
category: 'beginners', // Changed to beginners for new users
|
|
timerSeconds: 5 // 5 seconds timer
|
|
};
|
|
|
|
// Timer variables
|
|
let timerInterval = null;
|
|
let timeLeft = 0;
|
|
|
|
// DOM Elements
|
|
const cells = document.querySelectorAll('.cell');
|
|
const player1Element = document.getElementById('player1');
|
|
const player2Element = document.getElementById('player2');
|
|
const resetButton = document.getElementById('resetBtn');
|
|
const newGameButton = document.getElementById('newGameBtn');
|
|
const questionModal = document.getElementById('questionModal');
|
|
const questionText = document.getElementById('questionText');
|
|
const answerOptions = document.getElementById('answerOptions');
|
|
const feedback = document.getElementById('feedback');
|
|
const gameOverModal = document.getElementById('gameOverModal');
|
|
const gameResult = document.getElementById('gameResult');
|
|
const playAgainButton = document.getElementById('playAgainBtn');
|
|
const applySettingsButton = document.getElementById('applySettingsBtn');
|
|
|
|
// Timer elements
|
|
const timerContainer = document.getElementById('timerContainer');
|
|
const timerBar = document.getElementById('timerBar');
|
|
const timerText = document.getElementById('timerText');
|
|
|
|
// Settings elements
|
|
const questionTypeSelect = document.getElementById('questionType');
|
|
const questionLanguageSelect = document.getElementById('questionLanguage');
|
|
const questionCategorySelect = document.getElementById('questionCategory');
|
|
const questionTimerSelect = document.getElementById('questionTimer');
|
|
|
|
// Current question and selected cell
|
|
let currentQuestion = null;
|
|
let selectedCellIndex = null;
|
|
let questionPlayer = null; // Track which player clicked the cell
|
|
|
|
// Initialize the game
|
|
function initGame() {
|
|
// Add event listeners to cells
|
|
cells.forEach(cell => {
|
|
cell.addEventListener('click', () => handleCellClick(parseInt(cell.dataset.index)));
|
|
});
|
|
|
|
// Add event listeners to buttons
|
|
resetButton.addEventListener('click', resetGame);
|
|
newGameButton.addEventListener('click', newGame);
|
|
playAgainButton.addEventListener('click', newGame);
|
|
applySettingsButton.addEventListener('click', applySettings);
|
|
|
|
// Add event listener to language select to update categories
|
|
questionLanguageSelect.addEventListener('change', updateCategoryOptions);
|
|
|
|
// Initialize settings from selects
|
|
questionTypeSelect.value = gameSettings.questionType;
|
|
questionLanguageSelect.value = gameSettings.language;
|
|
questionCategorySelect.value = gameSettings.category;
|
|
questionTimerSelect.value = gameSettings.timerSeconds;
|
|
|
|
// Update category options based on selected language
|
|
updateCategoryOptions();
|
|
|
|
// Set up the initial game state
|
|
updateGameDisplay();
|
|
|
|
// Update UI language immediately based on default settings
|
|
updateUILanguage();
|
|
|
|
// Set document direction for Arabic
|
|
if (gameSettings.language === 'arabic') {
|
|
document.documentElement.dir = 'rtl';
|
|
}
|
|
}
|
|
|
|
// Update category options based on selected language
|
|
function updateCategoryOptions() {
|
|
const language = questionLanguageSelect.value;
|
|
|
|
// Clear existing options
|
|
questionCategorySelect.innerHTML = '';
|
|
|
|
if (language === 'english') {
|
|
// Add English categories
|
|
const mathOption = document.createElement('option');
|
|
mathOption.value = 'math';
|
|
mathOption.textContent = 'Mathematics';
|
|
questionCategorySelect.appendChild(mathOption);
|
|
|
|
// Set default for English
|
|
questionCategorySelect.value = 'math';
|
|
} else if (language === 'arabic') {
|
|
// Add Arabic categories
|
|
const islamicOption = document.createElement('option');
|
|
islamicOption.value = 'islamic';
|
|
islamicOption.textContent = 'Islamic';
|
|
questionCategorySelect.appendChild(islamicOption);
|
|
|
|
const beginnersOption = document.createElement('option');
|
|
beginnersOption.value = 'beginners';
|
|
beginnersOption.textContent = 'Beginners Arabic';
|
|
questionCategorySelect.appendChild(beginnersOption);
|
|
|
|
// Set default for Arabic
|
|
questionCategorySelect.value = 'beginners';
|
|
}
|
|
}
|
|
|
|
// Handle cell click
|
|
function handleCellClick(index) {
|
|
// Ignore clicks if game is not active or cell is already filled
|
|
if (!gameActive || gameBoard[index] !== '') return;
|
|
|
|
// Store the selected cell index and current player
|
|
selectedCellIndex = index;
|
|
questionPlayer = currentPlayer; // Store which player clicked the cell
|
|
|
|
// Get a random question based on current settings
|
|
currentQuestion = getRandomQuestion(
|
|
gameSettings.language,
|
|
gameSettings.category,
|
|
gameSettings.questionType
|
|
);
|
|
|
|
// Display the question
|
|
showQuestion(currentQuestion);
|
|
}
|
|
|
|
// Show question modal
|
|
function showQuestion(question) {
|
|
// Set the question text with appropriate language attribute
|
|
questionText.textContent = question.question;
|
|
if (gameSettings.language === 'arabic') {
|
|
questionText.setAttribute('lang', 'ar');
|
|
} else {
|
|
questionText.removeAttribute('lang');
|
|
}
|
|
|
|
// Clear previous answer options and feedback
|
|
answerOptions.innerHTML = '';
|
|
feedback.textContent = '';
|
|
feedback.className = 'feedback';
|
|
|
|
// Create answer options based on question type
|
|
if (gameSettings.questionType === 'truefalse') {
|
|
// True/False question
|
|
const trueOption = document.createElement('div');
|
|
trueOption.className = 'answer-option';
|
|
trueOption.textContent = gameSettings.language === 'arabic' ? 'صحيح' : 'True';
|
|
trueOption.addEventListener('click', () => checkAnswer(true));
|
|
|
|
const falseOption = document.createElement('div');
|
|
falseOption.className = 'answer-option';
|
|
falseOption.textContent = gameSettings.language === 'arabic' ? 'خطأ' : 'False';
|
|
falseOption.addEventListener('click', () => checkAnswer(false));
|
|
|
|
answerOptions.appendChild(trueOption);
|
|
answerOptions.appendChild(falseOption);
|
|
} else {
|
|
// Multiple choice question
|
|
|
|
// Create a shuffled array of indices
|
|
const originalAnswer = question.answer;
|
|
const indices = Array.from({ length: question.options.length }, (_, i) => i);
|
|
const shuffledIndices = shuffleArray([...indices]);
|
|
|
|
// Create a mapping from shuffled positions to original positions
|
|
const shuffleMap = {};
|
|
shuffledIndices.forEach((originalIndex, newIndex) => {
|
|
shuffleMap[newIndex] = originalIndex;
|
|
});
|
|
|
|
// Display options in shuffled order
|
|
shuffledIndices.forEach((originalIndex, newIndex) => {
|
|
const optionElement = document.createElement('div');
|
|
optionElement.className = 'answer-option';
|
|
optionElement.textContent = question.options[originalIndex];
|
|
if (gameSettings.language === 'arabic') {
|
|
optionElement.setAttribute('lang', 'ar');
|
|
}
|
|
// When clicked, map back to the original index for checking
|
|
optionElement.addEventListener('click', () => checkAnswer(originalIndex, shuffleMap));
|
|
answerOptions.appendChild(optionElement);
|
|
});
|
|
}
|
|
|
|
// Show the modal
|
|
questionModal.classList.add('show');
|
|
|
|
// Setup timer if enabled
|
|
if (gameSettings.timerSeconds > 0) {
|
|
// Show timer container
|
|
timerContainer.style.display = 'block';
|
|
|
|
// Reset timer
|
|
timeLeft = gameSettings.timerSeconds;
|
|
updateTimerDisplay();
|
|
|
|
// Clear any existing timer
|
|
if (timerInterval) {
|
|
clearInterval(timerInterval);
|
|
}
|
|
|
|
// Start the timer
|
|
timerInterval = setInterval(() => {
|
|
timeLeft--;
|
|
updateTimerDisplay();
|
|
|
|
if (timeLeft <= 0) {
|
|
// Time's up
|
|
clearInterval(timerInterval);
|
|
timeOut();
|
|
}
|
|
}, 1000);
|
|
} else {
|
|
// Hide timer if disabled
|
|
timerContainer.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Update the timer display
|
|
function updateTimerDisplay() {
|
|
// Update text
|
|
timerText.textContent = `${timeLeft}s`;
|
|
|
|
// Update progress bar
|
|
const percentage = (timeLeft / gameSettings.timerSeconds) * 100;
|
|
timerBar.style.width = `${percentage}%`;
|
|
|
|
// Change color based on time left
|
|
timerBar.className = 'timer-bar';
|
|
if (timeLeft <= Math.floor(gameSettings.timerSeconds / 3)) {
|
|
timerBar.classList.add('danger');
|
|
} else if (timeLeft <= Math.floor(gameSettings.timerSeconds / 2)) {
|
|
timerBar.classList.add('warning');
|
|
}
|
|
}
|
|
|
|
// Handle time out
|
|
function timeOut() {
|
|
// Show feedback
|
|
feedback.textContent = gameSettings.language === 'arabic'
|
|
? 'انتهى الوقت!'
|
|
: 'Time\'s up!';
|
|
feedback.classList.add('incorrect');
|
|
|
|
// Switch players after a delay
|
|
setTimeout(() => {
|
|
hideQuestionModal();
|
|
switchPlayer();
|
|
}, 1500);
|
|
}
|
|
|
|
// Check the answer
|
|
function checkAnswer(userAnswer, shuffleMap = null) {
|
|
// Clear the timer if it's running
|
|
if (timerInterval) {
|
|
clearInterval(timerInterval);
|
|
timerInterval = null;
|
|
}
|
|
|
|
// Get all answer options
|
|
const options = document.querySelectorAll('.answer-option');
|
|
|
|
// Determine if the answer is correct
|
|
let isCorrect = false;
|
|
|
|
if (gameSettings.questionType === 'truefalse') {
|
|
isCorrect = userAnswer === currentQuestion.answer;
|
|
|
|
// Highlight the selected option
|
|
options[userAnswer ? 0 : 1].classList.add('selected');
|
|
|
|
// Highlight the correct option
|
|
options[currentQuestion.answer ? 0 : 1].classList.add('correct');
|
|
|
|
// If wrong, highlight as incorrect
|
|
if (!isCorrect) {
|
|
options[userAnswer ? 0 : 1].classList.add('incorrect');
|
|
}
|
|
} else {
|
|
isCorrect = userAnswer === currentQuestion.answer;
|
|
|
|
// Find the positions in the shuffled array
|
|
let selectedPosition = -1;
|
|
let correctPosition = -1;
|
|
|
|
if (shuffleMap) {
|
|
// Find the position of the selected answer in the shuffled array
|
|
for (let i = 0; i < options.length; i++) {
|
|
if (shuffleMap[i] === userAnswer) {
|
|
selectedPosition = i;
|
|
}
|
|
if (shuffleMap[i] === currentQuestion.answer) {
|
|
correctPosition = i;
|
|
}
|
|
}
|
|
} else {
|
|
// If no shuffle map (shouldn't happen), use original positions
|
|
selectedPosition = userAnswer;
|
|
correctPosition = currentQuestion.answer;
|
|
}
|
|
|
|
// Highlight the selected option
|
|
if (selectedPosition >= 0 && selectedPosition < options.length) {
|
|
options[selectedPosition].classList.add('selected');
|
|
}
|
|
|
|
// Highlight the correct option
|
|
if (correctPosition >= 0 && correctPosition < options.length) {
|
|
options[correctPosition].classList.add('correct');
|
|
}
|
|
|
|
// If wrong, highlight as incorrect
|
|
if (!isCorrect && selectedPosition >= 0 && selectedPosition < options.length) {
|
|
options[selectedPosition].classList.add('incorrect');
|
|
}
|
|
}
|
|
|
|
// Show feedback
|
|
feedback.textContent = isCorrect
|
|
? (gameSettings.language === 'arabic' ? 'إجابة صحيحة!' : 'Correct!')
|
|
: (gameSettings.language === 'arabic' ? 'إجابة خاطئة!' : 'Incorrect!');
|
|
feedback.classList.add(isCorrect ? 'correct' : 'incorrect');
|
|
|
|
// Show explanation if available
|
|
if (currentQuestion.explanation) {
|
|
setTimeout(() => {
|
|
feedback.textContent += ' ' + currentQuestion.explanation;
|
|
}, 500);
|
|
}
|
|
|
|
// If correct, make the move after a short delay
|
|
if (isCorrect) {
|
|
setTimeout(() => {
|
|
// Make sure we're using the player who clicked the cell
|
|
if (currentPlayer !== questionPlayer) {
|
|
// If the player has changed, switch back to the original player
|
|
currentPlayer = questionPlayer;
|
|
|
|
// Update the active player display
|
|
player1Element.classList.toggle('active', currentPlayer === 'X');
|
|
player2Element.classList.toggle('active', currentPlayer === 'O');
|
|
}
|
|
|
|
makeMove(selectedCellIndex);
|
|
hideQuestionModal();
|
|
}, 1500);
|
|
} else {
|
|
// If incorrect, close the modal after a delay and switch players
|
|
setTimeout(() => {
|
|
hideQuestionModal();
|
|
|
|
// Only switch if the current player is still the one who clicked
|
|
if (currentPlayer === questionPlayer) {
|
|
switchPlayer();
|
|
}
|
|
|
|
// Reset the question player
|
|
questionPlayer = null;
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
// Hide the question modal
|
|
function hideQuestionModal() {
|
|
// Clear the timer if it's running
|
|
if (timerInterval) {
|
|
clearInterval(timerInterval);
|
|
timerInterval = null;
|
|
}
|
|
|
|
questionModal.classList.remove('show');
|
|
}
|
|
|
|
// Make a move on the board
|
|
function makeMove(index) {
|
|
// Update the game board
|
|
gameBoard[index] = currentPlayer;
|
|
|
|
// Update the cell display
|
|
cells[index].textContent = currentPlayer;
|
|
cells[index].classList.add(currentPlayer.toLowerCase());
|
|
|
|
// Check for win or draw
|
|
if (checkWin()) {
|
|
endGame(false);
|
|
} else if (checkDraw()) {
|
|
endGame(true);
|
|
} else {
|
|
// Switch players
|
|
switchPlayer();
|
|
}
|
|
}
|
|
|
|
// Switch the current player
|
|
function switchPlayer() {
|
|
currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
|
|
|
|
// Update the active player display
|
|
player1Element.classList.toggle('active');
|
|
player2Element.classList.toggle('active');
|
|
}
|
|
|
|
// Check if the current player has won
|
|
function checkWin() {
|
|
const winPatterns = [
|
|
[0, 1, 2], [3, 4, 5], [6, 7, 8], // Rows
|
|
[0, 3, 6], [1, 4, 7], [2, 5, 8], // Columns
|
|
[0, 4, 8], [2, 4, 6] // Diagonals
|
|
];
|
|
|
|
for (const pattern of winPatterns) {
|
|
const [a, b, c] = pattern;
|
|
if (gameBoard[a] && gameBoard[a] === gameBoard[b] && gameBoard[a] === gameBoard[c]) {
|
|
// Highlight the winning cells
|
|
cells[a].classList.add('highlight');
|
|
cells[b].classList.add('highlight');
|
|
cells[c].classList.add('highlight');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Check if the game is a draw
|
|
function checkDraw() {
|
|
return gameBoard.every(cell => cell !== '');
|
|
}
|
|
|
|
// End the game
|
|
function endGame(isDraw) {
|
|
gameActive = false;
|
|
|
|
if (!isDraw) {
|
|
// Update scores
|
|
scores[currentPlayer]++;
|
|
document.querySelector(`#player${currentPlayer === 'X' ? '1' : '2'} .player-score`).textContent = scores[currentPlayer];
|
|
|
|
// Show game over message
|
|
gameResult.textContent = gameSettings.language === 'arabic'
|
|
? `اللاعب ${currentPlayer} فاز!`
|
|
: `Player ${currentPlayer} wins!`;
|
|
} else {
|
|
// Show draw message
|
|
gameResult.textContent = gameSettings.language === 'arabic' ? 'تعادل!' : 'It\'s a draw!';
|
|
}
|
|
|
|
// Show game over modal
|
|
setTimeout(() => {
|
|
gameOverModal.classList.add('show');
|
|
}, 1000);
|
|
}
|
|
|
|
// Reset the current game
|
|
function resetGame() {
|
|
// Clear the board
|
|
gameBoard = ['', '', '', '', '', '', '', '', ''];
|
|
gameActive = true;
|
|
currentPlayer = 'X';
|
|
|
|
// Reset the UI
|
|
cells.forEach(cell => {
|
|
cell.textContent = '';
|
|
cell.className = 'cell';
|
|
});
|
|
|
|
player1Element.classList.add('active');
|
|
player2Element.classList.remove('active');
|
|
|
|
// Clear any active timers
|
|
if (timerInterval) {
|
|
clearInterval(timerInterval);
|
|
timerInterval = null;
|
|
}
|
|
|
|
// Hide modals
|
|
questionModal.classList.remove('show');
|
|
gameOverModal.classList.remove('show');
|
|
}
|
|
|
|
// Start a new game (reset game and scores)
|
|
function newGame() {
|
|
// Reset scores
|
|
scores = { X: 0, O: 0 };
|
|
document.querySelector('#player1 .player-score').textContent = '0';
|
|
document.querySelector('#player2 .player-score').textContent = '0';
|
|
|
|
// Reset the game
|
|
resetGame();
|
|
}
|
|
|
|
// Apply new settings
|
|
function applySettings() {
|
|
gameSettings.questionType = questionTypeSelect.value;
|
|
gameSettings.language = questionLanguageSelect.value;
|
|
gameSettings.category = questionCategorySelect.value;
|
|
gameSettings.timerSeconds = parseInt(questionTimerSelect.value);
|
|
|
|
// Validate language-category combination
|
|
// Make sure the selected category exists for the selected language
|
|
if (gameSettings.language === 'english' && gameSettings.category !== 'math') {
|
|
// For English, only math category is available
|
|
gameSettings.category = 'math';
|
|
questionCategorySelect.value = 'math';
|
|
} else if (gameSettings.language === 'arabic' &&
|
|
(gameSettings.category !== 'islamic' && gameSettings.category !== 'beginners')) {
|
|
// For Arabic, only islamic and beginners categories are available
|
|
gameSettings.category = 'beginners';
|
|
questionCategorySelect.value = 'beginners';
|
|
}
|
|
|
|
// Update UI language if needed
|
|
updateUILanguage();
|
|
|
|
// Start a new game with new settings
|
|
newGame();
|
|
}
|
|
|
|
// Update UI language based on selected language
|
|
function updateUILanguage() {
|
|
const isArabic = gameSettings.language === 'arabic';
|
|
|
|
// Update document direction
|
|
document.documentElement.dir = isArabic ? 'rtl' : 'ltr';
|
|
|
|
// Update button texts
|
|
resetButton.textContent = isArabic ? 'إعادة اللعبة' : 'Reset Game';
|
|
newGameButton.textContent = isArabic ? 'لعبة جديدة' : 'New Game';
|
|
playAgainButton.textContent = isArabic ? 'العب مرة أخرى' : 'Play Again';
|
|
applySettingsButton.textContent = isArabic ? 'تطبيق الإعدادات' : 'Apply Settings';
|
|
|
|
// Update headings
|
|
document.querySelector('h1').textContent = isArabic ? 'لعبة تيك تاك تو التعليمية' : 'Educational Tic Tac Toe';
|
|
document.querySelector('.settings h3').textContent = isArabic ? 'إعدادات اللعبة' : 'Game Settings';
|
|
document.querySelector('.used-words-container h3').textContent = isArabic ? 'الكلمات المستخدمة:' : 'Selected Words:';
|
|
|
|
// Update player names
|
|
document.querySelector('#player1 .player-name').textContent = isArabic ? 'اللاعب 1' : 'Player 1';
|
|
document.querySelector('#player2 .player-name').textContent = isArabic ? 'اللاعب 2' : 'Player 2';
|
|
|
|
// Update settings labels
|
|
const labels = document.querySelectorAll('.setting-group label');
|
|
if (isArabic) {
|
|
labels[0].textContent = 'نوع السؤال:';
|
|
labels[1].textContent = 'اللغة:';
|
|
labels[2].textContent = 'الفئة:';
|
|
labels[3].textContent = 'الوقت المحدد (ثواني):';
|
|
} else {
|
|
labels[0].textContent = 'Question Type:';
|
|
labels[1].textContent = 'Language:';
|
|
labels[2].textContent = 'Category:';
|
|
labels[3].textContent = 'Time Limit (seconds):';
|
|
}
|
|
|
|
// Update modal texts
|
|
document.querySelector('#questionModal h2').textContent = isArabic
|
|
? 'أجب على السؤال لإجراء حركتك'
|
|
: 'Answer the question to make your move';
|
|
}
|
|
|
|
// Update the game display
|
|
function updateGameDisplay() {
|
|
// Update UI language
|
|
updateUILanguage();
|
|
|
|
// Update player scores
|
|
document.querySelector('#player1 .player-score').textContent = scores.X;
|
|
document.querySelector('#player2 .player-score').textContent = scores.O;
|
|
|
|
// Update active player
|
|
player1Element.classList.toggle('active', currentPlayer === 'X');
|
|
player2Element.classList.toggle('active', currentPlayer === 'O');
|
|
}
|
|
|
|
// Utility function to shuffle an array (Fisher-Yates algorithm)
|
|
function shuffleArray(array) {
|
|
for (let i = array.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[array[i], array[j]] = [array[j], array[i]];
|
|
}
|
|
return array;
|
|
}
|
|
|
|
// Initialize the game when the page loads
|
|
window.addEventListener('load', initGame); |