File indexing completed on 2024-05-12 15:21:06

0001 /* GCompris - adjacent_numbers.js
0002  *
0003  * SPDX-FileCopyrightText: 2023 Alexandre Laurent <littlewhite.dev@gmail.com>
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 .pragma library
0007 .import QtQuick 2.12 as Quick
0008 .import "qrc:/gcompris/src/core/core.js" as Core
0009 
0010 var items
0011 
0012 var dataset
0013 var currentExercise
0014 var numberExercises
0015 var subLevelStartTiles // Start tiles for each sublevel
0016 
0017 var expectedAnswer // The expected answer, as an array of strings
0018                    // through this code, the answer and question array contains strings
0019 
0020 // Check if level data contains all the expected keys
0021 function hasMandatoryKeys(level, levelIndex)
0022 {
0023     var expectedKeys = ["title", "lowerBound", "upperBound", "step", "numberShown",
0024             "indicesToGuess", "numberRandomLevel", "fixedLevels", "numberPropositions"]
0025     var missingKeys = []
0026     for(const key of expectedKeys) {
0027         if(level[key] === undefined) {
0028             missingKeys.push(key)
0029         }
0030     }
0031 
0032     if (missingKeys.length !== 0)
0033     {
0034         console.error("Level " + levelIndex + " is malformated. The following elements are missing: " + missingKeys +
0035                       "\nLevel will be ignored")
0036         return false
0037     }
0038     return true
0039 }
0040 
0041 // Check the consistency of the values defined in the level structure
0042 // returns true when the level seems valid
0043 function validateLevel(level, levelIndex)
0044 {
0045     if(!hasMandatoryKeys(level, levelIndex))
0046     {
0047         return false
0048     }
0049 
0050     var valid = true
0051     const lowerBound = level.lowerBound
0052     const upperBound = level.upperBound
0053     const step = level.step
0054     if(step === 0)
0055     {
0056         console.error("The 'step' defined in level " + levelIndex + " is 0.")
0057         valid = false
0058     }
0059 
0060     if (lowerBound > upperBound)
0061     {
0062         console.error("'lowerBound' is greater than 'upperBound' (" + lowerBound + " > " + upperBound + ")")
0063         valid = false
0064     }
0065 
0066     for(var index of level.indicesToGuess)
0067     {
0068         if(index >= level.numberShown)
0069         {
0070             console.error("Invalid indice to guess (" + index + "). Only " + level.numberShown + " tiles will be displayed")
0071             valid = false
0072         }
0073     }
0074 
0075     for(var start of level.fixedLevels)
0076     {
0077         if(start < lowerBound ||
0078            start > upperBound)
0079         {
0080             console.error("Invalid start value (" + start + ") for fixedLevels")
0081             valid = false
0082         }
0083     }
0084 
0085     const numberPropositions = level.numberPropositions
0086     if (numberPropositions <= 0)
0087     {
0088         console.error("Invalid number of propositions (" + numberPropositions + ")")
0089         valid = false
0090     }
0091 
0092 
0093     if(!valid)
0094     {
0095         console.error("Level " + levelIndex + " contains errors and will be ignored")
0096     }
0097 
0098     return valid
0099 }
0100 
0101 function start(items_) {
0102     items = items_
0103 
0104     dataset = items.levels
0105     dataset = dataset.filter((level,i) => validateLevel(level,i))
0106 
0107     numberExercises = dataset.length
0108 
0109     currentExercise = Core.getInitialLevel(numberExercises)
0110     items.score.currentSubLevel = 0
0111 
0112     initLevel()
0113 }
0114 
0115 function stop() {
0116 }
0117 
0118 function initLevel() {
0119     items.bar.level = currentExercise + 1
0120 
0121     items.questionTilesModel.clear();
0122     items.proposedTilesModel.clear();
0123 
0124     items.answerCompleted = false
0125     items.buttonsEnabled = true
0126 
0127     items.instruction.text = dataset[currentExercise].title
0128 
0129     // Sublevel setup
0130     if(items.score.currentSubLevel === 0) // we need to generate the sublevels
0131     {
0132         subLevelStartTiles = []
0133         if(items.randomSubLevels) {
0134             const lowerBound = dataset[currentExercise].lowerBound
0135             const upperBound = dataset[currentExercise].upperBound
0136             const step = dataset[currentExercise].step
0137             var possibleStartTiles = getStartTiles(lowerBound, upperBound, step)
0138             possibleStartTiles = Core.shuffle(possibleStartTiles)
0139             subLevelStartTiles = possibleStartTiles.slice(0, dataset[currentExercise].numberRandomLevel)
0140         }
0141         else
0142         {
0143             subLevelStartTiles = Array.from(dataset[currentExercise].fixedLevels)
0144         }
0145     }
0146 
0147     // Question tile generation
0148     var question = getQuestionArray(subLevelStartTiles[items.score.currentSubLevel],
0149                                     dataset[currentExercise].step, dataset[currentExercise].numberShown,
0150                                     dataset[currentExercise].indicesToGuess);
0151     for(const value of question)
0152     {
0153         if (value === '?') {
0154             items.questionTilesModel.append({
0155                                                 "value": "?",
0156                                                 "tileState": "NONE",
0157                                                 "canDrop": true,
0158                                                 "tileEdited": false
0159                                             })
0160 
0161         }
0162         else {
0163             items.questionTilesModel.append({
0164                                                 "value": value,
0165                                                 "tileState": "NONE",
0166                                                 "canDrop": false,
0167                                                 "tileEdited": true
0168                                             })
0169         }
0170     }
0171 
0172     // Generation of answer tiles
0173     var proposedAnswers = getCorrectAnswers(question);
0174     // Generate more propositions and avoid duplicates
0175     while (proposedAnswers.length < dataset[currentExercise].numberPropositions)
0176     {
0177         // Find number close to the starting one for the current question
0178         var multiplier = Math.floor(Math.random() * 15) - 5 // between -5 and 15
0179         var proposal = subLevelStartTiles[items.score.currentSubLevel] + dataset[currentExercise].step * multiplier
0180         if (proposal >= dataset[currentExercise].lowerBound)
0181         {
0182             var proposalStr = proposal.toString()
0183             if(!proposedAnswers.includes(proposalStr))
0184             {
0185                 proposedAnswers.push(proposalStr)
0186             }
0187         }
0188     }
0189 
0190     proposedAnswers = Core.shuffle(proposedAnswers)
0191     for (var i = 0 ; i < proposedAnswers.length ; i++)
0192     {
0193         items.proposedTilesModel.append({
0194                                             "value": proposedAnswers[i],
0195                                         })
0196     }
0197 
0198     items.score.numberOfSubLevels = subLevelStartTiles.length
0199 }
0200 
0201 /// Returns an array containing the possible start tiles
0202 /// Loop from lowerBound to upperBound using the step, to only generate valid numbers
0203 function getStartTiles(lowerBound, upperBound, step)
0204 {
0205     var tiles = []
0206     var n = lowerBound
0207     while (n <= upperBound)
0208     {
0209         tiles.push(n)
0210         n += step
0211     }
0212     return tiles;
0213 }
0214 
0215 // Returns an array containing the items that the pupil should answer (the ones missing from the question)
0216 function getCorrectAnswers(question)
0217 {
0218     var correctAnswers = []
0219     for(var i = 0 ; i < question.length ; i++)
0220     {
0221         if (question[i] === '?')
0222         {
0223             correctAnswers.push(expectedAnswer[i])
0224         }
0225     }
0226     return correctAnswers
0227 }
0228 
0229 function nextLevel() {
0230     items.score.stopWinAnimation()
0231     items.score.currentSubLevel = 0
0232     currentExercise = Core.getNextLevel(currentExercise, numberExercises)
0233     initLevel();
0234 }
0235 
0236 function previousLevel() {
0237     items.score.stopWinAnimation()
0238     items.score.currentSubLevel = 0
0239     currentExercise = Core.getPreviousLevel(currentExercise, numberExercises)
0240     initLevel();
0241 }
0242 
0243 // Produce a question, using the given start as first item, and return the answer
0244 function getNextQuestionArray(start, step, number) {
0245     var question = []
0246     for (var i = 0 ; i < number ; i++)
0247     {
0248         question.push((start + i * step).toString())
0249     }
0250     return question
0251 }
0252 
0253 // Make a question out of an answer
0254 // copy answer and put '?' in it at tileIndices positions
0255 function hideTiles(answer, tileIndices)
0256 {
0257     var question = Array.from(answer)
0258     for(const index of tileIndices) {
0259         question[index] = '?'
0260     }
0261     return question
0262 }
0263 
0264 // Returns an array containing the question.
0265 // It generates the answer and modify it to form a question out of it.
0266 // The returned array contains numbers and '?' where the pupil should complete
0267 function getQuestionArray(startTile, step, number, indicesToGuess) {
0268     expectedAnswer = getNextQuestionArray(startTile,
0269                                           step, number)
0270 
0271     var question = hideTiles(expectedAnswer, indicesToGuess)
0272     return question;
0273 }
0274 
0275 
0276 function isPupilAnswerRight(pupilAnswer) {
0277     if(expectedAnswer.length !== pupilAnswer.length) {
0278         console.error("Something really bad happened. Mismatch between input and answer array sizes")
0279         return false
0280     }
0281 
0282     for(var i = 0; i < expectedAnswer.length; i++)
0283     {
0284         if (pupilAnswer[i] !== expectedAnswer[i]) {
0285             return false
0286         }
0287     }
0288     return true;
0289 }
0290 
0291 function getPupilAnswerArray() {
0292     var pupilAnswer = []
0293     for(var i = 0 ; i < items.questionTilesModel.count ; i++)
0294     {
0295         pupilAnswer.push(items.questionTilesModel.get(i).value)
0296     }
0297     return pupilAnswer
0298 }
0299 
0300 // Check if the answer given by the pupil is correct and display the result
0301 function checkAnswer() {
0302     items.buttonsEnabled = false
0303     // Trigger animation to show which tiles are correct or not
0304     for(var i = 0 ; i < items.questionTilesModel.count ; i++)
0305     {
0306         const tileData = items.questionTilesModel.get(i)
0307         if (tileData.canDrop)
0308         {
0309             var state = "NONE"
0310             if(checkTileAnswer(i, tileData.value)) {
0311                 state = "RIGHT"
0312             }
0313             else {
0314                 state = "WRONG"
0315                 items.answerCompleted = false
0316             }
0317             items.questionTilesModel.set(i, {
0318                                             "tileState": state,
0319                                         });
0320         }
0321     }
0322 
0323     // Check the complete answer
0324     if (isPupilAnswerRight(getPupilAnswerArray()))
0325     {
0326         goodAnswerFeedback()
0327     }
0328     else
0329     {
0330         items.audioEffects.play("qrc:/gcompris/src/core/resource/sounds/crash.wav")
0331     }
0332 }
0333 
0334 function resetTile(index)
0335 {
0336     items.questionTilesModel.set(index, {
0337                                      "value": '?',
0338                                      "tileState": "NONE",
0339                                      "tileEdited": false,
0340                                  });
0341 }
0342 
0343 function goodAnswerFeedback() {
0344         items.score.currentSubLevel++
0345         items.score.playWinAnimation()
0346         items.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/win.wav')
0347 }
0348 
0349 function checkTileAnswer(index, value) {
0350     if(value === expectedAnswer[index])
0351         return true
0352     return false
0353 }
0354 
0355 function updatePupilAnswer(index, newValue) {
0356     if(newValue === '?') {
0357         resetTile(index);
0358         return
0359     }
0360 
0361     var state = "ANSWERED"
0362     items.questionTilesModel.set(index, {
0363                                      "value": newValue,
0364                                      "tileState": state,
0365                                      "tileEdited": true,
0366                                  });
0367 
0368     var completed = isAnswerComplete()
0369     items.answerCompleted = completed
0370     if (items.immediateAnswer && completed) {
0371         checkAnswer()
0372     }
0373 }
0374 
0375 // True when the question is fully completed
0376 function isAnswerComplete() {
0377     var completed = true
0378     for(var i = 0 ; i < items.questionTilesModel.count ; i++)
0379     {
0380         if(items.questionTilesModel.get(i).value === '?' ||
0381             !items.questionTilesModel.get(i).tileEdited)
0382         {
0383             completed = false
0384             break
0385         }
0386     }
0387     return completed
0388 }
0389 
0390 // A sublevel has been successfully completed. Move to the next one
0391 // or to the next level if all sublevels are done
0392 function nextSubLevel()
0393 {
0394     if (items.score.currentSubLevel >= subLevelStartTiles.length)
0395     {
0396         items.bonus.good("smiley")
0397     }
0398     else
0399     {
0400         initLevel();
0401     }
0402 }