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 "beveled_rect.h"
10 #include "clock.h"
11 #include "generator.h"
12 #include "language_settings.h"
13 #include "letter.h"
14 #include "scores_dialog.h"
15 #include "solver.h"
16 #include "view.h"
17 #include "word_counts.h"
18 #include "word_tree.h"
19 
20 #include <QAction>
21 #include <QDialog>
22 #include <QDialogButtonBox>
23 #include <QGridLayout>
24 #include <QGraphicsScene>
25 #include <QHBoxLayout>
26 #include <QLabel>
27 #include <QLineEdit>
28 #include <QLineF>
29 #include <QMessageBox>
30 #include <QSettings>
31 #include <QStyle>
32 #include <QTabWidget>
33 #include <QToolButton>
34 #include <QVBoxLayout>
35 
36 #include <algorithm>
37 
38 //-----------------------------------------------------------------------------
39 
Board(QWidget * parent)40 Board::Board(QWidget* parent)
41 	: QWidget(parent)
42 	, m_paused(false)
43 	, m_wrong(false)
44 	, m_wrong_typed(false)
45 	, m_show_counts(1)
46 	, m_size(0)
47 	, m_minimum(0)
48 	, m_maximum(0)
49 	, m_max_score(0)
50 	, m_generator(nullptr)
51 {
52 	m_generator = new Generator(this);
53 	connect(m_generator, &Generator::finished, this, &Board::gameStarted);
54 	connect(m_generator, &Generator::optimizingStarted, this, &Board::optimizingStarted);
55 	connect(m_generator, &Generator::optimizingFinished, this, &Board::optimizingFinished);
56 
57 	m_view = new View(nullptr, this);
58 
59 	// Create clock and score widgets
60 	m_clock = new Clock(this);
61 	connect(m_clock, &Clock::finished, this, &Board::finish);
62 
63 	m_score = new QLabel(this);
64 
65 	m_max_score_details = new QToolButton(this);
66 	m_max_score_details->setAutoRaise(true);
67 	m_max_score_details->setIconSize(QSize(16, 16));
68 	m_max_score_details->setIcon(QIcon::fromTheme("dialog-information", QIcon(":/dialog-information.png")));
69 	m_max_score_details->setToolTip(tr("Details"));
70 	connect(m_max_score_details, &QToolButton::clicked, this, &Board::showMaximumWords);
71 
72 	QHBoxLayout* score_layout = new QHBoxLayout;
73 	score_layout->setContentsMargins(0, 0, 0, 0);
74 	score_layout->addWidget(m_score);
75 	score_layout->addWidget(m_max_score_details);
76 
77 	// Create guess widgets
78 	m_guess = new QLineEdit(this);
79 	m_guess->setDisabled(true);
80 	m_guess->setMaxLength(16);
81 	m_guess->installEventFilter(this);
82 	m_guess->setClearButtonEnabled(true);
83 	connect(m_guess, &QLineEdit::textEdited, this, &Board::guessChanged);
84 	connect(m_guess, &QLineEdit::returnPressed, this, &Board::guess);
85 	connect(m_view, &View::mousePressed, m_guess, QOverload<>::of(&QLineEdit::setFocus));
86 
87 	int size = style()->pixelMetric(QStyle::PM_ToolBarIconSize);
88 
89 	m_guess_button = new QToolButton(this);
90 	m_guess_button->setAutoRaise(true);
91 	m_guess_button->setIconSize(QSize(size, size));
92 	QIcon guess_fallback(":/tango/64x64/actions/list-add.png");
93 	guess_fallback.addFile(":/tango/48x48/actions/list-add.png");
94 	guess_fallback.addFile(":/tango/32x32/actions/list-add.png");
95 	guess_fallback.addFile(":/tango/24x24/actions/list-add.png");
96 	guess_fallback.addFile(":/tango/22x22/actions/list-add.png");
97 	guess_fallback.addFile(":/tango/16x16/actions/list-add.png");
98 	m_guess_button->setIcon(QIcon::fromTheme("list-add", guess_fallback));
99 	m_guess_button->setToolTip(tr("Guess"));
100 	m_guess_button->setEnabled(false);
101 	connect(m_guess_button, &QToolButton::clicked, this, &Board::guess);
102 
103 	QHBoxLayout* guess_layout = new QHBoxLayout;
104 	guess_layout->setSpacing(0);
105 	guess_layout->addStretch();
106 	guess_layout->addWidget(m_guess);
107 	guess_layout->addWidget(m_guess_button);
108 	guess_layout->addStretch();
109 
110 	// Create word lists
111 	m_found = new WordTree(this);
112 	m_found->setFocusPolicy(Qt::TabFocus);
113 	connect(m_found, &WordTree::itemSelectionChanged, this, &Board::wordSelected);
114 
115 	m_missed = new WordTree(this);
116 	m_missed->setFocusPolicy(Qt::TabFocus);
117 	m_missed->hide();
118 	connect(m_missed, &WordTree::itemSelectionChanged, this, &Board::wordSelected);
119 
120 	QWidget* found_tab = new QWidget(this);
121 	QVBoxLayout* found_layout = new QVBoxLayout(found_tab);
122 	found_layout->setSpacing(0);
123 	found_layout->setContentsMargins(0, 0, 0, 0);
124 	found_layout->addLayout(guess_layout);
125 	found_layout->addWidget(m_found);
126 
127 	m_tabs = new QTabWidget(this);
128 	m_tabs->addTab(found_tab, tr("Found"));
129 	connect(m_tabs, &QTabWidget::currentChanged, this, &Board::clearGuess);
130 
131 	int width = guess_layout->sizeHint().width();
132 	m_tabs->setFixedWidth(width);
133 
134 	m_counts = new WordCounts(this);
135 	m_counts->setMinimumWidth(width);
136 
137 	// Lay out board
138 	QGridLayout* layout = new QGridLayout(this);
139 	layout->setColumnStretch(1, 1);
140 	layout->setColumnStretch(1, 1);
141 	layout->setRowStretch(1, 1);
142 	layout->addWidget(m_tabs, 0, 0, 3, 1);
143 	layout->addWidget(m_clock, 0, 1, Qt::AlignCenter);
144 	layout->addWidget(m_view, 1, 1);
145 	layout->addLayout(score_layout, 2, 1, Qt::AlignCenter);
146 	layout->addWidget(m_counts, 3, 0, 1, 2);
147 }
148 
149 //-----------------------------------------------------------------------------
150 
~Board()151 Board::~Board()
152 {
153 	m_generator->cancel();
154 
155 	QSettings game;
156 	if (isFinished()) {
157 		// Clear current game
158 		game.remove("Current");
159 	} else {
160 		// Save current game
161 		game.beginGroup("Current");
162 
163 		QStringList found;
164 		for (int i = 0; i < m_found->topLevelItemCount(); ++i) {
165 			found += m_found->topLevelItem(i)->text(2);
166 		}
167 		game.setValue("Found", found);
168 
169 		QVariantList positions;
170 		QString word;
171 		for (const QPoint& position : qAsConst(m_positions)) {
172 			positions.append(position);
173 			word.append(m_cells[position.x()][position.y()]->text().toUpper());
174 		}
175 		if (!m_wrong && (m_guess->text() == word)) {
176 			game.setValue("Guess", m_guess->text());
177 			game.setValue("GuessPositions", positions);
178 		} else {
179 			game.remove("Guess");
180 			game.remove("GuessPositions");
181 		}
182 
183 		m_clock->save(game);
184 	}
185 }
186 
187 //-----------------------------------------------------------------------------
188 
isFinished() const189 bool Board::isFinished() const
190 {
191 	return m_clock->isFinished();
192 }
193 
194 //-----------------------------------------------------------------------------
195 
abort()196 void Board::abort()
197 {
198 	m_generator->cancel();
199 	m_clock->stop();
200 }
201 
202 //-----------------------------------------------------------------------------
203 
generate(const QSettings & game)204 bool Board::generate(const QSettings& game)
205 {
206 	constexpr unsigned int TANGLET_FILE_VERSION = 3;
207 
208 	// Verify version
209 	if (game.value("Version").toUInt() > TANGLET_FILE_VERSION) {
210 		return false;
211 	}
212 
213 	// Find values
214 	int size = qBound(4, game.value("Size").toInt(), 5);
215 	int density = qBound(0, game.value("Density").toInt(), 3);
216 	int minimum = game.value("Minimum").toInt();
217 	if (size == 4) {
218 		minimum = qBound(3, minimum, 6);
219 	} else {
220 		minimum = qBound(4, minimum, 7);
221 	}
222 	int timer = qBound(0, game.value("TimerMode").toInt(), Clock::TotalTimers - 1);
223 	QStringList letters = game.value("Letters").toStringList();
224 
225 	// Verify board size
226 	if (game.contains("Version") && ((size * size) != letters.size())) {
227 		return false;
228 	}
229 
230 	const LanguageSettings language(game);
231 	const bool is_hebrew = (QLocale(language.language()).language() == QLocale::Hebrew);
232 	m_found->setHebrew(is_hebrew);
233 	m_missed->setHebrew(is_hebrew);
234 
235 	// Store values
236 	{
237 		QSettings settings;
238 		settings.beginGroup("Current");
239 		settings.setValue("Version", TANGLET_FILE_VERSION);
240 		settings.setValue("Size", size);
241 		settings.setValue("Density", density);
242 		settings.setValue("Minimum", minimum);
243 		settings.setValue("TimerMode", timer);
244 		settings.setValue("Locale", language.language());
245 		settings.setValue("Dice", language.dice());
246 		settings.setValue("Words", language.words());
247 		settings.setValue("Dictionary", language.dictionary());
248 		if (!letters.isEmpty()) {
249 			settings.setValue("Letters", letters);
250 		}
251 	}
252 
253 	// Create new game
254 	m_generator->cancel();
255 	m_generator->create(density, size, minimum, timer, letters);
256 
257 	return true;
258 }
259 
260 //-----------------------------------------------------------------------------
261 
setPaused(bool pause)262 void Board::setPaused(bool pause)
263 {
264 	if (isFinished()) {
265 		return;
266 	}
267 
268 	m_paused = pause;
269 	m_guess->setDisabled(m_paused);
270 	m_clock->setPaused(m_paused);
271 
272 	if (!m_paused) {
273 		m_guess->setFocus();
274 	}
275 }
276 
277 //-----------------------------------------------------------------------------
278 
sizeToString(int size)279 QString Board::sizeToString(int size)
280 {
281 	return (size == 4) ? tr("Normal") : tr("Large");
282 }
283 
284 //-----------------------------------------------------------------------------
285 
setShowMaximumScore(QAction * show)286 void Board::setShowMaximumScore(QAction* show)
287 {
288 	int score_type = show->data().toInt();
289 	QSettings().setValue("ShowMaximumScore", score_type);
290 	m_show_counts = score_type;
291 	m_max_score_details->setVisible(isFinished() && m_show_counts && m_clock->timer() == Clock::Allotment);
292 	updateScore();
293 }
294 
295 //-----------------------------------------------------------------------------
296 
setShowMissedWords(bool show)297 void Board::setShowMissedWords(bool show)
298 {
299 	QSettings().setValue("ShowMissed", show);
300 	if (show) {
301 		if (m_tabs->count() == 1) {
302 			m_tabs->addTab(m_missed, tr("Missed"));
303 			m_tabs->setTabEnabled(1, isFinished());
304 		}
305 	} else {
306 		if (m_tabs->count() == 2) {
307 			m_tabs->removeTab(1);
308 			m_missed->hide();
309 		}
310 	}
311 }
312 
313 //-----------------------------------------------------------------------------
314 
setShowWordCounts(bool show)315 void Board::setShowWordCounts(bool show)
316 {
317 	QSettings().setValue("ShowWordCounts", show);
318 	m_counts->setVisible(show);
319 }
320 
321 //-----------------------------------------------------------------------------
322 
gameStarted()323 void Board::gameStarted()
324 {
325 	// Load settings
326 	QSettings settings;
327 	settings.beginGroup("Current");
328 
329 	m_clock->setTimer(m_generator->timer());
330 	if (m_generator->size() != m_size) {
331 		m_size = m_generator->size();
332 		m_cells = QVector<QVector<Letter*>>(m_size, QVector<Letter*>(m_size));
333 		m_maximum = m_size * m_size;
334 		m_guess->setMaxLength(m_maximum);
335 	}
336 	m_minimum = m_generator->minimum();
337 	m_max_score = m_generator->maxScore();
338 	m_max_score_details->hide();
339 	m_letters = m_generator->letters();
340 	m_solutions = m_generator->solutions();
341 	m_counts->setWords(m_solutions.keys());
342 	m_found->setDictionary(m_generator->dictionary());
343 	m_found->setTrie(m_generator->trie());
344 	m_missed->setDictionary(m_generator->dictionary());
345 	m_missed->setTrie(m_generator->trie());
346 	settings.setValue("Letters", m_letters);
347 
348 	// Create board
349 	QFont f = font();
350 	f.setBold(true);
351 	f.setPointSize(20);
352 	QFontMetrics metrics(f);
353 	int letter_size = 0;
354 	const auto dice = m_generator->dice(m_size);
355 	for (const QStringList& die : dice) {
356 		for (const QString& side : die) {
357 			letter_size = std::max(letter_size, metrics.boundingRect(side).width());
358 		}
359 	}
360 	int cell_size = std::max(metrics.height(), letter_size) + 10;
361 	int cell_padding_size = cell_size + 4;
362 	int board_size = (m_size * cell_padding_size) + 8;
363 
364 	delete m_view->scene();
365 	QGraphicsScene* scene = new QGraphicsScene(0, 0, board_size, board_size, this);
366 	m_view->setScene(scene);
367 	m_view->setMinimumSize(board_size + 4, board_size + 4);
368 	m_view->fitInView(m_view->sceneRect(), Qt::KeepAspectRatio);
369 
370 	BeveledRect* rect = new BeveledRect(board_size);
371 	rect->setColor(QColor(0, 0x57, 0xae));
372 	scene->addItem(rect);
373 
374 	// Create cells
375 	for (int r = 0; r < m_size; ++r) {
376 		for (int c = 0; c < m_size; ++c) {
377 			Letter* cell = new Letter(f, cell_size, QPoint(c, r));
378 			cell->setText(m_letters.at((r * m_size) + c));
379 			cell->moveBy((c * cell_padding_size) + 6, (r * cell_padding_size) + 6);
380 			scene->addItem(cell);
381 			m_cells[c][r] = cell;
382 			connect(cell, &Letter::clicked, this, &Board::letterClicked);
383 		}
384 	}
385 
386 	// Switch to found tab
387 	m_tabs->setCurrentWidget(m_found);
388 	m_tabs->setTabEnabled(1, false);
389 
390 	// Clear previous words
391 	m_paused = false;
392 	emit pauseAvailable(true);
393 	m_guess_button->setEnabled(true);
394 	m_positions.clear();
395 	m_found->removeAll();
396 	m_found->setColumnHidden(1, true);
397 	m_missed->removeAll();
398 	m_guess->setEnabled(true);
399 	m_guess->clear();
400 	m_guess->setEchoMode(QLineEdit::Normal);
401 	m_guess->setFocus();
402 	clearHighlight();
403 
404 	// Add solutions
405 	for (auto i = m_solutions.cbegin(), end = m_solutions.cend(); i != end; ++i) {
406 		m_missed->addWord(i.key());
407 	}
408 
409 	// Add found words
410 	const QStringList found = settings.value("Found").toStringList();
411 	for (const QString& text : found) {
412 		QTreeWidgetItem* item = m_found->findItems(text, Qt::MatchExactly, 2).value(0);
413 		if (m_missed->findItems(text, Qt::MatchExactly, 2).value(0) && !item) {
414 			m_found->addWord(text);
415 			delete m_missed->findItems(text, Qt::MatchExactly, 2).constFirst();
416 			m_counts->findWord(text);
417 		}
418 	}
419 
420 	// Add guess
421 	m_guess->setText(settings.value("Guess").toString());
422 	const QVariantList positions = settings.value("GuessPositions").toList();
423 	for (const QVariant& position : positions) {
424 		m_positions.append(position.toPoint());
425 	}
426 
427 	// Show errors
428 	QString error = m_generator->error();
429 	if (!error.isEmpty()) {
430 		abort();
431 		QMessageBox::warning(this, tr("Error"), error);
432 		return;
433 	}
434 
435 	// Start game
436 	emit started();
437 	if (m_missed->topLevelItemCount() > 0) {
438 		m_clock->start();
439 		if (settings.contains("TimerDetails/Time")) {
440 			m_clock->load(settings);
441 		}
442 		updateScore();
443 		updateClickableStatus();
444 		updateButtons();
445 		highlightWord();
446 	} else {
447 		m_clock->stop();
448 	}
449 }
450 
451 //-----------------------------------------------------------------------------
452 
clearGuess()453 void Board::clearGuess()
454 {
455 	m_wrong_typed = false;
456 	m_wrong = false;
457 	m_positions.clear();
458 	clearHighlight();
459 	updateClickableStatus();
460 	m_guess->clear();
461 	m_found->setCurrentItem(nullptr);
462 	m_missed->setCurrentItem(nullptr);
463 	m_guess->setFocus();
464 	updateButtons();
465 }
466 
467 //-----------------------------------------------------------------------------
468 
guess()469 void Board::guess()
470 {
471 	if (isFinished() || m_paused || m_positions.isEmpty() || m_wrong_typed || m_wrong) {
472 		return;
473 	}
474 
475 	const QString text = m_guess->text().trimmed().toUpper();
476 	if (text.isEmpty() || (text.length() < m_minimum) || (text.length() > m_maximum)) {
477 		return;
478 	}
479 	if (!m_solutions.contains(text)) {
480 		m_wrong = true;
481 		highlightWord();
482 		updateButtons();
483 		m_clock->addIncorrectWord(Solver::score(text));
484 		return;
485 	}
486 
487 	// Create found item
488 	QTreeWidgetItem* item = m_found->findItems(text, Qt::MatchExactly, 2).value(0);
489 	if (!item) {
490 		item = m_found->addWord(text);
491 		delete m_missed->findItems(item->text(2), Qt::MatchExactly, 2).constFirst();
492 
493 		m_clock->addWord(item->data(0, Qt::UserRole).toInt());
494 		updateScore();
495 
496 		QList<QList<QPoint>>& solutions = m_solutions[text];
497 		const int index = solutions.indexOf(m_positions);
498 		if (index != -1) {
499 			solutions.move(index, 0);
500 		} else {
501 			solutions.prepend(m_positions);
502 		}
503 
504 		m_counts->findWord(text);
505 	}
506 	m_found->scrollToItem(item);
507 	m_found->setCurrentItem(nullptr);
508 
509 	// Clear guess
510 	clearGuess();
511 
512 	// Handle finding all of the words
513 	if (m_missed->topLevelItemCount() == 0) {
514 		// Increase score
515 		for (int i = 0; i < m_found->topLevelItemCount(); ++i) {
516 			QTreeWidgetItem* item = m_found->topLevelItem(i);
517 			item->setData(0, Qt::UserRole, item->data(0, Qt::UserRole).toInt() + 1);
518 		}
519 
520 		// Stop the game
521 		m_clock->stop();
522 	}
523 }
524 
525 //-----------------------------------------------------------------------------
526 
guessChanged()527 void Board::guessChanged()
528 {
529 	m_wrong_typed = false;
530 	m_wrong = false;
531 	clearHighlight();
532 	m_found->setCurrentItem(nullptr);
533 
534 	QString word = m_guess->text().trimmed().toUpper();
535 	if (!word.isEmpty()) {
536 		int pos = m_guess->cursorPosition();
537 		m_guess->setText(word);
538 		m_guess->setCursorPosition(pos);
539 		QTreeWidgetItem* item = m_found->findItems(word, Qt::MatchStartsWith, 2).value(0);
540 		m_found->scrollToItem(item, QAbstractItemView::PositionAtTop);
541 
542 		Trie trie(word);
543 		Solver solver(trie, m_size, 0);
544 		solver.solve(m_letters);
545 		QList<QList<QPoint>> solutions = m_solutions.value(word, solver.solutions().value(word));
546 		m_wrong_typed = solutions.isEmpty();
547 		if (!m_wrong_typed) {
548 			int index = 0;
549 			int matched = -1;
550 			int difference = INT_MAX;
551 
552 			int count = solutions.count();
553 			for (int i = 0; i < count; i++) {
554 				bool order = true;
555 				int match = 0;
556 				int prev_pos = -1;
557 				int deltas = INT_MAX;
558 
559 				// Find how many cells match and are in order between solution and m_positions
560 				const QList<QPoint>& solution = solutions.at(i);
561 				for (const QPoint& cell : solution) {
562 					int pos = m_positions.indexOf(cell);
563 					if (pos != -1) {
564 						match++;
565 						if (prev_pos != -1) {
566 							int delta = pos - prev_pos;
567 							if (delta < 0) {
568 								order = false;
569 								break;
570 							} else {
571 								deltas += delta;
572 							}
573 						} else {
574 							deltas = pos;
575 						}
576 						prev_pos = pos;
577 					}
578 				}
579 
580 				// Figure out if current solution best matches m_positions
581 				if (order == true && (match > matched || (match == matched && deltas < difference))) {
582 					matched = match;
583 					difference = deltas;
584 					index = i;
585 				}
586 			}
587 			m_positions = solutions.at(index);
588 		}
589 		updateClickableStatus();
590 		highlightWord();
591 
592 		selectGuess();
593 	} else {
594 		m_positions.clear();
595 		updateClickableStatus();
596 	}
597 
598 	updateButtons();
599 }
600 
601 //-----------------------------------------------------------------------------
602 
finish()603 void Board::finish()
604 {
605 	m_clock->setText((m_missed->topLevelItemCount() == 0 && m_found->topLevelItemCount() > 0) ? tr("Success") : tr("Game Over"));
606 
607 	clearGuess();
608 	m_found->setColumnHidden(1, false);
609 	m_guess->setDisabled(true);
610 	m_guess_button->setDisabled(true);
611 	m_guess->setEchoMode(QLineEdit::NoEcho);
612 	m_guess->releaseKeyboard();
613 	m_tabs->setTabEnabled(1, true);
614 	m_max_score_details->setVisible(m_show_counts && m_clock->timer() == Clock::Allotment);
615 	emit pauseAvailable(false);
616 
617 	int score = updateScore();
618 	emit finished(score, m_max_score);
619 }
620 
621 //-----------------------------------------------------------------------------
622 
wordSelected()623 void Board::wordSelected()
624 {
625 	QList<QTreeWidgetItem*> items;
626 	if (m_tabs->currentWidget() == m_missed) {
627 		items = m_missed->selectedItems();
628 	} else {
629 		items = m_found->selectedItems();
630 	}
631 	if (items.isEmpty()) {
632 		return;
633 	}
634 
635 	QString word = items.first()->text(2);
636 	if (!word.isEmpty() && word != m_guess->text()) {
637 		m_guess->setText(word);
638 		m_positions = m_solutions.value(word).value(0);
639 		clearHighlight();
640 		updateClickableStatus();
641 		highlightWord();
642 	}
643 
644 	updateButtons();
645 }
646 
647 //-----------------------------------------------------------------------------
648 
letterClicked(Letter * letter)649 void Board::letterClicked(Letter* letter)
650 {
651 	// Handle adding a letter to the guess
652 	if (!m_positions.contains(letter->position())) {
653 		QString word = m_guess->text().trimmed().toUpper();
654 		word.append(letter->text().toUpper());
655 		m_guess->setText(word);
656 		QTreeWidgetItem* item = m_found->findItems(word, Qt::MatchStartsWith, 2).value(0);
657 		m_found->scrollToItem(item, QAbstractItemView::PositionAtTop);
658 
659 		m_wrong = false;
660 		m_positions.append(letter->position());
661 		clearHighlight();
662 		updateClickableStatus();
663 		highlightWord();
664 
665 		selectGuess();
666 	// Handle making or clearing a guess
667 	} else if (letter->position() == m_positions.last()) {
668 		if (m_positions.count() == 1) {
669 			clearGuess();
670 		} else {
671 			guess();
672 		}
673 	// Handle backing up in a guess
674 	} else {
675 		m_wrong = false;
676 		m_guess->clear();
677 		m_positions = m_positions.mid(0, m_positions.indexOf(letter->position()) + 1);
678 		clearHighlight();
679 		updateClickableStatus();
680 
681 		QString word;
682 		for (const QPoint& position : qAsConst(m_positions)) {
683 			word.append(m_cells[position.x()][position.y()]->text().toUpper());
684 		}
685 		m_guess->setText(word);
686 		highlightWord();
687 
688 		selectGuess();
689 	}
690 
691 	updateButtons();
692 }
693 
694 //-----------------------------------------------------------------------------
695 
highlightWord(const QList<QPoint> & positions,const QColor & color)696 void Board::highlightWord(const QList<QPoint>& positions, const QColor& color)
697 {
698 	Q_ASSERT(!positions.isEmpty());
699 
700 	m_cells[positions.at(0).x()][positions.at(0).y()]->setColor(color);
701 	for (int i = 1; i < positions.count(); ++i) {
702 		const QPoint& pos1 = positions.at(i);
703 		m_cells[pos1.x()][pos1.y()]->setColor(color);
704 
705 		const QPoint& pos0 = m_positions.at(i - 1);
706 		QLineF line(pos0, pos1);
707 		m_cells[pos0.x()][pos0.y()]->setArrow(line.angle(), i - 1);
708 	}
709 
710 	int alpha = 192 / positions.count();
711 	QColor border = Qt::white;
712 	for (int i = positions.count() - 1; i >= 0; --i) {
713 		const QPoint& position = positions.at(i);
714 		m_cells[position.x()][position.y()]->setCellColor(border);
715 		border.setAlpha(border.alpha() - alpha);
716 	}
717 }
718 
719 //-----------------------------------------------------------------------------
720 
highlightWord()721 void Board::highlightWord()
722 {
723 	QString guess = m_guess->text();
724 	if (guess.isEmpty()) {
725 		return;
726 	}
727 
728 	QPalette p = palette();
729 	if (!m_wrong) {
730 		if (m_wrong_typed) {
731 			p.setColor(m_guess->foregroundRole(), Qt::white);
732 			p.setColor(m_guess->backgroundRole(), Qt::red);
733 		} else if (!m_found->findItems(guess, Qt::MatchExactly).isEmpty()) {
734 			p.setColor(m_guess->foregroundRole(), Qt::white);
735 			p.setColor(m_guess->backgroundRole(), QColor(0xff, 0xaa, 0));
736 			highlightWord(m_positions, QColor(0xff, 0xaa, 0));
737 		} else if (m_positions.count() < m_minimum) {
738 			highlightWord(m_positions, QColor(0xbf, 0xd9, 0xff));
739 		} else {
740 			highlightWord(m_positions, QColor(0x80, 0xb3, 0xff));
741 		}
742 	} else {
743 		p.setColor(m_guess->foregroundRole(), Qt::white);
744 		p.setColor(m_guess->backgroundRole(), Qt::red);
745 		highlightWord(m_positions, Qt::red);
746 	}
747 	if (m_guess->isEnabled()) {
748 		m_guess->setPalette(p);
749 	}
750 }
751 
752 //-----------------------------------------------------------------------------
753 
clearHighlight()754 void Board::clearHighlight()
755 {
756 	QColor color = !isFinished() ? Qt::white : QColor(0xaa, 0xaa, 0xaa);
757 	for (int c = 0; c < m_size; ++c) {
758 		for (int r = 0; r < m_size; ++r) {
759 			m_cells[c][r]->setColor(color);
760 			m_cells[c][r]->setCellColor(QColor());
761 			m_cells[c][r]->setArrow(-1, 0);
762 		}
763 	}
764 	m_guess->setPalette(palette());
765 }
766 
767 //-----------------------------------------------------------------------------
768 
selectGuess()769 void Board::selectGuess()
770 {
771 	QTreeWidgetItem* item = m_found->findItems(m_guess->text(), Qt::MatchExactly, 0).value(0);
772 	if (item) {
773 		m_found->setCurrentItem(item);
774 		m_found->scrollToItem(item);
775 	} else {
776 		m_found->setCurrentItem(nullptr);
777 	}
778 }
779 
780 //-----------------------------------------------------------------------------
781 
updateScore()782 int Board::updateScore()
783 {
784 	int score = 0;
785 	for (int i = 0; i < m_found->topLevelItemCount(); ++i) {
786 		score += m_found->topLevelItem(i)->data(0, Qt::UserRole).toInt();
787 	}
788 
789 	if (m_show_counts == 2 || (m_show_counts == 1 && isFinished())) {
790 		if (score > 3) {
791 			m_score->setText(tr("%1 of %n point(s)", "", m_max_score).arg(score));
792 		} else if (score == 3) {
793 			m_score->setText(tr("3 of %n point(s)", "", m_max_score));
794 		} else if (score == 2) {
795 			m_score->setText(tr("2 of %n point(s)", "", m_max_score));
796 		} else if (score == 1) {
797 			m_score->setText(tr("1 of %n point(s)", "", m_max_score));
798 		} else {
799 			m_score->setText(tr("0 of %n point(s)", "", m_max_score));
800 		}
801 		m_counts->setMaximumsVisible(true);
802 	} else {
803 		m_score->setText(tr("%n point(s)", "", score));
804 		m_counts->setMaximumsVisible(false);
805 	}
806 
807 	QFont f = font();
808 	QPalette p = palette();
809 	switch (ScoresDialog::isHighScore(score, m_clock->timer())) {
810 	case 2:
811 		f.setBold(true);
812 		p.setColor(m_score->foregroundRole(), Qt::blue);
813 		break;
814 	case 1:
815 		f.setBold(true);
816 		break;
817 	default:
818 		break;
819 	}
820 	m_score->setFont(f);
821 	m_score->setPalette(p);
822 
823 	return score;
824 }
825 
826 //-----------------------------------------------------------------------------
827 
updateClickableStatus()828 void Board::updateClickableStatus()
829 {
830 	bool finished = isFinished();
831 	bool has_word = !m_positions.isEmpty() && !finished;
832 	bool clickable = !has_word && !finished;
833 
834 	for (int y = 0; y < m_size; ++y) {
835 		for (int x = 0; x < m_size; ++x) {
836 			m_cells[x][y]->setClickable(clickable);
837 		}
838 	}
839 
840 	if (has_word && !m_wrong_typed) {
841 		const QPoint& position = m_positions.last();
842 		int min_x = std::max(position.x() - 1, 0);
843 		int max_x = std::min(position.x() + 2, m_size);
844 		int min_y = std::max(position.y() - 1, 0);
845 		int max_y = std::min(position.y() + 2, m_size);
846 		for (int y = min_y; y < max_y; ++y) {
847 			for (int x = min_x; x < max_x; ++x) {
848 				m_cells[x][y]->setClickable(true);
849 			}
850 		}
851 
852 		for (const QPoint& position : qAsConst(m_positions)) {
853 			m_cells[position.x()][position.y()]->setClickable(true);
854 		}
855 	}
856 }
857 
858 //-----------------------------------------------------------------------------
859 
updateButtons()860 void Board::updateButtons()
861 {
862 	QString text = m_guess->text();
863 	bool has_guess = !text.isEmpty();
864 	m_guess_button->setEnabled(has_guess
865 			&& (text.length() >= m_minimum)
866 			&& (text.length() <= m_maximum)
867 			&& !m_positions.isEmpty()
868 			&& !m_wrong_typed
869 			&& !m_wrong);
870 }
871 
872 //-----------------------------------------------------------------------------
873 
showMaximumWords()874 void Board::showMaximumWords()
875 {
876 	QDialog dialog(window(), Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
877 	dialog.setWindowTitle(tr("Details"));
878 
879 	QList<int> scores;
880 	for (auto i = m_solutions.cbegin(), end = m_solutions.cend(); i != end; ++i) {
881 		scores.append(Solver::score(i.key()));
882 	}
883 	std::sort(scores.begin(), scores.end(), std::greater<int>());
884 	scores = scores.mid(0, 30);
885 
886 	QLabel* message = new QLabel(tr("The maximum score was calculated from the following thirty words:"), this);
887 	message->setWordWrap(true);
888 
889 	WordTree* words = new WordTree(this);
890 	words->setTrie(m_generator->trie());
891 	words->setDictionary(m_generator->dictionary());
892 	const QList<QTreeWidget*> trees{ m_found, m_missed };
893 	for (QTreeWidget* tree : trees) {
894 		for (int i = 0; i < tree->topLevelItemCount(); ++i) {
895 			QTreeWidgetItem* item = tree->topLevelItem(i);
896 			int index = scores.indexOf(item->data(0, Qt::UserRole).toInt());
897 			if (index != -1) {
898 				words->addWord(item->text(0));
899 				scores.removeAt(index);
900 			}
901 		}
902 	}
903 
904 	QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok, Qt::Horizontal, &dialog);
905 	buttons->setCenterButtons(style()->styleHint(QStyle::SH_MessageBox_CenterButtons));
906 	connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
907 
908 	QVBoxLayout* layout = new QVBoxLayout(&dialog);
909 	layout->addWidget(message);
910 	layout->addWidget(words);
911 	layout->addWidget(buttons);
912 
913 	dialog.exec();
914 }
915 
916 //-----------------------------------------------------------------------------
917