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 }