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 { return _currentFigure; }
194 
195     private FigurePosition _nextFigure;
196     /// next figure
197     @property ref FigurePosition nextFigure() return { 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