1 /***************************************************************************
2 * Copyright (C) 2005 by Joshua Keel <joshuakeel@gmail.com> *
3 * (C) 2007-2021 by Jeremy Whiting <jpwhiting@kde.org> *
4 * (C) 2012 by Laszlo Papp <lpapp@kde.org> *
5 * *
6 * Portions of this code taken from KMessedWords by Reuben Sutton *
7 * *
8 * This program is free software; you can redistribute it and/or modify *
9 * it under the terms of the GNU General Public License as published by *
10 * the Free Software Foundation; either version 2 of the License, or *
11 * (at your option) any later version. *
12 * *
13 * This program is distributed in the hope that it will be useful, *
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
16 * GNU General Public License for more details. *
17 * *
18 * You should have received a copy of the GNU General Public License *
19 * along with this program; if not, write to the *
20 * Free Software Foundation, Inc., *
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
22 ***************************************************************************/
23
24 #include "kanagramgame.h"
25
26 #include "kanagramsettings.h"
27
28 #include <sharedkvtmlfiles.h>
29 #include <KEduVocDocument>
30 #include <KEduVocExpression>
31 #ifdef HAVE_SPEECH
32 #include <QTextToSpeech>
33 #endif
34
35 #include <KLocalizedString>
36 #include <sonnet/speller.h>
37
38 #include <QLocale>
39 #include <QFileInfo>
40 #include <QRandomGenerator>
41 #include <QStandardPaths>
42
KanagramGame()43 KanagramGame::KanagramGame()
44 : m_fileIndex(-1)
45 ,m_document(NULL)
46 #ifdef HAVE_SPEECH
47 ,m_speech(NULL)
48 #endif
49 ,m_totalScore(0)
50 ,m_totalScore2(0)
51 ,m_currentPlayerNumber(1)
52 ,m_speller(NULL)
53 {
54 loadSettings();
55
56 // Get the list of vocabularies
57 refreshVocabularyList();
58
59 // Load the default vocabulary
60 loadDefaultVocabulary();
61 #ifdef HAVE_SPEECH
62 m_speech = new QTextToSpeech(this);
63 #endif
64
65 m_speller = new Sonnet::Speller();
66 m_speller->setLanguage(sanitizedDataLanguage());
67 }
68
~KanagramGame()69 KanagramGame::~KanagramGame()
70 {
71 // Save any settings that may have changed
72 KanagramSettings::self()->save();
73
74 delete m_document;
75 m_document = NULL;
76 delete m_speller;
77 m_speller = NULL;
78 #ifdef HAVE_SPEECH
79 delete m_speech;
80 m_speech = NULL;
81 #endif
82 }
83
checkFile()84 bool KanagramGame::checkFile()
85 {
86 if (!QFile::exists(m_filename) && !QFile::exists(QStandardPaths::locate(QStandardPaths::GenericDataLocation, m_filename)))
87 {
88 emit fileError(m_filename);
89 return false;
90 }
91
92 return true;
93 }
94
sanitizedDataLanguage() const95 QString KanagramGame::sanitizedDataLanguage() const
96 {
97 QString dataLanguage = KanagramSettings::dataLanguage();
98 QStringList languageCodes = SharedKvtmlFiles::languages();
99
100 if (dataLanguage.isEmpty() || !languageCodes.contains(dataLanguage)) {
101 if (languageCodes.contains(QLocale::system().uiLanguages().at(0))) {
102 dataLanguage = QLocale::system().uiLanguages().at(0);
103 } else {
104 dataLanguage = QStringLiteral("en");
105 }
106
107 }
108
109 return dataLanguage;
110
111 }
112
loadDefaultVocabulary()113 void KanagramGame::loadDefaultVocabulary()
114 {
115 int index = KanagramSettings::currentVocabulary();
116 if (index == -1)
117 index = 0;
118 useVocabulary(index);
119 nextAnagram();
120 }
121
setSinglePlayerMode(bool singlePlayer)122 void KanagramGame::setSinglePlayerMode(bool singlePlayer)
123 {
124 KanagramSettings::setSinglePlayerMode(singlePlayer);
125 emit singlePlayerChanged();
126 }
127
singlePlayerMode()128 bool KanagramGame::singlePlayerMode()
129 {
130 return KanagramSettings::singlePlayerMode();
131 }
132
getPlayerNumber()133 int KanagramGame::getPlayerNumber()
134 {
135 return m_currentPlayerNumber;
136 }
137
setPlayerNumber(int pnumber)138 void KanagramGame::setPlayerNumber(int pnumber)
139 {
140 m_currentPlayerNumber = pnumber;
141 emit currentPlayerChanged();
142 }
143
refreshVocabularyList()144 bool KanagramGame::refreshVocabularyList()
145 {
146 QString oldFilename = m_filename;
147 m_fileList = SharedKvtmlFiles::fileNames(sanitizedDataLanguage());
148 if ( m_document ) {
149 useVocabulary(m_document->title());
150 }
151 return oldFilename != m_filename;
152 }
153
vocabularyList() const154 QStringList KanagramGame::vocabularyList() const
155 {
156 return SharedKvtmlFiles::titles(sanitizedDataLanguage());
157 }
158
useVocabulary(const QString & vocabularyname)159 void KanagramGame::useVocabulary(const QString &vocabularyname)
160 {
161 useVocabulary(vocabularyList().indexOf(vocabularyname));
162 }
163
useVocabulary(int index)164 void KanagramGame::useVocabulary(int index)
165 {
166 int previous = m_fileIndex;
167 if (index < 0 && m_fileList.size() > 0)
168 {
169 // Use the last
170 index = m_fileList.size() - 1;
171 }
172 else if (index >= m_fileList.size())
173 {
174 index = 0;
175 }
176
177 m_fileIndex = index;
178 m_filename = m_fileList.size() > index && index >= 0 ? m_fileList.at(index) : QString();
179
180 if (m_fileIndex != previous && checkFile()) {
181 delete m_document;
182 m_document = new KEduVocDocument(this);
183 ///@todo open returns KEduVocDocument::ErrorCode
184 m_document->open(QUrl::fromLocalFile(m_filename), KEduVocDocument::FileIgnoreLock);
185 m_answeredWords.clear();
186 // Save the setting
187 KanagramSettings::setCurrentVocabulary(index);
188 KanagramSettings::self()->save();
189 emit titleChanged();
190 }
191 }
192
previousVocabulary()193 void KanagramGame::previousVocabulary()
194 {
195 useVocabulary(m_fileIndex - 1);
196 }
197
nextVocabulary()198 void KanagramGame::nextVocabulary()
199 {
200 useVocabulary(m_fileIndex + 1);
201 }
202
nextAnagram()203 void KanagramGame::nextAnagram()
204 {
205 if (checkFile())
206 {
207 int totalWords = m_document->lesson()->entryCount(KEduVocLesson::Recursive);
208 int randomWordIndex = QRandomGenerator::global()->bounded(totalWords);
209
210 if (totalWords == (int)m_answeredWords.size())
211 {
212 m_answeredWords.clear();
213 }
214
215 if (totalWords > 0)
216 {
217 KEduVocTranslation *translation = m_document->lesson()->entries(KEduVocLesson::Recursive).at(randomWordIndex)->translation(0);
218
219 // Find the next word not used yet
220 while (m_answeredWords.contains(translation->text()))
221 {
222 randomWordIndex = QRandomGenerator::global()->bounded(totalWords);
223 translation = m_document->lesson()->entries(KEduVocLesson::Recursive).at(randomWordIndex)->translation(0);
224 }
225
226 // Make case consistent so german words that start capitalized will not
227 // be so easy to guess
228 if (KanagramSettings::uppercaseOnly())
229 {
230 m_originalWord = translation->text().toUpper();
231 }
232 else
233 {
234 m_originalWord = translation->text().toLower();
235 }
236 m_picHintUrl = translation->imageUrl();
237 m_audioUrl = translation->soundUrl();
238
239 m_answeredWords.append(m_originalWord);
240 createAnagram();
241 m_hint = translation->comment();
242
243 if (m_hint.isEmpty())
244 {
245 m_hint = i18n("No hint");
246 }
247 }
248 else
249 {
250 // this file has no entries
251 m_originalWord = QLatin1String("");
252 m_hint = QLatin1String("");
253 m_picHintUrl = QUrl();
254 m_audioUrl = QUrl();
255 // TODO: add some error reporting here
256 }
257 emit userAnswerChanged();
258 emit wordChanged();
259 }
260 }
261
filename() const262 QString KanagramGame::filename() const
263 {
264 return m_fileList.isEmpty() ? m_filename : m_fileList.at(m_fileIndex);
265 }
266
anagram() const267 QStringList KanagramGame::anagram() const
268 {
269 QStringList resultList;
270 for (const QChar &userLetter : qAsConst(m_anagram))
271 {
272 resultList.append(userLetter);
273 }
274
275 return resultList;
276 }
277
hint() const278 QString KanagramGame::hint() const
279 {
280 return m_hint;
281 }
282
word() const283 QString KanagramGame::word() const
284 {
285 return m_originalWord;
286 }
287
userAnswer() const288 QStringList KanagramGame::userAnswer() const
289 {
290 QStringList returnList;
291 for (const QChar &letter : qAsConst(m_userAnswer))
292 {
293 returnList.append(letter);
294 }
295 return returnList;
296 }
297
298
createAnagram()299 void KanagramGame::createAnagram()
300 {
301 if (m_originalWord.count(m_originalWord.at(0)) < m_originalWord.size()) {
302 QString anagram;
303 QString letters;
304 int randomIndex;
305
306 do {
307 anagram.clear();
308 letters = m_originalWord;
309 while (!letters.isEmpty())
310 {
311 randomIndex = QRandomGenerator::global()->bounded(letters.count());
312 anagram.append(letters.at(randomIndex));
313 letters.remove(randomIndex, 1);
314 }
315 } while (anagram == m_originalWord);
316
317 m_anagram = anagram;
318 m_userAnswer.clear();
319 } else {
320 m_anagram = m_originalWord;
321 m_userAnswer.clear();
322 }
323 }
324
useSounds()325 bool KanagramGame::useSounds()
326 {
327 return KanagramSettings::useSounds();
328 }
329
documentTitle() const330 QString KanagramGame::documentTitle() const
331 {
332 if (m_document)
333 {
334 return m_document->title();
335 }
336
337 return QString();
338 }
339
languageNames()340 QStringList KanagramGame::languageNames()
341 {
342 const QStringList languageCodes = SharedKvtmlFiles::languages();
343 if (languageCodes.isEmpty()) {
344 return QStringList();
345 }
346
347 QStringList languageNames;
348
349 // Get the language names from the language codes
350 KConfig entry(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("locale/") + "all_languages"));
351
352 for (const QString& languageCode : languageCodes)
353 {
354 KConfigGroup group = entry.group(languageCode);
355
356 QString languageName = group.readEntry("Name");
357 if (languageName.isEmpty())
358 {
359 languageName = i18nc("@item:inlistbox no language for that locale", "None");
360 }
361
362 languageNames.append(languageName);
363 m_languageCodeNameHash.insert(languageCode, languageName);
364 }
365
366 std::sort(languageNames.begin(), languageNames.end());
367 return languageNames;
368 }
369
dataLanguage() const370 QString KanagramGame::dataLanguage() const
371 {
372 return QLocale::languageToString(QLocale(sanitizedDataLanguage()).language());
373 }
374
setDataLanguage(const QString & dataLanguage)375 void KanagramGame::setDataLanguage(const QString& dataLanguage)
376 {
377 KanagramSettings::setDataLanguage(m_languageCodeNameHash.key(dataLanguage));
378 KanagramSettings::self()->save();
379 // Update the speller's language accordingly
380 m_speller->setLanguage(sanitizedDataLanguage());
381 emit dataLanguageChanged();
382 }
383
picHint()384 QUrl KanagramGame::picHint()
385 {
386 return m_picHintUrl;
387 }
388
audioFile()389 QUrl KanagramGame::audioFile()
390 {
391 return m_audioUrl;
392 }
393
394 #ifdef HAVE_SPEECH
wordRevealed()395 void KanagramGame::wordRevealed()
396 {
397 if (KanagramSettings::enablePronunciation())
398 {
399 say(m_originalWord);
400 }
401 }
402
say(const QString & text)403 void KanagramGame::say(const QString &text)
404 {
405 if ( text.isEmpty() )
406 return;
407
408 if ( m_speech )
409 {
410 m_speech->say(text);
411 }
412 }
413 #endif
414
hintHideTime()415 int KanagramGame::hintHideTime()
416 {
417 QString hideTimeString = KanagramSettings::hintHideTime();
418
419 int hintHideTime = getNumericSetting(hideTimeString);
420 if (hintHideTime)
421 return ((hintHideTime * 2) + 1);
422 else
423 return 0;
424 }
425
resolveTime()426 int KanagramGame::resolveTime()
427 {
428 return KanagramSettings::resolveTime().toInt();
429 }
430
scoreTime()431 int KanagramGame::scoreTime()
432 {
433 QString scoreTimeString = KanagramSettings::scoreTime();
434
435 int scoreTime = getNumericSetting(scoreTimeString);
436 return ((scoreTime + 1) * 15);
437 }
438
moveLetterToUserAnswer(int position)439 void KanagramGame::moveLetterToUserAnswer(int position)
440 {
441 m_userAnswer.append(m_anagram[position]);
442 m_anagram.remove(position, 1);
443 emit wordChanged();
444 emit userAnswerChanged();
445 }
446
moveLetterToAnagram(int position)447 void KanagramGame::moveLetterToAnagram(int position)
448 {
449 m_anagram.append(m_userAnswer[position]);
450 m_userAnswer.remove(position, 1);
451 emit wordChanged();
452 emit userAnswerChanged();
453 }
454
resetAnagram()455 void KanagramGame::resetAnagram()
456 {
457 m_anagram = m_userAnswer;
458 m_userAnswer.clear();
459 emit wordChanged();
460 emit userAnswerChanged();
461 }
462
moveLetter(const QString & letter)463 void KanagramGame::moveLetter(const QString &letter)
464 {
465 QString small = letter.toLower();
466 QString strippedAnagram = stripAccents(m_anagram);
467 int index = m_anagram.toLower().indexOf(small);
468 if (index != -1)
469 {
470 moveLetterToUserAnswer(index);
471 }
472 else
473 {
474 index = strippedAnagram.toLower().indexOf(small);
475 if (index != -1)
476 {
477 moveLetterToUserAnswer(index);
478 }
479 else
480 {
481 QString strippedAnswer = stripAccents(m_userAnswer);
482 index = m_userAnswer.toLower().indexOf(small);
483 if (index != -1)
484 {
485 moveLetterToAnagram(index);
486 }
487 else
488 {
489 index = strippedAnswer.toLower().indexOf(small);
490 if (index != -1)
491 {
492 moveLetterToAnagram(index);
493 }
494 }
495 }
496 }
497 }
498
getNumericSetting(const QString & settingString)499 int KanagramGame::getNumericSetting(const QString &settingString)
500 {
501 int indexFound_setting = settingString.size();
502 for (int k = 0; k < indexFound_setting; ++k)
503 {
504 if (!settingString.at(k).isDigit())
505 {
506 indexFound_setting = k;
507 break;
508 }
509 }
510 return settingString.leftRef(indexFound_setting).toInt();
511 }
512
resetTotalScore()513 void KanagramGame::resetTotalScore()
514 {
515 if (m_currentPlayerNumber == 1)
516 {
517 m_totalScore = 0;
518 m_totalScore2 = 0;
519 emit scoreChanged();
520 }
521 }
522
adjustScore(int points)523 void KanagramGame::adjustScore(int points)
524 {
525 if (m_currentPlayerNumber == 1)
526 m_totalScore += points;
527 else
528 m_totalScore2 += points;
529 emit scoreChanged();
530 }
531
totalScore()532 int KanagramGame::totalScore()
533 {
534 return m_totalScore;
535 }
536
totalScore2()537 int KanagramGame::totalScore2()
538 {
539 return m_totalScore2;
540 }
541
revealWord()542 void KanagramGame::revealWord()
543 {
544 m_anagram = m_originalWord;
545 emit wordChanged();
546 }
547
checkWord()548 bool KanagramGame::checkWord()
549 {
550 QString enteredWord = m_userAnswer.toLower().trimmed();
551 QString lowerOriginal = m_originalWord.toLower();
552 QString strippedOriginal = stripAccents(m_originalWord);
553 if (!enteredWord.isEmpty())
554 {
555 if (enteredWord == lowerOriginal ||
556 stripAccents(enteredWord) == strippedOriginal ||
557 (m_speller->isCorrect(enteredWord) && m_speller->isValid() &&
558 (isAnagram(enteredWord, lowerOriginal) ||
559 isAnagram(enteredWord, strippedOriginal))))
560 {
561 #ifdef HAVE_SPEECH
562 if (KanagramSettings::enablePronunciation())
563 {
564 // User wants words spoken
565 say(m_originalWord);
566 }
567 #endif
568 return true;
569 }
570 else
571 {
572 return false;
573 }
574 }
575 else
576 {
577 return false;
578 }
579 }
580
isAnagram(QString & enteredword,QString & word)581 bool KanagramGame::isAnagram(QString& enteredword, QString& word)
582 {
583 QString test = word;
584 if (enteredword.length() <= word.length())
585 {
586 for (int i=0; i < enteredword.length(); i++)
587 {
588 int found = test.indexOf(enteredword[i]);
589
590 if (found != -1)
591 {
592 test.remove(found, 1);
593 }
594 else
595 break;
596 }
597
598 if (test.isEmpty())
599 return true;
600 else
601 return false;
602 }
603 else
604 return false;
605 }
606
stripAccents(QString & original)607 QString KanagramGame::stripAccents(QString& original)
608 {
609 QString noAccents;
610 QString decomposed = original.normalized(QString::NormalizationForm_D);
611 for (int i = 0; i < decomposed.length(); ++i) {
612 if ( decomposed[i].category() != QChar::Mark_NonSpacing ) {
613 noAccents.append(decomposed[i]);
614 }
615 }
616 return noAccents;
617 }
618
reloadSettings()619 void KanagramGame::reloadSettings()
620 {
621 loadSettings();
622 if (refreshVocabularyList())
623 {
624 nextVocabulary();
625 nextAnagram();
626 }
627 }
628
loadSettings()629 void KanagramGame::loadSettings()
630 {
631 QString correctAnswerScore = KanagramSettings::correctAnswerScore();
632
633 m_correctAnswerScore = getNumericSetting(correctAnswerScore);
634 m_correctAnswerScore = (m_correctAnswerScore + 1) * 5;
635
636 QString incorrectAnswerScore = KanagramSettings::incorrectAnswerScore();
637
638 m_incorrectAnswerScore = getNumericSetting(incorrectAnswerScore);
639 m_incorrectAnswerScore = (m_incorrectAnswerScore + 1) * (-1);
640
641 QString revealAnswerScore = KanagramSettings::revealAnswerScore();
642
643 m_revealAnswerScore = getNumericSetting(revealAnswerScore);
644 m_revealAnswerScore = (m_revealAnswerScore + 1) * (-2);
645
646 QString skippedWordScore = KanagramSettings::skippedWordScore();
647
648 m_skippedWordScore = getNumericSetting(skippedWordScore);
649 m_skippedWordScore = (m_skippedWordScore + 1) * (-2);
650
651 if (KanagramSettings::dataLanguage().isEmpty())
652 {
653 const QStringList userLanguagesCode = QLocale::system().uiLanguages();
654 QStringList sharedKvtmlFilesLanguages = SharedKvtmlFiles::languages();
655 QString foundLanguage;
656 for (const QString &userLanguageCode : userLanguagesCode)
657 {
658 if (sharedKvtmlFilesLanguages.contains(userLanguageCode))
659 {
660 foundLanguage = userLanguageCode;
661 break;
662 }
663 }
664
665 KanagramSettings::setDataLanguage(!foundLanguage.isEmpty() ? foundLanguage : QStringLiteral("en"));
666 }
667 }
668
answerCorrect()669 void KanagramGame::answerCorrect()
670 {
671 if (m_currentPlayerNumber == 1)
672 m_totalScore += m_correctAnswerScore;
673 else
674 m_totalScore2 += m_correctAnswerScore;
675 emit scoreChanged();
676 }
677
answerIncorrect()678 void KanagramGame::answerIncorrect()
679 {
680 if (m_currentPlayerNumber == 1)
681 m_totalScore += m_incorrectAnswerScore;
682 else
683 m_totalScore2 += m_incorrectAnswerScore;
684 emit scoreChanged();
685 }
686
answerRevealed()687 void KanagramGame::answerRevealed()
688 {
689 if (m_currentPlayerNumber == 1)
690 m_totalScore += m_revealAnswerScore;
691 else
692 m_totalScore2 += m_revealAnswerScore;
693 emit scoreChanged();
694 }
695
answerSkipped()696 void KanagramGame::answerSkipped()
697 {
698 if (m_currentPlayerNumber == 1)
699 m_totalScore += m_skippedWordScore;
700 else
701 m_totalScore2 += m_skippedWordScore;
702 emit scoreChanged();
703 }
704