initial commit

This commit is contained in:
2025-02-25 12:07:16 +01:00
commit 0657bc9b25
34 changed files with 3069 additions and 0 deletions

18
src/Core/Action.h Normal file
View File

@@ -0,0 +1,18 @@
#pragma once
/**
* The list of actions that can be taken by the player
*/
enum Action {
PAUSE,
RETRY,
HOLD,
SOFT_DROP,
HARD_DROP,
MOVE_LEFT,
MOVE_RIGHT,
ROTATE_CW,
ROTATE_180,
ROTATE_CCW
};

50
src/Core/Bag.cpp Normal file
View File

@@ -0,0 +1,50 @@
#include "Bag.h"
#include "../Pieces/Piece.h"
#include <Vector>
#include <cstdlib>
Bag::Bag(const std::vector<Piece>& pieces) : pieces(pieces) {
// initialize bags
this->currentBag.clear();
for (int i = 0; i < this->pieces.size(); i++) {
this->currentBag.push_back(i);
}
this->nextBag.clear();
// prepare first piece
this->prepareNext();
}
Piece Bag::lookNext() {
// return the next piece
return this->pieces.at(this->next);
}
Piece Bag::getNext() {
// get the piece to return
int nextIndex = this->next;
// prepare the piece even after the next
this->prepareNext();
// return the next piece
return this->pieces.at(nextIndex);
}
void Bag::prepareNext() {
// if the bag is empty switch to the next bag
if (this->currentBag.empty()) {
std::swap(this->currentBag, this->nextBag);
}
// pick a random piece from the current bag
int indexIndex = std::rand() % this->currentBag.size();
this->next = this->currentBag.at(indexIndex);
// move the piece over to the next bag
this->nextBag.push_back(this->next);
this->currentBag.erase(this->currentBag.begin() + indexIndex);
}

39
src/Core/Bag.h Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include "../Pieces/Piece.h"
#include <Vector>
/**
* A litteral bag of pieces, in which you take each of its piece randomly one by one then start again with a new bag
*/
class Bag {
private:
std::vector<Piece> pieces; // the pieces the bag can dispense
int next; // the next piece to give
std::vector<int> currentBag; // the list of pieces that are still to be taken out before starting a new bag
std::vector<int> nextBag; // the list of pieces that have been taken out of the current bag and have been placed in the next
public:
/**
* Creates a new bag of the specified list of pieces
*/
Bag(const std::vector<Piece>& pieces);
/**
* Looks at what the next picked piece will be
*/
Piece lookNext();
/**
* Picks a new piece from the current bag
*/
Piece getNext();
private:
/**
* Prepare the next picked piece in advance
*/
void prepareNext();
};

117
src/Core/Board.cpp Normal file
View File

@@ -0,0 +1,117 @@
#include "Board.h"
#include "../Pieces/Piece.h"
#include <Vector>
#include <Set>
#include <iostream>
Board::Board(int width, int height) : width(width), height(height) {
std::vector<Color> emptyRow;
for (int i = 0; i < width; i ++) {
emptyRow.push_back(NOTHING);
}
// initialize grid
this->grid.clear();
for (int j = 0; j < height; j++) {
this->grid.push_back(emptyRow);
}
}
void Board::addBlock(const Cell& position, Color block) {
// if the block is out of bounds we discard it
if (position.x < 0 || position.x >= this->width || position.y < 0) return;
// resize the grid if needed
if (position.y >= this->grid.size()) {
std::vector<Color> emptyRow;
for (int i = 0; i < width; i ++) {
emptyRow.push_back(NOTHING);
}
for (int j = this->grid.size(); j <= position.y; j++) {
this->grid.push_back(emptyRow);
}
}
// change the block in the grid
this->grid.at(position.y).at(position.x) = block;
}
int Board::clearRows() {
std::vector<Color> emptyRow;
for (int i = 0; i < width; i ++) {
emptyRow.push_back(NOTHING);
}
// check from top to bottom
int clearedLines = 0;
for (int j = this->grid.size() - 1; j >= 0; j--) {
// check if a line has a block on every column
bool isFull = true;
for (int i = 0; i < this->width; i++) {
if (this->grid.at(j).at(i) == NOTHING) {
isFull = false;
}
}
// if it has, erase it and add a new row at the top
if (isFull) {
this->grid.erase(this->grid.begin() + j);
if(this->grid.size() < height) this->grid.push_back(emptyRow);
clearedLines++;
}
}
return clearedLines;
}
Color Board::getBlock(const Cell& position) const {
// if the block is out of bounds
if (position.x < 0 || position.x >= this->width || position.y < 0) return OUT_OF_BOUND;
// if the block is higher than the current grid, since it can grow indefinitely we do as if it was there but empty
if (position.y >= this->grid.size()) return NOTHING;
// else get the color in the grid
return this->grid.at(position.y).at(position.x);
}
std::vector<std::vector<Color>> Board::getBlocks() const {
return this->grid;
}
int Board::getGridHeight() const {
return this->grid.size();
}
int Board::getBaseHeight() const {
return this->height;
}
int Board::getWidth() const {
return this->width;
}
std::ostream& operator<<(std::ostream& os, const Board& board) {
// print the board
for (int y = board.grid.size() - 1; y >= 0; y--) {
for (int x = 0; x < board.width; x++) {
Color block = board.grid.at(y).at(x);
os << COLOR_CODES[block];
if (block != NOTHING) {
os << "*";
}
else {
os << "-";
}
}
os << std::endl;
}
// reset console color
os << COLOR_RESET;
return os;
}

63
src/Core/Board.h Normal file
View File

