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 const Figure[7] FIGURES = [
73     // FIGURE1 ===========================================
74     //   ##     ####
75     // 00##       00##
76     // ##       
77     Figure([FigureShape([1, 0], [ 1, 1], [0,-1]),
78             FigureShape([0, 1], [-1, 1], [1, 0]),
79             FigureShape([1, 0], [ 1, 1], [0,-1]),
80             FigureShape([0, 1], [-1, 1], [1, 0])]),
81     // FIGURE2 ===========================================
82     // ##         ####
83     // 00##     ##00
84     //   ##     
85     Figure([FigureShape([1, 0], [0, 1], [ 1,-1]),
86             FigureShape([0, 1], [1, 1], [-1, 0]),
87             FigureShape([1, 0], [0, 1], [ 1,-1]),
88             FigureShape([0, 1], [1, 1], [-1, 0])]),
89     // FIGURE3 ===========================================
90     //            ##        ##      ####
91     // ##00##     00    ##00##        00
92     // ##         ####                ##
93     Figure([FigureShape([1, 0], [-1, 0], [-1,-1]),
94             FigureShape([0, 1], [ 0,-1], [ 1,-1]),
95             FigureShape([1, 0], [-1, 0], [ 1, 1]),
96             FigureShape([0, 1], [-1, 1], [ 0,-1])]),
97     // FIGURE4 ===========================================
98     //            ####  ##            ##
99     // ##00##     00    ##00##        00
100     //     ##     ##                ####
101     Figure([FigureShape([1, 0], [-1, 0], [ 1,-1]),
102             FigureShape([0, 1], [ 0,-1], [ 1, 1]),
103             FigureShape([1, 0], [-1, 0], [-1, 1]),
104             FigureShape([0, 1], [-1,-1], [ 0,-1])]),
105     // FIGURE5 ===========================================
106     //   ####
107     //   00##
108     //       
109     Figure([FigureShape([1, 0], [0, 1], [ 1, 1]),
110             FigureShape([1, 0], [0, 1], [ 1, 1]),
111             FigureShape([1, 0], [0, 1], [ 1, 1]),
112             FigureShape([1, 0], [0, 1], [ 1, 1])]),
113     // FIGURE6 ===========================================
114     //   ##
115     //   ##
116     //   00     ##00####
117     //   ##    
118     Figure([FigureShape([0, 1], [0, 2], [ 0,-1]),
119             FigureShape([1, 0], [2, 0], [-1, 0]),
120             FigureShape([0, 1], [0, 2], [ 0,-1]),
121             FigureShape([1, 0], [2, 0], [-1, 0])]),
122     // FIGURE7 ===========================================
123     //            ##      ##          ##
124     // ##00##     00##  ##00##      ##00
125     //   ##       ##                  ##
126     Figure([FigureShape([1, 0], [-1,0], [ 0,-1]),
127             FigureShape([0, 1], [0,-1], [ 1, 0]),
128             FigureShape([1, 0], [-1,0], [ 0, 1]),
129             FigureShape([0, 1], [0,-1], [-1, 0])]),
130 ];
131 
132 /// colors for different figure types
133 const uint[7] _figureColors = [0xC00000, 0x80A000, 0xA00080, 0x0000C0, 0x800020, 0x408000, 0x204000];
134 
135 /// Figure type, orientation and position container
136 struct FigurePosition {
137     int index;
138     int orientation;
139     int x;
140     int y;
141     this(int index, int orientation, int x, int y) {
142         this.index = index;
143         this.orientation = orientation;
144         this.x = x;
145         this.y = y;
146     }
147     /// return rotated position CCW for angle=1, CW for angle=-1
148     FigurePosition rotate(int angle) {
149         int newOrientation = (orientation + 4 + angle) & 3;
150         return FigurePosition(index, newOrientation, x, y);
151     }
152     /// return moved position
153     FigurePosition move(int dx, int dy = 0) {
154         return FigurePosition(index, orientation, x + dx, y + dy);
155     }
156     /// return shape for figure orientation
157     @property FigureShape shape() const {
158         return FIGURES[index - 1].shapes[orientation];
159     }
160     /// return color for figure
161     @property uint color() const {
162         return _figureColors[index - 1];
163     }
164     /// return true if figure index is not initialized
165     @property empty() const {
166         return index == 0;
167     }
168     /// clears content
169     void reset() {
170         index = 0;
171     }
172 }
173 
174 /** 
175 Cup content
176 
177 Coordinates are relative to bottom left corner.
178 */
179 struct Cup {
180     private int[] _cup;
181     private int _cols;
182     private int _rows;
183     private bool[] _destroyedFullRows;
184     private int[]  _cellGroups;
185 
186     private FigurePosition _currentFigure;
187     /// current figure index, orientation, position
188     @property ref FigurePosition currentFigure() { return _currentFigure; }
189 
190     private FigurePosition _nextFigure;
191     /// next figure
192     @property ref FigurePosition nextFigure() { return _nextFigure; }
193 
194     /// returns number of columns
195     @property int cols() {
196         return _cols;
197     }
198     /// returns number of columns
199     @property int rows() {
200         return _rows;
201     }
202     /// inits empty cup of specified size
203     void init(int cols, int rows) {
204         _cols = cols;
205         _rows = rows;
206         _cup = new int[_cols * _rows];
207         _destroyedFullRows = new bool[_rows];
208         _cellGroups = new int[_cols * _rows];
209     }
210     /// returns cell content at specified position
211     int opIndex(int col, int row) {
212         if (col < 0 || row < 0 || col >= _cols || row >= _rows)
213             return WALL;
214         return _cup[row * _cols + col];
215     }
216     /// set cell value
217     void opIndexAssign(int value, int col, int row) {
218         if (col < 0 || row < 0 || col >= _cols || row >= _rows)
219             return; // ignore modification of cells outside cup
220         _cup[row * _cols + col] = value;
221     }
222     /// put current figure into cup at current position and orientation
223     void putFigure() {
224         FigureShape shape = _currentFigure.shape;
225         foreach(cell; shape.cells) {
226             this[_currentFigure.x + cell.dx, _currentFigure.y + cell.dy] = _currentFigure.index;
227         }
228     }
229 
230     /// check if all cells where specified figure is located are free
231     bool isPositionFree(in FigurePosition pos) {
232         FigureShape shape = pos.shape;
233         foreach(cell; shape.cells) {
234             int value = this[pos.x + cell.dx, pos.y + cell.dy];
235             if (value != 0) // occupied
236                 return false;
237         }
238         return true;
239     }
240     /// returns true if specified row is full
241     bool isRowFull(int row) {
242         for (int i = 0; i < _cols; i++)
243             if (this[i, row] == EMPTY)
244                 return false;
245         return true;
246     }
247     /// returns true if at least one row is full
248     @property bool hasFullRows() {
249         for (int i = 0; i < _rows; i++)
250             if (isRowFull(i))
251                 return true;
252         return false;
253     }
254     /// destroy all full rows, saving flags for destroyed rows; returns count of destroyed rows, 0 if no rows destroyed
255     int destroyFullRows() {
256         int res = 0;
257         for (int i = 0; i < _rows; i++) {
258             if (isRowFull(i)) {
259                 _destroyedFullRows[i] = true;
260                 res++;
261                 for (int col = 0; col < _cols; col++)
262                     this[col, i] = EMPTY;
263             } else {
264                 _destroyedFullRows[i] = false;
265             }
266         }
267         return res;
268     }
269 
270     /// check if all cells where current figire is located are free
271     bool isPositionFree() {
272         return isPositionFree(_currentFigure);
273     }
274 
275     /// check if all cells where current figire is located are free
276     bool isPositionFreeBelow() {
277         return isPositionFree(_currentFigure.move(0, -1));
278     }
279 
280     /// try to rotate current figure, returns true if figure rotated
281     bool rotate(int angle, bool falling) {
282         FigurePosition newpos = _currentFigure.rotate(angle);
283         if (isPositionFree(newpos)) {
284             if (falling) {
285                 // special handling for fall animation
286                 if (!isPositionFree(newpos.move(0, -1))) {
287                     if (isPositionFreeBelow())
288                         return false;
289                 }
290             }
291             _currentFigure = newpos;
292             return true;
293         } else if (isPositionFree(newpos.move(0, -1))) {
294             _currentFigure = newpos.move(0, -1);
295             return true;
296         }
297         return false;
298     }
299 
300     /// try to move current figure, returns true if figure rotated
301     bool move(int deltaX, int deltaY, bool falling) {
302         FigurePosition newpos = _currentFigure.move(deltaX, deltaY);
303         if (isPositionFree(newpos)) {
304             if (falling && !isPositionFree(newpos.move(0, -1))) {
305                 if (isPositionFreeBelow())
306                     return false;
307             }
308             _currentFigure = newpos;
309             return true;
310         }
311         return false;
312     }
313 
314     /// random next figure
315     void genNextFigure() {
316         _nextFigure.index = uniform(FIGURE1, FIGURE7 + 1);
317         _nextFigure.orientation = ORIENTATION0;
318         _nextFigure.x = _cols / 2;
319         _nextFigure.y = _rows - _nextFigure.shape.extent + 1;
320     }
321 
322     /// New figure: put it on top of cup
323     bool dropNextFigure() {
324         if (_nextFigure.empty)
325             genNextFigure();
326         _currentFigure = _nextFigure;
327         _currentFigure.x = _cols / 2;
328         _currentFigure.y = _rows - 1 - _currentFigure.shape.y0;
329         return isPositionFree();
330     }
331 
332     /// get cell group / falling cell value
333     private int cellGroup(int col, int row) {
334         if (col < 0 || row < 0 || col >= _cols || row >= _rows)
335             return 0;
336         return _cellGroups[col + row * _cols];
337     }
338 
339     /// set cell group / falling cells value
340     private void setCellGroup(int value, int col, int row) {
341         _cellGroups[col + row * _cols] = value;
342     }
343 
344     /// recursive fill occupied area of cells with group id
345     private void fillCellGroup(int x, int y, int value) {
346         if (x < 0 || y < 0 || x >= _cols || y >= _rows)
347             return;
348         if (this[x, y] != EMPTY && cellGroup(x, y) == 0) {
349             setCellGroup(value, x, y);
350             fillCellGroup(x + 1, y, value);
351             fillCellGroup(x - 1, y, value);
352             fillCellGroup(x, y + 1, value);
353             fillCellGroup(x, y - 1, value);
354         }
355     }
356 
357     /// 1 == next cell below is occupied, 2 == one empty cell
358     private int distanceToOccupiedCellBelow(int col, int row) {
359         for (int y = row - 1; y >= -1; y--) {
360             if (this[col, y] != EMPTY)
361                 return row - y;
362         }
363         return 1;
364     }
365 
366     /// mark cells in _cellGroups[] matrix which can fall down (value > 0 is distance to fall)
367     bool markFallingCells() {
368         _cellGroups = new int[_cols * _rows];
369         int groupId = 1;
370         for (int y = 0; y < _rows; y++) {
371             for (int x = 0; x < _cols; x++) {
372                 if (this[x, y] != EMPTY && cellGroup(x, y) == 0) {
373                     fillCellGroup(x, y, groupId);
374                     groupId++;
375                 }
376             }
377         }
378         // check space below each group - can it fall down?
379         int[] spaceBelowGroup = new int[groupId];
380         for (int y = 0; y < _rows; y++) {
381             for (int x = 0; x < _cols; x++) {
382                 int group = cellGroup(x, y);
383                 if (group > 0) {
384                     if (y == 0)
385                         spaceBelowGroup[group] = 1;
386                     else if (this[x, y - 1] != EMPTY && cellGroup(x, y - 1) != group)
387                         spaceBelowGroup[group] = 1;
388                     else if (this[x, y - 1] == EMPTY) {
389                         int dist = distanceToOccupiedCellBelow(x, y);
390                         if (spaceBelowGroup[group] == 0 || spaceBelowGroup[group] > dist)
391                             spaceBelowGroup[group] = dist;
392                     }
393                 }
394             }
395         }
396         // replace group IDs with distance to fall (0 == cell cannot fall)
397         for (int y = 0; y < _rows; y++) {
398             for (int x = 0; x < _cols; x++) {
399                 int group = cellGroup(x, y);
400                 if (group > 0) {
401                     // distance to fall
402                     setCellGroup(spaceBelowGroup[group] - 1, x, y);
403                 }
404             }
405         }
406         bool canFall = false;
407         for (int i = 1; i < groupId; i++)
408             if (spaceBelowGroup[i] > 1)
409                 canFall = true;
410         return canFall;
411     }
412 
413     /// moves all falling cells one cell down
414     /// returns true if there are more cells to fall
415     bool moveFallingCells() {
416         bool res = false;
417         for (int y = 0; y < _rows - 1; y++) {
418             for (int x = 0; x < _cols; x++) {
419                 int dist = cellGroup(x, y + 1);
420                 if (dist > 0) {
421                     // move cell down, decreasing distance
422                     setCellGroup(dist - 1, x, y);
423                     this[x, y] = this[x, y + 1];
424                     setCellGroup(0, x, y + 1);
425                     this[x, y + 1] = EMPTY;
426                     if (dist > 1)
427                         res = true;
428                 }
429             }
430         }
431         return res;
432     }
433 
434     /// return true if cell is currently falling
435     bool isCellFalling(int col, int row) {
436         return cellGroup(col, row) > 0;
437     }
438 
439     /// returns true if next figure is generated
440     @property bool hasNextFigure() {
441         return !_nextFigure.empty;
442     }
443 }
444