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
58static const char *gameMagic = "kmahjongg-gamedata";
59static int gameDataVersion = 1;
60
61int is_paused = 0;
62
63/**
64 * This class implements
65 *
66 * longer description
67 *
68 * @author Mauricio Piacentini <mauricio@tabuleiro.com> */
69class Settings : public QWidget, public Ui::Settings
70{
71public:
72 /**
73 * Constructor */
74 Settings(QWidget *parent)
75 : QWidget(parent)
76 {
77 setupUi(this);
78 }
79};
80
81KMahjongg::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
121KMahjongg::~KMahjongg()
122{
123 delete bw;
124 delete boardEditor;
125}
126
127void 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
179void 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 *gameNumDivider = 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
206void KMahjongg::displayTime(const QString& timestring)
207{
208 gameTimerLabel->setText(i18n("Time: ") + timestring);
209}
210
211void KMahjongg::setDisplayedWidth()
212{
213 bw->setDisplayedWidth();
214 bw->drawBoard();
215}
216
217void 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
228void KMahjongg::undo()
229{
230 bw->Game->allow_redo += bw->undoMove();
231 demoModeChanged(false);
232}
233
234void KMahjongg::redo()
235{
236 if (bw->Game->allow_redo > 0) {
237 bw->Game->allow_redo--;
238 bw->redoMove();
239 demoModeChanged(false);
240 }
241}
242
243void 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
267void 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
280void 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
293void KMahjongg::showHighscores()
294{
295 KScoreDialog ksdialog(KScoreDialog::Name | KScoreDialog::Time, this);
296 ksdialog.setConfigGroup(bw->getLayoutName());
297 ksdialog.exec();
298}
299
300void KMahjongg::slotBoardEditor()
301{
302 boardEditor->setVisible(true);
303
304 // Set the default size.
305 boardEditor->setGeometry(Prefs::editorGeometry());
306}
307
308void KMahjongg::newGame()
309{
310 startNewGame();
311}
312
313void 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
330void KMahjongg::timerReset()
331{
332 // initialise the scoring system
333 gameElapsedTime = 0;
334
335 // start the game timer
336 gameTimer->restart();
337}
338
339void 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
351void KMahjongg::gameOver(unsigned short numRemoved, unsigned short cheats)
352{
353 int time;
354 int score;
355
356 gameTimer->pause();
357 long gameNum = 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
403void 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
410void 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
429void 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
449void 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
472void 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
549void 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