1/*
2 Copyright 2007 Dmitry Suzdalev <dimsuz@gmail.com>
3 Copyright 2010 Brian Croom <brian.s.croom@gmail.com>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18*/
19#include "minefielditem.h"
20
21#include <kdebug.h>
22#include <QGraphicsScene>
23#include <QGraphicsSceneMouseEvent>
24
25#include "cellitem.h"
26#include "borderitem.h"
27
28MineFieldItem::MineFieldItem(KGameRenderer* renderer)
29 : m_leftButtonPos(-1,-1), m_midButtonPos(-1,-1), m_gameOver(false),
30 m_emulatingMidButton(false), m_renderer(renderer)
31{
32 setFlag(QGraphicsItem::ItemHasNoContents);
33}
34
35void MineFieldItem::initField( int numRows, int numCols, int numMines )
36{
37 numMines = qMin(numMines, numRows*numCols - MINIMAL_FREE );
38
39 m_firstClick = true;
40 m_gameOver = false;
41
42 int oldSize = m_cells.size();
43 int newSize = numRows*numCols;
44 int oldBorderSize = m_borders.size();
45 int newBorderSize = (numCols+2)*2 + (numRows+2)*2-4;
46
47 // if field is being shrinked, delete elements at the end before resizing vector
48 if(oldSize > newSize)
49 {
50 for( int i=newSize; i<oldSize; ++i )
51 {
52 // is this the best way to remove an item?
53 scene()->removeItem(m_cells[i]);
54 delete m_cells[i];
55 }
56
57 // adjust border item array too
58 for( int i=newBorderSize; i<oldBorderSize; ++i)
59 {
60 scene()->removeItem(m_borders[i]);
61 delete m_borders[i];
62 }
63 }
64
65 m_cells.resize(newSize);
66 m_borders.resize(newBorderSize);
67
68 m_numRows = numRows;
69 m_numCols = numCols;
70 m_minesCount = numMines;
71 m_numUnrevealed = m_numRows*m_numCols;
72 m_midButtonPos = qMakePair(-1, -1);
73 m_leftButtonPos = qMakePair(-1, -1);
74
75 for(int i=0; i<newSize; ++i)
76 {
77 // reset old, create new
78 if(i<oldSize)
79 m_cells[i]->reset();
80 else
81 m_cells[i] = new CellItem(m_renderer, this);
82 // let it be empty by default
83 // generateField() will adjust needed cells
84 // to hold digits or mines
85 m_cells[i]->setDigit(0);
86 }
87
88 for(int i=oldBorderSize; i<newBorderSize; ++i)
89 m_borders[i] = new BorderItem(m_renderer, this);
90
91 setupBorderItems();
92
93 adjustItemPositions();
94 m_flaggedMinesCount = 0;
95 emit flaggedMinesCountChanged(m_flaggedMinesCount);
96}
97
98void MineFieldItem::generateField(int clickedIdx)
99{
100 // generating mines ensuring that clickedIdx won't hold mine
101 // and that it will be an empty cell so the user don't have
102 // to make random guesses at the start of the game
103 QList<int> cellsWithMines;
104 int minesToPlace = m_minesCount;
105 int randomIdx = 0;
106 CellItem* item = 0;
107 FieldPos fp = rowColFromIndex(clickedIdx);
108
109 // this is the list of items we don't want to put the mine in
110 // to ensure that clickedIdx will stay an empty cell
111 // (it will be empty if none of surrounding items holds mine)
112 QList<CellItem*> neighbForClicked = adjasentItemsFor(fp.first, fp.second);
113
114 while(minesToPlace != 0)
115 {
116 randomIdx = m_randomSeq.getLong( m_numRows*m_numCols );
117 item = m_cells.at(randomIdx);
118 if(!item->hasMine()
119 && neighbForClicked.indexOf(item) == -1
120 && randomIdx != clickedIdx)
121 {
122 // ok, let's mine this place! :-)
123 item->setHasMine(true);
124 cellsWithMines.append(randomIdx);
125 minesToPlace--;
126 }
127 else
128 continue;
129 }
130
131 foreach(int idx, cellsWithMines)
132 {
133 FieldPos rc = rowColFromIndex(idx);
134 QList<CellItem*> neighbours = adjasentItemsFor(rc.first, rc.second);
135 foreach( CellItem *item, neighbours )
136 {
137 if(!item->hasMine())
138 item->setDigit( item->digit()+1 );
139 }
140 }
141}
142
143void MineFieldItem::setupBorderItems()
144{
145 int i = 0;
146 for(int row=0; row<m_numRows+2; ++row)
147 for(int col=0; col<m_numCols+2; ++col)
148 {
149 if( row == 0 && col == 0)
150 {
151 m_borders.at(i)->setRowCol(0,0);
152 m_borders.at(i)->setBorderType(KMinesState::BorderCornerNW);
153 i++;
154 }
155 else if( row == 0 && col == m_numCols+1)
156 {
157 m_borders.at(i)->setRowCol(row,col);
158 m_borders.at(i)->setBorderType(KMinesState::BorderCornerNE);
159 i++;
160 }
161 else if( row == m_numRows+1 && col == 0 )
162 {
163 m_borders.at(i)->setRowCol(row,col);
164 m_borders.at(i)->setBorderType(KMinesState::BorderCornerSW);
165 i++;
166 }
167 else if( row == m_numRows+1 && col == m_numCols+1 )
168 {
169 m_borders.at(i)->setRowCol(row,col);
170 m_borders.at(i)->setBorderType(KMinesState::BorderCornerSE);
171 i++;
172 }
173 else if( row == 0 )
174 {
175 m_borders.at(i)->setRowCol(row,col);
176 m_borders.at(i)->setBorderType(KMinesState::BorderNorth);
177 i++;
178 }
179 else if( row == m_numRows+1 )
180 {
181 m_borders.at(i)->setRowCol(row,col);
182 m_borders.at(i)->setBorderType(KMinesState::BorderSouth);
183 i++;
184 }
185 else if( col == 0 )
186 {
187 m_borders.at(i)->setRowCol(row,col);
188 m_borders.at(i)->setBorderType(KMinesState::BorderWest);
189 i++;
190 }
191 else if( col == m_numCols+1 )
192 {
193 m_borders.at(i)->setRowCol(row,col);
194 m_borders.at(i)->setBorderType(KMinesState::BorderEast);
195 i++;
196 }
197 }
198}
199
200QRectF MineFieldItem::boundingRect() const
201{
202 // +2 - because of border on each side
203 return QRectF(0, 0, m_cellSize*(m_numCols+2), m_cellSize*(m_numRows+2));
204}
205
206void MineFieldItem::paint( QPainter * painter, const QStyleOptionGraphicsItem* opt, QWidget* w)
207{
208 Q_UNUSED(painter);
209 Q_UNUSED(opt);
210 Q_UNUSED(w);
211}
212
213void MineFieldItem::resizeToFitInRect(const QRectF& rect)
214{
215 prepareGeometryChange();
216
217 // +2 in some places - because of border on each side
218
219 // here follows "cooomplex" algorithm to choose which side to
220 // take when calculating cell size by dividing this side by
221 // numRows or numCols correspondingly
222 // it's cooomplex, because I have to paint some figures on paper
223 // to understand that criteria for choosing one side or another (for
224 // determining cell size from it) is comparing
225 // cols/r.width() and rows/r.height():
226 bool chooseHorizontalSide = (m_numCols+2) / rect.width() > (m_numRows+2) / rect.height();
227
228 qreal size = 0;
229 if( chooseHorizontalSide )
230 size = rect.width() / (m_numCols+2);
231 else
232 size = rect.height() / (m_numRows+2);
233
234 m_cellSize = static_cast<int>(size);
235
236 foreach( CellItem* item, m_cells )
237 item->setRenderSize(QSize(m_cellSize, m_cellSize));
238
239 foreach( BorderItem *item, m_borders)
240 item->setRenderSize(QSize(m_cellSize, m_cellSize));
241
242 adjustItemPositions();
243}
244
245void MineFieldItem::adjustItemPositions()
246{
247 Q_ASSERT( m_cells.size() == m_numRows*m_numCols );
248
249 for(int row=0; row<m_numRows; ++row)
250 for(int col=0; col<m_numCols; ++col)
251 {
252 itemAt(row,col)->setPos((col+1)*m_cellSize, (row+1)*m_cellSize);
253 }
254
255 foreach( BorderItem* item, m_borders )
256 {
257 item->setPos( item->col()*m_cellSize, item->row()*m_cellSize );
258 }
259}
260
261void MineFieldItem::onItemRevealed(int row, int col)
262{
263 m_numUnrevealed--;
264 if(itemAt(row,col)->hasMine())
265 {
266 revealAllMines();
267 }
268 else if(itemAt(row,col)->digit() == 0) // empty cell
269 {
270 revealEmptySpace(row,col);
271 }
272 // now let's check for possible win/loss
273 checkLost();
274 if(!m_gameOver) // checkLost might set it
275 checkWon();
276}
277
278void MineFieldItem::revealEmptySpace(int row, int col)
279{
280 // recursively reveal neighbour cells until we find cells with digit
281 QList<FieldPos> list = adjasentRowColsFor(row,col);
282 CellItem *item = 0;
283
284 foreach( const FieldPos& pos, list )
285 {
286 // first is row, second is col
287 item = itemAt(pos);
288 if(item->isRevealed() || item->isFlagged() || item->isQuestioned())
289 continue;
290 if(item->digit() == 0)
291 {
292 item->reveal();
293 m_numUnrevealed--;
294 revealEmptySpace(pos.first,pos.second);
295 }
296 else
297 {
298 item->reveal();
299 m_numUnrevealed--;
300 }
301 }
302}
303
304void MineFieldItem::mousePressEvent( QGraphicsSceneMouseEvent *ev )
305{
306 if(m_gameOver)
307 return;
308
309 int row = static_cast<int>(ev->pos().y()/m_cellSize)-1;
310 int col = static_cast<int>(ev->pos().x()/m_cellSize)-1;
311 if( row <0 || row >= m_numRows || col < 0 || col >= m_numCols )
312 return;
313
314 CellItem* itemUnderMouse = itemAt(row,col);
315 if(!itemUnderMouse)
316 {
317 kDebug() << "unexpected - no item under mouse";
318 return;
319 }
320
321 m_emulatingMidButton = ( (ev->buttons() & Qt::LeftButton) && (ev->buttons() & Qt::RightButton) );
322 bool midButtonPressed = (ev->button() == Qt::MidButton || m_emulatingMidButton );
323
324 if(midButtonPressed)
325 {
326 // in case we just started mid-button emulation (first LeftClick then added a RightClick)
327 // undo press that was made by LeftClick. in other cases it won't hurt :)
328 itemUnderMouse->undoPress();
329
330 QList<CellItem*> neighbours = adjasentItemsFor(row,col);
331 foreach(CellItem* item, neighbours)
332 {
333 if(!item->isFlagged() && !item->isQuestioned() && !item->isRevealed())
334 item->press();
335 m_midButtonPos = qMakePair(row,col);
336
337 m_leftButtonPos = qMakePair(-1,-1); // reset it
338 }
339 }
340 else if(ev->button() == Qt::LeftButton)
341 {
342 itemUnderMouse->press();
343 m_leftButtonPos = qMakePair(row,col);
344 }
345}
346
347void MineFieldItem::mouseReleaseEvent( QGraphicsSceneMouseEvent * ev)
348{
349 if(m_gameOver)
350 return;
351
352 int row = static_cast<int>(ev->pos().y()/m_cellSize)-1;
353 int col = static_cast<int>(ev->pos().x()/m_cellSize)-1;
354
355 if( row <0 || row >= m_numRows || col < 0 || col >= m_numCols )
356 {
357 // there might be the case when player moved mouse outside game field
358 // while holding mid button and released it outside the field
359 // in this case we must unpress pressed buttons, let's do it here
360 // and return
361 if(m_midButtonPos.first != -1)
362 {
363 QList<CellItem*> neighbours = adjasentItemsFor(m_midButtonPos.first,m_midButtonPos.second);
364 foreach(CellItem *item, neighbours)
365 item->undoPress();
366 m_midButtonPos = qMakePair(-1,-1);
367 m_emulatingMidButton = false;
368 }
369 // same with left button
370 if(m_leftButtonPos.first != -1)
371 {
372 itemAt(m_leftButtonPos)->undoPress();
373 m_leftButtonPos = qMakePair(-1,-1);
374 }
375 return;
376 }
377
378 CellItem* itemUnderMouse = itemAt(row,col);
379
380 bool midButtonReleased = (ev->button() == Qt::MidButton || m_emulatingMidButton);
381
382 if( midButtonReleased )
383 {
384 m_midButtonPos = qMakePair(-1,-1);
385
386 QList<CellItem*> neighbours = adjasentItemsFor(row,col);
387 if(!itemUnderMouse->isRevealed())
388 {
389 foreach(CellItem *item, neighbours)
390 item->undoPress();
391 return;
392 }
393
394 int numFlags = 0;
395 int numMines = 0;
396 foreach(CellItem *item, neighbours)
397 {
398 if(item->isFlagged())
399 numFlags++;
400 if(item->hasMine())
401 numMines++;
402 }
403 if(numFlags == numMines && numFlags != 0)
404 {
405 foreach(CellItem *item, neighbours)
406 {
407 if(!item->isRevealed()) // revealing only unrevealed ones
408 {
409 // force=true to omit Pressed check
410 item->release(true);
411 if(item->isRevealed())
412 onItemRevealed(item);
413 }
414 }
415 }
416 else
417 {
418 foreach(CellItem *item, neighbours)
419 item->undoPress();
420 }
421 }
422 else if(ev->button() == Qt::LeftButton && (ev->buttons() & Qt::RightButton) == false)
423 {
424 if(m_midButtonPos.first != -1) // mid-button is already pressed
425 {
426 itemUnderMouse->undoPress();
427 return;
428 }
429
430 // this can happen like this:
431 // mid-button pressed, left-button pressed, mid-button released, left-button released
432 // m_leftButtonPos never gets set in this scenario, so we must protect ourselves :)
433 if(m_leftButtonPos.first == -1)
434 return;
435
436 if(!itemUnderMouse->isRevealed()) // revealing only unrevealed ones
437 {
438 if(m_firstClick)
439 {
440 m_firstClick = false;
441 generateField( row*m_numCols + col );
442 emit firstClickDone();
443 }
444
445 itemUnderMouse->release();
446 if(itemUnderMouse->isRevealed())
447 onItemRevealed(row,col);
448 }
449 m_leftButtonPos = qMakePair(-1,-1);//reset
450 }
451 else if(ev->button() == Qt::RightButton && (ev->buttons() & Qt::LeftButton) == false)
452 {
453 bool wasFlagged = itemUnderMouse->isFlagged();
454
455 itemUnderMouse->mark();
456
457 bool flagStateChanged = (itemUnderMouse->isFlagged() != wasFlagged);
458 if(flagStateChanged)
459 {
460 if(itemUnderMouse->isFlagged())
461 m_flaggedMinesCount++;
462 else
463 m_flaggedMinesCount--;
464 emit flaggedMinesCountChanged(m_flaggedMinesCount);
465 }
466 }
467}
468
469void MineFieldItem::mouseMoveEvent( QGraphicsSceneMouseEvent *ev )
470{
471 if(m_gameOver)
472 return;
473
474 int row = static_cast<int>(ev->pos().y()/m_cellSize)-1;
475 int col = static_cast<int>(ev->pos().x()/m_cellSize)-1;
476
477 if( row < 0 || row >= m_numRows || col < 0 || col >= m_numCols )
478 return;
479
480 bool midButtonPressed = ((ev->buttons() & Qt::MidButton) ||
481 ( (ev->buttons() & Qt::LeftButton) && (ev->buttons() & Qt::RightButton) ) );
482
483 if(midButtonPressed)
484 {
485 if((m_midButtonPos.first != -1 && m_midButtonPos.second != -1) &&
486 (m_midButtonPos.first != row || m_midButtonPos.second != col))
487 {
488 // un-press previously pressed cells
489 QList<CellItem*> prevNeighbours = adjasentItemsFor(m_midButtonPos.first,
490 m_midButtonPos.second);
491 foreach(CellItem *item, prevNeighbours)
492 item->undoPress();
493
494 // and press current neighbours
495 QList<CellItem*> neighbours = adjasentItemsFor(row,col);
496 foreach(CellItem *item, neighbours)
497 item->press();
498
499 m_midButtonPos = qMakePair(row,col);
500 }
501 }
502 else if(ev->buttons() & Qt::LeftButton)
503 {
504 if((m_leftButtonPos.first != -1 && m_leftButtonPos.second != -1) &&
505 (m_leftButtonPos.first != row || m_leftButtonPos.second != col))
506 {
507 itemAt(m_leftButtonPos)->undoPress();
508 itemAt(row,col)->press();
509 m_leftButtonPos = qMakePair(row,col);
510 }
511 }
512}
513
514void MineFieldItem::revealAllMines()
515{
516 foreach( CellItem* item, m_cells )
517 {
518 if( (item->isFlagged() && !item->hasMine()) || (!item->isFlagged() && item->hasMine()) )
519 {
520 item->reveal();
521 m_numUnrevealed--;
522 }
523 }
524}
525
526void MineFieldItem::onItemRevealed(CellItem* item)
527{
528 int idx = m_cells.indexOf(item);
529 if(idx == -1)
530 {
531 kDebug() << "really strange - item not found";
532 return;
533 }
534
535 int row = idx / m_numCols;
536 int col = idx - row*m_numCols;
537 onItemRevealed(row,col);
538}
539
540void MineFieldItem::checkLost()
541{
542 // for loss...
543 foreach( CellItem* item, m_cells )
544 {
545 if(item->isExploded())
546 {
547 m_gameOver = true;
548 emit gameOver(false);
549 break;
550 }
551 }
552}
553
554void MineFieldItem::checkWon()
555{
556 // this also takes into account the trivial case when
557 // only some cells left unflagged and they
558 // all contain bombs. this counts as win
559 if(m_numUnrevealed == m_minesCount)
560 {
561 // mark not flagged cells (if any) with flags
562 foreach( CellItem* item, m_cells )
563 {
564 if( !item->isRevealed() && !item->isFlagged() )
565 item->mark();
566 }
567 m_gameOver = true;
568 // now all mines should be flagged, notify about this
569 emit flaggedMinesCountChanged(m_minesCount);
570 emit gameOver(true);
571 }
572}
573
574QList<FieldPos> MineFieldItem::adjasentRowColsFor(int row, int col)
575{
576 QList<FieldPos> resultingList;
577 if(row != 0 && col != 0) // upper-left diagonal
578 resultingList.append( qMakePair(row-1,col-1) );
579 if(row != 0) // upper
580 resultingList.append(qMakePair(row-1, col));
581 if(row != 0 && col != m_numCols-1) // upper-right diagonal
582 resultingList.append(qMakePair(row-1, col+1));
583 if(col != 0) // on the left
584 resultingList.append(qMakePair(row,col-1));
585 if(col != m_numCols-1) // on the right
586 resultingList.append(qMakePair(row, col+1));
587 if(row != m_numRows-1 && col != 0) // bottom-left diagonal
588 resultingList.append(qMakePair(row+1, col-1));
589 if(row != m_numRows-1) // bottom
590 resultingList.append(qMakePair(row+1, col));
591 if(row != m_numRows-1 && col != m_numCols-1) // bottom-right diagonal
592 resultingList.append(qMakePair(row+1, col+1));
593 return resultingList;
594}
595
596QList<CellItem*> MineFieldItem::adjasentItemsFor(int row, int col)
597{
598 QList<FieldPos > rowcolList = adjasentRowColsFor(row,col);
599 QList<CellItem*> resultingList;
600 foreach( const FieldPos& pos, rowcolList )
601 resultingList.append( itemAt(pos) );
602 return resultingList;
603}
604
605
606
607#include "minefielditem.moc"
608