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 initialize(_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 initialize(int cols, int rows) {
282 _cup.initialize(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 private TextWidget[] _labels;
497 void setCup(CupWidget cup) {
498 _cup = cup;
499 }
500 TextWidget createTextWidget(dstring str, uint color) {
501 TextWidget res = new TextWidget(null, str);
502 res.layoutWidth(FILL_PARENT).alignment(Align.Center).fontSize(18.pointsToPixels).textColor(color);
503 _labels ~= res;
504 return res;
505 }
506
507 Widget createControls() {
508 TableLayout res = new TableLayout();
509 res.colCount = 3;
510 foreach(const Action a; CUP_ACTIONS) {
511 ImageButton btn = new ImageButton(a);
512 btn.padding = 5.pointsToPixels;
513 btn.focusable = false;
514 res.addChild(btn);
515 }
516 res.alignment = Align.Center;
517 res.layoutWidth(WRAP_CONTENT).layoutHeight(WRAP_CONTENT).margins(Rect(5.pointsToPixels, 5.pointsToPixels, 5.pointsToPixels, 5.pointsToPixels)).alignment(Align.Center);
518 return res;
519 }
520
521 this() {
522 super("CUP_STATUS");
523
524 addChild(new VSpacer());
525
526 ImageWidget image = new ImageWidget(null, "tetris_logo_big");
527 image.layoutWidth(FILL_PARENT).alignment(Align.Center).clickable(true);
528 image.click = delegate(Widget src) {
529 _cup.handleAction(ACTION_PAUSE);
530 // about dialog when clicking on image
531 Window wnd = Platform.instance.createWindow("About...", window, WindowFlag.Modal);
532 wnd.mainWidget = createAboutWidget();
533 wnd.show();
534 return true;
535 };
536 addChild(image);
537
538 addChild(new VSpacer());
539 addChild(createTextWidget("Level:"d, 0x008000));
540 addChild((_level = createTextWidget(""d, 0x008000)));
541 addChild(new VSpacer());
542 addChild(createTextWidget("Rows:"d, 0x202080));
543 addChild((_rowsDestroyed = createTextWidget(""d, 0x202080)));
544 addChild(new VSpacer());
545 addChild(createTextWidget("Score:"d, 0x800000));
546 addChild((_score = createTextWidget(""d, 0x800000)));
547 addChild(new VSpacer());
548 HorizontalLayout h = new HorizontalLayout();
549 h.layoutWidth = FILL_PARENT;
550 h.layoutHeight = FILL_PARENT;
551 h.addChild(new HSpacer());
552 h.addChild(createControls());
553 h.addChild(new HSpacer());
554 addChild(h);
555 addChild(new VSpacer());
556
557 layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).layoutWeight(2).padding(Rect(5.pointsToPixels, 5.pointsToPixels, 5.pointsToPixels, 5.pointsToPixels));
558 }
559
560 /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
561 override void measure(int parentWidth, int parentHeight) {
562 int minw = min(parentWidth, parentHeight);
563 foreach(lbl; _labels) {
564 lbl.fontSize = minw / 20;
565 }
566 super.measure(parentWidth, parentHeight);
567 }
568
569 void setLevel(int level) {
570 _level.text = toUTF32(to!string(level));
571 }
572
573 void setScore(int score) {
574 _score.text = toUTF32(to!string(score));
575 }
576
577 void setRowsDestroyed(int rows) {
578 _rowsDestroyed.text = toUTF32(to!string(rows));
579 }
580
581 override bool handleAction(const Action a) {
582 return _cup.handleAction(a);
583 }
584 }
585
586 /// Cup page: cup widget + status widget
587 class CupPage : HorizontalLayout {
588 CupWidget _cup;
589 StatusWidget _status;
590 this() {
591 super("CUP_PAGE");
592 layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
593 _status = new StatusWidget();
594 _cup = new CupWidget(_status);
595 _status.setCup(_cup);
596 _cup.layoutWidth = 50.makePercentSize;
597 _status.layoutWidth = 50.makePercentSize;
598 addChild(_cup);
599 addChild(_status);
600 }
601 /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
602 override void measure(int parentWidth, int parentHeight) {
603 super.measure(parentWidth, parentHeight);
604 /// fixed size
605 measuredContent(parentWidth, parentHeight, 600, 550);
606 }
607 }
608
609 //
610 class GameWidget : FrameLayout {
611
612 CupPage _cupPage;
613 this() {
614 super("GAME");
615 _cupPage = new CupPage();
616 addChild(_cupPage);
617 //showChild(_cupPage.id, Visibility.Invisible, true);
618 backgroundImageId = "tx_fabric.tiled";
619 }
620 }