1/*******************************************************************
2 *
3 * Copyright 2006-2008 Dmitry Suzdalev <dimsuz@gmail.com>
4 *
5 * This file is part of the KDE project "KLines"
6 *
7 * KLines is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2, or (at your option)
10 * any later version.
11 *
12 * KLines is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with KLines; see the file COPYING. If not, write to
19 * the Free Software Foundation, 51 Franklin Street, Fifth Floor,
20 * Boston, MA 02110-1301, USA.
21 *
22 ********************************************************************/
23#include "scene.h"
24#include "ballitem.h"
25#include "previewitem.h"
26#include "animator.h"
27#include "renderer.h"
28
29#include <QGraphicsSceneMouseEvent>
30#include <QPainter>
31#include <QSet>
32
33#include <KGamePopupItem>
34#include <KLocale>
35#include <KDebug>
36
37inline uint qHash( const FieldPos& pos )
38{
39 return qHash( QPair<int,int>(pos.x,pos.y) );
40}
41
42KLinesScene::KLinesScene( QObject* parent )
43 : QGraphicsScene(parent),
44 m_playFieldBorderSize(0), m_numFreeCells(FIELD_SIZE*FIELD_SIZE),
45 m_score(0), m_bonusScore(0), m_cellSize(32), m_previewZoneVisible(true)
46{
47 m_animator = new KLinesAnimator(this);
48 connect( m_animator, SIGNAL(moveFinished()), SLOT(moveAnimFinished()) );
49 connect( m_animator, SIGNAL(removeFinished()), SLOT(removeAnimFinished()) );
50 connect( m_animator, SIGNAL(bornFinished()), SLOT(bornAnimFinished()) );
51
52 m_focusItem = new QGraphicsRectItem( QRectF(0, 0, m_cellSize, m_cellSize), 0, this );
53 m_focusItem->setZValue(1.0);
54 m_focusItem->setPen( Qt::DashLine );
55
56 m_previewItem = new PreviewItem(this);
57 m_previewItem->setPos( 0, 0 );
58
59 m_popupItem = new KGamePopupItem;
60 addItem(m_popupItem);
61
62 startNewGame();
63}
64
65void KLinesScene::startNewGame()
66{
67 if(m_animator->isAnimating())
68 return;
69
70 // reset all vars
71 m_selPos = FieldPos();
72 m_numFreeCells = FIELD_SIZE*FIELD_SIZE;
73 m_score = 0;
74 m_bonusScore = 0;
75 m_placeBalls = true;
76 m_gameOver = false;
77 m_itemsToDelete.clear();
78 m_nextColors.clear();
79 m_focusItem->setPos(0, 0);
80 m_focusItem->hide();
81
82 m_popupItem->forceHide();
83
84 // remove all ball items from the scene leaving other items untouched
85 QList<QGraphicsItem*> itemlist = items();
86 foreach( QGraphicsItem* item, itemlist )
87 {
88 BallItem* ball = qgraphicsitem_cast<BallItem*>(item);
89 if( ball )
90 {
91 removeItem(item);
92 delete item;
93 }
94 }
95
96 for(int x=0; x<FIELD_SIZE; ++x)
97 for(int y=0; y<FIELD_SIZE; ++y)
98 m_field[x][y] = 0;
99
100 // init m_nextColors
101 for(int i=0; i<3; i++)
102 {
103 // random color
104 BallColor c = static_cast<BallColor>(m_randomSeq.getLong(static_cast<int>(NumColors)));
105 m_nextColors.append(c);
106 }
107
108 emit stateChanged(QLatin1String( "not_undoable" ));
109
110 nextThreeBalls();
111}
112
113KLinesScene::~KLinesScene()
114{
115 delete m_animator;
116}
117
118void KLinesScene::resizeScene(int width,int height)
119{
120 // store focus item field pos (calculated using old cellSize)
121 FieldPos focusRectFieldPos = pixToField( m_focusItem->pos() );
122
123 bool hasBorder = KLinesRenderer::hasBorderElement();
124
125 int minDim = qMin( width, height );
126 // border width is hardcoded to be half of cell size.
127 // take it into account if it exists
128 m_cellSize = hasBorder ? minDim/(FIELD_SIZE+1) : minDim/FIELD_SIZE;
129
130 // set it only if current theme supports it
131 m_playFieldBorderSize = hasBorder ? m_cellSize/2 : 0;
132
133 int boardSize = m_cellSize * FIELD_SIZE;
134 if ( m_previewZoneVisible && boardSize +m_playFieldBorderSize*2 + m_cellSize > width) // No space enough for balls preview
135 {
136 minDim = width;
137 m_cellSize = hasBorder ? (minDim - m_cellSize - m_playFieldBorderSize*2)/FIELD_SIZE : (minDim - m_cellSize)/FIELD_SIZE;
138 boardSize = m_cellSize * FIELD_SIZE;
139 }
140
141
142 m_playFieldRect.setX( (width - (m_previewZoneVisible ? m_cellSize : 0))/2 - boardSize/2 - m_playFieldBorderSize );
143 m_playFieldRect.setY( height/2 - boardSize/2 - m_playFieldBorderSize );
144
145 m_playFieldRect.setWidth( boardSize + m_playFieldBorderSize*2 );
146 m_playFieldRect.setHeight( boardSize + m_playFieldBorderSize*2 );
147
148 setSceneRect( 0, 0, width, height );
149
150 // sets render sizes for cells
151 KLinesRenderer::setCellSize( m_cellSize );
152 QSize cellSize(m_cellSize, m_cellSize);
153
154 // re-render && recalc positions for all balls
155 for( int x=0; x<FIELD_SIZE; ++x)
156 for(int y=0; y< FIELD_SIZE; ++y)
157 {
158 if( m_field[x][y] )
159 {
160 // this will update pixmap
161 m_field[x][y]->setRenderSize(cellSize);
162 m_field[x][y]->setPos( fieldToPix( FieldPos(x,y) ) );
163 m_field[x][y]->setColor( m_field[x][y]->color() );
164 }
165 }
166
167 m_focusItem->setRect( QRect(0,0, m_cellSize, m_cellSize) );
168 m_focusItem->setPos( fieldToPix( focusRectFieldPos ) );
169
170 int previewOriginY = height / 2 - (3 * m_cellSize) / 2;
171 int previewOriginX = m_playFieldRect.x() + m_playFieldRect.width();
172 m_previewItem->setPos( previewOriginX, previewOriginY );
173 m_previewItem->setPreviewColors( m_nextColors );
174
175 //kDebug() << "resize:" << width << "," << height << "; cellSize:" << m_cellSize;
176}
177
178void KLinesScene::endTurn()
179{
180 if( m_gameOver )
181 return;
182
183 saveUndoInfo();
184 nextThreeBalls();
185}
186
187void KLinesScene::nextThreeBalls()
188{
189 if( m_animator->isAnimating() )
190 return;
191
192 QList<BallItem*> newItems;
193 BallItem* newBall;
194 for(int i=0; i<3; i++)
195 {
196 newBall = randomlyPlaceBall( m_nextColors.at(i) );
197 if( newBall )
198 newItems.append(newBall);
199 else
200 break; // the field is filled :).
201 }
202
203 for(int i=0; i<3; i++)
204 {
205 // random color
206 BallColor c = static_cast<BallColor>(m_randomSeq.getLong(static_cast<int>(NumColors)));
207 m_nextColors[i] = c;
208 }
209
210 m_previewItem->setPreviewColors( m_nextColors );
211
212 m_animator->animateBorn( newItems );
213}
214
215void KLinesScene::setPreviewZoneVisible( bool visible )
216{
217 if (visible == m_previewZoneVisible)
218 return;
219
220 m_previewZoneVisible = visible;
221 m_previewItem->setVisible( visible );
222 resizeScene((int) width(), (int) height());
223 invalidate( sceneRect() );
224}
225
226BallItem* KLinesScene::randomlyPlaceBall(BallColor c)
227{
228 m_numFreeCells--;
229 if(m_numFreeCells < 0)
230 {
231 // restore m_numFreeCells value, it will trigger
232 // saveAndErase() after bornAnimFinished to check if
233 // we have 5-in-a-row to erase
234 m_numFreeCells = 0;
235 return 0; // game over, we won't create more balls
236 }
237
238 int posx = -1, posy = -1;
239 // let's find random free cell
240 do
241 {
242 posx = m_randomSeq.getLong(FIELD_SIZE);
243 posy = m_randomSeq.getLong(FIELD_SIZE);
244 } while( m_field[posx][posy] != 0 );
245
246 BallItem* newBall = new BallItem( this);
247 newBall->setColor(c, false); // pixmap will be set by born animation
248 newBall->setPos( fieldToPix( FieldPos(posx,posy) ) );
249
250 m_field[posx][posy] = newBall;
251 return newBall;
252}
253
254void KLinesScene::mousePressEvent( QGraphicsSceneMouseEvent* ev )
255{
256 QGraphicsScene::mousePressEvent(ev);
257 QRect boardRect = m_playFieldRect.adjusted( m_playFieldBorderSize,
258 m_playFieldBorderSize,
259 -m_playFieldBorderSize,
260 -m_playFieldBorderSize );
261
262 if ( !boardRect.contains( ev->scenePos().toPoint() ) )
263 return;
264
265 selectOrMove( pixToField(ev->scenePos()) );
266}
267
268void KLinesScene::selectOrMove( const FieldPos& fpos )
269{
270 if( m_animator->isAnimating() )
271 return;
272
273 if( m_field[fpos.x][fpos.y] ) // ball was selected
274 {
275 if( m_selPos.isValid() )
276 {
277 m_field[m_selPos.x][m_selPos.y]->stopAnimation();
278
279 if ( m_selPos == fpos )
280 {
281 m_selPos.x = m_selPos.y = -1; // invalidate position
282 return;
283 }
284 }
285
286 m_field[fpos.x][fpos.y]->startSelectedAnimation();
287 m_selPos = fpos;
288 }
289 else // move selected ball to new location
290 {
291 if( m_selPos.isValid() && m_field[fpos.x][fpos.y] == 0 )
292 {
293 saveUndoInfo();
294 // start move animation
295 // slot moveAnimFinished() will be called when it finishes
296 bool pathExists = m_animator->animateMove(m_selPos, fpos);
297 if(!pathExists)
298 {
299 m_popupItem->setMessageTimeout(2500);
300 m_popupItem->showMessage(i18n("There is no path from the selected piece to this cell"), KGamePopupItem::BottomLeft);
301 }
302 }
303 }
304}
305
306void KLinesScene::moveAnimFinished()
307{
308 // m_field[m_selPos.x][m_selPos.y] still holds the ball pointer
309 // but animation placed it to new location.
310 // But it updated only pixel position, not the field one
311 // So let's do it here
312 BallItem *movedBall = m_field[m_selPos.x][m_selPos.y];
313 // movedBall has new pixel position - let's find out corresponding field pos
314 FieldPos newpos = pixToField(movedBall->pos());
315
316 m_field[m_selPos.x][m_selPos.y] = 0; // no more ball here
317 m_field[newpos.x][newpos.y] = movedBall;
318
319 m_selPos.x = m_selPos.y = -1; // invalidate position
320
321 m_placeBalls = true;
322 // after anim finished, slot removeAnimFinished()
323 // will be called
324 searchAndErase();
325}
326
327void KLinesScene::removeAnimFinished()
328{
329 if( m_itemsToDelete.isEmpty() && m_numFreeCells == 0 )
330 {
331 // game over
332 gameOverHandler();
333 return;
334 }
335
336 if( m_itemsToDelete.isEmpty() && m_placeBalls)
337 {
338 // slot bornAnimFinished() will be called
339 // when born animation finishes
340 // NOTE: removeAnimFinished() will be called again
341 // after new balls will born (because searchAndErase() will be called)
342 // but other if branch will be taken
343 nextThreeBalls();
344 }
345 else
346 {
347 // this is kind of 'things to do after one turn is finished'
348 // place in code :)
349
350 int numBallsErased = m_itemsToDelete.count();
351 if(numBallsErased)
352 {
353 // expression taked from previous code in klines.cpp
354 m_score += 2*numBallsErased*numBallsErased - 20*numBallsErased + 60 ;
355 m_score += m_bonusScore;
356 }
357
358 foreach( BallItem* item, m_itemsToDelete )
359 {
360 removeItem(item);
361 delete item;
362 }
363 m_itemsToDelete.clear();
364
365 if(numBallsErased)
366 emit scoreChanged(m_score);
367 }
368}
369
370void KLinesScene::bornAnimFinished()
371{
372 // note that if m_numFreeCells == 0, we still need to
373 // check for possible 5-in-a-row balls, i.e. call searchAndErase()
374 // So there's another gameOver-check in removeAnimFinished()
375 if( m_numFreeCells < 0 )
376 {
377 gameOverHandler();
378 return;
379 }
380 // There's a little trick here:
381 // searchAndErase() will cause m_animator to emit removeFinished()
382 // If there wasn't m_placeBalls var
383 // it would cause an infinite loop like this:
384 // SaE()->removeAnimFinished()->next3Balls()->bornAnimFinished()->
385 // SaE()->removeAnimFinished()->next3Balls()->...
386 // etc etc
387 m_placeBalls = false;
388 // after placing new balls new 5-in-a-row chunks can occur
389 // so we need to check for them
390 //
391 // And because of that we check for gameOver in removeAnimFinished()
392 // rather than here - there's a chance that searchAndErase() will remove
393 // balls making some free cells to play in
394 searchAndErase();
395}
396
397void KLinesScene::searchAndErase()
398{
399 // FIXME dimsuz: put more comments about bounds in for loops
400
401 // QSet - to exclude adding duplicates
402 QSet<FieldPos> positionsToDelete;
403
404 // horizontal chunks searching
405 for(int x=0; x<FIELD_SIZE-4; ++x)
406 for(int y=0;y<FIELD_SIZE; ++y)
407 {
408 if(m_field[x][y] == 0)
409 continue;
410
411 BallColor col = m_field[x][y]->color();
412 int tmpx = x+1;
413 while(tmpx < FIELD_SIZE && m_field[tmpx][y] && m_field[tmpx][y]->color() == col)
414 tmpx++;
415 // tmpx-x will be: how much balls of the same color we found
416 if(tmpx-x >= 5)
417 {
418 for(int i=x; i<tmpx;++i)
419 {
420 positionsToDelete.insert( FieldPos(i,y) );
421 }
422 }
423 else
424 continue;
425 }
426
427 // vertical chunks searching
428 for(int y=0; y<FIELD_SIZE-4; ++y)
429 for(int x=0;x<FIELD_SIZE; ++x)
430 {
431 if(m_field[x][y] == 0)
432 continue;
433
434 BallColor col = m_field[x][y]->color();
435 int tmpy = y+1;
436 while(tmpy < FIELD_SIZE && m_field[x][tmpy] && m_field[x][tmpy]->color() == col)
437 tmpy++;
438 // tmpy-y will be: how much balls of the same color we found
439 if(tmpy-y >= 5)
440 {
441 for(int j=y; j<tmpy;++j)
442 {
443 positionsToDelete.insert( FieldPos(x,j) );
444 }
445 }
446 else
447 continue;
448 }
449
450 // down-right diagonal
451 for(int x=0; x<FIELD_SIZE-4; ++x)
452 for(int y=0;y<FIELD_SIZE-4; ++y)
453 {
454 if(m_field[x][y] == 0)
455 continue;
456
457 BallColor col = m_field[x][y]->color();
458 int tmpx = x+1;
459 int tmpy = y+1;
460 while(tmpx < FIELD_SIZE && tmpy < FIELD_SIZE &&
461 m_field[tmpx][tmpy] && m_field[tmpx][tmpy]->color() == col)
462 {
463 tmpx++;
464 tmpy++;
465 }
466 // tmpx-x (and tmpy-y too) will be: how much balls of the same color we found
467 if(tmpx-x >= 5)
468 {
469 for(int i=x,j=y; i<tmpx;++i,++j)
470 {
471 positionsToDelete.insert( FieldPos(i,j) );
472 }
473 }
474 else
475 continue;
476 }
477
478 // up-right diagonal
479 for(int x=0; x<FIELD_SIZE-4; ++x)
480 for(int y=4; y<FIELD_SIZE; ++y)
481 {
482 if(m_field[x][y] == 0)
483 continue;
484
485 BallColor col = m_field[x][y]->color();
486 int tmpx = x+1;
487 int tmpy = y-1;
488 while(tmpx < FIELD_SIZE && tmpy >=0 &&
489 m_field[tmpx][tmpy] && m_field[tmpx][tmpy]->color() == col)
490 {
491 tmpx++;
492 tmpy--;
493 }
494 // tmpx-x (and tmpy-y too) will be: how much balls of the same color we found
495 if(tmpx-x >= 5)
496 {
497 for(int i=x,j=y; i<tmpx;++i,--j)
498 {
499 positionsToDelete.insert( FieldPos(i,j) );
500 }
501 }
502 else
503 continue;
504 }
505
506 foreach( const FieldPos& pos, positionsToDelete )
507 {
508 m_itemsToDelete.append(m_field[pos.x][pos.y]);
509 m_field[pos.x][pos.y] = 0;
510 m_numFreeCells++;
511 }
512
513 // after it finishes slot removeAnimFinished() will be called
514 // if m_itemsToDelete is empty removeAnimFinished() will be called immediately
515 m_animator->animateRemove( m_itemsToDelete );
516}
517
518void KLinesScene::moveFocusLeft()
519{
520 if( !m_focusItem->isVisible() )
521 {
522 m_focusItem->show();
523 // no action for the first time
524 return;
525 }
526 FieldPos focusPos = pixToField( m_focusItem->pos() );
527 focusPos.x--;
528 if (focusPos.x < 0) // rotate on the torus
529 focusPos.x = FIELD_SIZE - 1;
530
531 m_focusItem->setPos ( fieldToPix( focusPos ) );
532}
533
534void KLinesScene::moveFocusRight()
535{
536 if( !m_focusItem->isVisible() )
537 {
538 m_focusItem->show();
539 // no action for the first time
540 return;
541 }
542 FieldPos focusPos = pixToField( m_focusItem->pos() );
543 focusPos.x++;
544 if (focusPos.x >= FIELD_SIZE) // rotate on the torus
545 focusPos.x = 0;
546
547 m_focusItem->setPos ( fieldToPix( focusPos ) );
548}
549
550void KLinesScene::moveFocusUp()
551{
552 if( !m_focusItem->isVisible() )
553 {
554 m_focusItem->show();
555 // no action for the first time
556 return;
557 }
558 FieldPos focusPos = pixToField( m_focusItem->pos() );
559 focusPos.y--;
560 if (focusPos.y < 0) // rotate on the torus
561 focusPos.y = FIELD_SIZE - 1;
562
563 m_focusItem->setPos ( fieldToPix( focusPos ) );
564}
565
566void KLinesScene::moveFocusDown()
567{
568 if( !m_focusItem->isVisible() )
569 {
570 m_focusItem->show();
571 // no action for the first time
572 return;
573 }
574
575 FieldPos focusPos = pixToField( m_focusItem->pos() );
576 focusPos.y++;
577 if (focusPos.y >= FIELD_SIZE) // rotate on the torus
578 focusPos.y = 0;
579
580 m_focusItem->setPos ( fieldToPix( focusPos ) );
581}
582
583void KLinesScene::cellSelected()
584{
585 if( !m_focusItem->isVisible() )
586 m_focusItem->show();
587
588 // we're taking the center of the cell
589 selectOrMove( pixToField( m_focusItem->pos() + QPointF(m_cellSize/2,m_cellSize/2) ) );
590}
591
592void KLinesScene::saveUndoInfo()
593{
594 // save field state to undoInfo
595 for(int x=0;x<FIELD_SIZE;++x)
596 for(int y=0; y<FIELD_SIZE;++y)
597 // NumColors represents no color
598 m_undoInfo.fcolors[x][y] = ( m_field[x][y] ? m_field[x][y]->color() : NumColors );
599 m_undoInfo.numFreeCells = m_numFreeCells;
600 m_undoInfo.score = m_score;
601 m_undoInfo.nextColors = m_nextColors;
602
603 emit stateChanged(QLatin1String( "undoable" ));
604}
605
606// Brings m_field and some other vars to the state it was before last turn
607void KLinesScene::undo()
608{
609 // do not allow undo during animation
610 if(m_animator->isAnimating())
611 return;
612
613 if( m_selPos.isValid() )
614 m_field[m_selPos.x][m_selPos.y]->stopAnimation();
615
616 BallColor col;
617 for(int x=0;x<FIELD_SIZE;++x)
618 for(int y=0; y<FIELD_SIZE;++y)
619 {
620 col = m_undoInfo.fcolors[x][y];
621 if(col == NumColors) // no ball
622 {
623 if( m_field[x][y] )
624 {
625 removeItem( m_field[x][y] );
626 delete m_field[x][y];
627 m_field[x][y] = 0;
628 }
629 continue;
630 }
631
632 if( m_field[x][y] )
633 {
634 if( m_field[x][y]->color() != col )
635 m_field[x][y]->setColor(col);
636 //else live it as it is
637 }
638 else
639 {
640 BallItem *item = new BallItem(this);
641 item->setColor(col);
642 item->setPos( fieldToPix( FieldPos(x,y) ) );
643 item->show();
644 item->setRenderSize(KLinesRenderer::cellExtent());
645 m_field[x][y] = item;
646 }
647 }
648 m_numFreeCells = m_undoInfo.numFreeCells;
649 m_score = m_undoInfo.score;
650 m_nextColors = m_undoInfo.nextColors;
651
652 m_selPos = FieldPos();
653
654 m_previewItem->setPreviewColors( m_nextColors );
655
656 emit scoreChanged(m_score);
657
658 emit stateChanged(QLatin1String( "not_undoable" ));
659}
660
661void KLinesScene::drawBackground(QPainter *p, const QRectF&)
662{
663 QPixmap tile = KLinesRenderer::backgroundTilePixmap();
664 p->drawPixmap( 0, 0, KLinesRenderer::backgroundPixmap(sceneRect().size().toSize()) );
665 p->drawPixmap( m_playFieldRect.x(), m_playFieldRect.y(),
666 KLinesRenderer::backgroundBorderPixmap( m_playFieldRect.size() ) );
667
668 int startX = m_playFieldRect.x()+m_playFieldBorderSize;
669 int maxX = m_playFieldRect.x()+m_cellSize*FIELD_SIZE;
670 int startY = m_playFieldRect.y()+m_playFieldBorderSize;
671 int maxY = m_playFieldRect.y()+m_cellSize*FIELD_SIZE;
672
673 for(int x=startX; x<maxX; x+=m_cellSize)
674 for(int y=startY; y<maxY; y+=m_cellSize)
675 p->drawPixmap( x, y, tile );
676}
677
678void KLinesScene::gameOverHandler()
679{
680 if( m_gameOver )
681 return; // don't emit twice
682 m_gameOver = true;
683 kDebug() << "GAME OVER";
684 emit stateChanged(QLatin1String( "not_undoable" ));
685 //emit enableUndo(false);
686 emit gameOver(m_score);
687
688 // disable auto-hide
689 m_popupItem->setMessageTimeout(0);
690 m_popupItem->showMessage(i18n("<h1>Game over</h1>"), KGamePopupItem::Center);
691}
692
693#include "scene.moc"
694