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 }