mirror of https://github.com/buggins/dlangide.git
445 lines
14 KiB
D
445 lines
14 KiB
D
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
|
|
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;
|
|
/// 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);
|
|
extent = y0 = 0;
|
|
foreach (cell; cells) {
|
|
if (extent > cell.dy)
|
|
extent = cell.dy;
|
|
if (y0 < cell.dy)
|
|
y0 = cell.dy;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Figure data - shapes for 4 orientations
|
|
struct Figure {
|
|
FigureShape[4] shapes; // by orientation
|
|
this(FigureShape[4] v) {
|
|
shapes = v;
|
|
}
|
|
}
|
|
|
|
/// All shapes
|
|
const Figure[7] FIGURES = [
|
|
// FIGURE1 ===========================================
|
|
// ## ####
|
|
// 00## 00##
|
|
// ##
|
|
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([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([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([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([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, 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])]),
|
|
];
|
|
|
|
/// colors for different figure types
|
|
const uint[7] _figureColors = [0xC00000, 0x80A000, 0xA00080, 0x0000C0, 0x800020, 0x408000, 0x204000];
|
|
|
|
/// 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 res;
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
/// random next figure
|
|
void genNextFigure() {
|
|
_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();
|
|
_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;
|
|
}
|
|
|
|
/// returns true if next figure is generated
|
|
@property bool hasNextFigure() {
|
|
return !_nextFigure.empty;
|
|
}
|
|
}
|
|
|