@@ -0,0 +1,63 @@
#pragma once
#include "../Pieces/Piece.h"
#include <Vector>
#include <iostream>
/**
* A 2D grid of blocks
*/
class Board {
private:
std::vector<std::vector<Color>> grid; // the grid, (0,0) is downleft
int width; // the width of the grid
int height; // the base height of the grid, which can extends indefinitely
public:
/**
* Creates a new board of the specified size
*/
Board(int width, int height);
/**
* Change the color of the specified block, if the block is out of bounds it is simply ignored
*/
void addBlock(const Cell& position, Color block);
/**
* Clears any complete row and moves down the rows on top, returns the number of cleared rows
*/
int clearRows();
/**
* Returns the color of the block at the specified position
*/
Color getBlock(const Cell& position) const;
/**
* Returns a copy of the grid
*/
std::vector<std::vector<Color>> getBlocks() const;
/**
* Returns the actual height of the grid
*/
int getGridHeight() const;
/**
* Returns the base height of the grid
*/
int getBaseHeight() const;
/**
* Returns the width of the grid
*/
int getWidth() const;
/**
* Stream output operator, adds a 2D grid representing the board
*/
friend std::ostream& operator<<(std::ostream& os, const Board& board);
};

300
src/Core/Game.cpp Normal file
View File

@@ -0,0 +1,300 @@
#include "Game.h"
#include "GameBoard.h"
#include "GameParameters.h"
#include "Action.h"
#include <Vector>
static const int SUBPX_PER_ROW = 60; // the number of position the active piece can take "between" two rows
static const int SOFT_DROP_SCORE = 1; // the score gained by line soft dropped
static const int HARD_DROP_SCORE = 2; // the score gained by line hard dropped
static const int LINE_CLEAR_BASE_SCORE = 100; // the score value of clearing a single line
static const int B2B_SCORE_MULTIPLIER = 2; // by how much havaing B2B on multiplies the score of the line clear
static const int B2B_MIN_LINE_NUMBER = 4; // the minimum number of lines needed to be cleared at once to gain B2B (without a spin)
Game::Game(Gamemode gamemode, const Player& controls, int boardWidth, int boardHeight, const std::vector<Piece>& bag) : parameters(gamemode, controls), board(boardWidth, boardHeight, bag, parameters.getNextQueueLength()) {
// the game has not yet started
this->started = false;
this->lost = false;
// initialize stats
this->score = 0;
this->framesPassed = 0;
this->B2BChain = 0;
// nothing happened yet
this->heldActions.clear();
this->initialActions.clear();
this->heldDAS = 0;
this->heldARR = 0;
this->subVerticalPosition = 0;
this->leftARETime = 0;
this->totalLockDelay = 0;
this->totalForcedLockDelay = 0;
}
void Game::start() {
// starts the game
this->started = true;
this->lost = this->board.spawnNextPiece();
}
void Game::nextFrame(const std::set<Action>& playerActions) {
if (this->lost || this->hasWon()) return;
if (this->started) {
bool AREJustEnded = (this->leftARETime == 1);
if (this->leftARETime > 0) {
this->leftARETime--;
}
if (this->leftARETime == 0) {
if (AREJustEnded) {
this->board.spawnNextPiece();
}
/* IRS and IHS */
Rotation initialRotation = NONE
+ (this->initialActions.contains(ROTATE_CW)) ? CLOCKWISE : NONE
+ (this->initialActions.contains(ROTATE_180)) ? DOUBLE : NONE
+ (this->initialActions.contains(ROTATE_CCW)) ? COUNTERCLOCKWISE : NONE;
if (this->initialActions.contains(HOLD)) {
if (this->board.hold(initialRotation)) {
this->subVerticalPosition = 0;
this->totalLockDelay = 0;
this->heldARR = 0;
}
}
else {
if (initialRotation != NONE) {
this->board.rotate(initialRotation);
}
}
/* HOLD */
if (playerActions.contains(HOLD) && (!this->heldActions.contains(HOLD))) {
if (this->board.hold()) {
this->subVerticalPosition = 0;
this->totalLockDelay = 0;
this->heldARR = 0;
}
}
/* MOVE LEFT/RIGHT */
if (playerActions.contains(MOVE_LEFT)) {
this->movePiece(-1, (this->heldDAS >= 0));
}
if (playerActions.contains(MOVE_RIGHT)) {
this->movePiece(1, (this->heldDAS <= 0));
}
else {
this->heldDAS = 0;
}
/* ROTATIONS */
if (playerActions.contains(ROTATE_CW) && (!this->heldActions.contains(ROTATE_CW))) {
this->board.rotate(CLOCKWISE);
}
if (playerActions.contains(ROTATE_180) && (!this->heldActions.contains(ROTATE_180))) {
this->board.rotate(DOUBLE);
}
if (playerActions.contains(ROTATE_CCW) && (!this->heldActions.contains(ROTATE_CCW))) {
this->board.rotate(COUNTERCLOCKWISE);
}
/* SOFT DROP */
if (playerActions.contains(SOFT_DROP)) {
int appliedSDR = this->parameters.getSDR();
// SDR=0 -> instant drop
if (appliedSDR == 0) {
while (this->board.moveDown()) {
this->score += SOFT_DROP_SCORE;
}
}
// SDR>1 -> move down by specified amount
else {
this->subVerticalPosition += (SUBPX_PER_ROW / appliedSDR);
while (this->subVerticalPosition >= SUBPX_PER_ROW) {
this->subVerticalPosition -= SUBPX_PER_ROW;
this->score += (this->board.moveDown() * SOFT_DROP_SCORE);
}
}
}
/* HARD DROP */
// needs to be done last because we can enter ARE period afterwards
if (this->initialActions.contains(HARD_DROP) || (playerActions.contains(HARD_DROP) && (!this->heldActions.contains(HARD_DROP)))) {
while (this->board.moveDown()) {
this->score += HARD_DROP_SCORE;
}
this->lockPiece();
}
// no need to apply gravity and lock delay if the piece was hard dropped
else {
/* GRAVITY */
// parameters.getGravity() gives the gravity for an assumed 20-line high board
int appliedGravity = this->parameters.getGravity() * (this->board.getBoard().getBaseHeight() / 20.0);
this->subVerticalPosition += appliedGravity;
while (this->subVerticalPosition >= SUBPX_PER_ROW) {
this->subVerticalPosition -= SUBPX_PER_ROW;
this->board.moveDown();
}
/* LOCK DELAY */
if (this->board.touchesGround()) {
this->totalLockDelay++;
this->totalForcedLockDelay++;
}
else {
this->totalLockDelay = 0;
}
if ((this->totalLockDelay > this->parameters.getLockDelay()) || (this->totalForcedLockDelay > this->parameters.getForcedLockDelay())) {
this->lockPiece();
}
}
// remove initial actions only once they've been applied
if (AREJustEnded) {
this->initialActions.clear();
}
}
this->framesPassed++;
}
// update remembered actions
if ((!this->started) || this->leftARETime > 0) {
for (Action action : playerActions) {
this->initialActions.insert(action);
}
}
this->heldActions = playerActions;
if (playerActions.contains(MOVE_LEFT)) {
this->heldDAS = std::min(-1, this->heldDAS - 1);
}
if (playerActions.contains(MOVE_RIGHT)) {
this->heldDAS = std::max(1, this->heldDAS + 1);
}
else {
this->heldDAS = 0;
}
}
void Game::movePiece(int movement, bool resetDirection) {
if (resetDirection) {
this->heldDAS = movement;
this->heldARR = 0;
}
else {
this->heldDAS += movement;
}
if (abs(this->heldDAS) > this->parameters.getDAS()) {
int appliedARR = this->parameters.getARR();
// ARR=0 -> instant movement
if (appliedARR == 0) {
if (movement == -1) while (this->board.moveLeft());
if (movement == 1) while (this->board.moveRight());
}
// ARR>1 -> move by specified amount
else {
this->heldARR++;
if (this->heldARR == appliedARR) {
this->heldARR = 0;
if (movement == -1) this->board.moveLeft();
if (movement == 1) this->board.moveRight();
}
}
}
}
void Game::lockPiece() {
LineClear clear = this->board.lockPiece();
this->parameters.clearLines(clear.lines);
// update B2B and score
bool B2BConditions = ((clear.lines > B2B_MIN_LINE_NUMBER) || clear.isSpin || clear.isMiniSpin);
if (clear.lines > 0) {
/* clearing one more line is worth 2x more
clearing with a spin is worth as much as clearing 2x more lines */
long int clearScore = LINE_CLEAR_BASE_SCORE;
clearScore = clearScore << (clear.lines << (clear.isSpin));
if (this->B2BChain && B2BConditions) clearScore *= B2B_SCORE_MULTIPLIER;
this->score += clearScore;
}
this->B2BChain = B2BConditions;
// reset active piece
this->subVerticalPosition = 0;
this->totalLockDelay = 0;
this->totalForcedLockDelay = 0;
this->heldARR = 0;
// check for ARE
this->leftARETime = this->parameters.getARE();
if (this->leftARETime == 0) {
this->board.spawnNextPiece();
}
}
bool Game::hasWon() {
return this->parameters.hasWon(this->framesPassed);
}
bool Game::hasLost() {
return this->lost;
}
int Game::getClearedLines() {
return this->parameters.getClearedLines();
}
int Game::getLevel() {
return this->parameters.getLevel();
}
int Game::getFramesPassed() {
return this->framesPassed;
}
int Game::getScore() {
return this->score;
}
bool Game::isOnB2BChain() {
return this->B2BChain;
}
bool Game::areBlocksBones() {
return this->parameters.getBoneBlocks();
}
Board Game::getBoard() {
return this->board.getBoard();
}
Piece Game::getActivePiece() {
return this->board.getActivePiece();
}
Cell Game::getActivePiecePosition() {
return this->board.getActivePiecePosition();
}
Piece Game::getHeldPiece() {
return this->board.getHeldPiece();
}
std::vector<Piece> Game::getNextPieces() {
return this->board.getNextPieces();
}

