Warning, file /education/gcompris/src/activities/checkers/checkers.js was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /* GCompris - checkers.js
0002  *
0003  * SPDX-FileCopyrightText: 2017 Johnny Jazeix <jazeix@gmail.com>
0004  *
0005  * Authors:
0006  *   Johnny Jazeix <jazeix@gmail.com>
0007  *
0008  *   SPDX-License-Identifier: GPL-3.0-or-later
0009  */
0010 .pragma library
0011 .import QtQuick 2.12 as Quick
0012 .import "qrc:/gcompris/src/core/core.js" as Core
0013 .import "engine.js" as Engine
0014 
0015 var url = "qrc:/gcompris/src/activities/checkers/resource/"
0016 
0017 var numberOfLevel = 5
0018 var items
0019 var state
0020 
0021 function start(items_) {
0022     items = items_
0023     items.currentLevel = Core.getInitialLevel(numberOfLevel)
0024     initLevel()
0025 }
0026 
0027 function stop() {
0028     items.trigComputerMove.stop();
0029     items.redoTimer.stop();
0030 }
0031 
0032 function initLevel() {
0033     state = new Engine.Draughts('W:W31-50:B1-20')
0034     state.resetGame()
0035     items.from = -1
0036     items.gameOver = false
0037     items.redo_stack = []
0038     refresh()
0039     items.positions = 0 // Force a model reload
0040     items.positions = simplifiedState(state.position())
0041     clearAcceptMove()
0042 }
0043 
0044 function nextLevel() {
0045     items.currentLevel = Core.getNextLevel(items.currentLevel, numberOfLevel);
0046     initLevel();
0047 }
0048 
0049 function previousLevel() {
0050     items.currentLevel = Core.getPreviousLevel(items.currentLevel, numberOfLevel);
0051     initLevel();
0052 }
0053 
0054 function simplifiedState(position) {
0055     var newState = new Array()
0056     for(var i = 0; i < position.length; ++i) {
0057         var img = position[i] !== '0' && position[i] !== '?' ? position[i] : ''
0058         newState.push(
0059             {
0060                 'pos': engineToViewPos(i),
0061                 'img': img
0062             })
0063     }
0064     return newState
0065 }
0066 
0067 function updateMessage() {
0068     items.gameOver = false
0069     items.message = items.blackTurn ? qsTr("Black's turn") : qsTr("White's turn")
0070 
0071     if(state.gameOver()) {
0072         items.message = items.blackTurn ? qsTr("White wins") : qsTr("Black wins")
0073         items.gameOver = true
0074         if(!items.twoPlayer)
0075             if(items.blackTurn)
0076                 items.bonus.good('gnu')
0077             else
0078                 items.bonus.good('tux')
0079         else
0080             items.bonus.good('flower')
0081     }
0082 }
0083 
0084 function refresh() {
0085     items.blackTurn = (state.getTurn() == 'b')
0086     items.history = state.history()
0087     updateMessage()
0088     items.positions = 0 // Force a model reload
0089     items.positions = simplifiedState(state.position())
0090 }
0091 
0092 // Convert view position (QML) to the chess engine coordinate
0093 //
0094 // engine view:
0095 // |  01  02  03  04  05|
0096 // |06  07  08  09  10  |
0097 // |  11  12  13  14  15|
0098 // |16  17  18  19  20  |
0099 // |  21  22  23  24  25|
0100 // |26  27  28  29  30  |
0101 // |  31  32  33  34  35|
0102 // |36  37  38  39  40  |
0103 // |  41  42  43  44  45|
0104 // |46  47  48  49  50  |
0105 //
0106 // QML view:
0107 // |  91  93  95  97  99|
0108 // |80  82  84  86  88  |
0109 // |  71  73  75  77  79|
0110 // |60  62  64  66  68  |
0111 // |  51  53  55  57  59|
0112 // |40  42  44  46  48  |
0113 // |  31  33  35  37  39|
0114 // |20  22  24  26  28  |
0115 // |  11  13  15  17  19|
0116 // |00  02  04  06  08  |
0117 //
0118 function viewPosToEngine(pos) {
0119     var casesNumber = items.numberOfCases*items.numberOfCases
0120     var a = 10 * Math.floor((casesNumber - pos) / 10 + 1);
0121     var b = 20;
0122     if(pos % 10 !== 0) {
0123         b = (casesNumber - pos) % 10;
0124     }
0125     var newPos = (a - b + 1)
0126     newPos = Math.floor(newPos / 2 + 0.5)
0127     return newPos
0128 }
0129 
0130 // Convert chess engine coordinate to view position (QML)
0131 function engineToViewPos(pos) {
0132     var newPos = 90-10*Math.floor((2*pos-1)/10)+2*((pos-1)%5)+1+((-1+Math.pow(-1, Math.floor((2*pos-1)/10)))/2)
0133     return newPos
0134 }
0135 
0136 // move is the result from the engine move
0137 function visibleMove(move, from, to) {
0138     var piece = items.pieces.getPieceAt(from);
0139     items.pieces.moveTo(from, to, move)
0140 
0141     // promotion
0142     if (move.to <= 5 && piece.img === 'w')
0143         piece.promotion()
0144     else if (move.to >= 46 && piece.img === 'b')
0145         piece.promotion()
0146 }
0147 
0148 function findBestMove(currentState, depth, sign) {
0149     if(depth <= 0) {
0150         return getScore(currentState);
0151     }
0152 
0153     var moves = currentState.moves()
0154     if(moves.length === 0) {
0155         return [100, 0];
0156     }
0157     var bestScore = -1000;
0158     var bestScores;
0159     for(var move in moves) {
0160         currentState.move(moves[move]);
0161         var newScore = findBestMove(currentState, depth-1, -1.0*sign)
0162         currentState.undo()
0163         var score = sign*(newScore[0] - newScore[1])
0164         if(score > bestScore) {
0165             bestScore = score;
0166             bestScores = newScore;
0167         }
0168     }
0169     return bestScores
0170 }
0171 
0172 function computerMove() {
0173     var moves = state.moves()
0174     var bestScore = -1000
0175     var bestMoves = []
0176     var newState = new Engine.Draughts(state.fen())
0177     // 0 is b, 1 is b -> w, 2 is b -> w -> b guesses
0178     var depth = items.currentLevel === 5 ? 2 : 1;
0179 
0180     for(var move in moves) {
0181         newState.move(moves[move]);
0182         var newScore = findBestMove(newState, depth, 1)
0183         newState.undo()
0184         var score = newScore[1] - newScore[0]
0185         if(bestMoves.length == 0 || bestScore < score) {
0186             bestScore = score
0187             bestMoves = []
0188             bestMoves.push(moves[move])
0189         }
0190         else if(bestScore == score) {
0191             bestMoves.push(moves[move])
0192         }
0193     }
0194     bestMoves = Core.shuffle(bestMoves)
0195 
0196     var computer = bestMoves[0]
0197     var move = state.move({"from": computer.from, "to": computer.to})
0198     if(move) {
0199         visibleMove(move, engineToViewPos(computer.from), engineToViewPos(computer.to))
0200     }
0201     return move
0202 }
0203 
0204 function moveTo(from, to) {
0205     items.noPieceAnimation = false
0206     var move = state.move({"from": viewPosToEngine(from), "to": viewPosToEngine(to)})
0207     if(move) {
0208         visibleMove(move, from, to)
0209         clearAcceptMove()
0210         items.redo_stack = []
0211         if(!items.twoPlayer && !state.gameOver())
0212             items.trigComputerMove.start()
0213         items.from = -1;
0214     } else {
0215         // Probably a check makes the move is invalid
0216         updateMessage(move)
0217     }
0218     return move
0219 }
0220 
0221 function undo() {
0222     var move = state.undo();
0223     var redo_stack = items.redo_stack
0224     redo_stack.push(move)
0225     // In computer mode, the white always starts, take care
0226     // of undo after a mate which requires us to revert on
0227     // a white play
0228     if(!items.twoPlayer && state.getTurn() === 'b') {
0229         move = state.undo();
0230         redo_stack.push(move)
0231     }
0232     items.redo_stack = redo_stack
0233     refresh()
0234 }
0235 
0236 function moveByEngine(engineMove) {
0237     items.noPieceAnimation = false
0238     if(!engineMove)
0239         return
0240     var move = state.move(engineMove)
0241     visibleMove(move, engineToViewPos(engineMove.from), engineToViewPos(engineMove.to))
0242 }
0243 
0244 function redo() {
0245     var redo_stack = items.redo_stack
0246     var move = redo_stack.pop()
0247     moveByEngine(move)
0248    // In computer mode, the white always starts, take care
0249     // of undo after a mate which requires us to revert on
0250     // a white play
0251     if(!items.twoPlayer && state.getTurn() === 'b') {
0252         move = redo_stack.pop()
0253         moveByEngine(move)
0254     }
0255 
0256     // Force refresh
0257     items.redo_stack = []
0258     items.redo_stack = redo_stack
0259     clearAcceptMove()
0260 }
0261 
0262 // Random move depending on the level
0263 function randomMove() {
0264     if(!items.difficultyByLevel) {
0265         computerMove()
0266         return
0267     }
0268     // Disable random move if the situation is too bad for the user
0269     // This avoid having the computer playing bad against a user
0270     // with too few pieces making the game last too long
0271     var score = getScore(state)
0272     if(score[0] / score[1] < 0.7) {
0273         computerMove()
0274         return
0275     }
0276 
0277     // At level 2 we let the computer play 20% of the time
0278     // and 80% of the time we make a random move.
0279     if(Math.random() < items.currentLevel / (numberOfLevel - 1)) {
0280         computerMove()
0281         return
0282     }
0283     // Get all possible moves
0284     var moves = state.moves()
0285     moves = Core.shuffle(moves)
0286     var move = state.move(moves[0])
0287     if(move) {
0288         visibleMove(move, engineToViewPos(moves[0].from), engineToViewPos(moves[0].to))
0289     } else {
0290         // Bad move, should not happen
0291         computerMove()
0292     }
0293 }
0294 
0295 // Clear all accept move marker from the chessboard
0296 function clearAcceptMove() {
0297     for(var i=0; i < 50; ++i) {
0298         var square = items.squares.getSquareAt(engineToViewPos(i))
0299         if(square) {
0300             square['acceptMove'] = false
0301             square['jumpable'] = false
0302         }
0303     }
0304 }
0305 
0306 // Highlight the possible moves for the piece at position 'from'
0307 function showPossibleMoves(from) {
0308     var fromEngine = viewPosToEngine(from)
0309     var result = state.moves();
0310     clearAcceptMove()
0311     for(var i=0; i < result.length; ++i) {
0312         if(fromEngine === result[i].from) {
0313             var pos = engineToViewPos(result[i].to)
0314             items.squares.getSquareAt(pos)['acceptMove'] = true
0315             for(var v = 1; v < result[i].jumps.length; ++ v) {
0316                 items.squares.getSquareAt(engineToViewPos(result[i].jumps[v]))['jumpable'] = true
0317             }
0318         }
0319     }
0320 }
0321 
0322 // Calculate the score for black and white
0323 // Count the number of pieces with each piece having a given weight
0324 // Piece pawn queen
0325 // Value 1    4
0326 // @return [white, black]
0327 function getScore(board) {
0328     var white = 0
0329     var black = 0
0330     var queenScore = 4
0331     var position = board.position()
0332     
0333     for(var i = 0; i < position.length; ++i) {
0334         var img = position[i] !== '0' && position[i] !== '?' ? position[i] : ''
0335         if(img === '')
0336             continue;
0337         else if(img === 'w') {
0338             white ++;
0339         }
0340         else if(img === 'W') {
0341             white += queenScore;
0342         }
0343         if(img === 'b') {
0344             black ++;
0345         }
0346         else if(img === 'B') {
0347             black += queenScore;
0348         }
0349     }
0350     return [white, black]
0351 }