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 }