124
src/Core/Game.h Normal file
View File

@@ -0,0 +1,124 @@
#pragma once
#include "GameBoard.h"
#include "GameParameters.h"
#include "Action.h"
#include <Vector>
/**
* Interprets the player action into the game, depending on the state of the board and the current gamemode
*/
class Game {
private:
GameParameters parameters; // the current parameters of the game
GameBoard board; // the board in which the game is played
bool started; // wheter the game has started
bool lost; // wheter the game is lost
long int score; // the current score
int framesPassed; // how many frames have passed since the start of the game
bool B2BChain; // wheter the player is currently on a B2B chain
std::set<Action> heldActions; // the list of actions that were pressed last frame
std::set<Action> initialActions; // the list of actions that have been pressed while there was no active piece
int heldDAS; // the number of frames DAS has been held, positive for right or negative for left
int heldARR; // the number of frames ARR has been held
int subVerticalPosition; // how far the active piece is to go down one line
int leftARETime; // how many frames are left before ARE period finishes
int totalLockDelay; // how many frames has the active piece touched the ground without moving
int totalForcedLockDelay; // how many frames the active piece has touched the ground since the last spawned piece
public:
/**
* Initialize the parameters and creates a new board
*/
Game(Gamemode gamemode, const Player& controls, int boardWidth, int boardHeight, const std::vector<Piece>& bag);
/**
* Starts the game
*/
void start();
/**
* Advance to the next frame while excecuting the actions taken by the player,
* this is where the main game logic takes place
*/
void nextFrame(const std::set<Action>& playerActions);
private:
/**
* Move the piece in the specified direction
*/
void movePiece(int movement, bool resetDirection);
/**
* Locks the piece, updates level and score and spawn the next piece if necessary
*/
void lockPiece();
public:
/**
* Returns wheter the player has won
*/
bool hasWon();
/**
* Returns wheter the player has lost
*/
bool hasLost();
/**
* Returns the current level
*/
int getLevel();
/**
* Returns the current number of cleared lines
*/
int getClearedLines();
/**
* Returns the number of frames passed since the start of the game
*/
int getFramesPassed();
/**
* Returns the current score
*/
int getScore();
/**
* Returns wheter the player is currently on a B2B chain
*/
bool isOnB2BChain();
/**
* Returns wheter all blocks are currently bone blocks
*/
bool areBlocksBones();
/**
* Returns a copy of the board
*/
Board getBoard();
/**
* Returns a copy of the active piece
*/
Piece getActivePiece();
/**
* Returns a copy of the active piece position
*/
Cell getActivePiecePosition();
/**
* Returns a copy of the held piece
*/
Piece getHeldPiece();
/**
* Return a copy of the next pieces queue
*/
std::vector<Piece> getNextPieces();
};

