File indexing completed on 2024-04-28 04:02:29
0001 /* 0002 SPDX-FileCopyrightText: 2007 Dmitry Suzdalev <dimsuz@gmail.com> 0003 SPDX-FileCopyrightText: 2010 Brian Croom <brian.s.croom@gmail.com> 0004 0005 SPDX-License-Identifier: GPL-2.0-or-later 0006 */ 0007 0008 #include "minefielditem.h" 0009 0010 // own 0011 #include "kmines_debug.h" 0012 #include "cellitem.h" 0013 #include "borderitem.h" 0014 #include "settings.h" 0015 // Qt 0016 #include <QGraphicsScene> 0017 #include <QGraphicsSceneMouseEvent> 0018 #include <QRandomGenerator> 0019 0020 MineFieldItem::MineFieldItem(KGameRenderer* renderer) 0021 : m_leftButtonPos(-1,-1), m_midButtonPos(-1,-1), m_gameOver(false), 0022 m_emulatingMidButton(false), m_renderer(renderer) 0023 { 0024 setFlag(QGraphicsItem::ItemHasNoContents); 0025 } 0026 0027 void MineFieldItem::resetMines() 0028 { 0029 m_gameOver = false; 0030 m_numUnrevealed = m_numRows*m_numCols; 0031 0032 for(CellItem* item : std::as_const(m_cells)) { 0033 item->unreveal(); 0034 item->unflag(); 0035 item->unexplode(); 0036 } 0037 0038 m_flaggedMinesCount = 0; 0039 Q_EMIT flaggedMinesCountChanged(m_flaggedMinesCount); 0040 } 0041 0042 0043 void MineFieldItem::initField( int numRows, int numCols, int numMines ) 0044 { 0045 numMines = qMin(numMines, numRows*numCols - MINIMAL_FREE ); 0046 0047 m_firstClick = true; 0048 m_gameOver = false; 0049 0050 int oldSize = m_cells.size(); 0051 int newSize = numRows*numCols; 0052 int oldBorderSize = m_borders.size(); 0053 int newBorderSize = (numCols+2)*2 + (numRows+2)*2-4; 0054 0055 // if field is being shrunk, delete elements at the end before resizing vector 0056 if(oldSize > newSize) 0057 { 0058 for( int i=newSize; i<oldSize; ++i ) 0059 { 0060 // is this the best way to remove an item? 0061 scene()->removeItem(m_cells[i]); 0062 delete m_cells[i]; 0063 } 0064 0065 // adjust border item array too 0066 for( int i=newBorderSize; i<oldBorderSize; ++i) 0067 { 0068 scene()->removeItem(m_borders[i]); 0069 delete m_borders[i]; 0070 } 0071 } 0072 0073 m_cells.resize(newSize); 0074 m_borders.resize(newBorderSize); 0075 0076 m_numRows = numRows; 0077 m_numCols = numCols; 0078 m_minesCount = numMines; 0079 m_numUnrevealed = m_numRows*m_numCols; 0080 m_midButtonPos = qMakePair(-1, -1); 0081 m_leftButtonPos = qMakePair(-1, -1); 0082 0083 for(int i=0; i<newSize; ++i) 0084 { 0085 // reset old, create new 0086 if(i<oldSize) 0087 m_cells[i]->reset(); 0088 else 0089 m_cells[i] = new CellItem(m_renderer, this); 0090 // let it be empty by default 0091 // generateField() will adjust needed cells 0092 // to hold digits or mines 0093 m_cells[i]->setDigit(0); 0094 } 0095 0096 for(int i=oldBorderSize; i<newBorderSize; ++i) 0097 m_borders[i] = new BorderItem(m_renderer, this); 0098 0099 setupBorderItems(); 0100 0101 adjustItemPositions(); 0102 m_flaggedMinesCount = 0; 0103 Q_EMIT flaggedMinesCountChanged(m_flaggedMinesCount); 0104 } 0105 0106 void MineFieldItem::generateField(int clickedIdx) 0107 { 0108 // generating mines ensuring that clickedIdx won't hold mine 0109 // and that it will be an empty cell so the user don't have 0110 // to make random guesses at the start of the game 0111 QList<int> cellsWithMines; 0112 int minesToPlace = m_minesCount; 0113 int randomIdx = 0; 0114 CellItem* item = nullptr; 0115 FieldPos fp = rowColFromIndex(clickedIdx); 0116 0117 // this is the list of items we don't want to put the mine in 0118 // to ensure that clickedIdx will stay an empty cell 0119 // (it will be empty if none of surrounding items holds mine) 0120 QList<CellItem*> neighbForClicked = adjacentItemsFor(fp.first, fp.second); 0121 0122 QRandomGenerator random(QRandomGenerator::global()->generate()); 0123 while(minesToPlace != 0) 0124 { 0125 randomIdx = random.bounded( m_numRows*m_numCols ); 0126 item = m_cells.at(randomIdx); 0127 if(!item->hasMine() 0128 && neighbForClicked.indexOf(item) == -1 0129 && randomIdx != clickedIdx) 0130 { 0131 // ok, let's mine this place! :-) 0132 item->setHasMine(true); 0133 cellsWithMines.append(randomIdx); 0134 minesToPlace--; 0135 } 0136 else 0137 continue; 0138 } 0139 0140 for (int idx : std::as_const(cellsWithMines)) { 0141 FieldPos rc = rowColFromIndex(idx); 0142 const QList<CellItem*> neighbours = adjacentItemsFor(rc.first, rc.second); 0143 for (CellItem *item : neighbours) { 0144 if(!item->hasMine()) 0145 item->setDigit( item->digit()+1 ); 0146 } 0147 } 0148 } 0149 0150 void MineFieldItem::setupBorderItems() 0151 { 0152 int i = 0; 0153 for(int row=0; row<m_numRows+2; ++row) 0154 for(int col=0; col<m_numCols+2; ++col) 0155 { 0156 if( row == 0 && col == 0) 0157 { 0158 m_borders.at(i)->setRowCol(0,0); 0159 m_borders.at(i)->setBorderType(KMinesState::BorderCornerNW); 0160 i++; 0161 } 0162 else if( row == 0 && col == m_numCols+1) 0163 { 0164 m_borders.at(i)->setRowCol(row,col); 0165 m_borders.at(i)->setBorderType(KMinesState::BorderCornerNE); 0166 i++; 0167 } 0168 else if( row == m_numRows+1 && col == 0 ) 0169 { 0170 m_borders.at(i)->setRowCol(row,col); 0171 m_borders.at(i)->setBorderType(KMinesState::BorderCornerSW); 0172 i++; 0173 } 0174 else if( row == m_numRows+1 && col == m_numCols+1 ) 0175 { 0176 m_borders.at(i)->setRowCol(row,col); 0177 m_borders.at(i)->setBorderType(KMinesState::BorderCornerSE); 0178 i++; 0179 } 0180 else if( row == 0 ) 0181 { 0182 m_borders.at(i)->setRowCol(row,col); 0183 m_borders.at(i)->setBorderType(KMinesState::BorderNorth); 0184 i++; 0185 } 0186 else if( row == m_numRows+1 ) 0187 { 0188 m_borders.at(i)->setRowCol(row,col); 0189 m_borders.at(i)->setBorderType(KMinesState::BorderSouth); 0190 i++; 0191 } 0192 else if( col == 0 ) 0193 { 0194 m_borders.at(i)->setRowCol(row,col); 0195 m_borders.at(i)->setBorderType(KMinesState::BorderWest); 0196 i++; 0197 } 0198 else if( col == m_numCols+1 ) 0199 { 0200 m_borders.at(i)->setRowCol(row,col); 0201 m_borders.at(i)->setBorderType(KMinesState::BorderEast); 0202 i++; 0203 } 0204 } 0205 } 0206 0207 QRectF MineFieldItem::boundingRect() const 0208 { 0209 // +2 - because of border on each side 0210 return QRectF(0, 0, m_cellSize*(m_numCols+2), m_cellSize*(m_numRows+2)); 0211 } 0212 0213 int MineFieldItem::rowCount() const 0214 { 0215 return m_numRows; 0216 } 0217 0218 int MineFieldItem::columnCount() const 0219 { 0220 return m_numCols; 0221 } 0222 0223 int MineFieldItem::minesCount() const 0224 { 0225 return m_minesCount; 0226 } 0227 0228 void MineFieldItem::paint( QPainter * painter, const QStyleOptionGraphicsItem* opt, QWidget* w) 0229 { 0230 Q_UNUSED(painter); 0231 Q_UNUSED(opt); 0232 Q_UNUSED(w); 0233 } 0234 0235 void MineFieldItem::resizeToFitInRect(const QRectF& rect) 0236 { 0237 prepareGeometryChange(); 0238 0239 // +2 in some places - because of border on each side 0240 0241 // here follows "cooomplex" algorithm to choose which side to 0242 // take when calculating cell size by dividing this side by 0243 // numRows or numCols correspondingly 0244 // it's cooomplex, because I have to paint some figures on paper 0245 // to understand that criteria for choosing one side or another (for 0246 // determining cell size from it) is comparing 0247 // cols/r.width() and rows/r.height(): 0248 bool chooseHorizontalSide = (m_numCols+2) / rect.width() > (m_numRows+2) / rect.height(); 0249 0250 qreal size = 0; 0251 if( chooseHorizontalSide ) 0252 size = rect.width() / (m_numCols+2); 0253 else 0254 size = rect.height() / (m_numRows+2); 0255 0256 m_cellSize = static_cast<int>(size); 0257 0258 for (CellItem* item : std::as_const(m_cells)) { 0259 item->setRenderSize(QSize(m_cellSize, m_cellSize)); 0260 } 0261 0262 for (BorderItem *item : std::as_const(m_borders)) { 0263 item->setRenderSize(QSize(m_cellSize, m_cellSize)); 0264 } 0265 0266 adjustItemPositions(); 0267 } 0268 0269 void MineFieldItem::adjustItemPositions() 0270 { 0271 Q_ASSERT( m_cells.size() == m_numRows*m_numCols ); 0272 0273 for(int row=0; row<m_numRows; ++row) 0274 for(int col=0; col<m_numCols; ++col) 0275 { 0276 itemAt(row,col)->setPos((col+1)*m_cellSize, (row+1)*m_cellSize); 0277 } 0278 0279 for (BorderItem* item : std::as_const(m_borders)) { 0280 item->setPos( item->col()*m_cellSize, item->row()*m_cellSize ); 0281 } 0282 } 0283 0284 bool MineFieldItem::onItemRevealed(int row, int col) 0285 { 0286 m_numUnrevealed--; 0287 if(itemAt(row,col)->hasMine()) 0288 { 0289 revealAllMines(); 0290 } 0291 else if(itemAt(row,col)->digit() == 0) // empty cell 0292 { 0293 revealEmptySpace(row,col); 0294 } 0295 // now let's check for possible win/loss 0296 if(checkLost()) 0297 return true; 0298 return checkWon(); 0299 } 0300 0301 void MineFieldItem::revealEmptySpace(int row, int col) 0302 { 0303 // recursively reveal neighbour cells until we find cells with digit 0304 const QList<FieldPos> list = adjacentRowColsFor(row,col); 0305 CellItem *item = nullptr; 0306 0307 for (const FieldPos& pos : list) { 0308 // first is row, second is col 0309 item = itemAt(pos); 0310 if(item->isRevealed() || item->isFlagged() || item->isQuestioned()) 0311 continue; 0312 if(item->digit() == 0) 0313 { 0314 item->reveal(); 0315 m_numUnrevealed--; 0316 revealEmptySpace(pos.first,pos.second); 0317 } 0318 else 0319 { 0320 item->reveal(); 0321 m_numUnrevealed--; 0322 } 0323 } 0324 } 0325 0326 void MineFieldItem::handleFlag(CellItem* itemUnderMouse) 0327 { 0328 bool wasFlagged = itemUnderMouse->isFlagged(); 0329 0330 itemUnderMouse->mark(); 0331 0332 bool flagStateChanged = (itemUnderMouse->isFlagged() != wasFlagged); 0333 if(flagStateChanged) 0334 { 0335 if(itemUnderMouse->isFlagged()) 0336 m_flaggedMinesCount++; 0337 else 0338 m_flaggedMinesCount--; 0339 Q_EMIT flaggedMinesCountChanged(m_flaggedMinesCount); 0340 } 0341 } 0342 0343 void MineFieldItem::mousePressEvent( QGraphicsSceneMouseEvent *ev ) 0344 { 0345 if(m_gameOver) 0346 return; 0347 0348 int row = static_cast<int>(ev->pos().y()/m_cellSize)-1; 0349 int col = static_cast<int>(ev->pos().x()/m_cellSize)-1; 0350 if( row <0 || row >= m_numRows || col < 0 || col >= m_numCols ) 0351 return; 0352 0353 CellItem* itemUnderMouse = itemAt(row,col); 0354 if(!itemUnderMouse) 0355 { 0356 qCDebug(KMINES_LOG) << "unexpected - no item under mouse"; 0357 return; 0358 } 0359 0360 bool useFastExplore = Settings::exploreWithLeftClickOnNumberCells(); 0361 bool placeFlagWhenPressed = Settings::placeFlagOn() == Settings::EnumPlaceFlagOn::MousePress; 0362 m_emulatingMidButton = ( useFastExplore ? ( (ev->buttons() & Qt::LeftButton) && ( itemUnderMouse->isRevealed() ) ) : ( (ev->buttons() & Qt::LeftButton) && (ev->buttons() & Qt::RightButton) ) ); 0363 bool midButtonPressed = (ev->button() == Qt::MiddleButton || m_emulatingMidButton ); 0364 0365 if(midButtonPressed) 0366 { 0367 // in case we just started mid-button emulation (first LeftClick then added a RightClick) 0368 // undo press that was made by LeftClick. in other cases it won't hurt :) 0369 itemUnderMouse->undoPress(); 0370 0371 const QList<CellItem*> neighbours = adjacentItemsFor(row,col); 0372 for (CellItem* item : neighbours) { 0373 if(!item->isFlagged() && !item->isQuestioned() && !item->isRevealed()) 0374 item->press(); 0375 m_midButtonPos = qMakePair(row,col); 0376 0377 m_leftButtonPos = qMakePair(-1,-1); // reset it 0378 } 0379 } 0380 else if(ev->button() == Qt::LeftButton) 0381 { 0382 itemUnderMouse->press(); 0383 m_leftButtonPos = qMakePair(row,col); 0384 } 0385 else if(placeFlagWhenPressed && ev->button() == Qt::RightButton && (ev->buttons() & Qt::LeftButton) == false) 0386 { 0387 handleFlag(itemUnderMouse); 0388 } 0389 } 0390 0391 void MineFieldItem::mouseReleaseEvent( QGraphicsSceneMouseEvent * ev) 0392 { 0393 if(m_gameOver) 0394 return; 0395 0396 int row = static_cast<int>(ev->pos().y()/m_cellSize)-1; 0397 int col = static_cast<int>(ev->pos().x()/m_cellSize)-1; 0398 0399 if( row <0 || row >= m_numRows || col < 0 || col >= m_numCols ) 0400 { 0401 // there might be the case when player moved mouse outside game field 0402 // while holding mid button and released it outside the field 0403 // in this case we must unpress pressed buttons, let's do it here 0404 // and return 0405 if(m_midButtonPos.first != -1) 0406 { 0407 const QList<CellItem*> neighbours = adjacentItemsFor(m_midButtonPos.first,m_midButtonPos.second); 0408 for (CellItem *item : neighbours) { 0409 item->undoPress(); 0410 } 0411 m_midButtonPos = qMakePair(-1,-1); 0412 m_emulatingMidButton = false; 0413 } 0414 // same with left button 0415 if(m_leftButtonPos.first != -1) 0416 { 0417 itemAt(m_leftButtonPos)->undoPress(); 0418 m_leftButtonPos = qMakePair(-1,-1); 0419 } 0420 return; 0421 } 0422 0423 CellItem* itemUnderMouse = itemAt(row,col); 0424 0425 bool placeFlagWhenReleased = Settings::placeFlagOn() == Settings::EnumPlaceFlagOn::MouseRelease; 0426 bool midButtonReleased = (ev->button() == Qt::MiddleButton || m_emulatingMidButton); 0427 0428 if( midButtonReleased ) 0429 { 0430 m_midButtonPos = qMakePair(-1,-1); 0431 0432 const QList<CellItem*> neighbours = adjacentItemsFor(row,col); 0433 if(!itemUnderMouse->isRevealed()) 0434 { 0435 for (CellItem *item : neighbours) { 0436 item->undoPress(); 0437 } 0438 return; 0439 } 0440 0441 int numFlags = 0; 0442 int numMines = 0; 0443 for (CellItem *item : neighbours) { 0444 if(item->isFlagged()) 0445 numFlags++; 0446 if(item->hasMine()) 0447 numMines++; 0448 } 0449 if(numFlags == numMines && numFlags != 0) 0450 { 0451 for (CellItem *item : neighbours) { 0452 if(!item->isRevealed()) // revealing only unrevealed ones 0453 { 0454 // force=true to omit Pressed check 0455 item->release(true); 0456 // If revealing the item ends the game, stop the loop, 0457 // since everything that needs to be done for the current game is finished. 0458 // Otherwise, if the user has restarted the game, we'll be revealing 0459 // items for the new game. 0460 if(item->isRevealed() && onItemRevealed(item)) 0461 break; 0462 } 0463 } 0464 } 0465 else 0466 { 0467 for (CellItem *item : neighbours) { 0468 item->undoPress(); 0469 } 0470 } 0471 } 0472 else if(ev->button() == Qt::LeftButton && (ev->buttons() & Qt::RightButton) == false) 0473 { 0474 if(m_midButtonPos.first != -1) // mid-button is already pressed 0475 { 0476 itemUnderMouse->undoPress(); 0477 return; 0478 } 0479 0480 // this can happen like this: 0481 // mid-button pressed, left-button pressed, mid-button released, left-button released 0482 // m_leftButtonPos never gets set in this scenario, so we must protect ourselves :) 0483 if(m_leftButtonPos.first == -1) 0484 return; 0485 0486 if(!itemUnderMouse->isRevealed()) // revealing only unrevealed ones 0487 { 0488 if(m_firstClick) 0489 { 0490 m_firstClick = false; 0491 generateField( row*m_numCols + col ); 0492 Q_EMIT firstClickDone(); 0493 } 0494 0495 itemUnderMouse->release(); 0496 if(itemUnderMouse->isRevealed()) 0497 onItemRevealed(row,col); 0498 } 0499 m_leftButtonPos = qMakePair(-1,-1);//reset 0500 } 0501 else if(placeFlagWhenReleased && ev->button() == Qt::RightButton && (ev->buttons() & Qt::LeftButton) == false) 0502 { 0503 handleFlag(itemUnderMouse); 0504 } 0505 } 0506 0507 void MineFieldItem::mouseMoveEvent( QGraphicsSceneMouseEvent *ev ) 0508 { 0509 if(m_gameOver) 0510 return; 0511 0512 int row = static_cast<int>(ev->pos().y()/m_cellSize)-1; 0513 int col = static_cast<int>(ev->pos().x()/m_cellSize)-1; 0514 0515 if( row < 0 || row >= m_numRows || col < 0 || col >= m_numCols ) 0516 return; 0517 0518 bool midButtonPressed = ((ev->buttons() & Qt::MiddleButton) || 0519 ( (ev->buttons() & Qt::LeftButton) && (ev->buttons() & Qt::RightButton) ) ); 0520 0521 if(midButtonPressed) 0522 { 0523 if((m_midButtonPos.first != -1 && m_midButtonPos.second != -1) && 0524 (m_midButtonPos.first != row || m_midButtonPos.second != col)) 0525 { 0526 // un-press previously pressed cells 0527 const QList<CellItem*> prevNeighbours = adjacentItemsFor(m_midButtonPos.first, 0528 m_midButtonPos.second); 0529 for (CellItem *item : prevNeighbours) { 0530 item->undoPress(); 0531 } 0532 0533 // and press current neighbours 0534 const QList<CellItem*> neighbours = adjacentItemsFor(row,col); 0535 for (CellItem *item : neighbours) { 0536 item->press(); 0537 } 0538 0539 m_midButtonPos = qMakePair(row,col); 0540 } 0541 } 0542 else if(ev->buttons() & Qt::LeftButton) 0543 { 0544 if((m_leftButtonPos.first != -1 && m_leftButtonPos.second != -1) && 0545 (m_leftButtonPos.first != row || m_leftButtonPos.second != col)) 0546 { 0547 itemAt(m_leftButtonPos)->undoPress(); 0548 itemAt(row,col)->press(); 0549 m_leftButtonPos = qMakePair(row,col); 0550 } 0551 } 0552 } 0553 0554 void MineFieldItem::revealAllMines() 0555 { 0556 for (CellItem* item : std::as_const(m_cells)) { 0557 if( (item->isFlagged() && !item->hasMine()) || (!item->isFlagged() && item->hasMine()) ) 0558 { 0559 item->reveal(); 0560 m_numUnrevealed--; 0561 } 0562 } 0563 } 0564 0565 bool MineFieldItem::onItemRevealed(CellItem* item) 0566 { 0567 int idx = m_cells.indexOf(item); 0568 if(idx == -1) 0569 { 0570 qCDebug(KMINES_LOG) << "really strange - item not found"; 0571 return false; 0572 } 0573 0574 int row = idx / m_numCols; 0575 int col = idx - row*m_numCols; 0576 return onItemRevealed(row,col); 0577 } 0578 0579 bool MineFieldItem::checkLost() 0580 { 0581 // for loss... 0582 for (CellItem* item : std::as_const(m_cells)) { 0583 if(item->isExploded()) 0584 { 0585 m_gameOver = true; 0586 Q_EMIT gameOver(false); 0587 return true; 0588 } 0589 } 0590 return false; 0591 } 0592 0593 bool MineFieldItem::checkWon() 0594 { 0595 // this also takes into account the trivial case when 0596 // only some cells left unflagged and they 0597 // all contain bombs. this counts as win 0598 if(m_numUnrevealed == m_minesCount) 0599 { 0600 // mark not flagged cells (if any) with flags 0601 for (CellItem* item : std::as_const(m_cells)) { 0602 if( item->isQuestioned() ) 0603 item->mark(); 0604 if( !item->isRevealed() && !item->isFlagged() ) 0605 item->mark(); 0606 } 0607 m_gameOver = true; 0608 // now all mines should be flagged, notify about this 0609 Q_EMIT flaggedMinesCountChanged(m_minesCount); 0610 Q_EMIT gameOver(true); 0611 return true; 0612 } 0613 return false; 0614 } 0615 0616 QList<FieldPos> MineFieldItem::adjacentRowColsFor(int row, int col) 0617 { 0618 QList<FieldPos> resultingList; 0619 if(row != 0 && col != 0) // upper-left diagonal 0620 resultingList.append( qMakePair(row-1,col-1) ); 0621 if(row != 0) // upper 0622 resultingList.append(qMakePair(row-1, col)); 0623 if(row != 0 && col != m_numCols-1) // upper-right diagonal 0624 resultingList.append(qMakePair(row-1, col+1)); 0625 if(col != 0) // on the left 0626 resultingList.append(qMakePair(row,col-1)); 0627 if(col != m_numCols-1) // on the right 0628 resultingList.append(qMakePair(row, col+1)); 0629 if(row != m_numRows-1 && col != 0) // bottom-left diagonal 0630 resultingList.append(qMakePair(row+1, col-1)); 0631 if(row != m_numRows-1) // bottom 0632 resultingList.append(qMakePair(row+1, col)); 0633 if(row != m_numRows-1 && col != m_numCols-1) // bottom-right diagonal 0634 resultingList.append(qMakePair(row+1, col+1)); 0635 return resultingList; 0636 } 0637 0638 QList<CellItem*> MineFieldItem::adjacentItemsFor(int row, int col) 0639 { 0640 const QList<FieldPos > rowcolList = adjacentRowColsFor(row,col); 0641 QList<CellItem*> resultingList; 0642 for (const FieldPos& pos : rowcolList) { 0643 resultingList.append( itemAt(pos) ); 0644 } 0645 return resultingList; 0646 } 0647 0648 #include "moc_minefielditem.cpp"