Some checks reported errors
continuous-integration/drone Build encountered an error
651 lines
No EOL
21 KiB
JavaScript
651 lines
No EOL
21 KiB
JavaScript
// Game State
|
||
let gameState = {
|
||
difficulty: 1,
|
||
currentProblem: null,
|
||
points: 0,
|
||
totalAnswers: 0
|
||
};
|
||
|
||
// Difficulty configurations (will be updated with translations)
|
||
let difficultyConfig = {
|
||
1: {
|
||
num1Min: 1, num1Max: 9,
|
||
num2Min: 1, num2Max: 9,
|
||
descriptionKey: 'difficulty1'
|
||
},
|
||
2: {
|
||
num1Min: 10, num1Max: 99,
|
||
num2Min: 1, num2Max: 9,
|
||
descriptionKey: 'difficulty2'
|
||
},
|
||
3: {
|
||
num1Min: 10, num1Max: 99,
|
||
num2Min: 10, num2Max: 99,
|
||
descriptionKey: 'difficulty3'
|
||
},
|
||
4: {
|
||
num1Min: 100, num1Max: 999,
|
||
num2Min: 10, num2Max: 99,
|
||
descriptionKey: 'difficulty4'
|
||
},
|
||
5: {
|
||
num1Min: 10000, num1Max: 99999,
|
||
num2Min: 100, num2Max: 999,
|
||
descriptionKey: 'difficulty5'
|
||
}
|
||
};
|
||
|
||
// DOM Elements
|
||
const difficultySlider = document.getElementById('difficulty-slider');
|
||
const difficultyDisplay = document.getElementById('difficulty-display');
|
||
const difficultyDescription = document.getElementById('difficulty-description');
|
||
const problemDisplay = document.getElementById('problem-display');
|
||
const submitBtn = document.getElementById('submit-btn');
|
||
const feedbackDiv = document.getElementById('feedback');
|
||
const pointsScoreDisplay = document.getElementById('points-score');
|
||
const newProblemBtn = document.getElementById('new-problem-btn');
|
||
const victoryModal = document.getElementById('victory-modal');
|
||
const playAgainBtn = document.getElementById('play-again-btn');
|
||
|
||
// Event Listeners
|
||
difficultySlider.addEventListener('change', handleDifficultyChange);
|
||
submitBtn.addEventListener('click', handleSubmitAnswer);
|
||
newProblemBtn.addEventListener('click', generateNewProblem);
|
||
playAgainBtn.addEventListener('click', resetGame);
|
||
|
||
// Initialize
|
||
window.addEventListener('load', async () => {
|
||
await i18n.initialize();
|
||
initializeLanguageSelector();
|
||
updateUIText();
|
||
generateNewProblem();
|
||
});
|
||
|
||
/**
|
||
* Handle difficulty slider change
|
||
*/
|
||
function handleDifficultyChange() {
|
||
gameState.difficulty = parseInt(difficultySlider.value);
|
||
difficultyDisplay.textContent = gameState.difficulty;
|
||
difficultyDescription.textContent = difficultyConfig[gameState.difficulty].description;
|
||
generateNewProblem();
|
||
}
|
||
|
||
/**
|
||
* Generate a random number between min and max (inclusive)
|
||
*/
|
||
function getRandomNumber(min, max) {
|
||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||
}
|
||
|
||
/**
|
||
* Generate a new multiplication problem
|
||
*/
|
||
function generateNewProblem() {
|
||
const config = difficultyConfig[gameState.difficulty];
|
||
const num1 = getRandomNumber(config.num1Min, config.num1Max);
|
||
const num2 = getRandomNumber(config.num2Min, config.num2Max);
|
||
const answer = num1 * num2;
|
||
|
||
gameState.currentProblem = {
|
||
num1,
|
||
num2,
|
||
answer
|
||
};
|
||
|
||
displayProblem();
|
||
clearFeedback();
|
||
}
|
||
|
||
/**
|
||
* Calculate intermediate steps for vertical multiplication
|
||
*/
|
||
function calculateIntermediateSteps(num1, num2) {
|
||
const num2Str = num2.toString();
|
||
const steps = [];
|
||
|
||
// Calculate partial products for each digit of num2
|
||
for (let i = num2Str.length - 1; i >= 0; i--) {
|
||
const digit = parseInt(num2Str[i]);
|
||
const partialProduct = num1 * digit;
|
||
const shiftAmount = num2Str.length - 1 - i;
|
||
steps.push({
|
||
digit,
|
||
product: partialProduct,
|
||
shift: shiftAmount
|
||
});
|
||
}
|
||
|
||
return steps;
|
||
}
|
||
|
||
/**
|
||
* Display the multiplication problem
|
||
*/
|
||
function displayProblem() {
|
||
const { num1, num2 } = gameState.currentProblem;
|
||
const difficulty = gameState.difficulty;
|
||
|
||
if (difficulty === 1) {
|
||
// Horizontal layout for 1x1 with result
|
||
const answer = gameState.currentProblem.answer.toString();
|
||
let resultHTML = '<div class="line" style="justify-content: flex-end; gap: 2px;">';
|
||
for (let i = 0; i < answer.length; i++) {
|
||
resultHTML += `<input type="text" class="digit-input result-input" maxlength="1" data-pos="${i}" />`;
|
||
}
|
||
resultHTML += '</div>';
|
||
|
||
problemDisplay.innerHTML = `
|
||
<div class="multiplication-horizontal">
|
||
<span class="number">${num1}</span>
|
||
<span class="operator">×</span>
|
||
<span class="number">${num2}</span>
|
||
<span class="operator">=</span>
|
||
${resultHTML}
|
||
</div>
|
||
`;
|
||
|
||
// Add event listeners to result digit inputs
|
||
setTimeout(() => {
|
||
addResultDigitInputListeners();
|
||
}, 0);
|
||
} else {
|
||
// Vertical layout for 2+ digits
|
||
const num1Str = num1.toString();
|
||
const num2Str = num2.toString();
|
||
|
||
// Calculate intermediate steps for reference
|
||
const steps = calculateIntermediateSteps(num1, num2);
|
||
gameState.currentProblem.steps = steps;
|
||
|
||
// Create input fields for intermediate steps only if num2 has more than 1 digit
|
||
let intermediateHTML = '';
|
||
if (num2 > 9) {
|
||
// Calculate the maximum width needed for intermediate steps
|
||
const maxResultWidth = gameState.currentProblem.answer.toString().length;
|
||
|
||
for (let i = 0; i < steps.length; i++) {
|
||
const stepProduct = steps[i].product.toString();
|
||
const totalWidth = stepProduct.length + steps[i].shift;
|
||
intermediateHTML += '<div class="line" style="justify-content: flex-start; gap: 5px;">';
|
||
|
||
// Each row is offset one position to the left
|
||
// Add offset spaces at the beginning (left side)
|
||
for (let offset = 0; offset < i; offset++) {
|
||
intermediateHTML += '<span style="width: 90px;"></span>';
|
||
}
|
||
|
||
// Add empty spaces for shift
|
||
for (let s = 0; s < steps[i].shift; s++) {
|
||
intermediateHTML += '<span style="width: 90px;"></span>';
|
||
}
|
||
|
||
// Add input fields for each digit
|
||
for (let j = 0; j < stepProduct.length; j++) {
|
||
intermediateHTML += `<input type="text" class="digit-input intermediate-input" maxlength="1" data-step="${i}" data-pos="${j}" />`;
|
||
}
|
||
|
||
// Add trailing empty spaces on the right
|
||
const trailingSpaces = maxResultWidth - totalWidth - i;
|
||
for (let e = 0; e < trailingSpaces; e++) {
|
||
intermediateHTML += '<span style="width: 90px;"></span>';
|
||
}
|
||
|
||
intermediateHTML += '</div>';
|
||
}
|
||
}
|
||
|
||
// Create input fields for final result
|
||
const answer = gameState.currentProblem.answer.toString();
|
||
let resultHTML = '<div class="line" style="justify-content: flex-end; gap: 2px;">';
|
||
for (let i = 0; i < answer.length; i++) {
|
||
resultHTML += `<input type="text" class="digit-input result-input" maxlength="1" data-pos="${i}" />`;
|
||
}
|
||
resultHTML += '</div>';
|
||
|
||
let html = `
|
||
<div class="multiplication-vertical">
|
||
<div class="line">
|
||
<span>${num1Str}</span>
|
||
</div>
|
||
<div class="line">
|
||
<span class="operator">×</span>
|
||
<span>${num2Str}</span>
|
||
</div>
|
||
<div class="separator"></div>
|
||
`;
|
||
|
||
if (num2 > 9) {
|
||
html += `
|
||
<div id="intermediate-steps">
|
||
${intermediateHTML}
|
||
</div>
|
||
<div class="separator"></div>
|
||
`;
|
||
}
|
||
|
||
html += `
|
||
<div id="result-steps">
|
||
${resultHTML}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
problemDisplay.innerHTML = html;
|
||
|
||
// Add event listeners to digit inputs
|
||
setTimeout(() => {
|
||
addDigitInputListeners();
|
||
addResultDigitInputListeners();
|
||
}, 0);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Add event listeners to digit input fields
|
||
*/
|
||
function addDigitInputListeners() {
|
||
const digitInputs = document.querySelectorAll('.digit-input:not(.result-input)');
|
||
|
||
digitInputs.forEach((input, index) => {
|
||
input.addEventListener('input', (e) => {
|
||
// Only allow digits
|
||
if (!/^\d?$/.test(e.target.value)) {
|
||
e.target.value = '';
|
||
return;
|
||
}
|
||
|
||
// Move to next input if digit entered
|
||
if (e.target.value && index < digitInputs.length - 1) {
|
||
digitInputs[index + 1].focus();
|
||
}
|
||
});
|
||
|
||
input.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Backspace' && !e.target.value && index > 0) {
|
||
digitInputs[index - 1].focus();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Add event listeners to result digit input fields
|
||
* Navigation: left after filling, wrap to rightmost of next line
|
||
*/
|
||
function addResultDigitInputListeners() {
|
||
const resultInputs = document.querySelectorAll('.result-input');
|
||
|
||
resultInputs.forEach((input, index) => {
|
||
input.addEventListener('input', (e) => {
|
||
// Only allow digits
|
||
if (!/^\d?$/.test(e.target.value)) {
|
||
e.target.value = '';
|
||
return;
|
||
}
|
||
|
||
// Move to LEFT if digit entered
|
||
if (e.target.value) {
|
||
if (index > 0) {
|
||
// Move to previous input (left)
|
||
resultInputs[index - 1].focus();
|
||
} else if (index === 0) {
|
||
// At the leftmost position, wrap to rightmost of previous line
|
||
const allLines = document.querySelectorAll('#result-steps .line, #intermediate-steps .line');
|
||
if (allLines.length > 1) {
|
||
const prevLineInputs = allLines[allLines.length - 2].querySelectorAll('input');
|
||
if (prevLineInputs.length > 0) {
|
||
prevLineInputs[prevLineInputs.length - 1].focus();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
input.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Backspace' && !e.target.value && index < resultInputs.length - 1) {
|
||
resultInputs[index + 1].focus();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Find which digits are incorrect in the answer
|
||
*/
|
||
function findIncorrectDigits(userAnswer, correctAnswer) {
|
||
const userStr = userAnswer.toString();
|
||
const correctStr = correctAnswer.toString();
|
||
const incorrectPositions = [];
|
||
|
||
// Pad the shorter string with leading zeros for comparison
|
||
const maxLen = Math.max(userStr.length, correctStr.length);
|
||
const userPadded = userStr.padStart(maxLen, '0');
|
||
const correctPadded = correctStr.padStart(maxLen, '0');
|
||
|
||
for (let i = 0; i < maxLen; i++) {
|
||
if (userPadded[i] !== correctPadded[i]) {
|
||
incorrectPositions.push(i);
|
||
}
|
||
}
|
||
|
||
return incorrectPositions;
|
||
}
|
||
|
||
/**
|
||
* Display answer with highlighted incorrect digits
|
||
*/
|
||
function displayAnswerWithErrors(userAnswer, correctAnswer) {
|
||
const incorrectPositions = findIncorrectDigits(userAnswer, correctAnswer);
|
||
const userStr = userAnswer.toString();
|
||
const correctStr = correctAnswer.toString();
|
||
const maxLen = Math.max(userStr.length, correctStr.length);
|
||
const userPadded = userStr.padStart(maxLen, '0');
|
||
|
||
let html = '<div style="font-size: 1.5em; margin-top: 15px;"><strong>Your answer:</strong> ';
|
||
|
||
for (let i = 0; i < maxLen; i++) {
|
||
const digit = userPadded[i];
|
||
if (incorrectPositions.includes(i)) {
|
||
html += `<span style="border: 3px solid #dc2626; padding: 5px 8px; margin: 0 2px; display: inline-block; background-color: #fee2e2;">${digit}</span>`;
|
||
} else {
|
||
html += digit;
|
||
}
|
||
}
|
||
|
||
html += `<br><strong>Correct answer:</strong> ${correctStr}</div>`;
|
||
return html;
|
||
}
|
||
|
||
/**
|
||
* Get user answer from input boxes
|
||
*/
|
||
function getUserAnswer() {
|
||
const resultInputs = document.querySelectorAll('.result-input');
|
||
let answer = '';
|
||
resultInputs.forEach(input => {
|
||
answer += input.value;
|
||
});
|
||
return answer === '' ? null : parseInt(answer);
|
||
}
|
||
|
||
/**
|
||
* Get user intermediate steps from input boxes
|
||
*/
|
||
function getUserIntermediateSteps() {
|
||
const steps = [];
|
||
const intermediateLines = document.querySelectorAll('#intermediate-steps .line');
|
||
|
||
intermediateLines.forEach((line, lineIndex) => {
|
||
const inputs = line.querySelectorAll('input');
|
||
let stepValue = '';
|
||
inputs.forEach(input => {
|
||
stepValue += input.value;
|
||
});
|
||
if (stepValue) {
|
||
steps.push(parseInt(stepValue));
|
||
}
|
||
});
|
||
|
||
return steps;
|
||
}
|
||
|
||
/**
|
||
* Handle answer submission
|
||
*/
|
||
function handleSubmitAnswer() {
|
||
const userAnswer = getUserAnswer();
|
||
const correctAnswer = gameState.currentProblem.answer;
|
||
|
||
if (userAnswer === null) {
|
||
showFeedback('Please enter an answer!', 'incorrect');
|
||
return;
|
||
}
|
||
|
||
gameState.totalAnswers++;
|
||
|
||
// Check intermediate steps if they exist
|
||
const intermediateStepsDiv = document.getElementById('intermediate-steps');
|
||
let intermediateStepsCorrect = true;
|
||
let intermediateErrorMessage = '';
|
||
|
||
if (intermediateStepsDiv) {
|
||
const steps = gameState.currentProblem.steps;
|
||
const intermediateLines = document.querySelectorAll('#intermediate-steps .line');
|
||
|
||
intermediateLines.forEach((line, lineIndex) => {
|
||
const inputs = line.querySelectorAll('input');
|
||
let userStepValue = '';
|
||
inputs.forEach(input => {
|
||
userStepValue += input.value;
|
||
});
|
||
|
||
if (userStepValue) {
|
||
const userStepNum = parseInt(userStepValue);
|
||
const correctStepNum = steps[lineIndex].product;
|
||
|
||
if (userStepNum !== correctStepNum) {
|
||
intermediateStepsCorrect = false;
|
||
intermediateErrorMessage += `<br>Step ${lineIndex + 1}: You wrote ${userStepNum}, correct is ${correctStepNum}`;
|
||
|
||
// Highlight incorrect digits in this step
|
||
const correctStr = correctStepNum.toString();
|
||
const userStr = userStepValue;
|
||
inputs.forEach((input, digitIndex) => {
|
||
if (digitIndex < userStr.length && userStr[digitIndex] !== correctStr[correctStr.length - userStr.length + digitIndex]) {
|
||
input.classList.add('error');
|
||
} else if (digitIndex < userStr.length) {
|
||
input.classList.add('correct');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
if (userAnswer === correctAnswer && intermediateStepsCorrect) {
|
||
gameState.points += 1;
|
||
showFeedback('🎉 Correct! +1 point', 'correct');
|
||
updateScore();
|
||
|
||
// Check if reached 20 points
|
||
if (gameState.points >= 20) {
|
||
showVictoryModal();
|
||
return;
|
||
}
|
||
|
||
setTimeout(() => {
|
||
generateNewProblem();
|
||
}, 1500);
|
||
} else {
|
||
gameState.points = Math.max(0, gameState.points - 2);
|
||
|
||
// Mark incorrect result boxes with red color
|
||
if (userAnswer !== correctAnswer) {
|
||
const resultInputs = document.querySelectorAll('.result-input');
|
||
const userStr = userAnswer.toString();
|
||
const correctStr = correctAnswer.toString();
|
||
const maxLen = Math.max(userStr.length, correctStr.length);
|
||
const userPadded = userStr.padStart(maxLen, '0');
|
||
const correctPadded = correctStr.padStart(maxLen, '0');
|
||
|
||
resultInputs.forEach((input, index) => {
|
||
if (userPadded[index] !== correctPadded[index]) {
|
||
input.classList.add('error');
|
||
}
|
||
});
|
||
}
|
||
|
||
feedbackDiv.textContent = '❌ Your answer is wrong, check the red boxes';
|
||
feedbackDiv.className = 'feedback incorrect';
|
||
updateScore();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Show feedback message
|
||
*/
|
||
function showFeedback(message, type) {
|
||
feedbackDiv.textContent = message;
|
||
feedbackDiv.className = `feedback ${type}`;
|
||
}
|
||
|
||
/**
|
||
* Clear feedback message
|
||
*/
|
||
function clearFeedback() {
|
||
feedbackDiv.textContent = '';
|
||
feedbackDiv.className = 'feedback empty';
|
||
}
|
||
|
||
/**
|
||
* Update score display
|
||
*/
|
||
function updateScore() {
|
||
pointsScoreDisplay.textContent = `${gameState.points}/20`;
|
||
}
|
||
|
||
/**
|
||
* Show victory modal and create confetti
|
||
*/
|
||
function showVictoryModal() {
|
||
victoryModal.classList.add('show');
|
||
createConfetti();
|
||
}
|
||
|
||
/**
|
||
* Create confetti animation
|
||
*/
|
||
function createConfetti() {
|
||
const colors = ['#2563eb', '#7c3aed', '#dc2626', '#16a34a', '#ea580c', '#f59e0b'];
|
||
const confettiCount = 50;
|
||
|
||
for (let i = 0; i < confettiCount; i++) {
|
||
const confetti = document.createElement('div');
|
||
confetti.className = 'confetti';
|
||
confetti.style.left = Math.random() * 100 + '%';
|
||
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
|
||
confetti.style.delay = Math.random() * 0.5 + 's';
|
||
confetti.style.animationDuration = (Math.random() * 2 + 2.5) + 's';
|
||
document.body.appendChild(confetti);
|
||
|
||
// Remove confetti element after animation
|
||
setTimeout(() => {
|
||
confetti.remove();
|
||
}, 3500);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Reset game for new round
|
||
*/
|
||
function resetGame() {
|
||
gameState.points = 0;
|
||
gameState.totalAnswers = 0;
|
||
victoryModal.classList.remove('show');
|
||
updateScore();
|
||
generateNewProblem();
|
||
}
|
||
|
||
/**
|
||
* Initialize language selector
|
||
*/
|
||
function initializeLanguageSelector() {
|
||
const languageFlagsContainer = document.getElementById('language-flags');
|
||
const languages = i18n.getSupportedLanguages();
|
||
|
||
languageFlagsContainer.innerHTML = '';
|
||
|
||
Object.entries(languages).forEach(([langCode, langInfo]) => {
|
||
const flagButton = document.createElement('button');
|
||
flagButton.className = 'language-flag';
|
||
flagButton.textContent = langInfo.flag;
|
||
flagButton.title = langInfo.name;
|
||
flagButton.setAttribute('data-tooltip', langInfo.name);
|
||
|
||
if (langCode === i18n.getCurrentLanguage()) {
|
||
flagButton.classList.add('active');
|
||
}
|
||
|
||
flagButton.addEventListener('click', () => {
|
||
changeLanguage(langCode);
|
||
});
|
||
|
||
languageFlagsContainer.appendChild(flagButton);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Change language and update UI
|
||
*/
|
||
function changeLanguage(langCode) {
|
||
i18n.setLanguage(langCode);
|
||
|
||
// Update active flag
|
||
document.querySelectorAll('.language-flag').forEach(flag => {
|
||
flag.classList.remove('active');
|
||
});
|
||
const languages = i18n.getSupportedLanguages();
|
||
const flagButtons = document.querySelectorAll('.language-flag');
|
||
const langCodes = Object.keys(languages);
|
||
flagButtons.forEach((btn, index) => {
|
||
if (langCodes[index] === langCode) {
|
||
btn.classList.add('active');
|
||
}
|
||
});
|
||
|
||
updateUIText();
|
||
}
|
||
|
||
/**
|
||
* Update all UI text based on current language
|
||
*/
|
||
function updateUIText() {
|
||
// Update header
|
||
document.getElementById('title').textContent = '🎓 ' + i18n.t('title');
|
||
document.getElementById('subtitle').textContent = i18n.t('subtitle');
|
||
|
||
// Update difficulty label
|
||
const difficultyLabel = document.querySelector('.difficulty-label');
|
||
if (difficultyLabel) {
|
||
difficultyLabel.innerHTML = i18n.t('difficultyLevel') + ' <span id="difficulty-display">' + gameState.difficulty + '</span>';
|
||
}
|
||
|
||
// Update difficulty text labels
|
||
const difficultyTexts = document.querySelectorAll('.difficulty-text');
|
||
if (difficultyTexts.length >= 2) {
|
||
difficultyTexts[0].textContent = i18n.t('easy');
|
||
difficultyTexts[1].textContent = i18n.t('hard');
|
||
}
|
||
|
||
// Update difficulty description
|
||
const config = difficultyConfig[gameState.difficulty];
|
||
if (config && config.descriptionKey) {
|
||
document.getElementById('difficulty-description').textContent = i18n.t(config.descriptionKey);
|
||
}
|
||
|
||
// Update buttons
|
||
document.getElementById('submit-btn').textContent = i18n.t('checkAnswer');
|
||
document.getElementById('new-problem-btn').textContent = i18n.t('newProblem');
|
||
|
||
// Update score label
|
||
const scoreLabel = document.querySelector('.score-label');
|
||
if (scoreLabel) {
|
||
scoreLabel.textContent = i18n.t('points');
|
||
}
|
||
|
||
// Update modal
|
||
const modalTitle = document.querySelector('.modal-content h2');
|
||
const modalText = document.querySelector('.modal-content p');
|
||
const playAgainButton = document.getElementById('play-again-btn');
|
||
|
||
if (modalTitle) {
|
||
modalTitle.textContent = i18n.t('congratulations');
|
||
}
|
||
if (modalText) {
|
||
modalText.textContent = i18n.t('youReached20Points');
|
||
}
|
||
if (playAgainButton) {
|
||
playAgainButton.textContent = i18n.t('playAgain');
|
||
}
|
||
} |