1 /***************************************************************************
2 * Copyright 2007 Francesco Rossi <redsh@email.it> *
3 * Copyright 2006-2007 Mick Kappenburg <ksudoku@kappendburg.net> *
4 * Copyright 2006-2007 Johannes Bergmeier <johannes.bergmeier@gmx.net> *
5 * Copyright 2015 Ian Wadham <iandw.au@gmail.com> *
6 * *
7 * This program is free software; you can redistribute it and/or modify *
8 * it under the terms of the GNU General Public License as published by *
9 * the Free Software Foundation; either version 2 of the License, or *
10 * (at your option) any later version. *
11 * *
12 * This program is distributed in the hope that it will be useful, *
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
15 * GNU General Public License for more details. *
16 * *
17 * You should have received a copy of the GNU General Public License *
18 * along with this program; if not, write to the *
19 * Free Software Foundation, Inc., *
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
21 ***************************************************************************/
22
23 #include "ksudokugame.h"
24 #include "ksudoku_logging.h"
25
26 #include "puzzle.h"
27
28
29 #include "globals.h"
30
31 #include <KMessageBox>
32 #include <KLocalizedString>
33
34 #include <QList>
35 #include <QElapsedTimer>
36 #include <QTime>
37
38 class QWidget;
39
40 namespace ksudoku {
41
42 /**
43 * @TODO replace m_refCount with QAtomic (in KDE4 version)
44 */
45 class Game::Private : public GameIFace {
46 public:
Private()47 inline Private() : m_refCount(1) { }
~Private()48 inline ~Private() override {
49 delete puzzle;
50 }
51 public:
ref()52 inline void ref() { ++m_refCount; }
deref()53 inline bool deref() { return !--m_refCount; }
54 private:
55 int m_refCount;
56
57 public: // The slots of GameIFace
58 void undo() override;
59 void redo() override;
60 void addCheckpoint() override;
61 void undo2Checkpoint() override;
62
63 public:
emitModified(bool isModified)64 inline void emitModified(bool isModified) { Q_EMIT modified(isModified); }
emitCompleted(bool isCorrect,const QTime & required,bool withHelp)65 inline void emitCompleted(bool isCorrect, const QTime& required, bool withHelp) {
66 Q_EMIT completed(isCorrect, required, withHelp);
67 }
emitCellChange(int index)68 inline void emitCellChange(int index) { Q_EMIT cellChange(index); }
emitFullChange()69 inline void emitFullChange() { Q_EMIT fullChange(); }
emitCageChange(int cageNumP1,bool showLabel)70 inline void emitCageChange(int cageNumP1, bool showLabel)
71 { Q_EMIT cageChange(cageNumP1, showLabel); }
72
73 public:
74 PuzzleState state;
75
76 bool hadHelp : 1;
77 bool wasFinished : 1;
78
79 Puzzle* puzzle;
80 QElapsedTimer time;
81 int accumTime;
82 QUrl url;
83 QList<HistoryEvent> history;
84 int historyPos;
85
86 QVector<int> m_cage;
87 int m_cageValue;
88 CageOperator m_cageOperator;
89 int m_currentCageSaved;
90 QWidget * m_messageParent;
91 };
92
undo()93 void Game::Private::undo() {
94 if(historyPos == 0) return;
95
96 HistoryEvent event(history[--historyPos]);
97 event.undoOn(state);
98
99 const QVector<int>& indices = event.cellIndices();
100 if(indices.count() > 10) {
101 Q_EMIT fullChange();
102 } else {
103 for(int i = 0; i < indices.count(); ++i)
104 Q_EMIT cellChange(indices[i]);
105 }
106 Q_EMIT modified(true);
107 }
108
redo()109 void Game::Private::redo() {
110 if(historyPos == history.count()) return;
111
112 HistoryEvent event(history[historyPos++]);
113 event.redoOn(state);
114
115 const QVector<int>& indices = event.cellIndices();
116 if(indices.count() > 10) {
117 Q_EMIT fullChange();
118 } else {
119 for(int i = 0; i < indices.count(); ++i)
120 Q_EMIT cellChange(indices[i]);
121 }
122 Q_EMIT modified(true);
123 }
124
addCheckpoint()125 void Game::Private::addCheckpoint() {
126 }
127
undo2Checkpoint()128 void Game::Private::undo2Checkpoint() {
129 }
130
131 /*
132 * The Game
133 */
134
Game()135 Game::Game()
136 : m_private(nullptr)
137 {
138 }
139
Game(Puzzle * puzzle)140 Game::Game(Puzzle* puzzle)
141 : m_private(nullptr)
142 {
143 if(!puzzle) return;
144
145 m_private = new Private();
146
147 m_private->puzzle = puzzle;
148
149 m_private->hadHelp = false;
150 m_private->wasFinished = false;
151
152 m_private->state = PuzzleState(size(), m_private->puzzle->order());
153 m_private->state.reset();
154
155 for(int i = 0; i < size(); i++) {
156 m_private->state.setValue(i, m_private->puzzle->value(i));
157 if(value(i) != 0)
158 m_private->state.setGiven(i, true);
159 }
160 m_private->historyPos = 0;
161
162 m_private->accumTime = 0;
163 m_private->time.start();
164
165 m_private->m_currentCageSaved = false;
166 }
167
Game(const Game & game)168 Game::Game(const Game& game)
169 : m_private(game.m_private)
170 {
171 if(m_private) m_private->ref();
172 }
173
~Game()174 Game::~Game()
175 {
176 if(m_private && m_private->deref()) delete m_private;
177 }
178
operator =(const Game & game)179 Game& Game::operator=(const Game& game) {
180 if(m_private == game.m_private) return *this;
181
182 if(m_private && m_private->deref()) delete m_private;
183 m_private = game.m_private;
184 if(m_private) game.m_private->ref();
185 return *this;
186 }
187
188
189
simpleCheck() const190 bool Game::simpleCheck() const { // IDW TODO - This does nothing useful now
191 // that connections have gone.
192 if(!m_private) return false;
193
194 qCDebug(KSudokuLog) << "BYPASSED Game::simpleCheck()";
195 return true; // IDW: disabled rest of test.
196
197 // IDW test. Eliminated optimized[] arrays and xxxConnection() functions.
198 }
199
restart()200 void Game::restart() {
201 while (canUndo()) {
202 interface()->undo();
203 }
204 m_private->history.clear(); // otherwise we could do redo
205 m_private->wasFinished = false;
206 m_private->emitModified(true); // e.g. to update undo/redo action state
207 }
208
order() const209 int Game::order() const {
210 if(!m_private) return 0;
211 return m_private->puzzle->order();
212 }
213
size() const214 int Game::size() const {
215 if(!m_private) return 0;
216 return m_private->puzzle->size();
217 }
218
interface() const219 GameIFace* Game::interface() const {
220 return m_private;
221 }
222
puzzle() const223 Puzzle* Game::puzzle() const {
224 if(!m_private) return nullptr;
225 return m_private->puzzle;
226 }
227
setUrl(const QUrl & url)228 void Game::setUrl(const QUrl& url) {
229 if(!m_private) return;
230
231 m_private->url = url;
232 }
233
getUrl() const234 QUrl Game::getUrl() const {
235 if(!m_private) return QUrl();
236 return m_private->url;
237 }
238
239
setGiven(int index,bool given)240 void Game::setGiven(int index, bool given) {
241 if(!m_private) return;
242
243 if(given != m_private->state.given(index)) {
244 if(given) {
245 doEvent(HistoryEvent(index, CellInfo(GivenValue, m_private->state.value(index))));
246 } else {
247 doEvent(HistoryEvent(index, CellInfo(CorrectValue, m_private->state.value(index))));
248 }
249 m_private->emitCellChange(index);
250 m_private->emitModified(true);
251 }
252 }
253
setMarker(int index,int val,bool state)254 bool Game::setMarker(int index, int val, bool state) {
255 if(!m_private) return false;
256
257 if(val == 0 || val > m_private->puzzle->order())
258 return false;
259
260 if(m_private->state.given(index))
261 return false;
262 int val2 = value(index);
263 if(val == val2) {
264 doEvent(HistoryEvent(index, CellInfo()));
265 } else {
266 QBitArray markers = m_private->state.markers(index);
267 markers.detach();
268 if(val2 != 0) {
269 markers.setBit(val2 - 1, true);
270 }
271 markers.setBit(val - 1, state);
272 doEvent(HistoryEvent(index, CellInfo(markers)));
273 }
274
275 // almost every time this function will change the cell
276 m_private->emitCellChange(index);
277 m_private->emitModified(true);
278
279 return true;
280 }
281
setValue(int index,int val)282 void Game::setValue(int index, int val) {
283 if(!m_private) return;
284 // If entering in a puzzle, Mathdoku/KillerSudoku has its own procedure.
285 if (! m_private->puzzle->hasSolution()) {
286 if (addToCage (index, val)) {
287 return; // Value went in a Mathdoku/KillerSudoku puzzle.
288 }
289 }
290 if ((val == 32) || (val == 26)) { // Delete-action or Qt::Key_0.
291 val = 0; // Clear the cell.
292 }
293
294 // Solve all kinds of puzzles or enter in a Sudoku or Roxdoku puzzle.
295 if(val > m_private->puzzle->order()) return;
296
297 if(m_private->state.given(index)) return;
298
299 int oldvalue = value(index);
300 doEvent(HistoryEvent(index, CellInfo(CorrectValue, val)));
301
302 m_private->emitCellChange(index);
303 m_private->emitModified(true);
304
305 if(oldvalue != val)
306 checkCompleted();
307 }
308
addToCage(int pos,int val)309 bool Game::addToCage (int pos, int val)
310 {
311 SKGraph * g = m_private->puzzle->graph();
312 SudokuType t = g->specificType();
313 if ((t != Mathdoku) && (t != KillerSudoku)) {
314 return false; // We are not keying in a cage.
315 }
316 #ifdef MATHDOKUENTRY_LOG
317 qCDebug(KSudokuLog) << "Game::addToCage: pos" << pos << "action" << val;
318 #endif
319 if (! m_private->m_currentCageSaved) { // Start a new cage.
320 m_private->m_cage.clear();
321 m_private->m_cageValue = 0;
322 m_private->m_cageOperator = NoOperator;
323 }
324 if ((val != 32) && (! validCell (pos, g))) {
325 return true; // Invalid pos and not deleting: go no further.
326 }
327 CageOperator cageOp = m_private->m_cageOperator;
328 if ((val >= 1) && (val <= 9)) {
329 // Append a non-zero digit to the cage-value.
330 m_private->m_cageValue = 10 * m_private->m_cageValue + val;
331 }
332 else {
333 switch (val) {
334 case 24: // Qt::Key_X = multiply.
335 cageOp = Multiply;
336 break;
337 case 26: // Qt::Key_0
338 if (m_private->m_cageValue > 0) {
339 // Append a zero to the cage-value.
340 m_private->m_cageValue = 10 * m_private->m_cageValue;
341 }
342 break;
343 case 27: // Qt::Key_Slash.
344 cageOp = Divide;
345 break;
346 case 28: // Qt::Key_Minus.
347 cageOp = Subtract;
348 break;
349 case 29: // Qt::Key_Plus.
350 cageOp = Add;
351 break;
352 case 30: // Left click or Qt::Key_Space = drop through and
353 break; // add cell to cage.
354 case 31: // Qt::Key_Return = end cage.
355 finishCurrentCage (g);
356 return true;
357 break;
358 case 32: // Right click or Delete/Bkspace = delete a whole cage.
359 deleteCageAt (pos, g);
360 return true;
361 break;
362 default:
363 return false;
364 break;
365 }
366 }
367
368 // Valid keystroke and position: store and display the current cage.
369 if (m_private->m_cage.indexOf (pos) < 0) {
370 m_private->m_cage.append (pos); // Add cell to current cage.
371 }
372 if (t == KillerSudoku) {
373 if (cageOp != NoOperator) {
374 KMessageBox::information (messageParent(),
375 i18n("In Killer Sudoku, the operator is always + or none "
376 "and KSudoku automatically sets the correct choice."),
377 i18n("Killer Sudoku Cage"), QStringLiteral("KillerCageInfo"));
378 }
379 // Set the operator to none or Add, depending on the cage-size.
380 cageOp = (m_private->m_cage.size() > 1) ? Add : NoOperator;
381 }
382 // TODO - In Killer Sudoku, show the operator during data-entry.
383 m_private->m_cageOperator = cageOp;
384
385 // Change the last cage in the data-model in the SKGraph object.
386 if (m_private->m_currentCageSaved) { // If new cage, skip dropping.
387 int cageNum = g->cageCount() - 1;
388 #ifdef MATHDOKUENTRY_LOG
389 qCDebug(KSudokuLog) << " DROPPING CAGE" << cageNum
390 << "m_currentCageSaved" << m_private->m_currentCageSaved
391 << "m_cage" << m_private->m_cage;
392 #endif
393 g->dropCage (cageNum);
394 }
395 // Add a new cage or replace the previous version of the new cage.
396 g->addCage (m_private->m_cage,
397 m_private->m_cageOperator, m_private->m_cageValue);
398 #ifdef MATHDOKUENTRY_LOG
399 qCDebug(KSudokuLog) << " ADDED CAGE" << (g->cageCount() - 1)
400 << "value" << m_private->m_cageValue
401 << "op" << m_private->m_cageOperator
402 << m_private->m_cage;
403 #endif
404 m_private->m_currentCageSaved = true;
405
406 // Re-draw the boundary and label of the cage just added to the graph.
407 // We always display the label while the cage is being keyed in.
408 m_private->emitCageChange (g->cageCount(), true);
409 return true;
410 }
411
validCell(int pos,SKGraph * g)412 bool Game::validCell (int pos, SKGraph * g)
413 {
414 // No checks of selected cell needed if it is in the current cage.
415 if (m_private->m_cage.indexOf (pos) >= 0) {
416 return true;
417 }
418 // Selected cell must not be already in another cage.
419 for (int n = 0; n < g->cageCount(); n++) {
420 if (g->cage(n).indexOf (pos) >= 0) {
421 KMessageBox::information (messageParent(),
422 i18n("The cell you have selected has already been "
423 "used in a cage."),
424 i18n("Error in Cage"));
425 return false;
426 }
427 }
428 // Cell must adjoin the current cage or be the first cell in it.
429 int cageSize = m_private->m_cage.size();
430 if (cageSize > 0) {
431 int ix = g->cellPosX(pos);
432 int iy = g->cellPosY(pos);
433 int max = g->order();
434 bool adjoining = false;
435 for (int n = 0; n < cageSize; n++) {
436 int cell = m_private->m_cage.at(n);
437 int dx = g->cellPosX(cell) - ix;
438 int dy = g->cellPosY(cell) - iy;
439 if ((dy == 0) && (((ix > 0) && (dx == -1)) ||
440 ((ix < max) && (dx == 1)))) {
441 adjoining = true; // Adjoining to left or right.
442 break;
443 }
444 if ((dx == 0) && (((iy > 0) && (dy == -1)) ||
445 ((iy < max) && (dy == 1)))) {
446 adjoining = true; // Adjoining above or below.
447 break;
448 }
449 }
450 if (! adjoining) {
451 KMessageBox::information (messageParent(),
452 i18n("The cell you have selected is not next to "
453 "any cell in the cage you are creating."),
454 i18n("Error in Cage"));
455 return false;
456 }
457 }
458 return true;
459 }
460
finishCurrentCage(SKGraph * g)461 void Game::finishCurrentCage (SKGraph * g)
462 {
463 #ifdef MATHDOKUENTRY_LOG
464 qCDebug(KSudokuLog) << "END CAGE: value" << m_private->m_cageValue
465 << "op" << m_private->m_cageOperator
466 << m_private->m_cage;
467 #endif
468 // If Killer Sudoku and cage-size > 1, force operator to be +.
469 if ((g->specificType() == KillerSudoku) &&
470 (m_private->m_cage.size() > 1)) {
471 m_private->m_cageOperator = Add;
472 }
473 // Validate the contents of the cage.
474 if ((! m_private->m_currentCageSaved) ||
475 (m_private->m_cage.size() == 0)) {
476 KMessageBox::information (messageParent(),
477 i18n("The cage you wish to complete has no cells in it yet. "
478 "Please click on a cell or key in + - / x or a number."),
479 i18n("Error in Cage"));
480 return; // Invalid - cannot finalise the cage.
481 }
482 else if (m_private->m_cageValue == 0) {
483 KMessageBox::information (messageParent(),
484 i18n("The cage you wish to complete has no value yet. "
485 "Please key in a number with one or more digits."),
486 i18n("Error in Cage"));
487 return; // Invalid - cannot finalise the cage.
488 }
489 else if ((m_private->m_cage.size() > 1) &&
490 (m_private->m_cageOperator == NoOperator)) {
491 KMessageBox::information (messageParent(),
492 i18n("The cage you wish to complete has more than one cell, "
493 "but it has no operator yet. Please key in + - / or x."),
494 i18n("Error in Cage"));
495 return; // Invalid - cannot finalise the cage.
496 }
497 else if ((m_private->m_cage.size() == 1) &&
498 (m_private->m_cageValue > g->order())) {
499 KMessageBox::information (messageParent(),
500 i18n("The cage you wish to complete has one cell, but its "
501 "value is too large. A single-cell cage must have a value "
502 "from 1 to %1 in a puzzle of this size.", g->order()),
503 i18n("Error in Cage"));
504 return; // Invalid - cannot finalise the cage.
505 }
506
507 // Save and display the completed cage.
508 if (m_private->m_cage.size() == 1) { // Display digit.
509 doEvent(HistoryEvent(m_private->m_cage.first(),
510 CellInfo(CorrectValue, m_private->m_cageValue)));
511 m_private->emitCellChange(m_private->m_cage.first());
512 m_private->emitModified(true);
513 }
514 // IDW TODO - Unhighlight the cage that is being entered.
515 m_private->emitCageChange (g->cageCount(), // No label in size 1.
516 (m_private->m_cage.size() > 1));
517 // Start a new cage.
518 m_private->m_currentCageSaved = false;
519 }
520
deleteCageAt(int pos,SKGraph * g)521 void Game::deleteCageAt (int pos, SKGraph * g)
522 {
523 int cageNumP1 = g->cageCount();
524 if (cageNumP1 > 0) {
525 // IDW TODO - Hover-hilite the cage that is to be deleted.
526 cageNumP1 = 0;
527 for (int n = 0; n < g->cageCount(); n++) {
528 if (g->cage(n).indexOf (pos) >= 0) {
529 cageNumP1 = n + 1; // This cage is to be deleted.
530 break;
531 }
532 }
533 // If the right-click was on a cage, delete it.
534 if (cageNumP1 > 0) {
535 if(KMessageBox::questionYesNo (messageParent(),
536 i18n("Do you wish to delete this cage?"),
537 i18n("Delete Cage"), KStandardGuiItem::del(),
538 KStandardGuiItem::cancel(), QStringLiteral("CageDelConfirm"))
539 == KMessageBox::No) {
540 return;
541 }
542 if (g->cage(cageNumP1-1).size() == 1) { // Erase digit.
543 // Delete the digit shown in a size-1 cage.
544 doEvent(HistoryEvent(pos, CellInfo(CorrectValue, 0)));
545 m_private->emitCellChange(pos);
546 m_private->emitModified(true);
547 }
548 // Erase the cage boundary and label.
549 m_private->emitCageChange (-cageNumP1, false);
550 // Remove the cage from the puzzle's graph.
551 #ifdef MATHDOKUENTRY_LOG
552 qCDebug(KSudokuLog) << " DROP CAGE" << (cageNumP1 - 1);
553 #endif
554 g->dropCage (cageNumP1 - 1);
555 if (m_private->m_cage.indexOf (pos) >= 0) {
556 // The current cage was dropped.
557 m_private->m_currentCageSaved = false;
558 }
559 }
560 else {
561 KMessageBox::information (messageParent(),
562 i18n("The cell you have selected is not in any cage, "
563 "so the Delete action will not delete anything."),
564 i18n("Delete Cage"), QStringLiteral("CageDelMissed"));
565 }
566 }
567 else {
568 KMessageBox::information (messageParent(),
569 i18n("The Delete action finds that there are no cages "
570 "to delete."), i18n("Delete Cage"));
571 #ifdef MATHDOKUENTRY_LOG
572 qCDebug(KSudokuLog) << "NO CAGES TO DELETE.";
573 #endif
574 }
575 }
576
allValuesSetAndUsable() const577 bool Game::allValuesSetAndUsable() const {
578 for (int i = 0; i < size(); i++) {
579 if (value(i) == 0) {
580 return false;
581 }
582 }
583
584 return true;
585 }
586
checkCompleted()587 void Game::checkCompleted() {
588 if(!m_private || !m_private->puzzle->hasSolution()) return;
589
590 if (!allValuesSetAndUsable()) {
591 return;
592 }
593
594 for(int i = 0; i < size(); i++) {
595 if(value(i) != solution(i)) {
596 m_private->emitCompleted(false, time(), m_private->hadHelp);
597 return;
598 }
599 }
600 m_private->wasFinished = true;
601 m_private->emitCompleted(true, time(), m_private->hadHelp);
602 }
603
giveHint()604 bool Game::giveHint() {
605 if(!m_private || !m_private->puzzle->hasSolution()) return false;
606
607 int moveNum = 0;
608 int index = 0;
609 while (true) {
610 index = m_private->puzzle->hintIndex(moveNum);
611 if (index < 0) {
612 return false; // End of hint-list.
613 }
614 if (value(index) == 0) {
615 break; // Hint is for a cell not yet filled.
616 }
617 moveNum++;
618 }
619
620 m_private->hadHelp = true;
621
622 int val = solution(index);
623 doEvent(HistoryEvent(index, CellInfo(GivenValue, val)));
624
625 m_private->emitCellChange(index);
626 m_private->emitModified(true);
627
628 checkCompleted();
629
630 return true;
631 }
632
autoSolve()633 bool Game::autoSolve() {
634 if(!m_private || !m_private->puzzle->hasSolution()) return false;
635
636 m_private->hadHelp = true;
637
638 PuzzleState newState(size(), m_private->puzzle->order());
639 newState.reset();
640
641 for(int i = 0; i < size(); ++i) {
642 int val = solution(i);
643 newState.setValue(i, val);
644 newState.setGiven(i, true);
645 }
646
647 doEvent(HistoryEvent(newState));
648
649 m_private->emitFullChange();
650 m_private->emitModified(true);
651
652 m_private->wasFinished = true;
653 m_private->emitCompleted(true, time(), true);
654
655 return true;
656 }
657
658
value(int index) const659 int Game::value(int index) const {
660 if(!m_private) return 0;
661 return m_private->state.value(index);
662 }
663
solution(int index) const664 int Game::solution(int index) const {
665 if(!m_private) return 0;
666 return m_private->puzzle->solution(index);
667 }
668
given(int index) const669 bool Game::given(int index) const {
670 if(!m_private) return false;
671 return m_private->state.given(index);
672 }
673
marker(int index,int val) const674 bool Game::marker(int index, int val) const {
675 if(!m_private) return false;
676 return m_private->state.marker(index, val);
677 }
678
buttonState(int index) const679 ksudoku::ButtonState Game::buttonState(int index) const {
680 if(!m_private) return WrongValue;
681
682 if(given(index))
683 return GivenValue;
684 if(value(index) == 0)
685 return Marker;
686 if(value(index) == solution(index))
687 return CorrectValue;
688 if(solution(index))
689 return WrongValue;
690 return CorrectValue;
691 }
692
cellInfo(int index) const693 CellInfo Game::cellInfo(int index) const {
694 if(!m_private)
695 return CellInfo(WrongValue, 0);
696
697 if(given(index))
698 return CellInfo(GivenValue, value(index));
699 if(value(index) == 0)
700 return CellInfo(m_private->state.markers(index));
701 if(value(index) == solution(index))
702 return CellInfo(CorrectValue, value(index));
703 if(solution(index))
704 return CellInfo(WrongValue, value(index));
705 return CellInfo(CorrectValue, value(index));
706 }
707
allValues() const708 const BoardContents Game::allValues() const {
709 if(!m_private) return BoardContents();
710
711 return m_private->state.allValues();
712 }
713
time() const714 QTime Game::time() const {
715 if(!m_private) return QTime();
716 return QTime(0,0).addMSecs(msecsElapsed());
717 }
718
msecsElapsed() const719 int Game::msecsElapsed() const {
720 if(!m_private) return 0;
721 return (m_private->accumTime + m_private->time.elapsed());
722 }
723
setTime(int msecs) const724 void Game::setTime(int msecs) const {
725 if(!m_private) return;
726 m_private->accumTime = msecs;
727 m_private->time.start();
728 }
729
730 // History
731
doEvent(const HistoryEvent & event)732 void Game::doEvent(const HistoryEvent& event) {
733 if(!m_private) return;
734
735 HistoryEvent hisEvent(event);
736
737 // Remove events after current history position
738 m_private->history.erase(m_private->history.begin()+(m_private->historyPos), m_private->history.end());
739
740 // Append event
741 hisEvent.applyTo(m_private->state);
742 m_private->history.append(hisEvent); // always append after applying
743 m_private->historyPos++;
744 }
745
historyLength() const746 int Game::historyLength() const {
747 if(!m_private) return 0;
748
749 return m_private->history.count();
750 }
751
historyEvent(int i) const752 HistoryEvent Game::historyEvent(int i) const {
753 if(!m_private || i >= m_private->history.count())
754 return HistoryEvent();
755
756 return m_private->history[i];
757 }
758
canUndo() const759 bool Game::canUndo() const {
760 if(!m_private) return false;
761
762 return m_private->historyPos != 0;
763 }
764
canRedo() const765 bool Game::canRedo() const {
766 if(!m_private) return false;
767 return m_private->historyPos != m_private->history.count();
768 }
769
userHadHelp() const770 bool Game::userHadHelp() const {
771 if(!m_private) return false;
772 return m_private->hadHelp;
773 }
774
wasFinished() const775 bool Game::wasFinished() const {
776 if(!m_private) return false;
777 return m_private->wasFinished;
778 }
779
setUserHadHelp(bool hadHelp)780 void Game::setUserHadHelp(bool hadHelp) {
781 if(!m_private) return;
782 m_private->hadHelp = hadHelp;
783 }
784
setMessageParent(QWidget * messageParent)785 void Game::setMessageParent(QWidget * messageParent)
786 {
787 if (m_private) {
788 m_private->m_messageParent = messageParent;
789 }
790 }
791
messageParent()792 QWidget * Game::messageParent()
793 {
794 return (m_private ? m_private->m_messageParent : nullptr);
795 }
796
797 }
798
799
800