1 /*
2     SPDX-FileCopyrightText: 2008-2010 Stefan Majewsky <majewsky@gmx.net>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "board.h"
8 #include "diamond.h"
9 
10 #include <QPropertyAnimation>
11 #include <QRandomGenerator>
12 #include <KgDifficulty>
13 
14 const int KDiamond::Board::MoveDuration = 100; //duration of a move animation (per coordinate unit) in milliseconds
15 const int KDiamond::Board::RemoveDuration = 200; //duration of a move animation in milliseconds
16 
17 //NOTE: The corresponding difficulty values are {20, 30, 40, 50, 60} (see KgDifficultyLevel::StandardLevel).
18 static int boardSizes[] = { 12, 10, 8, 8, 8 };
19 static int boardColorCounts[] = { 5, 5, 5, 6, 7 };
20 
Board(KGameRenderer * renderer)21 KDiamond::Board::Board(KGameRenderer *renderer)
22     : m_difficultyIndex(Kg::difficultyLevel() / 10 - 2)
23     , m_size(boardSizes[m_difficultyIndex])
24     , m_colorCount(boardColorCounts[m_difficultyIndex])
25     , m_paused(false)
26     , m_renderer(renderer)
27     , m_diamonds(m_size *m_size, nullptr)
28 {
29     for (QPoint point; point.x() < m_size; ++point.rx())
30         for (point.ry() = 0; point.y() < m_size; ++point.ry()) {
31             //displacement vectors needed for the following algorithm
32             const QPoint dispY1(0, -1), dispY2(0, -2);
33             const QPoint dispX1(-1, 0), dispX2(-2, 0);
34             //roll the dice to get a color, but ensure that there are not three of a color in a row from the start
35             int color;
36             while (true) {
37                 color = QRandomGenerator::global()->bounded(1, m_colorCount + 1);
38                 //condition: no triplet in y axis (attention: only the diamonds above us are defined already)
39                 if (point.y() >= 2) { //no triplet possible for i = 0, 1
40                     const int otherColor1 = diamond(point + dispY1)->color();
41                     const int otherColor2 = diamond(point + dispY2)->color();
42                     if (otherColor1 == color && otherColor2 == color) {
43                         continue;    //roll the dice again
44                     }
45                 }
46                 //same condition on x axis
47                 if (point.x() >= 2) {
48                     const int otherColor1 = diamond(point + dispX1)->color();
49                     const int otherColor2 = diamond(point + dispX2)->color();
50                     if (otherColor1 == color && otherColor2 == color) {
51                         continue;
52                     }
53                 }
54                 break;
55             }
56             rDiamond(point) = spawnDiamond(color);
57             diamond(point)->setPos(point);
58         }
59 }
60 
spawnDiamond(int color)61 Diamond *KDiamond::Board::spawnDiamond(int color)
62 {
63     Diamond *diamond = new Diamond((KDiamond::Color) color, m_renderer, this);
64     connect(diamond, &Diamond::clicked, this, &Board::slotClicked);
65     connect(diamond, &Diamond::dragged, this, &Board::slotDragged);
66     return diamond;
67 }
68 
findDiamond(Diamond * diamond) const69 QPoint KDiamond::Board::findDiamond(Diamond *diamond) const
70 {
71     int index = m_diamonds.indexOf(diamond);
72     if (index == -1) {
73         return QPoint(-1, -1);
74     } else {
75         return QPoint(index % m_size, index / m_size);
76     }
77 }
78 
rDiamond(const QPoint & point)79 Diamond *&KDiamond::Board::rDiamond(const QPoint &point)
80 {
81     return m_diamonds[point.x() + point.y() * m_size];
82 }
83 
diamond(const QPoint & point) const84 Diamond *KDiamond::Board::diamond(const QPoint &point) const
85 {
86     return m_diamonds.value(point.x() + point.y() * m_size);
87 }
88 
gridSize() const89 int KDiamond::Board::gridSize() const
90 {
91     return m_size;
92 }
93 
hasDiamond(const QPoint & point) const94 bool KDiamond::Board::hasDiamond(const QPoint &point) const
95 {
96     return 0 <= point.x() && point.x() < m_size && 0 <= point.y() && point.y() < m_size;
97 }
98 
hasRunningAnimations() const99 bool KDiamond::Board::hasRunningAnimations() const
100 {
101     return !m_runningAnimations.isEmpty();
102 }
103 
slotAnimationFinished()104 void KDiamond::Board::slotAnimationFinished()
105 {
106     if (m_runningAnimations.isEmpty()) {
107         return;
108     }
109     //static_cast is enough, no need for a qobject_cast
110     //because result pointer is never dereferenced here
111     m_runningAnimations.removeAll(static_cast<QAbstractAnimation *>(sender()));
112     if (m_runningAnimations.isEmpty()) {
113         Q_EMIT animationsFinished();
114     }
115 }
116 
selections() const117 QList<QPoint> KDiamond::Board::selections() const
118 {
119     return m_selections;
120 }
121 
hasSelection(const QPoint & point) const122 bool KDiamond::Board::hasSelection(const QPoint &point) const
123 {
124     return m_selections.contains(point);
125 }
126 
setSelection(const QPoint & point,bool selected)127 void KDiamond::Board::setSelection(const QPoint &point, bool selected)
128 {
129     const int index = m_selections.indexOf(point);
130     if ((index >= 0) == selected)
131         //nothing to do
132     {
133         return;
134     }
135     if (selected) {
136         //add selection, possibly by reusing an old item instance
137         Diamond *selector;
138         if (!m_inactiveSelectors.isEmpty()) {
139             selector = m_inactiveSelectors.takeLast();
140         } else {
141             selector = new Diamond(KDiamond::Selection, m_renderer, this);
142         }
143         m_activeSelectors << selector;
144         m_selections << point;
145         selector->setPos(point);
146         selector->show();
147     } else {
148         //remove selection, but try to reuse item instance later
149         m_selections.removeAt(index);
150         Diamond *selector = m_activeSelectors.takeAt(index);
151         m_inactiveSelectors << selector;
152         selector->hide();
153     }
154 }
155 
clearSelection()156 void KDiamond::Board::clearSelection()
157 {
158     for (Diamond *selector : std::as_const(m_activeSelectors)) {
159         selector->hide();
160         m_inactiveSelectors << selector;
161     }
162     m_selections.clear();
163     m_activeSelectors.clear();
164 }
165 
setPaused(bool paused)166 void KDiamond::Board::setPaused(bool paused)
167 {
168     //During pauses, the board is hidden and any animations are suspended.
169     const bool visible = !paused;
170     if (isVisible() == visible) {
171         return;
172     }
173     setVisible(visible);
174     QList<QAbstractAnimation *>::const_iterator it1 = m_runningAnimations.constBegin(), it2 = m_runningAnimations.constEnd();
175     for (; it1 != it2; ++it1) {
176         (*it1)->setPaused(paused);
177     }
178 }
179 
removeDiamond(const QPoint & point)180 void KDiamond::Board::removeDiamond(const QPoint &point)
181 {
182     Diamond *diamond = this->diamond(point);
183     if (!diamond) {
184         return;    //diamond has already been removed
185     }
186     rDiamond(point) = nullptr;
187     //play remove animation (TODO: For non-animated sprites, play an opacity animation instead.)
188     QPropertyAnimation *animation = new QPropertyAnimation(diamond, "frame", this);
189     animation->setStartValue(0);
190     animation->setEndValue(diamond->frameCount() - 1);
191     animation->setDuration(KDiamond::Board::RemoveDuration);
192     animation->start(QAbstractAnimation::DeleteWhenStopped);
193     connect(animation, &QPropertyAnimation::finished, this, &Board::slotAnimationFinished);
194     connect(animation, &QPropertyAnimation::finished, diamond, &Diamond::deleteLater);
195     m_runningAnimations << animation;
196 }
197 
spawnMoveAnimations(const QList<MoveAnimSpec> & specs)198 void KDiamond::Board::spawnMoveAnimations(const QList<MoveAnimSpec> &specs)
199 {
200     for (const MoveAnimSpec &spec : specs) {
201         const int duration = KDiamond::Board::MoveDuration * (spec.to - spec.from).manhattanLength();
202         QPropertyAnimation *animation = new QPropertyAnimation(spec.diamond, "pos", this);
203         animation->setStartValue(spec.from);
204         animation->setEndValue(spec.to);
205         animation->setDuration(duration);
206         animation->start(QAbstractAnimation::DeleteWhenStopped);
207         connect(animation, &QPropertyAnimation::finished, this, &Board::slotAnimationFinished);
208         m_runningAnimations << animation;
209     }
210 }
211 
swapDiamonds(const QPoint & point1,const QPoint & point2)212 void KDiamond::Board::swapDiamonds(const QPoint &point1, const QPoint &point2)
213 {
214     //swap diamonds in internal representation
215     Diamond *diamond1 = this->diamond(point1);
216     Diamond *diamond2 = this->diamond(point2);
217     rDiamond(point1) = diamond2;
218     rDiamond(point2) = diamond1;
219     //play movement animations
220     const MoveAnimSpec spec1 = { diamond1, point1, point2 };
221     const MoveAnimSpec spec2 = { diamond2, point2, point1 };
222     spawnMoveAnimations(QList<MoveAnimSpec>() << spec1 << spec2);
223 }
224 
fillGaps()225 void KDiamond::Board::fillGaps()
226 {
227     QList<MoveAnimSpec> specs;
228     //fill gaps
229     int x, y, yt; //counters - (x, yt) is the target position of diamond (x,y)
230     for (x = 0; x < m_size; ++x) {
231         //We have to search from the bottom of the column. Exclude the lowest element (x = m_size - 1) because it cannot move down.
232         for (y = m_size - 2; y >= 0; --y) {
233             if (!diamond(QPoint(x, y)))
234                 //no need to move gaps -> these are moved later
235             {
236                 continue;
237             }
238             if (diamond(QPoint(x, y + 1)))
239                 //there is something right below this diamond -> Do not move.
240             {
241                 continue;
242             }
243             //search for the lowest possible position
244             for (yt = y; yt < m_size - 1; ++yt) {
245                 if (diamond(QPoint(x, yt + 1))) {
246                     break;    //xt now holds the lowest possible position
247                 }
248             }
249             rDiamond(QPoint(x, yt)) = diamond(QPoint(x, y));
250             rDiamond(QPoint(x, y)) = nullptr;
251             const MoveAnimSpec spec = { diamond(QPoint(x, yt)), QPoint(x, y), QPoint(x, yt) };
252             specs << spec;
253             //if this element is selected, move the selection, too
254             const int index = m_selections.indexOf(QPoint(x, y));
255             if (index != -1) {
256                 m_selections.replace(index, QPoint(x, yt));
257                 const MoveAnimSpec spec = { m_activeSelectors[index], QPoint(x, y), QPoint(x, yt) };
258                 specs << spec;
259             }
260         }
261     }
262     //fill top rows with new elements
263     for (x = 0; x < m_size; ++x) {
264         yt = 0; //now: holds the position from where the diamond comes (-1 for the lowest new diamond)
265         for (y = m_size - 1; y >= 0; --y) {
266             Diamond *&diamond = this->rDiamond(QPoint(x, y));
267             if (diamond) {
268                 continue;    //inside of diamond stack - no gaps to fill
269             }
270             --yt;
271             const quint32 randValue = QRandomGenerator::global()->bounded(1, m_colorCount + 1); //high value is excluse
272             diamond = spawnDiamond(randValue);
273             diamond->setPos(QPoint(x, yt));
274             const MoveAnimSpec spec = { diamond, QPoint(x, yt), QPoint(x, y) };
275             specs << spec;
276         }
277     }
278     spawnMoveAnimations(specs);
279 }
280 
renderer() const281 KGameRenderer *KDiamond::Board::renderer() const
282 {
283     return m_renderer;
284 }
285 
slotClicked()286 void KDiamond::Board::slotClicked()
287 {
288     const QPoint point = findDiamond(qobject_cast<Diamond *>(sender()));
289     if (point.x() >= 0 && point.y() >= 0) {
290         Q_EMIT clicked(point);
291     }
292 }
293 
slotDragged(const QPoint & direction)294 void KDiamond::Board::slotDragged(const QPoint &direction)
295 {
296     const QPoint point = findDiamond(qobject_cast<Diamond *>(sender()));
297     if (point.x() >= 0 && point.y() >= 0) {
298         Q_EMIT dragged(point, direction);
299     }
300 }
301 
boundingRect() const302 QRectF KDiamond::Board::boundingRect() const
303 {
304     return QRectF();
305 }
306 
paint(QPainter * painter,const QStyleOptionGraphicsItem * option,QWidget * widget)307 void KDiamond::Board::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
308 {
309     Q_UNUSED(painter) Q_UNUSED(option) Q_UNUSED(widget)
310 }
311 
312