dlangide/workspaces/tetris/src/model.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;
}
}