1//
2// KBlackBox
3//
4// A simple game inspired by an emacs module
5//
6/***************************************************************************
7 * Copyright (c) 1999-2000, Robert Cimrman *
8 * cimrman3@students.zcu.cz *
9 * *
10 * Copyright (c) 2007, Nicolas Roffet *
11 * nicolas-kde@roffet.com *
12 * *
13 * *
14 * This program is free software; you can redistribute it and/or modify *
15 * it under the terms of the GNU General Public License as published by *
16 * the Free Software Foundation; either version 2 of the License, or *
17 * (at your option) any later version. *
18 * *
19 * This program is distributed in the hope that it will be useful, *
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
22 * GNU General Public License for more details. *
23 * *
24 * You should have received a copy of the GNU General Public License *
25 * along with this program; if not, write to the *
26 * Free Software Foundation, Inc., *
27 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA *
28 ***************************************************************************/
29
30#include "kbbmainwindow.h"
31
32
33
34#include <QFile>
35#include <QHBoxLayout>
36#include <QWidget>
37
38
39#include <KActionCollection>
40#include <KConfigDialog>
41#include <KGameClock>
42#include <KGamePopupItem>
43#include <KGlobal>
44#include <KLocale>
45#include <KMessageBox>
46#include <KScoreDialog>
47#include <KStandardDirs>
48#include <KStandardGameAction>
49#include <KStatusBar>
50#include <KToggleAction>
51
52
53#include "kbbgamedoc.h"
54#include "kbbgraphicsitemtutorialmarker.h"
55#include "kbblevelconfigurationwidget.h"
56#include "kbbprefs.h"
57#include "kbbscalablegraphicwidget.h"
58#include "kbbthememanager.h"
59#include "kbbtutorial.h"
60
61
62
63//
64// Constructor / Destructor
65//
66
67KBBMainWindow::KBBMainWindow()
68{
69 m_boardEnabled = false;
70
71 //Read configuration options
72 m_customBallNumber = KBBPrefs::balls();
73 m_customColumns = KBBPrefs::columns();
74 m_customRows = KBBPrefs::rows();
75
76
77 // Status bar
78 statusBar()->insertPermanentItem("", SRUN, 1);
79 statusBar()->insertPermanentItem(i18n("Time: 00:00"), STIME, 1);
80 statusBar()->insertPermanentItem(i18n("Size: 00 x 00"), SSIZE);
81 statusBar()->setItemAlignment(SRUN, Qt::AlignLeft | Qt::AlignVCenter);
82
83
84 // Difficulty
85 Kg::difficulty()->addStandardLevelRange(
86 KgDifficultyLevel::VeryEasy, KgDifficultyLevel::ExtremelyHard,
87 KgDifficultyLevel::Medium //default
88 );
89 Kg::difficulty()->addLevel(new KgDifficultyLevel(
90 1000, QByteArray("config"), i18nc("Difficulty level title", "Configurable")
91 ));
92 KgDifficultyGUI::init(this);
93 connect(Kg::difficulty(), SIGNAL(currentLevelChanged(const KgDifficultyLevel*)), SLOT(levelChanged()));
94
95
96 // Menu "Game"
97 KStandardGameAction::gameNew(this, SLOT(newGame()), actionCollection());
98 m_pauseAction = KStandardGameAction::pause(this, SLOT(pause(bool)), actionCollection());
99 QAction* tutorial = actionCollection()->addAction( QLatin1String( "game_tutorial" ));
100 tutorial->setText(i18n("Start Tutorial"));
101 tutorial->setIcon(KIcon( QLatin1String( "footprint" )));
102 tutorial->setToolTip(i18n("Start tutorial"));
103 tutorial->setWhatsThis(i18n("<qt>The <b>tutorial</b> is a fast, user friendly and interactive way to learn the rules of the game. Start it if you do not know them!</qt>"));
104 connect(tutorial, SIGNAL(triggered(bool)), SLOT(startTutorial()));
105 KStandardGameAction::quit(this, SLOT(close()), actionCollection());
106 QAction* sandbox = actionCollection()->addAction( QLatin1String( "game_sandbox" ));
107 sandbox->setText(i18n("New Sandbox Game"));
108 sandbox->setToolTip(i18n("Start a new sandbox game"));
109 sandbox->setWhatsThis(i18n("<qt><p>In a <b>sandbox game</b>, the solution is displayed at the beginning of the game. This is useful to understand the game principles.</p><p>However: after a while, it is not really fun and you should try to start a real game!</p></qt>"));
110 connect(sandbox, SIGNAL(triggered(bool)), SLOT(startSandbox()));
111 KStandardGameAction::highscores(this, SLOT(showHighscores()), actionCollection());
112
113 // Menu "Move"
114 m_doneAction = actionCollection()->addAction( QLatin1String( "move_done" ));
115 m_doneAction->setText(i18nc("This is the last action of a game to check the result, when the user is done.", "Done!"));
116 m_doneAction->setWhatsThis(i18n("<qt><ul><li>First, you have to place all the balls on the black box. To guess the correct positions of the balls and see how they interact with laser beams, you should use the lasers that are positioned around the black box.</li><li><b>When you think you are done</b>, you should click here.</li></ul><p>Note that it is only possible to click here if you have placed the correct number of balls.</p></qt>"));
117 m_doneAction->setIcon(KIcon( QLatin1String( "dialog-ok" )));
118 connect(m_doneAction, SIGNAL(triggered(bool)), SLOT(done()));
119 m_solveAction = KStandardGameAction::solve(this, SLOT(solve()), actionCollection());
120 m_solveAction->setToolTip(i18n("Give up the game"));
121 m_solveAction->setWhatsThis(i18n("<qt><p>Choose \"<b>Solve</b>\" if you want to give up the current game. The solution will be displayed.</p><p>If you placed all the balls and do not want to give up, choose \"Done!\".</p></qt>"));
122
123 // Menu "Settings"
124 KStandardAction::preferences(this, SLOT(settingsDialog()), actionCollection());
125
126
127 // Theme manager
128 QString svgzFile = KBBPrefs::theme();
129 if (!QFile(svgzFile).exists())
130 svgzFile = KStandardDirs::locate("appdata", "pics/kblackbox.svgz");
131 m_themeManager = new KBBThemeManager(svgzFile);
132
133
134 // Tutorial widget
135 m_tutorial = new KBBTutorial(this);
136
137
138 // Board
139 m_gameDoc = new KBBGameDoc(this, m_tutorial);
140 connect(m_gameDoc, SIGNAL(updateStats()), this, SLOT(updateStats()) );
141 connect(m_gameDoc, SIGNAL(isRunning(bool)), SLOT(setRunning(bool)));
142
143
144 // Game widget
145 m_gameWidget = new KBBScalableGraphicWidget(m_gameDoc, m_themeManager, m_doneAction);
146 m_tutorial->setGameWidget(m_gameWidget, new KBBGraphicsItemTutorialMarker(m_gameWidget, m_themeManager, KBBTutorial::COLUMNS, KBBTutorial::ROWS));
147
148
149 // Central Widget
150 m_centralWidget = new QWidget(this);
151 QHBoxLayout *widgetLayout = new QHBoxLayout();
152 widgetLayout->setMargin(0);
153 m_centralWidget->setLayout(widgetLayout);
154 widgetLayout->addWidget(m_gameWidget);
155 widgetLayout->addWidget(m_tutorial);
156 setCentralWidget(m_centralWidget);
157
158
159 // Keyboard only
160 KAction* action = actionCollection()->addAction( QLatin1String( "move_down" ) );
161 action->setText( i18n("Move Down") );
162 connect(action, SIGNAL(triggered(bool)), m_gameWidget, SLOT(keyboardMoveDown()));
163 action->setShortcut(Qt::Key_Down);
164 addAction(action);
165
166 action = actionCollection()->addAction( QLatin1String( "move_up" ) );
167 action->setText( i18n("Move Up") );
168 connect(action, SIGNAL(triggered(bool)), m_gameWidget, SLOT(keyboardMoveUp()));
169 action->setShortcut(Qt::Key_Up);
170 addAction(action);
171
172 action = actionCollection()->addAction( QLatin1String( "move_left" ) );
173 action->setText( i18n("Move Left") );
174 connect(action, SIGNAL(triggered(bool)), m_gameWidget, SLOT(keyboardMoveLeft()));
175 action->setShortcut(Qt::Key_Left);
176 addAction(action);
177
178 action = actionCollection()->addAction( QLatin1String( "move_right" ) );
179 action->setText( i18n("Move Right") );
180 connect(action, SIGNAL(triggered(bool)), m_gameWidget, SLOT(keyboardMoveRight()));
181 action->setShortcut(Qt::Key_Right);
182 addAction(action);
183
184 action = actionCollection()->addAction( QLatin1String( "switch_ball" ));
185 action->setText(i18n("Switch Ball or Shoot Laser"));
186 connect(action, SIGNAL(triggered(bool)), m_gameWidget, SLOT(keyboardEnter()));
187 action->setShortcut(Qt::Key_Return);
188 addAction(action);
189
190 action = actionCollection()->addAction( QLatin1String( "switch_marker" ));
191 action->setText(i18n("Switch Marker"));
192 connect(action, SIGNAL(triggered(bool)), m_gameWidget, SLOT(keyboardSpace()));
193 action->setShortcut(Qt::Key_Space);
194 addAction(action);
195
196
197 m_gameClock = new KGameClock(this, KGameClock::MinSecOnly);
198 connect(m_gameClock, SIGNAL(timeChanged(QString)), SLOT(updateStats()));
199 connect(m_gameClock, SIGNAL(timeChanged(QString)), m_gameDoc, SLOT(timeChanged()));
200
201
202 levelChanged();
203
204 setupGUI();
205
206 // start a new game
207 startGame(false);
208}
209
210
211KBBMainWindow::~KBBMainWindow()
212{
213 KBBPrefs::self()->writeConfig();
214
215 delete m_gameWidget;
216 delete m_themeManager;
217}
218
219
220
221//
222// Public slots
223//
224
225void KBBMainWindow::levelChanged()
226{
227 KgDifficultyLevel::StandardLevel level = Kg::difficultyLevel();
228 switch(level) {
229 case KgDifficultyLevel::VeryEasy:
230 m_ballNumber = 2;
231 m_columns = 6;
232 m_rows = 6;
233 break;
234 case KgDifficultyLevel::Easy:
235 default:
236 m_ballNumber = 4;
237 m_columns = 8;
238 m_rows = 8;
239 level = KgDifficultyLevel::Medium;
240 break;
241 case KgDifficultyLevel::Medium:
242 m_ballNumber = 6;
243 m_columns = 10;
244 m_rows = 10;
245 break;
246 case KgDifficultyLevel::Hard:
247 m_ballNumber = 8;
248 m_columns = 12;
249 m_rows = 12;
250 break;
251 case KgDifficultyLevel::VeryHard:
252 m_ballNumber = 11;
253 m_columns = 14;
254 m_rows = 10;
255 break;
256 case KgDifficultyLevel::ExtremelyHard:
257 m_ballNumber = 15;
258 m_columns = 20;
259 m_rows = 12;
260 break;
261 case KgDifficultyLevel::Custom:
262 m_gameWidget->popupText(i18nc("The text may not be too wide. So please use some HTML-BR-tags to have something more or less as wide as in english. Thanks!", "Note: You can change<br />the parameters of<br />custom games in the<br />Settings dialog."));
263 break;
264 }
265
266 m_level = level;
267 startGame(m_sandboxMode);
268}
269
270
271void KBBMainWindow::setRunning(bool r)
272{
273 // Difficulty
274 Kg::difficulty()->setGameRunning(r);
275
276 // Clock
277 if (r) {
278 m_gameClock->resume();
279 m_gameDoc->timeChanged(); // It's important to end the current seconde before pausing so that the player cannot cheat with pause.
280 } else
281 m_gameClock->pause();
282
283 // Pause
284 m_pauseAction->setEnabled(r);
285}
286
287
288void KBBMainWindow::updateStats()
289{
290 int ballsLeftToPlace = m_gameDoc->numberOfBallsToPlace() - m_gameDoc->numberOfBallsPlaced();
291
292 m_doneAction->setEnabled(m_solveAction->isEnabled() && (ballsLeftToPlace==0));
293
294 if (ballsLeftToPlace<0)
295 m_doneAction->setToolTip(i18np("First, you need to remove 1 ball from the black box.", "First, you need to remove %1 balls from the black box.", -ballsLeftToPlace));
296 else if (ballsLeftToPlace==0) {
297 m_doneAction->setToolTip(i18n("To check if you successfully guessed the ball positions, click here!"));
298 } else if (ballsLeftToPlace>0) {
299 m_doneAction->setToolTip(i18np("You need to place 1 more ball on the black box.", "You need to place %1 more balls on the black box.", ballsLeftToPlace));
300 }
301
302 if (!m_boardEnabled)
303 m_doneAction->setToolTip(i18n("Game over."));
304 if (m_pauseAction->isChecked())
305 m_doneAction->setToolTip(i18n("Game paused."));
306
307
308 // Status bar
309 if (m_tutorial->isVisible())
310 statusBar()->changeItem(i18n("Tutorial"), SRUN );
311 if (!m_tutorial->isVisible()) {
312 if (m_boardEnabled) {
313 if (ballsLeftToPlace<0) {
314 statusBar()->changeItem((i18np("1 ball too many!", "%1 balls too many!", -ballsLeftToPlace)), SRUN);
315 } else if (ballsLeftToPlace==0) {
316 statusBar()->changeItem(i18n("No more balls to place"), SRUN);
317 } else if (ballsLeftToPlace>0) {
318 statusBar()->changeItem(i18np("1 ball to place", "%1 balls to place", ballsLeftToPlace), SRUN);
319 }
320 } else
321 statusBar()->changeItem(i18n("Game over"), SRUN );
322 }
323
324 statusBar()->changeItem(i18n("Time: %1", m_gameClock->timeString()), STIME);
325
326 statusBar()->changeItem( i18n("Size: %1 x %2", m_gameDoc->columns(), m_gameDoc->rows()), SSIZE );
327
328
329 // 2. Info Widget
330 m_gameWidget->setScore(m_gameDoc->score());
331}
332
333
334
335//
336// Private slots
337//
338
339void KBBMainWindow::done()
340{
341 if (m_tutorial->isVisible() && !m_tutorial->maySolve()) {
342 KMessageBox::sorry(this, i18n("Clicking on \"Done!\" is the normal way to check the positions of the balls at the end of the game. However, it is not possible in the tutorial to end the game before you reached the last step.\nPlease first finish the tutorial."), i18n("Check positions"));
343 } else {
344 solving();
345
346 const int score = m_gameDoc->score();
347 QString s;
348 if (score <= (m_ballNumber*35)) {
349 s = i18nc("The text may not be too wide. So please use some HTML-BR-tags to have something more or less as wide as in english. Thanks!", "Your final score is: %1.<br />You did really well!", score);
350 if (m_sandboxMode)
351 s += QString("<br /><br />") + i18nc("The text may not be too wide. So please use some HTML-BR-tags to have something more or less as wide as in english. Thanks!", "But it does not count<br />because <b>it is the sandbox!</b>");
352 } else
353 s = i18nc("The text may not be too wide. So please use some HTML-BR-tags to have something more or less as wide as in english. Thanks!", "Your final score is: %1.<br />I guess you need more practice.", score);
354
355 if ((!m_tutorial->isVisible()) && (!m_sandboxMode) && (Kg::difficultyLevel() != KgDifficultyLevel::Custom) && (score<KBBGameDoc::SCORE_LOST)) {
356 KScoreDialog scoreDialog(KScoreDialog::Score | KScoreDialog::Name, this);
357 scoreDialog.initFromDifficulty(Kg::difficulty());
358
359 KScoreDialog::FieldInfo scoreInfo;
360 scoreInfo[KScoreDialog::Score].setNum(score);
361 if(scoreDialog.addScore(scoreInfo, KScoreDialog::LessIsMore) != 0)
362 scoreDialog.exec();
363 }
364
365 m_gameWidget->popupText(s);
366 }
367}
368
369
370void KBBMainWindow::newGame()
371{
372 if (mayAbortGame())
373 startGame(false);
374}
375
376
377void KBBMainWindow::pause(bool state)
378{
379 if (state) {
380 m_gameClock->pause();
381 m_gameWidget->popupText(i18n("Game paused.<br />Press \"%1\" to resume.", m_pauseAction->shortcut().toString(QKeySequence::NativeText)), 0);
382 } else {
383 m_gameClock->resume();
384 m_gameWidget->popupText("");
385 }
386 m_solveAction->setEnabled(!state);
387
388 updateStats();
389 m_gameWidget->setPause(state);
390}
391
392
393void KBBMainWindow::settingsChanged()
394{
395 m_customBallNumber = m_levelConfig->balls();
396 m_customColumns = m_levelConfig->columns();
397 m_customRows = m_levelConfig->rows();
398
399 if (m_level==KgDifficultyLevel::Custom) {
400 bool mayRestart = true;
401 if (m_gameDoc->gameReallyStarted())
402 if (KMessageBox::questionYesNo(this, i18n("Do you want to cancel the current custom game and start a new one with the new parameters?"), QString(), KGuiItem(i18n("Start new game"))) == KMessageBox::No)
403 mayRestart = false;
404
405 if (mayRestart)
406 startGame(m_sandboxMode);
407 }
408}
409
410
411void KBBMainWindow::settingsDialog()
412{
413 if (!KConfigDialog::showDialog("settings")) {
414 KConfigDialog *dialog = new KConfigDialog(this, "settings", KBBPrefs::self());
415 m_levelConfig = new KBBLevelConfigurationWidget(dialog, m_customBallNumber, m_customColumns, m_customRows, m_themeManager);
416 dialog->addPage(m_levelConfig, i18n("Custom Game"), "games-config-custom");
417 connect(dialog, SIGNAL(settingsChanged(QString)), this, SLOT(settingsChanged()));
418 dialog->setHelp(QString(), "kblackbox");
419 dialog->show();
420 }
421}
422
423
424void KBBMainWindow::showHighscores()
425{
426 KScoreDialog scoreDialog(KScoreDialog::Score | KScoreDialog::Name, this);
427 scoreDialog.initFromDifficulty(Kg::difficulty());
428 scoreDialog.exec();
429}
430
431
432void KBBMainWindow::solve()
433{
434 if (m_tutorial->isVisible() && !m_tutorial->maySolve()) {
435 KMessageBox::sorry(this, i18n("Sorry, you may not give up during the tutorial."), i18n("Solve"));
436 } else {
437 if (m_gameDoc->numberOfBallsToPlace()==m_gameDoc->numberOfBallsPlaced()) {
438 if (KMessageBox::warningContinueCancel(this, i18n("You placed all the balls. Great!\nYou should now click on \"Done!\" to end the game and check if you guessed correctly.\nSo, do you really want to give up this game?"), QString(), KGuiItem(i18n("Give up"))) == KMessageBox::Continue)
439 solving();
440 } else if (KMessageBox::warningContinueCancel(this, i18np("You should place %1 ball!\n", "You should place %1 balls!\n", m_gameDoc->numberOfBallsToPlace()) + i18np("You have placed %1.\n", "You have placed %1.\n", m_gameDoc->numberOfBallsPlaced()) + i18n("Do you really want to give up this game?"), QString(), KGuiItem(i18n("Give up"))) == KMessageBox::Continue)
441 solving();
442 }
443}
444
445
446void KBBMainWindow::startSandbox()
447{
448 if (mayAbortGame()) {
449 startGame(true);
450 m_gameWidget->popupText(i18nc("The text may not be too wide. So please use some HTML-BR-tags to have something more or less as wide as in english. Thanks!", "Note: In the sandbox mode,<br />the solution is already displayed.<br />Have fun!"));
451 }
452}
453
454
455void KBBMainWindow::startTutorial()
456{
457 if (mayAbortGame()) {
458 m_gameDoc->startTutorial();
459 m_solveAction->setEnabled(true);
460 m_pauseAction->setChecked(false);
461 Kg::difficulty()->setEditable(false);
462
463 // Reset clock but don't start it yet.
464 m_gameClock->restart();
465 m_gameClock->pause();
466
467 updateStats();
468 }
469}
470
471
472
473//
474// Private
475//
476
477bool KBBMainWindow::mayAbortGame()
478{
479 bool mayAbort = true;
480
481 if (m_gameDoc->gameReallyStarted())
482 mayAbort = ( KMessageBox::warningContinueCancel(0, i18n("This will be the end of the current game!"), QString(), KGuiItem(i18n("Start new game"))) == KMessageBox::Continue );
483
484 return mayAbort;
485}
486
487
488void KBBMainWindow::solving()
489{
490 m_boardEnabled = false;
491 m_solveAction->setEnabled(false);
492 m_doneAction->setEnabled(false);
493 m_gameDoc->gameOver();
494 m_gameWidget->solve(false);
495 updateStats();
496}
497
498
499void KBBMainWindow::startGame(bool sandboxMode)
500{
501 if (m_level==KgDifficultyLevel::Custom) {
502 m_ballNumber = m_customBallNumber;
503 m_columns = m_customColumns;
504 m_rows = m_customRows;
505 }
506
507 m_boardEnabled = true;
508 m_sandboxMode = sandboxMode;
509
510 m_solveAction->setEnabled(true);
511 m_pauseAction->setChecked(false);
512 Kg::difficulty()->setEditable(true);
513 m_tutorial->hide();
514 m_gameDoc->newGame(m_ballNumber, m_columns, m_rows);
515 m_gameWidget->newGame(m_columns, m_rows, m_ballNumber);
516 if (m_sandboxMode)
517 m_gameWidget->solve(true);
518
519 // Reset clock but don't start it yet.
520 m_gameClock->restart();
521 m_gameClock->pause();
522
523 updateStats();
524}
525
526
527
528#include "kbbmainwindow.moc"
529