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 }