Files
wordclock/src/games/tetris.cpp
2023-08-21 20:26:38 +02:00

680 lines
18 KiB
C++

/**
* @file tetris.cpp
* @author techniccontroller (mail[at]techniccontroller.com)
* @brief Class implementation for tetris game
* @version 0.1
* @date 2022-03-05
*
* @copyright Copyright (c) 2022
*
* main tetris code originally written by Klaas De Craemer, Ing. David Hrbaty
*
*/
#include "tetris.h"
Tetris::Tetris()
{
}
/**
* @brief Construct a new Tetris:: Tetris object
*
* @param myledmatrix pointer to LEDMatrix object, need to provide gridAddPixel(x, y, col), draw_on_matrix(), gridFlush() and printNumber(x,y,n,col)
* @param mylogger pointer to UDPLogger object, need to provide a function log_string(message)
*/
Tetris::Tetris(LEDMatrix *myledmatrix, UDPLogger *mylogger)
{
_logger = mylogger;
_ledmatrix = myledmatrix;
_gameStatet = GAME_STATE_READY;
}
/**
* @brief Run main loop for one cycle
*
*/
void Tetris::loopCycle()
{
switch (_gameStatet)
{
case GAME_STATE_READY:
break;
case GAME_STATE_INIT:
tetrisInit();
break;
case GAME_STATE_RUNNING:
// If brick is still "on the loose", then move it down by one
if (_activeBrick.enabled)
{
// move faster down when allow drop
if (_allowdrop)
{
if (millis() > _droptime + 50)
{
_droptime = millis();
shiftActiveBrick(DIR_DOWN);
printField();
}
}
// move down with regular speed
if ((millis() - _prevUpdateTime) > (_brickSpeed * _speedtetris / 100))
{
_prevUpdateTime = millis();
shiftActiveBrick(DIR_DOWN);
printField();
}
}
else
{
_allowdrop = false;
// Active brick has "crashed", check for full lines
// and create new brick at top of field
checkFullLines();
newActiveBrick();
_prevUpdateTime = millis(); // Reset update time to avoid brick dropping two spaces
}
break;
case GAME_STATE_PAUSED:
break;
case GAME_STATE_END:
// at game end show all bricks on field in red color for 1.5 seconds, then show score
if (_tetrisGameOver == true)
{
_tetrisGameOver = false;
(*_logger).log_string("Tetris: end");
everythingRed();
_tetrisshowscore = millis();
}
if (millis() > (_tetrisshowscore + RED_END_TIME))
{
resetLEDs();
_score = _nbRowsTotal;
showscore();
_gameStatet = GAME_STATE_READY;
}
break;
}
}
/**
* @brief Trigger control: START (& restart)
*
*/
void Tetris::ctrlStart()
{
if (millis() > _lastButtonClick + DEBOUNCE_TIME)
{
_lastButtonClick = millis();
_gameStatet = GAME_STATE_INIT;
}
}
/**
* @brief Trigger control: PAUSE/PLAY
*
*/
void Tetris::ctrlPlayPause()
{
if (millis() > _lastButtonClick + DEBOUNCE_TIME)
{
_lastButtonClick = millis();
if (_gameStatet == GAME_STATE_PAUSED)
{
(*_logger).log_string("Tetris: continue");
_gameStatet = GAME_STATE_RUNNING;
}
else if (_gameStatet == GAME_STATE_RUNNING)
{
(*_logger).log_string("Tetris: pause");
_gameStatet = GAME_STATE_PAUSED;
}
}
}
/**
* @brief Trigger control: RIGHT
*
*/
void Tetris::ctrlRight()
{
if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameStatet == GAME_STATE_RUNNING)
{
_lastButtonClick = millis();
shiftActiveBrick(DIR_RIGHT);
printField();
}
}
/**
* @brief Trigger control: LEFT
*
*/
void Tetris::ctrlLeft()
{
if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameStatet == GAME_STATE_RUNNING)
{
_lastButtonClick = millis();
shiftActiveBrick(DIR_LEFT);
printField();
}
}
/**
* @brief Trigger control: UP (rotate)
*
*/
void Tetris::ctrlUp()
{
if (millis() > _lastButtonClick + DEBOUNCE_TIME && _gameStatet == GAME_STATE_RUNNING)
{
_lastButtonClick = millis();
rotateActiveBrick();
printField();
}
}
/**
* @brief Trigger control: DOWN (drop)
*
*/
void Tetris::ctrlDown()
{
// longer debounce time, to prevent immediate drop
if (millis() > _lastButtonClickr + DEBOUNCE_TIME * 5 && _gameStatet == GAME_STATE_RUNNING)
{
_allowdrop = true;
_lastButtonClickr = millis();
}
}
/**
* @brief Set game speed
*
* @param i new speed value
*/
void Tetris::setSpeed(int32_t i)
{
_logger->log_string("setSpeed: " + String(i));
_speedtetris = -10 * i + 150;
}
/**
* @brief Clear the led matrix (turn all leds off)
*
*/
void Tetris::resetLEDs()
{
_ledmatrix->flush();
_ledmatrix->draw_on_matrix_instant();
}
/**
* @brief Initialize the tetris game
*
*/
void Tetris::tetrisInit()
{
(*_logger).log_string("Tetris: init");
clearField();
_brickSpeed = INIT_SPEED;
_nbRowsThisLevel = 0;
_nbRowsTotal = 0;
_tetrisGameOver = false;
newActiveBrick();
_prevUpdateTime = millis();
_gameStatet = GAME_STATE_RUNNING;
}
/**
* @brief Draw current field representation to led matrix
*
*/
void Tetris::printField()
{
int x, y;
for (x = 0; x < MATRIX_WIDTH; x++)
{
for (y = 0; y < MATRIX_HEIGHT; y++)
{
uint8_t activeBrickPix = 0;
if (_activeBrick.enabled)
{ // Only draw brick if it is enabled
// Now check if brick is "in view"
if ((x >= _activeBrick.xpos) && (x < (_activeBrick.xpos + (_activeBrick.siz))) && (y >= _activeBrick.ypos) && (y < (_activeBrick.ypos + (_activeBrick.siz))))
{
activeBrickPix = (_activeBrick.pix)[x - _activeBrick.xpos][y - _activeBrick.ypos];
}
}
if (_field.pix[x][y] == 1)
{
_ledmatrix->grid_add_pixel(x, y, _field.color[x][y]);
}
else if (activeBrickPix == 1)
{
_ledmatrix->grid_add_pixel(x, y, _activeBrick.col);
}
else
{
_ledmatrix->grid_add_pixel(x, y, 0x000000);
}
}
}
_ledmatrix->draw_on_matrix_instant();
}
/* *** Game functions *** */
/**
* @brief Spawn new (random) brick
*
*/
void Tetris::newActiveBrick()
{
uint8_t selectedBrick = 0;
static uint8_t lastselectedBrick = 0;
// choose random next brick, but not the same as before
do
{
selectedBrick = random(7);
} while (lastselectedBrick == selectedBrick);
// Save selected brick for next round
lastselectedBrick = selectedBrick;
// every brick has its color, select corresponding color
uint32_t selectedCol = _brickLib[selectedBrick].col;
// Set properties of brick
_activeBrick.siz = _brickLib[selectedBrick].siz;
_activeBrick.yOffset = _brickLib[selectedBrick].yOffset;
_activeBrick.xpos = MATRIX_WIDTH / 2 - _activeBrick.siz / 2;
_activeBrick.ypos = BRICKOFFSET - _activeBrick.yOffset;
_activeBrick.enabled = true;
// Set color of brick
_activeBrick.col = selectedCol;
// _activeBrick.color = _colorLib[1];
// Copy pix array of selected Brick
uint8_t x, y;
for (y = 0; y < MAX_BRICK_SIZE; y++)
{
for (x = 0; x < MAX_BRICK_SIZE; x++)
{
_activeBrick.pix[x][y] = (_brickLib[selectedBrick]).pix[x][y];
}
}
// Check collision, if already, then game is over
if (checkFieldCollision(&_activeBrick))
{
_tetrisGameOver = true;
_gameStatet = GAME_STATE_END;
}
}
/**
* @brief Check collision between bricks in the field and the specified brick
*
* @param brick brick to be checked for collision
* @return boolean true if collision occured
*/
boolean Tetris::checkFieldCollision(struct Brick *brick)
{
uint8_t bx, by;
uint8_t fx, fy;
for (by = 0; by < MAX_BRICK_SIZE; by++)
{
for (bx = 0; bx < MAX_BRICK_SIZE; bx++)
{
fx = brick->xpos + bx;
fy = brick->ypos + by;
if ((brick->pix[bx][by] == 1) && (_field.pix[fx][fy] == 1))
{
return true;
}
}
}
return false;
}
/**
* @brief Check collision between specified brick and all sides of the playing field
*
* @param brick brick to be checked for collision
* @return boolean true if collision occured
*/
boolean Tetris::checkSidesCollision(struct Brick *brick)
{
// Check vertical collision with sides of field
uint8_t bx, by;
uint8_t fx; //, fy; /* Patch */
for (by = 0; by < MAX_BRICK_SIZE; by++)
{
for (bx = 0; bx < MAX_BRICK_SIZE; bx++)
{
if (brick->pix[bx][by] == 1)
{
fx = brick->xpos + bx; // Determine actual position in the field of the current pix of the brick
// fy = brick->ypos + by; /* Patch */
if (fx < 0 || fx >= MATRIX_WIDTH)
{
return true;
}
}
}
}
return false;
}
/**
* @brief Rotate current active brick
*
*/
void Tetris::rotateActiveBrick()
{
// Copy active brick pix array to temporary pix array
uint8_t x, y;
Brick tmpBrick;
for (y = 0; y < MAX_BRICK_SIZE; y++)
{
for (x = 0; x < MAX_BRICK_SIZE; x++)
{
tmpBrick.pix[x][y] = _activeBrick.pix[x][y];
}
}
tmpBrick.xpos = _activeBrick.xpos;
tmpBrick.ypos = _activeBrick.ypos;
tmpBrick.siz = _activeBrick.siz;
// Depending on size of the active brick, we will rotate differently
if (_activeBrick.siz == 3)
{
// Perform rotation around center pix
tmpBrick.pix[0][0] = _activeBrick.pix[0][2];
tmpBrick.pix[0][1] = _activeBrick.pix[1][2];
tmpBrick.pix[0][2] = _activeBrick.pix[2][2];
tmpBrick.pix[1][0] = _activeBrick.pix[0][1];
tmpBrick.pix[1][1] = _activeBrick.pix[1][1];
tmpBrick.pix[1][2] = _activeBrick.pix[2][1];
tmpBrick.pix[2][0] = _activeBrick.pix[0][0];
tmpBrick.pix[2][1] = _activeBrick.pix[1][0];
tmpBrick.pix[2][2] = _activeBrick.pix[2][0];
// Keep other parts of temporary block clear
tmpBrick.pix[0][3] = 0;
tmpBrick.pix[1][3] = 0;
tmpBrick.pix[2][3] = 0;
tmpBrick.pix[3][3] = 0;
tmpBrick.pix[3][2] = 0;
tmpBrick.pix[3][1] = 0;
tmpBrick.pix[3][0] = 0;
}
else if (_activeBrick.siz == 4)
{
// Perform rotation around center "cross"
tmpBrick.pix[0][0] = _activeBrick.pix[0][3];
tmpBrick.pix[0][1] = _activeBrick.pix[1][3];
tmpBrick.pix[0][2] = _activeBrick.pix[2][3];
tmpBrick.pix[0][3] = _activeBrick.pix[3][3];
tmpBrick.pix[1][0] = _activeBrick.pix[0][2];
tmpBrick.pix[1][1] = _activeBrick.pix[1][2];
tmpBrick.pix[1][2] = _activeBrick.pix[2][2];
tmpBrick.pix[1][3] = _activeBrick.pix[3][2];
tmpBrick.pix[2][0] = _activeBrick.pix[0][1];
tmpBrick.pix[2][1] = _activeBrick.pix[1][1];
tmpBrick.pix[2][2] = _activeBrick.pix[2][1];
tmpBrick.pix[2][3] = _activeBrick.pix[3][1];
tmpBrick.pix[3][0] = _activeBrick.pix[0][0];
tmpBrick.pix[3][1] = _activeBrick.pix[1][0];
tmpBrick.pix[3][2] = _activeBrick.pix[2][0];
tmpBrick.pix[3][3] = _activeBrick.pix[3][0];
}
else
{
_logger->log_string("Tetris: Brick size error");
}
// Now validate by checking collision.
// Collision possibilities:
// - Brick now sticks outside field
// - Brick now sticks inside fixed bricks of field
// In case of collision, we just discard the rotated temporary brick
if ((!checkSidesCollision(&tmpBrick)) && (!checkFieldCollision(&tmpBrick)))
{
// Copy temporary brick pix array to active pix array
for (y = 0; y < MAX_BRICK_SIZE; y++)
{
for (x = 0; x < MAX_BRICK_SIZE; x++)
{
_activeBrick.pix[x][y] = tmpBrick.pix[x][y];
}
}
}
}
/**
* @brief Shift brick left/right/down by one if possible
*
* @param dir direction to be shifted
*/
void Tetris::shiftActiveBrick(int dir)
{
// Change position of active brick (no copy to temporary needed)
if (dir == DIR_LEFT)
{
_activeBrick.xpos--;
}
else if (dir == DIR_RIGHT)
{
_activeBrick.xpos++;
}
else if (dir == DIR_DOWN)
{
_activeBrick.ypos++;
}
// Check position of active brick
// Two possibilities when collision is detected:
// - Direction was LEFT/RIGHT, just revert position back
// - Direction was DOWN, revert position and fix block to field on collision
// When no collision, keep _activeBrick coordinates
if ((checkSidesCollision(&_activeBrick)) || (checkFieldCollision(&_activeBrick)))
{
if (dir == DIR_LEFT)
{
_activeBrick.xpos++;
}
else if (dir == DIR_RIGHT)
{
_activeBrick.xpos--;
}
else if (dir == DIR_DOWN)
{
_activeBrick.ypos--; // Go back up one
addActiveBrickToField();
_activeBrick.enabled = false; // Disable brick, it is no longer moving
}
}
}
/**
* @brief Copy active pixels to field, including color
*
*/
void Tetris::addActiveBrickToField()
{
uint8_t bx, by;
uint8_t fx, fy;
for (by = 0; by < MAX_BRICK_SIZE; by++)
{
for (bx = 0; bx < MAX_BRICK_SIZE; bx++)
{
fx = _activeBrick.xpos + bx;
fy = _activeBrick.ypos + by;
if (fx >= 0 && fy >= 0 && fx < MATRIX_WIDTH && fy < MATRIX_HEIGHT && _activeBrick.pix[bx][by])
{ // Check if inside playing field
// _field.pix[fx][fy] = _field.pix[fx][fy] || _activeBrick.pix[bx][by];
_field.pix[fx][fy] = _activeBrick.pix[bx][by];
_field.color[fx][fy] = _activeBrick.col;
}
}
}
}
/**
* @brief Move all pix from the field above startRow down by one. startRow is overwritten
*
* @param startRow
*/
void Tetris::moveFieldDownOne(uint8_t startRow)
{
if (startRow == 0)
{ // Topmost row has nothing on top to move...
return;
}
uint8_t x, y;
for (y = startRow - 1; y > 0; y--)
{
for (x = 0; x < MATRIX_WIDTH; x++)
{
_field.pix[x][y + 1] = _field.pix[x][y];
_field.color[x][y + 1] = _field.color[x][y];
}
}
}
/**
* @brief Check if a line is complete
*
*/
void Tetris::checkFullLines()
{
int x, y;
int minY = 0;
for (y = (MATRIX_HEIGHT - 1); y >= minY; y--)
{
uint8_t rowSum = 0;
for (x = 0; x < MATRIX_WIDTH; x++)
{
rowSum = rowSum + (_field.pix[x][y]);
}
if (rowSum >= MATRIX_WIDTH)
{
// Found full row, animate its removal
_activeBrick.enabled = false;
for (x = 0; x < MATRIX_WIDTH; x++)
{
_field.pix[x][y] = 0;
printField();
delay(100);
}
// Move all upper rows down by one
moveFieldDownOne(y);
y++;
minY++;
printField();
delay(100);
_nbRowsThisLevel++;
_nbRowsTotal++;
if (_nbRowsThisLevel >= LEVELUP)
{
_nbRowsThisLevel = 0;
_brickSpeed = _brickSpeed - SPEED_STEP;
if (_brickSpeed < 200)
{
_brickSpeed = 200;
}
}
}
}
}
/**
* @brief Clear field
*
*/
void Tetris::clearField()
{
uint8_t x, y;
for (y = 0; y < MATRIX_HEIGHT; y++)
{
for (x = 0; x < MATRIX_WIDTH; x++)
{
_field.pix[x][y] = 0;
_field.color[x][y] = 0;
}
}
for (x = 0; x < MATRIX_WIDTH; x++)
{ // This last row is invisible to the player and only used for the collision detection routine
_field.pix[x][MATRIX_HEIGHT] = 1;
}
}
/**
* @brief Color all bricks on the field red
*
*/
void Tetris::everythingRed()
{
int x, y;
for (x = 0; x < MATRIX_WIDTH; x++)
{
for (y = 0; y < MATRIX_HEIGHT; y++)
{
uint8_t activeBrickPix = 0;
if (_activeBrick.enabled)
{ // Only draw brick if it is enabled
// Now check if brick is "in view"
if ((x >= _activeBrick.xpos) && (x < (_activeBrick.xpos + (_activeBrick.siz))) && (y >= _activeBrick.ypos) && (y < (_activeBrick.ypos + (_activeBrick.siz))))
{
activeBrickPix = (_activeBrick.pix)[x - _activeBrick.xpos][y - _activeBrick.ypos];
}
}
if (_field.pix[x][y] == 1)
{
_ledmatrix->grid_add_pixel(x, y, RED);
}
else if (activeBrickPix == 1)
{
_ledmatrix->grid_add_pixel(x, y, RED);
}
else
{
_ledmatrix->grid_add_pixel(x, y, 0x000000);
}
}
}
_ledmatrix->draw_on_matrix_instant();
}
/**
* @brief Draw score to led matrix
*
*/
void Tetris::showscore()
{
uint32_t color = LEDMatrix::color_24bit(255, 170, 0);
_ledmatrix->flush();
if (_score > 9)
{
_ledmatrix->print_number(2, 3, _score / 10, color);
_ledmatrix->print_number(6, 3, _score % 10, color);
}
else
{
_ledmatrix->print_number(4, 3, _score, color);
}
_ledmatrix->draw_on_matrix_instant();
}