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 }