1 module model;
2
3 import std.random : uniform;
4
5 /// Cell codes
6 enum : int {
7 WALL = -1,
8 EMPTY = 0,
9 FIGURE1,
10 FIGURE2,
11 FIGURE3,
12 FIGURE4,
13 FIGURE5,
14 FIGURE6,
15 FIGURE7,
16 }
17
18 /// Orientations
19 enum : int {
20 ORIENTATION0,
21 ORIENTATION90,
22 ORIENTATION180,
23 ORIENTATION270
24 }
25
26
27 /// Cell offset
28 struct FigureCell {
29 // horizontal offset
30 int dx;
31 // vertical offset
32 int dy;
33 this(int[2] v) {
34 dx = v[0];
35 dy = v[1];
36 }
37 }
38
39 /// Single figure shape for some particular orientation - 4 cells
40 struct FigureShape {
41 /// by cell index 0..3
42 FigureCell[4] cells;
43 /// lowest y coordinate - to show next figure above cup
44 int extent;
45 /// upper y coordinate - initial Y offset to place figure to cup
46 int y0;
47 /// Init cells (cell 0 is [0,0])
48 this(int[2] c2, int[2] c3, int[2] c4) {
49 cells[0] = FigureCell([0, 0]);
50 cells[1] = FigureCell(c2);
51 cells[2] = FigureCell(c3);
52 cells[3] = FigureCell(c4);
53 extent = y0 = 0;
54 foreach (cell; cells) {
55 if (extent > cell.dy)
56 extent = cell.dy;
57 if (y0 < cell.dy)
58 y0 = cell.dy;
59 }
60 }
61 }
62
63 /// Figure data - shapes for 4 orientations
64 struct Figure {
65 FigureShape[4] shapes; // by orientation
66 this(FigureShape[4] v) {
67 shapes = v;
68 }
69 }
70
71 /// All shapes
72 __gshared const Figure[7] FIGURES;
73
74 // workaround for dmd 2.0.67 - move initialization to static this
75 __gshared static this() {
76 FIGURES = [
77 // FIGURE1 ===========================================
78 // ## ####
79 // 00## 00##
80 // ##
81 Figure([FigureShape([1, 0], [ 1, 1], [0,-1]),
82 FigureShape([0, 1], [-1, 1], [1, 0]),
83 FigureShape([1, 0], [ 1, 1], [0,-1]),
84 FigureShape([0, 1], [-1, 1], [1, 0])]),
85 // FIGURE2 ===========================================
86 // ## ####
87 // 00## ##00
88 // ##
89 Figure([FigureShape([1, 0], [0, 1], [ 1,-1]),
90 FigureShape([0, 1], [1, 1], [-1, 0]),
91 FigureShape([1, 0], [0, 1], [ 1,-1]),
92 FigureShape([0, 1], [1, 1], [-1, 0])]),
93 // FIGURE3 ===========================================
94 // ## ## ####
95 // ##00## 00 ##00## 00
96 // ## #### ##
97 Figure([FigureShape([1, 0], [-1, 0], [-1,-1]),
98 FigureShape([0, 1], [ 0,-1], [ 1,-1]),
99 FigureShape([1, 0], [-1, 0], [ 1, 1]),
100 FigureShape([0, 1], [-1, 1], [ 0,-1])]),
101 // FIGURE4 ===========================================
102 // #### ## ##
103 // ##00## 00 ##00## 00
104 // ## ## ####
105 Figure([FigureShape([1, 0], [-1, 0], [ 1,-1]),
106 FigureShape([0, 1], [ 0,-1], [ 1, 1]),
107 FigureShape([1, 0], [-1, 0], [-1, 1]),
108 FigureShape([0, 1], [-1,-1], [ 0,-1])]),
109 // FIGURE5 ===========================================
110 // ####
111 // 00##
112 //
113 Figure([FigureShape([1, 0], [0, 1], [ 1, 1]),
114 FigureShape([1, 0], [0, 1], [ 1, 1]),
115 FigureShape([1, 0], [0, 1], [ 1, 1]),
116 FigureShape([1, 0], [0, 1], [ 1, 1])]),
117 // FIGURE6 ===========================================
118 // ##
119 // ##
120 // 00 ##00####
121 // ##
122 Figure([FigureShape([0, 1], [0, 2], [ 0,-1]),
123 FigureShape([1, 0], [2, 0], [-1, 0]),
124 FigureShape([0, 1], [0, 2], [ 0,-1]),
125 FigureShape([1, 0], [2, 0], [-1, 0])]),
126 // FIGURE7 ===========================================
127 // ## ## ##
128 // ##00## 00## ##00## ##00
129 // ## ## ##
130 Figure([FigureShape([1, 0], [-1,0], [ 0,-1]),
131 FigureShape([0, 1], [0,-1], [ 1, 0]),
132 FigureShape([1, 0], [-1,0], [ 0, 1]),
133 FigureShape([0, 1], [0,-1], [-1, 0])]),
134 ];
135 }
136
137 /// colors for different figure types
138 const uint[7] _figureColors = [0xC00000, 0x80A000, 0xA00080, 0x0000C0, 0x800020, 0x408000, 0x204000];
139
140 /// Figure type, orientation and position container
141 struct FigurePosition {
142 int index;
143 int orientation;
144 int x;
145 int y;
146 this(int index, int orientation, int x, int y) {
147 this.index = index;
148 this.orientation = orientation;
149 this.x = x;
150 this.y = y;
151 }
152 /// return rotated position CCW for angle=1, CW for angle=-1
153 FigurePosition rotate(int angle) {
154 int newOrientation = (orientation + 4 + angle) & 3;
155 return FigurePosition(index, newOrientation, x, y);
156 }
157 /// return moved position
158 FigurePosition move(int dx, int dy = 0) {
159 return FigurePosition(index, orientation, x + dx, y + dy);
160 }
161 /// return shape for figure orientation
162 @property FigureShape shape() const {
163 return FIGURES[index - 1].shapes[orientation];
164 }
165 /// return color for figure
166 @property uint color() const {
167 return _figureColors[index - 1];
168 }
169 /// return true if figure index is not initialized
170 @property empty() const {
171 return index == 0;
172 }
173 /// clears content
174 void reset() {
175 index = 0;
176 }
177 }
178
179 /**
180 Cup content
181
182 Coordinates are relative to bottom left corner.
183 */
184 struct Cup {
185 private int[] _cup;
186 private int _cols;
187 private int _rows;
188 private bool[] _destroyedFullRows;
189 private int[] _cellGroups;
190
191 private FigurePosition _currentFigure;
192 /// current figure index, orientation, position
193 @property ref FigurePosition currentFigure() { return _currentFigure; }
194
195 private FigurePosition _nextFigure;
196 /// next figure
197 @property ref FigurePosition nextFigure() { return _nextFigure; }
198
199 /// returns number of columns
200 @property int cols() {
201 return _cols;
202 }
203 /// returns number of columns
204 @property int rows() {
205 return _rows;
206 }
207 /// inits empty cup of specified size
208 void initialize(int cols, int rows) {
209 _cols = cols;
210 _rows = rows;
211 _cup = new int[_cols * _rows];
212 _destroyedFullRows = new bool[_rows];
213 _cellGroups = new int[_cols * _rows];
214 }
215 /// returns cell content at specified position
216 int opIndex(int col, int row) {
217 if (col < 0 || row < 0 || col >= _cols || row >= _rows)
218 return WALL;
219 return _cup[row * _cols + col];
220 }
221 /// set cell value
222 void opIndexAssign(int value, int col, int row) {
223 if (col < 0 || row < 0 || col >= _cols || row >= _rows)
224 return; // ignore modification of cells outside cup
225 _cup[row * _cols + col] = value;
226 }
227 /// put current figure into cup at current position and orientation
228 void putFigure() {
229 FigureShape shape = _currentFigure.shape;
230 foreach(cell; shape.cells) {
231 this[_currentFigure.x + cell.dx, _currentFigure.y + cell.dy] = _currentFigure.index;
232 }
233 }
234
235 /// check if all cells where specified figure is located are free
236 bool isPositionFree(in FigurePosition pos) {
237 FigureShape shape = pos.shape;
238 foreach(cell; shape.cells) {
239 int value = this[pos.x + cell.dx, pos.y + cell.dy];
240 if (value != 0) // occupied
241 return false;
242 }
243 return true;
244 }
245 /// returns true if specified row is full
246 bool isRowFull(int row) {
247 for (int i = 0; i < _cols; i++)
248 if (this[i, row] == EMPTY)
249 return false;
250 return true;
251 }
252 /// returns true if at least one row is full
253 @property bool hasFullRows() {
254 for (int i = 0; i < _rows; i++)
255 if (isRowFull(i))
256 return true;
257 return false;
258 }
259 /// destroy all full rows, saving flags for destroyed rows; returns count of destroyed rows, 0 if no rows destroyed
260 int destroyFullRows() {
261 int res = 0;
262 for (int i = 0; i < _rows; i++) {
263 if (isRowFull(i)) {
264 _destroyedFullRows[i] = true;
265 res++;
266 for (int col = 0; col < _cols; col++)
267 this[col, i] = EMPTY;
268 } else {
269 _destroyedFullRows[i] = false;
270 }
271 }
272 return res;
273 }
274
275 /// check if all cells where current figire is located are free
276 bool isPositionFree() {
277 return isPositionFree(_currentFigure);
278 }
279
280 /// check if all cells where current figire is located are free
281 bool isPositionFreeBelow() {
282 return isPositionFree(_currentFigure.move(0, -1));
283 }
284
285 /// try to rotate current figure, returns true if figure rotated
286 bool rotate(int angle, bool falling) {
287 FigurePosition newpos = _currentFigure.rotate(angle);
288 if (isPositionFree(newpos)) {
289 if (falling) {
290 // special handling for fall animation
291 if (!isPositionFree(newpos.move(0, -1))) {
292 if (isPositionFreeBelow())
293 return false;
294 }
295 }
296 _currentFigure = newpos;
297 return true;
298 } else if (isPositionFree(newpos.move(0, -1))) {
299 _currentFigure = newpos.move(0, -1);
300 return true;
301 }
302 return false;
303 }
304
305 /// try to move current figure, returns true if figure rotated
306 bool move(int deltaX, int deltaY, bool falling) {
307 FigurePosition newpos = _currentFigure.move(deltaX, deltaY);
308 if (isPositionFree(newpos)) {
309 if (falling && !isPositionFree(newpos.move(0, -1))) {
310 if (isPositionFreeBelow())
311 return false;
312 }
313 _currentFigure = newpos;
314 return true;
315 }
316 return false;
317 }
318
319 /// random next figure
320 void genNextFigure() {
321 _nextFigure.index = uniform(FIGURE1, FIGURE7 + 1);
322 _nextFigure.orientation = ORIENTATION0;
323 _nextFigure.x = _cols / 2;
324 _nextFigure.y = _rows - _nextFigure.shape.extent + 1;
325 }
326
327 /// New figure: put it on top of cup
328 bool dropNextFigure() {
329 if (_nextFigure.empty)
330 genNextFigure();
331 _currentFigure = _nextFigure;
332 _currentFigure.x = _cols / 2;
333 _currentFigure.y = _rows - 1 - _currentFigure.shape.y0;
334 return isPositionFree();
335 }
336
337 /// get cell group / falling cell value
338 private int cellGroup(int col, int row) {
339 if (col < 0 || row < 0 || col >= _cols || row >= _rows)
340 return 0;
341 return _cellGroups[col + row * _cols];
342 }
343
344 /// set cell group / falling cells value
345 private void setCellGroup(int value, int col, int row) {
346 _cellGroups[col + row * _cols] = value;
347 }
348
349 /// recursive fill occupied area of cells with group id
350 private void fillCellGroup(int x, int y, int value) {
351 if (x < 0 || y < 0 || x >= _cols || y >= _rows)
352 return;
353 if (this[x, y] != EMPTY && cellGroup(x, y) == 0) {
354 setCellGroup(value, x, y);
355 fillCellGroup(x + 1, y, value);
356 fillCellGroup(x - 1, y, value);
357 fillCellGroup(x, y + 1, value);
358 fillCellGroup(x, y - 1, value);
359 }
360 }
361
362 /// 1 == next cell below is occupied, 2 == one empty cell
363 private int distanceToOccupiedCellBelow(int col, int row) {
364 for (int y = row - 1; y >= -1; y--) {
365 if (this[col, y] != EMPTY)
366 return row - y;
367 }
368 return 1;
369 }
370
371 /// 1 == next cell below is occupied, 2 == one empty cell
372 private int distanceToOccupiedCellBelowForGroup(int group) {
373 int minDistanceFound = 0;
374 for (int y = 0; y < _rows; y++) {
375 for (int x = 0; x < _cols; x++) {
376 if (cellGroup(x, y) != group)
377 continue;
378 if (y == 0)
379 return 1; // right below
380 if (this[x, y - 1] != EMPTY) // check only lowest cell of group
381 continue;
382 int dist = 0;
383 for (int d = 1; y - d >= 0; d++) {
384 if (this[x, y - d] == EMPTY) {
385 dist = d + 1;
386 } else {
387 // reached non-empty cell
388 if (cellGroup(x, y - d) == group) {
389 // non-empty cell of the same group after empty space - ignore
390 dist = 0;
391 }
392 break;
393 }
394 }
395 if (dist > 0) {
396 // found some empty space below
397 if (minDistanceFound == 0 || minDistanceFound > dist)
398 minDistanceFound = dist;
399 }
400 }
401 }
402 if (minDistanceFound == 0)
403 return 1;
404 return minDistanceFound;
405 }
406
407 /// mark cells in _cellGroups[] matrix which can fall down (value > 0 is distance to fall)
408 bool markFallingCells() {
409 // clear cellGroups matrix
410 if (_cellGroups.length != _cols * _rows) {
411 _cellGroups = new int[_cols * _rows];
412 } else {
413 foreach(ref cell; _cellGroups)
414 cell = 0;
415 }
416 // find and mark all groups
417 int groupId = 1;
418 for (int y = 0; y < _rows; y++) {
419 for (int x = 0; x < _cols; x++) {
420 if (this[x, y] != EMPTY && cellGroup(x, y) == 0) {
421 fillCellGroup(x, y, groupId);
422 groupId++;
423 }
424 }
425 }
426 // check space below each group - can it fall down?
427 int[] spaceBelowGroup = new int[groupId];
428 static if (true) {
429 for (int i = 1; i < groupId; i++)
430 spaceBelowGroup[i] = distanceToOccupiedCellBelowForGroup(i);
431 } else {
432 for (int y = 0; y < _rows; y++) {
433 for (int x = 0; x < _cols; x++) {
434 int group = cellGroup(x, y);
435 if (group > 0) {
436 if (y == 0)
437 spaceBelowGroup[group] = 1;
438 else if (this[x, y - 1] != EMPTY && cellGroup(x, y - 1) != group)
439 spaceBelowGroup[group] = 1;
440 else if (this[x, y - 1] == EMPTY) {
441 int dist = distanceToOccupiedCellBelow(x, y);
442 if (spaceBelowGroup[group] == 0 || spaceBelowGroup[group] > dist)
443 spaceBelowGroup[group] = dist;
444 }
445 }
446 }
447 }
448 }
449 // replace group IDs with distance to fall (0 == cell cannot fall)
450 for (int y = 0; y < _rows; y++) {
451 for (int x = 0; x < _cols; x++) {
452 int group = cellGroup(x, y);
453 if (group > 0) {
454 // distance to fall
455 setCellGroup(spaceBelowGroup[group] - 1, x, y);
456 }
457 }
458 }
459 bool canFall = false;
460 for (int i = 1; i < groupId; i++)
461 if (spaceBelowGroup[i] > 1)
462 canFall = true;
463 return canFall;
464 }
465
466 /// moves all falling cells one cell down
467 /// returns true if there are more cells to fall
468 bool moveFallingCells() {
469 bool res = false;
470 for (int y = 0; y < _rows - 1; y++) {
471 for (int x = 0; x < _cols; x++) {
472 int dist = cellGroup(x, y + 1);
473 if (dist > 0) {
474 // move cell down, decreasing distance
475 setCellGroup(dist - 1, x, y);
476 this[x, y] = this[x, y + 1];
477 setCellGroup(0, x, y + 1);
478 this[x, y + 1] = EMPTY;
479 if (dist > 1)
480 res = true;
481 }
482 }
483 }
484 return res;
485 }
486
487 /// return true if cell is currently falling
488 bool isCellFalling(int col, int row) {
489 return cellGroup(col, row) > 0;
490 }
491
492 /// returns true if next figure is generated
493 @property bool hasNextFigure() {
494 return !_nextFigure.empty;
495 }
496 }
497