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
+
+