diff --git a/src/generator/sudokuboard.cpp b/src/generator/sudokuboard.cpp index a7c3e44..ce0cefc 100644 --- a/src/generator/sudokuboard.cpp +++ b/src/generator/sudokuboard.cpp @@ -1,1124 +1,1125 @@ /**************************************************************************** * Copyright 2011 Ian Wadham * * Copyright 2006 David Bau Original algorithms * * Copyright 2015 Ian Wadham * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ****************************************************************************/ #include "debug.h" #include "ksudoku_logging.h" #include "sudokuboard.h" #include "state.h" #include "mathdokugenerator.h" #include #include #include #include #include #include #include #include SudokuBoard::SudokuBoard (SKGraph * graph) : m_type (graph->specificType()), m_order (graph->order()), m_blockSize (graph->base()), m_boardSize (0), m_boardArea (graph->size()), m_overlap (0), m_nGroups (graph->cliqueCount()), m_groupSize (m_order), m_graph (graph), m_vacant (VACANT), m_unusable (UNUSABLE) { m_stats.type = m_type; m_stats.blockSize = m_blockSize; m_stats.order = m_order; m_boardSize = graph->sizeX(); // TODO - IDW. Rationalise grid sizes. qCDebug(KSudokuLog) << "SudokuBoard: type " << m_type << graph->name() << ", block " << m_blockSize << ", order " << m_order << ", BoardArea " << m_boardArea; } void SudokuBoard::setSeed() { static bool started = false; if (started) { qCDebug(KSudokuLog) << "setSeed(): RESET IS TURNED OFF"; // qsrand (m_stats.seed); // IDW test. } else { started = true; m_stats.seed = std::time(nullptr); qsrand (m_stats.seed); qCDebug(KSudokuLog) << "setSeed(): SEED = " << m_stats.seed; } } bool SudokuBoard::generatePuzzle (BoardContents & puzzle, BoardContents & solution, Difficulty difficultyRequired, Symmetry symmetry) { qCDebug(KSudokuLog) << "Entered generatePuzzle(): difficulty " << difficultyRequired << ", symmetry " << symmetry; setSeed(); SudokuType puzzleType = m_graph->specificType(); if ((puzzleType == Mathdoku) || (puzzleType == KillerSudoku)) { // Generate variants of Mathdoku (aka KenKen TM) or Killer Sudoku types. int maxTries = 10; int numTries = 0; bool success = false; while (true) { MathdokuGenerator mg (m_graph); // Find numbers to satisfy Sudoku rules: they will be the solution. solution = fillBoard(); // Generate a Mathdoku or Killer Sudoku puzzle having this solution. numTries++; success = mg.generateMathdokuTypes (puzzle, solution, &m_KSudokuMoves, difficultyRequired); if (success) { return true; } else if (numTries >= maxTries) { QWidget owner; if (KMessageBox::questionYesNo (&owner, i18n("Attempts to generate a puzzle failed after " "about 200 tries. Try again?"), i18n("Mathdoku or Killer Sudoku Puzzle")) == KMessageBox::No) { return false; // Go back to the Welcome screen. } numTries = 0; // Try again. } } } else { // Generate variants of Sudoku (2D) and Roxdoku (3D) types. return generateSudokuRoxdokuTypes (puzzle, solution, difficultyRequired, symmetry); } } bool SudokuBoard::generateSudokuRoxdokuTypes (BoardContents & puzzle, BoardContents & solution, Difficulty difficultyRequired, Symmetry symmetry) { const int maxTries = 20; int count = 0; float bestRating = 0.0; int bestDifficulty = 0; int bestNClues = 0; int bestNGuesses = 0; int bestFirstGuessAt = 0; BoardContents currPuzzle; BoardContents currSolution; QTime t; t.start(); if (m_graph->sizeZ() > 1) { symmetry = NONE; // Symmetry not implemented in 3-D. } if (symmetry == RANDOM_SYM) { // Choose a symmetry at random. symmetry = (Symmetry) (qrand() % (int) LAST_CHOICE); } qCDebug(KSudokuLog) << "SYMMETRY IS" << symmetry; if (symmetry == DIAGONAL_1) { // If diagonal symmetry, choose between NW->SE and NE->SW diagonals. symmetry = (qrand() % 2 == 0) ? DIAGONAL_1 : DIAGONAL_2; qCDebug(KSudokuLog) << "Diagonal symmetry, choosing " << ((symmetry == DIAGONAL_1) ? "DIAGONAL_1" : "DIAGONAL_2"); } while (true) { // Fill the board with values that satisfy the Sudoku rules but are // chosen in a random way: these values are the solution of the puzzle. currSolution = this->fillBoard(); qCDebug(KSudokuLog) << "Return from fillBoard() - time to fill board:" << t.elapsed() << " msec"; // Randomly insert solution-values into an empty board until a point is // reached where all the cells in the solution can be logically deduced. currPuzzle = insertValues (currSolution, difficultyRequired, symmetry); qCDebug(KSudokuLog) << "Return from insertValues() - duration:" << t.elapsed() << " msec"; if (difficultyRequired > m_stats.difficulty) { // Make the puzzle harder by removing values at random. currPuzzle = removeValues (currSolution, currPuzzle, difficultyRequired, symmetry); qCDebug(KSudokuLog) << "Return from removeValues() - duration:" << t.elapsed() << " msec"; } Difficulty d = calculateRating (currPuzzle, 5); count++; qCInfo(KSudokuLog) << "CYCLE" << count << ", achieved difficulty" << d << ", required" << difficultyRequired << ", rating" << m_accum.rating; // Use the highest rated puzzle so far. if (m_accum.rating > bestRating) { bestRating = m_accum.rating; bestDifficulty = d; bestNClues = m_stats.nClues; bestNGuesses = m_accum.nGuesses; bestFirstGuessAt = m_stats.firstGuessAt; solution = currSolution; puzzle = currPuzzle; } // Express the rating to 1 decimal place in whatever locale we have. QString ratingStr = ki18n("%1").subs(bestRating, 0, 'f', 1).toString(); // Check and explain the Sudoku/Roxdoku puzzle-generator's results. if ((d < difficultyRequired) && (count >= maxTries)) { // Exit after max attempts? QWidget owner; int ans = KMessageBox::questionYesNo (&owner, - i18n("After %1 tries, the best difficulty level achieved " + i18n("After %1 tries, the best difficulty level achieved by the generator " "is %2, with internal difficulty rating %3, but you " - "requested difficulty level %4. Do you wish to try " - "again or accept the puzzle as is?\n" + "requested difficulty level %4.\n" "\n" - "If you accept the puzzle, it may help to change to " - "No Symmetry or some low symmetry type, then use " - "Game->New and try generating another puzzle.", + "Do you wish to let the generator try again or accept the puzzle as is?\n" + "\n" + "Hint: you can try to increase the difficulty rating by doing the following: " + "Continue with the 'Accept' button, choose Game -> New, then change the Symmetry setting " + "to 'No Symmetry' or some low symmetry type and then use 'Generate A Puzzle' again.", maxTries, bestDifficulty, ratingStr, difficultyRequired), i18n("Difficulty Level"), KGuiItem(i18n("&Try Again")), KGuiItem(i18n("&Accept"))); if (ans == KMessageBox::Yes) { count = 0; // Continue on if the puzzle is not hard enough. continue; } break; // Exit if the puzzle is accepted. } if ((d >= difficultyRequired) || (count >= maxTries)) { QWidget owner; int ans = 0; if (m_accum.nGuesses == 0) { ans = KMessageBox::questionYesNo (&owner, i18n("It will be possible to solve the generated puzzle " "by logic alone. No guessing will be required.\n" "\n" "The internal difficulty rating is %1. There are " "%2 clues at the start and %3 moves to go.", ratingStr, bestNClues, (m_stats.nCells - bestNClues)), i18n("Difficulty Level"), KStandardGuiItem::ok(), KGuiItem(i18n("&Retry"))); } else { QString avGuessStr = ki18n("%1").subs(((float) bestNGuesses) / 5.0, 0, 'f', 1).toString(); // Format as for ratingStr. ans = KMessageBox::questionYesNo (&owner, i18n("Solving the generated puzzle will require an " "average of %1 guesses or branch points and if you " "guess wrong, backtracking will be necessary. The " "first guess should come after %2 moves.\n" "\n" "The internal difficulty rating is %3, there are " "%4 clues at the start and %5 moves to go.", avGuessStr, bestFirstGuessAt, ratingStr, bestNClues, (m_stats.nCells - bestNClues)), i18n("Difficulty Level"), KStandardGuiItem::ok(), KGuiItem(i18n("&Retry"))); } // Exit when the required difficulty or number of tries is reached. if (ans == KMessageBox::No) { count = 0; bestRating = 0.0; bestDifficulty = 0; bestNClues = 0; bestNGuesses = 0; bestFirstGuessAt = 0; continue; // Start again if the user rejects this puzzle. } break; // Exit if the puzzle is OK. } } qCDebug(KSudokuLog) << "FINAL PUZZLE" << puzzle; return true; } Difficulty SudokuBoard::calculateRating (const BoardContents & puzzle, int nSamples) { float avGuesses; float avDeduces; float avDeduced; float fracClues; m_accum.nSingles = m_accum.nSpots = m_accum.nGuesses = m_accum.nDeduces = 0; m_accum.rating = 0.0; BoardContents solution; clear (solution); setSeed(); for (int n = 0; n < nSamples; n++) { dbo1 "SOLVE PUZZLE %d\n", n); solution = solveBoard (puzzle, nSamples == 1 ? NotRandom : Random); dbo1 "PUZZLE SOLVED %d\n", n); analyseMoves (m_stats); fracClues = float (m_stats.nClues) / float (m_stats.nCells); m_accum.nSingles += m_stats.nSingles; m_accum.nSpots += m_stats.nSpots; m_accum.nGuesses += m_stats.nGuesses; m_accum.nDeduces += m_stats.nDeduces; m_accum.rating += m_stats.rating; avDeduced = float(m_stats.nSingles + m_stats.nSpots) / m_stats.nDeduces; dbo2 " Type %2d %2d: clues %3d %3d %2.1f%% %3d moves %3dP %3dS %3dG " "%3dM %3dD %3.1fR\n", m_stats.type, m_stats.order, m_stats.nClues, m_stats.nCells, fracClues * 100.0, (m_stats.nCells - m_stats.nClues), m_stats.nSingles, m_stats.nSpots, m_stats.nGuesses, (m_stats.nSingles + m_stats.nSpots + m_stats.nGuesses), m_stats.nDeduces, m_stats.rating); } avGuesses = float (m_accum.nGuesses) / nSamples; avDeduces = float (m_accum.nDeduces) / nSamples; avDeduced = float (m_accum.nSingles + m_accum.nSpots) / m_accum.nDeduces; m_accum.rating = m_accum.rating / nSamples; m_accum.difficulty = calculateDifficulty (m_accum.rating); dbo1 " Av guesses %2.1f Av deduces %2.1f" " Av per deduce %3.1f rating %2.1f difficulty %d\n", avGuesses, avDeduces, avDeduced, m_accum.rating, m_accum.difficulty); return m_accum.difficulty; } int SudokuBoard::checkPuzzle (const BoardContents & puzzle, const BoardContents & solution) { BoardContents answer = solveBoard (puzzle); if (answer.isEmpty()) { dbo1 "checkPuzzle: There is NO SOLUTION.\n"); return -1; // There is no solution. } if ((! solution.isEmpty()) && (answer != solution)) { dbo1 "checkPuzzle: The SOLUTION DIFFERS from the one supplied.\n"); return -2; // The solution differs from the one supplied. } analyseMoves (m_stats); answer.clear(); answer = tryGuesses (Random); if (! answer.isEmpty()) { dbo1 "checkPuzzle: There is MORE THAN ONE SOLUTION.\n"); return -3; // There is more than one solution. } return calculateDifficulty (m_stats.rating); } void SudokuBoard::getMoveList (QList & moveList) { moveList = m_KSudokuMoves; } BoardContents & SudokuBoard::solveBoard (const BoardContents & boardValues, GuessingMode gMode) { qCInfo(KSudokuLog) << "solveBoard()" << boardValues; m_currentValues = boardValues; return solve (gMode); } BoardContents & SudokuBoard::solve (GuessingMode gMode = Random) { // Eliminate any previous solver work. qDeleteAll (m_states); m_states.clear(); m_moves.clear(); m_moveTypes.clear(); int nClues = 0; int nCells = 0; int value = 0; for (int n = 0; n < m_boardArea; n++) { value = m_currentValues.at(n); if (value != m_unusable) { nCells++; if (value != m_vacant) { nClues++; } } } m_stats.nClues = nClues; m_stats.nCells = nCells; dbo1 "STATS: CLUES %d, CELLS %d, PERCENT %.1f\n", nClues, nCells, nClues * 100.0 / float (nCells)); // Attempt to deduce the solution in one hit. GuessesList g = deduceValues (m_currentValues, gMode); if (g.isEmpty()) { // The entire solution can be deduced by applying the Sudoku rules. dbo1 "NO GUESSES NEEDED, the solution can be entirely deduced.\n"); return m_currentValues; } // We need to use a mix of guessing, deducing and backtracking. m_states.push (new State (this, g, 0, m_currentValues, m_moves, m_moveTypes)); return tryGuesses (gMode); } BoardContents & SudokuBoard::tryGuesses (GuessingMode gMode = Random) { while (m_states.count() > 0) { GuessesList guesses = m_states.top()->guesses(); int n = m_states.top()->guessNumber(); if ((n >= guesses.count()) || (guesses.at (0) == -1)) { dbo2 "POP: Out of guesses at level %d\n", m_states.count()); delete m_states.pop(); if (m_states.count() > 0) { m_moves.clear(); m_moveTypes.clear(); m_moves = m_states.top()->moves(); m_moveTypes = m_states.top()->moveTypes(); } continue; } m_states.top()->setGuessNumber (n + 1); m_currentValues = m_states.top()->values(); m_moves.append (guesses.at(n)); m_moveTypes.append (Guess); m_currentValues [pairPos (guesses.at(n))] = pairVal (guesses.at(n)); dbo2 "\nNEXT GUESS: level %d, guess number %d\n", m_states.count(), n); dbo2 " Pick %d %d row %d col %d\n", pairVal (guesses.at(n)), pairPos (guesses.at(n)), pairPos (guesses.at(n))/m_boardSize + 1, pairPos (guesses.at(n))%m_boardSize + 1); guesses = deduceValues (m_currentValues, gMode); if (guesses.isEmpty()) { // NOTE: We keep the stack of states. It is needed by checkPuzzle() // for the multiple-solutions test and deleted when its parent // SudokuBoard object (i.e. this->) is deleted. return m_currentValues; } m_states.push (new State (this, guesses, 0, m_currentValues, m_moves, m_moveTypes)); } // No solution. m_currentValues.clear(); return m_currentValues; } BoardContents SudokuBoard::insertValues (const BoardContents & solution, const Difficulty required, const Symmetry symmetry) { BoardContents puzzle; BoardContents filled; QVector sequence (m_boardArea); int cell = 0; int value = 0; // Set up empty board areas. clear (puzzle); clear (filled); // Add cells in random order, but skip cells that can be deduced from them. dbo1 "Start INSERTING: %d solution values\n", solution.count()); randomSequence (sequence); int index = 0; for (int n = 0; n < m_boardArea; n++) { cell = sequence.at (n); value = filled.at (cell); if (filled.at (cell) == 0) { index = n; changeClues (puzzle, cell, symmetry, solution); changeClues (filled, cell, symmetry, solution); deduceValues (filled, Random /* NotRandom */); qCDebug(KSudokuLog) << "Puzzle:" << puzzle << "; filled" << filled; } } qCDebug(KSudokuLog) << "Puzzle:" << puzzle; while (true) { // Check the difficulty of the puzzle. solveBoard (puzzle); analyseMoves (m_stats); m_stats.difficulty = calculateDifficulty (m_stats.rating); if (m_stats.difficulty <= required) { break; // The difficulty is as required or not enough yet. } // The puzzle needs to be made easier. Add randomly-selected clues. for (int n = index; n < m_boardArea; n++) { cell = sequence.at (n); if (puzzle.at (cell) == 0) { changeClues (puzzle, cell, symmetry, solution); index = n; break; } } dbo1 "At index %d, added value %d, cell %d, row %d, col %d\n", index, solution.at (cell), cell, cell/m_boardSize + 1, cell%m_boardSize + 1); } qCDebug(KSudokuLog) << "Puzzle:" << puzzle; return puzzle; } BoardContents SudokuBoard::removeValues (const BoardContents & solution, BoardContents & puzzle, const Difficulty required, const Symmetry symmetry) { // Make the puzzle harder by removing values at random, making sure at each // step that the puzzle has a solution, the correct solution and only one // solution. Stop when these conditions can no longer be met and the // required difficulty is reached or failed to be reached with the current // (random) selection of board values. // Remove values in random order, but put them back if the solution fails. BoardContents vacant; QVector sequence (m_boardArea); int cell = 0; int value = 0; QList tailOfRemoved; // No guesses until this much of the puzzle, including clues, is filled in. float guessLimit = 0.6; int noGuesses = (int) (guessLimit * m_stats.nCells + 0.5); dbo1 "Guess limit = %.2f, nCells = %d, nClues = %d, noGuesses = %d\n", guessLimit, m_stats.nCells, m_stats.nClues, noGuesses); dbo1 "Start REMOVING:\n"); randomSequence (sequence); clear (vacant); for (int n = 0; n < m_boardArea; n++) { cell = sequence.at (n); value = puzzle.at (cell); if ((value == 0) || (value == m_unusable)) { continue; // Skip empty or unusable cells. } // Try removing this clue and its symmetry partners (if any). changeClues (puzzle, cell, symmetry, vacant); dbo1 "ITERATION %d: Removed %d from cell %d\n", n, value, cell); // Check the solution is still OK and calculate the difficulty roughly. int result = checkPuzzle (puzzle, solution); // Do not force the human solver to start guessing too soon. if ((result >= 0) && (required != Unlimited) && (m_stats.firstGuessAt <= (noGuesses - m_stats.nClues))) { dbo1 "removeValues: FIRST GUESS is too soon: move %d of %d.\n", m_stats.firstGuessAt, m_stats.nCells - m_stats.nClues); result = -4; } // If the solution is not OK, replace the removed value(s). if (result < 0) { dbo1 "ITERATION %d: Replaced %d at cell %d, check returned %d\n", n, value, cell, result); changeClues (puzzle, cell, symmetry, solution); } // If the solution is OK, check the difficulty (roughly). else { m_stats.difficulty = (Difficulty) result; dbo1 "CURRENT DIFFICULTY %d\n", m_stats.difficulty); if (m_stats.difficulty == required) { // Save removed positions while the difficulty is as required. tailOfRemoved.append (cell); dbo1 "OVERSHOOT %d at sequence %d\n", tailOfRemoved.count(), n); } else if (m_stats.difficulty > required) { // Finish if the required difficulty is exceeded. qCInfo(KSudokuLog) << "Break on difficulty - replaced" << value << "at cell" << cell << ", overshoot is" << tailOfRemoved.count(); // Replace the value involved. changeClues (puzzle, cell, symmetry, solution); break; } } } // If the required difficulty was reached and was not Unlimited, replace // half the saved values. // // This should avoid chance fluctuations in the calculated difficulty (when // the solution involves guessing) and provide a puzzle that is within the // required difficulty range. if ((required != Unlimited) && (tailOfRemoved.count() > 1)) { for (int k = 0; k < tailOfRemoved.count() / 2; k++) { cell = tailOfRemoved.takeLast(); dbo1 "Replaced clue(s) for cell %d\n", cell); changeClues (puzzle, cell, symmetry, solution); } } return puzzle; } void SudokuBoard::analyseMoves (Statistics & s) { dbo1 "\nanalyseMoves()\n"); s.nCells = m_stats.nCells; s.nClues = m_stats.nClues; s.firstGuessAt = s.nCells - s.nClues + 1; s.nSingles = s.nSpots = s.nDeduces = s.nGuesses = 0; m_KSudokuMoves.clear(); Move m; Move mType; while (! m_moves.isEmpty()) { m = m_moves.takeFirst(); mType = m_moveTypes.takeFirst(); int val = pairVal(m); int pos = pairPos(m); int row = m_graph->cellPosY (pos); int col = m_graph->cellPosX (pos); switch (mType) { case Single: dbo2 " Single Pick %d %d row %d col %d\n", val, pos, row+1, col+1); m_KSudokuMoves.append (pos); s.nSingles++; break; case Spot: dbo2 " Single Spot %d %d row %d col %d\n", val, pos, row+1, col+1); m_KSudokuMoves.append (pos); s.nSpots++; break; case Deduce: dbo2 "Deduce: Iteration %d\n", m); s.nDeduces++; break; case Guess: dbo2 "GUESS: %d %d row %d col %d\n", val, pos, row+1, col+1); m_KSudokuMoves.append (pos); if (s.nGuesses < 1) { s.firstGuessAt = s.nSingles + s.nSpots + 1; } s.nGuesses++; break; case Wrong: dbo2 "WRONG GUESS: %d %d row %d col %d\n", val, pos, row+1, col+1); break; case Result: break; } } // Calculate the empirical formula for the difficulty rating. Note that // guess-points are effectively weighted by 3, because the deducer must // always iterate one more time to establish that a guess is needed. s.rating = 2 * s.nGuesses + s.nDeduces - (float(s.nClues)/s.nCells); // Calculate the difficulty level for empirical ranges of the rating. s.difficulty = calculateDifficulty (s.rating); dbo1 " aM: Type %2d %2d: clues %3d %3d %2.1f%% %3dP %3dS %3dG " "%3dM %3dD %3.1fR D=%d F=%d\n\n", m_stats.type, m_stats.order, s.nClues, s.nCells, ((float) s.nClues / s.nCells) * 100.0, s.nSingles, s.nSpots, s.nGuesses, (s.nSingles + s.nSpots + s.nGuesses), s.nDeduces, s.rating, s.difficulty, s.firstGuessAt); } Difficulty SudokuBoard::calculateDifficulty (float rating) { // These ranges of the rating were arrived at empirically by solving a few // dozen published puzzles and comparing SudokuBoard's rating value with the // description of difficulty given by the publisher, e.g. Diabolical or Evil // puzzles gave ratings in the range 10.0 to 20.0, so became Diabolical. Difficulty d = Unlimited; if (rating < 1.7) { d = VeryEasy; } else if (rating < 2.7) { d = Easy; } else if (rating < 4.6) { d = Medium; } else if (rating < 10.0) { d = Hard; } else if (rating < 20.0) { d = Diabolical; } return d; } void SudokuBoard::print (const BoardContents & boardValues) { // Used for test and debug, but the format is also parsable and loadable. char nLabels[] = "123456789"; char aLabels[] = "abcdefghijklmnopqrstuvwxy"; int index, value; if (boardValues.size() != m_boardArea) { printf ("Error: %d board values to be printed, %d values required.\n\n", boardValues.size(), m_boardArea); return; } int depth = m_graph->sizeZ(); // If 2-D, depth == 1, else depth > 1. for (int k = 0; k < depth; k++) { int z = (depth > 1) ? (depth - k - 1) : k; for (int j = 0; j < m_graph->sizeY(); j++) { if ((j != 0) && (j % m_blockSize == 0)) { printf ("\n"); // Gap between square blocks. } int y = (depth > 1) ? (m_graph->sizeY() - j - 1) : j; for (int x = 0; x < m_graph->sizeX(); x++) { index = m_graph->cellIndex (x, y, z); value = boardValues.at (index); if (x % m_blockSize == 0) { printf (" "); // Gap between square blocks. } if (value == m_unusable) { printf (" '"); // Unused cell (e.g. in Samurai). } else if (value == 0) { printf (" -"); // Empty cell (to be solved). } else { value--; char label = (m_order > 9) ? aLabels[value] : nLabels[value]; printf (" %c", label); // Given cell (or clue). } } printf ("\n"); // End of row. } printf ("\n"); // Next Z or end of 2D puzzle/solution. } } GuessesList SudokuBoard::deduceValues (BoardContents & boardValues, GuessingMode gMode = Random) { int iteration = 0; setUpValueRequirements (boardValues); while (true) { iteration++; m_moves.append (iteration); m_moveTypes.append (Deduce); dbo2 "DEDUCE: Iteration %d\n", iteration); bool stuck = true; int count = 0; GuessesList guesses; for (int cell = 0; cell < m_boardArea; cell++) { if (boardValues.at (cell) == m_vacant) { GuessesList newGuesses; qint32 numbers = m_validCellValues.at (cell); dbo3 "Cell %d, valid numbers %03o\n", cell, numbers); if (numbers == 0) { dbo2 "SOLUTION FAILED: RETURN at cell %d\n", cell); return solutionFailed (guesses); } int validNumber = 1; while (numbers != 0) { dbo3 "Numbers = %03o, validNumber = %d\n", numbers, validNumber); if (numbers & 1) { newGuesses.append (setPair (cell, validNumber)); } numbers = numbers >> 1; validNumber++; } if (newGuesses.count() == 1) { m_moves.append (newGuesses.first()); m_moveTypes.append (Single); boardValues [cell] = pairVal (newGuesses.takeFirst()); dbo3 " Single Pick %d %d row %d col %d\n", boardValues.at (cell), cell, cell/m_boardSize + 1, cell%m_boardSize + 1); updateValueRequirements (boardValues, cell); stuck = false; } else if (stuck) { // Select a list of guesses. if (guesses.isEmpty() || (newGuesses.count() < guesses.count())) { guesses = newGuesses; count = 1; } else if (newGuesses.count() > guesses.count()) { ; } else if (gMode == Random) { if ((qrand() % count) == 0) { guesses = newGuesses; } count++; } } } // End if } // Next cell for (int group = 0; group < m_nGroups; group++) { QVector cellList = m_graph->clique (group); qint32 numbers = m_requiredGroupValues.at (group); dbo3 "Group %d, valid numbers %03o\n", group, numbers); if (numbers == 0) { continue; } int validNumber = 1; qint32 bit = 1; int cell = 0; while (numbers != 0) { if (numbers & 1) { GuessesList newGuesses; int index = group * m_groupSize; for (int n = 0; n < m_groupSize; n++) { cell = cellList.at (n); if ((m_validCellValues.at (cell) & bit) != 0) { newGuesses.append (setPair (cell, validNumber)); } index++; } if (newGuesses.isEmpty()) { dbo2 "SOLUTION FAILED: RETURN at group %d\n", group); return solutionFailed (guesses); } else if (newGuesses.count() == 1) { m_moves.append (newGuesses.first()); m_moveTypes.append (Spot); cell = pairPos (newGuesses.takeFirst()); boardValues [cell] = validNumber; dbo3 " Single Spot in Group %d value %d %d " "row %d col %d\n", group, validNumber, cell, cell/m_boardSize + 1, cell%m_boardSize + 1); updateValueRequirements (boardValues, cell); stuck = false; } else if (stuck) { // Select a list of guesses. if (guesses.isEmpty() || (newGuesses.count() < guesses.count())) { guesses = newGuesses; count = 1; } else if (newGuesses.count() > guesses.count()) { ; } else if (gMode == Random){ if ((qrand() % count) == 0) { guesses = newGuesses; } count++; } } } // End if (numbers & 1) numbers = numbers >> 1; bit = bit << 1; validNumber++; } // Next number } // Next group if (stuck) { GuessesList original = guesses; if (gMode == Random) { // Shuffle the guesses. QVector sequence (guesses.count()); randomSequence (sequence); guesses.clear(); for (int i = 0; i < original.count(); i++) { guesses.append (original.at (sequence.at (i))); } } dbo2 "Guess "); for (int i = 0; i < original.count(); i++) { dbo3 "%d,%d ", pairPos (original.at(i)), pairVal (original.at(i))); } dbo2 "\n"); dbo2 "Shuffled "); for (int i = 0; i < guesses.count(); i++) { dbo3 "%d,%d ", pairPos (guesses.at (i)), pairVal (guesses.at(i))); } dbo2 "\n"); return guesses; } } // End while (true) } GuessesList SudokuBoard::solutionFailed (GuessesList & guesses) { guesses.clear(); guesses.append (-1); return guesses; } void SudokuBoard::clear (BoardContents & boardValues) { boardValues = m_graph->emptyBoard(); // Set cells vacant or unusable. } BoardContents & SudokuBoard::fillBoard() { // Solve the empty board, thus filling it with values at random. These // values can be the starting point for generating a puzzle and also the // final solution of that puzzle. clear (m_currentValues); // Fill a central block with values 1 to m_order in random sequence. This // reduces the solveBoard() time considerably, esp. if blockSize is 4 or 5. QVector sequence (m_order); QVector cellList = m_graph->clique (m_nGroups / 2); randomSequence (sequence); for (int n = 0; n < m_order; n++) { m_currentValues [cellList.at (n)] = sequence.at (n) + 1; } solveBoard (m_currentValues); dbo1 "BOARD FILLED\n"); return m_currentValues; } void SudokuBoard::randomSequence (QVector & sequence) { if (sequence.isEmpty()) return; // Fill the vector with consecutive integers. int size = sequence.size(); for (int i = 0; i < size; i++) { sequence [i] = i; } if (size == 1) return; // Shuffle the integers. int last = size; int z = 0; int temp = 0; for (int i = 0; i < size; i++) { z = qrand() % last; last--; temp = sequence.at (z); sequence [z] = sequence.at (last); sequence [last] = temp; } } void SudokuBoard::setUpValueRequirements (BoardContents & boardValues) { // Set a 1-bit for each possible cell-value in this order of Sudoku, for // example 9 bits for a 9x9 grid with 3x3 blocks. qint32 allValues = (1 << m_order) - 1; dbo2 "Enter setUpValueRequirements()\n"); if (dbgLevel >= 2) { this->print (boardValues); } // Set bit-patterns to show what values each row, col or block needs. // The starting pattern is allValues, but bits are set to zero as the // corresponding values are supplied during puzzle generation and solving. m_requiredGroupValues.fill (0, m_nGroups); int index = 0; qint32 bitPattern = 0; for (int group = 0; group < m_nGroups; group++) { dbo3 "Group %3d ", group); QVector cellList = m_graph->clique (group); bitPattern = 0; for (int n = 0; n < m_groupSize; n++) { int value = boardValues.at (cellList.at (n)) - 1; if (value != m_unusable) { bitPattern |= (1 << value); // Add bit for each value found. } dbo3 "%3d=%2d ", cellList.at (n), value + 1); index++; } // Reverse all the bits, giving values currently not found in the group. m_requiredGroupValues [group] = bitPattern ^ allValues; dbo3 "bits %03o\n", m_requiredGroupValues.at (group)); } // Set bit-patterns to show that each cell can accept any value. Bits are // set to zero as possibilities for each cell are eliminated when solving. m_validCellValues.fill (allValues, m_boardArea); for (int i = 0; i < m_boardArea; i++) { if (boardValues.at (i) == m_unusable) { // No values are allowed in unusable cells (e.g. in Samurai type). m_validCellValues [i] = 0; } if (boardValues.at (i) != m_vacant) { // Cell is already filled in. m_validCellValues [i] = 0; } } // Now, for each cell, retain bits for values that are required by every // group to which that cell belongs. For example, if the row already has 1, // 2, 3, the column has 3, 4, 5, 6 and the block has 6, 9, then the cell // can only have 7 or 8, with bit value 192. index = 0; for (int group = 0; group < m_nGroups; group++) { QVector cellList = m_graph->clique (group); for (int n = 0; n < m_order; n++) { int cell = cellList.at (n); m_validCellValues [cell] &= m_requiredGroupValues.at (group); index++; } } dbo2 "Finished setUpValueRequirements()\n"); dbo3 "allowed:\n"); for (int i = 0; i < m_boardArea; i++) { dbo3 "'%03o', ", m_validCellValues.at (i)); if ((i + 1) % m_boardSize == 0) dbo3 "\n"); } dbo3 "needed:\n"); for (int group = 0; group < m_nGroups; group++) { dbo3 "'%03o', ", m_requiredGroupValues.at (group)); if ((group + 1) % m_order == 0) dbo3 "\n"); } dbo3 "\n"); } void SudokuBoard::updateValueRequirements (BoardContents & boardValues, int cell) { // Set a 1-bit for each possible cell-value in this order of Sudoku. qint32 allValues = (1 << m_order) - 1; // Set a complement-mask for this cell's new value. qint32 bitPattern = (1 << (boardValues.at (cell) - 1)) ^ allValues; // Show that this cell no longer requires values: it has been filled. m_validCellValues [cell] = 0; // Update the requirements for each group to which this cell belongs. const QList groupList = m_graph->cliqueList(cell); for (int group : groupList) { m_requiredGroupValues [group] &= bitPattern; QVector cellList = m_graph->clique (group); for (int n = 0; n < m_order; n++) { int cell = cellList.at (n); m_validCellValues [cell] &= bitPattern; } } } void SudokuBoard::changeClues (BoardContents & to, int cell, Symmetry type, const BoardContents & from) { int nSymm = 1; int indices[8]; nSymm = getSymmetricIndices (m_boardSize, type, cell, indices); for (int k = 0; k < nSymm; k++) { cell = indices [k]; to [cell] = from.at (cell); } } int SudokuBoard::getSymmetricIndices (int size, Symmetry type, int index, int * out) { int result = 1; out[0] = index; if (type == NONE) { return result; } int row = m_graph->cellPosY (index); int col = m_graph->cellPosX (index); int lr = size - col - 1; // For left-to-right reflection. int tb = size - row - 1; // For top-to-bottom reflection. switch (type) { case DIAGONAL_1: // Reflect a copy of the point around two central axes making its // reflection in the NW-SE diagonal the same as for NE-SW diagonal. row = tb; col = lr; // No break; fall through to case DIAGONAL_2. case DIAGONAL_2: // Reflect (col, row) in the main NW-SE diagonal by swapping coords. out[1] = m_graph->cellIndex(row, col); result = (out[1] == out[0]) ? 1 : 2; break; case CENTRAL: out[1] = (size * size) - index - 1; result = (out[1] == out[0]) ? 1 : 2; break; case SPIRAL: if ((size % 2 != 1) || (row != col) || (col != (size - 1)/2)) { result = 4; // This is not the central cell. out[1] = m_graph->cellIndex(lr, tb); out[2] = m_graph->cellIndex(row, lr); out[3] = m_graph->cellIndex(tb, col); } break; case FOURWAY: out[1] = m_graph->cellIndex(row, col); // Interchange X and Y. out[2] = m_graph->cellIndex(lr, row); // Left-to-right. out[3] = m_graph->cellIndex(row, lr); // Interchange X and Y. out[4] = m_graph->cellIndex(col, tb); // Top-to-bottom. out[5] = m_graph->cellIndex(tb, col); // Interchange X and Y. out[6] = m_graph->cellIndex(lr, tb); // Both L-R and T-B. out[7] = m_graph->cellIndex(tb, lr); // Interchange X and Y. int k; for (int n = 1; n < 8; n++) { for (k = 0; k < result; k++) { if (out[n] == out[k]) { break; // Omit duplicates. } } if (k >= result) { out[result] = out[n]; result++; // Use unique positions. } } break; case LEFT_RIGHT: out[1] = m_graph->cellIndex(lr, row); result = (out[1] == out[0]) ? 1 : 2; break; default: break; } return result; } diff --git a/src/gui/ksudoku.cpp b/src/gui/ksudoku.cpp index 3371f1a..b142d9e 100644 --- a/src/gui/ksudoku.cpp +++ b/src/gui/ksudoku.cpp @@ -1,926 +1,931 @@ /*************************************************************************** * Copyright 2005-2007 Francesco Rossi * * Copyright 2006-2007 Mick Kappenburg * * Copyright 2006-2008 Johannes Bergmeier * * Copyright 2012,2015 Ian Wadham * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "ksudoku_logging.h" #include "globals.h" #include "ksudoku.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define USE_UNSTABLE_LIBKDEGAMESPRIVATE_API #include #include "ksview.h" #include "gameactions.h" #include "renderer.h" #include "puzzle.h" // TODO #include "skgraph.h" #include "serializer.h" #include "puzzleprinter.h" #include "gamevariants.h" #include "welcomescreen.h" #include "valuelistwidget.h" #include "settings.h" #include "config.h" using namespace ksudoku; void KSudoku::onCompleted(bool isCorrect, const QTime& required, bool withHelp) { if(!isCorrect) { KMessageBox::information(this, i18n("Sorry, your solution contains mistakes.\n\nEnable \"Show errors\" in the settings to highlight them.")); return; } QString msg; int secs = QTime(0,0).secsTo(required); int mins = secs / 60; secs = secs % 60; if(withHelp) if (mins == 0) msg = i18np("Congratulations! You made it in 1 second. With some tricks.", "Congratulations! You made it in %1 seconds. With some tricks.", secs); else if (secs == 0) msg = i18np("Congratulations! You made it in 1 minute. With some tricks.", "Congratulations! You made it in %1 minutes. With some tricks.", mins); else msg = i18nc("The two parameters are strings like '2 minutes' or '1 second'.", "Congratulations! You made it in %1 and %2. With some tricks.", i18np("1 minute", "%1 minutes", mins), i18np("1 second", "%1 seconds", secs)); else if (mins == 0) msg = i18np("Congratulations! You made it in 1 second.", "Congratulations! You made it in %1 seconds.", secs); else if (secs == 0) msg = i18np("Congratulations! You made it in 1 minute.", "Congratulations! You made it in %1 minutes.", mins); else msg = i18nc("The two parameters are strings like '2 minutes' or '1 second'.", "Congratulations! You made it in %1 and %2.", i18np("1 minute", "%1 minutes", mins), i18np("1 second", "%1 seconds", secs)); onModified(true); // make sure buttons have the correct enabled state KMessageBox::information(this, msg); } // void KSudoku::updateStatusBar() // { // QString m=""; // // QWidget* current = m_tabs->currentPage(); // // if(KsView* view = dynamic_cast(current)) // // m = view->status(); // // if(currentView()) // // m = currentView()->status(); // // // TODO fix this: add new status bar generation code // // statusBar()->showMessage(m); // } KSudoku::KSudoku() : KXmlGuiWindow(), m_gameVariants(new GameVariantCollection(this, true)), m_puzzlePrinter(0) { setObjectName( QLatin1String("ksudoku" )); m_gameWidget = 0; m_gameUI = 0; m_gameActions = 0; // then, setup our actions setupActions(); setupGUI(ToolBar | Keys | Save | Create | StatusBar); wrapper = new QWidget(); (void) new QHBoxLayout(wrapper); QMainWindow::setCentralWidget(wrapper); wrapper->show(); // Create ValueListWidget m_valueListWidget = new ValueListWidget(wrapper); wrapper->layout()->addWidget(m_valueListWidget); m_valueListWidget->setFixedWidth(60); m_welcomeScreen = new WelcomeScreen(wrapper, m_gameVariants); wrapper->layout()->addWidget(m_welcomeScreen); connect(m_welcomeScreen, &ksudoku::WelcomeScreen::newGameStarted, this, &KSudoku::startGame); setupStatusBar(m_welcomeScreen->difficulty(), m_welcomeScreen->symmetry()); showWelcomeScreen(); updateShapesList(); // QTimer *timer = new QTimer( this ); // connect( timer, SIGNAL(timeout()), this, SLOT(updateStatusBar()) ); // updateStatusBar(); // timer->start( 1000); //TODO PORT, false ); // 2 seconds single-shot timer } KSudoku::~KSudoku() { delete m_puzzlePrinter; endCurrentGame(); } void KSudoku::updateShapesList() { // TODO clear the list GameVariant* variant = 0; variant = new SudokuGame(i18n("Sudoku Standard (9x9)"), 9, m_gameVariants); variant->setDescription(i18n("The classic and fashionable game")); variant->setIcon("ksudoku-ksudoku_9x9"); #ifdef OPENGL_SUPPORT variant = new RoxdokuGame(i18n("Roxdoku 9 (3x3x3)"), 9, m_gameVariants); variant->setDescription(i18n("The Rox 3D Sudoku")); variant->setIcon("ksudoku-roxdoku_3x3x3"); #endif const QStringList gamevariantdirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, "ksudoku", QStandardPaths::LocateDirectory); QFileInfoList filepaths; for (const QString& gamevariantdir : gamevariantdirs) { const auto fileNames = QDir(gamevariantdir).entryInfoList(QStringList() << QStringLiteral("*.desktop"), QDir::Files | QDir::Readable | QDir::NoDotAndDotDot); filepaths.append(fileNames); } QString variantName; QString variantDescr; QString variantDataPath; QString variantIcon; for (const QFileInfo &configFileInfo : qAsConst(filepaths)) { const QDir variantDir = configFileInfo.dir(); KConfig variantConfig(configFileInfo.filePath(), KConfig::SimpleConfig); KConfigGroup group = variantConfig.group ("KSudokuVariant"); variantName = group.readEntry("Name", i18n("Missing Variant Name")); // Translated. variantDescr = group.readEntry("Description", ""); // Translated. variantIcon = group.readEntry("Icon", "ksudoku-ksudoku_9x9"); const QString variantDataFile = group.readEntry("FileName", ""); if(variantDataFile == "") continue; variantDataPath = variantDir.filePath(variantDataFile); variant = new CustomGame(variantName, QUrl::fromLocalFile(variantDataPath), m_gameVariants); variant->setDescription(variantDescr); variant->setIcon(variantIcon); } // Put variants first and extra sizes last. variant = new SudokuGame(i18n("Sudoku 16x16"), 16, m_gameVariants); variant->setDescription(i18n("Sudoku with 16 symbols")); variant->setIcon("ksudoku-ksudoku_16x16"); variant = new SudokuGame(i18n("Sudoku 25x25"), 25, m_gameVariants); variant->setDescription(i18n("Sudoku with 25 symbols")); variant->setIcon("ksudoku-ksudoku_25x25"); #ifdef OPENGL_SUPPORT variant = new RoxdokuGame(i18n("Roxdoku 16 (4x4x4)"), 16, m_gameVariants); variant->setDescription(i18n("The Rox 3D sudoku with 16 symbols")); variant->setIcon("ksudoku-roxdoku_4x4x4"); variant = new RoxdokuGame(i18n("Roxdoku 25 (5x5x5)"), 25, m_gameVariants); variant->setDescription(i18n("The Rox 3D sudoku with 25 symbols")); variant->setIcon("ksudoku-roxdoku_5x5x5"); #endif } void KSudoku::startGame(const Game& game) { m_welcomeScreen->hide(); endCurrentGame(); KsView* view = new KsView(game, m_gameActions, this); view->setValueListWidget(m_valueListWidget); view->createView(); connect(view, &KsView::valueSelected, m_valueListWidget, &ksudoku::ValueListWidget::selectValue); connect(m_valueListWidget, &ksudoku::ValueListWidget::valueSelected, view, &KsView::selectValue); // connect(view, SIGNAL(valueSelected(int)), SLOT(updateStatusBar())); QWidget* widget = view->widget(); m_gameUI = view; Game g = currentGame(); g.setMessageParent(view->widget()); wrapper->layout()->addWidget(widget); widget->show(); widget->setFocus(); connect(game.interface(), SIGNAL(completed(bool,QTime,bool)), SLOT(onCompleted(bool,QTime,bool))); connect(game.interface(), SIGNAL(modified(bool)), SLOT(onModified(bool))); adaptActions2View(); QSizePolicy policy(QSizePolicy::Expanding, QSizePolicy::Expanding); policy.setHorizontalStretch(1); policy.setVerticalStretch(1); widget->setSizePolicy(policy); m_valueListWidget->setMaxValue(view->game().order()); m_valueListWidget->selectValue(1); m_valueListWidget->show(); SudokuType t = game.puzzle()->graph()->specificType(); bool playing = game.puzzle()->hasSolution(); if (playing && (t == Mathdoku)) { KMessageBox::information (this, i18n("Mathdoku puzzles can have any size from 3x3 up to 9x9. " "The solution is a grid in which every row and every " "column contains the available digits (1-3 up to 1-9) " "exactly once. The grid is covered with irregularly " "shaped cages.\n" "\n" "Cages of size 1 are starting-values or clues, but there " "are not many of them. Cages of larger size have a target " "value and an arithmetic operator (+-x/). The digits in " "the cage must combine together, using the operator, to " "reach the target value, e.g. '12x' means that the digits " "must multiply together to make 12. A digit can occur " "more than once in a cage, provided it occurs in " "different rows and columns.\n" "\n" "In general, larger Mathdokus are more difficult and so " "are larger cages. You can select the puzzle size in " "KSudoku's Settings dialog and the maximum cage-size by " "using KSudoku's Difficulty button."), i18n("Playing Mathdoku"), QString("PlayingMathdoku")); } else if (playing && (t == KillerSudoku)) { KMessageBox::information (this, i18n("Killer Sudoku puzzles can have sizes 4x4 or 9x9, with " "either four 2x2 blocks or nine 3x3 blocks. The solution " "must follow Classic Sudoku rules. The difference is that " "there are few starting-values or clues (if any). Instead " "the grid is covered with irregularly shaped cages.\n" "\n" "Cages of size 1 are starting-values or clues. Cages of " "larger size have a target value and the digits in them " "must add up to that value. In Killer Sudoku, a cage " "cannot contain any digit more than once.\n" "\n" "In general, larger cages are more difficult. You can " "select the maximum cage-size by using KSudoku's " "Difficulty button."), i18n("Playing Killer Sudoku"), QString("PlayingKillerSudoku")); } else if ((t == Mathdoku) || (t == KillerSudoku)) { KMessageBox::information (this, i18n("Mathdoku and Killer Sudoku puzzles have to be keyed in " "by working on one cage at a time. To start a cage, left " "click on any unused cell or enter a number in the cell " "that is under the cursor or enter + - / or x there. A " "small cage-label will appear in that cell. To extend the " "cage in any direction, left-click on a neigbouring cell " "or move the cursor there and type a Space.\n" "\n" "The number you type is the cage's value and it can have " "one or more digits, including zero. A cell of size 1 has " "to have a 1-digit number, as in a normal Sudoku puzzle. " "It becomes a starting-value or clue for the player.\n" "\n" "The + - / or x is the operator (Add, Subtract, Divide or " "Multiply). You must have one in cages of size 2 or more. " "In Killer Sudoku, the operator is provided automatically " "because it is always + or none.\n" "\n" "You can enter digits, operators and cells in any order. " "To complete the cage and start another cage, always " "press Return. If you make a mistake, the only thing to " "do is delete a whole cage and re-enter it. Use right " "click in the current cage or any earlier cage, if you " "wish to delete it. Alternatively, use the cursor and the " "Delete or Backspace key.\n" "\n" "When the grid is filled with cages, hit the Check " "button, to solve the puzzle and make sure there is only " "one solution. If the check fails, you have probably made " "an error somewhere in one of the cages."), i18n("Data-entry for Puzzles with Cages"), QString("CageDataEntry")); } } void KSudoku::endCurrentGame() { m_valueListWidget->hide(); delete m_gameUI; m_gameUI = 0; adaptActions2View(); } void KSudoku::loadGame(const QUrl& Url) { QString errorMsg; const Game game = ksudoku::Serializer::load(Url, this, errorMsg); if(!game.isValid()) { KMessageBox::information(this, errorMsg); return; } startGame(game); } void KSudoku::showWelcomeScreen() { endCurrentGame(); m_welcomeScreen->show(); } void KSudoku::giveHint() { Game game = currentGame(); if(!game.isValid()) return; game.giveHint(); } void KSudoku::autoSolve() { Game game = currentGame(); if(!game.isValid()) return; game.autoSolve(); } // Check the game setup, copy the puzzle, init and solve the copy and show the // result (i.e. implement the "Check" action). If the user agrees, start play. void KSudoku::dubPuzzle() { Game game = currentGame(); if(!game.isValid()) return; if(!game.simpleCheck()) { KMessageBox::information(this, i18n("The puzzle you entered contains some errors.")); return; } // Create a new Puzzle object, with same Graph and solution flag = true. ksudoku::Puzzle* puzzle = game.puzzle()->dubPuzzle(); // Copy the given values of the puzzle, then run it through the solver. // The solution, if valid, is saved in puzzle->m_solution2. int state = puzzle->init(game.allValues()); if(state <= 0) { KMessageBox::information (this, i18n("Sorry, no solutions have been found. Please check " "that you have entered in the puzzle completely and " "correctly."), i18n("Check Puzzle")); delete puzzle; return; } else if(state == 1) { KMessageBox::information (this, i18n("The Puzzle you entered has a unique solution and " "is ready to be played."), i18n("Check Puzzle")); } else { KMessageBox::information (this, i18n("The Puzzle you entered has multiple solutions. " "Please check that you have entered in the puzzle " "completely and correctly."), i18n("Check Puzzle")); } if(KMessageBox::questionYesNo(this, i18n("Do you wish to play the puzzle now?"), i18n("Play Puzzle"), KGuiItem(i18n("Play")), KStandardGuiItem::cancel() ) == KMessageBox::Yes) { startGame(ksudoku::Game(puzzle)); } else { delete puzzle; } return; } void KSudoku::genMultiple() { //KMessageBox::information(this, i18n("Sorry, this feature is under development.")); } void KSudoku::setupActions() { m_gameActions = new ksudoku::GameActions(actionCollection()); m_gameActions->init(); QKeySequence shortcut; setAcceptDrops(true); KStandardGameAction::gameNew(this, SLOT(gameNew()), actionCollection()); KStandardGameAction::restart(this, SLOT(gameRestart()), actionCollection()); KStandardGameAction::load(this, SLOT(gameOpen()), actionCollection()); m_gameSave = KStandardGameAction::save(this, SLOT(gameSave()), actionCollection()); m_gameSaveAs = KStandardGameAction::saveAs(this, SLOT(gameSaveAs()), actionCollection()); KStandardGameAction::print(this, SLOT(gamePrint()), actionCollection()); KStandardGameAction::quit(this, SLOT(close()), actionCollection()); // TODO Export is disabled due to missing port to KDE4. // createAction("game_export", SLOT(gameExport()), i18n("Export")); KStandardAction::preferences(this, SLOT(optionsPreferences()), actionCollection()); // Settings: enable messages that the user marked "Do not show again". QAction* enableMessagesAct = new QAction(i18n("Enable all messages"),0); actionCollection()->addAction("enable_messages", enableMessagesAct); connect(enableMessagesAct, SIGNAL(triggered()), SLOT(enableMessages())); //History KStandardGameAction::undo(this, SLOT(undo()), actionCollection()); KStandardGameAction::redo(this, SLOT(redo()), actionCollection()); QAction * a = KStandardGameAction::hint(this, SLOT(giveHint()), actionCollection()); // The default value (H) conflicts with the keys assigned // to add letter/numbers to the board. actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::Key_F2)); KStandardGameAction::solve(this, SLOT(autoSolve()), actionCollection()); a = new QAction(this); actionCollection()->addAction( QLatin1String( "move_dub_puzzle" ), a); a->setText(i18n("Check")); a->setIcon(QIcon::fromTheme( QLatin1String( "games-endturn" ))); connect(a, &QAction::triggered, this, &KSudoku::dubPuzzle); addAction(a); } void KSudoku::setupStatusBar (int difficulty, int symmetry) { // Use the standard combo box for difficulty, from KDE Games library. const int nStandardLevels = 4; const KGameDifficulty::standardLevel standardLevels[nStandardLevels] = {KGameDifficulty::VeryEasy, KGameDifficulty::Easy, KGameDifficulty::Medium, KGameDifficulty::Hard}; statusBar()->addPermanentWidget (new QLabel (i18n("Difficulty"))); KGameDifficulty::init (this, this, SLOT (difficultyChanged(KGameDifficulty::standardLevel)), SLOT (difficultyChanged(int))); KGameDifficulty::addStandardLevel(KGameDifficulty::VeryEasy); KGameDifficulty::addStandardLevel(KGameDifficulty::Easy); KGameDifficulty::addStandardLevel(KGameDifficulty::Medium); KGameDifficulty::addStandardLevel(KGameDifficulty::Hard); KGameDifficulty::addCustomLevel(Diabolical, i18nc("A level of difficulty in Sudoku puzzles", "Diabolical")); KGameDifficulty::addCustomLevel(Unlimited, i18nc("A level of difficulty in Sudoku puzzles", "Unlimited")); KGameDifficulty::setRestartOnChange(KGameDifficulty::NoRestartOnChange); // Set default value of difficulty. if (difficulty < nStandardLevels) { KGameDifficulty::setLevel (standardLevels[difficulty]); } else { KGameDifficulty::setLevelCustom (difficulty); } KGameDifficulty::setEnabled (true); // Set up a combo box for symmetry of puzzle layout. statusBar()->addPermanentWidget (new QLabel (i18n("Symmetry"))); QComboBox * symmetryBox = new QComboBox (this); QObject::connect(symmetryBox, static_cast(&QComboBox::activated), this, &KSudoku::symmetryChanged); symmetryBox->setToolTip(i18nc( "Symmetry of layout of clues when puzzle starts", "Symmetry")); symmetryBox->setWhatsThis(i18n( "The symmetry of layout of the clues when the puzzle starts")); statusBar()->addPermanentWidget (symmetryBox); symmetryBox->addItem(i18nc("Symmetry of layout of clues", "Diagonal")); symmetryBox->addItem(i18nc("Symmetry of layout of clues", "Central")); symmetryBox->addItem(i18nc("Symmetry of layout of clues", "Left-Right")); symmetryBox->addItem(i18nc("Symmetry of layout of clues", "Spiral")); symmetryBox->addItem(i18nc("Symmetry of layout of clues", "Four-Way")); symmetryBox->addItem(i18nc("Symmetry of layout of clues", "Random Choice")); symmetryBox->addItem(i18n("No Symmetry")); symmetryBox->setCurrentIndex (symmetry); } void KSudoku::adaptActions2View() { Game game = currentGame(); m_gameSave->setEnabled(game.isValid()); m_gameSaveAs->setEnabled(game.isValid()); action("game_new")->setEnabled(game.isValid()); action("game_restart")->setEnabled(game.isValid()); action("game_print")->setEnabled(game.isValid()); if(game.isValid()) { bool isEnterPuzzleMode = !game.puzzle()->hasSolution(); action("move_hint")->setVisible(!isEnterPuzzleMode); action("move_solve")->setVisible(!isEnterPuzzleMode); action("move_dub_puzzle")->setVisible(isEnterPuzzleMode); action("move_undo")->setEnabled(game.canUndo()); action("move_redo")->setEnabled(game.canRedo()); action("move_hint") ->setEnabled( game.puzzle()->hasSolution()); action("move_solve") ->setEnabled( game.puzzle()->hasSolution()); action("move_dub_puzzle")->setEnabled( ! game.puzzle()->hasSolution()); } else { action("move_undo")->setEnabled(false); action("move_redo")->setEnabled(false); action("move_hint")->setVisible(false); action("move_solve")->setVisible(false); action("move_dub_puzzle")->setVisible(false); } } void KSudoku::onModified(bool /*isModified*/) { Game game = currentGame(); if(game.isValid()) { action("move_undo")->setEnabled(game.canUndo()); action("move_redo")->setEnabled(game.canRedo()); action("move_hint")->setEnabled(!game.allValuesSetAndUsable()); action("move_solve")->setEnabled(!game.wasFinished()); } } void KSudoku::undo() { Game game = currentGame(); if(!game.isValid()) return; game.interface()->undo(); if(!game.canUndo()) { action("move_undo")->setEnabled(false); } } void KSudoku::redo() { Game game = currentGame(); if(!game.isValid()) return; game.interface()->redo(); if(!game.canRedo()) { action("move_redo")->setEnabled(false); } } void KSudoku::push() { // TODO replace this with history // if(type == 0) {if(m_view) m_view->push();return;} // if(glwin) glwin->push(); } void KSudoku::pop() { // TODO replace this with history // if(type == 0) {if(m_view) m_view->pop(); return;} // if(glwin) glwin->pop(); } void KSudoku::dragEnterEvent(QDragEnterEvent * event) { // accept uri drops only if(event->mimeData()->hasUrls()) event->accept(); } void KSudoku::dropEvent(QDropEvent *event) { const QMimeData * data = event->mimeData(); if(data->hasUrls()) { QList Urls = data->urls(); if ( !Urls.isEmpty() ) { // okay, we have a URI.. process it const QUrl &Url = Urls.first(); QString errorMsg; const Game game = ksudoku::Serializer::load(Url, this, errorMsg); if(game.isValid()) startGame(game); else KMessageBox::error(this, errorMsg, i18n("Could not load game.")); } } } void KSudoku::gameNew() { // this slot is called whenever the Game->New menu is selected, // the New shortcut is pressed (usually CTRL+N) or the New toolbar // button is clicked if(!currentView()) return; // only show question when the current game hasn't been finished until now if(!m_gameUI->game().wasFinished()) { if(KMessageBox::questionYesNo(this, i18n("Do you really want to end this game in order to start a new one?"), i18nc("window title", "New Game"), KGuiItem(i18nc("button label", "New Game")), KStandardGuiItem::cancel() ) != KMessageBox::Yes) return; } showWelcomeScreen(); } void KSudoku::gameRestart() { if (!currentView()) return; auto game = currentGame(); // only show question when the current game hasn't been finished until now if (!game.wasFinished()) { if (KMessageBox::questionYesNo(this, i18n("Do you really want to restart this game?"), i18nc("window title", "Restart Game"), KGuiItem(i18nc("button label", "Restart Game")), KStandardGuiItem::cancel() ) != KMessageBox::Yes) { return; } } game.restart(); } void KSudoku::gameOpen() { // this slot is called whenever the Game->Open menu is selected, // the Open shortcut is pressed (usually CTRL+O) or the Open toolbar // button is clicked // standard filedialog const QUrl Url = QFileDialog::getOpenFileUrl(this, i18n("Open Location"), QUrl::fromLocalFile(QDir::homePath()), QString()); if (!Url.isEmpty() && Url.isValid()) { QString errorMsg; Game game = ksudoku::Serializer::load(Url, this, errorMsg); if(!game.isValid()) { KMessageBox::error(this, errorMsg, i18n("Could not load game.")); return; } game.setUrl(Url); // (new KSudoku(game))->show(); startGame(game); // delete game; } } void KSudoku::gameSave() { // this slot is called whenever the Game->Save menu is selected, // the Save shortcut is pressed (usually CTRL+S) or the Save toolbar // button is clicked // save the current file Game game = currentGame(); if(!game.isValid()) return; if(game.getUrl().isEmpty()) game.setUrl(QFileDialog::getSaveFileUrl()); if (!game.getUrl().isEmpty() && game.getUrl().isValid()) { QString errorMsg; if (!ksudoku::Serializer::store(game, game.getUrl(), this, errorMsg)) KMessageBox::error(this, errorMsg, i18n("Error Writing File")); } } void KSudoku::gameSaveAs() { // this slot is called whenever the Game->Save As menu is selected, Game game = currentGame(); if(!game.isValid()) return; game.setUrl(QFileDialog::getSaveFileUrl()); if (!game.getUrl().isEmpty() && game.getUrl().isValid()) gameSave(); } void KSudoku::gamePrint() { // This slot is called whenever the Game->Print action is selected. Game game = currentGame(); if (! game.isValid()) { KMessageBox::information (this, i18n("There seems to be no puzzle to print.")); return; } if (! m_puzzlePrinter) { m_puzzlePrinter = new PuzzlePrinter (this); } m_puzzlePrinter->print (game); } bool KSudoku::queryClose() { if (m_puzzlePrinter) { m_puzzlePrinter->endPrint(); } return true; } void KSudoku::gameExport() { //TODO PORT /* Game game = currentGame(); if(!game.isValid()) return; ksudoku::ExportDlg e(*game.puzzle(), *game.symbols() ); e.exec(); */ } void KSudoku::optionsPreferences() { if ( KConfigDialog::showDialog("settings") ) return; KConfigDialog *dialog = new KConfigDialog(this, "settings", Settings::self()); GameConfig* gameConfig = new GameConfig(); dialog->addPage(gameConfig, i18nc("Game Section in Config", "Game"), "games-config-options"); dialog->addPage(new KGameThemeSelector(dialog, Settings::self(), KGameThemeSelector::NewStuffDisableDownload), i18n("Theme"), "games-config-theme"); //QT5 dialog->setHelp(QString(),"ksudoku"); connect(dialog, &KConfigDialog::settingsChanged, this, &KSudoku::updateSettings); dialog->show(); } void KSudoku::updateSettings() { Renderer::instance()->loadTheme(Settings::theme()); KsView* view = currentView(); if(view) { int order = view->game().order(); m_valueListWidget->setMaxValue(order); view->settingsChanged(); } emit settingsChanged(); } void KSudoku::difficultyChanged (KGameDifficulty::standardLevel difficulty) { qCDebug(KSudokuLog) << "Set difficulty =" << difficulty; int newDifficulty = VeryEasy; switch (difficulty) { case KGameDifficulty::VeryEasy: newDifficulty = VeryEasy; break; case KGameDifficulty::Easy: newDifficulty = Easy; break; case KGameDifficulty::Medium: newDifficulty = Medium; break; case KGameDifficulty::Hard: newDifficulty = Hard; break; default: return; } qCDebug(KSudokuLog) << "Set new difficulty =" << newDifficulty; m_welcomeScreen->setDifficulty(newDifficulty); return; } void KSudoku::difficultyChanged (int difficulty) { qCDebug(KSudokuLog) << "Set custom difficulty =" << difficulty; m_welcomeScreen->setDifficulty(difficulty); if (difficulty == Unlimited) { KMessageBox::information (this, i18n("Warning: The Unlimited difficulty level has no limit on " "how many guesses or branch points are required to solve " "the puzzle and there is no lower limit on how soon " - "guessing becomes necessary."), + "guessing becomes necessary.\n\n" + "Please also note that the generation of this type of puzzle " + "might take much longer than other ones. During this time " + "KSudoku will not respond."), i18n("Warning"), "WarningReUnlimited"); } } void KSudoku::symmetryChanged (int symmetry) { qCDebug(KSudokuLog) << "Set symmetry =" << symmetry; m_welcomeScreen->setSymmetry(symmetry); } // void KSudoku::changeStatusbar(const QString& text) // { // // display the text on the statusbar // statusBar()->showMessage(text); // } void KSudoku::changeCaption(const QString& text) { // display the text on the caption setCaption(text); } Game KSudoku::currentGame() const { ksudoku::KsView* view = currentView(); if(view) return view->game(); else return Game(); } ksudoku::KsView* KSudoku::currentView() const{ return m_gameUI; } void KSudoku::enableMessages() { // Enable all messages that the user has marked "Do not show again". int result = KMessageBox::questionYesNo(this, - i18n("Enable all messages")); + i18n("This will enable all the dialogs that you had disabled by marking " + "the 'Do not show this message again' option.\n\n" + "Do you want to continue?")); if (result == KMessageBox::Yes) { KMessageBox::enableAllMessages(); KSharedConfig::openConfig()->sync(); // Save the changes to disk. } } #if 0 KSudokuNewStuff::KSudokuNewStuff( KSudoku* v ) : KNewStuff( "ksudoku", (QWidget*) v ) { parent = v; } bool KSudokuNewStuff::install( const QString &fileName ) { KTar archive( fileName ); if ( !archive.open( QIODevice::ReadOnly ) ) return false; const KArchiveDirectory *archiveDir = archive.directory(); const QString destDir = QStandardPaths::writableLocation( QStandardPaths::GenericDataLocation ) + QStringLiteral("/ksudoku/"); QDir().mkpath(destDir); archiveDir->copyTo(destDir); archive.close(); //find custom shapes parent->updateShapesList(); return true; } bool KSudokuNewStuff::createUploadFile( const QString &/*fileName*/ ) { return true; } #endif