1 module gui;
2 
3 import model;
4 
5 import dlangui;
6 import std.algorithm;
7 
8 /// game action codes
9 enum TetrisAction : int {
10     MoveLeft = 10000,
11     MoveRight,
12     RotateCCW,
13     FastDown,
14     Pause,
15     LevelUp,
16 }
17 
18 const Action ACTION_MOVE_LEFT   = (new Action(TetrisAction.MoveLeft,  KeyCode.LEFT)).addAccelerator(KeyCode.KEY_A).iconId("arrow-left");
19 const Action ACTION_MOVE_RIGHT  = (new Action(TetrisAction.MoveRight, KeyCode.RIGHT)).addAccelerator(KeyCode.KEY_D).iconId("arrow-right");
20 const Action ACTION_ROTATE      = (new Action(TetrisAction.RotateCCW, KeyCode.UP)).addAccelerator(KeyCode.KEY_W).iconId("rotate");
21 const Action ACTION_FAST_DOWN   = (new Action(TetrisAction.FastDown,  KeyCode.SPACE)).addAccelerator(KeyCode.KEY_S).iconId("arrow-down");
22 const Action ACTION_PAUSE       = (new Action(TetrisAction.Pause,     KeyCode.ESCAPE)).addAccelerator(KeyCode.PAUSE).iconId("pause");
23 const Action ACTION_LEVEL_UP    = (new Action(TetrisAction.LevelUp,   KeyCode.ADD)).addAccelerator(KeyCode.INS).iconId("levelup");
24 
25 const Action[] CUP_ACTIONS = [ACTION_PAUSE,     ACTION_ROTATE,      ACTION_LEVEL_UP,
26                               ACTION_MOVE_LEFT, ACTION_FAST_DOWN,   ACTION_MOVE_RIGHT];
27 
28 /// about dialog
29 Widget createAboutWidget()
30 {
31     LinearLayout res = new VerticalLayout();
32     res.padding(Rect(10,10,10,10));
33     res.addChild(new TextWidget(null, "DLangUI Tetris demo app"d));
34     res.addChild(new TextWidget(null, "(C) Vadim Lopatin, 2014"d));
35     res.addChild(new TextWidget(null, "http://github.com/buggins/dlangui"d));
36     Button closeButton = new Button("close", "Close"d);
37     closeButton.click = delegate(Widget src) {
38         Log.i("Closing window");
39         res.window.close();
40         return true;
41     };
42     res.addChild(closeButton);
43     return res;
44 }
45 
46 /// Cup States
47 enum CupState : int {
48     /// New figure appears
49     NewFigure,
50     /// Game is paused
51     Paused,
52     /// Figure is falling
53     FallingFigure,
54     /// Figure is hanging - pause between falling by one row
55     HangingFigure,
56     /// destroying complete rows
57     DestroyingRows,
58     /// falling after some rows were destroyed
59     FallingRows,
60     /// Game is over
61     GameOver,
62 }
63 
64 /// Cup widget
65 class CupWidget : Widget {
66     /// cup columns count
67     int _cols;
68     /// cup rows count
69     int _rows;
70     /// cup data
71     Cup _cup;
72 
73 
74     /// Level 1..10
75     int _level;
76     /// Score
77     int _score;
78     /// Single cell movement duration for current level, in 1/10000000 of seconds
79     long _movementDuration;
80     /// When true, figure is falling down fast
81     bool _fastDownFlag;
82     /// animation helper for fade and movement in different states
83     AnimationHelper _animation;
84     /// GameOver popup
85     private PopupWidget _gameOverPopup;
86     /// Status widget
87     private StatusWidget _status;
88     /// Current state
89     protected CupState _state;
90 
91     protected int _totalRowsDestroyed;
92 
93     static const int[10] LEVEL_SPEED = [15000000, 10000000, 7000000, 6000000, 5000000, 4000000, 3000000, 2000000, 1500000, 1000000];
94 
95     static const int RESERVED_ROWS = 5; // reserved for next figure
96 
97     /// set difficulty level 1..10
98     void setLevel(int level) {
99         if (level > 10)
100             return;
101         _level = level;
102         _movementDuration = LEVEL_SPEED[level - 1];
103         _status.setLevel(_level);
104     }
105 
106     static const int MIN_FAST_FALLING_INTERVAL = 600000;
107 
108     static const int ROWS_FALLING_INTERVAL = 1200000;
109 
110     /// change game state, init state animation when necessary
111     void setCupState(CupState state) {
112         int animationIntervalPercent = 100;
113         switch (state) {
114             case CupState.FallingFigure:
115                 animationIntervalPercent = _fastDownFlag ? 10 : 25;
116                 break;
117             case CupState.HangingFigure:
118                 animationIntervalPercent = 75;
119                 break;
120             case CupState.NewFigure:
121                 animationIntervalPercent = 100;
122                 break;
123             case CupState.FallingRows:
124                 animationIntervalPercent = 25;
125                 break;
126             case CupState.DestroyingRows:
127                 animationIntervalPercent = 50;
128                 break;
129             default:
130                 // no animation for other states
131                 animationIntervalPercent = 0;
132                 break;
133         }
134         _state = state;
135         if (animationIntervalPercent) {
136             long interval = _movementDuration * animationIntervalPercent / 100;
137             if (_fastDownFlag && falling && interval > MIN_FAST_FALLING_INTERVAL)
138                 interval = MIN_FAST_FALLING_INTERVAL;
139             if (_state == CupState.FallingRows)
140                 interval = ROWS_FALLING_INTERVAL;
141             _animation.start(interval, 255);
142         }
143         invalidate();
144     }
145 
146     void addScore(int score) {
147         _score += score;
148         _status.setScore(_score);
149     }
150 
151     /// returns true if figure is in falling - movement state
152     @property bool falling() {
153         return _state == CupState.FallingFigure;
154     }
155 
156     /// Turn on / off fast falling down
157     bool handleFastDown(bool fast) {
158         if (fast == true) {
159             if (_fastDownFlag)
160                 return false;
161             // handle turn on fast down
162             if (falling) {
163                 _fastDownFlag = true;
164                 // if already falling, just increase speed
165                 _animation.interval = _movementDuration * 10 / 100;
166                 if (_animation.interval > MIN_FAST_FALLING_INTERVAL)
167                     _animation.interval = MIN_FAST_FALLING_INTERVAL;
168                 return true;
169             } else if (_state == CupState.HangingFigure) {
170                 _fastDownFlag = true;
171                 setCupState(CupState.FallingFigure);
172                 return true;
173             } else {
174                 return false;
175             }
176         }
177         _fastDownFlag = fast;
178         return true;
179     }
180 
181     static const int[] NEXT_LEVEL_SCORE = [0, 20, 50, 100, 200, 350, 500, 750, 1000, 1500, 2000];
182 
183     /// try start next figure
184     protected void nextFigure() {
185         if (!_cup.dropNextFigure()) {
186             // Game Over
187             setCupState(CupState.GameOver);
188             Widget popupWidget = new TextWidget("popup", "Game Over!"d);
189             popupWidget.padding(Rect(30, 30, 30, 30)).backgroundImageId("popup_background").alpha(0x40).fontWeight(800).fontSize(30);
190             _gameOverPopup = window.showPopup(popupWidget, this);
191         } else {
192             setCupState(CupState.NewFigure);
193             if (_level < 10 && _totalRowsDestroyed >= NEXT_LEVEL_SCORE[_level])
194                 setLevel(_level + 1); // level up
195         }
196     }
197 
198     protected void destroyFullRows() {
199         setCupState(CupState.DestroyingRows);
200     }
201 
202     protected void onAnimationFinished() {
203         switch (_state) {
204             case CupState.NewFigure:
205                 _fastDownFlag = false;
206                 _cup.genNextFigure();
207                 setCupState(CupState.HangingFigure);
208                 break;
209             case CupState.FallingFigure:
210                 if (_cup.isPositionFreeBelow()) {
211                     _cup.move(0, -1, false);
212                     if (_fastDownFlag)
213                         setCupState(CupState.FallingFigure);
214                     else
215                         setCupState(CupState.HangingFigure);
216                 } else {
217                     // At bottom of cup
218                     _cup.putFigure();
219                     _fastDownFlag = false;
220                     if (_cup.hasFullRows) {
221                         destroyFullRows();
222                     } else {
223                         nextFigure();
224                     }
225                 }
226                 break;
227             case CupState.HangingFigure:
228                 setCupState(CupState.FallingFigure);
229                 break;
230             case CupState.DestroyingRows:
231                 int rowsDestroyed = _cup.destroyFullRows();
232                 _totalRowsDestroyed += rowsDestroyed;
233                 _status.setRowsDestroyed(_totalRowsDestroyed);
234                 int scorePerRow = 0;
235                 for (int i = 0; i < rowsDestroyed; i++) {
236                     scorePerRow += 10;
237                     addScore(scorePerRow);
238                 }
239                 if (_cup.markFallingCells()) {
240                     setCupState(CupState.FallingRows);
241                 } else {
242                     nextFigure();
243                 }
244                 break;
245             case CupState.FallingRows:
246                 if (_cup.moveFallingCells()) {
247                     // more cells to fall
248                     setCupState(CupState.FallingRows);
249                 } else {
250                     // no more cells to fall, next figure
251                     if (_cup.hasFullRows) {
252                         // new full rows were constructed: destroy
253                         destroyFullRows();
254                     } else {
255                         // next figure
256                         nextFigure();
257                     }
258                 }
259                 break;
260             default:
261                 break;
262         }
263     }
264 
265     /// start new game
266     void newGame() {
267         setLevel(1);
268         initialize(_cols, _rows);
269         _cup.dropNextFigure();
270         setCupState(CupState.NewFigure);
271         if (window && _gameOverPopup) {
272             window.removePopup(_gameOverPopup);
273             _gameOverPopup = null;
274         }
275         _score = 0;
276         _status.setScore(0);
277         _totalRowsDestroyed = 0;
278         _status.setRowsDestroyed(0);
279     }
280 
281     /// init cup
282     void initialize(int cols, int rows) {
283         _cup.initialize(cols, rows);
284         _cols = cols;
285         _rows = rows;
286     }
287 
288     protected Rect cellRect(Rect rc, int col, int row) {
289         int dx = rc.width / _cols;
290         int dy = rc.height / (_rows + RESERVED_ROWS);
291         int dd = dx;
292         if (dd > dy)
293             dd = dy;
294         int x0 = rc.left + (rc.width - dd * _cols) / 2 + dd * col;
295         int y0 = rc.bottom - (rc.height - dd * (_rows + RESERVED_ROWS)) / 2 - dd * row - dd;
296         return Rect(x0, y0, x0 + dd, y0 + dd);
297     }
298 
299     /// Handle keys
300     override bool onKeyEvent(KeyEvent event) {
301         if (event.action == KeyAction.KeyDown && _state == CupState.GameOver) {
302             // restart game
303             newGame();
304             return true;
305         }
306         if (event.action == KeyAction.KeyDown && _state == CupState.NewFigure) {
307             // stop new figure fade in if key is pressed
308             onAnimationFinished();
309         }
310         if (event.keyCode == KeyCode.DOWN) {
311             if (event.action == KeyAction.KeyDown) {
312                 handleFastDown(true);
313             } else if (event.action == KeyAction.KeyUp) {
314                 handleFastDown(false);
315             }
316             return true;
317         }
318         if ((event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) && event.keyCode != KeyCode.SPACE)
319             handleFastDown(false); // don't stop fast down on Space key KeyUp
320         return super.onKeyEvent(event);
321     }
322 
323     /// draw cup cell
324     protected void drawCell(DrawBuf buf, Rect cellRc, uint color, int offset = 0) {
325         cellRc.top += offset;
326         cellRc.bottom += offset;
327 
328         cellRc.right--;
329         cellRc.bottom--;
330 
331         int w = cellRc.width / 6;
332         buf.drawFrame(cellRc, color, Rect(w,w,w,w));
333         cellRc.shrink(w, w);
334         color = addAlpha(color, 0xC0);
335         buf.fillRect(cellRc, color);
336     }
337 
338     /// draw figure
339     protected void drawFigure(DrawBuf buf, Rect rc, FigurePosition figure, int dy, uint alpha = 0) {
340         uint color = addAlpha(_figureColors[figure.index - 1], alpha);
341         FigureShape shape = figure.shape;
342         foreach(cell; shape.cells) {
343             Rect cellRc = cellRect(rc, figure.x + cell.dx, figure.y + cell.dy);
344             cellRc.top += dy;
345             cellRc.bottom += dy;
346             drawCell(buf, cellRc, color);
347         }
348     }
349 
350     //=================================================================================================
351     // Overrides of Widget methods
352 
353     /// returns true is widget is being animated - need to call animate() and redraw
354     override @property bool animating() {
355         switch (_state) {
356             case CupState.NewFigure:
357             case CupState.FallingFigure:
358             case CupState.HangingFigure:
359             case CupState.DestroyingRows:
360             case CupState.FallingRows:
361                 return true;
362             default:
363                 return false;
364         }
365     }
366 
367     /// animates window; interval is time left from previous draw, in hnsecs (1/10000000 of second)
368     override void animate(long interval) {
369         _animation.animate(interval);
370         if (_animation.finished) {
371             onAnimationFinished();
372         }
373     }
374 
375     /// Draw widget at its position to buffer
376     override void onDraw(DrawBuf buf) {
377         super.onDraw(buf);
378         Rect rc = _pos;
379         applyMargins(rc);
380         auto saver = ClipRectSaver(buf, rc, alpha);
381         applyPadding(rc);
382 
383         Rect topLeft = cellRect(rc, 0, _rows - 1);
384         Rect bottomRight = cellRect(rc, _cols - 1, 0);
385         Rect cupRc = Rect(topLeft.left, topLeft.top, bottomRight.right, bottomRight.bottom);
386 
387         int fw = 7;
388         int dw = 0;
389         uint fcl = 0xA0606090;
390         buf.fillRect(cupRc, 0xC0A0C0B0);
391         buf.fillRect(Rect(cupRc.left - dw - fw, cupRc.top, cupRc.left - dw,       cupRc.bottom + dw), fcl);
392         buf.fillRect(Rect(cupRc.right + dw,     cupRc.top, cupRc.right + dw + fw, cupRc.bottom + dw), fcl);
393         buf.fillRect(Rect(cupRc.left - dw - fw, cupRc.bottom + dw, cupRc.right + dw + fw, cupRc.bottom + dw + fw), fcl);
394 
395         int fallingCellOffset = 0;
396         if (_state == CupState.FallingRows) {
397             fallingCellOffset = _animation.getProgress(topLeft.height);
398         }
399 
400         for (int row = 0; row < _rows; row++) {
401             uint cellAlpha = 0;
402             if (_state == CupState.DestroyingRows && _cup.isRowFull(row))
403                 cellAlpha = _animation.progress;
404             for (int col = 0; col < _cols; col++) {
405 
406                 int value = _cup[col, row];
407                 Rect cellRc = cellRect(rc, col, row);
408 
409                 Point middle = cellRc.middle;
410                 buf.fillRect(Rect(middle.x - 1, middle.y - 1, middle.x + 1, middle.y + 1), 0x80404040);
411 
412                 if (value != EMPTY) {
413                     uint cl = addAlpha(_figureColors[value - 1], cellAlpha);
414                     int offset = fallingCellOffset > 0 && _cup.isCellFalling(col, row) ? fallingCellOffset : 0;
415                     drawCell(buf, cellRc, cl, offset);
416                 }
417             }
418         }
419 
420         // draw current figure falling
421         if (_state == CupState.FallingFigure || _state == CupState.HangingFigure) {
422             int dy = 0;
423             if (falling && _cup.isPositionFreeBelow())
424                 dy = _animation.getProgress(topLeft.height);
425             drawFigure(buf, rc, _cup.currentFigure, dy, 0);
426         }
427 
428         // draw next figure
429         if (_cup.hasNextFigure) {
430             //auto shape = _nextFigure.shape;
431             uint nextFigureAlpha = 0;
432             if (_state == CupState.NewFigure) {
433                 nextFigureAlpha = _animation.progress;
434                 drawFigure(buf, rc, _cup.currentFigure, 0, 255 - nextFigureAlpha);
435             }
436             if (_state != CupState.GameOver) {
437                 drawFigure(buf, rc, _cup.nextFigure, 0, blendAlpha(0xA0, nextFigureAlpha));
438             }
439         }
440 
441     }
442 
443     /// override to handle specific actions
444     override bool handleAction(const Action a) {
445         switch (a.id) {
446             case TetrisAction.MoveLeft:
447                 _cup.move(-1, 0, falling);
448                 return true;
449             case TetrisAction.MoveRight:
450                 _cup.move(1, 0, falling);
451                 return true;
452             case TetrisAction.RotateCCW:
453                 _cup.rotate(1, falling);
454                 return true;
455             case TetrisAction.FastDown:
456                 handleFastDown(true);
457                 return true;
458             case TetrisAction.Pause:
459                 // TODO: implement pause
460                 return true;
461             case TetrisAction.LevelUp:
462                 setLevel(_level + 1);
463                 return true;
464             default:
465                 if (parent) // by default, pass to parent widget
466                     return parent.handleAction(a);
467                 return false;
468         }
469     }
470 
471     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
472     override void measure(int parentWidth, int parentHeight) {
473         measuredContent(parentWidth, parentHeight, parentWidth * 3 / 5, parentHeight);
474     }
475 
476     this(StatusWidget status) {
477         super("CUP");
478         this._status = status;
479         layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).layoutWeight(2).setState(State.Default).focusable(true).padding(Rect(20, 20, 20, 20));
480 
481         _cols = 10;
482         _rows = 18;
483         newGame();
484 
485         focusable = true;
486 
487         acceleratorMap.add(CUP_ACTIONS);
488     }
489 }
490 
491 /// Panel to show game status
492 class StatusWidget : VerticalLayout {
493     private TextWidget _level;
494     private TextWidget _rowsDestroyed;
495     private TextWidget _score;
496     private CupWidget _cup;
497     private TextWidget[] _labels;
498     void setCup(CupWidget cup) {
499         _cup = cup;
500     }
501     TextWidget createTextWidget(dstring str, uint color) {
502         TextWidget res = new TextWidget(null, str);
503         res.layoutWidth(FILL_PARENT).alignment(Align.Center).fontSize(18.pointsToPixels).textColor(color);
504         _labels ~= res;
505         return res;
506     }
507 
508     Widget createControls() {
509         TableLayout res = new TableLayout();
510         res.colCount = 3;
511         foreach(const Action a; CUP_ACTIONS) {
512             ImageButton btn = new ImageButton(a);
513             btn.padding = 5.pointsToPixels;
514             btn.focusable = false;
515             res.addChild(btn);
516         }
517         res.alignment = Align.Center;
518         res.layoutWidth(WRAP_CONTENT).layoutHeight(WRAP_CONTENT).margins(Rect(5.pointsToPixels, 5.pointsToPixels, 5.pointsToPixels, 5.pointsToPixels)).alignment(Align.Center);
519         return res;
520     }
521 
522     this() {
523         super("CUP_STATUS");
524 
525         addChild(new VSpacer());
526 
527         ImageWidget image = new ImageWidget(null, "tetris_logo_big");
528         image.layoutWidth(FILL_PARENT).alignment(Align.Center).clickable(true);
529         image.click = delegate(Widget src) {
530             _cup.handleAction(ACTION_PAUSE);
531             // about dialog when clicking on image
532             Window wnd = Platform.instance.createWindow("About...", window, WindowFlag.Modal);
533             wnd.mainWidget = createAboutWidget();
534             wnd.show();
535             return true;
536         };
537         addChild(image);
538 
539         addChild(new VSpacer());
540         addChild(createTextWidget("Level:"d, 0x008000));
541         addChild((_level = createTextWidget(""d, 0x008000)));
542         addChild(new VSpacer());
543         addChild(createTextWidget("Rows:"d, 0x202080));
544         addChild((_rowsDestroyed = createTextWidget(""d, 0x202080)));
545         addChild(new VSpacer());
546         addChild(createTextWidget("Score:"d, 0x800000));
547         addChild((_score = createTextWidget(""d, 0x800000)));
548         addChild(new VSpacer());
549         HorizontalLayout h = new HorizontalLayout();
550         h.layoutWidth = FILL_PARENT;
551         h.layoutHeight = FILL_PARENT;
552         h.addChild(new HSpacer());
553         h.addChild(createControls());
554         h.addChild(new HSpacer());
555         addChild(h);
556         addChild(new VSpacer());
557 
558         layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).layoutWeight(2).padding(Rect(5.pointsToPixels, 5.pointsToPixels, 5.pointsToPixels, 5.pointsToPixels));
559     }
560 
561     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
562     override void measure(int parentWidth, int parentHeight) {
563         import std.algorithm: min;
564         int minw = min(parentWidth, parentHeight);
565         foreach(lbl; _labels) {
566             lbl.fontSize = minw / 20;
567         }
568         super.measure(parentWidth, parentHeight);
569     }
570 
571     void setLevel(int level) {
572         _level.text = toUTF32(to!string(level));
573     }
574 
575     void setScore(int score) {
576         _score.text = toUTF32(to!string(score));
577     }
578 
579     void setRowsDestroyed(int rows) {
580         _rowsDestroyed.text = toUTF32(to!string(rows));
581     }
582 
583     override bool handleAction(const Action a) {
584         return _cup.handleAction(a);
585     }
586 }
587 
588 /// Cup page: cup widget + status widget
589 class CupPage : HorizontalLayout {
590     CupWidget _cup;
591     StatusWidget _status;
592     this() {
593         super("CUP_PAGE");
594         layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
595         _status = new StatusWidget();
596         _cup = new CupWidget(_status);
597         _status.setCup(_cup);
598         _cup.layoutWidth = 50.makePercentSize;
599         _status.layoutWidth = 50.makePercentSize;
600         addChild(_cup);
601         addChild(_status);
602     }
603     /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
604     override void measure(int parentWidth, int parentHeight) {
605         super.measure(parentWidth, parentHeight);
606         /// fixed size
607         measuredContent(parentWidth, parentHeight, 600, 550);
608     }
609 }
610 
611 //
612 class GameWidget : FrameLayout {
613 
614     CupPage _cupPage;
615     this() {
616         super("GAME");
617         _cupPage = new CupPage();
618         addChild(_cupPage);
619         //showChild(_cupPage.id, Visibility.Invisible, true);
620         backgroundImageId = "tx_fabric.tiled";
621     }
622 }