1 /*
2 SPDX-FileCopyrightText: 2009-2021 Graeme Gott <graeme@gottcode.org>
3
4 SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7 #include "board.h"
8
9 #include "cell.h"
10 #include "pattern.h"
11 #include "puzzle.h"
12 #include "solver_logic.h"
13
14 #include <QApplication>
15 #include <QFrame>
16 #include <QGridLayout>
17 #include <QKeyEvent>
18 #include <QLabel>
19 #include <QPagedPaintDevice>
20 #include <QPageLayout>
21 #include <QPainter>
22 #include <QSettings>
23 #include <QUndoStack>
24
25 //-----------------------------------------------------------------------------
26
Board(QWidget * parent)27 Board::Board(QWidget* parent)
28 : Frame(parent)
29 , m_puzzle(new Puzzle(this))
30 , m_notes(new SolverLogic)
31 , m_active_key(1)
32 , m_active_cell(nullptr)
33 , m_hint_cell(nullptr)
34 , m_auto_switch(true)
35 , m_highlight_active(false)
36 , m_notes_mode(false)
37 , m_finished(false)
38 , m_loaded(false)
39 , m_auto_notes(ManualNotes)
40 {
41 setBackgroundRole(QPalette::Mid);
42
43 connect(m_puzzle, &Puzzle::generated, this, [this](int symmetry, int difficulty) {
44 puzzleGenerated(symmetry, difficulty);
45 savePuzzle();
46 });
47
48 m_moves = new QUndoStack(this);
49
50 QGridLayout* layout = new QGridLayout(this);
51 layout->setContentsMargins(3, 3, 3, 3);
52 layout->setSpacing(0);
53
54 // Create cells
55 for (int i = 0; i < 9; ++i) {
56 int col = (i % 3) * 3;
57 int max_col = col + 3;
58 int row = (i / 3) * 3;
59 int max_row = row + 3;
60
61 QGridLayout* box = new QGridLayout;
62 box->setContentsMargins(2, 2, 2, 2);
63 box->setSpacing(1);
64 layout->addLayout(box, row / 3, col / 3);
65
66 for (int r = row; r < max_row; ++r) {
67 for (int c = col; c < max_col; ++c) {
68 Cell* cell = new Cell(c, r, this, this);
69 box->addWidget(cell, r - row, c - col);
70 m_cells[c][r] = cell;
71 }
72 }
73 }
74
75 // Create success message
76 m_message = new QLabel(this);
77 QFontMetrics metrics(QFont("Sans", 24));
78 int width = metrics.boundingRect(tr("Success")).width();
79 int height = metrics.height();
80 int ratio = devicePixelRatio();
81 QPixmap success(QSize(width + height, height * 2) * ratio);
82 success.setDevicePixelRatio(ratio);
83 success.fill(QColor(0, 0, 0, 0));
84 {
85 QPainter painter(&success);
86
87 painter.setPen(Qt::NoPen);
88 painter.setBrush(QColor(0, 0, 0, 200));
89 painter.setRenderHint(QPainter::Antialiasing, true);
90 painter.drawRoundedRect(0, 0, width + height, height * 2, 10, 10);
91
92 painter.setFont(QFont("Sans", 24));
93 painter.setPen(Qt::white);
94 painter.setRenderHint(QPainter::TextAntialiasing, true);
95 painter.drawText(height / 2, height / 2 + metrics.ascent(), tr("Success"));
96 }
97 m_message->setPixmap(success);
98 m_message->hide();
99 layout->addWidget(m_message, 0, 0, 3, 3, Qt::AlignCenter);
100 }
101
102 //-----------------------------------------------------------------------------
103
~Board()104 Board::~Board()
105 {
106 QSettings settings;
107 if (!m_finished) {
108 settings.setValue("Key", m_active_key);
109 savePuzzle();
110 } else {
111 settings.remove("Current");
112 }
113
114 delete m_notes;
115 }
116
117 //-----------------------------------------------------------------------------
118
newPuzzle(int symmetry,int difficulty)119 void Board::newPuzzle(int symmetry, int difficulty)
120 {
121 QSettings settings;
122
123 if (symmetry == -1) {
124 symmetry = settings.value("Symmetry", Pattern::Rotational180).toInt();
125 }
126 settings.setValue("Symmetry", symmetry);
127 if (symmetry == Pattern::Random) {
128 symmetry = QRandomGenerator::global()->bounded(Pattern::Rotational180, Pattern::Random);
129 }
130
131 if (difficulty == -1) {
132 difficulty = settings.value("Difficulty", Puzzle::VeryEasy).toInt();
133 }
134 settings.setValue("Difficulty", difficulty);
135
136 // Create puzzle
137 reset();
138 m_puzzle->generate(symmetry, difficulty);
139 }
140
141 //-----------------------------------------------------------------------------
142
newPuzzle(const std::array<int,81> & givens)143 bool Board::newPuzzle(const std::array<int, 81>& givens)
144 {
145 // Create puzzle
146 if (!m_puzzle->load(givens)) {
147 return false;
148 }
149
150 // Load puzzle
151 reset();
152 puzzleGenerated(Pattern::None, SolverLogic().solvePuzzle(givens, Puzzle::Unsolved));
153
154 // Store puzzle layout
155 savePuzzle();
156
157 return true;
158 }
159
160 //-----------------------------------------------------------------------------
161
loadPuzzle()162 bool Board::loadPuzzle()
163 {
164 QSettings settings;
165 settings.beginGroup("Current");
166
167 // Check version number
168 if (settings.value("Version", 0).toInt() != 5) {
169 return false;
170 }
171
172 // Load board layout
173 const QString cells = settings.value("Board").toString();
174 if (cells.length() != 81) {
175 return false;
176 }
177
178 // Create puzzle
179 std::array<int, 81> givens;
180 for (int i = 0; i < 81; ++i) {
181 givens[i] = cells[i].digitValue();
182 }
183
184 if (!m_puzzle->load(givens)) {
185 return false;
186 }
187
188 // Fetch puzzle details
189 const int difficulty = settings.value("Difficulty").toInt();
190 const int symmetry = settings.value("Symmetry").toInt();
191 const QStringList moves = settings.value("Moves").toStringList();
192 const QStringList active = settings.value("Active").toString().split('x');
193
194 // Load puzzle
195 reset();
196 puzzleGenerated(symmetry, difficulty);
197
198 // Load moves
199 for (const QString& move : moves) {
200 if (move.length() == 4) {
201 m_notes_mode = (move[2] == 'n');
202 Cell* c = cell(move[0].digitValue(), move[1].digitValue());
203 QKeyEvent event(QEvent::KeyPress, Qt::Key_0 + move[3].digitValue(), Qt::NoModifier);
204 QApplication::sendEvent(c, &event);
205 }
206 }
207 m_notes_mode = false;
208
209 // Select current cell
210 if (active.count() == 2) {
211 m_active_cell = cell(active[0].toInt(), active[1].toInt());
212 m_active_cell->setFocus();
213 }
214
215 // Store puzzle layout and moves
216 savePuzzle();
217
218 return true;
219 }
220
221 //-----------------------------------------------------------------------------
222
savePuzzle()223 void Board::savePuzzle()
224 {
225 if (!m_loaded) {
226 return;
227 }
228
229 QSettings settings;
230 settings.beginGroup("Current");
231
232 // Store board layout
233 QString cells;
234 cells.reserve(81);
235 for (int r = 0; r < 9; ++r) {
236 for (int c = 0; c < 9; ++c) {
237 cells.append(QChar(m_puzzle->given(c, r) + '0'));
238 }
239 }
240 settings.setValue("Board", cells);
241
242 // Store moves
243 QStringList moves;
244 int count = m_moves->index();
245 for (int i = 0; i < count; ++i) {
246 moves += m_moves->text(i);
247 }
248 if (count) {
249 settings.setValue("Moves", moves);
250 }
251
252 // Store current cell
253 if (m_active_cell) {
254 settings.setValue("Active", QString("%1x%2").arg(m_active_cell->column()).arg(m_active_cell->row()));
255 }
256 }
257
258 //-----------------------------------------------------------------------------
259
restartPuzzle()260 void Board::restartPuzzle()
261 {
262 if (!m_loaded) {
263 return;
264 }
265
266 // Fetch puzzle details
267 QSettings settings;
268 const int difficulty = settings.value("Difficulty").toInt();
269 const int symmetry = settings.value("Symmetry").toInt();
270
271 // Load puzzle
272 reset();
273 puzzleGenerated(symmetry, difficulty);
274 }
275
276 //-----------------------------------------------------------------------------
277
hasPossible(int column,int row,int value) const278 bool Board::hasPossible(int column, int row, int value) const
279 {
280 return m_notes->hasPossible(column, row, value);
281 }
282
283 //-----------------------------------------------------------------------------
284
print(QPagedPaintDevice * printer) const285 void Board::print(QPagedPaintDevice* printer) const
286 {
287 if (!m_loaded) {
288 return;
289 }
290
291 // Make sure page margins are at least 2cm
292 QMarginsF margins = printer->pageLayout().margins(QPageLayout::Millimeter);
293 qreal min = 20.0;
294 if (margins.left() > min) {
295 min = margins.left();
296 }
297 if (margins.top() > min) {
298 min = margins.top();
299 }
300 if (margins.right() > min) {
301 min = margins.right();
302 }
303 if (margins.bottom() > min) {
304 min = margins.bottom();
305 }
306 printer->setPageMargins(QMarginsF(min, min, min, min), QPageLayout::Millimeter);
307
308 // Begin painting
309 QPainter painter;
310 painter.begin(printer);
311
312 // Center board on the page
313 QRect rect = painter.viewport();
314 const qreal scale = rect.width() / 472.0;
315 if (rect.height() > rect.width()) {
316 const int offset = (rect.height() - rect.width()) / 2;
317 painter.translate(0, offset);
318 rect.setHeight(rect.width());
319 } else {
320 const int offset = (rect.width() - rect.height()) / 2;
321 painter.translate(offset, 0);
322 rect.setWidth(rect.height());
323 }
324
325 // Operate on a scaled viewport so that calculations are easier
326 painter.save();
327 painter.scale(scale, scale);
328
329 // Draw box divider lines
330 painter.setPen(QPen(Qt::black, 4));
331 for (int i = 0; i < 4; ++i) {
332 int pos = (152 * i) + (i * 4) + 2;
333 painter.drawLine(2, pos, 470, pos);
334 painter.drawLine(pos, 2, pos, 470);
335 }
336
337 // Draw cell divider lines
338 painter.setPen(QPen(Qt::black, 1));
339 for (int i = 1; i < 3; ++i) {
340 int pos = (51 * i) + 3;
341 painter.drawLine(1, pos, 471, pos);
342 painter.drawLine(pos, 1, pos, 471);
343 }
344 for (int i = 4; i < 6; ++i) {
345 int pos = (51 * i) + 6;
346 painter.drawLine(1, pos, 471, pos);
347 painter.drawLine(pos, 1, pos, 471);
348 }
349 for (int i = 7; i < 9; ++i) {
350 int pos = (51 * i) + 9;
351 painter.drawLine(1, pos, 471, pos);
352 painter.drawLine(pos, 1, pos, 471);
353 }
354
355 // Draw values
356 const QPen pen_value(QColor(0x88, 0x88, 0x88));
357 const QPen pen_given(Qt::black);
358
359 QFont font_value = painter.font();
360 font_value.setPixelSize(24);
361
362 QFont font_given = font_value;
363 font_given.setBold(true);
364
365 for (int r = 0; r < 9; ++r) {
366 const int r_offset = 4 + (r % 3) + ((r / 3) * 6);
367 for (int c = 0; c < 9; ++c) {
368 int given = m_puzzle->given(c, r);
369 int value = m_cells[c][r]->value();
370 if (given) {
371 value = given;
372 painter.setPen(pen_given);
373 painter.setFont(font_given);
374 } else if (value) {
375 painter.setPen(pen_value);
376 painter.setFont(font_value);
377 } else {
378 continue;
379 }
380
381 const int c_offset = 4 + (c % 3) + ((c / 3) * 6);
382 painter.drawText((c * 50) + c_offset, (r * 50) + r_offset, 50, 50, Qt::AlignCenter, QString::number(value));
383 }
384 }
385
386 // Finish
387 painter.restore();
388 painter.end();
389 }
390
391 //-----------------------------------------------------------------------------
392
checkFinished()393 void Board::checkFinished()
394 {
395 bool was_finished = m_finished;
396
397 m_finished = true;
398 for (int r = 0; r < 9; ++r) {
399 for (int c = 0; c < 9; ++c) {
400 m_finished = m_finished && m_cells[c][r]->isCorrect();
401 }
402 }
403
404 if (m_finished) {
405 for (int r = 0; r < 9; ++r) {
406 for (int c = 0; c < 9; ++c) {
407 m_cells[c][r]->clearFocus();
408 m_cells[c][r]->setFocusPolicy(Qt::NoFocus);
409 }
410 }
411 m_message->show();
412 update();
413 emit gameFinished();
414 } else if (was_finished) {
415 m_message->hide();
416 update();
417 emit gameStarted();
418 }
419 }
420
421 //-----------------------------------------------------------------------------
422
hint()423 void Board::hint()
424 {
425 if (m_finished) {
426 return;
427 }
428
429 if (!m_hint_cell || m_hint_cell->isCorrect()) {
430 // Find status of cells
431 QList<Cell*> incorrect;
432 QList<Cell*> empty;
433 empty.reserve(81);
434 for (int r = 0; r < 9; ++r) {
435 for (int c = 0; c < 9; ++c) {
436 Cell* cell = m_cells[c][r];
437 if (cell->isCorrect()) {
438 continue;
439 }
440 if (cell->value()) {
441 incorrect.append(cell);
442 } else {
443 empty.append(cell);
444 }
445 }
446 }
447
448 // Show an incorrect cell if they exist
449 if (!incorrect.isEmpty()) {
450 std::shuffle(incorrect.begin(), incorrect.end(), *QRandomGenerator::global());
451 m_hint_cell = incorrect.first();
452 m_hint_cell->showWrong(true);
453 return;
454 }
455
456 // Find a cell to fill
457 if (!empty.isEmpty()) {
458 std::shuffle(empty.begin(), empty.end(), *QRandomGenerator::global());
459 m_hint_cell = empty.first();
460 }
461 }
462
463 // Fill cell with correct value
464 if (m_hint_cell) {
465 const int key = m_puzzle->value(m_hint_cell->column(), m_hint_cell->row());
466 QKeyEvent event(QEvent::KeyPress, Qt::Key_0 + key, Qt::NoModifier);
467 QApplication::sendEvent(m_hint_cell, &event);
468 m_hint_cell->setFocus();
469 m_hint_cell = nullptr;
470 }
471 }
472
473 //-----------------------------------------------------------------------------
474
showWrong(bool show)475 void Board::showWrong(bool show)
476 {
477 for (int r = 0; r < 9; ++r) {
478 for (int c = 0; c < 9; ++c) {
479 m_cells[c][r]->showWrong(show);
480 }
481 }
482 }
483
484 //-----------------------------------------------------------------------------
485
moveFocus(int column,int row,int xdelta,int ydelta)486 void Board::moveFocus(int column, int row, int xdelta, int ydelta)
487 {
488 xdelta = qBound(-1, xdelta, 2);
489 ydelta = qBound(-1, ydelta, 2);
490 Q_ASSERT(xdelta != ydelta);
491
492 if (column + xdelta < 0) {
493 column = 9;
494 } else if (column + xdelta > 8) {
495 column = -1;
496 }
497 column += xdelta;
498
499 if (row + ydelta < 0) {
500 row = 9;
501 } else if (row + ydelta > 8) {
502 row = -1;
503 }
504 row += ydelta;
505
506 m_cells[column][row]->setFocus();
507 }
508
509 //-----------------------------------------------------------------------------
510
decreaseKeyCount(int key)511 void Board::decreaseKeyCount(int key)
512 {
513 key--;
514 if (key < 0 || key > 8) {
515 return;
516 }
517 m_key_count[key]--;
518 emit keysChanged();
519 }
520
521 //-----------------------------------------------------------------------------
522
increaseKeyCount(int key)523 void Board::increaseKeyCount(int key)
524 {
525 key--;
526 if (key < 0 || key > 8) {
527 return;
528 }
529 m_key_count[key]++;
530 emit keysChanged();
531 }
532
533 //-----------------------------------------------------------------------------
534
updatePossibles()535 void Board::updatePossibles()
536 {
537 std::array<int, 81> givens;
538 for (int r = 0; r < 9; ++r) {
539 for (int c = 0; c < 9; ++c) {
540 givens[c + (r * 9)] = m_cells[c][r]->value();
541 }
542 }
543 m_notes->loadPuzzle(givens);
544 }
545
546 //-----------------------------------------------------------------------------
547
cellNeighbors(int column,int row) const548 QList<Cell*> Board::cellNeighbors(int column, int row) const
549 {
550 QList<Cell*> cells;
551
552 // Fetch neighbors in row
553 for (unsigned int c = 0; c < 9; ++c) {
554 Cell* cell = m_cells[c][row];
555 if (!cells.contains(cell)) {
556 cells.append(cell);
557 }
558 }
559
560 // Fetch neighbors in column
561 for (unsigned int r = 0; r < 9; ++r) {
562 Cell* cell = m_cells[column][r];
563 if (!cells.contains(cell)) {
564 cells.append(cell);
565 }
566 }
567
568 // Fetch neighbors in box
569 const unsigned int box_r = (row / 3) * 3;
570 const unsigned int box_c = (column / 3) * 3;
571 for (unsigned int r = box_r; r < (box_r + 3); ++r) {
572 for (unsigned int c = box_c; c < (box_c + 3); ++c) {
573 Cell* cell = m_cells[c][r];
574 if (!cells.contains(cell)) {
575 cells.append(cell);
576 }
577 }
578 }
579
580 // Do not include cell in neighbors list
581 cells.removeOne(m_cells[column][row]);
582
583 return cells;
584 }
585
586 //-----------------------------------------------------------------------------
587
setActiveKey(int key)588 void Board::setActiveKey(int key)
589 {
590 m_active_key = qBound(1, key, 10);
591 update();
592 emit activeKeyChanged(m_active_key);
593 }
594
595 //-----------------------------------------------------------------------------
596
setActiveCell(Cell * cell)597 void Board::setActiveCell(Cell* cell)
598 {
599 m_active_cell = cell;
600 }
601
602 //-----------------------------------------------------------------------------
603
setAutoNotes(int auto_notes)604 void Board::setAutoNotes(int auto_notes)
605 {
606 m_auto_notes = qBound(ManualNotes, AutoNotes(auto_notes), AutoFillNotes);
607
608 QString name;
609 switch (m_auto_notes) {
610 case AutoClearNotes:
611 name = "Clear";
612 break;
613 case AutoFillNotes:
614 name = "Fill";
615 break;
616 case ManualNotes:
617 default:
618 name = "None";
619 break;
620 }
621 QSettings().setValue("AutoNotes", name);
622
623 update();
624 }
625
626 //-----------------------------------------------------------------------------
627
setAutoSwitch(bool auto_switch)628 void Board::setAutoSwitch(bool auto_switch)
629 {
630 m_auto_switch = auto_switch;
631 QSettings().setValue("AutoSwitch", m_auto_switch);
632 }
633
634 //-----------------------------------------------------------------------------
635
setHighlightActive(bool highlight)636 void Board::setHighlightActive(bool highlight)
637 {
638 m_highlight_active = highlight;
639 QSettings().setValue("Highlight", m_highlight_active);
640 update();
641 }
642
643 //-----------------------------------------------------------------------------
644
setMode(int mode)645 void Board::setMode(int mode)
646 {
647 m_notes_mode = mode;
648 QSettings().setValue("Mode", (m_notes_mode ? "Pencil" : "Pen"));
649 emit notesModeChanged(mode);
650 }
651
652 //-----------------------------------------------------------------------------
653
puzzleGenerated(int symmetry,int difficulty)654 void Board::puzzleGenerated(int symmetry, int difficulty)
655 {
656 for (int r = 0; r < 9; ++r) {
657 for (int c = 0; c < 9; ++c) {
658 m_cells[c][r]->setPuzzle(m_puzzle);
659 }
660 }
661
662 updatePossibles();
663
664 // Store puzzle details
665 QSettings settings;
666 settings.remove("Current");
667 settings.beginGroup("Current");
668 settings.setValue("Version", 5);
669 settings.setValue("Difficulty", difficulty);
670 settings.setValue("Symmetry", symmetry);
671
672 m_loaded = true;
673
674 emit gameStarted();
675 }
676
677 //-----------------------------------------------------------------------------
678
reset()679 void Board::reset()
680 {
681 showWrong(false);
682 m_finished = false;
683 m_loaded = false;
684 m_message->hide();
685 m_moves->clear();
686 for (int i = 0; i < 9; ++i) {
687 m_key_count[i] = 0;
688 }
689 emit keysChanged();
690 }
691
692 //-----------------------------------------------------------------------------
693