360
src/Core/GameBoard.cpp Normal file
View File

@@ -0,0 +1,360 @@
#include "GameBoard.h"
#include "../Pieces/Piece.h"
#include "Board.h"
#include "Bag.h"
#include "LineClear.h"
#include <Vector>
#include <Set>
#include <memory>
GameBoard::GameBoard(int boardWidth, int boardHeight, const std::vector<Piece>& bag, int nextQueueLength) : board(boardWidth, boardHeight), generator(bag), nextQueueLength(nextQueueLength) {
// initialize queue
this->nextQueue.clear();
for (int i = 0; i < nextQueueLength; i++) {
this->nextQueue.push_back(this->generator.getNext());
}
}
bool GameBoard::moveLeft() {
// check if the piece can be moved one cell left
if (this->isActivePieceInWall(Cell{-1, 0})) {
return false;
}
else {
this->activePiecePosition.x -= 1;
this->isLastMoveKick = false;
return true;
}
}
bool GameBoard::moveRight() {
// check if the piece can be moved one cell right
if (this->isActivePieceInWall(Cell{1, 0})) {
return false;
}
else {
this->activePiecePosition.x += 1;
this->isLastMoveKick = false;
return true;
}
}
bool GameBoard::moveDown() {
// check if the piece can be moved one cell down
if (this->isActivePieceInWall(Cell{0, -1})) {
return false;
}
else {
this->activePiecePosition.y -= 1;
this->isLastMoveKick = false;
return true;
}
}
bool GameBoard::rotate(Rotation rotation) {
// copy the original piece before rotating it
Piece stored = *this->activePiece;
this->rotate(rotation);
// check if the piece can rotate
if (!this->isActivePieceInWall()) {
this->isLastMoveKick = false;
return true;
}
// get the list of cells that touches the original piece
std::set<Cell> safeCells;
for (Cell cell : stored.getPositions()) {
Cell cellInGrid(cell + this->activePiecePosition);
safeCells.insert(cellInGrid);
safeCells.insert(cellInGrid + Cell{0, 1});
safeCells.insert(cellInGrid + Cell{1, 0});
safeCells.insert(cellInGrid + Cell{0, -1});
safeCells.insert(cellInGrid + Cell{-1, 0});
}
// try kicking the piece down
bool suceeded = this->tryKicking(true, safeCells);
if (suceeded) {
this->isLastMoveKick = true;
return true;
}
// if it doesn't work try kicking the piece up
suceeded = this->tryKicking(false, safeCells);
if (suceeded) {
this->isLastMoveKick = true;
return true;
}
// if it still doesn't work, abort the rotation
this->activePiece = std::make_shared<Piece>(stored);
return false;
}
bool GameBoard::tryKicking(bool testingBottom, const std::set<Cell>& safeCells) {
// we try from the original height of the piece, moving vertically as long as the kicked piece touches the original
bool overlapsVertically = true;
int j = 0;
do {
// we try from the center to the sides as long as the kicked piece touches the original
bool overlapsLeft = true;
bool overlapsRight = true;
int i = 0;
do {
// check right before right arbitrarly, we don't decide this with rotations since it would still be arbitrary with 180° rotations
if (overlapsRight) {
Cell shift{+i, j};
// the kicked position must touch the original piece
if (!this->activePieceOverlapsOneCell(safeCells, shift)) {
overlapsLeft = false;
}
else {
// if the position is valid we place the active piece there
if (!this->isActivePieceInWall(shift)) {
this->activePiecePosition += shift;
return true;
}
}
}
// do the same on the left side
if (overlapsLeft) {
Cell shift{-i, j};
if (!this->activePieceOverlapsOneCell(safeCells, shift)) {
overlapsLeft = false;
}
else {
if (!this->isActivePieceInWall(shift)) {
this->activePiecePosition += shift;
return true;
}
}
}
i++;
} while (overlapsLeft && overlapsRight);
// test if no position touched the original piece
if (i == 1) {
overlapsVertically = false;
}
// move one line up or down
(testingBottom) ? j-- : j++;
} while (overlapsVertically);
return false;
}
bool GameBoard::hold(Rotation initialRotation) {
// swap with held piece
std::swap(this->activePiece, this->heldPiece);
// if it's the first time holding try the next piece
bool isFirstTimeHolding = false;
if (this->activePiece == nullptr) {
isFirstTimeHolding = true;
// if no pieces in next queue look at what the next would be
if (this->nextQueueLength == 0) {
this->activePiece = std::make_shared<Piece>(this->generator.lookNext());
}
else {
this->activePiece = std::make_shared<Piece>(this->nextQueue.front());
}
}
// set the spawned piece to the correct position
this->goToSpawnPosition();
// apply initial rotation
Piece stored = *this->activePiece;
this->rotate(initialRotation);
// if the piece can't spawn, abort initial rotation
if (this->isActivePieceInWall()) {
this->activePiece = std::make_shared<Piece>(stored);
// if the piece still can't spawn, abort holding
if (this->isActivePieceInWall()) {
if (isFirstTimeHolding) {
this->activePiece = nullptr;
}
std::swap(this->activePiece, this->heldPiece);
return false;
}
}
// if it's the first time holding, confirm we keep this piece
if (isFirstTimeHolding) {
if (this->nextQueueLength == 0) {
this->generator.getNext();
}
else {
this->spawnNextPiece();
}
}
// this piece has done nothing yet
this->isLastMoveKick = false;
return true;
}
bool GameBoard::spawnNextPiece() {
// add a piece to the queue
this->nextQueue.push_back(this->generator.getNext());
// get next piece from queue
this->activePiece = std::make_shared<Piece>(this->nextQueue.front());
this->nextQueue.erase(this->nextQueue.begin());
// set the spawned piece to the correct position
this->goToSpawnPosition();
// this piece has done nothing yet
this->isLastMoveKick = false;
// returns wheter the piece can spawn correctly
return !this->isActivePieceInWall();
}
bool GameBoard::touchesGround() {
return this->isActivePieceInWall(Cell{0, -1});
}
LineClear GameBoard::lockPiece() {
// check if the piece is locked in place
bool isLocked = (this->isActivePieceInWall(Cell{0, 1}) && this->isActivePieceInWall(Cell{1, 0}) &&
this->isActivePieceInWall(Cell{-1, 0}) && this->isActivePieceInWall(Cell{0, -1}));
// put the piece in the board
for (Cell cell : this->activePiece->getPositions()) {
this->board.addBlock(cell + this->activePiecePosition, this->activePiece->getColor());
}
// check for lines to clear
return LineClear{this->board.clearRows(), isLocked, (!isLocked) && this->isLastMoveKick};
}
Board GameBoard::getBoard() const {
return this->board;
}
Piece GameBoard::getActivePiece() const {
return *this->activePiece;
}
Cell GameBoard::getActivePiecePosition() const {
return this->activePiecePosition;
}
Piece GameBoard::getHeldPiece() const {
return *this->heldPiece;
}
std::vector<Piece> GameBoard::getNextPieces() const {
return this->nextQueue;
}
bool GameBoard::isActivePieceInWall(const Cell& shift) const {
// check if every cell of the active piece is in an empty spot
for (Cell cell : this->activePiece->getPositions()) {
if (this->board.getBlock(cell + this->activePiecePosition + shift) != NOTHING) return true;
}
return false;
}
bool GameBoard::activePieceOverlapsOneCell(const std::set<Cell>& safeCells, const Cell& shift) const {
// check if one cell of the translated active piece overlaps with one cell of the given piece set
for (Cell cell : this->activePiece->getPositions()) {
if (safeCells.contains(cell + shift)) return true;
}
return false;
}
void GameBoard::goToSpawnPosition() {
// get the lowest cell of the piece
int lowestCell = this->activePiece->getLength() - 1;
for (Cell cell : this->activePiece->getPositions()) {
if (cell.y < lowestCell) lowestCell = cell.y;
}
// set the piece one line above the board
this->activePiecePosition.y = this->board.getBaseHeight() - lowestCell;
// center the piece horizontally, biased towards left
this->activePiecePosition.x = (this->board.getWidth() - this->activePiece->getLength()) / 2;
}
std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard) {
// print over the board (only the active piece if it is there)
if (gameboard.activePiece != nullptr) {
// change to the color of the active piece
Color pieceColor = gameboard.activePiece->getColor();
os << COLOR_CODES[pieceColor];
// print only the cell were the active piece is
for (int y = gameboard.activePiecePosition.y + gameboard.activePiece->getLength() - 1; y >= gameboard.board.getBaseHeight(); y--) {
for (int x = 0; x < gameboard.board.getWidth(); x++) {
bool hasActivePiece = gameboard.activePiece->getPositions().contains(Cell{x, y} - gameboard.activePiecePosition);
if (hasActivePiece) {
os << "*";
}
else {
os << " ";
}
}
os << std::endl;
}
}
// print the board
Color pieceColor = (gameboard.activePiece == nullptr) ? NOTHING : gameboard.activePiece->getColor();
for (int y = gameboard.board.getBaseHeight() - 1; y >= 0; y--) {
for (int x = 0; x < gameboard.board.getWidth(); x++) {
bool hasActivePiece = (gameboard.activePiece == nullptr) ? false : gameboard.activePiece->getPositions().contains(Cell{x, y} - gameboard.activePiecePosition);
// if the active piece is on this cell, print it
if (hasActivePiece) {
os << COLOR_CODES[pieceColor];
os << "*";
}
// else print the cell of the board
else {
Color block = gameboard.board.getBlock(Cell{x, y});
os << COLOR_CODES[block];
if (block != NOTHING) {
os << "*";
}
else {
os << "-";
}
}
}
os << std::endl;
}
// print held piece
os << "Hold:" << std::endl;
if (!(gameboard.heldPiece == nullptr)) {
os << *gameboard.heldPiece;
}
// print next queue
os << "Next:" << std::endl;
for (const Piece& piece : gameboard.nextQueue) {
os << piece;
}
// reset console color
os << COLOR_RESET;
return os;
}

