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