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