126
src/Core/GameBoard.h Normal file
View File

@@ -0,0 +1,126 @@
#pragma once
#include "../Pieces/Piece.h"
#include "Board.h"
#include "Bag.h"
#include "LineClear.h"
#include <Vector>
#include <memory>
/**
* Links a board with the pieces moving in it
*/
class GameBoard {
private:
Board board; // the board in which pieces moves, (0, 0) is downleft
Bag generator; // the piece generator
std::shared_ptr<Piece> activePiece; // the piece currently in the board
Cell activePiecePosition; // the position of the piece currently in the board
std::shared_ptr<Piece> heldPiece; // a piece being holded
int nextQueueLength; // the number of next pieces seeable at a time
std::vector<Piece> nextQueue; // the list of the next pieces to spawn in the board
bool isLastMoveKick; // wheter the last action the piece did was kicking
public:
/**
* Creates a new board, generator, and next queue
*/
GameBoard(int boardWidth, int boardHeight, const std::vector<Piece>& bag, int nextQueueLength);
/**
* Try moving the piece one cell to the left, and returns wheter it was sucessfull
*/
bool moveLeft();
/**
* Try moving the piece one cell to the right, and returns wheter it was sucessfull
*/
bool moveRight();
/**
* Try moving the piece one cell down, and returns wheter it was sucessfull
*/
bool moveDown();
/**
* Try rotating the piece and kicking it if necessary, and returns wheter it was sucessfull
*/
bool rotate(Rotation rotation);
private:
/**
* Try kicking the piece, testing position either above or below the piece's initial position
*/
bool tryKicking(bool testingBottom, const std::set<Cell>& safeCells);
public:
/**
* Try holding the active piece or swapping it if one was already stocked, while trying to apply an initial rotation to the newly spawned piece,
* and returns wheter it was sucessfull
*/
bool hold(Rotation initialRotation = NONE);
/**
* Spawns the next piece from the queue, and returns wheter it spawns in a wall
*/
bool spawnNextPiece();
/**
* Returns wheter the active piece is touching walls directly below it
*/
bool touchesGround();
/**
* Lock the active piece into the board and returns the resulting line clear
*/
LineClear lockPiece();
/**
* Returns a copy of the board
*/
Board getBoard() const;
/**
* Returns a copy of the active piece
*/
Piece getActivePiece() const;
/**
* Returns a copy of the position of the active piece
*/
Cell getActivePiecePosition() const;
/**
* Returns a copy of the held piece
*/
Piece getHeldPiece() const;
/**
* Returns a copy of the next piece queue
*/
std::vector<Piece> getNextPieces() const;
private:
/**
* Returns wheter the translated active piece is in a wall
*/
bool isActivePieceInWall(const Cell& shift = Cell{0, 0}) const;
/**
* Returns wheter the translated active piece overlaps with at least one of the cells
*/
bool activePieceOverlapsOneCell(const std::set<Cell>& safeCells, const Cell& shift = Cell{0, 0}) const;
/**
* Sets the active piece to its spawn position
*/
void goToSpawnPosition();
public:
/**
* Stream output operator, adds the board, the hold box and the next queue
*/
friend std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard);
};

