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 | |
28 | MineFieldItem::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 | |
35 | void 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 | |
98 | void 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 | |
143 | void 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 | |
200 | QRectF 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 | |
206 | void MineFieldItem::paint( QPainter * painter, const QStyleOptionGraphicsItem* opt, QWidget* w) |
207 | { |
208 | Q_UNUSED(painter); |
209 | Q_UNUSED(opt); |
210 | Q_UNUSED(w); |
211 | } |
212 | |
213 | void 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 | |
245 | void 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 | |
261 | void 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 | |
278 | void 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 | |
304 | void 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 | |
347 | void 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 | |
469 | void 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 | |
514 | void 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 | |
526 | void 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 | |
540 | void 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 | |
554 | void 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 | |
574 | QList<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 | |
596 | QList<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 | |