File indexing completed on 2024-04-14 04:02:09

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"