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