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 }