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 }