Tetris example improvements

This commit is contained in:
Vadim Lopatin 2014-12-26 14:20:12 +03:00
parent af5a631051
commit a77a20e6e7
5 changed files with 222 additions and 176 deletions

View File

@ -1,14 +1,29 @@
module gui;
import dlangui.all;
import dlangui.widgets.popup;
import dlangui.graphics.drawbuf;
//import std.stdio;
import std.conv;
import std.utf;
import model;
import dlangui.all;
/// game action codes
enum TetrisAction : int {
MoveLeft = 10000,
MoveRight,
RotateCCW,
FastDown,
Pause,
LevelUp,
}
const Action ACTION_MOVE_LEFT = (new Action(TetrisAction.MoveLeft, KeyCode.LEFT)).addAccelerator(KeyCode.KEY_A);
const Action ACTION_MOVE_RIGHT = (new Action(TetrisAction.MoveRight, KeyCode.RIGHT)).addAccelerator(KeyCode.KEY_D);
const Action ACTION_ROTATE = (new Action(TetrisAction.RotateCCW, KeyCode.UP)).addAccelerator(KeyCode.KEY_W);
const Action ACTION_FAST_DOWN = (new Action(TetrisAction.FastDown, KeyCode.SPACE)).addAccelerator(KeyCode.KEY_S);
const Action ACTION_PAUSE = (new Action(TetrisAction.Pause, KeyCode.ESCAPE)).addAccelerator(KeyCode.PAUSE);
const Action ACTION_LEVEL_UP = (new Action(TetrisAction.LevelUp, KeyCode.ADD)).addAccelerator(KeyCode.INS);
const Action[] CUP_ACTIONS = [ACTION_MOVE_LEFT, ACTION_MOVE_RIGHT, ACTION_ROTATE, ACTION_FAST_DOWN, ACTION_PAUSE, ACTION_LEVEL_UP];
/// about dialog
Widget createAboutWidget()
{
LinearLayout res = new VerticalLayout();
@ -26,16 +41,24 @@ Widget createAboutWidget()
return res;
}
enum TetrisAction : int {
MoveLeft = 10000,
MoveRight,
RotateCCW,
FastDown,
Pause,
LevelUp,
/// 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,
}
/// Cup widget
class CupWidget : Widget {
/// cup columns count
@ -54,37 +77,21 @@ class CupWidget : Widget {
long _movementDuration;
/// When true, figure is falling down fast
bool _fastDownFlag;
/// animation helper for fade and movement in different states
AnimationHelper _animation;
/// GameOver popup
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,
}
/// Status widget
private StatusWidget _status;
/// Current state
protected CupState _state;
protected int _totalRowsDestroyed;
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;
@ -93,8 +100,10 @@ class CupWidget : Widget {
}
static const int MIN_FAST_FALLING_INTERVAL = 600000;
static const int ROWS_FALLING_INTERVAL = 600000;
static const int ROWS_FALLING_INTERVAL = 1200000;
/// change game state, init state animation when necessary
void setCupState(CupState state) {
int animationIntervalPercent = 100;
switch (state) {
@ -114,6 +123,8 @@ class CupWidget : Widget {
animationIntervalPercent = 50;
break;
default:
// no animation for other states
animationIntervalPercent = 0;
break;
}
_state = state;
@ -128,18 +139,14 @@ class CupWidget : Widget {
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;
}
void addScore(int score) {
_score += score;
_status.setScore(_score);
}
/// returns true if figure is in falling - movement state
@property bool falling() {
return _state == CupState.FallingFigure;
}
/// Turn on / off fast falling down
@ -167,6 +174,8 @@ class CupWidget : Widget {
return true;
}
static const int[] NEXT_LEVEL_SCORE = [0, 20, 50, 100, 200, 350, 500, 750, 1000, 1500, 2000];
/// try start next figure
protected void nextFigure() {
if (!_cup.dropNextFigure()) {
@ -177,6 +186,8 @@ class CupWidget : Widget {
_gameOverPopup = window.showPopup(popupWidget, this);
} else {
setCupState(CupState.NewFigure);
if (_level < 10 && _totalRowsDestroyed >= NEXT_LEVEL_SCORE[_level])
setLevel(_level + 1); // level up
}
}
@ -214,6 +225,8 @@ class CupWidget : Widget {
break;
case CupState.DestroyingRows:
int rowsDestroyed = _cup.destroyFullRows();
_totalRowsDestroyed += rowsDestroyed;
_status.setRowsDestroyed(_totalRowsDestroyed);
int scorePerRow = 0;
for (int i = 0; i < rowsDestroyed; i++) {
scorePerRow += 10;
@ -245,14 +258,23 @@ class CupWidget : Widget {
}
}
/// 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();
/// start new game
void newGame() {
setLevel(1);
init(_cols, _rows);
_cup.dropNextFigure();
setCupState(CupState.NewFigure);
if (window && _gameOverPopup) {
window.removePopup(_gameOverPopup);
_gameOverPopup = null;
}
_score = 0;
_status.setScore(0);
_totalRowsDestroyed = 0;
_status.setRowsDestroyed(0);
}
/// init cup
void init(int cols, int rows) {
_cup.init(cols, rows);
_cols = cols;
@ -321,6 +343,31 @@ class CupWidget : Widget {
}
}
//=================================================================================================
// Overrides of Widget methods
/// 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;
}
}
/// 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();
}
}
/// Draw widget at its position to buffer
override void onDraw(DrawBuf buf) {
super.onDraw(buf);
@ -387,15 +434,6 @@ class CupWidget : Widget {
}
}
}
/// 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
@ -428,54 +466,30 @@ class CupWidget : Widget {
}
}
void addScore(int score) {
_score += score;
_status.setScore(_score);
/// Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
override void measure(int parentWidth, int parentHeight) {
measuredContent(parentWidth, parentHeight, parentWidth * 3 / 5, parentHeight);
}
/// 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;
layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).layoutWeight(3).setState(State.Default).focusable(true).padding(Rect(20, 20, 20, 20));
_cols = 10;
_rows = 18;
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),
]);
acceleratorMap.add(CUP_ACTIONS);
}
}
/// Panel to show game status
class StatusWidget : VerticalLayout {
private TextWidget _level;
private TextWidget _rowsDestroyed;
private TextWidget _score;
private CupWidget _cup;
void setCup(CupWidget cup) {
@ -483,26 +497,27 @@ class StatusWidget : VerticalLayout {
}
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);
res.layoutWidth(FILL_PARENT).alignment(Align.Center).fontSize(25).textColor(color);
return res;
}
this() {
super("CUP_STATUS");
//backgroundColor = 0xC080FF80;
addChild(new VSpacer());
addChild(new ImageWidget(null, "tetris_logo_big"));
addChild((new ImageWidget(null, "tetris_logo_big")).layoutWidth(FILL_PARENT).alignment(Align.Center));
addChild(new VSpacer());
addChild(createTextWidget("Level:"d, 0x008000));
addChild((_level = createTextWidget(""d, 0x008000)));
addChild(new VSpacer());
addChild(createTextWidget("Rows:"d, 0x202080));
addChild((_rowsDestroyed = createTextWidget(""d, 0x202080)));
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));
layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT).layoutWeight(2).padding(Rect(20, 20, 20, 20));
}
void setLevel(int level) {
@ -513,17 +528,16 @@ class StatusWidget : VerticalLayout {
_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);
void setRowsDestroyed(int rows) {
_rowsDestroyed.text = toUTF32(to!string(rows));
}
override bool handleAction(const Action a) {
return _cup.handleAction(a);
}
}
/// Cup page: cup widget + status widget
class CupPage : HorizontalLayout {
CupWidget _cup;
StatusWidget _status;
@ -539,13 +553,13 @@ class CupPage : HorizontalLayout {
/// 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);
/// fixed size
measuredContent(parentWidth, parentHeight, 550, 550);
}
}
//FrameLayout
class GameWidget : HorizontalLayout {
//
class GameWidget : FrameLayout {
CupPage _cupPage;
this() {
@ -555,9 +569,4 @@ class GameWidget : HorizontalLayout {
//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);
}
}

View File

@ -19,10 +19,9 @@ import dlangui.all;
import model;
import gui;
/// Required for Windows platform: DMD cannot find WinMain if it's in library
mixin APP_ENTRY_POINT;
/// entry point for dlangui based application
extern (C) int UIAppMain(string[] args) {
@ -50,14 +49,10 @@ extern (C) int UIAppMain(string[] args) {
// load theme from file "theme_default.xml"
Platform.instance.uiTheme = "theme_default";
//drawableCache.get("tx_fabric.tiled");
// create window
Window window = Platform.instance.createWindow("DLangUI: Tetris game example", null, WindowFlag.Modal);
Window window = Platform.instance.createWindow("DLangUI: Tetris game example"d, null, WindowFlag.Modal);
GameWidget game = new GameWidget();
window.mainWidget = game;
window.mainWidget = new GameWidget();
window.windowIcon = drawableCache.getImage("dtetris-logo1");

View File

@ -2,6 +2,28 @@ module model;
import std.random : uniform;
/// Cell codes
enum : int {
WALL = -1,
EMPTY = 0,
FIGURE1,
FIGURE2,
FIGURE3,
FIGURE4,
FIGURE5,
FIGURE6,
FIGURE7,
}
/// Orientations
enum : int {
ORIENTATION0,
ORIENTATION90,
ORIENTATION180,
ORIENTATION270
}
/// Cell offset
struct FigureCell {
// horizontal offset
@ -22,8 +44,9 @@ struct FigureShape {
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);
/// Init cells (cell 0 is [0,0])
this(int[2] c2, int[2] c3, int[2] c4) {
cells[0] = FigureCell([0, 0]);
cells[1] = FigureCell(c2);
cells[2] = FigureCell(c3);
cells[3] = FigureCell(c4);
@ -37,6 +60,7 @@ struct FigureShape {
}
}
/// Figure data - shapes for 4 orientations
struct Figure {
FigureShape[4] shapes; // by orientation
this(FigureShape[4] v) {
@ -44,71 +68,69 @@ struct Figure {
}
}
const Figure[6] FIGURES = [
/// All shapes
const Figure[7] FIGURES = [
// FIGURE1 ===========================================
// ## ####
// 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])]),
Figure([FigureShape([1, 0], [1, 1], [0, -1]),
FigureShape([0, 1], [-1, 1], [1, 0]),
FigureShape([1, 0], [1, 1], [0, -1]),
FigureShape([0, 1], [-1, 1], [1, 0])]),
// FIGURE2 ===========================================
// ## ####
// 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])]),
Figure([FigureShape([1, 0], [0, 1], [1, 1]),
FigureShape([0, 1], [1, 1], [-1, 0]),
FigureShape([1, 0], [0, 1], [1, 1]),
FigureShape([0, 1], [1, 1], [-1, 0])]),
// FIGURE3 ===========================================
// ## ## ####
// ##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])]),
Figure([FigureShape([1, 0], [-1,0], [-1,-1]),
FigureShape([0, 1], [0,-1], [ 1,-1]),
FigureShape([1, 0], [-1,0], [1, 1]),
FigureShape([0, 1], [-1,1], [0,-1])]),
// FIGURE4 ===========================================
// #### ## ##
// ##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])]),
Figure([FigureShape([1, 0], [-1,0], [ 1,-1]),
FigureShape([0, 1], [0,-1], [ 1, 1]),
FigureShape([1, 0], [-1,0], [-1, 1]),
FigureShape([0, 1], [-1,-1], [0, -1])]),
// FIGURE5 ===========================================
// ####
// 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])]),
Figure([FigureShape([1, 0], [0, 1], [ 1, 1]),
FigureShape([1, 0], [0, 1], [ 1, 1]),
FigureShape([1, 0], [0, 1], [ 1, 1]),
FigureShape([1, 0], [0, 1], [ 1, 1])]),
// FIGURE6 ===========================================
// ##
// ##
// 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])]),
Figure([FigureShape([0, 1], [0, 2], [ 0,-1]),
FigureShape([1, 0], [2, 0], [-1, 0]),
FigureShape([0, 1], [0, 2], [ 0,-1]),
FigureShape([1, 0], [2, 0], [-1, 0])]),
// FIGURE7 ===========================================
// ## ## ##
// ##00## 00## ##00## ##00
// ## ## ##
Figure([FigureShape([1, 0], [-1,0], [ 0,-1]),
FigureShape([0, 1], [0,-1], [ 1, 0]),
FigureShape([1, 0], [-1,0], [ 0, 1]),
FigureShape([0, 1], [0,-1], [-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];
/// colors for different figure types
const uint[7] _figureColors = [0xC00000, 0x80A000, 0xA00080, 0x0000C0, 0x800020, 0x408000, 0x204000];
/// Figure type, orientation and position container
struct FigurePosition {
@ -289,13 +311,15 @@ struct Cup {
return false;
}
/// random next figure
void genNextFigure() {
_nextFigure.index = uniform(FIGURE1, FIGURE6 + 1);
_nextFigure.index = uniform(FIGURE1, FIGURE7 + 1);
_nextFigure.orientation = ORIENTATION0;
_nextFigure.x = _cols / 2;
_nextFigure.y = _rows - _nextFigure.shape.extent + 1;
}
/// New figure: put it on top of cup
bool dropNextFigure() {
if (_nextFigure.empty)
genNextFigure();
@ -412,6 +436,7 @@ struct Cup {
return cellGroup(col, row) > 0;
}
/// returns true if next figure is generated
@property bool hasNextFigure() {
return !_nextFigure.empty;
}

View File

@ -48,7 +48,8 @@ module dlangui.all;
public import dlangui.core.logger;
public import dlangui.core.types;
public import dlangui.platforms.common.platform;
public import dlangui.core.i18n;
public import dlangui.core.files;
public import dlangui.graphics.images;
public import dlangui.widgets.widget;
public import dlangui.widgets.controls;
@ -61,6 +62,11 @@ public import dlangui.widgets.editors;
public import dlangui.widgets.grid;
public import dlangui.widgets.tree;
public import dlangui.widgets.combobox;
public import dlangui.widgets.popup;
public import dlangui.graphics.fonts;
public import dlangui.core.i18n;
public import dlangui.core.files;
public import dlangui.graphics.drawbuf;
public import dlangui.platforms.common.platform;
// some useful imports from Phobos
public import std.conv : to;
public import std.utf : toUTF32, toUTF8;

View File

@ -140,6 +140,10 @@ class Action {
@property Accelerator[] accelerators() {
return _accelerators;
}
/// returs const array of accelerators
@property const(Accelerator)[] accelerators() const {
return _accelerators;
}
/// returns text description for first accelerator of action; null if no accelerators
@property dstring acceleratorText() {
if (_accelerators.length < 1)
@ -217,6 +221,13 @@ struct ActionMap {
_map[acc] = a;
}
}
/// Add array of actions
void add(const Action[] items) {
foreach(a; items) {
foreach(acc; a.accelerators)
_map[acc] = a.clone;
}
}
/// Add action
void add(Action a) {
foreach(acc; a.accelerators)