237
src/Core/GameParameters.cpp Normal file
View File

@@ -0,0 +1,237 @@
#include "GameParameters.h"
#include "Gamemode.h"
#include "Player.h"
GameParameters::GameParameters(Gamemode gamemode, const Player& controls) : gamemode(gamemode), controls(controls) {
// initialize lines and level
this->clearedLines = 0;
switch (this->gamemode) {
// lowest gravity
case SPRINT : {this->level = 1; break;}
// lowest gravity
case ULTRA : {this->level = 1; break;}
// goes from level 1 to 20
case MARATHON : {this->level = 1; break;}
// goes from level 20 to 39
case MASTER : {this->level = 20; break;}
default : this->level = 1;
}
// initialize stats
this->updateStats();
}
void GameParameters::clearLines(int lineNumber) {
// update lines and level
switch (this->gamemode) {
// modes where level increases
case MARATHON :
case MASTER : {
// update cleared lines
int previousLines = this->clearedLines;
this->clearedLines += lineNumber;
// level increments every 10 lines, stats only changes on level up
if (previousLines / 10 < this->clearedLines / 10) {
this->level = this->clearedLines / 10;
this->updateStats();
}
break;
}
// other modes
default : this->clearedLines += lineNumber;
}
}
bool GameParameters::hasWon(int framesPassed) {
switch (this->gamemode) {
// win once 40 lines have been cleared
case SPRINT : return this->clearedLines >= 40;
// win once 2mn have passed
case ULTRA : return (framesPassed / 60) >= 120;
// win once 200 lines have been cleared
case MARATHON : return this->clearedLines >= 200;
// win once 200 lines have been cleared
case MASTER : return this->clearedLines >= 200;
default : return false;
}
}
void GameParameters::updateStats() {
/* NEXT QUEUE */
switch (this->gamemode) {
// 5 for rapidity gamemodes
case SPRINT :
case ULTRA : {
this->nextQueueLength = 5;
break;
}
// 3 for endurance gamemodes
case MARATHON :
case MASTER : {
this->nextQueueLength = 3;
break;
}
default : this->nextQueueLength = 1;
}
/* BONE BLOCKS */
switch (this->gamemode) {
// blocks turns into bone blocks at level 30
case MASTER : this->boneBlocks = (this->level >= 30);
default : this->boneBlocks = false;
}
/* GRAVITY */
if (level >= 20) {
// all levels above 20 are instant gravity
this->gravity = 20 * 60;
}
else {
// get gravity for an assumed 20-rows board
switch (this->level) {
case 1 : {this->gravity = 1; break;} // 60f/line, 20s total
case 2 : {this->gravity = 2; break;} // 30f/line, 10s total
case 3 : {this->gravity = 3; break;} // 20f/line, 6.66s total
case 4 : {this->gravity = 4; break;} // 15f/line, 5s total
case 5 : {this->gravity = 5; break;} // 12f/line, 4s total
case 6 : {this->gravity = 6; break;} // 10f/line, 3.33 total
case 7 : {this->gravity = 7; break;} // 8.57f/line, 2.85s total
case 8 : {this->gravity = 8; break;} // 7.5f/line, 2.5s total
case 9 : {this->gravity = 10; break;} // 6f/line, 2s total
case 10 : {this->gravity = 12; break;} // 5f/line, 1.66s total
case 11 : {this->gravity = 14; break;} // 4.28f/line, 1.42s total
case 12 : {this->gravity = 17; break;} // 3.52f/line, 1.17s total
case 13 : {this->gravity = 20; break;} // 3f/line, 60f total
case 14 : {this->gravity = 24; break;} // 2.5f/line, 50f total
case 15 : {this->gravity = 30; break;} // 2f/line, 40f total
case 16 : {this->gravity = 40; break;} // 1.5f/line, 30f total
case 17 : {this->gravity = 1 * 60; break;} // 1line/f, 20f total
case 18 : {this->gravity = 2 * 60; break;} // 2line/f, 10f total
case 19 : {this->gravity = 4 * 60; break;} // 4line/f, 5f total
default : this->gravity = 1;
}
}
/* LOCK DELAY */
switch (this->gamemode) {
// starts at 500ms (30f) at lvl 20 and ends at 183ms (11f) at lvl 39
case MASTER : {this->lockDelay = 30 - (this->level - 20); break;}
// 1s by default
default : this->lockDelay = 60;
}
/* FORCED LOCK DELAY */
this->forcedLockDelay = this->lockDelay * 10;
/* ARE */
switch (this->gamemode) {
// starts at 400ms (24f) at lvl 1 and ends at 083ms (5f) at lvl 20
case MARATHON : {this->ARE = 24 - (this->level - 1); break;}
// starts at 400ms (24f) at lvl 20 and ends at 083ms (5f) at lvl 39
case MASTER : {this->ARE = 24 - (this->level - 20); break;}
// no ARE by default
default : this->ARE = 0;
}
/* LINE ARE */
this->lineARE = this->ARE * 2;
/* DAS */
this->DAS = this->controls.getDAS();
switch (this->gamemode) {
// for modes with reduced lock delay, ensure DAS is lower than lock delay, but at least 1
case MASTER : {
if (this->lockDelay <= this->DAS) {
this->DAS = this->lockDelay - 6; // give 6f (100ms) to change directions
}
if (this->DAS < 1) {
this->DAS = 1;
}
break;
}
// modes with no reduced lock delay
default : break;
}
/* ARR */
this->ARR = this->controls.getARR();
switch (this->gamemode) {
// for modes with reduced lock delay, ensure ARR is lower than DAS, but not lower than 1
case MASTER : {
if (this->DAS <= this->ARR) {
this->ARR = this->DAS - 1;
}
if (this->ARR < 1) {
this->ARR = 1;
}
break;
}
// modes with no reduced lock delay
default : break;
}
/* SDR */
this->SDR = this->controls.getSDR();
switch (this->gamemode) {
// modes where we don't want instant soft drop to be possible
case MARATHON : {
if (this->SDR < 1) {
this->SDR = 1;
}
break;
}
// modes where we don't care
default : break;
}
}
int GameParameters::getClearedLines() {
return this->clearedLines;
}
int GameParameters::getLevel() {
return this->level;
}
int GameParameters::getNextQueueLength() {
return this->nextQueueLength;
}
bool GameParameters::getBoneBlocks() {
return this->boneBlocks;
}
int GameParameters::getGravity() {
return this->gravity;
}
int GameParameters::getLockDelay() {
return this->lockDelay;
}
int GameParameters::getForcedLockDelay() {
return this->forcedLockDelay;
}
int GameParameters::getARE() {
return this->ARE;
}
int GameParameters::getLineARE() {
return this->lineARE;
}
int GameParameters::getDAS() {
return this->DAS;
}
int GameParameters::getARR() {
return this->ARR;
}
int GameParameters::getSDR() {
return this->SDR;
}

