mathstuff/script.js
Juan José Gutiérrez de Quevedo Pérez b907866c85
Some checks failed
continuous-integration/drone/push Build is failing
Refactor: Improve i18n support and fix multiplication steps layout
- Integrate i18n translations for difficulty descriptions
- Fix intermediate steps alignment in long multiplication display
- Simplify spacing calculations for better layout consistency
- Update translation files for all supported languages (en, es, el, sv)
2025-11-25 17:46:30 +01:00

643 lines
No EOL
21 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);
document.getElementById('difficulty-display').textContent = gameState.difficulty;
const config = difficultyConfig[gameState.difficulty];
if (config && config.descriptionKey) {
difficultyDescription.textContent = i18n.t(config.descriptionKey);
}
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 = maxResultWidth;
intermediateHTML += '<div class="line" style="justify-content: flex-end; gap: 5px;">';
// 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 = 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 10 points
if (gameState.points >= 10) {
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}/10`;
}
/**
* 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('youReached10Points');
}
if (playAgainButton) {
playAgainButton.textContent = i18n.t('playAgain');
}
}