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