110
src/Core/GameParameters.h Normal file
View File

@@ -0,0 +1,110 @@
#pragma once
#include "Gamemode.h"
#include "Player.h"
/**
* Computes the parameters of the game depending on the current gamemode and the player's controls,
* this is basically where all the hard-coded values are stored
*/
class GameParameters {
private:
Gamemode gamemode; // the current gamemode
Player controls; // the player's controls
int clearedLines; // the number of cleared lines
int level; // the current level
int nextQueueLength; // the number of pieces visibles in the next queue
bool boneBlocks; // wheter all blocks are bone blocks
int gravity; // the gravity at which pieces drop
int lockDelay; // the time before the piece lock in place
int forcedLockDelay; // the forced time before the piece lock in place
int ARE; // the time before the next piece spawn
int lineARE; // the time before the next piece spawn, after clearing a line
int DAS; // the time before the piece repeats moving
int ARR; // the rate at which the piece repeats moving
int SDR; // the rate at which the piece soft drops
public:
/**
* Sets the current gamemode and the player's controls
*/
GameParameters(Gamemode gamemode, const Player& controls);
/**
* Count the newly cleared lines and update level and stats if needed
*/
void clearLines(int lineNumber);
/**
* Returns wheter the game ended
*/
bool hasWon(int framesPassed);
private:
/**
* Updates all the parameters
*/
void updateStats();
public:
/**
* Returns the current number of cleared line
*/
int getClearedLines();
/**
* Returns the current level
*/
int getLevel();
/**
* Returns the length of the next queue
*/
int getNextQueueLength();
/**
* Returns wheter the blocks are currently bone blocks
*/
bool getBoneBlocks();
/**
* Returns the current gravity for a 20-line high board
*/
int getGravity();
/**
* Returns the current lock delay
*/
int getLockDelay();
/**
* Returns the current forced lock delay
*/
int getForcedLockDelay();
/**
* Returns the current ARE
*/
int getARE();
/**
* Returns the current line ARE
*/
int getLineARE();
/**
* Returns the current DAS
*/
int getDAS();
/**
* Returns the current ARR
*/
int getARR();
/**
* Returns the current SDR
*/
int getSDR();
};

12
src/Core/Gamemode.h Normal file
View File

@@ -0,0 +1,12 @@
#pragma once
/**
* Every gamemode supported by the game
*/
enum Gamemode {
SPRINT,
MARATHON,
ULTRA,
MASTER
};

11
src/Core/LineClear.h Normal file
View File

@@ -0,0 +1,11 @@
#pragma once
/**
* Specify how many lines were cleared and how
*/
struct LineClear {
int lines; // the number of lines cleared
bool isSpin; // if the move was a spin
bool isMiniSpin; // if the move was a spin mini
};

49
src/Core/Player.cpp Normal file
View File

@@ -0,0 +1,49 @@
#include "Player.h"
static const int DAS_MIN_VALUE = 0;
static const int DAS_MAX_VALUE = 30;
static const int ARR_MIN_VALUE = 0;
static const int ARR_MAX_VALUE = 30;
static const int SDR_MIN_VALUE = 0;
static const int SDR_MAX_VALUE = 6;
Player::Player() {
// default settings
this->DAS = 15; // 250ms
this->ARR = 3; // 50ms
this->SDR = 2; // 33ms
}
bool Player::setDAS(int DAS) {
if (DAS < DAS_MIN_VALUE || DAS > DAS_MAX_VALUE) return false;
this->DAS = DAS;
return true;
}
bool Player::setARR(int ARR) {
if (ARR < ARR_MIN_VALUE || ARR > ARR_MAX_VALUE) return false;
this->ARR = ARR;
return true;
}
bool Player::setSDR(int SDR) {
if (SDR < SDR_MIN_VALUE || SDR > SDR_MAX_VALUE) return false;
this->SDR = SDR;
return true;
}
int Player::getDAS() {
return this->DAS;
}
int Player::getARR() {
return this->ARR;
}
int Player::getSDR() {
return this->SDR;
}

