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