diff --git a/examples/tetris/gui.d b/examples/tetris/gui.d new file mode 100644 index 00000000..334ac23e --- /dev/null +++ b/examples/tetris/gui.d @@ -0,0 +1,563 @@ +module gui; + +import dlangui.all; +import dlangui.widgets.popup; +import dlangui.graphics.drawbuf; +//import std.stdio; +import std.conv; +import std.utf; +import model; + + +Widget createAboutWidget() +{ + LinearLayout res = new VerticalLayout(); + res.padding(Rect(10,10,10,10)); + res.addChild(new TextWidget(null, "DLangUI Tetris demo app"d)); + res.addChild(new TextWidget(null, "(C) Vadim Lopatin, 2014"d)); + res.addChild(new TextWidget(null, "http://github.com/buggins/dlangui"d)); + Button closeButton = new Button("close", "Close"d); + closeButton.onClickListener = delegate(Widget src) { + Log.i("Closing window"); + res.window.close(); + return true; + }; + res.addChild(closeButton); + return res; +} + +enum TetrisAction : int { + MoveLeft = 10000, + MoveRight, + RotateCCW, + FastDown, + Pause, + LevelUp, +} + + +/// Cup widget +class CupWidget : Widget { + /// cup columns count + int _cols; + /// cup rows count + int _rows; + /// cup data + Cup _cup; + + + /// Level 1..10 + int _level; + /// Score + int _score; + /// Single cell movement duration for current level, in 1/10000000 of seconds + long _movementDuration; + /// When true, figure is falling down fast + bool _fastDownFlag; + + AnimationHelper _animation; + private PopupWidget _gameOverPopup; + + /// Cup States + enum CupState : int { + /// New figure appears + NewFigure, + /// Game is paused + Paused, + /// Figure is falling + FallingFigure, + /// Figure is hanging - pause between falling by one row + HangingFigure, + /// destroying complete rows + DestroyingRows, + /// falling after some rows were destroyed + FallingRows, + /// Game is over + GameOver, + } + + protected CupState _state; + + static const int[10] LEVEL_SPEED = [15000000, 10000000, 7000000, 6000000, 5000000, 4000000, 3500000, 3000000, 2500000, 2000000]; + + + static const int RESERVED_ROWS = 5; // reserved for next figure + + + + /// set difficulty level 1..10 + void setLevel(int level) { + _level = level; + _movementDuration = LEVEL_SPEED[level - 1]; + _status.setLevel(_level); + } + + static const int MIN_FAST_FALLING_INTERVAL = 600000; + static const int ROWS_FALLING_INTERVAL = 600000; + + void setCupState(CupState state) { + int animationIntervalPercent = 100; + switch (state) { + case CupState.FallingFigure: + animationIntervalPercent = _fastDownFlag ? 10 : 25; + break; + case CupState.HangingFigure: + animationIntervalPercent = 75; + break; + case CupState.NewFigure: + animationIntervalPercent = 100; + break; + case CupState.FallingRows: + animationIntervalPercent = 25; + break; + case CupState.DestroyingRows: + animationIntervalPercent = 50; + break; + default: + break; + } + _state = state; + if (animationIntervalPercent) { + long interval = _movementDuration * animationIntervalPercent / 100; + if (_fastDownFlag && falling && interval > MIN_FAST_FALLING_INTERVAL) + interval = MIN_FAST_FALLING_INTERVAL; + if (_state == CupState.FallingRows) + interval = ROWS_FALLING_INTERVAL; + _animation.start(interval, 255); + } + invalidate(); + } + + /// returns true is widget is being animated - need to call animate() and redraw + override @property bool animating() { + switch (_state) { + case CupState.NewFigure: + case CupState.FallingFigure: + case CupState.HangingFigure: + case CupState.DestroyingRows: + case CupState.FallingRows: + return true; + default: + return false; + } + } + + /// Turn on / off fast falling down + bool handleFastDown(bool fast) { + if (fast == true) { + if (_fastDownFlag) + return false; + // handle turn on fast down + if (falling) { + _fastDownFlag = true; + // if already falling, just increase speed + _animation.interval = _movementDuration * 10 / 100; + if (_animation.interval > MIN_FAST_FALLING_INTERVAL) + _animation.interval = MIN_FAST_FALLING_INTERVAL; + return true; + } else if (_state == CupState.HangingFigure) { + _fastDownFlag = true; + setCupState(CupState.FallingFigure); + return true; + } else { + return false; + } + } + _fastDownFlag = fast; + return true; + } + + /// try start next figure + protected void nextFigure() { + if (!_cup.dropNextFigure()) { + // Game Over + setCupState(CupState.GameOver); + Widget popupWidget = new TextWidget("popup", "Game Over!"d); + popupWidget.padding(Rect(30, 30, 30, 30)).backgroundImageId("popup_background").alpha(0x40).fontWeight(800).fontSize(30); + _gameOverPopup = window.showPopup(popupWidget, this); + } else { + setCupState(CupState.NewFigure); + } + } + + protected void destroyFullRows() { + setCupState(CupState.DestroyingRows); + } + + protected void onAnimationFinished() { + switch (_state) { + case CupState.NewFigure: + _fastDownFlag = false; + _cup.genNextFigure(); + setCupState(CupState.HangingFigure); + break; + case CupState.FallingFigure: + if (_cup.isPositionFreeBelow()) { + _cup.move(0, -1, false); + if (_fastDownFlag) + setCupState(CupState.FallingFigure); + else + setCupState(CupState.HangingFigure); + } else { + // At bottom of cup + _cup.putFigure(); + _fastDownFlag = false; + if (_cup.hasFullRows) { + destroyFullRows(); + } else { + nextFigure(); + } + } + break; + case CupState.HangingFigure: + setCupState(CupState.FallingFigure); + break; + case CupState.DestroyingRows: + int rowsDestroyed = _cup.destroyFullRows(); + int scorePerRow = 0; + for (int i = 0; i < rowsDestroyed; i++) { + scorePerRow += 10; + addScore(scorePerRow); + } + if (_cup.markFallingCells()) { + setCupState(CupState.FallingRows); + } else { + nextFigure(); + } + break; + case CupState.FallingRows: + if (_cup.moveFallingCells()) { + // more cells to fall + setCupState(CupState.FallingRows); + } else { + // no more cells to fall, next figure + if (_cup.hasFullRows) { + // new full rows were constructed: destroy + destroyFullRows(); + } else { + // next figure + nextFigure(); + } + } + break; + default: + break; + } + } + + /// animates window; interval is time left from previous draw, in hnsecs (1/10000000 of second) + override void animate(long interval) { + _animation.animate(interval); + if (_animation.finished) { + onAnimationFinished(); + } + } + + void init(int cols, int rows) { + _cup.init(cols, rows); + _cols = cols; + _rows = rows; + } + + protected Rect cellRect(Rect rc, int col, int row) { + int dx = rc.width / _cols; + int dy = rc.height / (_rows + RESERVED_ROWS); + int dd = dx; + if (dd > dy) + dd = dy; + int x0 = rc.left + (rc.width - dd * _cols) / 2 + dd * col; + int y0 = rc.bottom - (rc.height - dd * (_rows + RESERVED_ROWS)) / 2 - dd * row - dd; + return Rect(x0, y0, x0 + dd, y0 + dd); + } + + /// Handle keys + override bool onKeyEvent(KeyEvent event) { + if (event.action == KeyAction.KeyDown && _state == CupState.GameOver) { + // restart game + newGame(); + return true; + } + if (event.action == KeyAction.KeyDown && _state == CupState.NewFigure) { + // stop new figure fade in if key is pressed + onAnimationFinished(); + } + if (event.keyCode == KeyCode.DOWN) { + if (event.action == KeyAction.KeyDown) { + handleFastDown(true); + } else if (event.action == KeyAction.KeyUp) { + handleFastDown(false); + } + return true; + } + if ((event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) && event.keyCode != KeyCode.SPACE) + handleFastDown(false); // don't stop fast down on Space key KeyUp + return super.onKeyEvent(event); + } + + /// draw cup cell + protected void drawCell(DrawBuf buf, Rect cellRc, uint color, int offset = 0) { + cellRc.top += offset; + cellRc.bottom += offset; + + cellRc.right--; + cellRc.bottom--; + + int w = cellRc.width / 6; + buf.drawFrame(cellRc, color, Rect(w,w,w,w)); + cellRc.shrink(w, w); + color = addAlpha(color, 0xC0); + buf.fillRect(cellRc, color); + } + + /// draw figure + protected void drawFigure(DrawBuf buf, Rect rc, FigurePosition figure, int dy, uint alpha = 0) { + uint color = addAlpha(_figureColors[figure.index - 1], alpha); + FigureShape shape = figure.shape; + foreach(cell; shape.cells) { + Rect cellRc = cellRect(rc, figure.x + cell.dx, figure.y + cell.dy); + cellRc.top += dy; + cellRc.bottom += dy; + drawCell(buf, cellRc, color); + } + } + + /// Draw widget at its position to buffer + override void onDraw(DrawBuf buf) { + super.onDraw(buf); + Rect rc = _pos; + applyMargins(rc); + auto saver = ClipRectSaver(buf, rc, alpha); + applyPadding(rc); + + Rect topLeft = cellRect(rc, 0, _rows - 1); + Rect bottomRight = cellRect(rc, _cols - 1, 0); + Rect cupRc = Rect(topLeft.left, topLeft.top, bottomRight.right, bottomRight.bottom); + + int fw = 7; + int dw = 0; + uint fcl = 0xA0606090; + buf.fillRect(cupRc, 0xC0A0C0B0); + buf.fillRect(Rect(cupRc.left - dw - fw, cupRc.top, cupRc.left - dw, cupRc.bottom + dw), fcl); + buf.fillRect(Rect(cupRc.right + dw, cupRc.top, cupRc.right + dw + fw, cupRc.bottom + dw), fcl); + buf.fillRect(Rect(cupRc.left - dw - fw, cupRc.bottom + dw, cupRc.right + dw + fw, cupRc.bottom + dw + fw), fcl); + + int fallingCellOffset = 0; + if (_state == CupState.FallingRows) { + fallingCellOffset = _animation.getProgress(topLeft.height); + } + + for (int row = 0; row < _rows; row++) { + uint cellAlpha = 0; + if (_state == CupState.DestroyingRows && _cup.isRowFull(row)) + cellAlpha = _animation.progress; + for (int col = 0; col < _cols; col++) { + + int value = _cup[col, row]; + Rect cellRc = cellRect(rc, col, row); + + Point middle = cellRc.middle; + buf.fillRect(Rect(middle.x - 1, middle.y - 1, middle.x + 1, middle.y + 1), 0x80404040); + + if (value != EMPTY) { + uint cl = addAlpha(_figureColors[value - 1], cellAlpha); + int offset = fallingCellOffset > 0 && _cup.isCellFalling(col, row) ? fallingCellOffset : 0; + drawCell(buf, cellRc, cl, offset); + } + } + } + + // draw current figure falling + if (_state == CupState.FallingFigure || _state == CupState.HangingFigure) { + int dy = 0; + if (falling && _cup.isPositionFreeBelow()) + dy = _animation.getProgress(topLeft.height); + drawFigure(buf, rc, _cup.currentFigure, dy, 0); + } + + // draw next figure + if (_cup.hasNextFigure) { + //auto shape = _nextFigure.shape; + uint nextFigureAlpha = 0; + if (_state == CupState.NewFigure) { + nextFigureAlpha = _animation.progress; + drawFigure(buf, rc, _cup.currentFigure, 0, 255 - nextFigureAlpha); + } + if (_state != CupState.GameOver) { + drawFigure(buf, rc, _cup.nextFigure, 0, blendAlpha(0xA0, nextFigureAlpha)); + } + } + + } + /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). + override void measure(int parentWidth, int parentHeight) { + /// fixed size 350 x 550 + measuredContent(parentWidth, parentHeight, 350, 550); + } + + @property bool falling() { + return _state == CupState.FallingFigure; + } + + /// override to handle specific actions + override bool handleAction(const Action a) { + switch (a.id) { + case TetrisAction.MoveLeft: + _cup.move(-1, 0, falling); + return true; + case TetrisAction.MoveRight: + _cup.move(1, 0, falling); + return true; + case TetrisAction.RotateCCW: + _cup.rotate(1, falling); + return true; + case TetrisAction.FastDown: + handleFastDown(true); + return true; + case TetrisAction.Pause: + // TODO: implement pause + return true; + case TetrisAction.LevelUp: + if (_level < 10) + _level++; + // TODO: update state + return true; + default: + if (parent) // by default, pass to parent widget + return parent.handleAction(a); + return false; + } + } + + void addScore(int score) { + _score += score; + _status.setScore(_score); + } + + /// start new game + void newGame() { + setLevel(1); + _score = 0; + init(_cols, _rows); + _cup.dropNextFigure(); + setCupState(CupState.NewFigure); + if (window && _gameOverPopup) { + window.removePopup(_gameOverPopup); + _gameOverPopup = null; + } + _status.setScore(_score); + } + + private StatusWidget _status; + this(StatusWidget status) { + super("CUP"); + this._status = status; + layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).layoutWeight(3); + setState(State.Default); + //backgroundColor = 0xC0808080; + padding(Rect(20, 20, 20, 20)); + _cols = 11; + _rows = 15; + newGame(); + + focusable = true; + + acceleratorMap.add( [ + (new Action(TetrisAction.MoveLeft, KeyCode.LEFT)).addAccelerator(KeyCode.KEY_A), + (new Action(TetrisAction.MoveRight, KeyCode.RIGHT)).addAccelerator(KeyCode.KEY_D), + (new Action(TetrisAction.RotateCCW, KeyCode.UP)).addAccelerator(KeyCode.KEY_W), + (new Action(TetrisAction.FastDown, KeyCode.SPACE)).addAccelerator(KeyCode.KEY_S), + (new Action(TetrisAction.Pause, KeyCode.ESCAPE)).addAccelerator(KeyCode.PAUSE), + (new Action(TetrisAction.LevelUp, KeyCode.ADD)).addAccelerator(KeyCode.INS), + ]); + + } +} + +/// Panel to show game status +class StatusWidget : VerticalLayout { + private TextWidget _level; + private TextWidget _score; + private CupWidget _cup; + void setCup(CupWidget cup) { + _cup = cup; + } + TextWidget createTextWidget(dstring str, uint color) { + TextWidget res = new TextWidget(null, str); + res.layoutWidth(FILL_PARENT).alignment(Align.Center); + res.fontSize(30); + res.textColor(color); + return res; + } + this() { + super("CUP_STATUS"); + //backgroundColor = 0xC080FF80; + addChild(new VSpacer()); + addChild(new ImageWidget(null, "tetris_logo_big")); + addChild(new VSpacer()); + addChild(createTextWidget("Level:"d, 0x008000)); + addChild((_level = createTextWidget(""d, 0x008000))); + addChild(new VSpacer()); + addChild(createTextWidget("Score:"d, 0x800000)); + addChild((_score = createTextWidget(""d, 0x800000))); + addChild(new VSpacer()); + addChild(new VSpacer()); + layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).layoutWeight(2); + padding(Rect(20, 20, 20, 20)); + } + + void setLevel(int level) { + _level.text = toUTF32(to!string(level)); + } + + void setScore(int score) { + _score.text = toUTF32(to!string(score)); + } + + /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). + override void measure(int parentWidth, int parentHeight) { + super.measure(parentWidth, parentHeight); + /// fixed size 350 x 550 + measuredContent(parentWidth, parentHeight, 150, 550); + } + override bool handleAction(const Action a) { + return _cup.handleAction(a); + } +} + +class CupPage : HorizontalLayout { + CupWidget _cup; + StatusWidget _status; + this() { + super("CUP_PAGE"); + layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); + _status = new StatusWidget(); + _cup = new CupWidget(_status); + _status.setCup(_cup); + addChild(_cup); + addChild(_status); + } + /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). + override void measure(int parentWidth, int parentHeight) { + super.measure(parentWidth, parentHeight); + /// fixed size 350 x 550 + measuredContent(parentWidth, parentHeight, 500, 550); + } +} + +//FrameLayout +class GameWidget : HorizontalLayout { + + CupPage _cupPage; + this() { + super("GAME"); + _cupPage = new CupPage(); + addChild(_cupPage); + //showChild(_cupPage.id, Visibility.Invisible, true); + backgroundImageId = "tx_fabric.tiled"; + } + /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). + override void measure(int parentWidth, int parentHeight) { + super.measure(parentWidth, parentHeight); + measuredContent(parentWidth, parentHeight, 500, 550); + } +} diff --git a/examples/tetris/model.d b/examples/tetris/model.d new file mode 100644 index 00000000..72e00468 --- /dev/null +++ b/examples/tetris/model.d @@ -0,0 +1,419 @@ +module model; + +import std.random : uniform; + +/// Cell offset +struct FigureCell { + // horizontal offset + int dx; + // vertical offset + int dy; + this(int[2] v) { + dx = v[0]; + dy = v[1]; + } +} + +/// Single figure shape for some particular orientation - 4 cells +struct FigureShape { + /// by cell index 0..3 + FigureCell[4] cells; + /// lowest y coordinate - to show next figure above cup + int extent; + /// upper y coordinate - initial Y offset to place figure to cup + int y0; + this(int[2] c1, int[2] c2, int[2] c3, int[2] c4) { + cells[0] = FigureCell(c1); + cells[1] = FigureCell(c2); + cells[2] = FigureCell(c3); + cells[3] = FigureCell(c4); + extent = y0 = 0; + foreach (cell; cells) { + if (extent > cell.dy) + extent = cell.dy; + if (y0 < cell.dy) + y0 = cell.dy; + } + } +} + +struct Figure { + FigureShape[4] shapes; // by orientation + this(FigureShape[4] v) { + shapes = v; + } +} + +const Figure[6] FIGURES = [ + // ## #### + // 00## 00## + // ## + Figure([FigureShape([0, 0], [1, 0], [1, 1], [0, -1]), + FigureShape([0, 0], [0, 1], [-1, 1], [1, 0]), + FigureShape([0, 0], [1, 0], [1, 1], [0, -1]), + FigureShape([0, 0], [0, 1], [-1, 1], [1, 0])]), + // ## #### + // 00## ##00 + // ## + Figure([FigureShape([0, 0], [1, 0], [0, 1], [1, 1]), + FigureShape([0, 0], [0, 1], [1, 1], [-1, 0]), + FigureShape([0, 0], [1, 0], [0, 1], [1, 1]), + FigureShape([0, 0], [0, 1], [1, 1], [-1, 0])]), + // ## ## #### + // ##00## 00 ##00## 00 + // ## #### ## + Figure([FigureShape([0, 0], [1, 0], [-1,0], [-1,-1]), + FigureShape([0, 0], [0, 1], [0,-1], [ 1,-1]), + FigureShape([0, 0], [1, 0], [-1,0], [1, 1]), + FigureShape([0, 0], [0, 1], [-1,1], [0,-1])]), + // #### ## ## + // ##00## 00 ##00## 00 + // ## ## #### + Figure([FigureShape([0, 0], [1, 0], [-1,0], [ 1, 1]), + FigureShape([0, 0], [0, 1], [0,-1], [ 1, 1]), + FigureShape([0, 0], [1, 0], [-1,0], [-1, 1]), + FigureShape([0, 0], [0, 1], [-1,-1], [0, -1])]), + // #### + // 00## + // + Figure([FigureShape([0, 0], [1, 0], [0, 1], [ 1, 1]), + FigureShape([0, 0], [1, 0], [0, 1], [ 1, 1]), + FigureShape([0, 0], [1, 0], [0, 1], [ 1, 1]), + FigureShape([0, 0], [1, 0], [0, 1], [ 1, 1])]), + // ## + // ## + // 00 ##00#### + // ## + Figure([FigureShape([0, 0], [0, 1], [0, 2], [ 0,-1]), + FigureShape([0, 0], [1, 0], [2, 0], [-1, 0]), + FigureShape([0, 0], [0, 1], [0, 2], [ 0,-1]), + FigureShape([0, 0], [1, 0], [2, 0], [-1, 0])]), +]; + +enum : int { + WALL = -1, + EMPTY = 0, + FIGURE1, + FIGURE2, + FIGURE3, + FIGURE4, + FIGURE5, + FIGURE6, +} + +enum : int { + ORIENTATION0, + ORIENTATION90, + ORIENTATION180, + ORIENTATION270 +} + +const uint[6] _figureColors = [0xFF0000, 0xA0A000, 0xA000A0, 0x0000FF, 0x800000, 0x408000]; + +/// Figure type, orientation and position container +struct FigurePosition { + int index; + int orientation; + int x; + int y; + this(int index, int orientation, int x, int y) { + this.index = index; + this.orientation = orientation; + this.x = x; + this.y = y; + } + /// return rotated position CCW for angle=1, CW for angle=-1 + FigurePosition rotate(int angle) { + int newOrientation = (orientation + 4 + angle) & 3; + return FigurePosition(index, newOrientation, x, y); + } + /// return moved position + FigurePosition move(int dx, int dy = 0) { + return FigurePosition(index, orientation, x + dx, y + dy); + } + /// return shape for figure orientation + @property FigureShape shape() const { + return FIGURES[index - 1].shapes[orientation]; + } + /// return color for figure + @property uint color() const { + return _figureColors[index - 1]; + } + /// return true if figure index is not initialized + @property empty() const { + return index == 0; + } + /// clears content + void reset() { + index = 0; + } +} + +/** +Cup content + +Coordinates are relative to bottom left corner. +*/ +struct Cup { + private int[] _cup; + private int _cols; + private int _rows; + private bool[] _destroyedFullRows; + private int[] _cellGroups; + + private FigurePosition _currentFigure; + /// current figure index, orientation, position + @property ref FigurePosition currentFigure() { return _currentFigure; } + + private FigurePosition _nextFigure; + /// next figure + @property ref FigurePosition nextFigure() { return _nextFigure; } + + /// returns number of columns + @property int cols() { + return _cols; + } + /// returns number of columns + @property int rows() { + return _rows; + } + /// inits empty cup of specified size + void init(int cols, int rows) { + _cols = cols; + _rows = rows; + _cup = new int[_cols * _rows]; + _destroyedFullRows = new bool[_rows]; + _cellGroups = new int[_cols * _rows]; + } + /// returns cell content at specified position + int opIndex(int col, int row) { + if (col < 0 || row < 0 || col >= _cols || row >= _rows) + return WALL; + return _cup[row * _cols + col]; + } + /// set cell value + void opIndexAssign(int value, int col, int row) { + if (col < 0 || row < 0 || col >= _cols || row >= _rows) + return; // ignore modification of cells outside cup + _cup[row * _cols + col] = value; + } + /// put current figure into cup at current position and orientation + void putFigure() { + FigureShape shape = _currentFigure.shape; + foreach(cell; shape.cells) { + this[_currentFigure.x + cell.dx, _currentFigure.y + cell.dy] = _currentFigure.index; + } + } + + /// check if all cells where specified figure is located are free + bool isPositionFree(in FigurePosition pos) { + FigureShape shape = pos.shape; + foreach(cell; shape.cells) { + int value = this[pos.x + cell.dx, pos.y + cell.dy]; + if (value != 0) // occupied + return false; + } + return true; + } + /// returns true if specified row is full + bool isRowFull(int row) { + for (int i = 0; i < _cols; i++) + if (this[i, row] == EMPTY) + return false; + return true; + } + /// returns true if at least one row is full + @property bool hasFullRows() { + for (int i = 0; i < _rows; i++) + if (isRowFull(i)) + return true; + return false; + } + /// destroy all full rows, saving flags for destroyed rows; returns count of destroyed rows, 0 if no rows destroyed + int destroyFullRows() { + int res = 0; + for (int i = 0; i < _rows; i++) { + if (isRowFull(i)) { + _destroyedFullRows[i] = true; + res++; + for (int col = 0; col < _cols; col++) + this[col, i] = EMPTY; + } else { + _destroyedFullRows[i] = false; + } + } + return true; + } + + /// check if all cells where current figire is located are free + bool isPositionFree() { + return isPositionFree(_currentFigure); + } + + /// check if all cells where current figire is located are free + bool isPositionFreeBelow() { + return isPositionFree(_currentFigure.move(0, -1)); + } + + /// try to rotate current figure, returns true if figure rotated + bool rotate(int angle, bool falling) { + FigurePosition newpos = _currentFigure.rotate(angle); + if (isPositionFree(newpos)) { + if (falling) { + // special handling for fall animation + if (!isPositionFree(newpos.move(0, -1))) { + if (isPositionFreeBelow()) + return false; + } + } + _currentFigure = newpos; + return true; + } else if (isPositionFree(newpos.move(0, -1))) { + _currentFigure = newpos.move(0, -1); + return true; + } + return false; + } + + /// try to move current figure, returns true if figure rotated + bool move(int deltaX, int deltaY, bool falling) { + FigurePosition newpos = _currentFigure.move(deltaX, deltaY); + if (isPositionFree(newpos)) { + if (falling && !isPositionFree(newpos.move(0, -1))) { + if (isPositionFreeBelow()) + return false; + } + _currentFigure = newpos; + return true; + } + return false; + } + + void genNextFigure() { + _nextFigure.index = uniform(FIGURE1, FIGURE6 + 1); + _nextFigure.orientation = ORIENTATION0; + _nextFigure.x = _cols / 2; + _nextFigure.y = _rows - _nextFigure.shape.extent + 1; + } + + bool dropNextFigure() { + if (_nextFigure.empty) + genNextFigure(); + _currentFigure = _nextFigure; + _currentFigure.x = _cols / 2; + _currentFigure.y = _rows - 1 - _currentFigure.shape.y0; + return isPositionFree(); + } + + /// get cell group / falling cell value + private int cellGroup(int col, int row) { + if (col < 0 || row < 0 || col >= _cols || row >= _rows) + return 0; + return _cellGroups[col + row * _cols]; + } + + /// set cell group / falling cells value + private void setCellGroup(int value, int col, int row) { + _cellGroups[col + row * _cols] = value; + } + + /// recursive fill occupied area of cells with group id + private void fillCellGroup(int x, int y, int value) { + if (x < 0 || y < 0 || x >= _cols || y >= _rows) + return; + if (this[x, y] != EMPTY && cellGroup(x, y) == 0) { + setCellGroup(value, x, y); + fillCellGroup(x + 1, y, value); + fillCellGroup(x - 1, y, value); + fillCellGroup(x, y + 1, value); + fillCellGroup(x, y - 1, value); + } + } + + /// 1 == next cell below is occupied, 2 == one empty cell + private int distanceToOccupiedCellBelow(int col, int row) { + for (int y = row - 1; y >= -1; y--) { + if (this[col, y] != EMPTY) + return row - y; + } + return 1; + } + + /// mark cells in _cellGroups[] matrix which can fall down (value > 0 is distance to fall) + bool markFallingCells() { + _cellGroups = new int[_cols * _rows]; + int groupId = 1; + for (int y = 0; y < _rows; y++) { + for (int x = 0; x < _cols; x++) { + if (this[x, y] != EMPTY && cellGroup(x, y) == 0) { + fillCellGroup(x, y, groupId); + groupId++; + } + } + } + // check space below each group - can it fall down? + int[] spaceBelowGroup = new int[groupId]; + for (int y = 0; y < _rows; y++) { + for (int x = 0; x < _cols; x++) { + int group = cellGroup(x, y); + if (group > 0) { + if (y == 0) + spaceBelowGroup[group] = 1; + else if (this[x, y - 1] != EMPTY && cellGroup(x, y - 1) != group) + spaceBelowGroup[group] = 1; + else if (this[x, y - 1] == EMPTY) { + int dist = distanceToOccupiedCellBelow(x, y); + if (spaceBelowGroup[group] == 0 || spaceBelowGroup[group] > dist) + spaceBelowGroup[group] = dist; + } + } + } + } + // replace group IDs with distance to fall (0 == cell cannot fall) + for (int y = 0; y < _rows; y++) { + for (int x = 0; x < _cols; x++) { + int group = cellGroup(x, y); + if (group > 0) { + // distance to fall + setCellGroup(spaceBelowGroup[group] - 1, x, y); + } + } + } + bool canFall = false; + for (int i = 1; i < groupId; i++) + if (spaceBelowGroup[i] > 1) + canFall = true; + return canFall; + } + + /// moves all falling cells one cell down + /// returns true if there are more cells to fall + bool moveFallingCells() { + bool res = false; + for (int y = 0; y < _rows - 1; y++) { + for (int x = 0; x < _cols; x++) { + int dist = cellGroup(x, y + 1); + if (dist > 0) { + // move cell down, decreasing distance + setCellGroup(dist - 1, x, y); + this[x, y] = this[x, y + 1]; + setCellGroup(0, x, y + 1); + this[x, y + 1] = EMPTY; + if (dist > 1) + res = true; + } + } + } + return res; + } + + /// return true if cell is currently falling + bool isCellFalling(int col, int row) { + return cellGroup(col, row) > 0; + } + + @property bool hasNextFigure() { + return !_nextFigure.empty; + } +} + diff --git a/examples/tetris/res/tetris_logo_big.png b/examples/tetris/res/tetris_logo_big.png new file mode 100644 index 00000000..896901e3 Binary files /dev/null and b/examples/tetris/res/tetris_logo_big.png differ diff --git a/examples/tetris/src/main.d b/examples/tetris/src/main.d index fc5b482e..760473c9 100644 --- a/examples/tetris/src/main.d +++ b/examples/tetris/src/main.d @@ -16,950 +16,12 @@ Authors: Vadim Lopatin, coolreader.org@gmail.com module main; import dlangui.all; -//import dlangui.dialogs.dialog; -//import dlangui.dialogs.filedlg; -//import dlangui.dialogs.msgbox; -import dlangui.widgets.popup; -import dlangui.graphics.drawbuf; -import std.stdio; -import std.conv; -import std.utf; -import std.random; +import model; +import gui; mixin APP_ENTRY_POINT; -Widget createAboutWidget() -{ - LinearLayout res = new VerticalLayout(); - res.padding(Rect(10,10,10,10)); - res.addChild(new TextWidget(null, "DLangUI Tetris demo app"d)); - res.addChild(new TextWidget(null, "(C) Vadim Lopatin, 2014"d)); - res.addChild(new TextWidget(null, "http://github.com/buggins/dlangui"d)); - Button closeButton = new Button("close", "Close"d); - closeButton.onClickListener = delegate(Widget src) { - Log.i("Closing window"); - res.window.close(); - return true; - }; - res.addChild(closeButton); - return res; -} - -/// Cell offset -struct FigureCell { - // horizontal offset - int dx; - // vertical offset - int dy; - this(int[2] v) { - dx = v[0]; - dy = v[1]; - } -} - -/// Single figure shape for some particular orientation - 4 cells -struct FigureShape { - /// by cell index 0..3 - FigureCell[4] cells; - /// lowest y coordinate - to show next figure above cup - int extent; - /// upper y coordinate - initial Y offset to place figure to cup - int y0; - this(int[2] c1, int[2] c2, int[2] c3, int[2] c4) { - cells[0] = FigureCell(c1); - cells[1] = FigureCell(c2); - cells[2] = FigureCell(c3); - cells[3] = FigureCell(c4); - extent = y0 = 0; - foreach (cell; cells) { - if (extent > cell.dy) - extent = cell.dy; - if (y0 < cell.dy) - y0 = cell.dy; - } - } -} - -struct Figure { - FigureShape[4] shapes; // by orientation - this(FigureShape[4] v) { - shapes = v; - } -} - -const Figure[6] FIGURES = [ - // ## #### - // 00## 00## - // ## - Figure([FigureShape([0, 0], [1, 0], [1, 1], [0, -1]), - FigureShape([0, 0], [0, 1], [-1, 1], [1, 0]), - FigureShape([0, 0], [1, 0], [1, 1], [0, -1]), - FigureShape([0, 0], [0, 1], [-1, 1], [1, 0])]), - // ## #### - // 00## ##00 - // ## - Figure([FigureShape([0, 0], [1, 0], [0, 1], [1, 1]), - FigureShape([0, 0], [0, 1], [1, 1], [-1, 0]), - FigureShape([0, 0], [1, 0], [0, 1], [1, 1]), - FigureShape([0, 0], [0, 1], [1, 1], [-1, 0])]), - // ## ## #### - // ##00## 00 ##00## 00 - // ## #### ## - Figure([FigureShape([0, 0], [1, 0], [-1,0], [-1,-1]), - FigureShape([0, 0], [0, 1], [0,-1], [ 1,-1]), - FigureShape([0, 0], [1, 0], [-1,0], [1, 1]), - FigureShape([0, 0], [0, 1], [-1,1], [0,-1])]), - // #### ## ## - // ##00## 00 ##00## 00 - // ## ## #### - Figure([FigureShape([0, 0], [1, 0], [-1,0], [ 1, 1]), - FigureShape([0, 0], [0, 1], [0,-1], [ 1, 1]), - FigureShape([0, 0], [1, 0], [-1,0], [-1, 1]), - FigureShape([0, 0], [0, 1], [-1,-1], [0, -1])]), - // #### - // 00## - // - Figure([FigureShape([0, 0], [1, 0], [0, 1], [ 1, 1]), - FigureShape([0, 0], [1, 0], [0, 1], [ 1, 1]), - FigureShape([0, 0], [1, 0], [0, 1], [ 1, 1]), - FigureShape([0, 0], [1, 0], [0, 1], [ 1, 1])]), - // ## - // ## - // 00 ##00#### - // ## - Figure([FigureShape([0, 0], [0, 1], [0, 2], [ 0,-1]), - FigureShape([0, 0], [1, 0], [2, 0], [-1, 0]), - FigureShape([0, 0], [0, 1], [0, 2], [ 0,-1]), - FigureShape([0, 0], [1, 0], [2, 0], [-1, 0])]), -]; - -enum TetrisAction : int { - MoveLeft = 10000, - MoveRight, - RotateCCW, - FastDown, - Pause, - LevelUp, -} - -enum : int { - WALL = -1, - EMPTY = 0, - FIGURE1, - FIGURE2, - FIGURE3, - FIGURE4, - FIGURE5, - FIGURE6, -} - -enum : int { - ORIENTATION0, - ORIENTATION90, - ORIENTATION180, - ORIENTATION270 -} - -const uint[6] _figureColors = [0xFF0000, 0xA0A000, 0xA000A0, 0x0000FF, 0x800000, 0x408000]; - -/// Figure type, orientation and position container -struct FigurePosition { - int index; - int orientation; - int x; - int y; - this(int index, int orientation, int x, int y) { - this.index = index; - this.orientation = orientation; - this.x = x; - this.y = y; - } - /// return rotated position CCW for angle=1, CW for angle=-1 - FigurePosition rotate(int angle) { - int newOrientation = (orientation + 4 + angle) & 3; - return FigurePosition(index, newOrientation, x, y); - } - /// return moved position - FigurePosition move(int dx, int dy = 0) { - return FigurePosition(index, orientation, x + dx, y + dy); - } - /// return shape for figure orientation - @property FigureShape shape() const { - return FIGURES[index - 1].shapes[orientation]; - } - /// return color for figure - @property uint color() const { - return _figureColors[index - 1]; - } - /// return true if figure index is not initialized - @property empty() const { - return index == 0; - } - /// clears content - void reset() { - index = 0; - } -} - -/** - Cup content - - Coordinates are relative to bottom left corner. - */ -struct Cup { - private int[] _cup; - private int _cols; - private int _rows; - private bool[] _destroyedFullRows; - private int[] _cellGroups; - - private FigurePosition _currentFigure; - /// current figure index, orientation, position - @property ref FigurePosition currentFigure() { return _currentFigure; } - - private FigurePosition _nextFigure; - /// next figure - @property ref FigurePosition nextFigure() { return _nextFigure; } - - /// returns number of columns - @property int cols() { - return _cols; - } - /// returns number of columns - @property int rows() { - return _rows; - } - /// inits empty cup of specified size - void init(int cols, int rows) { - _cols = cols; - _rows = rows; - _cup = new int[_cols * _rows]; - _destroyedFullRows = new bool[_rows]; - _cellGroups = new int[_cols * _rows]; - } - /// returns cell content at specified position - int opIndex(int col, int row) { - if (col < 0 || row < 0 || col >= _cols || row >= _rows) - return WALL; - return _cup[row * _cols + col]; - } - /// set cell value - void opIndexAssign(int value, int col, int row) { - if (col < 0 || row < 0 || col >= _cols || row >= _rows) - return; // ignore modification of cells outside cup - _cup[row * _cols + col] = value; - } - /// put current figure into cup at current position and orientation - void putFigure() { - FigureShape shape = _currentFigure.shape; - foreach(cell; shape.cells) { - this[_currentFigure.x + cell.dx, _currentFigure.y + cell.dy] = _currentFigure.index; - } - } - - /// check if all cells where specified figure is located are free - bool isPositionFree(in FigurePosition pos) { - FigureShape shape = pos.shape; - foreach(cell; shape.cells) { - int value = this[pos.x + cell.dx, pos.y + cell.dy]; - if (value != 0) // occupied - return false; - } - return true; - } - /// returns true if specified row is full - bool isRowFull(int row) { - for (int i = 0; i < _cols; i++) - if (this[i, row] == EMPTY) - return false; - return true; - } - /// returns true if at least one row is full - @property bool hasFullRows() { - for (int i = 0; i < _rows; i++) - if (isRowFull(i)) - return true; - return false; - } - /// destroy all full rows, saving flags for destroyed rows; returns true if any rows were destroyed - bool destroyFullRows() { - bool res = false; - for (int i = 0; i < _rows; i++) { - if (isRowFull(i)) { - _destroyedFullRows[i] = true; - res = true; - for (int col = 0; col < _cols; col++) - this[col, i] = EMPTY; - } else { - _destroyedFullRows[i] = false; - } - } - return true; - } - - @property int lowestDestroyedRow() { - for (int i = 0; i < _rows; i++) { - if (_destroyedFullRows[i]) { - return i; - } - } - return -1; - } - - bool fallAfterDestroy() { - int bottomRow = lowestDestroyedRow; - if (bottomRow < 0) - return false; - for (int i = bottomRow; i < _rows; i++) { - _destroyedFullRows[i] = i < _rows - 1 ? _destroyedFullRows[i + 1] : false; - for (int col = 0; col < _cols; col++) { - this[col, i] = i < _rows - 1 ? this[col, i + 1] : EMPTY; - } - } - return true; - } - - /// check if all cells where current figire is located are free - bool isPositionFree() { - return isPositionFree(_currentFigure); - } - - /// check if all cells where current figire is located are free - bool isPositionFreeBelow() { - return isPositionFree(_currentFigure.move(0, -1)); - } - - /// try to rotate current figure, returns true if figure rotated - bool rotate(int angle, bool falling) { - FigurePosition newpos = _currentFigure.rotate(angle); - if (isPositionFree(newpos)) { - if (falling) { - // special handling for fall animation - if (!isPositionFree(newpos.move(0, -1))) { - if (isPositionFreeBelow()) - return false; - } - } - _currentFigure = newpos; - return true; - } else if (isPositionFree(newpos.move(0, -1))) { - _currentFigure = newpos.move(0, -1); - return true; - } - return false; - } - - /// try to move current figure, returns true if figure rotated - bool move(int deltaX, int deltaY, bool falling) { - FigurePosition newpos = _currentFigure.move(deltaX, deltaY); - if (isPositionFree(newpos)) { - if (falling && !isPositionFree(newpos.move(0, -1))) { - if (isPositionFreeBelow()) - return false; - } - _currentFigure = newpos; - return true; - } - return false; - } - - void genNextFigure() { - _nextFigure.index = uniform(FIGURE1, FIGURE6 + 1); - _nextFigure.orientation = ORIENTATION0; - _nextFigure.x = _cols / 2; - _nextFigure.y = _rows - _nextFigure.shape.extent + 1; - } - - bool dropNextFigure() { - if (_nextFigure.empty) - genNextFigure(); - _currentFigure = _nextFigure; - _currentFigure.x = _cols / 2; - _currentFigure.y = _rows - 1 - _currentFigure.shape.y0; - return isPositionFree(); - } - - /// get cell group / falling cell value - private int cellGroup(int col, int row) { - if (col < 0 || row < 0 || col >= _cols || row >= _rows) - return 0; - return _cellGroups[col + row * _cols]; - } - - /// set cell group / falling cells value - private void setCellGroup(int value, int col, int row) { - _cellGroups[col + row * _cols] = value; - } - - /// recursive fill occupied area of cells with group id - private void fillCellGroup(int x, int y, int value) { - if (x < 0 || y < 0 || x >= _cols || y >= _rows) - return; - if (this[x, y] != EMPTY && cellGroup(x, y) == 0) { - setCellGroup(value, x, y); - fillCellGroup(x + 1, y, value); - fillCellGroup(x - 1, y, value); - fillCellGroup(x, y + 1, value); - fillCellGroup(x, y - 1, value); - } - } - - /// 1 == next cell below is occupied, 2 == one empty cell - private int distanceToOccupiedCellBelow(int col, int row) { - for (int y = row - 1; y >= -1; y--) { - if (this[col, y] != EMPTY) - return row - y; - } - return 1; - } - - /// mark cells in _cellGroups[] matrix which can fall down (value > 0 is distance to fall) - bool markFallingCells() { - _cellGroups = new int[_cols * _rows]; - int groupId = 1; - for (int y = 0; y < _rows; y++) { - for (int x = 0; x < _cols; x++) { - if (this[x, y] != EMPTY && cellGroup(x, y) == 0) { - fillCellGroup(x, y, groupId); - groupId++; - } - } - } - // check space below each group - can it fall down? - int[] spaceBelowGroup = new int[groupId]; - for (int y = 0; y < _rows; y++) { - for (int x = 0; x < _cols; x++) { - int group = cellGroup(x, y); - if (group > 0) { - if (y == 0) - spaceBelowGroup[group] = 1; - else if (this[x, y - 1] != EMPTY && cellGroup(x, y - 1) != group) - spaceBelowGroup[group] = 1; - else if (this[x, y - 1] == EMPTY) { - int dist = distanceToOccupiedCellBelow(x, y); - if (spaceBelowGroup[group] == 0 || spaceBelowGroup[group] > dist) - spaceBelowGroup[group] = dist; - } - } - } - } - // replace group IDs with distance to fall (0 == cell cannot fall) - for (int y = 0; y < _rows; y++) { - for (int x = 0; x < _cols; x++) { - int group = cellGroup(x, y); - if (group > 0) { - // distance to fall - setCellGroup(spaceBelowGroup[group] - 1, x, y); - } - } - } - bool canFall = false; - for (int i = 1; i < groupId; i++) - if (spaceBelowGroup[i] > 1) - canFall = true; - return canFall; - } - - /// moves all falling cells one cell down - /// returns true if there are more cells to fall - bool moveFallingCells() { - bool res = false; - for (int y = 0; y < _rows - 1; y++) { - for (int x = 0; x < _cols; x++) { - int dist = cellGroup(x, y + 1); - if (dist > 0) { - // move cell down, decreasing distance - setCellGroup(dist - 1, x, y); - this[x, y] = this[x, y + 1]; - setCellGroup(0, x, y + 1); - this[x, y + 1] = EMPTY; - if (dist > 1) - res = true; - } - } - } - return res; - } - - /// return true if cell is currently falling - bool isCellFalling(int col, int row) { - return cellGroup(col, row) > 0; - } - -} - -/// Cup widget -class CupWidget : Widget { - /// cup columns count - int _cols; - /// cup rows count - int _rows; - /// cup data - Cup _cup; - - - /// Level 1..10 - int _level; - /// Single cell movement duration for current level, in 1/10000000 of seconds - long _movementDuration; - /// When true, figure is falling down fast - bool _fastDownFlag; - - AnimationHelper _animation; - private PopupWidget _gameOverPopup; - - /// Cup States - enum CupState : int { - /// New figure appears - NewFigure, - /// Game is paused - Paused, - /// Figure is falling - FallingFigure, - /// Figure is hanging - pause between falling by one row - HangingFigure, - /// destroying complete rows - DestroyingRows, - /// falling after some rows were destroyed - FallingRows, - /// Game is over - GameOver, - } - - protected CupState _state; - - static const int[10] LEVEL_SPEED = [15000000, 10000000, 7000000, 6000000, 5000000, 4000000, 3500000, 3000000, 2500000, 2000000]; - - - static const int RESERVED_ROWS = 5; // reserved for next figure - - - - /// set difficulty level 1..10 - void setLevel(int level) { - _level = level; - _movementDuration = LEVEL_SPEED[level - 1]; - } - - static const int MIN_FAST_FALLING_INTERVAL = 600000; - static const int ROWS_FALLING_INTERVAL = 600000; - - void setCupState(CupState state) { - int animationIntervalPercent = 100; - switch (state) { - case CupState.FallingFigure: - animationIntervalPercent = _fastDownFlag ? 10 : 25; - break; - case CupState.HangingFigure: - animationIntervalPercent = 75; - break; - case CupState.NewFigure: - animationIntervalPercent = 100; - break; - case CupState.FallingRows: - animationIntervalPercent = 25; - break; - case CupState.DestroyingRows: - animationIntervalPercent = 50; - break; - default: - break; - } - _state = state; - if (animationIntervalPercent) { - long interval = _movementDuration * animationIntervalPercent / 100; - if (_fastDownFlag && falling && interval > MIN_FAST_FALLING_INTERVAL) - interval = MIN_FAST_FALLING_INTERVAL; - if (_state == CupState.FallingRows) - interval = ROWS_FALLING_INTERVAL; - _animation.start(interval, 255); - } - invalidate(); - } - - /// returns true is widget is being animated - need to call animate() and redraw - override @property bool animating() { - switch (_state) { - case CupState.NewFigure: - case CupState.FallingFigure: - case CupState.HangingFigure: - case CupState.DestroyingRows: - case CupState.FallingRows: - return true; - default: - return false; - } - } - - /// Turn on / off fast falling down - bool handleFastDown(bool fast) { - if (fast == true) { - if (_fastDownFlag) - return false; - // handle turn on fast down - if (falling) { - _fastDownFlag = true; - // if already falling, just increase speed - _animation.interval = _movementDuration * 10 / 100; - if (_animation.interval > MIN_FAST_FALLING_INTERVAL) - _animation.interval = MIN_FAST_FALLING_INTERVAL; - return true; - } else if (_state == CupState.HangingFigure) { - _fastDownFlag = true; - setCupState(CupState.FallingFigure); - return true; - } else { - return false; - } - } - _fastDownFlag = fast; - return true; - } - - /// try start next figure - protected void nextFigure() { - if (!_cup.dropNextFigure()) { - // Game Over - setCupState(CupState.GameOver); - Widget popupWidget = new TextWidget("popup", "Game Over!"d); - popupWidget.padding(Rect(30, 30, 30, 30)).backgroundImageId("popup_background").alpha(0x40).fontWeight(800).fontSize(30); - _gameOverPopup = window.showPopup(popupWidget, this); - } else { - setCupState(CupState.NewFigure); - } - } - - protected void destroyFullRows() { - setCupState(CupState.DestroyingRows); - } - - protected void onAnimationFinished() { - switch (_state) { - case CupState.NewFigure: - _fastDownFlag = false; - _cup.genNextFigure(); - setCupState(CupState.HangingFigure); - break; - case CupState.FallingFigure: - if (_cup.isPositionFreeBelow()) { - _cup.move(0, -1, false); - if (_fastDownFlag) - setCupState(CupState.FallingFigure); - else - setCupState(CupState.HangingFigure); - } else { - // At bottom of cup - _cup.putFigure(); - _fastDownFlag = false; - if (_cup.hasFullRows) { - destroyFullRows(); - } else { - nextFigure(); - } - } - break; - case CupState.HangingFigure: - setCupState(CupState.FallingFigure); - break; - case CupState.DestroyingRows: - _cup.destroyFullRows(); - if (_cup.markFallingCells()) { - setCupState(CupState.FallingRows); - } else { - nextFigure(); - } - break; - case CupState.FallingRows: - if (_cup.moveFallingCells()) { - // more cells to fall - setCupState(CupState.FallingRows); - } else { - // no more cells to fall, next figure - nextFigure(); - } - break; - default: - break; - } - } - - /// animates window; interval is time left from previous draw, in hnsecs (1/10000000 of second) - override void animate(long interval) { - _animation.animate(interval); - if (_animation.finished) { - onAnimationFinished(); - } - } - - void init(int cols, int rows) { - _cup.init(cols, rows); - _cols = cols; - _rows = rows; - } - - protected Rect cellRect(Rect rc, int col, int row) { - int dx = rc.width / _cols; - int dy = rc.height / (_rows + RESERVED_ROWS); - int dd = dx; - if (dd > dy) - dd = dy; - int x0 = rc.left + (rc.width - dd * _cols) / 2 + dd * col; - int y0 = rc.bottom - (rc.height - dd * (_rows + RESERVED_ROWS)) / 2 - dd * row - dd; - return Rect(x0, y0, x0 + dd, y0 + dd); - } - - /// Handle keys - override bool onKeyEvent(KeyEvent event) { - if (event.action == KeyAction.KeyDown && _state == CupState.GameOver) { - // restart game - newGame(); - return true; - } - if (event.action == KeyAction.KeyDown && _state == CupState.NewFigure) { - // stop new figure fade in if key is pressed - onAnimationFinished(); - } - if (event.keyCode == KeyCode.DOWN) { - if (event.action == KeyAction.KeyDown) { - handleFastDown(true); - } else if (event.action == KeyAction.KeyUp) { - handleFastDown(false); - } - return true; - } - if ((event.action == KeyAction.KeyDown || event.action == KeyAction.KeyUp) && event.keyCode != KeyCode.SPACE) - handleFastDown(false); // don't stop fast down on Space key KeyUp - return super.onKeyEvent(event); - } - - /// draw cup cell - protected void drawCell(DrawBuf buf, Rect cellRc, uint color, int offset = 0) { - cellRc.top += offset; - cellRc.bottom += offset; - - cellRc.right--; - cellRc.bottom--; - - int w = cellRc.width / 6; - buf.drawFrame(cellRc, color, Rect(w,w,w,w)); - cellRc.shrink(w, w); - color = addAlpha(color, 0xC0); - buf.fillRect(cellRc, color); - } - - /// draw figure - protected void drawFigure(DrawBuf buf, Rect rc, FigurePosition figure, int dy, uint alpha = 0) { - uint color = addAlpha(_figureColors[figure.index - 1], alpha); - FigureShape shape = figure.shape; - foreach(cell; shape.cells) { - Rect cellRc = cellRect(rc, figure.x + cell.dx, figure.y + cell.dy); - cellRc.top += dy; - cellRc.bottom += dy; - drawCell(buf, cellRc, color); - } - } - - /// Draw widget at its position to buffer - override void onDraw(DrawBuf buf) { - super.onDraw(buf); - Rect rc = _pos; - applyMargins(rc); - auto saver = ClipRectSaver(buf, rc, alpha); - applyPadding(rc); - - Rect topLeft = cellRect(rc, 0, _rows - 1); - Rect bottomRight = cellRect(rc, _cols - 1, 0); - Rect cupRc = Rect(topLeft.left, topLeft.top, bottomRight.right, bottomRight.bottom); - - int fw = 7; - int dw = 0; - uint fcl = 0xA0606090; - buf.fillRect(cupRc, 0xC0A0C0B0); - buf.fillRect(Rect(cupRc.left - dw - fw, cupRc.top, cupRc.left - dw, cupRc.bottom + dw), fcl); - buf.fillRect(Rect(cupRc.right + dw, cupRc.top, cupRc.right + dw + fw, cupRc.bottom + dw), fcl); - buf.fillRect(Rect(cupRc.left - dw - fw, cupRc.bottom + dw, cupRc.right + dw + fw, cupRc.bottom + dw + fw), fcl); - - int fallingCellOffset = 0; - if (_state == CupState.FallingRows) { - fallingCellOffset = _animation.getProgress(topLeft.height); - } - - for (int row = 0; row < _rows; row++) { - uint cellAlpha = 0; - if (_state == CupState.DestroyingRows && _cup.isRowFull(row)) - cellAlpha = _animation.progress; - for (int col = 0; col < _cols; col++) { - - int value = _cup[col, row]; - Rect cellRc = cellRect(rc, col, row); - - Point middle = cellRc.middle; - buf.fillRect(Rect(middle.x - 1, middle.y - 1, middle.x + 1, middle.y + 1), 0x80404040); - - if (value != EMPTY) { - uint cl = addAlpha(_figureColors[value - 1], cellAlpha); - int offset = fallingCellOffset > 0 && _cup.isCellFalling(col, row) ? fallingCellOffset : 0; - drawCell(buf, cellRc, cl, offset); - } - } - } - - // draw current figure falling - if (_state == CupState.FallingFigure || _state == CupState.HangingFigure) { - int dy = 0; - if (falling && _cup.isPositionFreeBelow()) - dy = _animation.getProgress(topLeft.height); - drawFigure(buf, rc, _cup.currentFigure, dy, 0); - } - - // draw next figure - if (!_cup._nextFigure.empty) { - //auto shape = _nextFigure.shape; - uint nextFigureAlpha = 0; - if (_state == CupState.NewFigure) { - nextFigureAlpha = _animation.progress; - drawFigure(buf, rc, _cup.currentFigure, 0, 255 - nextFigureAlpha); - } - if (_state != CupState.GameOver) { - drawFigure(buf, rc, _cup.nextFigure, 0, blendAlpha(0xA0, nextFigureAlpha)); - } - } - - } - /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). - override void measure(int parentWidth, int parentHeight) { - /// fixed size 350 x 550 - measuredContent(parentWidth, parentHeight, 350, 550); - } - - @property bool falling() { - return _state == CupState.FallingFigure; - } - - /// override to handle specific actions - override bool handleAction(const Action a) { - switch (a.id) { - case TetrisAction.MoveLeft: - _cup.move(-1, 0, falling); - return true; - case TetrisAction.MoveRight: - _cup.move(1, 0, falling); - return true; - case TetrisAction.RotateCCW: - _cup.rotate(1, falling); - return true; - case TetrisAction.FastDown: - handleFastDown(true); - return true; - case TetrisAction.Pause: - // TODO: implement pause - return true; - case TetrisAction.LevelUp: - if (_level < 10) - _level++; - // TODO: update state - return true; - default: - if (parent) // by default, pass to parent widget - return parent.handleAction(a); - return false; - } - } - - /// start new game - void newGame() { - setLevel(1); - init(_cols, _rows); - _cup.dropNextFigure(); - setCupState(CupState.NewFigure); - if (window && _gameOverPopup) { - window.removePopup(_gameOverPopup); - _gameOverPopup = null; - } - } - - this() { - super("CUP"); - layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).layoutWeight(3); - setState(State.Default); - backgroundColor = 0xC0808080; - padding(Rect(20, 20, 20, 20)); - _cols = 11; - _rows = 15; - newGame(); - - focusable = true; - - acceleratorMap.add( [ - (new Action(TetrisAction.MoveLeft, KeyCode.LEFT)).addAccelerator(KeyCode.KEY_A), - (new Action(TetrisAction.MoveRight, KeyCode.RIGHT)).addAccelerator(KeyCode.KEY_D), - (new Action(TetrisAction.RotateCCW, KeyCode.UP)).addAccelerator(KeyCode.KEY_W), - (new Action(TetrisAction.FastDown, KeyCode.SPACE)).addAccelerator(KeyCode.KEY_S), - (new Action(TetrisAction.Pause, KeyCode.ESCAPE)).addAccelerator(KeyCode.PAUSE), - (new Action(TetrisAction.LevelUp, KeyCode.ADD)).addAccelerator(KeyCode.INS), - ]); - - } -} - -class StatusWidget : VerticalLayout { - TextWidget _score; - TextWidget _lblNext; - this() { - super("CUP_STATUS"); - _score = new TextWidget("SCORE", "Score: 0"d); - _lblNext = new TextWidget("NEXT", "Next:"d); - backgroundColor = 0xC080FF80; - addChild(_score); - addChild(_lblNext); - layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).layoutWeight(2); - padding(Rect(20, 20, 20, 20)); - } - /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). - override void measure(int parentWidth, int parentHeight) { - super.measure(parentWidth, parentHeight); - /// fixed size 350 x 550 - measuredContent(parentWidth, parentHeight, 150, 550); - } -} - -class CupPage : HorizontalLayout { - CupWidget _cup; - StatusWidget _status; - this() { - super("CUP_PAGE"); - layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); - _cup = new CupWidget(); - _status = new StatusWidget(); - addChild(_cup); - addChild(_status); - } - /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). - override void measure(int parentWidth, int parentHeight) { - super.measure(parentWidth, parentHeight); - /// fixed size 350 x 550 - measuredContent(parentWidth, parentHeight, 500, 550); - } -} - -//FrameLayout -class GameWidget : HorizontalLayout { - - CupPage _cupPage; - this() { - super("GAME"); - _cupPage = new CupPage(); - addChild(_cupPage); - //showChild(_cupPage.id, Visibility.Invisible, true); - backgroundImageId = "tx_fabric.tiled"; - } - /// Measure widget according to desired width and height constraints. (Step 1 of two phase layout). - override void measure(int parentWidth, int parentHeight) { - super.measure(parentWidth, parentHeight); - measuredContent(parentWidth, parentHeight, 500, 550); - } -} /// entry point for dlangui based application extern (C) int UIAppMain(string[] args) { @@ -969,10 +31,10 @@ extern (C) int UIAppMain(string[] args) { // resource directory search paths string[] resourceDirs = [ - appendPath(exePath, "../../../res/"), // for Visual D and DUB builds - appendPath(exePath, "../../../res/mdpi/"), // for Visual D and DUB builds - appendPath(exePath, "../../../../res/"),// for Mono-D builds - appendPath(exePath, "../../../../res/mdpi/"),// for Mono-D builds + appendPath(exePath, "../../../res/"), // for Visual D and DUB builds + appendPath(exePath, "../../../res/mdpi/"), // for Visual D and DUB builds + appendPath(exePath, "../../../../res/"),// for Mono-D builds + appendPath(exePath, "../../../../res/mdpi/"),// for Mono-D builds appendPath(exePath, "res/"), // when res dir is located at the same directory as executable appendPath(exePath, "../res/"), // when res dir is located at project directory appendPath(exePath, "../../res/"), // when res dir is located at the same directory as executable diff --git a/examples/tetris/tetris.visualdproj b/examples/tetris/tetris.visualdproj index 7d8fe575..71879d10 100644 --- a/examples/tetris/tetris.visualdproj +++ b/examples/tetris/tetris.visualdproj @@ -189,6 +189,8 @@ *.obj;*.cmd;*.build;*.json;*.dep + +