// 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 = '
';
for (let i = 0; i < answer.length; i++) {
resultHTML += ``;
}
resultHTML += '
';
problemDisplay.innerHTML = `
${num1}×${num2}=
${resultHTML}
`;
// 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 += '
';
// Add input fields for each digit
for (let j = 0; j < stepProduct.length; j++) {
intermediateHTML += ``;
}
// Add trailing empty spaces on the right
const trailingSpaces = i;
for (let e = 0; e < trailingSpaces; e++) {
intermediateHTML += ``;
}
intermediateHTML += '
';
}
}
// Create input fields for final result
const answer = gameState.currentProblem.answer.toString();
let resultHTML = '
';
for (let i = 0; i < answer.length; i++) {
resultHTML += ``;
}
resultHTML += '
';
let html = `
${num1Str}
×${num2Str}
`;
if (num2 > 9) {
html += `
${intermediateHTML}
`;
}
html += `
${resultHTML}
`;
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 = '
Your answer: ';
for (let i = 0; i < maxLen; i++) {
const digit = userPadded[i];
if (incorrectPositions.includes(i)) {
html += `${digit}`;
} else {
html += digit;
}
}
html += ` Correct answer: ${correctStr}
`;
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 += ` 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') + ' ' + gameState.difficulty + '';
}
// 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');
}
}