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