1 module gui; 2 3 import model; 4 5 import dlangui.all; 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.onClickListener = 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.onClickListener = 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 }