1 /***********************************************************************
2  *
3  * Copyright (C) 2007, 2008, 2009, 2012, 2014, 2015, 2016, 2018, 2019 Graeme Gott <graeme@gottcode.org>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  ***********************************************************************/
19 
20 #include "board.h"
21 
22 #include "maze.h"
23 #include "solver.h"
24 #include "theme.h"
25 
26 #include <QKeyEvent>
27 #include <QLabel>
28 #include <QMainWindow>
29 #include <QMessageBox>
30 #include <QPainter>
31 #include <QSettings>
32 #include <QStatusBar>
33 #include <QTimeLine>
34 #include <QTime>
35 #include <QTimer>
36 
37 #include <algorithm>
38 #include <ctime>
39 #include <random>
40 
41 // ============================================================================
42 
Board(QMainWindow * parent)43 Board::Board(QMainWindow* parent)
44 :	QWidget(parent),
45 	m_done(false),
46 	m_paused(false),
47 	m_total_targets(3),
48 	m_maze(0),
49 	m_solver(0),
50 	m_show_path(true),
51 	m_smooth_movement(true),
52 	m_col_delta(0),
53 	m_row_delta(0),
54 	m_unit(32),
55 	m_zoom(5),
56 	m_max_zoom(5),
57 	m_zoom_size(14),
58 	m_player_angle(360),
59 	m_player_steps(0),
60 	m_player_total_time(0),
61 	m_hint(-1, -1),
62 	m_hint_angle(0)
63 {
64 	setMinimumSize(448, 448);
65 	setFocusPolicy(Qt::StrongFocus);
66 
67 	m_move_animation = new QTimeLine(60, this);
68 	m_move_animation->setFrameRange(0, 3);
69 	m_move_animation->setCurveShape(QTimeLine::LinearCurve);
70 	m_move_animation->setUpdateInterval(15);
71 	connect(m_move_animation, &QTimeLine::frameChanged, this, static_cast<void (Board::*)()>(&Board::repaint));
72 
73 	// Create status messages
74 	m_status_time_message = new QLabel;
75 	m_status_time_message->setContentsMargins(10, 0, 10, 0);
76 	parent->statusBar()->addPermanentWidget(m_status_time_message);
77 
78 	m_status_steps_message = new QLabel;
79 	m_status_steps_message->setContentsMargins(10, 0, 10, 0);
80 	parent->statusBar()->addPermanentWidget(m_status_steps_message);
81 
82 	m_status_remain_message = new QLabel;
83 	m_status_remain_message->setContentsMargins(10, 0, 10, 0);
84 	parent->statusBar()->addPermanentWidget(m_status_remain_message);
85 
86 	m_status_timer = new QTimer(this);
87 	m_status_timer->setInterval(1000);
88 	connect(m_status_timer, &QTimer::timeout, this, &Board::updateStatusMessage);
89 
90 	// Setup theme support
91 	m_theme = new Theme;
92 	m_theme->setDevicePixelRatio(devicePixelRatio());
93 
94 	loadSettings();
95 
96 	// Start or load game
97 	if (QSettings().contains("Current/Seed")) {
98 		loadGame();
99 	} else {
100 		m_done = true;
101 		newGame();
102 	}
103 }
104 
105 // ============================================================================
106 
~Board()107 Board::~Board()
108 {
109 	delete m_maze;
110 	delete m_solver;
111 	delete m_theme;
112 }
113 
114 // ============================================================================
115 
newGame()116 void Board::newGame()
117 {
118 	// Stop tracking time
119 	m_status_timer->stop();
120 
121 	// Fetch new seed
122 #ifndef Q_OS_WIN
123 	std::random_device rd;
124 	unsigned int seed = rd();
125 #else
126 	std::mt19937 gen(time(0));
127 	std::uniform_int_distribution<unsigned int> dist;
128 	unsigned int seed = dist(gen);
129 #endif
130 
131 	// Set values for new game
132 	QSettings settings;
133 	settings.remove("Current");
134 	settings.setValue("Current/Algorithm", settings.value("New/Algorithm", 4).toInt());
135 	settings.setValue("Current/Seed", seed);
136 	settings.setValue("Current/Size", settings.value("New/Size", 20).toInt());
137 	settings.setValue("Current/Targets", settings.value("New/Targets", 3).toInt());
138 	settings.setValue("Current/Version", 3);
139 	m_player_angle = 360;
140 	m_player_steps = 0;
141 
142 	// Create new game
143 	m_done = false;
144 	generate(seed);
145 	saveGame();
146 
147 	// Begin tracking time
148 	m_player_total_time = 0;
149 	m_player_time.start();
150 	m_status_timer->start();
151 
152 	// Show
153 	update();
154 	updateStatusMessage();
155 	m_status_remain_message->setVisible(true);
156 
157 	m_paused = false;
158 	emit pauseAvailable(true);
159 	emit hintAvailable(true);
160 	emit pauseChecked(false);
161 }
162 
163 // ============================================================================
164 
loadGame()165 void Board::loadGame()
166 {
167 	m_status_time_message->clear();
168 	m_status_steps_message->clear();
169 	m_status_remain_message->clear();
170 
171 	QSettings settings;
172 
173 	// Load maze
174 	bool success = false;
175 	if (settings.value("Current/Version").toInt() == 3) {
176 		generate(settings.value("Current/Seed").toUInt());
177 		success = m_maze->load();
178 	}
179 	if (!success) {
180 		QMessageBox::warning(this, tr("Sorry"), tr("Unable to load previous game. A new game will be started."));
181 		m_done = true;
182 		return newGame();
183 	}
184 
185 	// Place player at last location
186 	m_player = settings.value("Current/Player").toPoint();
187 	m_player_angle = settings.value("Current/Rotation", 360).toInt();
188 	if (m_player_angle % 90 != 0 || m_player_angle < 0 || m_player_angle > 360) {
189 		m_player_angle = 360;
190 	}
191 	m_player_steps = settings.value("Current/Steps", 0).toInt();
192 
193 	// Resume tracking time
194 	m_player_total_time = settings.value("Current/Time", 0).toInt();
195 	m_player_time.start();
196 	m_status_timer->start();
197 
198 	// Remove any targets with matching movement
199 	for (int i = 0; i < m_targets.size(); ++i) {
200 		if (m_maze->cell(m_targets[i].x(), m_targets[i].y()).pathMarker() || m_player == m_targets[i]) {
201 			QPoint target = m_targets.takeAt(i);
202 			m_solver->removeTarget(target);
203 			--i;
204 		}
205 	}
206 
207 	// Show
208 	update();
209 	updateStatusMessage();
210 	m_status_remain_message->setVisible(true);
211 
212 	// Should not happen, but handle a finished game
213 	if (m_targets.isEmpty()) {
214 		finish();
215 	}
216 }
217 
218 // ============================================================================
219 
saveGame()220 void Board::saveGame()
221 {
222 	if (!m_done) {
223 		m_maze->save();
224 		QSettings settings;
225 		settings.setValue("Current/Player", m_player);
226 		settings.setValue("Current/Rotation", m_player_angle);
227 		settings.setValue("Current/Steps", m_player_steps);
228 		int msecs = m_player_total_time;
229 		if (!m_paused) {
230 			msecs += m_player_time.elapsed();
231 		}
232 		settings.setValue("Current/Time", msecs);
233 	}
234 }
235 
236 // ============================================================================
237 
pauseGame(bool paused)238 void Board::pauseGame(bool paused)
239 {
240 	m_paused = paused;
241 	if (paused) {
242 		m_status_timer->stop();
243 		m_player_total_time += m_player_time.elapsed();
244 	} else {
245 		m_player_time.start();
246 		m_status_timer->start();
247 		updateStatusMessage();
248 	}
249 	update();
250 	emit hintAvailable(!m_paused);
251 }
252 
253 // ============================================================================
254 
hint()255 void Board::hint()
256 {
257 	if (m_done || m_paused || (m_smooth_movement && m_move_animation->state() == QTimeLine::Running)) {
258 		return;
259 	}
260 
261 	m_hint = m_solver->hint(m_player);
262 	if (m_hint.x() < m_player.x()) {
263 		m_hint_angle = 270;
264 	} else if (m_hint.x() > m_player.x()) {
265 		m_hint_angle = 90;
266 	} else if (m_hint.y() < m_player.y()) {
267 		m_hint_angle = 360;
268 	} else {
269 		m_hint_angle = 180;
270 	}
271 	int pos = (m_zoom / 2) + 1;
272 	m_hint = m_hint - m_player + QPoint(pos, pos);
273 	update();
274 }
275 
276 // ============================================================================
277 
zoomIn()278 void Board::zoomIn()
279 {
280 	if (m_zoom > 5) {
281 		m_zoom -= 2;
282 		scale();
283 		update();
284 	}
285 }
286 
287 // ============================================================================
288 
zoomOut()289 void Board::zoomOut()
290 {
291 	if (m_zoom < m_max_zoom) {
292 		m_zoom += 2;
293 		scale();
294 		update();
295 	}
296 }
297 
298 // ============================================================================
299 
loadSettings()300 void Board::loadSettings()
301 {
302 	QSettings settings;
303 
304 	// Load zoom
305 	m_zoom = QSettings().value("Zoom", 5).toInt();
306 	if ((m_zoom % 2) == 0) {
307 		m_zoom--;
308 	}
309 	m_zoom = std::max(m_zoom, 5);
310 
311 	// Load gameplay settings
312 	m_status_steps_message->setVisible(settings.value("Show Steps", true).toBool());
313 	m_status_time_message->setVisible(settings.value("Show Time", true).toBool());
314 	m_show_path = settings.value("Show Path", true).toBool();
315 	m_smooth_movement = settings.value("Smooth Movement", true).toBool();
316 
317 	// Load player controls
318 	m_controls_up = settings.value("Controls/Up", Qt::Key_Up).toUInt();
319 	m_controls_down = settings.value("Controls/Down", Qt::Key_Down).toUInt();
320 	m_controls_left = settings.value("Controls/Left", Qt::Key_Left).toUInt();
321 	m_controls_right = settings.value("Controls/Right", Qt::Key_Right).toUInt();
322 	m_controls_flag = settings.value("Controls/Flag", Qt::Key_Space).toUInt();
323 	m_controls_hint = settings.value("Controls/Hint", Qt::Key_H).toUInt();
324 
325 	// Load theme
326 	m_theme->load(settings.value("Theme", "Mouse").toString());
327 	renderBackground();
328 
329 	// Show
330 	update();
331 	updateStatusMessage();
332 }
333 
334 // ============================================================================
335 
keyPressEvent(QKeyEvent * event)336 void Board::keyPressEvent(QKeyEvent* event)
337 {
338 	// Prevent player from changing a paused or finished maze
339 	if (m_done || m_paused) {
340 		return;
341 	}
342 
343 	// Prevent movement during animation
344 	if (m_smooth_movement && m_move_animation->state() == QTimeLine::Running) {
345 		return;
346 	}
347 
348 	m_col_delta = m_row_delta = 0;
349 	QPoint position = m_player;
350 	const Cell& cell = m_maze->cell(m_player.x(), m_player.y());
351 
352 	unsigned int keypress = event->key();
353 	if (keypress == m_controls_left) {
354 		m_player_angle = 270;
355 		if (!cell.leftWall()) {
356 			Q_ASSERT(m_player.x() > 0);
357 			m_player.rx()--;
358 		}
359 	} else if (keypress == m_controls_right) {
360 		m_player_angle = 90;
361 		if (!cell.rightWall()) {
362 			Q_ASSERT(m_player.x() < m_maze->columns() - 1);
363 			m_player.rx()++;
364 		}
365 	} else if (keypress == m_controls_up) {
366 		m_player_angle = 360;
367 		if (!cell.topWall()) {
368 			Q_ASSERT(m_player.y() > 0);
369 			m_player.ry()--;
370 		}
371 	} else if (keypress == m_controls_down) {
372 		m_player_angle = 180;
373 		if (!cell.bottomWall()) {
374 			Q_ASSERT(m_player.y() < m_maze->rows() - 1);
375 			m_player.ry()++;
376 		}
377 	} else if (keypress == m_controls_flag) {
378 		m_maze->cellMutable(m_player.x(), m_player.y()).toggleFlag();
379 	} else if (keypress == m_controls_hint) {
380 		hint();
381 	} else {
382 		return;
383 	}
384 
385 	// Handle player movement
386 	if (position != m_player) {
387 		m_player_steps++;
388 		m_col_delta = m_player.x() - position.x();
389 		m_row_delta = m_player.y() - position.y();
390 		if (m_smooth_movement) {
391 			m_move_animation->start();
392 		}
393 		m_hint = QPoint(-1, -1);
394 
395 		// Add path marker
396 		if (m_maze->cell(position.x(), position.y()).pathMarker() == 0) {
397 			int angle = 0;
398 			if (m_col_delta) {
399 				angle = 180 - (m_col_delta * 90);
400 			} else {
401 				angle = 360 - ((m_row_delta + 1) * 90);
402 			}
403 			m_maze->cellMutable(position.x(), position.y()).setPathMarker(angle);
404 		}
405 	}
406 
407 	// Check for collisions with targets
408 	for (int i = 0; i < m_targets.size(); ++i) {
409 		if (m_player == m_targets.at(i)) {
410 			QPoint target = m_targets.takeAt(i);
411 			m_solver->removeTarget(target);
412 			--i;
413 		}
414 	}
415 
416 	// Show updated maze
417 	update();
418 	updateStatusMessage();
419 
420 	// Handle finishing a maze
421 	if (m_targets.isEmpty()) {
422 		m_maze->cellMutable(m_player.x(), m_player.y()).setPathMarker(m_player_angle);
423 		finish();
424 	}
425 }
426 
427 // ============================================================================
428 
paintEvent(QPaintEvent *)429 void Board::paintEvent(QPaintEvent*)
430 {
431 	if (!m_paused) {
432 		if (!m_done) {
433 			renderMaze();
434 		} else {
435 			renderDone();
436 		}
437 	} else {
438 		renderPause();
439 	}
440 }
441 
442 // ============================================================================
443 
resizeEvent(QResizeEvent *)444 void Board::resizeEvent(QResizeEvent*)
445 {
446 	scale();
447 }
448 
449 // ============================================================================
450 
updateStatusMessage()451 void Board::updateStatusMessage()
452 {
453 	if (m_done || m_paused) {
454 		return;
455 	}
456 
457 	QTime t = QTime(0, 0, 0).addMSecs(m_player_time.elapsed() + m_player_total_time);
458 	m_status_time_message->setText(tr("%1 elapsed") .arg(t.toString("hh:mm:ss")));
459 	m_status_steps_message->setText(tr("%1 steps taken") .arg(m_player_steps));
460 	m_status_remain_message->setText(tr("%1 of %2 targets remain") .arg(m_targets.size()) .arg(m_total_targets));
461 }
462 
463 // ============================================================================
464 
scale()465 void Board::scale()
466 {
467 	m_zoom_size = (m_zoom * 3) - 1;
468 	m_unit = std::min(width(), height()) / m_zoom_size;
469 	m_theme->scale(m_unit);
470 	renderBackground();
471 	emit zoomOutAvailable(m_zoom < m_max_zoom);
472 	emit zoomInAvailable(m_zoom > 5);
473 	QSettings().setValue("Zoom", m_zoom);
474 }
475 
476 // ============================================================================
477 
generate(unsigned int seed)478 void Board::generate(unsigned int seed)
479 {
480 	QSettings settings;
481 	int size = qBound(10, settings.value("Current/Size").toInt(), 100);
482 	m_total_targets = qBound(1, settings.value("Current/Targets").toInt(), 100);
483 	m_max_zoom = size / 2;
484 	if ((m_max_zoom % 2) == 0) {
485 		m_max_zoom--;
486 	}
487 	m_zoom = std::min(m_zoom, m_max_zoom);
488 	scale();
489 
490 	// Create new maze
491 	m_targets.clear();
492 	delete m_maze;
493 	switch (QSettings().value("Current/Algorithm").toInt()) {
494 	case 0:
495 		m_maze = new HuntAndKillMaze;
496 		break;
497 	case 1:
498 		m_maze = new KruskalMaze;
499 		break;
500 	case 2:
501 		m_maze = new PrimMaze;
502 		break;
503 	case 3:
504 		m_maze = new RecursiveBacktrackerMaze;
505 		break;
506 	case 5:
507 		m_maze = new Stack2Maze;
508 		break;
509 	case 6:
510 		m_maze = new Stack3Maze;
511 		break;
512 	case 7:
513 		m_maze = new Stack4Maze;
514 		break;
515 	case 8:
516 		m_maze = new Stack5Maze;
517 		break;
518 	case 4:
519 	default:
520 		m_maze = new StackMaze;
521 		break;
522 	}
523 	std::mt19937 gen(seed);
524 	m_maze->generate(size, size, gen);
525 
526 	// Add player and targets
527 	QList<QPoint> locations;
528 	for (int y = 0; y < size; ++y) {
529 		for (int x = 0; x < size; ++x) {
530 			locations.append(QPoint(x,y));
531 		}
532 	}
533 	std::shuffle(locations.begin(), locations.end(), gen);
534 	m_player = m_start = locations.first();
535 	m_targets = locations.mid(1, m_total_targets);
536 
537 	// Find solutions
538 	delete m_solver;
539 	m_solver = new Solver(m_maze, m_start, m_targets);
540 	m_hint = QPoint(-1, -1);
541 }
542 
543 // ============================================================================
544 
finish()545 void Board::finish()
546 {
547 	emit hintAvailable(false);
548 	emit pauseAvailable(false);
549 	m_move_animation->stop();
550 	m_move_animation->setCurrentTime(m_move_animation->duration());
551 
552 	QSettings settings;
553 	settings.beginGroup("Current");
554 
555 	// Get score values
556 	int seconds = (m_player_total_time + m_player_time.elapsed()) / 1000;
557 	int algorithm = settings.value("Algorithm").toInt();
558 	int size = settings.value("Size").toInt();
559 
560 	// Remove game from disk
561 	m_done = true;
562 	settings.remove("");
563 
564 	// Show congratulations
565 	m_status_timer->stop();
566 	update();
567 
568 	// Add high score
569 	emit finished(m_player_steps, seconds, algorithm, size);
570 }
571 
572 // ============================================================================
573 
renderBackground()574 void Board::renderBackground()
575 {
576 	int size = (m_zoom_size + 6) * m_unit;
577 	int ratio = devicePixelRatio();
578 	m_back = QPixmap(QSize(size, size) * ratio);
579 	m_back.setDevicePixelRatio(ratio);
580 	QPainter painter(&m_back);
581 	m_theme->drawBackground(painter);
582 }
583 
584 // ============================================================================
585 
renderMaze()586 void Board::renderMaze()
587 {
588 	int frame = m_smooth_movement ? m_move_animation->currentFrame() : 3;
589 	Q_ASSERT(frame > -1);
590 	Q_ASSERT(frame < 5);
591 
592 	int pos = (m_zoom / 2) + 1;
593 	int column = m_player.x() - m_col_delta - pos;
594 	int row = m_player.y() - m_row_delta - pos;
595 	int columns = m_maze->columns();
596 	int rows = m_maze->rows();
597 	Q_ASSERT(m_player.x() > -1);
598 	Q_ASSERT(m_player.x() < columns);
599 	Q_ASSERT(m_player.y() > -1);
600 	Q_ASSERT(m_player.y() < rows);
601 
602 	// Create painter
603 	QPainter painter(this);
604 	int size = m_unit * m_zoom_size;
605 	painter.setClipRect((width() - size) >> 1, (height() - size) >> 1, size, size);
606 	painter.translate((width() - size) >> 1, (height() - size) >> 1);
607 	painter.translate(-3 * m_unit, -3 * m_unit);
608 
609 	// Shift by frame amount
610 	painter.save();
611 	int delta = frame * -m_unit;
612 	painter.translate(delta * m_col_delta, delta * m_row_delta);
613 
614 	// Draw background
615 	painter.drawPixmap(0, 0, m_back);
616 
617 	// Initialize corners
618 	int full_view = m_zoom + 3;
619 	unsigned char corners[full_view][full_view];
620 	for (int r = 0; r < full_view; ++r) {
621 		for (int c = 0; c < full_view; ++c) {
622 			corners[c][r] = 0;
623 		}
624 	}
625 
626 	// Setup columns
627 	int column_start = 0;
628 	int column_count = m_zoom + 2;
629 	if (column < 1) {
630 		column_start = abs(column);
631 	} else if ((column + m_zoom + 1) >= columns) {
632 		column_count = columns - column;
633 	}
634 
635 	// Setup rows
636 	int row_start = 0;
637 	int row_count = m_zoom + 2;
638 	if (row < 1) {
639 		row_start = abs(row);
640 	} else if ((row + m_zoom + 1) >= rows) {
641 		row_count = rows - row;
642 	}
643 
644 	// Draw cells
645 	int angle = 0;
646 	for (int r = row_start; r < row_count; ++r) {
647 		for (int c = column_start; c < column_count; ++c) {
648 
649 			const Cell& cell = m_maze->cell(column + c, row + r);
650 
651 			// Draw walls
652 			if (cell.topWall()) {
653 				m_theme->drawWall(painter, c, r);
654 			}
655 			if (cell.leftWall()) {
656 				m_theme->drawWall(painter, c, r, true);
657 			}
658 			if (column + c + 1 == columns) {
659 				m_theme->drawWall(painter, c + 1, r, true);
660 			}
661 			if (row + r + 1 == rows) {
662 				m_theme->drawWall(painter, c, r + 1);
663 			}
664 
665 			// Draw marker
666 			if (m_show_path) {
667 				angle = cell.pathMarker();
668 				if (angle) {
669 					m_theme->draw(painter, c, r, Theme::Marker, angle);
670 				}
671 			}
672 
673 			// Draw flag
674 			if (cell.flag()) {
675 				m_theme->draw(painter, c, r, Theme::Flag);
676 			}
677 
678 			// Configure corners
679 			unsigned char& corner1 = corners[c][r];
680 			corner1 |= (cell.topWall() << 1);
681 			corner1 |= (cell.leftWall() << 2);
682 			unsigned char& corner2 = corners[c + 1][r];
683 			corner2 |= (cell.topWall() << 3);
684 			corner2 |= (cell.rightWall() << 2);
685 			unsigned char& corner3 = corners[c + 1][r + 1];
686 			corner3 |= (cell.rightWall() << 0);
687 			corner3 |= (cell.bottomWall() << 3);
688 			unsigned char& corner4 = corners[c][r + 1];
689 			corner4 |= (cell.leftWall() << 0);
690 			corner4 |= (cell.bottomWall() << 1);
691 		}
692 	}
693 
694 	// Draw corners
695 	for (int r = 0; r < full_view; ++r) {
696 		for (int c = 0; c < full_view; ++c) {
697 			unsigned char walls = corners[c][r];
698 			if (walls) {
699 				m_theme->drawCorner(painter, c, r, walls);
700 			}
701 		}
702 	}
703 
704 	// Draw start
705 	QRect view(column, row, m_zoom + 2, m_zoom + 2);
706 	if (view.contains(m_start)) {
707 		m_theme->draw(painter, m_start.x() - column, m_start.y() - row, Theme::Start);
708 	}
709 
710 	// Draw targets
711 	for (const QPoint& target : m_targets) {
712 		if (view.contains(target)) {
713 			m_theme->draw(painter, target.x() - column, target.y() - row, Theme::Target);
714 		}
715 	}
716 
717 	painter.restore();
718 
719 	// Draw hint
720 	if (m_hint.x() != -1) {
721 		painter.save();
722 		switch (m_hint_angle) {
723 		case 90:
724 			painter.translate(-m_unit, 0);
725 			break;
726 		case 180:
727 			painter.translate(0, -m_unit);
728 			break;
729 		case 270:
730 			painter.translate(m_unit, 0);
731 			break;
732 		case 360:
733 			painter.translate(0, m_unit);
734 			break;
735 		default:
736 			break;
737 		};
738 		m_theme->draw(painter, m_hint.x(), m_hint.y(), Theme::Hint, m_hint_angle);
739 		painter.restore();
740 	}
741 
742 	// Draw player
743 	m_theme->draw(painter, pos, pos, Theme::Player, m_player_angle);
744 }
745 
746 // ============================================================================
747 
renderDone()748 void Board::renderDone()
749 {
750 	int columns = m_maze->columns();
751 	int rows = m_maze->rows();
752 
753 	// Determine sizes
754 	int mcr = std::min(columns, rows);
755 	int cell_width = std::min(width(), height());
756 	cell_width -= (mcr + 1);
757 	cell_width /= mcr;
758 	cell_width += 1;
759 	int w = columns * cell_width + 1;
760 	int h = rows * cell_width + 1;
761 
762 	// Create painter
763 	QPainter painter(this);
764 	painter.save();
765 	painter.translate((width() - w) >> 1, (height() - h) >> 1);
766 	painter.fillRect(0, 0, w, h, Qt::white);
767 
768 	// Draw image
769 	int x1, x2, y1, y2;
770 	for (int r = 0; r < rows; ++r) {
771 		for (int c = 0; c < columns; ++c) {
772 			const Cell& cell = m_maze->cell(c, r);
773 			x1 = c  * cell_width;
774 			x2 = x1 + cell_width;
775 			y1 = r  * cell_width;
776 			y2 = y1 + cell_width;
777 			if (cell.pathMarker()) {
778 				painter.fillRect(x1, y1, cell_width, cell_width, Qt::lightGray);
779 			}
780 			if (cell.topWall()) {
781 				painter.drawLine(x1, y1, x2, y1);
782 			}
783 			if (cell.leftWall()) {
784 				painter.drawLine(x1, y1, x1, y2);
785 			}
786 		}
787 	}
788 	painter.drawLine(0, rows * cell_width, columns * cell_width, rows * cell_width);
789 	painter.drawLine(columns * cell_width, 0, columns * cell_width, rows * cell_width);
790 
791 	// Draw congratulations
792 	painter.restore();
793 	renderText(&painter, tr("Success"));
794 }
795 
796 // ============================================================================
797 
renderPause()798 void Board::renderPause()
799 {
800 	int size = m_unit * m_zoom_size;
801 
802 	// Create painter
803 	QPainter painter(this);
804 	painter.save();
805 	painter.translate((width() - size) >> 1, (height() - size) >> 1);
806 	painter.fillRect(0, 0, size, size, Qt::white);
807 
808 	// Draw message
809 	painter.restore();
810 	renderText(&painter, tr("Paused"));
811 }
812 
813 // ============================================================================
814 
renderText(QPainter * painter,const QString & message) const815 void Board::renderText(QPainter* painter, const QString& message) const
816 {
817 	// Find message size
818 	QFont f = font();
819 	f.setPointSize(24);
820 	QFontMetrics metrics(f);
821 	int width = metrics.boundingRect(message).width();
822 	int height = metrics.height();
823 
824 	painter->save();
825 	painter->translate(rect().center() - QRect(0, 0, width + height, height * 2).center());
826 
827 	// Draw black background
828 	painter->setPen(Qt::NoPen);
829 	painter->setBrush(QColor(0, 0, 0, 200));
830 	painter->setRenderHint(QPainter::Antialiasing, true);
831 	painter->drawRoundedRect(0, 0, width + height, height * 2, 10, 10);
832 
833 	// Draw message
834 	painter->setFont(f);
835 	painter->setPen(Qt::white);
836 	painter->setRenderHint(QPainter::TextAntialiasing, true);
837 	painter->drawText(height / 2, height / 2 + metrics.ascent(), message);
838 
839 	painter->restore();
840 }
841 
842 // ============================================================================
843