48
src/Core/Player.h Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
/**
* The controls of a player
*/
class Player {
private:
int DAS; // Delayed Auto-Shift, goes from 0 (instant) to 30 (500ms)
int ARR; // Auto-Repeat Rate, goes from 0 (instant) to 30 (500ms)
int SDR; // Soft Drop Rate, goes from 0 (instant) to 6 (100ms)
public:
/**
* Sets default controls
*/
Player();
/**
* Try setting DAS to the desired value, and returns wheter it is possible
*/
bool setDAS(int DAS);
/**
* Try setting ARR to the desired value, and returns wheter it is possible
*/
bool setARR(int ARR);
/**
* Try setting SDR to the desired value, and returns wheter it is possible
*/
bool setSDR(int SDR);
/**
* Returns DAS value
*/
int getDAS();
/**
* Returns ARR value
*/
int getARR();
/**
* Returns SDR value
*/
int getSDR();
};

165
src/Core/main.cpp Normal file
View File

@@ -0,0 +1,165 @@
#include "../Pieces/PiecesFiles.h"
#include "../Pieces/Generator.h"
#include "GameBoard.h"
#include <chrono>
#include <string>
#include <algorithm>
#include <filesystem>
#include <fstream>
void testGeneratorForAllSizes(int amount);
void testGeneratorForOneSize(int size);
void testGeneratorByprintingAllNminos(int n);
void testStoringAndRetrievingPieces(int size);
void generateFilesForAllSizes(int amount);
void readStatsFromFilesForAllSizes(int amount);
int main(int argc, char** argv) {
std::srand(std::time(NULL));
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::duration;
using std::chrono::milliseconds;
PiecesFiles pf;
std::vector<Piece> pieces;
std::vector<int> convexPieces;
std::vector<int> holelessPieces;
std::vector<int> otherPieces;
pf.loadPieces(13, pieces, convexPieces, holelessPieces, otherPieces);
auto t1 = high_resolution_clock::now();
Bag bg(pieces);
auto t2 = high_resolution_clock::now();
duration<double, std::milli> ms_double = t2 - t1;
std::cout << ms_double.count() << "ms" << std::endl;
return 0;
}
void testGeneratorForAllSizes(int amount) {
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::duration;
using std::chrono::milliseconds;
Generator generator;
for (int i = 1; i <= amount; i++) {
auto t1 = high_resolution_clock::now();
std::vector<Polyomino> n_minos = generator.generatePolyominos(i);
auto t2 = high_resolution_clock::now();
duration<double, std::milli> ms_double = t2 - t1;
std::cout << "generated " << n_minos.size() << " polyominos of size " << i << " in " << ms_double.count() << "ms" << std::endl;
}
}
void testGeneratorForOneSize(int size) {
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::duration;
using std::chrono::milliseconds;
Generator generator;
std::cout << "Generating " << size << "-minos" << std::endl;
for (int i = 0; i < 10; i++) {
auto t1 = high_resolution_clock::now();
std::vector<Polyomino> n_minos = generator.generatePolyominos(size);
auto t2 = high_resolution_clock::now();
duration<double, std::milli> ms_double = t2 - t1;
std::cout << ms_double.count() << "ms" << std::endl;
}
}
void testGeneratorByprintingAllNminos(int n) {
Generator generator;
std::vector<Polyomino> n_minos = generator.generatePolyominos(n);
for (Polyomino& n_mino : n_minos) {
n_mino.goToSpawnPosition();
}
std::sort(n_minos.begin(), n_minos.end());
for (Polyomino& n_mino : n_minos) {
std::cout << n_mino << std::endl;
}
}
void testStoringAndRetrievingPieces(int size) {
PiecesFiles piecesFiles;
piecesFiles.savePieces(size);
std::vector<Piece> pieces;
std::vector<int> convexPieces;
std::vector<int> holelessPieces;
std::vector<int> otherPieces;
piecesFiles.loadPieces(size, pieces, convexPieces, holelessPieces, otherPieces);
std::cout << "Convex " << size << "-minos:" << std::endl;
for (int index : convexPieces) {
std::cout << pieces.at(index);
}
std::cout << "Holeless " << size << "-minos:" << std::endl;
for (int index : holelessPieces) {
std::cout << pieces.at(index);
}
std::cout << "Others " << size << "-minos:" << std::endl;
for (int index : otherPieces) {
std::cout << pieces.at(index);
}
}
void generateFilesForAllSizes(int amount) {
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::duration;
using std::chrono::milliseconds;
PiecesFiles piecesFiles;
for (int i = 1; i <= amount; i++) {
auto t1 = high_resolution_clock::now();
piecesFiles.savePieces(i);
auto t2 = high_resolution_clock::now();
duration<double, std::milli> ms_double = t2 - t1;
std::cout << "Generated pieces files for size " << i << " in " << ms_double.count() << "ms" << std::endl;
}
for (int i = 1; i <= amount; i++) {
auto t1 = high_resolution_clock::now();
std::vector<Piece> pieces;
std::vector<int> convexPieces;
std::vector<int> holelessPieces;
std::vector<int> otherPieces;
piecesFiles.loadPieces(i, pieces, convexPieces, holelessPieces, otherPieces);
auto t2 = high_resolution_clock::now();
duration<double, std::milli> ms_double = t2 - t1;
std::cout << "Read pieces from files for size " << i << " in " << ms_double.count() << "ms" << std::endl;
}
}
void readStatsFromFilesForAllSizes(int amount) {
PiecesFiles piecesFiles;
for (int i = 1; i <= amount; i++) {
std::vector<Piece> pieces;
std::vector<int> convexPieces;
std::vector<int> holelessPieces;
std::vector<int> otherPieces;
piecesFiles.loadPieces(i, pieces, convexPieces, holelessPieces, otherPieces);
std::cout << i << "-minos : " << pieces.size() << std::endl;
std::cout << "Convex " << i << "-minos : " << convexPieces.size() << std::endl;
std::cout << "Holeless " << i << "-minos : " << holelessPieces.size() << std::endl;
std::cout << "Others " << i << "-minos : " << otherPieces.size() << std::endl;
}
}