File indexing completed on 2024-04-28 07:54:40
0001 /* 0002 SPDX-FileCopyrightText: 2008 Ian Wadham <iandw.au@gmail.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 // Local includes 0008 #include "game.h" 0009 #include "movetracker.h" 0010 #include "scenelabel.h" 0011 #include "kubrick_debug.h" 0012 0013 #include <KConfig> 0014 #include <KConfigGroup> 0015 #include <KLocalizedString> 0016 #include <KMessageBox> 0017 #include <KStandardAction> 0018 0019 #include <QFileDialog> 0020 #include <QRegularExpression> 0021 #include <QStandardPaths> 0022 #include <QTimer> 0023 0024 // Create the main game/document object 0025 Game::Game (Kubrick * parent) 0026 : QObject (parent), 0027 0028 smSelectionStart (0), 0029 smSelectionLength (0), 0030 0031 smMoveAxis (Z), 0032 smMoveSlice (0), 0033 smMoveDirection (CLOCKWISE), 0034 0035 random(QDateTime::currentMSecsSinceEpoch()), 0036 cubeAligned (true), 0037 moveIndex (-1) 0038 { 0039 myParent = parent; 0040 cube = nullptr; 0041 gameGLView = nullptr; // OpenGL view is not yet created. 0042 mainWindow = nullptr; // MW exists, but the GUI is not set up. 0043 0044 smInitInput(); // Initialise the move-text parsing. 0045 0046 setDefaults (); // Set all options to default values. 0047 restoreState (); // Restore the last cube and its state. 0048 demoPhase = false; // No demo yet. 0049 0050 moveTracker = new MoveTracker (myParent); 0051 0052 connect(moveTracker, &MoveTracker::newMove, this, &Game::addPlayersMove); 0053 connect(moveTracker, &MoveTracker::cubeRotated, this, &Game::setCubeNotAligned); 0054 0055 blinkStartTime = 300; 0056 } 0057 0058 0059 Game::~Game () 0060 { 0061 delete frontVL; 0062 delete backVL; 0063 delete demoL; 0064 qDeleteAll(cubeViews); 0065 qDeleteAll(moves); 0066 } 0067 0068 0069 void Game::initGame (GameGLView * glv, Kubrick * mw) 0070 { 0071 // OK, now we have an OpenGL view and all the GUI widgets in KXmlGuiWindow. 0072 gameGLView = glv; // Save OpenGL view, used when drawing. 0073 mainWindow = mw; // Save main window, shows status, etc. 0074 0075 // mainWindow->setToggle (QStringLiteral("toggle_tumbling"), tumbling); 0076 mainWindow->setToggle (QStringLiteral("watch_shuffling"), (bool) option [optViewShuffle]); 0077 mainWindow->setToggle (QStringLiteral("watch_moves"), (bool) option [optViewMoves]); 0078 gameGLView->setBevelAmount (option [optBevel]); 0079 gameGLView->setCursor (Qt::CrossCursor); 0080 0081 // Create the view-label texts for cube pictures. 0082 frontVL = new SceneLabel (i18n("Front View")); 0083 backVL = new SceneLabel (i18n("Back View")); 0084 0085 // Don't show them at startup time. 0086 frontVL-> setVisible (false); 0087 backVL-> setVisible (false); 0088 0089 // Create a label text for the demos. 0090 demoL = new SceneLabel (i18n("DEMO - Click anywhere to begin playing")); 0091 demoL-> setVisible (false); // Show it whenever the demo starts. 0092 0093 // Set the scene parameters for 1, 2 or 3 cubes: cube has ID, turnability, 0094 // size, position of centre, turn, tilt, label location and label widget. 0095 0096 setCubeView (1, TURNS, 1.1, 0.0, 0.0, -45.0, +40.0, 0, 0, NoLabel); 0097 0098 setCubeView (2, TURNS, 1.1, -0.25, 0.0, -20.0, +40.0, 2, 0, FrontLbl); 0099 setCubeView (2, TURNS, 1.1, +0.25, 0.0, 125.0, +30.0, 6, 0, BackLbl); 0100 0101 setCubeView (3, TURNS, 1.1, -0.17, 0.0, -30.0, +40.0, 0, 0, NoLabel); 0102 setCubeView (3, FIXED, 0.55, +0.33, +0.25, -60.0, +40.0, 7, 0, FrontLbl); 0103 setCubeView (3, FIXED, 0.55, +0.33, -0.25, 130.0, +40.0, 7, 4, BackLbl); 0104 0105 // Create the first cube and picture here, otherwise paintGL() will 0106 // die horribly when the main window calls it during startup. 0107 0108 saveState (); 0109 0110 // This will set the scene, clear animation variables and do no more. 0111 currentSceneID = 0; // Forces startAnimation() to initialise the scene. 0112 startAnimation (QString(), option [optSceneID], false, false); 0113 0114 qDeleteAll(moves); 0115 moves.clear(); 0116 shuffleMoves = 0; 0117 playerMoves = 0; 0118 0119 startDemo (); // Start the demo. 0120 randomDemo (); // Main window has shown "Welcome". 0121 0122 // Implement game ticks [SLOT(advance()) does most of the work in Kubrick]. 0123 QTimer* timer = new QTimer (this); 0124 nTick = 0; 0125 connect(timer, &QTimer::timeout, this, &Game::advance); 0126 0127 timer->start (20); // Tick interval is 20 msec. 0128 } 0129 0130 0131 /******************************************************************************/ 0132 /***************************** PUBLIC SLOTS ***********************************/ 0133 /******************************************************************************/ 0134 0135 void Game::newPuzzle () 0136 { 0137 if (demoPhase) { 0138 toggleDemo(); 0139 } 0140 else if (tooBusy()) { 0141 return; 0142 } 0143 if (shuffleMoves <= 0) { 0144 KMessageBox::information (myParent, 0145 i18n("Sorry, the cube cannot be shuffled at the moment. The " 0146 "number of shuffling moves is set to zero. Please select " 0147 "your number of shuffling moves in the options dialog at " 0148 "menu item Game->Choose Puzzle Type->Make Your Own..."), 0149 i18n("New Puzzle")); 0150 } 0151 0152 // Create a new cube, with the same options as before, then shuffle it. 0153 newCube (cubeSize [X], cubeSize [Y], cubeSize [Z], shuffleMoves); 0154 } 0155 0156 0157 void Game::load () 0158 { 0159 if (demoPhase) { 0160 toggleDemo(); 0161 } 0162 else if (tooBusy()) { 0163 return; 0164 } 0165 QString loadFilename = QFileDialog::getOpenFileName(myParent, i18n("Load Puzzle"), 0166 QString(), i18n("Kubrick Game Files (*.kbk)")); 0167 if (loadFilename.isNull()) { 0168 return; 0169 } 0170 0171 KConfig config (loadFilename, KConfig::SimpleConfig); 0172 0173 if (! config.hasGroup (QStringLiteral("KubrickGame"))) { 0174 KMessageBox::error(mainWindow, 0175 i18n("The file '%1' is not a valid Kubrick game-file.", loadFilename)); 0176 return; 0177 } 0178 0179 loadPuzzle (config); 0180 } 0181 0182 0183 void Game::save () 0184 { 0185 doSave (false); // Use previous file name, if available. 0186 } 0187 0188 0189 void Game::saveAs () 0190 { 0191 doSave (true); // Use the file dialog to get a name. 0192 } 0193 0194 0195 void Game::changePuzzle (const Kubrick::PuzzleItem & puzzle) 0196 { 0197 if (demoPhase) { 0198 toggleDemo(); 0199 } 0200 else if (tooBusy()) { 0201 return; 0202 } 0203 // Set the options for the new puzzle-type. 0204 option [optXDim] = puzzle.x; 0205 option [optYDim] = puzzle.y; 0206 option [optZDim] = puzzle.z; 0207 option [optShuffleMoves] = puzzle.shuffleMoves; 0208 option [optViewShuffle] = puzzle.viewShuffle; 0209 option [optViewMoves] = puzzle.viewMoves; 0210 mainWindow->setToggle (QStringLiteral("watch_shuffling"), (bool) option [optViewShuffle]); 0211 mainWindow->setToggle (QStringLiteral("watch_moves"), (bool) option [optViewMoves]); 0212 0213 // Build the new cube and shuffle it. 0214 newCube (option [optXDim], option [optYDim], option [optZDim], 0215 option [optShuffleMoves]); 0216 } 0217 0218 0219 void Game::newCubeDialog () 0220 { 0221 if (demoPhase) { 0222 toggleDemo(); 0223 } 0224 if (doOptionsDialog (true) == QDialog::Accepted) { 0225 newCube (option [optXDim], option [optYDim], option [optZDim], 0226 option [optShuffleMoves]); 0227 mainWindow->describePuzzle (option [optXDim], option [optYDim], 0228 option [optZDim], option [optShuffleMoves]); 0229 } 0230 } 0231 0232 0233 void Game::undoMove () 0234 { 0235 startUndo (QStringLiteral("u"), i18n("Undo")); 0236 } 0237 0238 0239 void Game::redoMove () 0240 { 0241 startRedo (QStringLiteral("r"), i18n("Redo")); 0242 } 0243 0244 0245 void Game::solveCube () 0246 { 0247 if (tooBusy()) 0248 return; 0249 if (shuffleMoves <= 0) { 0250 KMessageBox::information (myParent, 0251 i18n("This cube has not been shuffled, so there is " 0252 "nothing to solve."), 0253 i18n("Solve the Cube")); 0254 return; 0255 } 0256 0257 if (smMoveToComplete()) { 0258 // There is an incomplete Singmaster move, so undo that first. 0259 smInitInput(); 0260 smShowSingmasterMoves(); // Re-display the Singmaster moves. 0261 } 0262 0263 if (playerMoves > 0) { 0264 // Undo player moves, wait, solve (undo shuffle), wait, redo shuffle. 0265 QString s; 0266 s = s.fill (QLatin1Char('r'), playerMoves); // Afterwards, redo each player move. 0267 startAnimation (QStringLiteral("Uwwswwhww") + s, option [optSceneID], true, false); 0268 } 0269 else { 0270 // No player moves: solve (undo shuffle), wait, redo shuffle. 0271 startAnimation (QStringLiteral("swwh"), option [optSceneID], true, false); 0272 } 0273 } 0274 0275 0276 void Game::setCubeNotAligned() 0277 { 0278 cubeAligned = false; // User has rotated the cube manually. 0279 } 0280 0281 0282 void Game::setStandardView() 0283 { 0284 if (tooBusy()) 0285 return; 0286 0287 if (cubeAligned) { 0288 return; // The cube is already aligned. 0289 } 0290 0291 QList<Move *> tempMoves; 0292 moveTracker->realignCube (tempMoves); 0293 cubeAligned = true; 0294 0295 if (tempMoves.isEmpty()) { 0296 return; // The cube was close to being aligned. 0297 } 0298 0299 // Transfer all the whole-cube alignment moves into the player's list and 0300 // execute them. They can then be undone/redone. More importantly, the 0301 // cube's internal axes will be aligned with the player's eye view, making 0302 // keyboard (XYZ) and Singmaster (LRFBUD) moves properly meaningful. 0303 0304 // Delete moves that have been undone and not redone. 0305 truncateUndoneMoves(); 0306 0307 // Make sure the latest Singmaster move is completed, if there is one. 0308 if (smMoveToComplete()) { 0309 // Record and make the move internally, but do not animate it. This is 0310 // a rare case but must be handled this way, because the cube moves that 0311 // will come after the Singmaster move cannot be animated. 0312 forceImmediateMove (smMoveAxis, smMoveSlice, smMoveDirection); 0313 0314 // Add it to the Singmaster Moves display. 0315 singmasterString.append (smTempString); 0316 0317 smInitInput(); // Re-initialise the move-text parsing. 0318 } 0319 0320 // Record and make each whole-cube move internally, but do not animate it. 0321 // The resulting view of the cube has already been adjusted and displayed. 0322 // The series of moves is delimited by spaces in the Singmaster display. 0323 0324 if ((singmasterString.length() > 0) && 0325 (singmasterString.right (1) != QLatin1Char(SingmasterNotation [SM_SPACER]))) 0326 { 0327 singmasterString.append (QLatin1Char(SingmasterNotation [SM_SPACER])); 0328 } 0329 0330 while (! tempMoves.isEmpty()) { 0331 Move * move = tempMoves.takeFirst(); 0332 forceImmediateMove (move); 0333 0334 QString s = convertMoveToSingmaster (move); 0335 smSelectionStart = singmasterString.length(); 0336 smSelectionLength = s.length(); 0337 singmasterString.append (s); 0338 } 0339 0340 singmasterString.append (QLatin1Char(SingmasterNotation [SM_SPACER])); 0341 smSelectionLength++; 0342 0343 // Update the Singmaster Moves display. 0344 smShowSingmasterMoves(); 0345 } 0346 0347 0348 void Game::undoAll () 0349 { 0350 startUndo (QStringLiteral("U"), i18n("Restart Puzzle (Undo All)")); 0351 } 0352 0353 0354 void Game::redoAll () 0355 { 0356 startRedo (QStringLiteral("R"), i18n("Redo All")); 0357 } 0358 0359 0360 void Game::changeScene (const int newSceneID) 0361 { 0362 QString sceneActionName = QStringLiteral("scene_%1").arg(newSceneID); 0363 mainWindow->setToggle (sceneActionName, true); 0364 0365 currentSceneID = newSceneID; 0366 option [optSceneID] = currentSceneID; 0367 setSceneLabels (); 0368 } 0369 0370 0371 void Game::cycleSceneUp () 0372 { 0373 // Add a cube to the view or cycle forward to a 1-cube view. 0374 changeScene ((currentSceneID < (nSceneIDs - 1)) ? (currentSceneID + 1) 0375 : OneCube); 0376 } 0377 0378 0379 void Game::cycleSceneDown () 0380 { 0381 // Remove a cube from the view or cycle back to a 3-cube view. 0382 changeScene ((currentSceneID > 1) ? (currentSceneID - 1) : ThreeCubes); 0383 } 0384 0385 0386 void Game::toggleDemo () 0387 { 0388 if (demoPhase) { 0389 stopDemo (); 0390 restoreState (); 0391 mainWindow->describePuzzle (option [optXDim], option [optYDim], 0392 option [optZDim], option [optShuffleMoves]); 0393 } 0394 else { 0395 saveState (); 0396 startDemo (); 0397 randomDemo (); 0398 } 0399 } 0400 0401 0402 void Game::loadDemo (const QString & file) 0403 { 0404 if ((! demoPhase) && tooBusy()) 0405 return; 0406 QString demoFile = QStandardPaths::locate(QStandardPaths::AppDataLocation, file); 0407 KConfig config (demoFile, KConfig::SimpleConfig); 0408 if (config.hasGroup (QStringLiteral("KubrickGame"))) { 0409 if (! demoPhase) { 0410 saveState (); 0411 startDemo (); 0412 } 0413 loadPuzzle (config); 0414 } 0415 else { 0416 KMessageBox::information (myParent, 0417 i18n("Sorry, could not find a valid Kubrick demo file " 0418 "called %1. It should have been installed in the " 0419 "'apps/kubrick' sub-directory.", file), 0420 i18n("File Not Found")); 0421 } 0422 } 0423 0424 0425 void Game::watchShuffling () 0426 { 0427 bool v; 0428 v = (bool) option [optViewShuffle]; 0429 option [optViewShuffle] = (int) (! v); 0430 if (! demoPhase) { 0431 viewShuffle = (! v); 0432 } 0433 } 0434 0435 0436 void Game::watchMoves () 0437 { 0438 bool v; 0439 v = (bool) option [optViewMoves]; 0440 option [optViewMoves] = (int) (! v); 0441 if (! demoPhase) { 0442 viewMoves = (! v); 0443 } 0444 } 0445 0446 0447 void Game::enableMessages () 0448 { 0449 // Show error messages that have been disabled by the "don't show" option. 0450 KMessageBox::enableAllMessages (); 0451 } 0452 0453 0454 void Game::optionsDialog () 0455 { 0456 (void) doOptionsDialog (false); 0457 } 0458 0459 0460 // IDW - Key K for switching the background (temporary) - FIX IT FOR KDE 4.2. 0461 void Game::switchBackground() 0462 { 0463 gameGLView->changeBackground (); 0464 } 0465 0466 0467 void Game::setMoveAxis (int i) 0468 { 0469 if (tooBusy()) 0470 return; 0471 0472 if (! cubeAligned) { 0473 setStandardView(); // Make sure the cube is realigned. 0474 } 0475 0476 // Triggered by key x, y or z. 0477 currentMoveAxis = (Axis) i; 0478 startBlinking (); 0479 } 0480 0481 0482 void Game::setMoveSlice (int slice) 0483 { 0484 if (tooBusy()) 0485 return; 0486 0487 if (! cubeAligned) { 0488 setStandardView(); // Make sure the cube is realigned. 0489 } 0490 0491 // Triggered by keys 1 to 6 or C for rotating whole cube. 0492 if (slice > cubeSize [currentMoveAxis]) 0493 return; // Invalid (for current cube). 0494 if (slice == 0) { 0495 // Rotate the whole cube. 0496 currentMoveSlice = WHOLE_CUBE; 0497 } 0498 else { 0499 // Calculate the Cube co-ordinate of the slice to rotate. 0500 currentMoveSlice = 2 * slice - cubeSize [currentMoveAxis] - 1; 0501 } 0502 startBlinking (); 0503 } 0504 0505 0506 void Game::setMoveDirection (int direction) 0507 { 0508 if (tooBusy()) 0509 return; 0510 0511 if (! cubeAligned) { 0512 setStandardView(); // Make sure the cube is realigned. 0513 } 0514 0515 // Triggered by LeftArrow or RightArrow key. 0516 currentMoveDirection = (Rotation) direction; 0517 cube->setBlinkingOff (); 0518 moveFeedback = None; 0519 0520 Move * move = new Move; 0521 0522 move->axis = currentMoveAxis; 0523 move->slice = currentMoveSlice; 0524 move->direction = currentMoveDirection; 0525 0526 addPlayersMove (move); // Add the move to the list. 0527 } 0528 0529 0530 void Game::addPlayersMove (Move * move) 0531 { 0532 // Mouse moves and XYZ moves come here. Singmaster moves do not. 0533 // So check if there is a Singmaster move to be completed. 0534 0535 if (smMoveToComplete()) { 0536 // Complete the Singmaster move and add it to the list of moves to do. 0537 // This also deletes moves that have been undone and not redone. 0538 smInput (SM_EXECUTE); 0539 } 0540 else { 0541 // Delete moves that have been undone and not redone. 0542 truncateUndoneMoves(); 0543 } 0544 0545 // Add the new move to the queue. 0546 appendMove (move); 0547 0548 // Add the move to the Singmaster Moves display. 0549 QString tempString = convertMoveToSingmaster (move); 0550 singmasterString.append (tempString); 0551 0552 // Trigger the animation's advance() cycle to do the move(s). 0553 startAnimation (displaySequence + QLatin1Char('m'), option [optSceneID], 0554 option [optViewShuffle], option [optViewMoves]); 0555 } 0556 0557 0558 void Game::smShowSingmasterMoves() 0559 { 0560 // Display or re-display the Singmaster moves, if the GUI has been set up. 0561 if (mainWindow != nullptr) { 0562 mainWindow->setSingmaster (singmasterString + smTempString); 0563 mainWindow->setSingmasterSelection 0564 (smSelectionStart, smSelectionLength); 0565 } 0566 } 0567 0568 0569 bool Game::smMoveToComplete() 0570 { 0571 if (!smTempString.isEmpty()) { 0572 if (keyboardState == SingmasterFaceIDSeen) { 0573 // Singmaster move must stay in list when next move is added. 0574 return true; 0575 } 0576 else { 0577 // Incomplete move: prefix only. Re-initialise move-text parsing. 0578 smInitInput(); 0579 } 0580 } 0581 return false; // No Singmaster move to complete. 0582 } 0583 0584 0585 void Game::smInitInput() 0586 { 0587 // Initialise or re-initialise the move-text parsing. 0588 smDotCount = 0; 0589 smTempString = QLatin1String(""); 0590 keyboardState = WaitingForInput; 0591 } 0592 0593 0594 void Game::smInput (const int smCode) 0595 { 0596 if (tooBusy()) 0597 return; 0598 0599 if (! cubeAligned) { 0600 setStandardView(); // Make sure the cube is realigned. 0601 } 0602 0603 // States: WaitingForInput, SingmasterPrefixSeen, SingmasterFaceIDSeen 0604 // 0605 // STATE INPUT ACTIONS NEXT-STATE 0606 // 0607 // WaitingForInput SM_INNER Count, limit SingmasterPrefixSeen 0608 // SM_A_CLOCK Error No change 0609 // SM_DOUBLE Error " " 0610 // SM_2_SLICE Error " " 0611 // SM_A_SLICE Error " " 0612 // SM_EXECUTE Error " " 0613 // SM_SPACER Add space " " 0614 // FaceID Save faceID SingmasterFaceIDSeen 0615 // SingmasterPrefixSeen SM_INNER Count, limit No change 0616 // SM_A_CLOCK Error " " 0617 // SM_DOUBLE Error " " 0618 // SM_2_SLICE Error " " 0619 // SM_A_SLICE Error " " 0620 // SM_EXECUTE Error " " 0621 // SM_SPACER Error " " 0622 // FaceID Save faceID SingmasterFaceIDSeen 0623 // SingmasterFaceIDSeen SM_INNER Execute, count, limit SingmasterPrefixSeen 0624 // SM_A_CLOCK Execute WaitingForInput 0625 // SM_DOUBLE Execute " " 0626 // SM_2_SLICE Execute " " 0627 // SM_A_SLICE Execute " " 0628 // SM_EXECUTE Execute " " 0629 // SM_SPACER Execute, add space " " 0630 // FaceID Execute, save faceID No change 0631 0632 switch (keyboardState) { 0633 case WaitingForInput: 0634 smInitInput(); // Playing safe. 0635 smWaitingForInput ((SingmasterMove) smCode); 0636 break; 0637 case SingmasterPrefixSeen: 0638 smSingmasterPrefixSeen ((SingmasterMove) smCode); 0639 break; 0640 case SingmasterFaceIDSeen: 0641 smSingmasterFaceIDSeen ((SingmasterMove) smCode); 0642 break; 0643 default: 0644 break; 0645 } 0646 smShowSingmasterMoves(); // Re-display the Singmaster moves. 0647 } 0648 0649 0650 void Game::smWaitingForInput (const SingmasterMove smCode) 0651 { 0652 switch (smCode) { 0653 case SM_INNER: 0654 smDotCount++; 0655 smTempString.append (QLatin1Char(SingmasterNotation [SM_INNER])); 0656 keyboardState = SingmasterPrefixSeen; // Change the state. 0657 break; 0658 case SM_ANTICLOCKWISE: 0659 case SM_DOUBLE: 0660 case SM_2_SLICE: 0661 case SM_ANTISLICE: 0662 case SM_EXECUTE: 0663 // USER'S TYPO: Swallow the keystroke and do not change the state. 0664 keyboardState = WaitingForInput; 0665 break; 0666 case SM_SPACER: 0667 singmasterString.append (QLatin1Char(SingmasterNotation [SM_SPACER])); 0668 keyboardState = WaitingForInput; // No change of state. 0669 break; 0670 case SM_UP: 0671 case SM_DOWN: 0672 case SM_LEFT: 0673 case SM_RIGHT: 0674 case SM_FRONT: 0675 case SM_BACK: 0676 saveSingmasterFaceID (smCode); 0677 keyboardState = SingmasterFaceIDSeen; // Change the state. 0678 break; 0679 default: 0680 qCDebug(KUBRICK_LOG) << "Unknown Singmaster code" << smCode; 0681 break; 0682 } 0683 } 0684 0685 0686 void Game::smSingmasterPrefixSeen (const SingmasterMove smCode) 0687 { 0688 switch (smCode) { 0689 case SM_INNER: 0690 smDotCount++; 0691 smTempString.append (QLatin1Char(SingmasterNotation [SM_INNER])); 0692 keyboardState = SingmasterPrefixSeen; // No change of state. 0693 break; 0694 case SM_ANTICLOCKWISE: 0695 case SM_DOUBLE: 0696 case SM_2_SLICE: 0697 case SM_ANTISLICE: 0698 case SM_EXECUTE: 0699 case SM_SPACER: 0700 // USER'S TYPO: Swallow the keystroke and do not change the state. 0701 keyboardState = SingmasterPrefixSeen; 0702 break; 0703 case SM_UP: 0704 case SM_DOWN: 0705 case SM_LEFT: 0706 case SM_RIGHT: 0707 case SM_FRONT: 0708 case SM_BACK: 0709 saveSingmasterFaceID (smCode); 0710 keyboardState = SingmasterFaceIDSeen; // Change the state. 0711 break; 0712 default: 0713 qCDebug(KUBRICK_LOG) << "Unknown Singmaster code" << smCode; 0714 break; 0715 } 0716 } 0717 0718 0719 void Game::smSingmasterFaceIDSeen (const SingmasterMove smCode) 0720 { 0721 switch (smCode) { 0722 case SM_INNER: 0723 executeSingmasterMove (SM_EXECUTE); 0724 smDotCount = 1; 0725 smTempString = QLatin1Char(SingmasterNotation [SM_INNER]); 0726 keyboardState = SingmasterPrefixSeen; // Change the state. 0727 break; 0728 case SM_ANTICLOCKWISE: 0729 case SM_DOUBLE: 0730 case SM_2_SLICE: 0731 case SM_ANTISLICE: 0732 case SM_EXECUTE: 0733 executeSingmasterMove (smCode); 0734 keyboardState = WaitingForInput; // Change the state. 0735 break; 0736 case SM_SPACER: 0737 executeSingmasterMove (smCode); 0738 keyboardState = WaitingForInput; // Change the state. 0739 break; 0740 case SM_UP: 0741 case SM_DOWN: 0742 case SM_LEFT: 0743 case SM_RIGHT: 0744 case SM_FRONT: 0745 case SM_BACK: 0746 executeSingmasterMove (SM_EXECUTE); 0747 saveSingmasterFaceID (smCode); 0748 keyboardState = SingmasterFaceIDSeen; // No change of state. 0749 break; 0750 default: 0751 qCDebug(KUBRICK_LOG) << "Unknown Singmaster code" << smCode; 0752 break; 0753 } 0754 } 0755 0756 0757 void Game::saveSingmasterFaceID (const SingmasterMove smCode) 0758 { 0759 int direction; 0760 int slice; 0761 0762 switch (smCode) { 0763 case SM_UP: 0764 smMoveAxis = (Axis) Y; 0765 direction = +1; // Face visible. 0766 break; 0767 case SM_DOWN: 0768 smMoveAxis = (Axis) Y; 0769 direction = -1; // Face not visible. 0770 break; 0771 case SM_RIGHT: 0772 smMoveAxis = (Axis) X; 0773 direction = +1; // Face visible. 0774 break; 0775 case SM_LEFT: 0776 smMoveAxis = (Axis) X; 0777 direction = -1; // Face not visible. 0778 break; 0779 case SM_FRONT: 0780 smMoveAxis = (Axis) Z; 0781 direction = +1; // Face visible. 0782 break; 0783 case SM_BACK: 0784 smMoveAxis = (Axis) Z; 0785 direction = -1; // Face not visible. 0786 break; 0787 default: 0788 qCDebug(KUBRICK_LOG) << "'Impossible' Singmaster code" << smCode; 0789 return; 0790 break; 0791 } 0792 0793 // We can only have inner slices on cube dimensions greater than 2 slices 0794 // and the number of inner slices is 2 less than the cube dimension, so 0795 // adjust the dot-count if necessary. 0796 0797 smDotCount = (cubeSize [smMoveAxis] <= 2) ? 0 : smDotCount; 0798 smDotCount = (smDotCount > cubeSize [smMoveAxis] - 2) ? 0799 (cubeSize [smMoveAxis] - 2) : smDotCount; 0800 0801 // Edit the temporary Singmaster move-string and add the move-notation. 0802 smTempString = (smDotCount > 0) ? smTempString.left (smDotCount) : QString(); 0803 smTempString.append (QLatin1Char(SingmasterNotation [smCode])); 0804 0805 // An invisible face (D, L, B) will be slice 1 and a visible face will have 0806 // a slice number the same as the cube dimension. This is adjusted up or 0807 // down by the dot-count, so as to be that many slices "in" from the face. 0808 0809 slice = (direction < 0) ? 1 : cubeSize [smMoveAxis]; 0810 slice = slice - direction * smDotCount; 0811 0812 // Calculate the Cube co-ordinate of the slice to rotate. 0813 0814 smMoveSlice = 2 * slice - cubeSize [smMoveAxis] - 1; 0815 0816 // In Singmaster convention, faces rotate clockwise, as seen when looking 0817 // at the face and towards the centre of the cube, so the invisible faces 0818 // must rotate ANTI-clockwise in the Kubrick convention, as seen from the 0819 // Up, Front, Right perspective view, looking at faces U, F and R. 0820 0821 smMoveDirection = (direction < 0) ? ANTICLOCKWISE : CLOCKWISE; 0822 0823 // NOTE: The move can be further modified if ' 2 + or - is appended. 0824 } 0825 0826 0827 void Game::executeSingmasterMove (const SingmasterMove smCode) 0828 { 0829 switch (smCode) { 0830 case SM_ANTICLOCKWISE: 0831 smTempString.append (QLatin1Char(SingmasterNotation [SM_ANTICLOCKWISE])); 0832 smMoveDirection = (smMoveDirection == CLOCKWISE) ? 0833 ANTICLOCKWISE : CLOCKWISE; 0834 break; 0835 case SM_DOUBLE: 0836 smTempString.append (QLatin1Char(SingmasterNotation [SM_DOUBLE])); 0837 smMoveDirection = ONE_EIGHTY; 0838 break; 0839 case SM_2_SLICE: 0840 case SM_ANTISLICE: 0841 case SM_EXECUTE: 0842 break; 0843 case SM_SPACER: 0844 smTempString.append (QLatin1Char(SingmasterNotation [SM_SPACER])); 0845 break; 0846 default: 0847 qCDebug(KUBRICK_LOG) << "'Impossible' Singmaster code" << smCode; 0848 return; 0849 break; 0850 } 0851 0852 Move * move = new Move; 0853 0854 move->axis = smMoveAxis; 0855 move->slice = smMoveSlice; 0856 move->direction = smMoveDirection; 0857 0858 // Delete moves that have been undone and not redone. 0859 truncateUndoneMoves(); 0860 0861 // Add the new move to the queue. 0862 appendMove (move); 0863 0864 // Add it to the Singmaster Moves display. 0865 singmasterString.append (smTempString); 0866 0867 // Trigger the animation's advance() cycle to do the move. 0868 startAnimation (displaySequence + QLatin1Char('m'), option [optSceneID], 0869 option [optViewShuffle], option [optViewMoves]); 0870 0871 smInitInput(); // Re-initialise the move-text parsing. 0872 } 0873 0874 0875 QString Game::convertMoveToSingmaster (const Move * move) 0876 { 0877 SingmasterMove s; 0878 switch ((int) move->axis) { 0879 case X: 0880 s = (move->slice < 0) ? SM_LEFT : SM_RIGHT; 0881 break; 0882 case Y: 0883 s = (move->slice < 0) ? SM_DOWN : SM_UP; 0884 break; 0885 case Z: 0886 s = (move->slice < 0) ? SM_BACK : SM_FRONT; 0887 break; 0888 } 0889 0890 QString dots; 0891 int slice = (move->slice + cubeSize [move->axis] + 1) / 2; 0892 int direction = move->direction; 0893 0894 if (move->slice == WHOLE_CUBE) { 0895 dots = QLatin1Char(SingmasterNotation [SM_CUBE]); 0896 } 0897 else { 0898 dots.fill (QLatin1Char(SingmasterNotation [SM_INNER]), cubeSize [move->axis]); 0899 dots = (move->slice < 0) ? dots.left (slice - 1) : 0900 dots.left (cubeSize [move->axis] - slice); 0901 } 0902 0903 if (move->slice < 0) { 0904 direction = (direction == CLOCKWISE) ? ANTICLOCKWISE : CLOCKWISE; 0905 } 0906 0907 QString smMove = dots + QLatin1Char(SingmasterNotation [s]) + ((direction == CLOCKWISE) ? 0908 QString() : QString (QLatin1Char(SingmasterNotation [SM_ANTICLOCKWISE]))); 0909 return smMove; 0910 } 0911 0912 0913 void Game::setSceneLabels () 0914 { 0915 int x, y; 0916 int w = gameGLView->width(); 0917 int h = gameGLView->height(); 0918 SceneLabel * labelObj = nullptr; 0919 0920 frontVL->setVisible (false); 0921 backVL->setVisible (false); 0922 0923 for (CubeView * v : std::as_const(cubeViews)) { 0924 if ((v->sceneID != currentSceneID) || (v->label == NoLabel)) 0925 continue; // Skip unwanted scene IDs and labels. 0926 0927 // Convert label-index to run-time label object pointer. 0928 switch ((int) v->label) { 0929 case FrontLbl: 0930 labelObj = frontVL; 0931 break; 0932 case BackLbl: 0933 labelObj = backVL; 0934 break; 0935 } 0936 0937 // Position the label in 1/8ths of gameGLView dimensions. 0938 x = (v->labelX * w)/8 - labelObj->width()/2 + 10; 0939 y = (v->labelY * h)/8 + labelObj->height(); 0940 labelObj->move (x, y); 0941 labelObj->setVisible (true); 0942 } 0943 demoL->move (10, gameGLView->height() - 10); 0944 } 0945 0946 0947 void Game::saveState () 0948 { 0949 if (demoPhase) { 0950 return; // Don't save if quitting during a demo. 0951 } 0952 const QString sFile = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + QStringLiteral("kubrick.save"); 0953 KConfig config (sFile, KConfig::SimpleConfig); 0954 savePuzzle (config); 0955 } 0956 0957 0958 void Game::restoreState () 0959 { 0960 const QString rFile = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + QStringLiteral("kubrick.save"); 0961 KConfig config (rFile, KConfig::SimpleConfig); 0962 if (config.hasGroup (QStringLiteral("KubrickGame"))) { 0963 loadPuzzle (config); 0964 } 0965 else { 0966 // No Kubrick game-file in this user's area, so save a default puzzle. 0967 newCube (option [optXDim], option [optYDim], option [optZDim], 0968 option [optShuffleMoves]); 0969 savePuzzle (config); 0970 } 0971 } 0972 0973 0974 void Game::newCube (int xDim, int yDim, int zDim, int shMoves) 0975 { 0976 delete cube; // Delete the previous cube (if any). 0977 cubeSize [X] = xDim; 0978 cubeSize [Y] = yDim; 0979 cubeSize [Z] = zDim; 0980 nMax = qMax(qMax(xDim, yDim), zDim); 0981 shuffleMoves = shMoves; 0982 0983 cube = new Cube (this, cubeSize[X], cubeSize[Y], cubeSize[Z]); 0984 0985 // Set default parameters for the first move (if using keyboard control). 0986 currentMoveAxis = Z; 0987 currentMoveSlice = cubeSize[Z] - 1; // Front face (+Z). 0988 currentMoveDirection = CLOCKWISE; 0989 0990 moveFeedback = None; // No move being selected. 0991 0992 qDeleteAll(moves); // Re-initialise the internal move-list. 0993 moves.clear(); 0994 playerMoves = 0; 0995 0996 singmasterString = QString(); // Re-initialise the Singmaster moves 0997 smSelectionStart = 0; // display and move-text parsing. 0998 smSelectionLength = 0; 0999 smInitInput(); 1000 1001 // Clear the Singmaster Moves display. 1002 smShowSingmasterMoves(); 1003 1004 // Shuffle the cube. 1005 QString dSeq; // No moves to do, if no shuffling. 1006 if (shuffleMoves > 0) { 1007 shuffleCube (); // Calculate the shuffling moves. 1008 dSeq = QLatin1Char('h'); // Ask to do the shuffling moves. 1009 } 1010 // Trigger the animation's advance() cycle to do the moves. 1011 startAnimation (dSeq, option [optSceneID], option [optViewShuffle], 1012 option [optViewMoves]); 1013 } 1014 1015 1016 int Game::doOptionsDialog (bool changePuzzle) // Private function. 1017 { 1018 int result = QDialog::Rejected; 1019 if (tooBusy()) 1020 return (result); 1021 int optionTemp [nOptions]; 1022 1023 LOOP (n, nOptions) { 1024 optionTemp [n] = option [n]; 1025 } 1026 1027 // Show the options dialog for the new cube. 1028 GameDialog * d = new GameDialog (changePuzzle, optionTemp, myParent); 1029 1030 int count = 0; 1031 result = QDialog::Accepted; 1032 while ((result = d->exec()) == QDialog::Accepted) { 1033 // The number of cubies on each edge is >=1 and <= 6. 1034 count = 0; 1035 LOOP (axis, nAxes) { 1036 if (optionTemp [optXDim + axis] == 1) { 1037 count++; 1038 } 1039 } 1040 1041 // At most one side should have size 1. 1042 if (count > 1) { 1043 KMessageBox::information (myParent, 1044 i18n("Only one of your dimensions can be one cubie wide."), 1045 i18n("Cube Options")); 1046 continue; // Repeat the dialog. 1047 } 1048 else { 1049 break; // The dimensions are valid. 1050 } 1051 } 1052 1053 if (result == QDialog::Accepted) { 1054 LOOP (n, nOptions) { 1055 option [n] = optionTemp [n]; 1056 } 1057 moveSpeed = option [optMoveSpeed]; 1058 gameGLView->setBevelAmount (option [optBevel]); 1059 viewShuffle = (bool) option [optViewShuffle]; 1060 viewMoves = (bool) option [optViewMoves]; 1061 mainWindow->setToggle (QStringLiteral("watch_shuffling"), (bool)option[optViewShuffle]); 1062 mainWindow->setToggle (QStringLiteral("watch_moves"), (bool)option[optViewMoves]); 1063 } 1064 1065 delete d; 1066 1067 return (result); 1068 } 1069 1070 1071 void Game::drawScene () 1072 { 1073 // Save data with QOpenGLWidget only valid during paintGL() 1074 moveTracker->saveSceneInfo(); 1075 1076 // The size of the cubes on-screen is determined by the height of the 1077 // window and the OpenGL perspective projection, in which the window-height 1078 // always has a set angle of view. The lateral spacing of the cubes (X 1079 // co-ordinates of centres) is scaled by the aspect-ratio, to give us an 1080 // even spacing however wide the window is. 1081 1082 double aspect = (double) gameGLView->width () / gameGLView->height (); 1083 float fieldHeight = -cubeCentreZ * 2.0 * tan (3.14159 * viewAngle/360.0); 1084 float fieldWidth = aspect * fieldHeight; 1085 1086 for (CubeView * v : std::as_const(cubeViews)) { 1087 if (v->sceneID != currentSceneID) 1088 continue; // Skip unwanted scene IDs. 1089 1090 v->position [X] = v->relX * fieldWidth; 1091 v->position [Y] = v->relY * fieldHeight; 1092 v->position [Z] = cubeCentreZ; 1093 1094 gameGLView->pushGLMatrix (); 1095 gameGLView->moveGLView (v->position[X], 1096 v->position[Y], 1097 v->position[Z]); 1098 v->cubieSize = v->size / nMax; 1099 1100 // Turn and tilt, to make 3 faces visible. 1101 gameGLView->rotateGLView (v->turn, 0.0, 1.0, 0.0); 1102 gameGLView->rotateGLView (v->tilt, 1.0, 0.0, -1.0); 1103 1104 // Save the matrix for this (standard) view of the cube. 1105 glGetDoublev (GL_MODELVIEW_MATRIX, v->matrix0); 1106 1107 // Tumble or rotate the cube as required, if this cube-view can rotate. 1108 if (v->rotates) { 1109 if (demoPhase) { 1110 // Calculate a pseudo-random rotation. 1111 tumble(); 1112 } 1113 else { 1114 // Apply the total of the user's arbitrary rotations (if any). 1115 moveTracker->usersRotation(); 1116 } 1117 } 1118 1119 // Save the matrix for this (fully rotated) view of the cube. 1120 glGetDoublev (GL_MODELVIEW_MATRIX, v->matrix); 1121 1122 cube->drawCube (gameGLView, v->cubieSize); 1123 1124 gameGLView->popGLMatrix (); 1125 } 1126 1127 // Draw whichever scene-labels are visible. 1128 demoL-> drawLabel (gameGLView); // "DEMO - Click anywhere ...". 1129 frontVL-> drawLabel (gameGLView); // "Front View". 1130 backVL-> drawLabel (gameGLView); // "Back View". 1131 } 1132 1133 1134 void Game::setCubeView (int sceneID, bool rotates, float size, 1135 float relX, float relY, 1136 float turn, float tilt, int labelX, int labelY, LabelID label) 1137 { 1138 CubeView * v = new CubeView; 1139 1140 double aspect = (double) gameGLView->width () / gameGLView->height (); 1141 float fieldHeight = -cubeCentreZ * 2.0 * tan (3.14159 * viewAngle/360.0); 1142 float fieldWidth = aspect * fieldHeight; 1143 1144 v->sceneID = sceneID; 1145 v->rotates = rotates; 1146 v->size = size; 1147 v->relX = relX; 1148 v->relY = relY; 1149 v->position [X] = v->relX * fieldWidth; 1150 v->position [Y] = v->relY * fieldHeight; 1151 v->position [Z] = cubeCentreZ; 1152 v->turn = turn; 1153 v->tilt = tilt; 1154 v->labelX = labelX; 1155 v->labelY = labelY; 1156 v->label = label; 1157 1158 cubeViews.append (v); 1159 } 1160 1161 1162 void Game::tumble () 1163 { 1164 if (tumblingTicks > 0) { 1165 // If the cube has been tumbled, restore its rotation position or 1166 // tumble it some more (advance() bumps tumblingTicks if tumbling on). 1167 gameGLView->rotateGLView ((float) (tumblingTicks % 360), 1168 1.0, 0.0, 0.0); // X rotation. 1169 gameGLView->rotateGLView ((float) (((tumblingTicks * 9) / 10) % 360), 1170 0.0, 1.0, 0.0); // Y rotation. 1171 gameGLView->rotateGLView ((float) (((tumblingTicks * 8) / 10) % 360), 1172 0.0, 0.0, 1.0); // Z rotation. 1173 } 1174 } 1175 1176 1177 void Game::startDemo () 1178 { 1179 // Set the demo's toggle-button ON. 1180 mainWindow->setToggle (KGameStandardAction::name (KGameStandardAction::Demo), true); 1181 // Disable the Save actions. 1182 mainWindow->setAvail (KGameStandardAction::name (KGameStandardAction::Save), false); 1183 mainWindow->setAvail (KGameStandardAction::name (KGameStandardAction::SaveAs), false); 1184 // Disable the Preferences action. 1185 mainWindow->setAvail (KStandardAction::name (KStandardAction::Preferences), false); 1186 1187 demoPhase = true; 1188 tumblingTicks = 0; // Show an untumbled cube. 1189 demoL->setVisible (true); // Show the "click to stop" message. 1190 } 1191 1192 1193 void Game::randomDemo () 1194 { 1195 double pickShape = random.generateDouble(); 1196 1197 // Pick cubes 40% of the time. 1198 cubeSize [X] = pickANumber (2, 6); 1199 cubeSize [Y] = cubeSize [X]; 1200 cubeSize [Z] = cubeSize [X]; 1201 1202 if (pickShape < 0.6) { 1203 // Pick square-cross-section prisms 40% of the time. 1204 while (cubeSize [Z] == cubeSize [X]) { 1205 cubeSize [Z] = pickANumber (1, 6); 1206 } 1207 } 1208 1209 if (pickShape < 0.2) { 1210 // Pick irregular shapes 20% of the time. 1211 while ((cubeSize [Y] == cubeSize [X]) || 1212 (cubeSize [Y] == cubeSize [Z])) { 1213 cubeSize [Y] = pickANumber (2, 6); 1214 } 1215 } 1216 1217 shuffleMoves = pickANumber (5, 12); 1218 tumbling = true; 1219 // mainWindow->setToggle ("toggle_tumbling", tumbling); 1220 moveSpeed = 5; 1221 1222 // Create the demo cube. 1223 newCube (cubeSize [X], cubeSize [Y], cubeSize [Z], shuffleMoves); 1224 1225 // Shuffle, solve, start next demo ... current scene, all moves animated. 1226 startAnimation (QStringLiteral("whwswd"), currentSceneID, true, true); 1227 mainWindow->describePuzzle (cubeSize [X], cubeSize [Y], cubeSize [Z], 1228 shuffleMoves); 1229 } 1230 1231 1232 void Game::stopDemo () 1233 { 1234 // Set the demo's toggle-button OFF. 1235 mainWindow->setToggle (KGameStandardAction::name (KGameStandardAction::Demo), false); 1236 // Enable the Save actions. 1237 mainWindow->setAvail (KGameStandardAction::name (KGameStandardAction::Save), true); 1238 mainWindow->setAvail (KGameStandardAction::name (KGameStandardAction::SaveAs), true); 1239 // Enable the Preferences action. 1240 mainWindow->setAvail (KStandardAction::name (KStandardAction::Preferences), true); 1241 1242 tumbling = false; 1243 // mainWindow->setToggle (QStringLiteral("toggle_tumbling"), tumbling); 1244 1245 demoL->setVisible (false); // Hide the DEMO text. 1246 demoPhase = false; 1247 } 1248 1249 1250 void Game::setDefaults() 1251 { 1252 // Set default values for the options. 1253 1254 option [optXDim] = 3; // Cube size 3x3x3. 1255 option [optYDim] = 3; 1256 option [optZDim] = 3; 1257 1258 option [optShuffleMoves] = 4; // Four shuffling moves. 1259 option [optViewShuffle] = (int) true; // Animate the shuffling moves. 1260 option [optViewMoves] = (int) false; // Don't animate the player's moves. 1261 option [optBevel] = 12; // 12% bevel on edges of cubies. 1262 option [optMoveSpeed] = 5; // Speed of moves (5 deg/tick) [1..10]. 1263 1264 option [optSceneID] = TwoCubes; // Scene: 2 cubes, front and back views. 1265 option [optTumbling] = (int) false; // No tumbling. 1266 option [optTumblingTicks] = 0; // "Home" orientation. 1267 option [optMouseBlink] = (int) true; // Blink during mouse-controlled move. 1268 } 1269 1270 1271 void Game::startBlinking () 1272 { 1273 cube->setBlinkingOff (); 1274 cube->setBlinkingOn (currentMoveAxis, currentMoveSlice); 1275 moveFeedback = Keyboard; 1276 time.start(); 1277 } 1278 1279 1280 void Game::appendMove (Move * move) 1281 { 1282 move->degrees = 90; 1283 1284 // If single slice and not square, rotate 180 degrees rather than 90. 1285 if ((move->slice != WHOLE_CUBE) && 1286 (cubeSize [(move->axis+1)%nAxes] != cubeSize [(move->axis+2)%nAxes])) { 1287 move->degrees = 180; 1288 } 1289 1290 // IDW testing - qCDebug(KUBRICK_LOG) << move->axis << move->slice << 1291 // IDW testing - move->direction << move->degrees; 1292 moves.append (move); 1293 } 1294 1295 1296 void Game::forceImmediateMove (Axis axis, int slice, Rotation direction) 1297 { 1298 Move * move = new Move; 1299 move->axis = axis; 1300 move->slice = slice; 1301 move->direction = direction; 1302 forceImmediateMove (move); 1303 } 1304 1305 1306 void Game::forceImmediateMove (Move * move) 1307 { 1308 // Add the move to the queue. 1309 appendMove (move); 1310 1311 // Make the move internally and immediately, without animating it. 1312 playerMoves++; 1313 cube->moveSlice (move->axis, move->slice, move->direction); 1314 } 1315 1316 1317 void Game::truncateUndoneMoves() 1318 { 1319 while (moves.count() > (shuffleMoves + playerMoves)) { 1320 delete moves.takeLast(); // Remove undone moves (if any). 1321 } 1322 singmasterString = singmasterString.left 1323 (smSelectionStart + smSelectionLength); 1324 } 1325 1326 1327 void Game::doSave (bool getFilename) 1328 { 1329 if (demoPhase || tooBusy()) 1330 return; 1331 if (saveFilename.isEmpty() || getFilename) { 1332 QString newFilename = QFileDialog::getSaveFileName(myParent, i18n("Save Puzzle"), 1333 QString(), i18n("Kubrick Game Files (*.kbk)")); 1334 if (newFilename.isNull()) { 1335 return; 1336 } 1337 saveFilename = newFilename; 1338 } 1339 1340 KConfig config (saveFilename, KConfig::SimpleConfig); 1341 savePuzzle (config); 1342 } 1343 1344 1345 void Game::savePuzzle (KConfig & config) 1346 { 1347 // Make sure the latest Singmaster move is completed, if there is one. 1348 if (smMoveToComplete()) { 1349 // Record and make the move internally, but do not animate it. This is 1350 // a rare case but should be handled this way, because the player may be 1351 // quitting, saving or switching to demo mode and should not lose input. 1352 1353 // Delete moves that have been undone and not redone. 1354 truncateUndoneMoves(); 1355 1356 // Do the Singmaster move. 1357 forceImmediateMove (smMoveAxis, smMoveSlice, smMoveDirection); 1358 1359 // Add it to the Singmaster Moves display. 1360 smSelectionStart = singmasterString.length(); 1361 smSelectionLength = smTempString.length(); 1362 singmasterString.append (smTempString); 1363 1364 smInitInput(); // Re-initialise the move-text parsing. 1365 1366 smShowSingmasterMoves(); // Update the Singmaster Moves display. 1367 } 1368 1369 // Clear any previously saved info (in case there are fewer moves now). 1370 config.deleteGroup (QStringLiteral("KubrickGame")); 1371 KConfigGroup configGroup = config.group(QStringLiteral("KubrickGame")); 1372 1373 QStringList list; 1374 1375 // Save the option settings. 1376 LOOP (i, nOptions) { 1377 list.append (QString::asprintf ("%d", option [i])); 1378 } 1379 configGroup.writeEntry ("a) Options", list); 1380 1381 // Save the display sequence. 1382 configGroup.writeEntry ("c) DisplaySequence", displaySequence); 1383 QString dsTemp = configGroup.readEntry ("c) DisplaySequence", ""); 1384 1385 // Save the current move counts. 1386 list.clear (); 1387 list.append (QString::asprintf ("%d", shuffleMoves)); 1388 list.append (QString::asprintf ("%d", playerMoves)); 1389 list.append (QString::asprintf ("%" PRIdQSIZETYPE, moves.count())); 1390 configGroup.writeEntry ("f) MoveCounts", list); 1391 1392 // Save the list of Singmaster moves. 1393 configGroup.writeEntry ("g) SingmasterMoves", singmasterString); 1394 1395 // Save the list of moves, using names "m) 001", "m) 002", etc. 1396 int n = 0; 1397 list.clear (); 1398 for (Move * m : std::as_const(moves)) { 1399 list.append (QString::asprintf ("%d", (int) m->axis)); 1400 list.append (QString::asprintf ("%d", m->slice)); 1401 list.append (QString::asprintf ("%d", (int) m->direction)); 1402 list.append (QString::asprintf ("%d", m->degrees)); 1403 1404 n++; 1405 configGroup.writeEntry (QString::asprintf ("m) %03d", n), list); 1406 list.clear (); 1407 } 1408 1409 configGroup.sync(); // Make sure it all goes to disk. 1410 } 1411 1412 1413 void Game::loadPuzzle (KConfig & config) 1414 { 1415 QStringList list; 1416 QStringList notFound; 1417 QStringList::Iterator it; 1418 int optionTemp [nOptions]; 1419 QString dsTemp; 1420 int moveCounts [3]; 1421 QList<Move *> movesTemp; 1422 Move * moveTemp; 1423 1424 KConfigGroup configGroup = config.group (QStringLiteral("KubrickGame")); 1425 1426 list = configGroup.readEntry ("a) Options", notFound); 1427 int nOpt = 0; 1428 for (it = list.begin(); it != list.end(); ++it) { 1429 optionTemp [nOpt] = (*it).toInt(); 1430 nOpt++; 1431 } 1432 1433 dsTemp = configGroup.readEntry ("c) DisplaySequence", ""); 1434 1435 list = configGroup.readEntry ("f) MoveCounts", notFound); 1436 int n = 0; 1437 for (it = list.begin(); it != list.end(); ++it) { 1438 moveCounts [n] = (*it).toInt(); 1439 n++; 1440 } 1441 1442 QString key; 1443 for (n = 0; n < moveCounts [2]; ++n) { 1444 key = QString::asprintf ("m) %03d", n + 1); 1445 list = configGroup.readEntry (key, notFound); 1446 moveTemp = new Move; 1447 it = list.begin(); 1448 moveTemp->axis = (Axis) (*it).toInt(); 1449 it++; 1450 moveTemp->slice = (*it).toInt(); 1451 it++; 1452 moveTemp->direction = (Rotation) (*it).toInt(); 1453 it++; 1454 moveTemp->degrees = (*it).toInt(); 1455 movesTemp.append (moveTemp); 1456 } 1457 1458 LOOP (n, nOpt) { 1459 option [n] = optionTemp [n]; 1460 } 1461 1462 // Build the new cube but don't shuffle it (yet). 1463 newCube (option [optXDim], option [optYDim], option [optZDim], 0); 1464 1465 moveSpeed = option [optMoveSpeed]; 1466 if (gameGLView != nullptr) { 1467 gameGLView->setBevelAmount (option [optBevel]); 1468 } 1469 tumbling = option [optTumbling]; 1470 tumblingTicks = option [optTumblingTicks]; 1471 if (mainWindow != nullptr) { 1472 // mainWindow->setToggle (QStringLiteral("toggle_tumbling"), tumbling); 1473 mainWindow->setToggle (QStringLiteral("watch_shuffling"), (bool)option[optViewShuffle]); 1474 mainWindow->setToggle (QStringLiteral("watch_moves"), (bool)option[optViewMoves]); 1475 } 1476 1477 qDeleteAll(moves); 1478 moves = movesTemp; 1479 shuffleMoves = moveCounts [0]; 1480 playerMoves = moveCounts [1]; 1481 1482 // Restore the list of Singmaster moves. 1483 singmasterString = configGroup.readEntry ("g) SingmasterMoves", ""); 1484 1485 // Show the Singmaster Moves, in case there are moves but all are undone. 1486 smShowSingmasterMoves(); 1487 1488 // If there are no saved moves and the cube should be shuffled, do it now. 1489 if ((moveCounts [2] == 0) && (option [optShuffleMoves] > 0)) { 1490 shuffleMoves = option [optShuffleMoves]; 1491 shuffleCube (); 1492 } 1493 1494 // Set the required display sequence, reconstructing it if necessary. 1495 QString dSeq = dsTemp; 1496 if (dSeq.isEmpty ()) { 1497 if (shuffleMoves > 0) { 1498 dSeq = QLatin1Char('h'); 1499 } 1500 if (playerMoves > 0) { 1501 // Redo all the player moves, using "M" (not "R", Redo All, in 1502 // case there are some undone moves on the end of the list). 1503 dSeq += QLatin1Char('M'); 1504 } 1505 } 1506 1507 // If dSeq is empty, we will clear previous animations and do no more, 1508 // otherwise we will reconstruct the saved position by re-doing moves 1509 // and the Singmaster Moves display will also get updated if required. 1510 1511 if (demoPhase) { 1512 // Always animate demo sequences (pretty patterns or solving moves). 1513 startAnimation (dSeq, option [optSceneID], option [optViewShuffle], 1514 option [optViewMoves]); 1515 } 1516 else { 1517 // Never re-animate restoreState() or other saved user files. 1518 startAnimation (dSeq, option [optSceneID], false, false); 1519 } 1520 1521 if (gameGLView != nullptr) { 1522 advance (); // Do all moves or just one anim step. 1523 chooseMousePointer (); // Choose busy or idle mouse pointer. 1524 setSceneLabels (); // Position the labels. 1525 } 1526 } 1527 1528 1529 void Game::startUndo (const QString &code, const QString &header) 1530 { 1531 if (tooBusy()) 1532 return; 1533 1534 if (smMoveToComplete()) { 1535 // There is an incomplete Singmaster move, so undo that first. 1536 smInitInput(); 1537 smShowSingmasterMoves(); // Re-display the Singmaster moves. 1538 if ((playerMoves <= 0) || (code == QLatin1Char('u'))) { 1539 return; // The Undo or Undo All is finished. 1540 } 1541 } 1542 1543 if (playerMoves <= 0) { 1544 KMessageBox::information (myParent, 1545 i18n("You have no moves to undo."), 1546 header); 1547 return; 1548 } 1549 1550 // Start the undo off. 1551 startAnimation (code, option [optSceneID], option [optViewShuffle], 1552 option [optViewMoves]); 1553 } 1554 1555 1556 void Game::startRedo (const QString &code, const QString &header) 1557 { 1558 if (tooBusy()) 1559 return; 1560 1561 if (moves.count() > (shuffleMoves + playerMoves)) { 1562 // If there is an incompletely entered Singmaster move, 1563 // abandon it in favour of redoing older move(s). 1564 if (smMoveToComplete()) { 1565 smInitInput(); // Undo the incomplete Singmaster input. 1566 smShowSingmasterMoves(); // Re-display the Singmaster moves. 1567 } 1568 1569 // Start the redo off. 1570 startAnimation (code, option [optSceneID], option [optViewShuffle], 1571 option [optViewMoves]); 1572 } 1573 else { 1574 KMessageBox::information (myParent, 1575 i18n("There are no moves to redo.\n\nThat could be because " 1576 "you have not undone any or you have redone them all or " 1577 "because all previously undone moves are automatically " 1578 "deleted whenever you make a new move using the keyboard " 1579 "or mouse."), 1580 header); 1581 } 1582 } 1583 1584 void Game::handleMouseEvent (MouseEvent event, int button, int mX, int mY) 1585 { 1586 // A mouse click (press and release) stops the demo. 1587 if (demoPhase) { 1588 if (event == ButtonUp) { 1589 stopDemo (); 1590 restoreState (); 1591 mainWindow->describePuzzle (option [optXDim], option [optYDim], 1592 option [optZDim], option [optShuffleMoves]); 1593 } 1594 return; 1595 } 1596 1597 if (tooBusy()) 1598 return; 1599 1600 if (((event == ButtonDown) && (moveFeedback != None)) || 1601 ((event == ButtonUp) && (moveFeedback != Mouse))) { 1602 // There is move feedback on some other device (e.g. keyboard). 1603 return; // Ignore mouse clicks. 1604 } 1605 1606 // Start or end mouse feedback. 1607 if (event == ButtonDown) { 1608 moveFeedback = Mouse; 1609 time.start(); 1610 gameGLView->setBlinkIntensity (1.0); // Keep intensity high at first. 1611 } 1612 else { 1613 moveFeedback = None; 1614 } 1615 1616 // Position in window follows OpenGL convention (with y = 0 at bottom). 1617 moveTracker->mouseInput (currentSceneID, cubeViews, cube, 1618 event, button, mX, mY); 1619 } 1620 1621 1622 int Game::pickANumber (int lo, int hi) 1623 { 1624 // Pick an integer in the range (lo..hi). 1625 return random.bounded(lo, hi + 1); 1626 } 1627 1628 1629 void Game::shuffleCube () 1630 { 1631 Move * move; 1632 Move * prev; 1633 int sliceNo; 1634 1635 qDeleteAll(moves); 1636 moves.clear(); 1637 1638 LOOP (n, shuffleMoves) { 1639 move = new Move; 1640 1641 while (true) { 1642 // Calculate a random move. 1643 move->axis = (Axis) pickANumber (0, nAxes - 1); 1644 if (cubeSize [move->axis] == 1) { 1645 continue; // Thin "cube" - don't rotate the whole thing. 1646 } 1647 sliceNo = (int) pickANumber (1, cubeSize [move->axis]); 1648 move->slice = 2*sliceNo - cubeSize [move->axis] - 1; 1649 move->direction = (Rotation) pickANumber (ANTICLOCKWISE, CLOCKWISE); 1650 move->degrees = 90; 1651 1652 // If the slice is not square, rotate 180 degrees rather than 90. 1653 if (cubeSize [(move->axis+1)%nAxes] != 1654 cubeSize [(move->axis+2)%nAxes]) { 1655 move->degrees = 180; 1656 } 1657 1658 // Always accept the first move (n = 0). 1659 if (n > 0) { 1660 prev = moves.at (n-1); // Look at the last move. 1661 if ((move->axis == prev->axis) && 1662 (move->slice == prev->slice) && 1663 ((move->degrees == 180) || 1664 (move->direction != prev->direction))) { 1665 // Reject a move that reverses the previous move. 1666 continue; 1667 } 1668 } 1669 if (n > 1) { 1670 if (move->axis == prev->axis) { // Axis same as in last move? 1671 prev = moves.at (n-2); // Look at the last but one. 1672 if (move->axis == prev->axis) { 1673 // Reject a third successive move around the same axis. 1674 continue; 1675 } 1676 } 1677 } 1678 break; 1679 } 1680 1681 moves.append (move); 1682 } 1683 } 1684 1685 void Game::startAnimation (const QString &dSeq, int sID, bool vShuffle, bool vMoves) 1686 { 1687 // Set the scene ID, animation display sequence and whether to animate the 1688 // shuffle and/or player moves or do them instantly (within one tick). 1689 1690 if ((sID != currentSceneID) && (mainWindow != nullptr)) { 1691 changeScene (sID); 1692 currentSceneID = sID; 1693 } 1694 displaySequence = dSeq; 1695 viewShuffle = vShuffle; 1696 viewMoves = vMoves; 1697 1698 // Initialise Game::advance(). 1699 movesToDo = 0; 1700 pauseTicks = 0; 1701 undoing = false; 1702 1703 // Clear any previous animation. 1704 moveAngleMax = 0; 1705 moveAngleStep = 0; 1706 moveAngle = 0; 1707 1708 // Game::advance() takes over and does the rest. 1709 } 1710 1711 1712 void Game::advance() 1713 { 1714 // Increase the game tick counter 1715 // 1716 // Note that you might think this variable could overflow. However, even if 1717 // advance() was called once per ms (which would be far too often) it would 1718 // take at least 24 days of uninterrupted running until the variable would 1719 // overflow! So usually you do not need to worry about it. 1720 1721 nTick++; 1722 1723 // This next counter keeps track of the position when the cube is tumbling. 1724 if (tumbling) { 1725 tumblingTicks++; // Change tumbling position. 1726 } 1727 1728 // If feedback to show player's next move, update it every 100 msec. 1729 if ((moveFeedback != None) && (nTick % 5 == 0)) { 1730 int t = time.elapsed(); 1731 1732 if (moveFeedback == Mouse) { 1733 // Position in window uses OpenGL convention (y = 0 at bottom). 1734 QPoint p = gameGLView->getMousePosition(); 1735 moveTracker->mouseInput (currentSceneID, cubeViews, cube, 1736 Tracking, 0, p.x(), p.y()); 1737 } 1738 else { 1739 // Blink slice(s) when selecting a move. 1740 gameGLView->setBlinkIntensity ((t < blinkStartTime) ? 1.0 : 1741 ((float) ((nTick/5)%3) * 0.1) + 0.5); 1742 } 1743 } 1744 1745 1746 // If moves to do, either animate the first one or do them all in one tick. 1747 while ((pauseTicks > 0) || (! displaySequence.isEmpty()) || (movesToDo > 0)) 1748 { 1749 // If pausing, during an animation, count the time to pause. 1750 if (pauseTicks > 0) { 1751 pauseTicks--; // This is the wait sequence ('w'). 1752 break; 1753 } 1754 // Do any moves that are waiting to be done. 1755 if (movesToDo > 0) { 1756 if (moveAngleMax > 0) { 1757 // The moves are being animated: change the move angle. 1758 moveAngle = moveAngle + moveAngleStep; 1759 if (abs(moveAngle) <= moveAngleMax) { 1760 cube->setMoveAngle (moveAngle); 1761 break; // Let the animation continue. 1762 } 1763 // If that animated move is completed, start the next one. 1764 movesToDo--; 1765 cube->setMoveAngle (0); 1766 startNextMove (abs(moveAngleStep)); 1767 } 1768 else { 1769 // Do all the moves at once, without any animation. 1770 movesToDo--; 1771 startNextMove (0); 1772 } 1773 } 1774 // If there are no moves, see if there is a display item to be started. 1775 if (movesToDo <= 0) { 1776 if (displaySequence.isEmpty()) { 1777 // The animation is finished. 1778 chooseMousePointer (); 1779 } 1780 else { 1781 // There is another animation sequence to do: start it now. 1782 startNextDisplay (); 1783 } 1784 } 1785 } // End while 1786 1787 // Force a redraw. Note that we use a timer event to trigger it, so that 1788 // we do not do this in the advance() method itself. This is not essential, 1789 // but makes the game-logic and rendering more independent of each other. 1790 1791 QTimer::singleShot(0, gameGLView, qOverload<>(&GameGLView::update)); 1792 } 1793 1794 1795 void Game::chooseMousePointer () 1796 { 1797 if (tumbling || (movesToDo > 0) || (! displaySequence.isEmpty())) { 1798 gameGLView->setCursor (Qt::WaitCursor); 1799 } 1800 else { 1801 gameGLView->setCursor (Qt::CrossCursor); 1802 } 1803 } 1804 1805 1806 void Game::startNextDisplay () 1807 { 1808 // Pick off the first character of the display sequence. 1809 char c = displaySequence.at(0).toLatin1(); 1810 displaySequence.remove (0, 1); 1811 int nRMoves = 0; 1812 1813 // Set the animation speed: 0 = no animation, 15 = fastest. Note that if 1814 // the "Watch Your Own Moves" option is off, animation is set to very fast. 1815 int shSpeed = viewShuffle ? moveSpeed : 0; 1816 int mvSpeed = viewMoves ? moveSpeed : defaultOwnMove; 1817 1818 // Initiate the required sequence of moves (or single move). 1819 switch (c) { 1820 case 'd': // Start a random demo sequence ("whwswd"). 1821 randomDemo (); 1822 break; 1823 case 'h': // Start a shuffling sequence of moves. 1824 startMoves (shuffleMoves, 0, false, shSpeed); 1825 break; 1826 case 'm': // Start doing or 1827 case 'r': // redoing a player's move. 1828 playerMoves++; 1829 startMoves (1, shuffleMoves + playerMoves - 1, false, mvSpeed); 1830 break; 1831 case 'M': // Start restoring (reloading) a player's moves. 1832 mvSpeed = viewMoves ? moveSpeed : 0; // Avoid an ugly fast animation. 1833 startMoves (playerMoves, shuffleMoves, false, mvSpeed); 1834 break; 1835 case 'R': // Start redoing all a player's undone moves. 1836 nRMoves = moves.count() - (uint) (shuffleMoves + playerMoves); 1837 mvSpeed = viewMoves ? moveSpeed : 0; // Avoid an ugly fast animation. 1838 startMoves (nRMoves, shuffleMoves + playerMoves, false, mvSpeed); 1839 playerMoves = playerMoves + nRMoves; 1840 break; 1841 case 'u': // Start undoing a player's move. 1842 startMoves (1, shuffleMoves + playerMoves - 1, true, mvSpeed); 1843 playerMoves--; 1844 break; 1845 case 'U': // Start undoing all the player's moves. 1846 mvSpeed = viewMoves ? moveSpeed : 0; // Avoid an ugly fast animation. 1847 startMoves (playerMoves, shuffleMoves + playerMoves - 1, true, mvSpeed); 1848 playerMoves = 0; 1849 break; 1850 case 's': // Start a solution sequence (undo shuffle). 1851 startMoves (shuffleMoves, shuffleMoves - 1, true, shSpeed); 1852 break; 1853 case 'w': // WAIT (pause without changing the display). 1854 // 1 second if static, 2 seconds if tumbling (time to view all faces). 1855 pauseTicks = tumbling ? 100 : 50; 1856 break; 1857 } 1858 chooseMousePointer (); 1859 } 1860 1861 1862 bool Game::tooBusy () 1863 { 1864 if (tumbling || (movesToDo > 0) || (! displaySequence.isEmpty())) { 1865 KMessageBox::information (myParent, 1866 i18n("The cube has animated moves in progress " 1867 "or the demo is running.\n\n" 1868 "Please wait or click on the cube to stop the demo."), 1869 i18n("Sorry, too busy.")); 1870 return (true); 1871 } 1872 else { 1873 return (false); 1874 } 1875 } 1876 1877 1878 void Game::startMoves (int nMoves, int index, bool pUndo, int speed) 1879 { 1880 movesToDo = nMoves; 1881 moveIndex = index; 1882 undoing = pUndo; 1883 moveFeedback = None; 1884 1885 Move * firstMove = moves.at (moveIndex); 1886 cube->setMoveInProgress (firstMove->axis, firstMove->slice); 1887 cube->setMoveAngle (0); 1888 startAnimatedMove (firstMove, speed); 1889 } 1890 1891 1892 void Game::startAnimatedMove (Move * move, int speed) 1893 { 1894 // Update the Singmaster moves display, if not a shuffling move. 1895 if (moveIndex > (shuffleMoves - 1)) { 1896 // The "-" in the pattern must be just before the "]", otherwise it 1897 // defines a range of characters and does not get matched as a "-". 1898 1899 QRegularExpression smPattern (QStringLiteral("[.C]*[FBLRUD]['2 +-]*")); 1900 // IDW testing - qCDebug(KUBRICK_LOG) << "Undoing" << undoing << singmasterString << 1901 // IDW testing - smSelectionStart << smSelectionLength; 1902 if (undoing) { 1903 int pos1 = 0; 1904 int pos2 = smSelectionStart; 1905 smSelectionStart = 0; 1906 smSelectionLength = 0; 1907 while (smSelectionStart < pos2) { 1908 QRegularExpressionMatch match = smPattern.match(singmasterString, pos1); 1909 pos1 = match.capturedStart(); 1910 if ((pos1 >= pos2) || (pos1 < 0)) { 1911 break; 1912 } 1913 smSelectionStart = pos1; 1914 smSelectionLength = match.capturedLength(); 1915 pos1 = pos1 + smSelectionLength; 1916 } 1917 } 1918 else { 1919 int pos1 = smSelectionStart + smSelectionLength; 1920 QRegularExpressionMatch match = smPattern.match(singmasterString, pos1); 1921 int pos2 = match.capturedStart(); 1922 if (pos2 >= 0) { 1923 pos2 = pos2 + match.capturedLength(); 1924 smSelectionStart = pos1; 1925 smSelectionLength = pos2 - pos1; 1926 } 1927 } 1928 smShowSingmasterMoves(); // Re-display the Singmaster moves. 1929 } 1930 1931 if (speed == 0) { 1932 return; // No animation required. 1933 } 1934 1935 // Set up the initial conditions, the advance() slot will do the rest. 1936 moveAngleMax = move->degrees; // 90 or 180 degrees. 1937 moveAngleStep = (move->direction == CLOCKWISE) ? +speed : -speed; 1938 if (undoing) { 1939 moveAngleStep = -moveAngleStep; 1940 } 1941 1942 moveAngle = 0; 1943 } 1944 1945 1946 void Game::startNextMove (int speed) 1947 { 1948 // Save the effect of the move just completed in the Cube's state. 1949 Move * move = moves.at (moveIndex); 1950 Rotation rot = move->direction; 1951 1952 if (undoing && (rot != ONE_EIGHTY)) { 1953 rot = (rot == CLOCKWISE) ? ANTICLOCKWISE : CLOCKWISE; 1954 } 1955 cube->moveSlice (move->axis, move->slice, rot); 1956 1957 // Re-initialise (clear) the animation angles. 1958 moveAngleMax = 0; 1959 moveAngleStep = 0; 1960 moveAngle = 0; 1961 1962 if (movesToDo <= 0) { 1963 // NO MOVES LEFT. 1964 return; 1965 } 1966 1967 if (undoing) { 1968 --moveIndex; 1969 } 1970 else { 1971 ++moveIndex; 1972 } 1973 move = moves.at (moveIndex); 1974 1975 cube->setMoveInProgress (move->axis, move->slice); 1976 cube->setMoveAngle (0); 1977 startAnimatedMove (move, speed); 1978 } 1979 1980 #include "moc_game.cpp"