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"