Warning, /education/gcompris/src/activities/chess/Chess.qml is written in an unsupported language. File is not indexed.

0001 /* GCompris - chess.qml
0002  *
0003  * SPDX-FileCopyrightText: 2015 Bruno Coudoin <bruno.coudoin@gcompris.net>
0004  *
0005  * Authors:
0006  *   Bruno Coudoin <bruno.coudoin@gcompris.net> (GTK+ version)
0007  *   Bruno Coudoin <bruno.coudoin@gcompris.net> (Qt Quick port)
0008  *   Timothée Giet <animtim@gmail.com> (big layout refactoring)
0009  *
0010  *   SPDX-License-Identifier: GPL-3.0-or-later
0011  */
0012 import QtQuick 2.12
0013 import GCompris 1.0
0014 
0015 import "../../core"
0016 import "."
0017 import "chess.js" as Activity
0018 
0019 ActivityBase {
0020     id: activity
0021 
0022     property bool acceptClick: true
0023     property bool buttonsBlocked: false
0024     onButtonsBlockedChanged: if(buttonsBlocked) unlockButtonBlock.restart()
0025     Timer {
0026         id: unlockButtonBlock
0027         interval: 400
0028         running: false
0029         repeat: false
0030         onTriggered: buttonsBlocked = false
0031     }
0032 
0033     property bool twoPlayers: false
0034     property bool displayTakenPiecesButton: true
0035     property int coordsOpacity: 1
0036     property int movesCount: 0
0037     // difficultyByLevel means that at level 1 computer is bad better at last level
0038     property bool difficultyByLevel: true
0039     property var fen: [
0040         ["initial state", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 1 1"],
0041         ["initial state", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 1 1"],
0042         ["initial state", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 1 1"],
0043         ["initial state", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 1 1"],
0044         ["initial state", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 1 1"]
0045     ]
0046 
0047     onStart: focus = true
0048     onStop: unlockButtonBlock.stop()
0049 
0050     pageComponent: Image {
0051         id: background
0052         anchors.fill: parent
0053         source: Activity.url + 'background-wood.svg'
0054         signal start
0055         signal stop
0056 
0057         Component.onCompleted: {
0058             activity.start.connect(start)
0059             activity.stop.connect(stop)
0060         }
0061 
0062         // Add here the QML items you need to access in javascript
0063         QtObject {
0064             id: items
0065             property Item main: activity.main
0066             property GCSfx audioEffects: activity.audioEffects
0067             property alias background: background
0068             property int currentLevel: activity.currentLevel
0069             property alias bonus: bonus
0070             property var barHeightAddon: ApplicationSettings.isBarHidden ? textMessage.height : bar.height
0071             property bool isPortrait: (background.height >= background.width)
0072             property int cellSize: boardBg.width * 0.1
0073             property var fen: activity.fen
0074             property bool twoPlayer: activity.twoPlayers
0075             property bool difficultyByLevel: activity.difficultyByLevel
0076             property var positions
0077             property var pieces: pieces
0078             property var squares: squares
0079             property var history
0080             property var redo_stack
0081             property alias redoTimer: redoTimer
0082             property int from
0083             property bool blackTurn
0084             property bool gameOver
0085             property string message
0086             property bool isWarningMessage
0087             property alias trigComputerMove: trigComputerMove
0088             property alias timerSwap: timerSwap
0089             property alias whiteTakenPieceModel: whiteTakenPieces.takenPiecesModel
0090             property alias blackTakenPieceModel: blackTakenPieces.takenPiecesModel
0091             property bool displayUndoAllDialog: false
0092             // Used to stop piece animation on board resize; set to true on board resize, and to false on any action that triggers a piece move
0093             property bool noPieceAnimation: false
0094         }
0095 
0096         onStart: { Activity.start(items) }
0097         onStop: { Activity.stop() }
0098 
0099         function performUndo() {
0100             if(items.history.length <= 0)
0101                 return
0102             Activity.undo()
0103             if(whiteTakenPieces.pushedLast[whiteTakenPieces.pushedLast.length-1] == movesCount) {
0104                 whiteTakenPieces.pushedLast.pop()
0105                 whiteTakenPieces.takenPiecesModel.remove(whiteTakenPieces.takenPiecesModel.count-1)
0106             }
0107             if(!items.twoPlayer) {
0108                 movesCount--
0109             }
0110             if(blackTakenPieces.pushedLast[blackTakenPieces.pushedLast.length-1] == movesCount) {
0111                 blackTakenPieces.pushedLast.pop()
0112                 blackTakenPieces.takenPiecesModel.remove(blackTakenPieces.takenPiecesModel.count-1)
0113             }
0114             movesCount--
0115         }
0116 
0117         Loader {
0118             id: undoAllDialogLoader
0119             sourceComponent: GCDialog {
0120                 parent: activity
0121                 isDestructible: false
0122                 //: Translators: undo all the moves in chess activity
0123                 message: qsTr("Do you really want to undo all the moves?")
0124                 button1Text: qsTr("Yes")
0125                 button2Text: qsTr("No")
0126                 onClose: items.displayUndoAllDialog = false
0127                 onButton1Hit: {
0128                     stop()
0129                     while(items.history.length > 0)
0130                         performUndo()
0131                 }
0132                 onButton2Hit: {}
0133             }
0134             anchors.fill: parent
0135             focus: true
0136             active: items.displayUndoAllDialog
0137             onStatusChanged: if (status == Loader.Ready) item.start()
0138         }
0139 
0140         GCText {
0141             id: textMessage
0142             z: 20
0143             color: items.isWarningMessage ? "red" : "white"
0144             anchors.horizontalCenter: parent.horizontalCenter
0145             anchors.top: parent.top
0146             width: layoutArea.width
0147             fontSize: smallSize
0148             text: items.message
0149             horizontalAlignment: Text.AlignHCenter
0150             wrapMode: TextEdit.WordWrap
0151         }
0152 
0153         Grid {
0154             id: controls
0155             z: 20
0156             spacing: (boardBg.width - items.cellSize * 4) * 0.25
0157             columns: items.isPortrait ? 4 : 1
0158             horizontalItemAlignment: Grid.AlignHCenter
0159             verticalItemAlignment: Grid.AlignVCenter
0160 
0161             GCTimerButton {
0162                 id: undo
0163                 height: items.cellSize
0164                 width: items.cellSize
0165                 enabled: !buttonsBlocked && opacity == 1
0166                 opacity: items.history.length > 0 ? 1 : 0
0167                 Image {
0168                     source: Activity.url + 'undo.svg'
0169                     height: items.cellSize
0170                     width: items.cellSize
0171                     sourceSize.height: items.cellSize
0172                     fillMode: Image.PreserveAspectFit
0173                 }
0174 
0175                 onClicked: {
0176                     if(!items.displayUndoAllDialog)
0177                         performUndo()
0178                 }
0179                 onPressAndHold: {
0180                     items.displayUndoAllDialog = true
0181                 }
0182                 Behavior on opacity {
0183                     PropertyAnimation {
0184                         easing.type: Easing.InQuad
0185                         duration: 200
0186                     }
0187                 }
0188             }
0189 
0190             GCButton {
0191                 id: redo
0192                 height: items.cellSize
0193                 width: items.cellSize
0194                 text: "";
0195                 theme: "noStyle"
0196                 onClicked: {
0197                     buttonsBlocked = true
0198                     if (!twoPlayers) {
0199                         acceptClick = false;
0200                         Activity.redo()
0201                     } else {
0202                         Activity.redo()
0203                     }
0204                 }
0205                 enabled: !buttonsBlocked && opacity == 1
0206                 opacity: items.redo_stack.length > 0 && acceptClick ? 1 : 0
0207                 Image {
0208                     source: Activity.url + 'redo.svg'
0209                     height: items.cellSize
0210                     width: items.cellSize
0211                     sourceSize.height: items.cellSize
0212                     fillMode: Image.PreserveAspectFit
0213                 }
0214                 Behavior on opacity {
0215                     PropertyAnimation {
0216                         easing.type: Easing.InQuad
0217                         duration: 200
0218                     }
0219                 }
0220             }
0221 
0222             GCButton {
0223                 id: drawerButton
0224                 height: items.cellSize
0225                 width: items.cellSize
0226                 text: "";
0227                 theme: "noStyle"
0228 
0229                 onClicked: {
0230                     whiteTakenPieces.open = !whiteTakenPieces.open
0231                     blackTakenPieces.open = !blackTakenPieces.open
0232                 }
0233 
0234                 enabled: !buttonsBlocked && displayTakenPiecesButton
0235                 opacity: displayTakenPiecesButton ? 1 : 0
0236                 Image {
0237                     source: Activity.url + 'captured.svg'
0238                     height: items.cellSize
0239                     width: items.cellSize
0240                     sourceSize.height: items.cellSize
0241                     fillMode: Image.PreserveAspectFit
0242                 }
0243                 Behavior on opacity {
0244                     PropertyAnimation {
0245                         easing.type: Easing.InQuad
0246                         duration: 200
0247                     }
0248                 }
0249             }
0250 
0251             GCButton {
0252                 height: items.cellSize
0253                 width: items.cellSize
0254                 text: "";
0255                 theme: "noStyle"
0256                 enabled: !timerSwap.running && items.twoPlayer
0257                 opacity: items.twoPlayer
0258                 Image {
0259                     source: Activity.url + 'turn.svg'
0260                     height: items.cellSize
0261                     width: items.cellSize
0262                     sourceSize.height: items.cellSize
0263                     fillMode: Image.PreserveAspectFit
0264                 }
0265                 onClicked: chessboard.swap()
0266             }
0267         }
0268 
0269         Rectangle {
0270             id: layoutArea
0271             width: background.width
0272             height: background.height - textMessage.height - items.barHeightAddon * 1.1
0273             opacity: 0
0274             anchors.horizontalCenter: background.horizontalCenter
0275         }
0276 
0277         Rectangle {
0278             id: controlsArea
0279             anchors.left: background.left
0280             anchors.right: boardBg.left
0281             anchors.top: boardBg.top
0282             anchors.bottom: boardBg.bottom
0283             opacity: 0
0284         }
0285 
0286         states: [
0287             State {
0288                 name: "portraitLayout"; when: items.isPortrait
0289                 PropertyChanges {
0290                     target: layoutArea
0291                     width: background.width * 0.86
0292                     height: background.height - textMessage.height - bar.height * 1.1
0293                 }
0294                 PropertyChanges {
0295                     target: controls
0296                     width:layoutArea.width
0297                     height: items.cellSize * 1.2
0298                     anchors.leftMargin: controls.spacing * 0.5
0299                     anchors.topMargin: 0
0300                     anchors.horizontalCenterOffset: 0
0301                 }
0302                 PropertyChanges {
0303                     target: boardBg
0304                     anchors.verticalCenterOffset: items.cellSize * -0.6
0305                 }
0306                 AnchorChanges {
0307                     target: layoutArea
0308                     anchors.top: controls.bottom
0309                 }
0310                 AnchorChanges {
0311                     target: controls
0312                     anchors.top: textMessage.bottom
0313                     anchors.horizontalCenter: undefined
0314                     anchors.left: boardBg.left
0315                 }
0316             },
0317             State {
0318                 name: "horizontalLayout"; when: !items.isPortrait
0319                 PropertyChanges {
0320                     target: layoutArea
0321                     width: background.width
0322                     height: background.height - textMessage.height - items.barHeightAddon * 1.1
0323                 }
0324                 PropertyChanges {
0325                     target: controls
0326                     width: items.cellSize * 1.2
0327                     height: layoutArea.height
0328                     anchors.leftMargin: 0
0329                     anchors.topMargin: controls.spacing * 0.5
0330                     anchors.horizontalCenterOffset: items.cellSize * 0.8
0331                 }
0332                 PropertyChanges {
0333                     target: boardBg
0334                     anchors.verticalCenterOffset: 0
0335                 }
0336                 AnchorChanges {
0337                     target: layoutArea
0338                     anchors.top: textMessage.bottom
0339                 }
0340                 AnchorChanges {
0341                     target: controls
0342                     anchors.top: controlsArea.top
0343                     anchors.horizontalCenter: controlsArea.horizontalCenter
0344                     anchors.left: undefined
0345                 }
0346             }
0347         ]
0348 
0349         Rectangle {
0350             id: boardBg
0351             width: Math.min(layoutArea.width, layoutArea.height)
0352             height: boardBg.width
0353             anchors.centerIn: layoutArea
0354             z: 08
0355             color: "#452501"
0356             onWidthChanged: items.noPieceAnimation = true
0357 
0358             // The chessboard
0359             GridView {
0360                 id: chessboard
0361                 cellWidth: items.cellSize
0362                 cellHeight: items.cellSize
0363                 width: items.cellSize * 8
0364                 height: chessboard.width
0365                 interactive: false
0366                 keyNavigationWraps: true
0367                 model: 64
0368                 layoutDirection: Qt.RightToLeft
0369                 delegate: square
0370                 rotation: 180
0371                 z: 10
0372                 anchors.centerIn: boardBg
0373 
0374                 Component {
0375                     id: square
0376                     Image {
0377                         source: index % 2 + (Math.floor(index / 8) % 2) == 1 ?
0378                         Activity.url + 'chess-white.svg' : Activity.url + 'chess-black.svg';
0379                         width: items.cellSize
0380                         height: items.cellSize
0381                     }
0382                 }
0383 
0384                 Behavior on rotation { PropertyAnimation { easing.type: Easing.InOutQuad; duration: 1400 } }
0385 
0386                 function swap() {
0387                     items.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/flip.wav')
0388                     coordsOpacity = 0
0389                     timerSwap.start()
0390                     if(chessboard.rotation == 180)
0391                         chessboard.rotation = 0
0392                         else
0393                             chessboard.rotation = 180
0394                 }
0395             }
0396 
0397             Timer {
0398                 id: timerSwap
0399                 interval: 1500
0400                 running: false
0401                 repeat: false
0402                 onTriggered: coordsOpacity = 1
0403             }
0404 
0405             Item {
0406                 id: letters
0407                 anchors.left: chessboard.left
0408                 anchors.top: chessboard.bottom
0409                 opacity: coordsOpacity
0410                 Behavior on opacity { PropertyAnimation { easing.type: Easing.InOutQuad; duration: 500} }
0411                 Repeater {
0412                     id: lettersA
0413                     model: chessboard.rotation == 0 ? ["H", "G", "F", "E", "D", "C", "B", "A"] : ["A", "B", "C", "D", "E", "F", "G", "H"]
0414                     GCText {
0415                         x: items.cellSize * (index % 8) + (items.cellSize/2-width/2)
0416                         y: items.cellSize * Math.floor(index / 8)
0417                         text: modelData
0418                         color: "#CBAE7B"
0419                         font.pointSize: NaN
0420                         font.pixelSize: items.cellSize * 0.5
0421                     }
0422                 }
0423             }
0424             Item {
0425                 id: numbers
0426                 anchors.left: chessboard.right
0427                 anchors.top: chessboard.top
0428                 opacity: coordsOpacity
0429                 Behavior on opacity { PropertyAnimation { easing.type: Easing.InOutQuad; duration: 500} }
0430                 Repeater {
0431                     model: chessboard.rotation == 0 ? ["1", "2", "3", "4", "5", "6", "7", "8"] : ["8", "7", "6", "5", "4", "3", "2", "1"]
0432                     GCText {
0433                         x: items.cellSize * Math.floor(index / 8) + width
0434                         y: items.cellSize * (index % 8) + (items.cellSize/2-height/2)
0435                         text: modelData
0436                         color: "#CBAE7B"
0437                         font.pointSize: NaN
0438                         font.pixelSize: items.cellSize * 0.5
0439                     }
0440                 }
0441             }
0442 
0443             Rectangle {
0444                 id: boardBorder
0445                 width: items.cellSize * 10
0446                 height: boardBorder.width
0447                 anchors.centerIn: boardBg
0448                 z: -1
0449                 color: "#542D0F"
0450                 border.color: "#3A1F0A"
0451                 border.width: items.cellSize * 0.1
0452             }
0453         }
0454 
0455         Repeater {
0456             id: squares
0457             model: items.positions
0458             delegate: square
0459             parent: chessboard
0460 
0461             DropArea {
0462                 id: square
0463                 x: items.cellSize * (7 - pos % 8) + spacing / 2
0464                 y: items.cellSize * Math.floor(pos / 8) + spacing / 2
0465                 width: items.cellSize - spacing
0466                 height: square.width
0467                 z: 1
0468                 keys: acceptMove ? ['acceptMe'] : ['sorryNo']
0469                 property bool acceptMove : false
0470                 property int pos: modelData.pos
0471                 property int spacing: 6 * ApplicationInfo.ratio
0472                 Rectangle {
0473                     id: possibleMove
0474                     anchors.fill: parent
0475                     color: parent.containsDrag ? '#803ACAFF' : 'transparent'
0476                     border.width: parent.acceptMove ? 5 : 0
0477                     border.color: '#FF4EB271'
0478                     z: 1
0479                 }
0480             }
0481 
0482             function getSquareAt(pos) {
0483                 for(var i=0; i < squares.count; i++) {
0484                     if(squares.itemAt(i).pos === pos)
0485                         return squares.itemAt(i)
0486                 }
0487                 return(undefined)
0488             }
0489         }
0490 
0491         Repeater {
0492             id: pieces
0493             model: items.positions
0494             delegate: piece
0495             parent: chessboard
0496 
0497             Piece {
0498                 id: piece
0499                 sourceSize.width: items.cellSize
0500                 width: items.cellSize - spacing
0501                 height: piece.width
0502                 source: img ? Activity.url + img + '.svg' : ''
0503                 img: modelData.img
0504                 x: items.cellSize * (7 - pos % 8) + spacing / 2
0505                 y: items.cellSize * Math.floor(pos / 8) + spacing / 2
0506                 z: 1
0507                 pos: modelData.pos
0508                 newPos: modelData.pos
0509                 rotation: -chessboard.rotation
0510 
0511                 property int spacing: 6 * ApplicationInfo.ratio
0512 
0513                 Drag.active: dragArea.drag.active
0514                 Drag.hotSpot.x: width / 2
0515                 Drag.hotSpot.y: height / 2
0516 
0517                 MouseArea {
0518                     id: dragArea
0519                     anchors.fill: parent
0520                     enabled: !items.gameOver && !items.trigComputerMove.running
0521                     drag.target: ((items.blackTurn && !parent.isWhite) || (!items.blackTurn && parent.isWhite)) ?
0522                                 parent : null
0523                     onPressed: {
0524                         piece.Drag.keys = ['acceptMe']
0525                         parent.z = 100
0526                         if(parent.isWhite == 1 && !items.blackTurn ||
0527                                 parent.isWhite == 0 && items.blackTurn) {
0528                             items.from = parent.newPos
0529                             Activity.showPossibleMoves(items.from)
0530                         } else if(items.from != -1 && squares.getSquareAt(parent.newPos)['acceptMove']) {
0531                             Activity.moveTo(items.from, parent.newPos)
0532                         }
0533                     }
0534                     onReleased: {
0535                         // If no target or move not possible, we reset the position
0536                         if(!piece.Drag.target || (items.from != -1 && !Activity.moveTo(items.from, piece.Drag.target.pos))) {
0537                             var pos = parent.pos
0538                             // Force recalc of the old x,y position
0539                             parent.pos = -1
0540                             pieces.getPieceAt(pos).move(pos)
0541                         }
0542                     }
0543                 }
0544             }
0545 
0546             function moveTo(from, to) {
0547                 movesCount ++
0548                 var fromPiece = getPieceAt(from)
0549                 var toPiece = getPieceAt(to)
0550 
0551                 // Specific case for en passant move. It is a case where
0552                 // we capture a pawn without having it at the "to" position.
0553                 // To know if we captured, we browse the whole board to look 
0554                 // for missing pawn
0555                 var state = Activity.simplifiedState(Activity.state.board)
0556                 for(var i=0; i < state.length; ++i) {
0557                     var pos = state[i].pos
0558                     var pawnPiece = getPieceAt(pos)
0559                     if(pos != from && state[i].img === "" &&
0560                        pawnPiece && pawnPiece.img[1] === 'p') {
0561                            items.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/smudge.wav')
0562                            if(pawnPiece.isWhite) {
0563                                whiteTakenPieces.takenPiecesModel.append(pawnPiece)
0564                                whiteTakenPieces.pushedLast.push(movesCount)
0565                            } else {
0566                                blackTakenPieces.takenPiecesModel.append(pawnPiece)
0567                                blackTakenPieces.pushedLast.push(movesCount)
0568                            }
0569                            pawnPiece.hide(pawnPiece.pos)
0570                            break
0571                     }
0572                 }
0573 
0574                 if(toPiece.img !== '') {
0575                     items.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/smudge.wav')
0576                     if(toPiece.isWhite) {
0577                         whiteTakenPieces.takenPiecesModel.append(toPiece)
0578                         whiteTakenPieces.pushedLast.push(movesCount)
0579                     } else {
0580                         blackTakenPieces.takenPiecesModel.append(toPiece)
0581                         blackTakenPieces.pushedLast.push(movesCount)
0582                     }
0583                 }
0584                 else
0585                     items.audioEffects.play('qrc:/gcompris/src/core/resource/sounds/scroll.wav')
0586                 toPiece.hide(from)
0587                 fromPiece.move(to)
0588             }
0589 
0590             function promotion(to) {
0591                 var toPiece = getPieceAt(to)
0592                 toPiece.promotion()
0593             }
0594 
0595             function getPieceAt(pos) {
0596                 for(var i=0; i < pieces.count; i++) {
0597                     if(pieces.itemAt(i).newPos === pos)
0598                         return pieces.itemAt(i)
0599                 }
0600                 return(undefined)
0601             }
0602         }
0603 
0604         Timer {
0605             id: trigComputerMove
0606             repeat: false
0607             interval: 400
0608             onTriggered: Activity.randomMove()
0609         }
0610 
0611         // Use to redo the computer move after the user move
0612         Timer {
0613             id: redoTimer
0614             repeat: false
0615             interval: 400
0616             onTriggered: {
0617                 acceptClick = true;
0618                 Activity.moveByEngine(move)
0619             }
0620             property var move
0621 
0622             function moveByEngine(engineMove) {
0623                 move = engineMove
0624                 redoTimer.start()
0625             }
0626         }
0627 
0628         TakenPiecesList {
0629             id: whiteTakenPieces
0630             width: items.cellSize * 0.8
0631             edge: false
0632         }
0633         TakenPiecesList {
0634             id: blackTakenPieces
0635             width: items.cellSize * 0.8
0636             edge: true
0637         }
0638 
0639         DialogHelp {
0640             id: dialogHelp
0641             onClose: home()
0642         }
0643 
0644         Bar {
0645             id: bar
0646             level: items.currentLevel + 1
0647             content: BarEnumContent { value: help | home | (items.twoPlayer ? 0 : level) |
0648                                              (items.twoPlayer && !items.gameOver ? 0 : reload) }
0649 
0650             onHelpClicked: {
0651                 displayDialog(dialogHelp)
0652             }
0653             onPreviousLevelClicked: Activity.previousLevel()
0654             onNextLevelClicked: Activity.nextLevel()
0655             onHomeClicked: activity.home()
0656             onReloadClicked: {
0657                 trigComputerMove.stop()
0658                 Activity.initLevel()
0659             }
0660         }
0661 
0662         Bonus {
0663             id: bonus
0664         }
0665     }
0666 
0667 }