1 | /* Copyright (C) 1997 Mathias Mueller <in5y158@public.uni-hamburg.de> |
2 | * Copyright (C) 2006 Mauricio Piacentini <mauricio@tabuleiro.com> |
3 | * |
4 | * Kmahjongg is free software; you can redistribute it and/or modify |
5 | * it under the terms of the GNU General Public License as published by |
6 | * the Free Software Foundation; either version 2 of the License, or |
7 | * (at your option) any later version. |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License |
15 | * along with this program; if not, write to the Free Software |
16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ |
17 | |
18 | #include "boardwidget.h" |
19 | #include "prefs.h" |
20 | |
21 | #include <QTimer> |
22 | #include <qpainter.h> |
23 | #include <qapplication.h> |
24 | |
25 | #include <klocale.h> |
26 | #include <kstandarddirs.h> |
27 | #include <kmessagebox.h> |
28 | #include <krandom.h> |
29 | #include <kconfig.h> |
30 | #include <kglobal.h> |
31 | #include <KDebug> |
32 | |
33 | |
34 | BoardWidget::BoardWidget(QWidget *parent) |
35 | : KGameCanvasWidget(parent), |
36 | theTiles() |
37 | { |
38 | QPalette palette; |
39 | palette.setColor(backgroundRole(), Qt::black); |
40 | setPalette(palette); |
41 | |
42 | timer = new QTimer(this); |
43 | connect(timer, SIGNAL(timeout()), this, SLOT(helpMoveTimeout())); |
44 | |
45 | TimerState = Stop; |
46 | gamePaused = false; |
47 | iTimerStep = 0; |
48 | matchCount = 0; |
49 | showMatch = false; |
50 | showHelp = false; |
51 | |
52 | //memset( &Game->Mask, 0, sizeof( Game->Mask ) ); |
53 | gameGenerationNum = 0; |
54 | m_angle = (TileViewAngle) Prefs::angle(); |
55 | |
56 | // Load layout first |
57 | loadBoardLayout(Prefs::layout()); |
58 | |
59 | //Initialize our Game structure |
60 | Game = new GameData(theBoardLayout.board()); |
61 | |
62 | MouseClickPos1.e = Game->m_depth; // mark tile position as invalid |
63 | MouseClickPos2.e = Game->m_depth; |
64 | |
65 | //Animation timers |
66 | animateForwardTimer = new QTimer(this); |
67 | animateForwardTimer->setSingleShot(true); |
68 | animateForwardTimer->setInterval(100); |
69 | connect(animateForwardTimer, SIGNAL(timeout()), SLOT(animatingMoveListForward())); |
70 | animateForwardTimer->stop(); |
71 | |
72 | animateBackwardsTimer = new QTimer(this); |
73 | animateBackwardsTimer->setSingleShot(true); |
74 | animateBackwardsTimer->setInterval(100); |
75 | connect(animateBackwardsTimer, SIGNAL(timeout()), SLOT(animatingMoveListBackwards())); |
76 | animateBackwardsTimer->stop(); |
77 | |
78 | loadSettings(); |
79 | } |
80 | |
81 | BoardWidget::~BoardWidget() |
82 | { |
83 | delete Game; |
84 | } |
85 | |
86 | void BoardWidget::loadSettings() |
87 | { |
88 | // Load tileset. First try to load the last use tileset |
89 | |
90 | if (!loadTileset(Prefs::tileSet())) { |
91 | kDebug() << "An error occurred when loading the tileset" << Prefs::tileSet() << "KMahjongg " |
92 | "will continue with the default tileset." ; |
93 | } |
94 | |
95 | // Load background |
96 | if (!loadBackground(Prefs::background(), false)) { |
97 | kDebug() << "An error occurred when loading the background" << Prefs::background() << "KMah" |
98 | "jongg will continue with the default background." ; |
99 | } |
100 | |
101 | setShowMatch(Prefs::showMatchingTiles()); |
102 | |
103 | // If the random layout is activated, we won't load a boardlayout, as it will be created |
104 | // randomly in calculateNewGame(). |
105 | if (!Prefs::randomLayout()) { |
106 | if (QString::compare(Prefs::layout(), theBoardLayout.path(), Qt::CaseSensitive) !=0 ) { |
107 | //TODO: WARN USER HERE ABOUT DESTRUCTIVE OPERATION!!! |
108 | loadBoardLayout(Prefs::layout()); |
109 | calculateNewGame(); |
110 | } |
111 | } else { |
112 | calculateNewGame(); |
113 | } |
114 | |
115 | setDisplayedWidth(); |
116 | drawBoard(true); |
117 | |
118 | //Store our updated settings, some values might have been changed to defaults |
119 | saveSettings(); |
120 | } |
121 | |
122 | void BoardWidget::resizeEvent(QResizeEvent *event) |
123 | { |
124 | if (event->spontaneous()) { |
125 | return; |
126 | } |
127 | |
128 | resizeTileset(event->size()); |
129 | theBackground.sizeChanged(requiredWidth(), requiredHeight()); |
130 | drawBoard(true); |
131 | } |
132 | |
133 | void BoardWidget::resizeTileset(const QSize &wsize) |
134 | { |
135 | QSize newtiles = theTiles.preferredTileSize(wsize, requiredHorizontalCells(), |
136 | requiredVerticalCells()); |
137 | |
138 | theTiles.reloadTileset(newtiles); |
139 | stopMatchAnimation(); |
140 | } |
141 | |
142 | void BoardWidget::saveSettings() |
143 | { |
144 | Prefs::setTileSet(theTiles.path()); |
145 | Prefs::setLayout(theBoardLayout.path()); |
146 | Prefs::setBackground(theBackground.path()); |
147 | Prefs::setAngle(m_angle); |
148 | Prefs::self()->writeConfig(); |
149 | } |
150 | |
151 | void BoardWidget::setDisplayedWidth() |
152 | { |
153 | //TODO remove method and let the layout handle it |
154 | //for now we just force our resizeEvent() to be called |
155 | resize(width(), height()); |
156 | } |
157 | |
158 | void BoardWidget::populateSpriteMap() |
159 | { |
160 | //Delete previous sprites (full update), synchronize state with GameData |
161 | while (!items()->isEmpty()) { |
162 | delete items()->first(); |
163 | } |
164 | |
165 | //Clear our spritemap as well |
166 | spriteMap.clear(); |
167 | |
168 | //Recreate our background |
169 | QPalette palette; |
170 | palette.setBrush(backgroundRole(), theBackground.getBackground()); |
171 | setPalette(palette); |
172 | setAutoFillBackground(true); |
173 | |
174 | //create the sprites |
175 | for (int z = 0; z < Game->m_depth; z++) { |
176 | // we draw down the board so the tile below over rights our border |
177 | for (int y = 0; y < Game->m_height; y++) { |
178 | // drawing right to left to prevent border overwrite |
179 | for (int x = Game->m_width - 1; x >= 0; x--) { |
180 | |
181 | // skip if no tile to display |
182 | if (!Game->tilePresent(z, y, x)) { |
183 | continue; |
184 | } |
185 | |
186 | QPixmap s; |
187 | QPixmap us; |
188 | QPixmap f; |
189 | bool selected = false; |
190 | s = theTiles.selectedTile(m_angle); |
191 | us = theTiles.unselectedTile(m_angle); |
192 | f = theTiles.tileface(Game->BoardData(z, y, x) - TILE_OFFSET); |
193 | |
194 | if (Game->HighlightData(z, y, x)) { |
195 | selected = true; |
196 | } |
197 | |
198 | TileSprite *thissprite = new TileSprite(this, us, s, f, m_angle, selected); |
199 | |
200 | spriteMap.insert(TileCoord(x, y, z), thissprite); |
201 | } |
202 | } |
203 | } |
204 | |
205 | updateSpriteMap(); |
206 | } |
207 | |
208 | void BoardWidget::updateSpriteMap() |
209 | { |
210 | // initial offset on the screen of tile 0,0 |
211 | // think of it as what is left if you add all tilefaces (qwidth/heigh*2) plus one |
212 | // (wholetile - shadow(3dindent) - tileface), and divide by 2 |
213 | int xOffset = (width() - (Game->m_width * (theTiles.qWidth())) - (theTiles.width() |
214 | - (theTiles.qWidth() * 2))) / 2; |
215 | int yOffset = (height() - (Game->m_height * (theTiles.qHeight())) - (theTiles.height() |
216 | - (theTiles.qHeight() * 2))) / 2; |
217 | |
218 | // we iterate over the depth stacking order. Each successive level is |
219 | // drawn one indent up and to the right. The indent is the width |
220 | // of the 3d relief on the tile left (tile shadow width) |
221 | switch (m_angle) { |
222 | case NW: |
223 | //remove shadow from margin calculation |
224 | xOffset += theTiles.levelOffsetX() / 2; |
225 | yOffset += theTiles.levelOffsetY() / 2; |
226 | |
227 | //Position |
228 | for (int z = 0; z < Game->m_depth; z++) { |
229 | // we draw down the board so the tile below over rights our border |
230 | for (int y = 0; y < Game->m_height; y++) { |
231 | // drawing right to left to prevent border overwrite |
232 | for (int x=Game->m_width - 1; x >= 0; x--) { |
233 | int sx = x * (theTiles.qWidth()) + xOffset; |
234 | int sy = y * (theTiles.qHeight()) + yOffset; |
235 | |
236 | // skip if no tile to display |
237 | if (!Game->tilePresent(z, y, x)) { |
238 | continue; |
239 | } |
240 | |
241 | TileSprite *thissprite = spriteMap.value(TileCoord(x, y, z)); |
242 | |
243 | if (thissprite) { |
244 | thissprite->moveTo(sx, sy); |
245 | } |
246 | |
247 | if (thissprite) { |
248 | thissprite->show(); |
249 | } |
250 | } |
251 | } |
252 | |
253 | xOffset += theTiles.levelOffsetX(); |
254 | yOffset -= theTiles.levelOffsetY(); |
255 | } |
256 | |
257 | //Layer |
258 | for (int z = 0; z < Game->m_depth; z++) { |
259 | // start drawing in diagonal for correct layering. For this we double the board, |
260 | //actually starting outside of it, in the bottom right corner, so our first diagonal ends |
261 | // at the actual top right corner of the board |
262 | for (int x = Game->m_width * 2; x >= 0; x--) { |
263 | // reset the offset |
264 | int offset = 0; |
265 | |
266 | for (int y = Game->m_height - 1; y >= 0; y--) { |
267 | if (Game->tilePresent(z, y, x - offset)) { |
268 | TileSprite *thissprite = spriteMap.value(TileCoord(x - offset, y, z)); |
269 | |
270 | if (thissprite) { |
271 | thissprite->raise(); |
272 | } |
273 | } |
274 | |
275 | //at each pass, move one place to the left |
276 | offset++; |
277 | } |
278 | } |
279 | } |
280 | |
281 | break; |
282 | |
283 | case NE: |
284 | xOffset -= theTiles.levelOffsetX() / 2; |
285 | yOffset += theTiles.levelOffsetY() / 2; |
286 | |
287 | //Position |
288 | for (int z = 0; z < Game->m_depth; z++) { |
289 | // we draw down the board so the tile below over rights our border |
290 | for (int y = 0; y < Game->m_height; y++) { |
291 | // drawing right to left to prevent border overwrite |
292 | for (int x = 0; x <= Game->m_width - 1; x++) { |
293 | int sx = x * (theTiles.qWidth()) + xOffset; |
294 | int sy = y * (theTiles.qHeight()) + yOffset; |
295 | |
296 | // skip if no tile to display |
297 | if (!Game->tilePresent(z, y, x)) { |
298 | continue; |
299 | } |
300 | |
301 | TileSprite *thissprite = spriteMap.value(TileCoord(x, y, z)); |
302 | |
303 | if (thissprite) { |
304 | thissprite->moveTo(sx, sy); |
305 | } |
306 | |
307 | if (thissprite) { |
308 | thissprite->show(); |
309 | } |
310 | } |
311 | } |
312 | |
313 | xOffset -= theTiles.levelOffsetX(); |
314 | yOffset -= theTiles.levelOffsetY(); |
315 | } |
316 | |
317 | //Layer |
318 | for (int z = 0; z < Game->m_depth; z++) { |
319 | // start drawing in diagonal for correct layering. For this we double the board, |
320 | //actually starting outside of it, in the bottom right corner, so our first diagonal ends |
321 | // at the actual top right corner of the board |
322 | for (int x =- (Game->m_width); x <= Game->m_width - 1; x++) { |
323 | // reset the offset |
324 | int offset = 0; |
325 | |
326 | for (int y = Game->m_height - 1; y >= 0; y--) { |
327 | if (Game->tilePresent(z, y, x + offset)) { |
328 | TileSprite *thissprite = spriteMap.value(TileCoord(x + offset, y, z)); |
329 | |
330 | if (thissprite) { |
331 | thissprite->raise(); |
332 | } |
333 | } |
334 | |
335 | //at each pass, move one place to the right |
336 | offset++; |
337 | } |
338 | } |
339 | } |
340 | |
341 | break; |
342 | |
343 | case SE: |
344 | xOffset -= theTiles.levelOffsetX() / 2; |
345 | yOffset -= theTiles.levelOffsetY() / 2; |
346 | |
347 | //Position |
348 | for (int z = 0; z < Game->m_depth; z++) { |
349 | for (int y = Game->m_height - 1; y >= 0; y--) { |
350 | for (int x = 0; x <= Game->m_width - 1; x++) { |
351 | int sx = x * (theTiles.qWidth()) + xOffset; |
352 | int sy = y * (theTiles.qHeight()) + yOffset; |
353 | |
354 | if (!Game->tilePresent(z, y, x)) { |
355 | continue; |
356 | } |
357 | |
358 | TileSprite *thissprite = spriteMap.value(TileCoord(x, y, z)); |
359 | |
360 | if (thissprite) { |
361 | thissprite->moveTo(sx, sy); |
362 | } |
363 | |
364 | if (thissprite) { |
365 | thissprite->show(); |
366 | } |
367 | } |
368 | } |
369 | |
370 | xOffset -= theTiles.levelOffsetX(); |
371 | yOffset += theTiles.levelOffsetY(); |
372 | } |
373 | |
374 | //Layer |
375 | for (int z = 0; z < Game->m_depth; z++) { |
376 | for (int x =- (Game->m_width); x <= Game->m_width - 1; x++) { |
377 | int offset = 0; |
378 | |
379 | for (int y = 0; y < Game->m_height; y++) { |
380 | if (Game->tilePresent(z, y, x + offset)) { |
381 | TileSprite *thissprite = spriteMap.value(TileCoord(x + offset, y, z)); |
382 | if (thissprite) { |
383 | thissprite->raise(); |
384 | } |
385 | } |
386 | |
387 | offset++; |
388 | } |
389 | } |
390 | } |
391 | |
392 | break; |
393 | |
394 | case SW: |
395 | xOffset += theTiles.levelOffsetX() / 2; |
396 | yOffset -= theTiles.levelOffsetY() / 2; |
397 | |
398 | //Position |
399 | for (int z = 0; z < Game->m_depth; z++) { |
400 | for (int y = Game->m_height - 1; y >= 0; y--) { |
401 | for (int x = Game->m_width - 1; x >= 0; x--) { |
402 | int sx = x * (theTiles.qWidth()) + xOffset; |
403 | int sy = y * (theTiles.qHeight()) + yOffset; |
404 | |
405 | if (!Game->tilePresent(z, y, x)) { |
406 | continue; |
407 | } |
408 | |
409 | TileSprite *thissprite = spriteMap.value(TileCoord(x, y, z)); |
410 | |
411 | if (thissprite) { |
412 | thissprite->moveTo(sx, sy); |
413 | } |
414 | |
415 | if (thissprite) { |
416 | thissprite->show(); |
417 | } |
418 | } |
419 | } |
420 | |
421 | xOffset += theTiles.levelOffsetX(); |
422 | yOffset += theTiles.levelOffsetY(); |
423 | } |
424 | |
425 | //Layer |
426 | for (int z = 0; z < Game->m_depth; z++) { |
427 | for (int x = Game->m_width * 2; x >= 0; x--) { |
428 | int offset = 0; |
429 | |
430 | for (int y = 0; y < Game->m_height; y++) { |
431 | if (Game->tilePresent(z, y, x - offset)) { |
432 | TileSprite *thissprite = spriteMap.value(TileCoord(x - offset, y, z)); |
433 | |
434 | if (thissprite) { |
435 | thissprite->raise(); |
436 | } |
437 | } |
438 | |
439 | offset++; |
440 | } |
441 | } |
442 | } |
443 | |
444 | break; |
445 | } |
446 | } |
447 | |
448 | void BoardWidget::pause() |
449 | { |
450 | gamePaused = !gamePaused; |
451 | drawBoard(!gamePaused); |
452 | } |
453 | |
454 | void BoardWidget::gameLoaded() |
455 | { |
456 | int i; |
457 | Game->initialiseRemovedTiles(); |
458 | i = Game->TileNum; |
459 | |
460 | // use the history of moves to put in the removed tiles area the correct tiles |
461 | while (i < Game->MaxTileNum) { |
462 | Game->setRemovedTilePair(Game->MoveListData(i), Game->MoveListData(i + 1)); |
463 | i += 2; |
464 | } |
465 | |
466 | populateSpriteMap(); |
467 | drawBoard(true); |
468 | } |
469 | |
470 | int BoardWidget::undoMove() |
471 | { |
472 | cancelUserSelectedTiles(); |
473 | |
474 | if (Game->TileNum < Game->MaxTileNum) |
475 | { |
476 | Game->clearRemovedTilePair(Game->MoveListData(Game->TileNum), Game->MoveListData( |
477 | Game->TileNum + 1)); |
478 | putTileInBoard(Game->MoveListData(Game->TileNum), false); |
479 | Game->TileNum++; |
480 | putTileInBoard(Game->MoveListData(Game->TileNum)); |
481 | Game->TileNum++; |
482 | drawTileNumber(); |
483 | setStatusText(i18n("Undo operation done successfully." )); |
484 | |
485 | return 1; |
486 | } else { |
487 | setStatusText(i18n("What do you want to undo? You have done nothing!" )); |
488 | |
489 | return 0; |
490 | } |
491 | } |
492 | |
493 | void BoardWidget::helpMove() |
494 | { |
495 | cancelUserSelectedTiles(); |
496 | if (showHelp) helpMoveStop(); |
497 | |
498 | if (Game->findMove(TimerPos1, TimerPos2)) { |
499 | cheatsUsed++; |
500 | iTimerStep = 1; |
501 | showHelp = true; |
502 | helpMoveTimeout(); |
503 | } else { |
504 | setStatusText(i18n("Sorry, you have lost the game." )); |
505 | } |
506 | } |
507 | |
508 | void BoardWidget::helpMoveTimeout() |
509 | { |
510 | if (iTimerStep & 1) { |
511 | hilightTile(TimerPos1, true, false); |
512 | hilightTile(TimerPos2, true); |
513 | } else { |
514 | hilightTile(TimerPos1, false, false); |
515 | hilightTile(TimerPos2, false); |
516 | } |
517 | |
518 | // restart timer |
519 | if (iTimerStep++ < 8) { |
520 | timer->setSingleShot(true); |
521 | timer->start(ANIMSPEED); |
522 | } else { |
523 | showHelp = false; |
524 | } |
525 | } |
526 | |
527 | void BoardWidget::helpMoveStop() |
528 | { |
529 | timer->stop(); |
530 | iTimerStep = 8; |
531 | hilightTile(TimerPos1, false, false); |
532 | hilightTile(TimerPos2, false); |
533 | showHelp = false; |
534 | } |
535 | |
536 | void BoardWidget::startDemoMode() |
537 | { |
538 | calculateNewGame(); |
539 | |
540 | if (TimerState == Stop) { |
541 | TimerState = Demo; |
542 | iTimerStep = 0; |
543 | emit demoModeChanged(true); |
544 | setStatusText(i18n("Demo mode. Click mousebutton to stop." )); |
545 | demoMoveTimeout(); |
546 | } |
547 | } |
548 | |
549 | void BoardWidget::stopDemoMode() |
550 | { |
551 | TimerState = Stop; // stop demo |
552 | calculateNewGame(); |
553 | setStatusText(i18n("Now it is you again." )); |
554 | |
555 | emit demoModeChanged(false); |
556 | emit gameCalculated(); |
557 | } |
558 | |
559 | void BoardWidget::demoMoveTimeout() |
560 | { |
561 | if (TimerState == Demo) { |
562 | switch (iTimerStep++ % 6) { |
563 | case 0: |
564 | if (!Game->findMove(TimerPos1, TimerPos2)) { |
565 | // if computer has won |
566 | if (Game->TileNum == 0) { |
567 | animateMoveList(); |
568 | } else { |
569 | setStatusText(i18n("Your computer has lost the game." )); |
570 | |
571 | while (Game->TileNum < Game->MaxTileNum) { |
572 | putTileInBoard(Game->MoveListData(Game->TileNum), false); |
573 | Game->TileNum++; |
574 | putTileInBoard(Game->MoveListData(Game->TileNum)); |
575 | Game->TileNum++; |
576 | drawTileNumber(); |
577 | } |
578 | } |
579 | |
580 | TimerState = Stop; |
581 | //startDemoMode(); |
582 | //do not loop demo |
583 | stopDemoMode(); |
584 | |
585 | return; |
586 | } |
587 | |
588 | break; |
589 | |
590 | case 1: |
591 | case 3: |
592 | hilightTile(TimerPos1, true, false); |
593 | hilightTile(TimerPos2, true); |
594 | |
595 | break; |
596 | |
597 | case 2: |
598 | case 4: |
599 | hilightTile(TimerPos1, false, false); |
600 | hilightTile(TimerPos2, false); |
601 | |
602 | break; |
603 | |
604 | case 5: |
605 | Game->setRemovedTilePair(TimerPos1, TimerPos2); |
606 | removeTile(TimerPos1, false); |
607 | removeTile(TimerPos2); |
608 | drawTileNumber(); |
609 | |
610 | break; |
611 | } |
612 | |
613 | // restart timer |
614 | QTimer::singleShot(ANIMSPEED, this, SLOT(demoMoveTimeout())); |
615 | } |
616 | } |
617 | |
618 | void BoardWidget::setShowMatch(bool show) |
619 | { |
620 | if (showMatch) { |
621 | stopMatchAnimation(); |
622 | } |
623 | |
624 | showMatch = show; |
625 | } |
626 | |
627 | void BoardWidget::matchAnimationTimeout() |
628 | { |
629 | if (matchCount == 0) { |
630 | return; |
631 | } |
632 | |
633 | if (iTimerStep++ & 1) { |
634 | for (short Pos = 0; Pos < matchCount; Pos++) { |
635 | hilightTile(Game->getFromPosTable(Pos), true); |
636 | } |
637 | } else { |
638 | for (short Pos = 0; Pos < matchCount; Pos++) { |
639 | hilightTile(Game->getFromPosTable(Pos), false); |
640 | } |
641 | } |
642 | |
643 | if (TimerState == Match) { |
644 | QTimer::singleShot(ANIMSPEED, this, SLOT(matchAnimationTimeout())); |
645 | } |
646 | } |
647 | |
648 | void BoardWidget::stopMatchAnimation() |
649 | { |
650 | for (short Pos = 0; Pos < matchCount; Pos++) { |
651 | hilightTile(Game->getFromPosTable(Pos), false); |
652 | } |
653 | |
654 | TimerState = Stop; |
655 | matchCount = 0; |
656 | } |
657 | |
658 | void BoardWidget::redoMove() |
659 | { |
660 | Game->setRemovedTilePair(Game->MoveListData(Game->TileNum - 1), Game->MoveListData(Game->TileNum |
661 | - 2)); |
662 | removeTile(Game->MoveListData(Game->TileNum - 1), false); |
663 | removeTile(Game->MoveListData(Game->TileNum - 1)); |
664 | drawTileNumber(); |
665 | } |
666 | |
667 | void BoardWidget::animateMoveList() |
668 | { |
669 | setStatusText(i18n("Congratulations. You have won!" )); |
670 | animatingMoveListForward(); |
671 | } |
672 | |
673 | void BoardWidget::animatingMoveListForward() |
674 | { |
675 | if (Game->TileNum < Game->MaxTileNum) { |
676 | // put back all tiles |
677 | putTileInBoard(Game->MoveListData(Game->TileNum)); |
678 | Game->TileNum++; |
679 | putTileInBoard(Game->MoveListData(Game->TileNum), false); |
680 | Game->TileNum++; |
681 | drawTileNumber(); |
682 | animateForwardTimer->start(); //it is a single shot timer |
683 | } else { |
684 | //start removal |
685 | animateBackwardsTimer->start(); //it is a single shot timer |
686 | } |
687 | } |
688 | |
689 | void BoardWidget::animatingMoveListBackwards() |
690 | { |
691 | if (Game->TileNum > 0) { |
692 | // remove all tiles |
693 | removeTile(Game->MoveListData(Game->TileNum - 1), false); |
694 | removeTile(Game->MoveListData(Game->TileNum - 1)); |
695 | drawTileNumber(); |
696 | animateBackwardsTimer->start(); //it is a single shot timer |
697 | } else { |
698 | //end of animation |
699 | stopEndAnimation(); |
700 | } |
701 | } |
702 | |
703 | void BoardWidget::stopEndAnimation() |
704 | { |
705 | animateForwardTimer->stop(); |
706 | animateBackwardsTimer->stop(); |
707 | } |
708 | |
709 | QString BoardWidget::getRandomLayoutName() const |
710 | { |
711 | QStringList tilesAvailable = KGlobal::dirs()->findAllResources("kmahjongglayout" , |
712 | QString("*.desktop" ), KStandardDirs::Recursive); |
713 | |
714 | return tilesAvailable.at(qrand() % tilesAvailable.size()); |
715 | } |
716 | |
717 | void BoardWidget::calculateNewGame(int gNumber) |
718 | { |
719 | cancelUserSelectedTiles(); |
720 | stopMatchAnimation(); |
721 | stopEndAnimation(); |
722 | Game->initialiseRemovedTiles(); |
723 | |
724 | // If random layout is true, we will create a new random layout from the existing layouts. |
725 | if (Prefs::randomLayout()) { |
726 | QString layoutName = getRandomLayoutName(); |
727 | loadBoardLayout(layoutName); |
728 | } |
729 | |
730 | setStatusText(i18n("Calculating new game..." )); |
731 | |
732 | if (!loadBoard()) { |
733 | setStatusText(i18n("Error converting board information!" )); |
734 | |
735 | return; |
736 | } |
737 | |
738 | if (gNumber == -1) { |
739 | gameGenerationNum = KRandom::random(); |
740 | } else { |
741 | gameGenerationNum = gNumber; |
742 | } |
743 | |
744 | Game->random.setSeed(gameGenerationNum); |
745 | |
746 | // Translate Game->Map to an array of POSITION data. We only need to |
747 | // do this once for each new game. |
748 | Game->generateTilePositions(); |
749 | |
750 | // Now use the tile position data to generate tile dependency data. |
751 | // We only need to do this once for each new game. |
752 | Game->generatePositionDepends(); |
753 | |
754 | // Now try to position tiles on the board, 64 tries max. |
755 | for (short nr = 0; nr < 64; nr++) { |
756 | if (Game->generateStartPosition2()) { |
757 | drawBoard(true); |
758 | setStatusText(i18n("Ready. Now it is your turn." )); |
759 | cheatsUsed = 0; |
760 | emit gameCalculated(); |
761 | |
762 | return; |
763 | } |
764 | } |
765 | |
766 | drawBoard(false); |
767 | setStatusText(i18n("Error generating new game!" )); |
768 | } |
769 | |
770 | void BoardWidget::hilightTile(POSITION& Pos, bool on, bool doRepaint) |
771 | { |
772 | TileSprite *atile = 0; |
773 | TileCoord coord = TileCoord(Pos.x, Pos.y, Pos.e); |
774 | |
775 | if (spriteMap.contains(coord)) { |
776 | atile = spriteMap.value(coord); |
777 | } |
778 | |
779 | if (on) { |
780 | Game->setHighlightData(Pos.e, Pos.y, Pos.x, 1); |
781 | if (atile) { |
782 | atile->setSelected(true); |
783 | } |
784 | } else { |
785 | Game->setHighlightData(Pos.e, Pos.y, Pos.x, 0); |
786 | if (atile) { |
787 | atile->setSelected(false); |
788 | } |
789 | } |
790 | } |
791 | |
792 | void BoardWidget::drawBoard(bool showTiles) |
793 | { |
794 | if (gamePaused) { |
795 | showTiles = false; |
796 | } |
797 | |
798 | if (showTiles) { |
799 | populateSpriteMap(); |
800 | drawTileNumber(); |
801 | } else { |
802 | //Delete previous sprites |
803 | while (!items()->isEmpty()) { |
804 | delete items()->first(); |
805 | } |
806 | |
807 | //Clear our spritemap as well |
808 | spriteMap.clear(); |
809 | |
810 | //Recreate our background |
811 | QPalette palette; |
812 | palette.setBrush(backgroundRole(), theBackground.getBackground()); |
813 | setPalette(palette); |
814 | setAutoFillBackground(true); |
815 | } |
816 | } |
817 | |
818 | void BoardWidget::putTileInBoard(POSITION& Pos, bool doRepaint) |
819 | { |
820 | short E = Pos.e; |
821 | short Y = Pos.y; |
822 | short X = Pos.x; |
823 | |
824 | // we ensure that any tile we put on has highlighting off |
825 | Game->putTile(E, Y, X, Pos.f); |
826 | Game->setHighlightData(E, Y, X, 0); |
827 | |
828 | QPixmap s; |
829 | QPixmap us; |
830 | QPixmap f; |
831 | s = theTiles.selectedTile(m_angle); |
832 | us = theTiles.unselectedTile(m_angle); |
833 | f = theTiles.tileface(Game->BoardData(E, Y, X) - TILE_OFFSET); |
834 | TileSprite *thissprite = new TileSprite(this, us, s, f, m_angle, false); |
835 | thissprite->show(); |
836 | spriteMap.insert(TileCoord(X, Y, E), thissprite); |
837 | |
838 | updateSpriteMap(); |
839 | } |
840 | |
841 | void BoardWidget::removeTile(POSITION& Pos, bool doRepaint) |
842 | { |
843 | short E = Pos.e; |
844 | short Y = Pos.y; |
845 | short X = Pos.x; |
846 | |
847 | Game->TileNum--; // Eine Figur weniger |
848 | Game->setMoveListData(Game->TileNum,Pos); // Position ins Protokoll eintragen |
849 | |
850 | delete spriteMap.take(TileCoord(X,Y,E)); |
851 | |
852 | // remove tile from game board |
853 | Game->putTile(E, Y, X, 0); |
854 | } |
855 | |
856 | void BoardWidget::mousePressEvent(QMouseEvent *event) |
857 | { |
858 | if (gamePaused) { |
859 | return; |
860 | } |
861 | |
862 | if (event->button() == Qt::LeftButton) { |
863 | if (TimerState == Demo) { |
864 | stopDemoMode(); |
865 | } else if (showMatch) { |
866 | stopMatchAnimation(); |
867 | } |
868 | |
869 | if (showHelp) { // stop hilighting tiles |
870 | helpMoveStop(); |
871 | } |
872 | |
873 | if (MouseClickPos1.e == Game->m_depth) { // first tile |
874 | transformPointToPosition(event->pos(), MouseClickPos1); |
875 | |
876 | if (MouseClickPos1.e != Game->m_depth && showMatch) { |
877 | matchCount = Game->findAllMatchingTiles(MouseClickPos1); |
878 | TimerState = Match; |
879 | iTimerStep = 1; |
880 | matchAnimationTimeout(); |
881 | cheatsUsed++; |
882 | } |
883 | } else { // second tile |
884 | transformPointToPosition(event->pos(), MouseClickPos2); |
885 | if (MouseClickPos2.e == Game->m_depth) { |
886 | cancelUserSelectedTiles(); |
887 | } else { |
888 | if (Game->isMatchingTile(MouseClickPos1, MouseClickPos2)) { |
889 | // update the removed tiles (we do this before the remove below |
890 | // so that we only require 1 screen paint for both actions) |
891 | Game->setRemovedTilePair(MouseClickPos1, MouseClickPos2); |
892 | |
893 | // now we remove the tiles from the board*t, |
894 | removeTile(MouseClickPos1, false); |
895 | removeTile(MouseClickPos2); |
896 | |
897 | // removing a tile means redo is impossible without |
898 | // a further undo. |
899 | Game->allow_redo = false; |
900 | demoModeChanged(false); |
901 | drawTileNumber(); |
902 | |
903 | // if no tiles are left, the player has `won`, so celebrate |
904 | if (Game->TileNum == 0) { |
905 | gameOver(Game->MaxTileNum,cheatsUsed); |
906 | } else { |
907 | // else if no more moves are possible, display the sour grapes dialog |
908 | validMovesAvailable(); |
909 | } |
910 | } else { |
911 | // redraw tiles in normal state |
912 | hilightTile(MouseClickPos1, false, false); |
913 | hilightTile(MouseClickPos2, false); |
914 | } |
915 | |
916 | MouseClickPos1.e = Game->m_depth; // mark tile position as invalid |
917 | MouseClickPos2.e = Game->m_depth; |
918 | } |
919 | } |
920 | } |
921 | } |
922 | |
923 | KGameCanvasItem* BoardWidget::itemAt(const QPoint& point) const |
924 | { |
925 | // Get the shadows... |
926 | // theTiles.width() == The whole tile width including offset and shadow. |
927 | // theTiles.qWidth() == Half of the width of the tile face (without the offset and the shadow). |
928 | // theTiles.levelOffsetX() == The level of the tile (the perspective height of the tile) |
929 | int shadowWidth = theTiles.width() - (theTiles.qWidth() * 2 + theTiles.levelOffsetX()); |
930 | int shadowHeight = theTiles.height() - (theTiles.qHeight() * 2 + theTiles.levelOffsetY()); |
931 | |
932 | for (int i = m_items.size() - 1; i >= 0; i--) { |
933 | KGameCanvasItem *canvasItem = m_items[i]; |
934 | |
935 | // Cause of the shadow, that is not really a part of a tile. Correct the rect positions |
936 | // related to the angle. Actually we just correct the rect of the canvas item cause of the |
937 | // shadow, that is not part of the clickable tile. |
938 | QRect oldRect = canvasItem->rect(); |
939 | QRect rectCorrection = oldRect; |
940 | |
941 | // Correct the positions related to the angle we had set. |
942 | switch (m_angle) { |
943 | case NW: |
944 | rectCorrection.setRect(oldRect.x() + shadowWidth, oldRect.y(), |
945 | oldRect.width() - shadowWidth, oldRect.height() - shadowHeight); |
946 | break; |
947 | case NE: |
948 | rectCorrection.setRect(oldRect.x(), oldRect.y(), oldRect.width() - shadowWidth, |
949 | oldRect.height() - shadowHeight); |
950 | break; |
951 | case SW: |
952 | rectCorrection.setRect(oldRect.x() + shadowWidth, oldRect.y() + shadowHeight, |
953 | oldRect.width() - shadowWidth, oldRect.height() - shadowHeight); |
954 | break; |
955 | case SE: |
956 | rectCorrection.setRect(oldRect.x(), oldRect.y() + shadowHeight, |
957 | oldRect.width() - shadowWidth, oldRect.height() - shadowHeight); |
958 | break; |
959 | } |
960 | |
961 | // if the canvas is visible and we click inside the corrected rect of the canvas item, we |
962 | // can return the actual canvas item. |
963 | if (canvasItem->visible() && rectCorrection.contains(point)) { |
964 | return canvasItem; |
965 | } |
966 | } |
967 | |
968 | return NULL; |
969 | } |
970 | |
971 | void BoardWidget::transformPointToPosition(const QPoint &point, POSITION &MouseClickPos) |
972 | { |
973 | int E; |
974 | int X; |
975 | int Y; |
976 | |
977 | TileSprite *clickedItem = NULL; |
978 | clickedItem = (TileSprite *) itemAt(point); |
979 | |
980 | if (!clickedItem) { |
981 | //no item under mouse |
982 | qDebug() << "no tile registered" ; |
983 | |
984 | return; |
985 | } |
986 | |
987 | TileCoord coord = spriteMap.key(clickedItem); |
988 | E = coord.z(); |
989 | X = coord.x(); |
990 | Y = coord.y(); |
991 | |
992 | //sanity checking |
993 | if (X < 0 || X >= Game->m_width || Y < 0 || Y >= Game->m_height) { |
994 | return; |
995 | } |
996 | |
997 | // if gameboard is empty, skip |
998 | //sanity checking |
999 | if (!Game->BoardData(E, Y, X)) { |
1000 | qDebug() << "Tile not in BoardData. Fading out?" ; |
1001 | |
1002 | return; |
1003 | } |
1004 | |
1005 | // tile must be 'free' (nothing left, right or above it) |
1006 | //Optimization, skip "over" test for the top layer |
1007 | if (E < Game->m_depth - 1) { |
1008 | if (Game->BoardData(E + 1, Y, X) |
1009 | || (Y < Game->m_height - 1 && Game->BoardData(E + 1, Y + 1, X)) |
1010 | || (X < Game->m_width - 1 && Game->BoardData(E + 1, Y, X + 1)) |
1011 | || (X < Game->m_width - 1 && Y < Game->m_height - 1 |
1012 | && Game->BoardData(E + 1, Y + 1, X + 1))) { |
1013 | return; |
1014 | } |
1015 | } |
1016 | |
1017 | // No left test on left edge |
1018 | if (( X > 0) && (Game->BoardData(E, Y, X - 1) || Game->BoardData(E, Y + 1, X - 1))) { |
1019 | if ((X < Game->m_width - 2) |
1020 | && (Game->BoardData(E, Y, X + 2) || Game->BoardData(E, Y + 1, X + 2))) { |
1021 | return; |
1022 | } |
1023 | } |
1024 | |
1025 | // if we reach here, position is legal |
1026 | MouseClickPos.e = E; |
1027 | MouseClickPos.y = Y; |
1028 | MouseClickPos.x = X; |
1029 | MouseClickPos.f = Game->BoardData(E, Y, X); |
1030 | |
1031 | // give visible feedback |
1032 | hilightTile( MouseClickPos ); |
1033 | } |
1034 | |
1035 | bool BoardWidget::loadBoard( ) |
1036 | { |
1037 | delete Game; |
1038 | Game = new GameData(theBoardLayout.board()); |
1039 | |
1040 | return true; |
1041 | } |
1042 | |
1043 | void BoardWidget::setStatusText(const QString &pszText) |
1044 | { |
1045 | emit statusTextChanged(pszText, gameGenerationNum); |
1046 | } |
1047 | |
1048 | bool BoardWidget::loadBackground(const QString &pszFileName, bool bShowError) |
1049 | { |
1050 | if (theBackground.load(pszFileName, requiredWidth(), requiredHeight())) { |
1051 | if (theBackground.loadGraphics()) { |
1052 | return true; |
1053 | } |
1054 | } |
1055 | |
1056 | //Try default |
1057 | if (theBackground.loadDefault()) { |
1058 | if (theBackground.loadGraphics()) { |
1059 | } |
1060 | } |
1061 | |
1062 | return false; |
1063 | } |
1064 | |
1065 | void BoardWidget::drawTileNumber() |
1066 | { |
1067 | emit tileNumberChanged(Game->MaxTileNum, Game->TileNum, Game->moveCount()); |
1068 | } |
1069 | |
1070 | void BoardWidget::cancelUserSelectedTiles() |
1071 | { |
1072 | if (MouseClickPos1.e != Game->m_depth) { |
1073 | hilightTile(MouseClickPos1, false); // redraw tile |
1074 | MouseClickPos1.e = Game->m_depth; // mark tile invalid |
1075 | } |
1076 | } |
1077 | |
1078 | bool BoardWidget::loadTileset(const QString &path) |
1079 | { |
1080 | if (theTiles.loadTileset(path)) { |
1081 | if (theTiles.loadGraphics()) { |
1082 | resizeTileset(size()); |
1083 | |
1084 | return true; |
1085 | } |
1086 | } |
1087 | |
1088 | //Tileset or graphics could not be loaded, try default |
1089 | if (theTiles.loadDefault()) { |
1090 | if (theTiles.loadGraphics()) { |
1091 | resizeTileset(size()); |
1092 | } |
1093 | } |
1094 | |
1095 | return false; |
1096 | } |
1097 | |
1098 | bool BoardWidget::loadBoardLayout(const QString &file) |
1099 | { |
1100 | if (theBoardLayout.load(file)) { |
1101 | return true; |
1102 | } else { |
1103 | if (theBoardLayout.loadDefault()) { |
1104 | return false; |
1105 | } else { |
1106 | return false; |
1107 | } |
1108 | } |
1109 | } |
1110 | |
1111 | int BoardWidget::requiredHorizontalCells() |
1112 | { |
1113 | int res = (Game->m_width / 2); |
1114 | /*if (Prefs::showRemoved()) |
1115 | res = res + 3; //space for removed tiles*/ |
1116 | |
1117 | return res; |
1118 | } |
1119 | |
1120 | int BoardWidget::requiredVerticalCells() |
1121 | { |
1122 | int res = (Game->m_height / 2); |
1123 | |
1124 | return res; |
1125 | } |
1126 | |
1127 | int BoardWidget::requiredWidth() |
1128 | { |
1129 | //int res = ((BoardLayout::width+12)* theTiles.qWidth()); |
1130 | int res = width(); |
1131 | |
1132 | return res; |
1133 | } |
1134 | |
1135 | int BoardWidget::requiredHeight() |
1136 | { |
1137 | //int res = ((BoardLayout::height+3)* theTiles.qHeight()); |
1138 | int res = height(); |
1139 | |
1140 | return res; |
1141 | } |
1142 | |
1143 | void BoardWidget::angleSwitchCCW() |
1144 | { |
1145 | switch (m_angle) { |
1146 | case NW : |
1147 | m_angle = NE; |
1148 | |
1149 | break; |
1150 | |
1151 | case NE : |
1152 | m_angle = SE; |
1153 | |
1154 | break; |
1155 | |
1156 | case SE : |
1157 | m_angle = SW; |
1158 | |
1159 | break; |
1160 | |
1161 | case SW : |
1162 | m_angle = NW; |
1163 | |
1164 | break; |
1165 | } |
1166 | |
1167 | QHashIterator<TileCoord, TileSprite*> i(spriteMap); |
1168 | while (i.hasNext()) { |
1169 | i.next(); |
1170 | QPixmap u = theTiles.unselectedTile(m_angle); |
1171 | QPixmap s = theTiles.selectedTile(m_angle); |
1172 | i.value()->setAngle(m_angle, u, s); |
1173 | } |
1174 | |
1175 | //re-position and re-layer |
1176 | updateSpriteMap(); |
1177 | |
1178 | //save angle |
1179 | saveSettings(); |
1180 | } |
1181 | |
1182 | void BoardWidget::angleSwitchCW() |
1183 | { |
1184 | switch (m_angle) { |
1185 | case NW : |
1186 | m_angle = SW; |
1187 | |
1188 | break; |
1189 | |
1190 | case NE : |
1191 | m_angle = NW; |
1192 | |
1193 | break; |
1194 | |
1195 | case SE : |
1196 | m_angle = NE; |
1197 | |
1198 | break; |
1199 | |
1200 | case SW : |
1201 | m_angle = SE; |
1202 | |
1203 | break; |
1204 | } |
1205 | |
1206 | QHashIterator<TileCoord, TileSprite *> i(spriteMap); |
1207 | while (i.hasNext()) { |
1208 | i.next(); |
1209 | QPixmap u = theTiles.unselectedTile(m_angle); |
1210 | QPixmap s = theTiles.selectedTile(m_angle); |
1211 | i.value()->setAngle(m_angle, u, s); |
1212 | } |
1213 | |
1214 | //re-position and re-layer |
1215 | updateSpriteMap(); |
1216 | |
1217 | //save settings |
1218 | saveSettings(); |
1219 | } |
1220 | |
1221 | void BoardWidget::shuffle() |
1222 | { |
1223 | cancelUserSelectedTiles(); |
1224 | Game->shuffle(); |
1225 | |
1226 | // force a full redraw |
1227 | populateSpriteMap(); |
1228 | |
1229 | // I consider this s very bad cheat so, I punish the user |
1230 | // 300 points per use |
1231 | cheatsUsed += 15; |
1232 | drawTileNumber(); |
1233 | |
1234 | // Test if any moves are available |
1235 | validMovesAvailable(); |
1236 | } |
1237 | |
1238 | bool BoardWidget::validMovesAvailable() |
1239 | { |
1240 | if (!Game->findMove(TimerPos1, TimerPos2)) { |
1241 | KMessageBox::information(this, i18n("Game over: You have no moves left." )); |
1242 | |
1243 | return false; |
1244 | } |
1245 | |
1246 | return true; |
1247 | } |
1248 | |
1249 | QString BoardWidget::getLayoutName() |
1250 | { |
1251 | QString key("Name" ); |
1252 | |
1253 | return theBoardLayout.authorProperty(key); |
1254 | } |
1255 | |
1256 | void BoardWidget::wheelEvent(QWheelEvent *event) |
1257 | { |
1258 | if (event->orientation() == Qt::Vertical) { |
1259 | if (event->delta() > 0) { |
1260 | angleSwitchCCW(); |
1261 | } else { |
1262 | angleSwitchCW(); |
1263 | } |
1264 | } |
1265 | |
1266 | event->accept(); |
1267 | } |
1268 | |
1269 | |
1270 | #include "boardwidget.moc" |
1271 | |