File indexing completed on 2024-05-12 03:42:44
0001 /* GCompris - calcudoku.js 0002 * 0003 * SPDX-FileCopyrightText: 2023 Johnny Jazeix <jazeix@gmail.com> 0004 * SPDX-License-Identifier: GPL-3.0-or-later 0005 * 0006 */ 0007 .pragma library 0008 .import QtQuick 2.12 as Quick 0009 .import "qrc:/gcompris/src/core/core.js" as Core 0010 0011 var numberOfLevel; 0012 var items; 0013 var symbols; 0014 var url = "qrc:/gcompris/src/activities/sudoku/resource/"; 0015 0016 var cages = []; // { "indexes": [], "result": integer, "operator": "" } 0017 var cagesIndexes = []; // for each cage, remember which cage it is in 0018 var initialCalcudoku; 0019 0020 var OperandsEnum = { 0021 TIMES_SIGN: "\u00D7", 0022 PLUS_SIGN: "\u002B", 0023 MINUS_SIGN: "\u2212", 0024 DIVIDE_SIGN: "\u2215" 0025 } 0026 0027 var Direction = { 0028 TOP: 0, 0029 BOTTOM: 1, 0030 RIGHT: 2, 0031 LEFT: 3 0032 } 0033 function getVisualOperator(operator) { 0034 if(operator == "+") { 0035 return OperandsEnum.PLUS_SIGN; 0036 } 0037 else if(operator == "*") { 0038 return OperandsEnum.TIMES_SIGN; 0039 } 0040 else if(operator == "-") { 0041 return OperandsEnum.MINUS_SIGN; 0042 } 0043 else if(operator == ":") { 0044 return OperandsEnum.DIVIDE_SIGN; 0045 } 0046 return operator; 0047 } 0048 0049 function start(items_) { 0050 items = items_; 0051 numberOfLevel = items.levels.length; 0052 items.currentLevel = Core.getInitialLevel(numberOfLevel); 0053 items.score.currentSubLevel = 0; 0054 // Shuffle all levels 0055 for(var nb = 0 ; nb < items.levels.length ; ++ nb) { 0056 if(items.levels[nb]["data"]) { 0057 Core.shuffle(items.levels[nb]["data"]); 0058 } 0059 } 0060 initLevel(); 0061 } 0062 0063 function stop() { 0064 } 0065 0066 function tryExpand(cagesIndexes, cages, newCage, id, attempt, attemptsMax, size) { 0067 // Wander 0068 while(newCage.length < size && attempt < 5) { 0069 var direction = Core.shuffle([Direction.TOP, Direction.BOTTOM, Direction.RIGHT, Direction.LEFT])[0]; 0070 attempt = attempt + 1; // Make sure we don't do too much trials 0071 if(direction == Direction.TOP && id >= size && cagesIndexes[id-size] == -1) { 0072 cagesIndexes[id-size] = id; 0073 newCage.push(id-size); 0074 //print("expand to top"); 0075 tryExpand(cagesIndexes, cages, newCage, id-size, attempt+1, attemptsMax, size); 0076 } 0077 if(direction == Direction.BOTTOM && id + size < size * size && cagesIndexes[id+size] == -1) { 0078 cagesIndexes[id+size] = id; 0079 newCage.push(id+size); 0080 //print("expand to bottom"); 0081 tryExpand(cagesIndexes, cages, newCage, id+size, attempt+1, attemptsMax, size); 0082 } 0083 if(direction == Direction.LEFT && id%size != 0 && cagesIndexes[id-1] == -1) { 0084 cagesIndexes[id-1] = id; 0085 newCage.push(id-1); 0086 //print("expand to left"); 0087 tryExpand(cagesIndexes, cages, newCage, id-1, attempt+1, attemptsMax, size); 0088 } 0089 if(direction == Direction.RIGHT && id%size != size-1 && cagesIndexes[id+1] == -1) { 0090 cagesIndexes[id+1] = id; 0091 newCage.push(id+1); 0092 //print("expand to right"); 0093 tryExpand(cagesIndexes, cages, newCage, id+1, attempt+1, attemptsMax, size); 0094 } 0095 } 0096 } 0097 0098 function computeResult(values, indexes, operator) { 0099 var result = operator == OperandsEnum.TIMES_SIGN ? 1 : 0; 0100 for(var index = 0 ; index < indexes.length ; ++ index) { 0101 var value = values[indexes[index]]; 0102 if(operator == OperandsEnum.DIVIDE_SIGN) { 0103 if(result == 0) { 0104 result = value; 0105 } 0106 else if(result > value) { 0107 result = result / value; 0108 } 0109 else { 0110 result = value / result; 0111 } 0112 } 0113 else if(operator == OperandsEnum.MINUS_SIGN) { 0114 if(result == 0) { 0115 result = value; 0116 } 0117 else if(result > value) { 0118 result = result - value; 0119 } 0120 else { 0121 result = value - result; 0122 } 0123 } 0124 else if(operator == OperandsEnum.TIMES_SIGN) { 0125 result = result * value; 0126 } 0127 else if(operator == OperandsEnum.PLUS_SIGN) { 0128 result = result + value; 0129 } 0130 else { 0131 result = value; 0132 } 0133 } 0134 return result; 0135 } 0136 0137 function generateLevel(size, allowedOperators) { 0138 var columns = []; 0139 var level = []; 0140 var line = []; 0141 var indexInLevel = 0; 0142 for(var c = 0 ; c < size; c++) { 0143 columns.push([]); 0144 } 0145 for(var x = 0 ; x < size; ++ x) { 0146 // Generate a line with values between 1 and size 0147 line = Core.shuffle(Array.from(Array(size+1).keys()).slice(1)); 0148 for(var c = 0 ; c < size ; ++ c) { 0149 var notInColumn = line.filter(v => !columns[c].includes(v)); 0150 //print("column", c, columns[c]); 0151 //print("notInColumn", notInColumn); 0152 //print("line before", line); 0153 var value = notInColumn[Math.floor(Math.random() * notInColumn.length)]; 0154 //print("value", value); 0155 // Revert the whole line if it is not possible and create a new line again. 0156 // Not really optimised but working :) 0157 if(notInColumn.length == 0) { 0158 x = x-1; 0159 for(var g = 0 ; g < c; g++) { 0160 columns[g].splice(columns[g].length-1, 1); 0161 indexInLevel --; 0162 } 0163 break; 0164 } 0165 level[indexInLevel ++] = value; 0166 line.splice(line.indexOf(value), 1); 0167 columns[c].push(value); 0168 } 0169 } 0170 var cagesIndexes = Array(size*size).fill(-1); 0171 var cages = []; 0172 // Now generate the cages (between 1 and size elements) 0173 for(var id = 0 ; id < size*size; ++ id) { 0174 if(cagesIndexes[id] == -1) { 0175 var newCage = [id]; 0176 cagesIndexes[id] = id; 0177 tryExpand(cagesIndexes, cages, newCage, id, 1, 10, size); 0178 cages.push({"indexes": newCage, "result": -1, "operator": ""}); 0179 } 0180 } 0181 //print("Level", level); 0182 //print("Cages", JSON.stringify(cages)); 0183 //print("cagesIndexes", cagesIndexes); 0184 // Now, if there are 2 elements, it's either - or /, we should favorise if they are allowed, if it is more it can only be + or *. Don't do * if the result is above 100 ? 0185 for(var cageId = 0 ; cageId < cages.length ; ++ cageId) { 0186 var currentCage = cages[cageId]; 0187 if(currentCage.indexes.length == 1) { 0188 currentCage.operator = ""; 0189 currentCage.result = level[currentCage.indexes[0]]; 0190 } 0191 else if(currentCage.indexes.length == 2) { 0192 var operatorsProbabilities = {}; 0193 operatorsProbabilities[OperandsEnum.MINUS_SIGN] = 40; 0194 operatorsProbabilities[OperandsEnum.DIVIDE_SIGN] = 35; 0195 operatorsProbabilities[OperandsEnum.TIMES_SIGN] = 25; 0196 operatorsProbabilities[OperandsEnum.PLUS_SIGN] = 20; 0197 // remove all operators not allowed... 0198 // then get a number between 0 and 100 and assign the good operator according to "luck" 0199 var firstValue = level[currentCage.indexes[0]]; 0200 var secondValue = level[currentCage.indexes[1]]; 0201 if(firstValue/secondValue % 1 != 0 && secondValue/firstValue % 1 != 0) { 0202 delete operatorsProbabilities[OperandsEnum.DIVIDE_SIGN]; 0203 } 0204 var operators = {}; 0205 for(var ope = 0 ; ope < allowedOperators.length ; ++ ope) { 0206 if(operatorsProbabilities[allowedOperators[ope]] != undefined) { 0207 operators[allowedOperators[ope]] = operatorsProbabilities[allowedOperators[ope]]; 0208 } 0209 } 0210 var keys = Object.keys(operators); 0211 var maxValue = 0; 0212 for(var v = 0 ; v < keys.length ; ++ v) { 0213 maxValue = maxValue + operators[keys[v]]; 0214 } 0215 var rand = Math.floor(Math.random() * maxValue); 0216 var sum = 0; 0217 for(var c = 0 ; c < keys.length ; ++ c) { 0218 sum = sum + operators[keys[c]]; 0219 if(sum > rand) { 0220 currentCage.operator = keys[c]; 0221 currentCage.result = computeResult(level, currentCage.indexes, currentCage.operator); 0222 break; 0223 } 0224 } 0225 } 0226 else if(currentCage.indexes.length > 2) { 0227 var operatorsProbabilities = {}; 0228 operatorsProbabilities[OperandsEnum.TIMES_SIGN] = 40; 0229 operatorsProbabilities[OperandsEnum.PLUS_SIGN] = 60; 0230 // remove all operators not allowed... 0231 // then get a number between 0 and the max value autorised and assign the corresponding operator 0232 var operators = {}; 0233 for(var ope = 0 ; ope < allowedOperators.length ; ++ ope) { 0234 if(operatorsProbabilities[allowedOperators[ope]] != undefined) { 0235 operators[allowedOperators[ope]] = operatorsProbabilities[allowedOperators[ope]]; 0236 } 0237 } 0238 var maxValue = 0; 0239 var keys = Object.keys(operators); 0240 for(var v = 0 ; v < keys.length ; ++ v) { 0241 maxValue = maxValue + operators[keys[v]]; 0242 } 0243 var rand = Math.floor(Math.random() * maxValue); 0244 var sum = 0; 0245 for(var c = 0 ; c < keys.length ; ++ c) { 0246 sum = sum + operators[keys[c]]; 0247 if(sum > rand) { 0248 currentCage.operator = keys[c]; 0249 currentCage.result = computeResult(level, currentCage.indexes, currentCage.operator); 0250 break; 0251 } 0252 } 0253 } 0254 } 0255 return {"cages": cages, "size": size}; 0256 } 0257 0258 function replaceOperator(array, operator, newValue) { 0259 var id = array.indexOf(operator); 0260 if(id != -1) { 0261 array[id] = newValue; 0262 } 0263 } 0264 0265 function initLevel() { 0266 symbols = items.levels[items.currentLevel]["symbols"]; 0267 0268 for(var i = items.availablePiecesModel.model.count-1 ; i >= 0 ; -- i) { 0269 items.availablePiecesModel.model.remove(i); 0270 } 0271 items.calcudokuModel.clear(); 0272 if(items.levels[items.currentLevel].random) { 0273 items.score.numberOfSubLevels = items.levels[items.currentLevel]["length"]; 0274 replaceOperator(items.levels[items.currentLevel]["operators"], "+", OperandsEnum.PLUS_SIGN); 0275 replaceOperator(items.levels[items.currentLevel]["operators"], "*", OperandsEnum.TIMES_SIGN); 0276 replaceOperator(items.levels[items.currentLevel]["operators"], "-", OperandsEnum.MINUS_SIGN); 0277 replaceOperator(items.levels[items.currentLevel]["operators"], ":", OperandsEnum.DIVIDE_SIGN); 0278 initialCalcudoku = generateLevel(items.levels[items.currentLevel]["size"], items.levels[items.currentLevel]["operators"]); 0279 } 0280 else { 0281 items.score.numberOfSubLevels = items.levels[items.currentLevel]["data"].length 0282 initialCalcudoku = items.levels[items.currentLevel]["data"][items.score.currentSubLevel]; 0283 } 0284 0285 items.columns = initialCalcudoku.size; 0286 items.rows = items.columns; 0287 0288 var cagesCount = initialCalcudoku.cages.length; 0289 var operatorsPositions = []; 0290 cages = []; 0291 cagesIndexes = []; 0292 0293 for(var v = 0 ; v < cagesCount ; ++ v) { 0294 var currentCage = initialCalcudoku.cages[v]; 0295 cages.push(currentCage); 0296 for (var p = 0 ; p < currentCage.indexes.length ; ++ p) { 0297 cagesIndexes[currentCage.indexes[p]] = v; 0298 } 0299 // Easiest way to retrieve the top info of the case 0300 var operatorResult = {"operator":"", "result": ""}; 0301 // Adding here the space to display before the operator 0302 operatorResult.operator = " " + getVisualOperator(currentCage.operator); 0303 operatorResult.result = "" + currentCage.result; 0304 operatorsPositions[currentCage.indexes[0]] = operatorResult; 0305 } 0306 //print("initLevel, cages", JSON.stringify(cages)); 0307 //print("initLevel, operatorsPositions", JSON.stringify(operatorsPositions)); 0308 //print("initLevel, cagesIndexes", JSON.stringify(cagesIndexes)); 0309 // Create grid 0310 for(var i = 0 ; i < items.rows ; ++ i) { 0311 for(var j = 0 ; j < items.columns ; ++ j) { 0312 var id = i * items.columns + j; 0313 var operator = ""; 0314 var result = ""; 0315 if(operatorsPositions[id] != undefined) { 0316 operator = operatorsPositions[id].operator; 0317 result = operatorsPositions[id].result; 0318 } 0319 0320 var cage = cages[cagesIndexes[id]].indexes; 0321 // Check walls 0322 var topWall = false; 0323 if(id < items.columns || cage.indexOf(id-items.columns) == -1) { 0324 topWall = true; 0325 } 0326 var bottomWall = false; 0327 if(id+items.columns >= items.columns*items.columns || cage.indexOf(id+items.columns) == -1) { 0328 bottomWall = true; 0329 } 0330 var leftWall = false; 0331 if(id%items.columns == 0 || cage.indexOf(id-1) == -1) { 0332 leftWall = true; 0333 } 0334 var rightWall = false; 0335 if(id%items.columns == items.columns-1 || cage.indexOf(id+1) == -1) { 0336 rightWall = true; 0337 } 0338 0339 0340 items.calcudokuModel.append({ 0341 'textValue': "", // Empty for now, TODO check if we want initial values, else remove it 0342 'initial': false, // remove this if we don't have default values 0343 'mState': "default", 0344 'resultValue': result, 0345 'operatorValue': operator, 0346 'topWall': topWall, 0347 'leftWall': leftWall, 0348 'rightWall': rightWall, 0349 'bottomWall': bottomWall 0350 }); 0351 } 0352 } 0353 0354 for(var line = 0 ; line < initialCalcudoku.size ; ++ line) { 0355 items.availablePiecesModel.model.append({"imgName": (line+1)+".svg", "text": ""+(line+1)}); 0356 } 0357 items.buttonsBlocked = false; 0358 } 0359 0360 function reinitLevel() { 0361 for(var i = 0 ; i < items.calcudokuModel.count ; ++ i) { 0362 items.calcudokuModel.get(i).textValue = ""; 0363 } 0364 } 0365 0366 function nextLevel() { 0367 items.score.stopWinAnimation(); 0368 items.score.currentSubLevel = 0; 0369 items.currentLevel = Core.getNextLevel(items.currentLevel, numberOfLevel); 0370 initLevel(); 0371 } 0372 0373 function previousLevel() { 0374 items.score.stopWinAnimation(); 0375 items.score.currentSubLevel = 0; 0376 items.currentLevel = Core.getPreviousLevel(items.currentLevel, numberOfLevel); 0377 initLevel(); 0378 } 0379 0380 /* 0381 Code that increments the sublevel and level 0382 And bail out if no more levels are available 0383 */ 0384 function incrementLevel() { 0385 if(items.score.currentSubLevel >= items.score.numberOfSubLevels) { 0386 items.bonus.good("flower"); 0387 } 0388 else { 0389 initLevel(); 0390 } 0391 } 0392 0393 function clickOn(caseX, caseY) { 0394 var currentCase = caseX + caseY * initialCalcudoku.size; 0395 0396 var currentSymbol = items.availablePiecesModel.model.get(items.availablePiecesModel.view.currentIndex); 0397 var isGood = isLegal(caseX, caseY, currentSymbol.text); 0398 /* 0399 If current case is empty, we look if it is legal and put the symbol. 0400 Else, we colorize the existing cases in conflict with the one pressed 0401 */ 0402 if(items.calcudokuModel.get(currentCase).textValue == "") { 0403 if(isGood) { 0404 items.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/win.wav'); 0405 items.calcudokuModel.get(currentCase).textValue = currentSymbol.text; 0406 } else { 0407 items.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/smudge.wav'); 0408 } 0409 } 0410 else { 0411 // Already a symbol in this case, we remove it 0412 items.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/darken.wav'); 0413 items.calcudokuModel.get(currentCase).textValue = ""; 0414 } 0415 0416 if(isSolved()) { 0417 items.buttonsBlocked = true; 0418 items.score.currentSubLevel += 1; 0419 items.score.playWinAnimation(); 0420 items.audioEffects.play("qrc:/gcompris/src/core/resource/sounds/completetask.wav"); 0421 } 0422 } 0423 0424 // return true or false if the given number is possible 0425 function isLegal(posX, posY, value) { 0426 var possible = true 0427 0428 // Check this number is not already in a row 0429 var firstX = posY * items.columns; 0430 var lastX = firstX + items.columns-1; 0431 0432 var clickedCase = posX + posY * items.columns; 0433 0434 for (var x = firstX ; x <= lastX ; ++ x) { 0435 if (x == clickedCase) 0436 continue 0437 0438 var rowValue = items.calcudokuModel.get(x) 0439 0440 if(value == rowValue.textValue) { 0441 items.calcudokuModel.get(x).mState = "error"; 0442 possible = false 0443 } 0444 } 0445 0446 var firstY = posX; 0447 var lastY = items.calcudokuModel.count - items.columns + firstY; 0448 0449 // Check this number is not already in a column 0450 for (var y = firstY ; y <= lastY ; y += items.columns) { 0451 if (y == clickedCase) 0452 continue; 0453 0454 var colValue = items.calcudokuModel.get(y); 0455 0456 if(value == colValue.textValue) { 0457 items.calcudokuModel.get(y).mState = "error"; 0458 possible = false; 0459 } 0460 } 0461 0462 return possible; 0463 } 0464 0465 /* 0466 Return true or false if the given calcudoku is solved 0467 We don't really check it's solved, only that all squares 0468 have a value. This works because only valid numbers can 0469 be entered up front. 0470 */ 0471 function isSolved() { 0472 var level = []; 0473 for(var i = 0 ; i < items.calcudokuModel.count ; ++ i) { 0474 var value = items.calcudokuModel.get(i).textValue; 0475 if(value == "") 0476 return false; 0477 level[i] = parseInt(value); 0478 } 0479 // for each cage, check if the count is correct 0480 for(var r = 0 ; r < cages.length ; ++ r) { 0481 var currentCage = cages[r]; 0482 var result = computeResult(level, currentCage.indexes, currentCage.operator); 0483 if(result != currentCage.result) { 0484 return false; 0485 } 0486 } 0487 return true; 0488 } 0489 0490 function restoreState(mCase) { 0491 items.calcudokuModel.get(mCase.gridIndex).mState = mCase.isInitial ? "initial" : "default"; 0492 } 0493 0494 function dataToImageSource(data) { 0495 var imageName = ""; 0496 0497 for(var i = 0 ; i < symbols.length ; ++ i) { 0498 if(symbols[i].text == data) { 0499 imageName = url + symbols[i].imgName; 0500 break; 0501 } 0502 } 0503 0504 return imageName; 0505 } 0506 0507 function onKeyPressed(event) { 0508 var keyValue = -1; 0509 switch(event.key) 0510 { 0511 case Qt.Key_1: 0512 keyValue = 0; 0513 break; 0514 case Qt.Key_2: 0515 keyValue = 1; 0516 break; 0517 case Qt.Key_3: 0518 keyValue = 2; 0519 break; 0520 case Qt.Key_4: 0521 keyValue = 3; 0522 break; 0523 case Qt.Key_5: 0524 keyValue = 4; 0525 break; 0526 case Qt.Key_6: 0527 keyValue = 5; 0528 break; 0529 case Qt.Key_7: 0530 keyValue = 6; 0531 break; 0532 case Qt.Key_8: 0533 keyValue = 7; 0534 break; 0535 case Qt.Key_9: 0536 keyValue = 8; 0537 break; 0538 } 0539 if(keyValue >= 0 && keyValue < items.availablePiecesModel.model.count) { 0540 items.availablePiecesModel.view.currentIndex = keyValue; 0541 event.accepted = true; 0542 } 0543 }