Tetris example: refactoring

This commit is contained in:
Vadim Lopatin 2014-12-26 13:19:40 +03:00
parent a29fdb0fa1
commit 52b21b9d88
5 changed files with 990 additions and 944 deletions

563
examples/tetris/gui.d Normal file
View File

@ -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);
}
}

419
examples/tetris/model.d Normal file
View File

@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

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

View File

@ -189,6 +189,8 @@
<filesToClean>*.obj;*.cmd;*.build;*.json;*.dep</filesToClean>
</Config>
<Folder name="tetris">
<File path="gui.d" />
<File path="src\main.d" />
<File path="model.d" />
</Folder>
</DProject>