Warning, /plasma/kdeplasma-addons/applets/fifteenPuzzle/package/contents/ui/FifteenPuzzle.qml is written in an unsupported language. File is not indexed.

0001 /*
0002  * SPDX-FileCopyrightText: 2014 Jeremy Whiting <jpwhiting@kde.org>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 import QtQuick 2.15
0008 import QtQuick.Layouts 1.15
0009 
0010 import org.kde.coreaddons 1.0 as KCoreAddons
0011 import org.kde.plasma.core as PlasmaCore
0012 import org.kde.kirigami 2.20 as Kirigami
0013 import org.kde.plasma.components 3.0 as PlasmaComponents3
0014 import org.kde.plasma.plasmoid 2.0
0015 
0016 Item {
0017     id: main
0018 
0019     Layout.preferredWidth: Math.max(boardSize * 50, controlsRow.width)
0020     Layout.preferredHeight: boardSize * 50 + controlsRow.height
0021 
0022     readonly property int boardSize: Plasmoid.configuration.boardSize
0023     property Component piece: Piece {}
0024     property var pieces: []
0025     property int currentPosition: -1
0026 
0027     property int seconds: 0
0028 
0029     Keys.onPressed: {
0030         let newPosition = main.currentPosition;
0031         switch (event.key) {
0032         case Qt.Key_Up: {
0033             if (main.currentPosition < 0) {
0034                 newPosition = (main.boardSize - 1) * main.boardSize;  // Start from bottom
0035             } else if (main.currentPosition >= main.boardSize) {
0036                 newPosition = main.currentPosition - main.boardSize;
0037             }
0038             if (pieces[newPosition].empty) {
0039                 if (main.currentPosition < 0) {
0040                     newPosition = (main.boardSize - 2) * main.boardSize;
0041                 } else if (newPosition >= main.boardSize) {
0042                     newPosition -= main.boardSize;
0043                 }
0044             }
0045             break;
0046         }
0047         case Qt.Key_Down: {
0048             if (main.currentPosition < 0) {
0049                 newPosition = 0;  // Start from top
0050             } else if (main.currentPosition < main.boardSize * (main.boardSize - 1)) {
0051                 newPosition = main.currentPosition + main.boardSize;
0052             }
0053             if (pieces[newPosition].empty) {
0054                 if (main.currentPosition < 0) {
0055                     newPosition = main.boardSize;
0056                 } else if (newPosition < main.boardSize * (main.boardSize - 1)) {
0057                     newPosition += main.boardSize;
0058                 }
0059             }
0060             break;
0061         }
0062         case Qt.Key_Left: {
0063             if (main.currentPosition < 0) {
0064                 newPosition = main.boardSize - 1;  // Start from right
0065             } else if (main.currentPosition % main.boardSize) {
0066                 newPosition = main.currentPosition - 1;
0067             }
0068             if (pieces[newPosition].empty) {
0069                 if (main.currentPosition < 0) {
0070                     newPosition = main.boardSize - 2;
0071                 } else if (newPosition % main.boardSize) {
0072                     newPosition -= 1;
0073                 }
0074             }
0075             break;
0076         }
0077         case Qt.Key_Right: {
0078             if (main.currentPosition < 0) {
0079                 newPosition = 0;  // Start from left
0080             } else if ((main.currentPosition + 1) % main.boardSize) {
0081                 newPosition = main.currentPosition + 1;
0082             }
0083             if (pieces[newPosition].empty) {
0084                 if (main.currentPosition < 0) {
0085                     newPosition = 1;
0086                 } else if ((newPosition + 1) % main.boardSize) {
0087                     newPosition += 1;
0088                 }
0089             }
0090             break;
0091         }
0092         default:
0093             return;
0094         }
0095 
0096         // Edge empty case: don't move
0097         if (pieces[newPosition].empty) {
0098             newPosition = main.currentPosition;
0099         }
0100 
0101         pieces[newPosition].forceActiveFocus();
0102         event.accepted = true;
0103     }
0104 
0105     function fillBoard() {
0106         // Clear out old board
0107         for (const piece of pieces) {
0108             piece.destroy();
0109         }
0110         main.currentPosition = -1;
0111 
0112         pieces = [];
0113         const count = boardSize * boardSize;
0114         if (piece.status === Component.Ready) {
0115             for (let i = 0; i < count; ++i) {
0116                 const newPiece = piece.createObject(mainGrid, {"number": i, "position": i });
0117                 pieces[i] = newPiece;
0118                 newPiece.activeFocusChanged.connect(() => {
0119                     if (newPiece.activeFocus) {
0120                         main.currentPosition = newPiece.position;
0121                     }
0122                 });
0123                 newPiece.activated.connect(pieceClicked);
0124             }
0125             shuffleBoard();
0126         }
0127     }
0128 
0129     function shuffleBoard() {
0130         // Hide the solved rectangle in case it was visible
0131         solvedRect.visible = false;
0132         main.seconds = 0;
0133         main.currentPosition = -1;
0134 
0135         const count = boardSize * boardSize;
0136         for (let i = count - 1; i >= 0; --i) {
0137             // choose a random number such that 0 <= rand <= i
0138             const rand = Math.floor(Math.random() * 10) % (i + 1);
0139             swapPieces(i, rand);
0140         }
0141 
0142         // make sure the new board is solveable
0143 
0144         // count the number of inversions
0145         // an inversion is a pair of tiles at positions a, b where
0146         // a < b but value(a) > value(b)
0147 
0148         // also count the number of lines the blank tile is from the bottom
0149         let inversions = 0;
0150         let blankRow = -1;
0151         for (let i = 0; i < count; ++i) {
0152             if (pieces[i].empty) {
0153                 blankRow = Math.floor(i / boardSize);
0154                 continue;
0155             }
0156             for (let j = 0; j < i; ++j) {
0157                 if (pieces[j].empty) {
0158                     continue;
0159                 }
0160                 if (pieces[i].number < pieces[j].number) {
0161                     ++inversions;
0162                 }
0163             }
0164         }
0165 
0166         if (blankRow === -1) {
0167             console.log("Unable to find row of blank tile");
0168         }
0169 
0170         // we have a solveable board if:
0171         // size is odd:  there are an even number of inversions
0172         // size is even: the number of inversions is odd if and only if
0173         //               the blank tile is on an odd row from the bottom-
0174         const sizeMod2 = Math.floor(boardSize % 2);
0175         const inversionsMod2 = Math.floor(inversions % 2);
0176         const solveable = (sizeMod2 === 1 && inversionsMod2 === 0) ||
0177                           (sizeMod2 === 0 && (inversionsMod2 === 0) === (Math.floor((boardSize - blankRow) % 2) === 1));
0178         if (!solveable) {
0179             // make the grid solveable by swapping two adjacent pieces around
0180             let pieceA = 0;
0181             let pieceB = 1;
0182             if (pieces[pieceA].empty) {
0183                 pieceA = boardSize + 1;
0184             } else if (pieces[pieceB].empty) {
0185                 pieceB = boardSize;
0186             }
0187             swapPieces(pieceA, pieceB);
0188         }
0189         secondsTimer.stop();
0190     }
0191 
0192     // recursive function: performs swap and returns true when it has found an
0193     // empty piece in the direction given by deltas.
0194     function swapWithEmptyPiece(position, deltaRow, deltaColumn): bool {
0195         const row = Math.floor(position / boardSize);
0196         const column = position % boardSize;
0197 
0198         const nextRow = row + deltaRow;
0199         const nextColumn = column + deltaColumn;
0200         const nextPosition = nextRow * boardSize + nextColumn;
0201 
0202         if (nextRow < 0 || nextRow >= boardSize || nextColumn < 0 || nextColumn >= boardSize) {
0203             return false;
0204         }
0205 
0206         if (pieces[nextPosition].empty || swapWithEmptyPiece(nextPosition, deltaRow, deltaColumn)) {
0207             swapPieces(position, nextPosition);
0208             return true;
0209         }
0210 
0211         return false;
0212     }
0213 
0214     function pieceClicked(position) {
0215         // deltas: up, down, left, right
0216         for (const [row, col] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
0217             // stop at first direction that has (or rather "had" at this point) the empty piece
0218             if (swapWithEmptyPiece(position, row, col)) {
0219                 main.currentPosition += col + main.boardSize * row;
0220                 break;
0221             }
0222         }
0223         secondsTimer.start();
0224         checkSolved();
0225     }
0226 
0227     function checkSolved() {
0228         const count = boardSize * boardSize;
0229         for (let i = 0; i < count - 2; ++i) {
0230             if (pieces[i].number > pieces[i + 1].number) {
0231                 // Not solved.
0232                 return;
0233             }
0234         }
0235         solved();
0236     }
0237 
0238     function solved() {
0239         // Show a message that it was solved.
0240         console.log("Puzzle was solved");
0241         solvedRect.visible = true;
0242         // Stop the timer
0243         secondsTimer.stop();
0244     }
0245 
0246     function swapPieces(first, second) {
0247         const firstPiece = pieces[first];
0248         const secondPiece = pieces[second];
0249         let temp = firstPiece.position;
0250         firstPiece.position = secondPiece.position;
0251         secondPiece.position = temp;
0252         temp = pieces[first];
0253         pieces[first] = pieces[second];
0254         pieces[second] = temp;
0255     }
0256 
0257     function timerText() {
0258         return i18nc("The time since the puzzle started, in minutes and seconds",
0259                      "Time: %1", KCoreAddons.Format.formatDuration(seconds * 1000, KCoreAddons.FormatTypes.FoldHours));
0260     }
0261 
0262     Rectangle {
0263         id: mainGrid
0264         color: Kirigami.Theme.backgroundColor
0265         anchors {
0266             top: parent.top
0267             left: parent.left
0268             right: parent.right
0269             bottom: controlsRow.top
0270             bottomMargin: Kirigami.Units.smallSpacing
0271         }
0272 
0273         activeFocusOnTab: true
0274 
0275         onActiveFocusChanged: {
0276             // Move focus to the first non-empty piece
0277             if (activeFocus) {
0278                 if (main.currentPosition < 0) {
0279                     if (main.pieces[0].empty) {
0280                         main.pieces[1].forceActiveFocus();
0281                     } else {
0282                         main.pieces[0].forceActiveFocus();
0283                     }
0284                 } else {
0285                     main.pieces[main.currentPosition].forceActiveFocus();
0286                 }
0287             }
0288         }
0289     }
0290 
0291     RowLayout {
0292         id: controlsRow
0293         anchors {
0294             margins: Kirigami.Units.smallSpacing
0295             bottom: parent.bottom
0296             horizontalCenter: parent.horizontalCenter
0297         }
0298         PlasmaComponents3.Button {
0299             id: button
0300             Layout.fillWidth: true
0301             icon.name: "roll"
0302             text: i18nc("@action:button", "Shuffle");
0303             onClicked: main.shuffleBoard();
0304         }
0305 
0306         PlasmaComponents3.Label {
0307             id: timeLabel
0308             Layout.fillWidth: true
0309             text: main.timerText()
0310             textFormat: Text.PlainText
0311             color: Kirigami.Theme.textColor
0312         }
0313     }
0314 
0315     Rectangle {
0316         id: solvedRect
0317         visible: false
0318         anchors.fill: mainGrid
0319         color: Kirigami.Theme.backgroundColor
0320         z: 0
0321 
0322         Image {
0323             id: solvedImage
0324             anchors.fill: parent
0325             z: 1
0326             source: "image://fifteenpuzzle/" + boardSize + "-all-0-0-" + Plasmoid.configuration.imagePath
0327             visible: Plasmoid.configuration.useImage
0328             cache: false
0329             function update() {
0330                 const tmp = source;
0331                 source = "";
0332                 source = tmp;
0333             }
0334         }
0335 
0336         PlasmaComponents3.Label {
0337             id: solvedLabel
0338             anchors.centerIn: parent
0339             color: Kirigami.Theme.textColor
0340             text: i18nc("@info", "Solved! Try again.")
0341             textFormat: Text.PlainText
0342             z: 2
0343         }
0344     }
0345 
0346     Timer {
0347         id: secondsTimer
0348         interval: 1000
0349         repeat: true
0350 
0351         onTriggered: ++main.seconds
0352     }
0353 
0354     Connections {
0355         target: Plasmoid.configuration
0356         function onBoardSizeChanged() {
0357             main.fillBoard();
0358             solvedImage.update();
0359         }
0360     }
0361 
0362     Connections {
0363         target: Plasmoid.configuration
0364         function onImagePathChanged() {
0365             main.fillBoard();
0366             solvedImage.update();
0367         }
0368     }
0369 
0370     Component.onCompleted: {
0371         main.fillBoard();
0372         solvedImage.update();
0373     }
0374 }