1 /*
2     KShisen - A japanese game similar to Mahjongg
3     SPDX-FileCopyrightText: 1997 Mario Weilguni <mweilguni@sime.com>
4     SPDX-FileCopyrightText: 2002-2004 Dave Corrie <kde@davecorrie.com>
5     SPDX-FileCopyrightText: 2007 Mauricio Piacentini <mauricio@tabuleiro.com>
6     SPDX-FileCopyrightText: 2009-2016 Frederik Schwarzer <schwarzer@kde.org>
7 
8     SPDX-License-Identifier: GPL-2.0-or-later
9 */
10 
11 // own
12 #include "app.h"
13 
14 // STL
15 #include <cmath>
16 
17 // Qt
18 #include <QIcon>
19 #include <QLabel>
20 #include <QPointer>
21 #include <QStatusBar>
22 #include <QTimer>
23 
24 // KF
25 #include <KActionCollection>
26 #include <KConfig>
27 #include <KConfigDialog>
28 #include <KLocalizedString>
29 #include <KMessageBox>
30 #include <KShortcutsDialog>
31 #include <KStandardAction>
32 #include <KToggleAction>
33 
34 // KDEGames
35 #include <KScoreDialog>
36 #include <KStandardGameAction>
37 
38 // LibKmahjongg
39 #include <KMahjonggConfigDialog>
40 
41 // KShisen
42 #include "board.h"
43 #include "debug.h"
44 #include "prefs.h"
45 #include "ui_settings.h"
46 
47 namespace KShisen
48 {
49 /**
50  * @brief Class holding the settings dialog and its functions
51  */
52 class Settings : public QWidget, public Ui::Settings
53 {
54 public:
Settings(QWidget * parent)55     explicit Settings(QWidget * parent)
56         : QWidget(parent)
57     {
58         setupUi(this);
59     }
60 };
61 
App(QWidget * parent)62 App::App(QWidget * parent)
63     : KXmlGuiWindow(parent)
64     , m_gameTipLabel(nullptr)
65     , m_gameTimerLabel(nullptr)
66     , m_gameTilesLabel(nullptr)
67     , m_gameCheatLabel(nullptr)
68     , m_board(nullptr)
69 {
70     m_board = new Board(this);
71     m_board->setObjectName(QStringLiteral("board"));
72 
73     setCentralWidget(m_board);
74 
75     setupStatusBar();
76     setupActions();
77     setupGUI();
78 
79     updateItems();
80     updateTileDisplay();
81 }
82 
83 
setupStatusBar()84 void App::setupStatusBar()
85 {
86     m_gameTipLabel = new QLabel(i18n("Select a tile"), statusBar());
87     statusBar()->addWidget(m_gameTipLabel, 1);
88 
89     m_gameTimerLabel = new QLabel(i18n("Time: 0:00:00"), statusBar());
90     statusBar()->addWidget(m_gameTimerLabel);
91 
92     m_gameTilesLabel = new QLabel(i18n("Removed: 0/0"), statusBar());
93     statusBar()->addWidget(m_gameTilesLabel);
94 
95     m_gameCheatLabel = new QLabel(i18n("Cheat mode"), statusBar());
96     statusBar()->addWidget(m_gameCheatLabel);
97     m_gameCheatLabel->hide();
98 }
99 
setupActions()100 void App::setupActions()
101 {
102     // Game
103     KStandardGameAction::gameNew(this, &App::invokeNewGame, actionCollection());
104     KStandardGameAction::restart(this, &App::restartGame, actionCollection());
105     KStandardGameAction::pause(this, &App::togglePause, actionCollection());
106     KStandardGameAction::highscores(this, &App::showHighscores, actionCollection());
107     KStandardGameAction::quit(this, &App::close, actionCollection());
108 
109     // Move
110     KStandardGameAction::undo(this, &App::undo, actionCollection());
111     KStandardGameAction::redo(this, &App::redo, actionCollection());
112     KStandardGameAction::hint(this, &App::hint, actionCollection());
113 
114     auto soundAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("speaker")), i18n("Play Sounds"), this);
115     soundAction->setChecked(Prefs::sounds());
116     actionCollection()->addAction(QStringLiteral("sounds"), soundAction);
117     connect(soundAction, &KToggleAction::triggered, m_board, &Board::setSoundsEnabled);
118 
119     // Settings
120     KStandardAction::preferences(this, &App::showSettingsDialog, actionCollection());
121 
122     connect(m_board, &Board::cheatStatusChanged, this, &App::updateCheatDisplay);
123     connect(m_board, &Board::changed, this, &App::updateItems);
124     connect(m_board, &Board::tilesDoNotMatch, this, &App::notifyTilesDoNotMatch);
125     connect(m_board, &Board::invalidMove, this, &App::notifyInvalidMove);
126     connect(m_board, &Board::selectATile, this, &App::notifySelectATile);
127     connect(m_board, &Board::selectAMatchingTile, this, &App::notifySelectAMatchingTile);
128     connect(m_board, &Board::selectAMove, this, &App::notifySelectAMove);
129 
130     auto timer = new QTimer(this);
131     connect(timer, &QTimer::timeout, this, &App::updateTimeDisplay);
132     timer->start(1000);
133 
134     connect(m_board, &Board::tileCountChanged, this, &App::updateTileDisplay);
135     connect(m_board, &Board::endOfGame, this, &App::slotEndOfGame);
136 
137     connect(this, &App::invokeNewGame, m_board, &Board::newGame);
138     connect(m_board, &Board::newGameStarted, this, &App::newGame);
139 }
140 
newGame()141 void App::newGame()
142 {
143     setCheatModeEnabled(false);
144     setPauseEnabled(false);
145     updateItems();
146     updateTileDisplay();
147 }
148 
restartGame()149 void App::restartGame()
150 {
151     m_board->setUpdatesEnabled(false);
152     while (m_board->canUndo()) {
153         m_board->undo();
154     }
155     m_board->resetRedo();
156     m_board->resetTimer();
157     setCheatModeEnabled(false);
158     m_board->setGameOverEnabled(false);
159     m_board->setGameStuckEnabled(false);
160     m_board->setUpdatesEnabled(true);
161     updateItems();
162 }
163 
togglePause()164 void App::togglePause()
165 {
166     m_board->setPauseEnabled(!m_board->isPaused());
167 }
168 
setPauseEnabled(bool enabled)169 void App::setPauseEnabled(bool enabled)
170 {
171     m_board->setPauseEnabled(enabled);
172     updateItems();
173 }
174 
undo()175 void App::undo()
176 {
177     if (!m_board->canUndo()) {
178         return;
179     }
180     m_board->undo();
181     setCheatModeEnabled(true);
182 
183     // If the game is stuck (no matching tiles anymore), the player can decide
184     // to undo some steps and try a different approach.
185     m_board->setGameStuckEnabled(false);
186 
187     updateItems();
188     updateTileDisplay();
189 }
190 
redo()191 void App::redo()
192 {
193     if (!m_board->canRedo()) {
194         return;
195     }
196     m_board->redo();
197     updateItems();
198     updateTileDisplay();
199 }
200 
hint()201 void App::hint()
202 {
203 #ifdef DEBUGGING
204     m_board->makeHintMove();
205 #else
206     m_board->showHint();
207     setCheatModeEnabled(true);
208 #endif
209     updateItems();
210 }
211 
updateItems()212 void App::updateItems()
213 {
214     if (m_board->isOver()) {
215         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Undo)))->setEnabled(false);
216         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Redo)))->setEnabled(false);
217         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Pause)))->setEnabled(false);
218         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Hint)))->setEnabled(false);
219     } else if (m_board->isPaused()) {
220         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Undo)))->setEnabled(false);
221         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Redo)))->setEnabled(false);
222         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Restart)))->setEnabled(false);
223         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Pause)))->setChecked(true);
224         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Hint)))->setEnabled(false);
225     } else if (m_board->isStuck()) {
226         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Pause)))->setEnabled(false);
227         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Hint)))->setEnabled(false);
228     } else {
229         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Undo)))->setEnabled(m_board->canUndo());
230         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Redo)))->setEnabled(m_board->canRedo());
231         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Restart)))->setEnabled(m_board->canUndo());
232         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Pause)))->setEnabled(true);
233         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Pause)))->setChecked(false);
234         actionCollection()->action(QLatin1String(KStandardGameAction::name(KStandardGameAction::Hint)))->setEnabled(true);
235     }
236 }
237 
slotEndOfGame()238 void App::slotEndOfGame()
239 {
240     if (m_board->tilesLeft() > 0) {
241         m_board->setGameStuckEnabled(true);
242     } else {
243         m_board->setGameOverEnabled(true);
244         auto const timeString = i18nc("time string: hh:mm:ss", "%1:%2:%3",
245                                       QString::asprintf("%02d", m_board->currentTime() / 3600),
246                                       QString::asprintf("%02d", (m_board->currentTime() / 60) % 60),
247                                       QString::asprintf("%02d", m_board->currentTime() % 60));
248         KScoreDialog::FieldInfo scoreInfo;
249         scoreInfo[KScoreDialog::Score].setNum(score(m_board->xTiles(), m_board->yTiles(), m_board->currentTime(), m_board->gravityFlag()));
250         scoreInfo[KScoreDialog::Time] = timeString;
251 
252         QPointer<KScoreDialog> scoreDialog = new KScoreDialog(KScoreDialog::Name | KScoreDialog::Time | KScoreDialog::Score, this);
253         scoreDialog->addField(KScoreDialog::Custom1, i18n("Gravity"), QStringLiteral("gravity"));
254         // FIXME: This is bad, because the translated words are stored in the highscores and thus switching the language makes ugly things (schwarzer)
255         if (m_board->gravityFlag()) {
256             scoreInfo[KScoreDialog::Custom1] = i18n("Yes");
257         } else {
258             scoreInfo[KScoreDialog::Custom1] = i18n("No");
259         }
260         auto const configGroup = QStringLiteral("%1x%2").arg(m_board->xTiles()).arg(m_board->yTiles());
261         scoreDialog->setConfigGroup(qMakePair(QByteArray(configGroup.toUtf8()), configGroup));
262 
263         if (m_board->hasCheated()) {
264             auto const message = i18n("\nYou could have been in the highscores\nif you did not use Undo or Hint.\nTry without them next time.");
265             KMessageBox::information(this, message, i18n("End of Game"));
266         } else {
267             if (scoreDialog->addScore(scoreInfo) > 0) {
268                 auto const message = i18n("Congratulations!\nYou made it into the hall of fame.");
269                 scoreDialog->setComment(message);
270                 scoreDialog->exec();
271             } else {
272                 auto const message = i18nc("%1 - time string like hh:mm:ss", "You made it in %1", timeString);
273                 KMessageBox::information(this, message, i18n("End of Game"));
274             }
275         }
276         delete scoreDialog;
277     }
278     updateItems();
279 }
280 
updateTimeDisplay()281 void App::updateTimeDisplay()
282 {
283     if (m_board->isStuck() || m_board->isOver()) {
284         return;
285     }
286     //qCDebug(KSHISEN_General) << "Time: " << m_board->currentTime();
287     auto const currentTime = m_board->currentTime();
288     auto const message = i18n("Your time: %1:%2:%3 %4",
289                               QString::asprintf("%02d", currentTime / 3600),
290                               QString::asprintf("%02d", (currentTime / 60) % 60),
291                               QString::asprintf("%02d", currentTime % 60),
292                               m_board->isPaused() ? i18n("(Paused) ") : QString());
293 
294     m_gameTimerLabel->setText(message);
295 }
296 
updateTileDisplay()297 void App::updateTileDisplay()
298 {
299     auto const numberOfTiles = m_board->tiles();
300     m_gameTilesLabel->setText(i18n("Removed: %1/%2 ", numberOfTiles - m_board->tilesLeft(), numberOfTiles));
301 }
302 
updateCheatDisplay()303 void App::updateCheatDisplay()
304 {
305     m_gameCheatLabel->setVisible(m_board->hasCheated());
306 }
307 
score(int x,int y,int seconds,bool gravity) const308 int App::score(int x, int y, int seconds, bool gravity) const
309 {
310     auto const ntiles = static_cast<double>(x * y);
311     auto const tilespersec = ntiles / static_cast<double>(seconds);
312 
313     auto const sizebonus = std::sqrt(ntiles / static_cast<double>(14.0 * 6.0));
314     auto const points = tilespersec / 0.14 * 100.0;
315 
316     if (gravity) {
317         return static_cast<int>(2.0 * points * sizebonus);
318     }
319     return static_cast<int>(points * sizebonus);
320 }
321 
notifySelectATile()322 void App::notifySelectATile()
323 {
324     m_gameTipLabel->setText(i18n("Select a tile"));
325 }
326 
notifySelectAMatchingTile()327 void App::notifySelectAMatchingTile()
328 {
329     m_gameTipLabel->setText(i18n("Select a matching tile"));
330 }
331 
notifySelectAMove()332 void App::notifySelectAMove()
333 {
334     m_gameTipLabel->setText(i18n("Select the move you want by clicking on the blue line"));
335 }
336 
notifyTilesDoNotMatch()337 void App::notifyTilesDoNotMatch()
338 {
339     m_gameTipLabel->setText(i18n("This tile did not match the one you selected"));
340 }
341 
notifyInvalidMove()342 void App::notifyInvalidMove()
343 {
344     m_gameTipLabel->setText(i18n("You cannot make this move"));
345 }
346 
setCheatModeEnabled(bool enabled)347 void App::setCheatModeEnabled(bool enabled)
348 {
349     m_board->setCheatModeEnabled(enabled);
350     m_gameCheatLabel->setVisible(enabled);
351 }
352 
showHighscores()353 void App::showHighscores()
354 {
355     KScoreDialog scoreDialog(KScoreDialog::Name | KScoreDialog::Time, this);
356     scoreDialog.addField(KScoreDialog::Custom1, i18n("Gravity"), QStringLiteral("gravity"));
357     auto const configGroup = QStringLiteral("%1x%2").arg(m_board->xTiles()).arg(m_board->yTiles());
358     scoreDialog.setConfigGroup(qMakePair(QByteArray(configGroup.toUtf8()), configGroup));
359     scoreDialog.exec();
360 }
361 
keyBindings()362 void App::keyBindings()
363 {
364     KShortcutsDialog::showDialog(actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, this);
365 }
366 
showSettingsDialog()367 void App::showSettingsDialog()
368 {
369     if (KConfigDialog::showDialog(QStringLiteral("settings"))) {
370         return;
371     }
372 
373     //Use the classes exposed by LibKmahjongg for our configuration dialog
374     auto dialog = new KMahjonggConfigDialog(this, QStringLiteral("settings"), Prefs::self());
375     dialog->addPage(new Settings(nullptr), i18n("General"), QStringLiteral("games-config-options"));
376     dialog->addTilesetPage();
377     dialog->addBackgroundPage();
378 
379     connect(dialog, &KMahjonggConfigDialog::settingsChanged, m_board, &Board::loadSettings);
380     dialog->show();
381 }
382 } // namespace KShisen
383 
384 // vim: expandtab:tabstop=4:shiftwidth=4
385 // kate: space-indent on; indent-width 4
386