File indexing completed on 2024-04-28 07:51:31

0001 /*
0002     SPDX-FileCopyrightText: 2008-2010 Stefan Majewsky <majewsky@gmx.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "board.h"
0008 #include "diamond.h"
0009 
0010 #include <QPropertyAnimation>
0011 #include <QRandomGenerator>
0012 #include <KGameDifficulty>
0013 
0014 const int KDiamond::Board::MoveDuration = 100; //duration of a move animation (per coordinate unit) in milliseconds
0015 const int KDiamond::Board::RemoveDuration = 200; //duration of a move animation in milliseconds
0016 
0017 //NOTE: The corresponding difficulty values are {20, 30, 40, 50, 60} (see KGameDifficultyLevel::StandardLevel).
0018 static int boardSizes[] = { 12, 10, 8, 8, 8 };
0019 static int boardColorCounts[] = { 5, 5, 5, 6, 7 };
0020 
0021 KDiamond::Board::Board(KGameGraphicsViewRenderer *renderer)
0022     : m_difficultyIndex(KGameDifficulty::globalLevel() / 10 - 2)
0023     , m_size(boardSizes[m_difficultyIndex])
0024     , m_colorCount(boardColorCounts[m_difficultyIndex])
0025     , m_paused(false)
0026     , m_renderer(renderer)
0027     , m_diamonds(m_size *m_size, nullptr)
0028 {
0029     for (QPoint point; point.x() < m_size; ++point.rx())
0030         for (point.ry() = 0; point.y() < m_size; ++point.ry()) {
0031             //displacement vectors needed for the following algorithm
0032             const QPoint dispY1(0, -1), dispY2(0, -2);
0033             const QPoint dispX1(-1, 0), dispX2(-2, 0);
0034             //roll the dice to get a color, but ensure that there are not three of a color in a row from the start
0035             int color;
0036             while (true) {
0037                 color = QRandomGenerator::global()->bounded(1, m_colorCount + 1);
0038                 //condition: no triplet in y axis (attention: only the diamonds above us are defined already)
0039                 if (point.y() >= 2) { //no triplet possible for i = 0, 1
0040                     const int otherColor1 = diamond(point + dispY1)->color();
0041                     const int otherColor2 = diamond(point + dispY2)->color();
0042                     if (otherColor1 == color && otherColor2 == color) {
0043                         continue;    //roll the dice again
0044                     }
0045                 }
0046                 //same condition on x axis
0047                 if (point.x() >= 2) {
0048                     const int otherColor1 = diamond(point + dispX1)->color();
0049                     const int otherColor2 = diamond(point + dispX2)->color();
0050                     if (otherColor1 == color && otherColor2 == color) {
0051                         continue;
0052                     }
0053                 }
0054                 break;
0055             }
0056             rDiamond(point) = spawnDiamond(color);
0057             diamond(point)->setPos(point);
0058         }
0059 }
0060 
0061 Diamond *KDiamond::Board::spawnDiamond(int color)
0062 {
0063     Diamond *diamond = new Diamond((KDiamond::Color) color, m_renderer, this);
0064     connect(diamond, &Diamond::clicked, this, &Board::slotClicked);
0065     connect(diamond, &Diamond::dragged, this, &Board::slotDragged);
0066     return diamond;
0067 }
0068 
0069 QPoint KDiamond::Board::findDiamond(Diamond *diamond) const
0070 {
0071     int index = m_diamonds.indexOf(diamond);
0072     if (index == -1) {
0073         return QPoint(-1, -1);
0074     } else {
0075         return QPoint(index % m_size, index / m_size);
0076     }
0077 }
0078 
0079 Diamond *&KDiamond::Board::rDiamond(const QPoint &point)
0080 {
0081     return m_diamonds[point.x() + point.y() * m_size];
0082 }
0083 
0084 Diamond *KDiamond::Board::diamond(const QPoint &point) const
0085 {
0086     return m_diamonds.value(point.x() + point.y() * m_size);
0087 }
0088 
0089 int KDiamond::Board::gridSize() const
0090 {
0091     return m_size;
0092 }
0093 
0094 bool KDiamond::Board::hasDiamond(const QPoint &point) const
0095 {
0096     return 0 <= point.x() && point.x() < m_size && 0 <= point.y() && point.y() < m_size;
0097 }
0098 
0099 bool KDiamond::Board::hasRunningAnimations() const
0100 {
0101     return !m_runningAnimations.isEmpty();
0102 }
0103 
0104 void KDiamond::Board::slotAnimationFinished()
0105 {
0106     if (m_runningAnimations.isEmpty()) {
0107         return;
0108     }
0109     //static_cast is enough, no need for a qobject_cast
0110     //because result pointer is never dereferenced here
0111     m_runningAnimations.removeAll(static_cast<QAbstractAnimation *>(sender()));
0112     if (m_runningAnimations.isEmpty()) {
0113         Q_EMIT animationsFinished();
0114     }
0115 }
0116 
0117 QList<QPoint> KDiamond::Board::selections() const
0118 {
0119     return m_selections;
0120 }
0121 
0122 bool KDiamond::Board::hasSelection(const QPoint &point) const
0123 {
0124     return m_selections.contains(point);
0125 }
0126 
0127 void KDiamond::Board::setSelection(const QPoint &point, bool selected)
0128 {
0129     const int index = m_selections.indexOf(point);
0130     if ((index >= 0) == selected)
0131         //nothing to do
0132     {
0133         return;
0134     }
0135     if (selected) {
0136         //add selection, possibly by reusing an old item instance
0137         Diamond *selector;
0138         if (!m_inactiveSelectors.isEmpty()) {
0139             selector = m_inactiveSelectors.takeLast();
0140         } else {
0141             selector = new Diamond(KDiamond::Selection, m_renderer, this);
0142         }
0143         m_activeSelectors << selector;
0144         m_selections << point;
0145         selector->setPos(point);
0146         selector->show();
0147     } else {
0148         //remove selection, but try to reuse item instance later
0149         m_selections.removeAt(index);
0150         Diamond *selector = m_activeSelectors.takeAt(index);
0151         m_inactiveSelectors << selector;
0152         selector->hide();
0153     }
0154 }
0155 
0156 void KDiamond::Board::clearSelection()
0157 {
0158     for (Diamond *selector : std::as_const(m_activeSelectors)) {
0159         selector->hide();
0160         m_inactiveSelectors << selector;
0161     }
0162     m_selections.clear();
0163     m_activeSelectors.clear();
0164 }
0165 
0166 void KDiamond::Board::setPaused(bool paused)
0167 {
0168     //During pauses, the board is hidden and any animations are suspended.
0169     const bool visible = !paused;
0170     if (isVisible() == visible) {
0171         return;
0172     }
0173     setVisible(visible);
0174     QList<QAbstractAnimation *>::const_iterator it1 = m_runningAnimations.constBegin(), it2 = m_runningAnimations.constEnd();
0175     for (; it1 != it2; ++it1) {
0176         (*it1)->setPaused(paused);
0177     }
0178 }
0179 
0180 void KDiamond::Board::removeDiamond(const QPoint &point)
0181 {
0182     Diamond *diamond = this->diamond(point);
0183     if (!diamond) {
0184         return;    //diamond has already been removed
0185     }
0186     rDiamond(point) = nullptr;
0187     //play remove animation (TODO: For non-animated sprites, play an opacity animation instead.)
0188     QPropertyAnimation *animation = new QPropertyAnimation(diamond, "frame", this);
0189     animation->setStartValue(0);
0190     animation->setEndValue(diamond->frameCount() - 1);
0191     animation->setDuration(KDiamond::Board::RemoveDuration);
0192     animation->start(QAbstractAnimation::DeleteWhenStopped);
0193     connect(animation, &QPropertyAnimation::finished, this, &Board::slotAnimationFinished);
0194     connect(animation, &QPropertyAnimation::finished, diamond, &Diamond::deleteLater);
0195     m_runningAnimations << animation;
0196 }
0197 
0198 void KDiamond::Board::spawnMoveAnimations(const QList<MoveAnimSpec> &specs)
0199 {
0200     for (const MoveAnimSpec &spec : specs) {
0201         const int duration = KDiamond::Board::MoveDuration * (spec.to - spec.from).manhattanLength();
0202         QPropertyAnimation *animation = new QPropertyAnimation(spec.diamond, "pos", this);
0203         animation->setStartValue(spec.from);
0204         animation->setEndValue(spec.to);
0205         animation->setDuration(duration);
0206         animation->start(QAbstractAnimation::DeleteWhenStopped);
0207         connect(animation, &QPropertyAnimation::finished, this, &Board::slotAnimationFinished);
0208         m_runningAnimations << animation;
0209     }
0210 }
0211 
0212 void KDiamond::Board::swapDiamonds(const QPoint &point1, const QPoint &point2)
0213 {
0214     //swap diamonds in internal representation
0215     Diamond *diamond1 = this->diamond(point1);
0216     Diamond *diamond2 = this->diamond(point2);
0217     rDiamond(point1) = diamond2;
0218     rDiamond(point2) = diamond1;
0219     //play movement animations
0220     const MoveAnimSpec spec1 = { diamond1, point1, point2 };
0221     const MoveAnimSpec spec2 = { diamond2, point2, point1 };
0222     spawnMoveAnimations(QList<MoveAnimSpec>() << spec1 << spec2);
0223 }
0224 
0225 void KDiamond::Board::fillGaps()
0226 {
0227     QList<MoveAnimSpec> specs;
0228     //fill gaps
0229     int x, y, yt; //counters - (x, yt) is the target position of diamond (x,y)
0230     for (x = 0; x < m_size; ++x) {
0231         //We have to search from the bottom of the column. Exclude the lowest element (x = m_size - 1) because it cannot move down.
0232         for (y = m_size - 2; y >= 0; --y) {
0233             if (!diamond(QPoint(x, y)))
0234                 //no need to move gaps -> these are moved later
0235             {
0236                 continue;
0237             }
0238             if (diamond(QPoint(x, y + 1)))
0239                 //there is something right below this diamond -> Do not move.
0240             {
0241                 continue;
0242             }
0243             //search for the lowest possible position
0244             for (yt = y; yt < m_size - 1; ++yt) {
0245                 if (diamond(QPoint(x, yt + 1))) {
0246                     break;    //xt now holds the lowest possible position
0247                 }
0248             }
0249             rDiamond(QPoint(x, yt)) = diamond(QPoint(x, y));
0250             rDiamond(QPoint(x, y)) = nullptr;
0251             const MoveAnimSpec spec = { diamond(QPoint(x, yt)), QPoint(x, y), QPoint(x, yt) };
0252             specs << spec;
0253             //if this element is selected, move the selection, too
0254             const int index = m_selections.indexOf(QPoint(x, y));
0255             if (index != -1) {
0256                 m_selections.replace(index, QPoint(x, yt));
0257                 const MoveAnimSpec spec = { m_activeSelectors[index], QPoint(x, y), QPoint(x, yt) };
0258                 specs << spec;
0259             }
0260         }
0261     }
0262     //fill top rows with new elements
0263     for (x = 0; x < m_size; ++x) {
0264         yt = 0; //now: holds the position from where the diamond comes (-1 for the lowest new diamond)
0265         for (y = m_size - 1; y >= 0; --y) {
0266             Diamond *&diamond = this->rDiamond(QPoint(x, y));
0267             if (diamond) {
0268                 continue;    //inside of diamond stack - no gaps to fill
0269             }
0270             --yt;
0271             const quint32 randValue = QRandomGenerator::global()->bounded(1, m_colorCount + 1); //high value is excluse
0272             diamond = spawnDiamond(randValue);
0273             diamond->setPos(QPoint(x, yt));
0274             const MoveAnimSpec spec = { diamond, QPoint(x, yt), QPoint(x, y) };
0275             specs << spec;
0276         }
0277     }
0278     spawnMoveAnimations(specs);
0279 }
0280 
0281 KGameGraphicsViewRenderer *KDiamond::Board::renderer() const
0282 {
0283     return m_renderer;
0284 }
0285 
0286 void KDiamond::Board::slotClicked()
0287 {
0288     const QPoint point = findDiamond(qobject_cast<Diamond *>(sender()));
0289     if (point.x() >= 0 && point.y() >= 0) {
0290         Q_EMIT clicked(point);
0291     }
0292 }
0293 
0294 void KDiamond::Board::slotDragged(const QPoint &direction)
0295 {
0296     const QPoint point = findDiamond(qobject_cast<Diamond *>(sender()));
0297     if (point.x() >= 0 && point.y() >= 0) {
0298         Q_EMIT dragged(point, direction);
0299     }
0300 }
0301 
0302 QRectF KDiamond::Board::boundingRect() const
0303 {
0304     return QRectF();
0305 }
0306 
0307 void KDiamond::Board::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
0308 {
0309     Q_UNUSED(painter) Q_UNUSED(option) Q_UNUSED(widget)
0310 }
0311 
0312 #include "moc_board.cpp"