File indexing completed on 2024-05-12 15:21:22
0001 /* GCompris - guess24.js 0002 * 0003 * SPDX-FileCopyrightText: 2023 Bruno ANSELME <be.root@free.fr> 0004 * SPDX-License-Identifier: GPL-3.0-or-later 0005 * 0006 * Reference : 0007 * https://en.wikipedia.org/wiki/Shunting_yard_algorithm 0008 * 0009 * function infixToPostfix(text) is a simplified version of Shunting Yard Algorithm 0010 * - Well formed solutions (no check for missing parenthesis) 0011 * - Only four operators, all left associative 0012 */ 0013 .pragma library 0014 .import QtQuick 2.12 as Quick 0015 .import "qrc:/gcompris/src/core/core.js" as Core 0016 0017 const dataUrl = "qrc:/gcompris/src/activities/guess24/resource/guess24.json" 0018 var numberOfLevel 0019 var items 0020 var allProblems = [] // All dataset problems 0021 var problems = [] // Dataset for current level 0022 var operationsStack = [] 0023 var stepsStack= [] 0024 var result = 0 0025 var lastAction 0026 var animActions = [ "forward", "backward", "cancel" ] 0027 var unstack = false // Unstack all operations when true (hintButton) 0028 var helpCount = 0 // Number of lines shown with hintButton (0 to 3) 0029 var splittedSolution = [] // 3 lines for help 0030 0031 var OperandsEnum = { 0032 NO_SIGN: -1, 0033 PLUS_SIGN: 0, 0034 MINUS_SIGN: 1, 0035 TIMES_SIGN: 2, 0036 DIVIDE_SIGN: 3 0037 } 0038 0039 function start(items_) { 0040 items = items_ 0041 numberOfLevel = items.levels.length 0042 items.currentLevel = Core.getInitialLevel(numberOfLevel) 0043 allProblems = items.jsonParser.parseFromUrl(dataUrl) 0044 initLevel() 0045 } 0046 0047 function stop() { 0048 } 0049 0050 function initLevel() { 0051 items.errorRectangle.resetState() 0052 items.score.currentSubLevel = 0 0053 problems = allProblems.filter(obj => { return items.levels[items.currentLevel].complexities.includes(obj.complexity) }) 0054 Core.shuffle(problems) 0055 problems = problems.slice(0, items.levels[items.currentLevel].count) 0056 items.subLevelCount = problems.length 0057 items.operatorsCount = items.levels[items.currentLevel].operatorsCount 0058 items.cardsBoard.enabled = true 0059 items.operators.enabled = true 0060 initCards() 0061 } 0062 0063 function nextLevel() { 0064 items.score.stopWinAnimation() 0065 items.currentLevel = Core.getNextLevel(items.currentLevel, numberOfLevel); 0066 initLevel(); 0067 } 0068 0069 function previousLevel() { 0070 items.score.stopWinAnimation() 0071 items.currentLevel = Core.getPreviousLevel(items.currentLevel, numberOfLevel); 0072 initLevel(); 0073 } 0074 0075 function nextSubLevel() { 0076 if(items.score.currentSubLevel >= problems.length) 0077 items.bonus.good("sun"); 0078 else 0079 initCards(); 0080 } 0081 0082 function randomSolution(problem, complexities) { 0083 var max = 0 0084 for (var i = 0; i < complexities.length; i++) { 0085 max = Math.max(complexities[i], max) 0086 } 0087 var validSolutions = [] 0088 // Check if solution fits required complexity level 0089 for (var solution of problem["solutions"]) { 0090 if ((solution.indexOf("/") !== -1) && (max < 3)) { 0091 continue 0092 } 0093 if ((solution.indexOf("*") !== -1) && (max < 2)) { 0094 continue 0095 } 0096 validSolutions.push(solution.trim()) 0097 } 0098 Core.shuffle(validSolutions) 0099 var sol = validSolutions[0] // Choose first valid solution from shuffled array 0100 sol = sol.replace(new RegExp(/\*/, "g"), "×") 0101 sol = sol.replace(new RegExp(/\//, "g"), "÷") 0102 return sol 0103 } 0104 0105 // Convert infixed notation string into a reverse polish notation array (RPN) 0106 // Shunting Yard Algorithm 0107 function infixToPostfix(text) { 0108 var words = text.match(new RegExp(/(\d+|[\(\)\+-×÷])/g)) 0109 var precedence = { "×": 3, "÷": 3, "+": 2, "-": 2 } 0110 var rpnStack = [] // Reverse polish notation stack 0111 var opStack = [] // operators stack 0112 var op = "" 0113 while (words.length) { 0114 var atom = words.shift() 0115 switch (atom) { 0116 case "+" : 0117 case "-" : 0118 case "×" : 0119 case "÷" : 0120 op = opStack[opStack.length - 1] 0121 while ((op !== "") && (op !== "(") && (precedence[op] >= precedence[atom])) { 0122 rpnStack.push(opStack.pop()) 0123 op = opStack.length ? opStack[opStack.length - 1] : "" 0124 } 0125 opStack.push(atom) 0126 break 0127 case "(" : 0128 opStack.push(atom) 0129 break 0130 case ")" : 0131 op = "" 0132 while ((op = opStack.pop()) !== "(") { 0133 rpnStack.push(op) 0134 } 0135 break 0136 default : 0137 rpnStack.push(atom) 0138 break 0139 } 0140 } 0141 while (opStack.length) { 0142 rpnStack.push(opStack.pop()) 0143 } 0144 return rpnStack 0145 } 0146 0147 function parseSolution(solutionText) { 0148 splittedSolution = [] 0149 var rpnStack = infixToPostfix(solutionText) 0150 0151 // Recursive function to solve RPN array (from end to beginning) 0152 function calcStack() { 0153 if (!rpnStack.length) 0154 return 0155 var atom = rpnStack.pop() 0156 var value = parseInt(atom) 0157 if (!isNaN(value)) { 0158 return value 0159 } else { 0160 var calc = 0 0161 var b = calcStack() 0162 var a = calcStack() 0163 switch (atom) { 0164 case "+": calc = a + b; break 0165 case "-": calc = a - b; break 0166 case "×": calc = a * b; break 0167 case "÷": calc = a / b; break 0168 } 0169 splittedSolution.push(`${a} ${atom} ${b} = ${calc}`) 0170 return calc 0171 } 0172 } 0173 calcStack() // build solution steps, add them to splittedSolution 0174 } 0175 0176 function initCards() { 0177 helpCount = 0 0178 items.animationCard.state = "" 0179 items.cancelButton.visible = false 0180 items.hintButton.visible = false 0181 items.keysOnValues = true 0182 var puzzle = problems[items.score.currentSubLevel]["puzzle"].split(" ") 0183 items.cardsModel.clear() 0184 for (var i = 0; i < 4; i++) { 0185 items.cardsModel.append( { "value_" : puzzle[i] }) 0186 } 0187 items.cardsModel.shuffleModel() 0188 items.currentValue = -1 0189 items.cardsBoard.currentIndex = 0 0190 items.operators.currentIndex = 0 0191 items.currentOperator = OperandsEnum.NO_SIGN 0192 unstack = false 0193 operationsStack = [] 0194 stepsStack = [] 0195 items.steps.text = stepsStack.join("\n") 0196 parseSolution(randomSolution(problems[items.score.currentSubLevel], items.levels[items.currentLevel].complexities)) 0197 items.solution.text = "" 0198 items.buttonsBlocked = false 0199 } 0200 0201 function valueClicked(idx) { 0202 result = 0 0203 if (items.currentValue === -1 || items.currentValue === idx) { 0204 items.cardsBoard.currentIndex = idx 0205 items.currentValue = idx 0206 } else { 0207 if (items.currentOperator !== OperandsEnum.NO_SIGN) { 0208 var a = Number(items.cardsModel.get(items.currentValue).value_) 0209 var b = Number(items.cardsModel.get(idx).value_) 0210 var text = "" 0211 switch (items.currentOperator) { 0212 case OperandsEnum.PLUS_SIGN: 0213 result = a + b 0214 text = `${a} + ${b} = ${result}` 0215 break 0216 case OperandsEnum.MINUS_SIGN: 0217 result = a - b 0218 text = `${a} - ${b} = ${result}` 0219 break 0220 case OperandsEnum.TIMES_SIGN: 0221 result = a * b 0222 text = `${a} × ${b} = ${result}` 0223 break 0224 case OperandsEnum.DIVIDE_SIGN: 0225 if (b !== 0) 0226 result = a / b 0227 else 0228 result = -1.5 // Any rational number will trigger an error 0229 text = `${a} ÷ ${b} = ${result}` 0230 break 0231 } 0232 stepsStack.push(text) 0233 // Check if result is not an integer 0234 if (result !== Math.floor(result)) { 0235 result = `${a}÷${b}` 0236 } 0237 items.cardsBoard.currentIndex = items.currentValue 0238 // Init card animation 0239 items.animationCard.value = a 0240 items.animationCard.x = items.cardsBoard.currentItem.x 0241 items.animationCard.y = items.cardsBoard.currentItem.y 0242 operationsStack.push({ from: items.currentValue, 0243 valFrom: String(a), 0244 to: idx, 0245 valTo: String(b) 0246 }) 0247 items.cancelButton.visible = true 0248 items.cardsBoard.currentItem.visible = false 0249 items.currentValue = idx 0250 items.cardsBoard.currentIndex = idx 0251 // Start card animation 0252 items.animationCard.action = "forward" 0253 items.animationCard.state = "moveto" 0254 } else { 0255 items.currentValue = idx 0256 items.cardsBoard.currentIndex = idx 0257 } 0258 } 0259 items.keysOnValues = false 0260 } 0261 0262 function checkResult() { 0263 items.operators.currentIndex = items.currentOperator 0264 items.currentOperator = OperandsEnum.NO_SIGN 0265 if ((result < 0) || (result !== Math.floor(result))) { 0266 items.animationCard.x = items.cardsBoard.currentItem.x 0267 items.animationCard.y = items.cardsBoard.currentItem.y 0268 items.animationCard.value = String(result) 0269 items.animationCard.action = "cancel" 0270 items.animationCard.state = "wait" 0271 return 0272 } 0273 items.steps.text = stepsStack.join("\n") 0274 items.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/bleep.wav') 0275 items.cardsModel.setProperty(items.currentValue, "value_", String(result)) 0276 if (operationsStack.length === 3) { 0277 items.buttonsBlocked = true; 0278 if (Number(items.cardsModel.get(items.currentValue).value_) === 24) { 0279 items.cancelButton.visible = false 0280 items.score.currentSubLevel++ 0281 items.score.playWinAnimation() 0282 items.audioEffects.play("qrc:/gcompris/src/core/resource/sounds/completetask.wav") 0283 } else { 0284 items.errorRectangle.startAnimation() 0285 items.audioEffects.play("qrc:/gcompris/src/core/resource/sounds/crash.wav") 0286 items.hintButton.visible = true 0287 } 0288 } 0289 } 0290 0291 function operatorClicked(idx) { 0292 if ((items.currentValue === -1) || (items.animationCard.state !== "")) 0293 return 0294 if (items.currentOperator === idx) 0295 items.currentOperator = OperandsEnum.NO_SIGN 0296 else { 0297 items.currentOperator = idx 0298 items.operators.currentIndex = idx 0299 } 0300 items.keysOnValues = true 0301 } 0302 0303 function popOperation() { 0304 items.cancelButton.enabled = false 0305 lastAction = operationsStack.pop(); 0306 items.cardsModel.setProperty(lastAction.to, "value_", lastAction.valTo) 0307 items.animationCard.value = lastAction.valFrom 0308 items.currentValue = items.cardsBoard.currentIndex = lastAction.to 0309 items.animationCard.x = items.cardsBoard.currentItem.x 0310 items.animationCard.y = items.cardsBoard.currentItem.y 0311 items.currentValue = items.cardsBoard.currentIndex = lastAction.from 0312 items.animationCard.action = "backward" 0313 items.animationCard.state = "moveto" 0314 items.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/smudge.wav') 0315 items.hintButton.visible = false 0316 if (operationsStack.length < 1) 0317 items.cancelButton.visible = false 0318 items.keysOnValues = true 0319 } 0320 0321 function endPopOperation() { 0322 stepsStack.pop() 0323 items.steps.text = stepsStack.join("\n") 0324 items.cardsModel.setProperty(lastAction.from, "value_", lastAction.valFrom) 0325 items.currentValue = items. cardsBoard.currentIndex = lastAction.from 0326 items.cardsBoard.currentItem.visible = true 0327 if (!operationsStack.length) 0328 unstack = false 0329 if ((unstack) && (operationsStack.length > 0)) 0330 popOperation() 0331 if (!unstack) 0332 items.cancelButton.enabled = true 0333 } 0334 0335 function moveToNextCard() { 0336 if (items.keysOnValues) { 0337 do { 0338 items.cardsBoard.currentIndex = ++items.cardsBoard.currentIndex % items.cardsBoard.count 0339 } while (!items.cardsBoard.currentItem.visible) 0340 } else { 0341 items.operators.currentIndex = ++items.operators.currentIndex % items.operatorsCount 0342 } 0343 } 0344 0345 function handleKeys(event) { 0346 if (items.buttonsBlocked) 0347 return 0348 items.keyboardNavigation = true 0349 switch (event.key) { 0350 case Qt.Key_Up: 0351 case Qt.Key_Down: 0352 if (items.keysOnValues) { 0353 do { 0354 switch (items.cardsBoard.currentIndex) { 0355 case OperandsEnum.PLUS_SIGN: items.cardsBoard.currentIndex = OperandsEnum.TIMES_SIGN; break; 0356 case OperandsEnum.MINUS_SIGN: items.cardsBoard.currentIndex = OperandsEnum.DIVIDE_SIGN; break; 0357 case OperandsEnum.TIMES_SIGN: items.cardsBoard.currentIndex = OperandsEnum.PLUS_SIGN; break; 0358 case OperandsEnum.DIVIDE_SIGN: items.cardsBoard.currentIndex = OperandsEnum.MINUS_SIGN; break; 0359 } 0360 0361 } while (!items.cardsBoard.currentItem.visible) 0362 } else { 0363 items.keysOnValues = true 0364 } 0365 break 0366 case Qt.Key_Left: 0367 if (items.keysOnValues) { 0368 do { 0369 items.cardsBoard.currentIndex = (items.cardsBoard.count + items.cardsBoard.currentIndex - 1) % items.cardsBoard.count 0370 } while (!items.cardsBoard.currentItem.visible) 0371 } else { 0372 items.operators.currentIndex = (items.operatorsCount + items.operators.currentIndex - 1) % items.operatorsCount 0373 } 0374 0375 break 0376 case Qt.Key_Right: 0377 moveToNextCard() 0378 break 0379 case Qt.Key_Return: 0380 case Qt.Key_Enter: 0381 case Qt.Key_Space: 0382 if (items.keysOnValues) 0383 valueClicked(items.cardsBoard.currentIndex) 0384 else { 0385 operatorClicked(items.operators.currentIndex) 0386 moveToNextCard() 0387 } 0388 break 0389 case Qt.Key_Plus: 0390 operatorClicked(OperandsEnum.PLUS_SIGN) 0391 break 0392 case Qt.Key_Minus: 0393 operatorClicked(OperandsEnum.MINUS_SIGN) 0394 break 0395 case Qt.Key_Asterisk: 0396 if (items.operatorsCount > 2) 0397 operatorClicked(OperandsEnum.TIMES_SIGN) 0398 break 0399 case Qt.Key_Slash: 0400 if (items.operatorsCount > 3) 0401 operatorClicked(OperandsEnum.DIVIDE_SIGN) 0402 break 0403 case Qt.Key_Delete: 0404 case Qt.Key_Backspace: 0405 if (items.solutionRect.opacity !== 0.0) 0406 items.animSol.start() 0407 else if ((items.cancelButton.enabled) && (operationsStack.length > 0)) 0408 popOperation() 0409 break 0410 case Qt.Key_Tab: 0411 if (items.currentValue != -1) 0412 items.keysOnValues = !items.keysOnValues 0413 break 0414 } 0415 }