1 | /* kmahjongg, the classic mahjongg game for KDE project |
2 | * |
3 | * Copyright (C) 1997 Mathias Mueller <in5y158@public.uni-hamburg.de> |
4 | * Copyright (C) 2006-2007 Mauricio Piacentini <mauricio@tabuleiro.com> |
5 | * |
6 | * This program is free software; you can redistribute it and/or modify |
7 | * it under the terms of the GNU General Public License as published by |
8 | * the Free Software Foundation; either version 2 of the License, or |
9 | * (at your option) any later version. |
10 | * |
11 | * This program is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | * GNU General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License |
17 | * along with this program; if not, write to the Free Software |
18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ |
19 | |
20 | #include "kmahjongg.h" |
21 | |
22 | #include "prefs.h" |
23 | #include "kmahjongglayoutselector.h" |
24 | #include "ui_settings.h" |
25 | #include "Editor.h" |
26 | |
27 | #include <kmahjonggconfigdialog.h> |
28 | |
29 | #include <limits.h> |
30 | |
31 | #include <QPixmapCache> |
32 | #include <QLabel> |
33 | #include <QDesktopWidget> |
34 | |
35 | #include <KAboutData> |
36 | #include <KAction> |
37 | #include <KConfigDialog> |
38 | #include <KInputDialog> |
39 | #include <KMenuBar> |
40 | #include <KMessageBox> |
41 | #include <KStandardGameAction> |
42 | #include <KStandardAction> |
43 | #include <KIcon> |
44 | #include <KScoreDialog> |
45 | #include <KGameClock> |
46 | |
47 | #include <kio/netaccess.h> |
48 | #include <klocale.h> |
49 | #include <ktoggleaction.h> |
50 | #include <kactioncollection.h> |
51 | |
52 | |
53 | #define ID_STATUS_TILENUMBER 1 |
54 | #define ID_STATUS_MESSAGE 2 |
55 | #define ID_STATUS_GAME 3 |
56 | |
57 | |
58 | static const char *gameMagic = "kmahjongg-gamedata" ; |
59 | static int gameDataVersion = 1; |
60 | |
61 | int is_paused = 0; |
62 | |
63 | /** |
64 | * This class implements |
65 | * |
66 | * longer description |
67 | * |
68 | * @author Mauricio Piacentini <mauricio@tabuleiro.com> */ |
69 | class Settings : public QWidget, public Ui::Settings |
70 | { |
71 | public: |
72 | /** |
73 | * Constructor */ |
74 | Settings(QWidget *parent) |
75 | : QWidget(parent) |
76 | { |
77 | setupUi(this); |
78 | } |
79 | }; |
80 | |
81 | KMahjongg::KMahjongg(QWidget *parent) |
82 | : KXmlGuiWindow(parent) |
83 | { |
84 | //Use up to 3MB for global application pixmap cache |
85 | QPixmapCache::setCacheLimit(3 * 1024); |
86 | |
87 | // minimum area required to display the field |
88 | setMinimumSize(320, 320); |
89 | |
90 | // init board widget |
91 | bw = new BoardWidget(this); |
92 | setCentralWidget(bw); |
93 | |
94 | // Initialize boardEditor |
95 | boardEditor = new Editor(); |
96 | boardEditor->setModal(false); |
97 | |
98 | // Set the tileset setted in the the settings. |
99 | boardEditor->setTilesetFromSettings(); |
100 | |
101 | setupStatusBar(); |
102 | setupKAction(); |
103 | |
104 | gameTimer = new KGameClock(this); |
105 | |
106 | connect(gameTimer, SIGNAL(timeChanged(QString)), this, SLOT(displayTime(QString))); |
107 | |
108 | mFinished = false; |
109 | bDemoModeActive = false; |
110 | |
111 | connect(bw, SIGNAL(statusTextChanged(QString,long)), SLOT(showStatusText(QString,long))); |
112 | connect(bw, SIGNAL(tileNumberChanged(int,int,int)), SLOT(showTileNumber(int,int,int))); |
113 | connect(bw, SIGNAL(demoModeChanged(bool)), SLOT(demoModeChanged(bool))); |
114 | connect(bw, SIGNAL(gameOver(unsigned short,unsigned short)), this, |
115 | SLOT(gameOver(unsigned short,unsigned short))); |
116 | connect(bw, SIGNAL(gameCalculated()), this, SLOT(timerReset())); |
117 | |
118 | startNewGame(); |
119 | } |
120 | |
121 | KMahjongg::~KMahjongg() |
122 | { |
123 | delete bw; |
124 | delete boardEditor; |
125 | } |
126 | |
127 | void KMahjongg::setupKAction() |
128 | { |
129 | KStandardGameAction::gameNew(this, SLOT(newGame()), actionCollection()); |
130 | KStandardGameAction::load(this, SLOT(loadGame()), actionCollection()); |
131 | KStandardGameAction::save(this, SLOT(saveGame()), actionCollection()); |
132 | KStandardGameAction::quit(this, SLOT(close()), actionCollection()); |
133 | KStandardGameAction::restart(this, SLOT(restartGame()), actionCollection()); |
134 | |
135 | QAction *newNumGame = actionCollection()->addAction(QLatin1String("game_new_numeric" )); |
136 | newNumGame->setText(i18n("New Numbered Game..." )); |
137 | connect(newNumGame, SIGNAL(triggered(bool)), SLOT(startNewNumeric())); |
138 | |
139 | QAction *action = KStandardGameAction::hint(bw, SLOT(helpMove()), this); |
140 | actionCollection()->addAction(action->objectName(), action); |
141 | |
142 | QAction *shuffle = actionCollection()->addAction(QLatin1String("move_shuffle" )); |
143 | shuffle->setText(i18n("Shu&ffle" )); |
144 | shuffle->setIcon(KIcon(QLatin1String("view-refresh" ))); |
145 | connect(shuffle, SIGNAL(triggered(bool)), bw, SLOT(shuffle())); |
146 | |
147 | KAction *angleccw = actionCollection()->addAction(QLatin1String("view_angleccw" )); |
148 | angleccw->setText(i18n("Rotate View Counterclockwise" )); |
149 | angleccw->setIcon(KIcon(QLatin1String("object-rotate-left" ))); |
150 | angleccw->setShortcuts(KShortcut("f" )); |
151 | connect(angleccw, SIGNAL(triggered(bool)), bw, SLOT(angleSwitchCCW())); |
152 | |
153 | KAction *anglecw = actionCollection()->addAction(QLatin1String("view_anglecw" )); |
154 | anglecw->setText(i18n("Rotate View Clockwise" )); |
155 | anglecw->setIcon(KIcon(QLatin1String("object-rotate-right" ))); |
156 | anglecw->setShortcuts(KShortcut("g" )); |
157 | connect(anglecw, SIGNAL(triggered(bool)), bw, SLOT(angleSwitchCW())); |
158 | |
159 | demoAction = KStandardGameAction::demo(this, SLOT(demoMode()), actionCollection()); |
160 | |
161 | KStandardGameAction::highscores(this, SLOT(showHighscores()), actionCollection()); |
162 | pauseAction = KStandardGameAction::pause(this, SLOT(pause()), actionCollection()); |
163 | |
164 | // move |
165 | undoAction = KStandardGameAction::undo(this, SLOT(undo()), actionCollection()); |
166 | redoAction = KStandardGameAction::redo(this, SLOT(redo()), actionCollection()); |
167 | |
168 | // edit |
169 | QAction *boardEdit = actionCollection()->addAction(QLatin1String("game_board_editor" )); |
170 | boardEdit->setText(i18n("&Board Editor" )); |
171 | connect(boardEdit, SIGNAL(triggered(bool)), SLOT(slotBoardEditor())); |
172 | |
173 | // settings |
174 | KStandardAction::preferences(this, SLOT(showSettings()), actionCollection()); |
175 | |
176 | setupGUI(qApp->desktop()->availableGeometry().size() * 0.7); |
177 | } |
178 | |
179 | void KMahjongg::setupStatusBar() |
180 | { |
181 | gameTimerLabel = new QLabel(i18n("Time: 0:00:00" ), statusBar()); |
182 | statusBar()->addWidget(gameTimerLabel); |
183 | |
184 | QFrame *timerDivider = new QFrame(statusBar()); |
185 | timerDivider->setFrameStyle(QFrame::VLine); |
186 | statusBar()->addWidget(timerDivider); |
187 | |
188 | tilesLeftLabel = new QLabel(i18n("Removed: 0000/0000" ), statusBar()); |
189 | statusBar()->addWidget(tilesLeftLabel, 1); |
190 | |
191 | QFrame *tileDivider = new QFrame(statusBar()); |
192 | tileDivider->setFrameStyle(QFrame::VLine); |
193 | statusBar()->addWidget(tileDivider); |
194 | |
195 | gameNumLabel = new QLabel(i18n("Game: 000000000000000000000" ), statusBar()); |
196 | statusBar()->addWidget(gameNumLabel); |
197 | |
198 | QFrame * = new QFrame(statusBar()); |
199 | gameNumDivider->setFrameStyle(QFrame::VLine); |
200 | statusBar()->addWidget(gameNumDivider); |
201 | |
202 | statusLabel = new QLabel("Kmahjongg" , statusBar()); |
203 | statusBar()->addWidget(statusLabel); |
204 | } |
205 | |
206 | void KMahjongg::displayTime(const QString& timestring) |
207 | { |
208 | gameTimerLabel->setText(i18n("Time: " ) + timestring); |
209 | } |
210 | |
211 | void KMahjongg::setDisplayedWidth() |
212 | { |
213 | bw->setDisplayedWidth(); |
214 | bw->drawBoard(); |
215 | } |
216 | |
217 | void KMahjongg::startNewNumeric() |
218 | { |
219 | bool ok; |
220 | int s = KInputDialog::getInteger(i18n("New Game" ), i18n("Enter game number:" ), 0, 0, INT_MAX, 1, |
221 | &ok, this); |
222 | |
223 | if (ok) { |
224 | startNewGame(s); |
225 | } |
226 | } |
227 | |
228 | void KMahjongg::undo() |
229 | { |
230 | bw->Game->allow_redo += bw->undoMove(); |
231 | demoModeChanged(false); |
232 | } |
233 | |
234 | void KMahjongg::redo() |
235 | { |
236 | if (bw->Game->allow_redo > 0) { |
237 | bw->Game->allow_redo--; |
238 | bw->redoMove(); |
239 | demoModeChanged(false); |
240 | } |
241 | } |
242 | |
243 | void KMahjongg::showSettings() |
244 | { |
245 | if (KConfigDialog::showDialog("settings" )) { |
246 | return; |
247 | } |
248 | |
249 | //Use the classes exposed by LibKmahjongg for our configuration dialog |
250 | KMahjonggConfigDialog *dialog = new KMahjonggConfigDialog(this, "settings" , Prefs::self()); |
251 | |
252 | //The Settings class is ours |
253 | dialog->addPage(new Settings(0), i18n("General" ), "games-config-options" ); |
254 | dialog->addPage(new KMahjonggLayoutSelector(0, Prefs::self()), i18n("Board Layout" ), "games-con" |
255 | "fig-board" ); |
256 | dialog->addTilesetPage(); |
257 | dialog->addBackgroundPage(); |
258 | dialog->setHelp(QString(),"kmahjongg" ); |
259 | |
260 | connect(dialog, SIGNAL(settingsChanged(QString)), bw, SLOT(loadSettings())); |
261 | connect(dialog, SIGNAL(settingsChanged(QString)), boardEditor, SLOT(setTilesetFromSettings())); |
262 | connect(dialog, SIGNAL(settingsChanged(QString)), this, SLOT(setDisplayedWidth())); |
263 | |
264 | dialog->show(); |
265 | } |
266 | |
267 | void KMahjongg::demoMode() |
268 | { |
269 | if (bDemoModeActive) { |
270 | bw->stopDemoMode(); |
271 | } else { |
272 | // we assume demo mode removes tiles so we can |
273 | // disable redo here. |
274 | bw->Game->allow_redo = false; |
275 | bw->startDemoMode(); |
276 | } |
277 | |
278 | } |
279 | |
280 | void KMahjongg::pause() |
281 | { |
282 | if (is_paused) { |
283 | gameTimer->resume(); |
284 | } else { |
285 | gameTimer->pause(); |
286 | } |
287 | |
288 | is_paused = !is_paused; |
289 | demoModeChanged(false); |
290 | bw->pause(); |
291 | } |
292 | |
293 | void KMahjongg::showHighscores() |
294 | { |
295 | KScoreDialog ksdialog(KScoreDialog::Name | KScoreDialog::Time, this); |
296 | ksdialog.setConfigGroup(bw->getLayoutName()); |
297 | ksdialog.exec(); |
298 | } |
299 | |
300 | void KMahjongg::slotBoardEditor() |
301 | { |
302 | boardEditor->setVisible(true); |
303 | |
304 | // Set the default size. |
305 | boardEditor->setGeometry(Prefs::editorGeometry()); |
306 | } |
307 | |
308 | void KMahjongg::newGame() |
309 | { |
310 | startNewGame(); |
311 | } |
312 | |
313 | void KMahjongg::startNewGame(int item) |
314 | { |
315 | if (!bDemoModeActive) { |
316 | bw->calculateNewGame(item); |
317 | |
318 | // initialise button states |
319 | bw->Game->allow_redo = bw->Game->allow_undo = 0; |
320 | |
321 | timerReset(); |
322 | |
323 | // update the initial enabled/disabled state for |
324 | // the menu and the tool bar. |
325 | mFinished = false; |
326 | demoModeChanged(false); |
327 | } |
328 | } |
329 | |
330 | void KMahjongg::timerReset() |
331 | { |
332 | // initialise the scoring system |
333 | gameElapsedTime = 0; |
334 | |
335 | // start the game timer |
336 | gameTimer->restart(); |
337 | } |
338 | |
339 | void KMahjongg::changeEvent(QEvent *event) |
340 | { |
341 | if (event->type() == QEvent::WindowStateChange) { |
342 | QWindowStateChangeEvent *stateEvent = (QWindowStateChangeEvent *) event; |
343 | |
344 | if ((isMinimized() && stateEvent->oldState() != Qt::WindowMinimized) |
345 | || (!isMinimized() && stateEvent->oldState() == Qt::WindowMinimized)) { |
346 | pause(); |
347 | } |
348 | } |
349 | } |
350 | |
351 | void KMahjongg::gameOver(unsigned short numRemoved, unsigned short cheats) |
352 | { |
353 | int time; |
354 | int score; |
355 | |
356 | gameTimer->pause(); |
357 | long = bw->getGameNum(); |
358 | |
359 | KMessageBox::information(this, i18n("You have won!" )); |
360 | |
361 | mFinished = true; |
362 | demoModeChanged(false); |
363 | |
364 | int elapsed = gameTimer->seconds(); |
365 | |
366 | time = score = 0; |
367 | |
368 | // get the time in milli secs |
369 | // subtract from 20 minutes to get bonus. if longer than 20 then ignore |
370 | time = (60 * 20) - gameTimer->seconds(); |
371 | if (time < 0) { |
372 | time =0; |
373 | } |
374 | // conv back to secs (max bonus = 60*20 = 1200 |
375 | |
376 | // points per removed tile bonus (for deragon max = 144*10 = 1440 |
377 | score += (numRemoved * 20); |
378 | // time bonus one point per second under one hour |
379 | score += time; |
380 | // points per cheat penalty (max penalty = 1440 for dragon) |
381 | score -= (cheats * 20); |
382 | if (score < 0) { |
383 | score = 0; |
384 | } |
385 | |
386 | //TODO: add gameNum as a Custom KScoreDialog field? |
387 | // theHighScores->checkHighScore(score, elapsed, gameNum, bw->getBoardName()); |
388 | KScoreDialog ksdialog(KScoreDialog::Name | KScoreDialog::Time, this); |
389 | ksdialog.setConfigGroup(bw->getLayoutName()); |
390 | KScoreDialog::FieldInfo scoreInfo; |
391 | scoreInfo[KScoreDialog::Score].setNum(score); |
392 | scoreInfo[KScoreDialog::Time] = gameTimer->timeString(); |
393 | if(ksdialog.addScore(scoreInfo, KScoreDialog::AskName)) { |
394 | ksdialog.exec(); |
395 | } |
396 | |
397 | bw->animateMoveList(); |
398 | |
399 | timerReset(); |
400 | } |
401 | |
402 | |
403 | void KMahjongg::showStatusText(const QString &msg, long board) |
404 | { |
405 | statusLabel->setText(msg); |
406 | QString str = i18n("Game number: %1" , board); |
407 | gameNumLabel->setText(str); |
408 | } |
409 | |
410 | void KMahjongg::showTileNumber(int iMaximum, int iCurrent, int iLeft) |
411 | { |
412 | // Hmm... seems iCurrent is the number of remaining tiles, not removed ... |
413 | //QString szBuffer = i18n("Removed: %1/%2").arg(iCurrent).arg(iMaximum); |
414 | QString szBuffer = i18n("Removed: %1/%2 Combinations left: %3" , iMaximum-iCurrent, iMaximum, |
415 | iLeft); |
416 | tilesLeftLabel->setText(szBuffer); |
417 | |
418 | // Update here since undo allow is effected by demo mode |
419 | // removal. However we only change the enabled state of the |
420 | // items when not in demo mode |
421 | bw->Game->allow_undo = iMaximum != iCurrent; |
422 | |
423 | // update undo menu item, if demomode is inactive |
424 | if (!bDemoModeActive && !is_paused && !mFinished) { |
425 | undoAction->setEnabled(bw->Game->allow_undo); |
426 | } |
427 | } |
428 | |
429 | void KMahjongg::demoModeChanged(bool bActive) |
430 | { |
431 | bDemoModeActive = bActive; |
432 | |
433 | pauseAction->setChecked(is_paused); |
434 | demoAction->setChecked(bActive || is_paused); |
435 | |
436 | if (is_paused) { |
437 | stateChanged("paused" ); |
438 | } else if (mFinished) { |
439 | stateChanged("finished" ); |
440 | } else if (bActive) { |
441 | stateChanged("active" ); |
442 | } else { |
443 | stateChanged("inactive" ); |
444 | undoAction->setEnabled(bw->Game->allow_undo); |
445 | redoAction->setEnabled(bw->Game->allow_redo); |
446 | } |
447 | } |
448 | |
449 | void KMahjongg::restartGame() |
450 | { |
451 | if (!bDemoModeActive) { |
452 | bw->calculateNewGame(bw->getGameNum()); |
453 | |
454 | // initialise button states |
455 | bw->Game->allow_redo = bw->Game->allow_undo = 0; |
456 | |
457 | timerReset(); |
458 | |
459 | // update the initial enabled/disabled state for |
460 | // the menu and the tool bar. |
461 | mFinished = false; |
462 | demoModeChanged(false); |
463 | |
464 | if (is_paused) { |
465 | pauseAction->setChecked(false); |
466 | is_paused = false; |
467 | bw->pause(); |
468 | } |
469 | } |
470 | } |
471 | |
472 | void KMahjongg::loadGame() |
473 | { |
474 | QString fname; |
475 | |
476 | // Get the name of the file to load |
477 | KUrl url = KFileDialog::getOpenUrl(KUrl(), "*.kmgame" , this, i18n("Load Game" )); |
478 | |
479 | if (url.isEmpty()) { |
480 | return; |
481 | } |
482 | |
483 | KIO::NetAccess::download(url, fname, this); |
484 | |
485 | // open the file for reading |
486 | QFile infile(fname); |
487 | |
488 | if (!infile.open(QIODevice::ReadOnly)) { |
489 | KMessageBox::sorry(this, i18n("Could not read from file. Aborting." )); |
490 | return; |
491 | } |
492 | |
493 | QDataStream in(&infile); |
494 | |
495 | // verify the magic |
496 | QString magic; |
497 | in >> magic; |
498 | |
499 | if (QString::compare(magic, gameMagic, Qt::CaseSensitive) != 0) { |
500 | KMessageBox::sorry(this, i18n("File is not a KMahjongg game." )); |
501 | infile.close(); |
502 | |
503 | return; |
504 | } |
505 | |
506 | // Read the version |
507 | qint32 version; |
508 | in >> version; |
509 | |
510 | if (version == gameDataVersion) { |
511 | in.setVersion(QDataStream::Qt_4_0); |
512 | } else { |
513 | KMessageBox::sorry(this, i18n("File format not recognized." )); |
514 | infile.close(); |
515 | |
516 | return; |
517 | } |
518 | |
519 | QString theTilesName; |
520 | QString theBackgroundName; |
521 | QString theBoardLayoutName; |
522 | in >> theTilesName; |
523 | bw->loadTileset(theTilesName); |
524 | in >> theBackgroundName; |
525 | bw->loadBackground(theBackgroundName, false); |
526 | in >> theBoardLayoutName; |
527 | |
528 | //GameTime |
529 | uint seconds; |
530 | in >> seconds; |
531 | gameTimer->setTime(seconds); |
532 | |
533 | delete bw->Game; |
534 | bw->loadBoardLayout(theBoardLayoutName); |
535 | bw->Game = new GameData(bw->theBoardLayout.board()); |
536 | bw->Game->loadFromStream(in); |
537 | |
538 | infile.close(); |
539 | |
540 | KIO::NetAccess::removeTempFile(fname); |
541 | |
542 | // refresh the board |
543 | bw->gameLoaded(); |
544 | |
545 | mFinished = false; |
546 | demoModeChanged(false); |
547 | } |
548 | |
549 | void KMahjongg::saveGame() |
550 | { |
551 | //Pause timer |
552 | gameTimer->pause(); |
553 | |
554 | // Get the name of the file to save |
555 | KUrl url = KFileDialog::getSaveUrl(KUrl(), "*.kmgame" , this, i18n("Save Game" )); |
556 | |
557 | if (url.isEmpty()) { |
558 | gameTimer->resume(); |
559 | |
560 | return; |
561 | } |
562 | |
563 | if (!url.isLocalFile()) { |
564 | KMessageBox::sorry(this, i18n("Only saving to local files currently supported." )); |
565 | gameTimer->resume(); |
566 | |
567 | return; |
568 | } |
569 | |
570 | QFile outfile(url.path()); |
571 | |
572 | if (!outfile.open(QIODevice::WriteOnly)) { |
573 | KMessageBox::sorry(this, i18n("Could not write saved game." )); |
574 | gameTimer->resume(); |
575 | |
576 | return; |
577 | } |
578 | |
579 | QDataStream out(&outfile); |
580 | |
581 | // Write a header with a "magic number" and a version |
582 | out << QString(gameMagic); |
583 | out << (qint32) gameDataVersion; |
584 | out.setVersion(QDataStream::Qt_4_0); |
585 | |
586 | out << bw->theTiles.path(); |
587 | out << bw->theBackground.path(); |
588 | out << bw->theBoardLayout.path(); |
589 | |
590 | //GameTime |
591 | out << gameTimer->seconds(); |
592 | // Write the Game data |
593 | bw->Game->saveToStream(out); |
594 | |
595 | outfile.close(); |
596 | gameTimer->resume(); |
597 | } |
598 | |
599 | |
600 | #include "kmahjongg.moc" |
601 | |