diff --git a/doc/class_diagramm_core.png b/doc/class_diagramm_core.png index 89b7726..bd54c31 100644 Binary files a/doc/class_diagramm_core.png and b/doc/class_diagramm_core.png differ diff --git a/doc/class_diagramm_pieces.png b/doc/class_diagramm_pieces.png index a4ccf14..636767f 100644 Binary files a/doc/class_diagramm_pieces.png and b/doc/class_diagramm_pieces.png differ diff --git a/doc/game_logic.md b/doc/game_logic.md index 1a44892..23212f1 100644 --- a/doc/game_logic.md +++ b/doc/game_logic.md @@ -7,11 +7,13 @@ We will only talk about pieces and not polyominos. In this project, pieces are a Each frame, the UI will translate the user's input into a series of action to apply to the game. The list of action is the following: +- Quit the game - Pause - Retry - Hold - Move left - Move right +- Rotate 0° - Rotate clockwise (CW) - Rotate 180° - Rotate counter-clockwise (CCW) @@ -44,11 +46,14 @@ Since this game uses polyomino of high sizes which are very unplayable, we will 1. Before rotating, mark every position containing the piece or touching the piece, we will call the set of all theses positions the ``safePositions`` 2. Rotate the piece, if it fit stop the algorithm -3. Try fitting the piece, going from the center to the sides, that means we try to move the piece 1 position right, then 1 position left, then 2 position right, etc. until it fit (and then stop the algorithm), if at one point a position doesn't touch one of the ``safePositions`` we stop trying in this direction +3. Try fitting the piece, going from the center to the sides, that means we try to move the piece 1 position right, then 1 position left, then 2 position right, etc. until it fit (and then stop the algorithm), if at one point none of the squares of the pieces are in one of the ``safePositions`` we stop trying in this direction 4. Move the piece one line down, and repeat step 3 again, until we hit a line were the first position (shifted by 0 positions horizontally) touched none of the ``safePositions`` 5. Do the same as step 4 but now we move the piece one line up every time 6. Cancel the rotation +Kicking is primarly designed for rotating, but it is also applied when a piece spawns into a wall. +0° rotations will first try to move the piece one position down, and if it can't try kicking the piece, only registering a kick if the piece couldn't move down. + ## Detecting spins Another common mechanic of stacker games that goes alongside kicking is spinning. A spin is a special move (a move is calculated once a piece has been locked to the board) which usually happen when the last move a piece did was a kick or a rotation and the piece is locked in place or is locked in certain corners, but the rules varies a lot from game to game. diff --git a/src/Core/Action.h b/src/Core/Action.h index 7b4acb2..65ef9e4 100644 --- a/src/Core/Action.h +++ b/src/Core/Action.h @@ -1,10 +1,13 @@ #pragma once +#include + /** * The list of actions that can be taken by the player */ enum Action { + QUIT, PAUSE, RETRY, HOLD, @@ -12,7 +15,33 @@ enum Action { HARD_DROP, MOVE_LEFT, MOVE_RIGHT, + ROTATE_0, ROTATE_CW, ROTATE_180, ROTATE_CCW }; + + +static const std::string ACTION_NAMES[] = { // name for each action + "Quit", + "Pause", + "Retry", + "Hold", + "Soft drop", + "Hard drop", + "Move left", + "Move right", + "Rotate 0°", + "Rotate CW", + "Rotate 180°", + "Rotate CCW" +}; + +/** + * Stream output operator, adds the name of the action + * @return A reference to the output stream + */ +inline std::ostream& operator<<(std::ostream& os, const Action action) { + os << ACTION_NAMES[action]; + return os; +} diff --git a/src/Core/Board.cpp b/src/Core/Board.cpp index 6d835b5..9ac46e4 100644 --- a/src/Core/Board.cpp +++ b/src/Core/Board.cpp @@ -87,7 +87,7 @@ Block Board::getBlock(const Position& position) const { return this->grid.at(position.y).at(position.x); } -std::vector> Board::getBlocks() const { +const std::vector>& Board::getBlocks() const { return this->grid; } diff --git a/src/Core/Board.h b/src/Core/Board.h index ad765b0..fcd48fa 100644 --- a/src/Core/Board.h +++ b/src/Core/Board.h @@ -44,14 +44,14 @@ class Board { void clearBoard(); /** - * @return A copy of the block at the specified position + * @return The block at the specified position */ Block getBlock(const Position& position) const; /** - * @return A copy of the grid + * @return The grid */ - std::vector> getBlocks() const; + const std::vector>& getBlocks() const; /** * @return The width of the grid diff --git a/src/Core/Game.cpp b/src/Core/Game.cpp index d1bc6b7..8447cfa 100644 --- a/src/Core/Game.cpp +++ b/src/Core/Game.cpp @@ -25,7 +25,7 @@ Game::Game(Gamemode gamemode, const Player& controls, int boardWidth, int boardH void Game::start() { this->started = true; - this->lost = this->board.spawnNextPiece(); + this->leftARETime = 1; } void Game::reset() { @@ -48,7 +48,6 @@ void Game::initialize() { this->heldDAS = 0; this->heldARR = 0; this->subVerticalPosition = 0; - this->leftARETime = 0; this->totalLockDelay = 0; this->totalForcedLockDelay = 0; } @@ -64,14 +63,14 @@ void Game::nextFrame(const std::set& playerActions) { if (this->leftARETime == 0) { if (AREJustEnded) { - this->board.spawnNextPiece(); + this->lost = 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; + + ((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)) { @@ -79,13 +78,35 @@ void Game::nextFrame(const std::set& playerActions) { this->totalLockDelay = 0; this->heldARR = 0; } + else { + this->lost = true; + } } else { - if (initialRotation != NONE) { - this->board.rotate(initialRotation); + if (initialRotation != NONE || this->initialActions.contains(ROTATE_0)) { + Position before = this->board.getActivePiecePosition(); + if (this->board.rotate(initialRotation)) { + this->totalLockDelay = 0; + if (before != this->board.getActivePiecePosition()) { + this->subVerticalPosition = 0; + } + } + else { + this->lost = true; + } } } + if (this->lost) { + if (initialRotation == NONE && (!this->initialActions.contains(ROTATE_0))) { + this->board.rotate(NONE); + } + } + if (this->lost) { + this->framesPassed++; + return; + } + /* HOLD */ if (playerActions.contains(HOLD) && (!this->heldActions.contains(HOLD))) { if (this->board.hold()) { @@ -96,25 +117,48 @@ void Game::nextFrame(const std::set& playerActions) { } /* MOVE LEFT/RIGHT */ + Position before = this->board.getActivePiecePosition(); + if (playerActions.contains(MOVE_LEFT)) { - this->movePiece(-1, (this->heldDAS >= 0)); + this->movePiece(-1); } - if (playerActions.contains(MOVE_RIGHT)) { - this->movePiece(1, (this->heldDAS <= 0)); + else if (playerActions.contains(MOVE_RIGHT)) { + this->movePiece(1); } else { this->heldDAS = 0; } + if (before != this->board.getActivePiecePosition()) { + this->totalLockDelay = 0; + } + /* ROTATIONS */ + before = this->board.getActivePiecePosition(); + + if (playerActions.contains(ROTATE_0) && (!this->heldActions.contains(ROTATE_0))) { + if (this->board.rotate(NONE)) { + this->totalLockDelay = 0; + } + } if (playerActions.contains(ROTATE_CW) && (!this->heldActions.contains(ROTATE_CW))) { - this->board.rotate(CLOCKWISE); + if (this->board.rotate(CLOCKWISE)) { + this->totalLockDelay = 0; + } } if (playerActions.contains(ROTATE_180) && (!this->heldActions.contains(ROTATE_180))) { - this->board.rotate(DOUBLE); + if (this->board.rotate(DOUBLE)) { + this->totalLockDelay = 0; + } } if (playerActions.contains(ROTATE_CCW) && (!this->heldActions.contains(ROTATE_CCW))) { - this->board.rotate(COUNTERCLOCKWISE); + if (this->board.rotate(COUNTERCLOCKWISE)) { + this->totalLockDelay = 0; + } + } + + if (before != this->board.getActivePiecePosition()) { + this->subVerticalPosition = 0; } /* SOFT DROP */ @@ -149,7 +193,7 @@ void Game::nextFrame(const std::set& playerActions) { 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); + int appliedGravity = this->parameters.getGravity() * std::max((double) this->board.getBoard().getBaseHeight() / 20.0, 1.0); this->subVerticalPosition += appliedGravity; while (this->subVerticalPosition >= SUBPX_PER_ROW) { @@ -171,16 +215,18 @@ void Game::nextFrame(const std::set& playerActions) { } } - // remove initial actions only once they've been applied if (AREJustEnded) { this->initialActions.clear(); } } this->framesPassed++; + if (this->lost) { + return; + } } - // update remembered actions + // update remembered actions for next frame if ((!this->started) || this->leftARETime > 0) { for (Action action : playerActions) { this->initialActions.insert(action); @@ -189,19 +235,24 @@ void Game::nextFrame(const std::set& playerActions) { 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; + if (this->leftARETime > 0) { + if (playerActions.contains(MOVE_LEFT)) { + this->heldDAS = std::min(-1, this->heldDAS - 1); + } + else 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) { +void Game::movePiece(int movement) { + int appliedDAS = this->parameters.getDAS(); + int appliedARR = this->parameters.getARR(); + + if ((this->heldDAS * movement) <= 0) { this->heldDAS = movement; this->heldARR = 0; } @@ -209,9 +260,12 @@ void Game::movePiece(int movement, bool resetDirection) { this->heldDAS += movement; } - if (abs(this->heldDAS) > this->parameters.getDAS()) { - int appliedARR = this->parameters.getARR(); + if (abs(this->heldDAS) == appliedDAS + 1) { + if (movement == -1) this->board.moveLeft(); + if (movement == 1) this->board.moveRight(); + } + if (abs(this->heldDAS) > appliedDAS + 1) { // ARR=0 -> instant movement if (appliedARR == 0) { if (movement == -1) while (this->board.moveLeft()); @@ -233,7 +287,6 @@ void Game::lockPiece() { LineClear clear = this->board.lockPiece(); this->parameters.clearLines(clear.lines); - // update B2B and score bool B2BConditionsAreMet = ((clear.lines > B2B_MIN_LINE_NUMBER) || clear.isSpin || clear.isMiniSpin); if (clear.lines > 0) { /* clearing one more line is worth 2x more @@ -246,16 +299,14 @@ void Game::lockPiece() { } this->B2BChain = B2BConditionsAreMet; - // 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(); + this->lost = this->board.spawnNextPiece(); } } @@ -291,22 +342,26 @@ bool Game::areBlocksBones() const { return this->parameters.getBoneBlocks(); } -Board Game::getBoard() const { +Position Game::ghostPiecePosition() const { + return this->board.lowestPosition(); +} + +const Board& Game::getBoard() const { return this->board.getBoard(); } -Piece Game::getActivePiece() const { +const std::shared_ptr& Game::getActivePiece() const { return this->board.getActivePiece(); } -Position Game::getActivePiecePosition() const { +const Position& Game::getActivePiecePosition() const { return this->board.getActivePiecePosition(); } -Piece Game::getHeldPiece() const { +const std::shared_ptr& Game::getHeldPiece() const { return this->board.getHeldPiece(); } -std::vector Game::getNextPieces() const { +const std::vector& Game::getNextPieces() const { return this->board.getNextPieces(); } diff --git a/src/Core/Game.h b/src/Core/Game.h index 61fac3e..0c3beeb 100644 --- a/src/Core/Game.h +++ b/src/Core/Game.h @@ -63,7 +63,7 @@ class Game { /** * Move the piece in the specified direction */ - void movePiece(int movement, bool resetDirection); + void movePiece(int movement); /** * Locks the piece, updates level and score and spawns the next piece if necessary @@ -112,27 +112,32 @@ class Game { bool areBlocksBones() const; /** - * @return A copy of the board + * @return The position of the ghost piece */ - Board getBoard() const; + Position ghostPiecePosition() const; /** - * @return A copy of the active piece + * @return The board */ - Piece getActivePiece() const; + const Board& getBoard() const; /** - * @return A copy of the active piece position + * @return A pointer to the active piece, can be null */ - Position getActivePiecePosition() const; + const std::shared_ptr& getActivePiece() const; /** - * @return A copy of the held piece + * @return The position of the active piece */ - Piece getHeldPiece() const; + const Position& getActivePiecePosition() const; /** - * @return A copy of the next pieces queue + * @return A pointer to the held piece, can be null */ - std::vector getNextPieces() const; + const std::shared_ptr& getHeldPiece() const; + + /** + * @return The next piece queue, can be empty + */ + const std::vector& getNextPieces() const; }; diff --git a/src/Core/GameBoard.cpp b/src/Core/GameBoard.cpp index e32a5b2..f5e0526 100644 --- a/src/Core/GameBoard.cpp +++ b/src/Core/GameBoard.cpp @@ -72,15 +72,22 @@ bool GameBoard::moveDown() { bool GameBoard::rotate(Rotation rotation) { Piece stored = *this->activePiece; - this->rotate(rotation); + this->activePiece->rotate(rotation); // before trying to kick, check if the piece can rotate without kicking - if (!this->activePieceInWall()) { - this->isLastMoveKick = false; - return true; + if (rotation == NONE) { + if (this->moveDown()) { + this->isLastMoveKick = false; + return true; + } + } + else { + if (!this->activePieceInWall()) { + this->isLastMoveKick = false; + return true; + } } - // get the list of positions that touches the original piece std::set safePositions; for (Position position : stored.getPositions()) { Position positionInGrid(position + this->activePiecePosition); @@ -91,7 +98,10 @@ bool GameBoard::rotate(Rotation rotation) { safePositions.insert(positionInGrid + Position{-1, 0}); } - // try kicking the piece down + // first try kicking the piece down + if (rotation == NONE) { + this->activePiecePosition.y -= 1; + } bool suceeded = this->tryKicking(true, safePositions); if (suceeded) { this->isLastMoveKick = true; @@ -99,6 +109,9 @@ bool GameBoard::rotate(Rotation rotation) { } // if it doesn't work try kicking the piece up + if (rotation == NONE) { + this->activePiecePosition.y += 1; + } suceeded = this->tryKicking(false, safePositions); if (suceeded) { this->isLastMoveKick = true; @@ -107,7 +120,10 @@ bool GameBoard::rotate(Rotation rotation) { // if it still doesn't work, abort the rotation this->activePiece = std::make_shared(stored); - return false; + if (rotation == NONE) { + this->isLastMoveKick = false; + } + return (rotation == NONE); } bool GameBoard::tryKicking(bool testingBottom, const std::set& safePositions) { @@ -118,9 +134,9 @@ bool GameBoard::tryKicking(bool testingBottom, const std::set& safePos // 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; + int i = (j == 0) ? 1 : 0; do { - // check right before right arbitrarly, we don't decide this with rotations since it would still be arbitrary with 180° rotations + // check right before left arbitrarly, we don't decide this with rotations since it would still be arbitrary with 180° rotations if (overlapsRight) { Position shift{+i, j}; if (!this->activePieceOverlaps(safePositions, shift)) { @@ -175,21 +191,23 @@ bool GameBoard::hold(Rotation initialRotation) { } } - this->goToSpawnPosition(); - Piece stored = *this->activePiece; + Position storedPosition = this->activePiecePosition; + this->goToSpawnPosition(); this->rotate(initialRotation); // if the piece can't spawn, abort initial rotation if (this->activePieceInWall()) { this->activePiece = std::make_shared(stored); - + this->goToSpawnPosition(); + // if the piece still can't spawn, abort holding if (this->activePieceInWall()) { if (isFirstTimeHolding) { this->activePiece = nullptr; } std::swap(this->activePiece, this->heldPiece); + this->activePiecePosition = storedPosition; return false; } } @@ -200,6 +218,8 @@ bool GameBoard::hold(Rotation initialRotation) { this->nextQueue.erase(this->nextQueue.begin()); } + this->heldPiece->defaultRotation(); + // this piece has done nothing yet this->isLastMoveKick = false; @@ -207,10 +227,7 @@ bool GameBoard::hold(Rotation initialRotation) { } bool GameBoard::spawnNextPiece() { - // generate a new piece this->nextQueue.push_back(this->generator.getNext()); - - // get next piece from queue this->activePiece = std::make_shared(this->nextQueue.front()); this->nextQueue.erase(this->nextQueue.begin()); @@ -219,13 +236,22 @@ bool GameBoard::spawnNextPiece() { // this piece has done nothing yet this->isLastMoveKick = false; - return !this->activePieceInWall(); + return this->activePieceInWall(); } -bool GameBoard::touchesGround() { +bool GameBoard::touchesGround() const { return this->activePieceInWall(Position{0, -1}); } +Position GameBoard::lowestPosition() const { + Position shift = Position{0, -1}; + while (!activePieceInWall(shift)) { + shift.y -= 1; + } + shift.y += 1; + return (this->activePiecePosition + shift); +} + LineClear GameBoard::lockPiece() { bool isLockedInPlace = (this->activePieceInWall(Position{0, 1}) && this->activePieceInWall(Position{1, 0}) && this->activePieceInWall(Position{-1, 0}) && this->activePieceInWall(Position{0, -1})); @@ -248,23 +274,23 @@ void GameBoard::addGarbageRows(int number) { } } -Board GameBoard::getBoard() const { +const Board& GameBoard::getBoard() const { return this->board; } -Piece GameBoard::getActivePiece() const { - return *this->activePiece; +const std::shared_ptr& GameBoard::getActivePiece() const { + return this->activePiece; } -Position GameBoard::getActivePiecePosition() const { +const Position& GameBoard::getActivePiecePosition() const { return this->activePiecePosition; } -Piece GameBoard::getHeldPiece() const { - return *this->heldPiece; +const std::shared_ptr& GameBoard::getHeldPiece() const { + return this->heldPiece; } -std::vector GameBoard::getNextPieces() const { +const std::vector& GameBoard::getNextPieces() const { return this->nextQueue; } @@ -293,6 +319,8 @@ void GameBoard::goToSpawnPosition() { // center the piece horizontally, biased towards left this->activePiecePosition.x = (this->board.getWidth() - this->activePiece->getLength()) / 2; + + this->activePiece->defaultRotation(); } std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard) { diff --git a/src/Core/GameBoard.h b/src/Core/GameBoard.h index 68e8137..b55097f 100644 --- a/src/Core/GameBoard.h +++ b/src/Core/GameBoard.h @@ -60,7 +60,7 @@ class GameBoard { bool moveDown(); /** - * Tries rotating the piece and kicking it if necessary + * Tries rotating the piece and kicking it if necessary, if it's a 0° rotation, it will forcefully try kicking * @return If it suceeded */ bool rotate(Rotation rotation); @@ -89,7 +89,13 @@ class GameBoard { * Checks is the active piece as a wall directly below one of its position * @return If it touches a ground */ - bool touchesGround(); + bool touchesGround() const; + + /** + * Computes what the piece position would be if it were to be dropped down as much as possible + * @return The lowest position before hitting a wall + */ + Position lowestPosition() const; /** * Locks the active piece into the board and clears lines if needed @@ -103,34 +109,34 @@ class GameBoard { void addGarbageRows(int number); /** - * @return A copy of the board + * @return The board */ - Board getBoard() const; + const Board& getBoard() const; /** - * @return A copy of the active piece + * @return A pointer to the active piece, can be null */ - Piece getActivePiece() const; + const std::shared_ptr& getActivePiece() const; /** - * @return A copy of the position of the active piece + * @return The position of the active piece */ - Position getActivePiecePosition() const; + const Position& getActivePiecePosition() const; /** - * @return A copy of the held piece + * @return A pointer to the held piece, can be null */ - Piece getHeldPiece() const; + const std::shared_ptr& getHeldPiece() const; /** - * @return A copy of the next piece queue + * @return The next piece queue, can be empty */ - std::vector getNextPieces() const; + const std::vector& getNextPieces() const; private: /** * Checks if one of the active piece's positions touches a wall in the board - * @return If the active piece is in a wall + * @return If the active piece spawned in a wall */ bool activePieceInWall(const Position& shift = Position{0, 0}) const; diff --git a/src/Core/GameParameters.cpp b/src/Core/GameParameters.cpp index 2f7009b..e9590d8 100644 --- a/src/Core/GameParameters.cpp +++ b/src/Core/GameParameters.cpp @@ -118,7 +118,7 @@ void GameParameters::updateStats() { if (this->level < 0) { this->gravity = gravityPerLevel[0]; } - else if (this->gravity > 20) { + else if (this->level > 20) { this->gravity = gravityPerLevel[20]; } else { diff --git a/src/Core/PiecesList.h b/src/Core/PiecesList.h index 5f26216..4110ae4 100644 --- a/src/Core/PiecesList.h +++ b/src/Core/PiecesList.h @@ -77,12 +77,12 @@ class PiecesList { int getNumberOfPieces(int size) const; /** - * @return The indexes of all selected pieces + * @return A copy of the indexes of all selected pieces */ std::vector> getSelectedPieces() const; /** - * @return The piece corresponding to the specified index + * @return A copy of the piece corresponding to the specified index */ Piece getPiece(const std::pair& pieceIndex) const; diff --git a/src/Pieces/Piece.cpp b/src/Pieces/Piece.cpp index 05c1aee..7023cf7 100644 --- a/src/Pieces/Piece.cpp +++ b/src/Pieces/Piece.cpp @@ -12,6 +12,8 @@ Piece::Piece(const Polyomino& polyomino, Block blockType) : polyomino(polyomino), blockType(blockType) { + + this->rotationState = NONE; } void Piece::rotate(Rotation rotation) { @@ -21,9 +23,22 @@ void Piece::rotate(Rotation rotation) { this->polyomino.rotate180(); if (rotation == COUNTERCLOCKWISE) this->polyomino.rotateCCW(); + + this->rotationState += rotation; } -std::set Piece::getPositions() const { +void Piece::defaultRotation() { + if (this->rotationState == CLOCKWISE) + this->polyomino.rotateCCW(); + if (this->rotationState == DOUBLE) + this->polyomino.rotate180(); + if (this->rotationState == COUNTERCLOCKWISE) + this->polyomino.rotateCW(); + + this->rotationState = NONE; +} + +const std::set& Piece::getPositions() const { return this->polyomino.getPositions(); } diff --git a/src/Pieces/Piece.h b/src/Pieces/Piece.h index 085da47..cd74eb1 100644 --- a/src/Pieces/Piece.h +++ b/src/Pieces/Piece.h @@ -13,8 +13,9 @@ */ class Piece { private: - Polyomino polyomino; // a polyomino representing the piece, (0, 0) is downleft - Block blockType; // the block type of the piece + Polyomino polyomino; // a polyomino representing the piece, (0, 0) is downleft + Block blockType; // the block type of the piece + Rotation rotationState; // the current rotation of the piece public: /** @@ -28,9 +29,14 @@ class Piece { void rotate(Rotation rotation); /** - * @return A copy of the list of positions of the piece + * Rotates the piece to its default rotation */ - std::set getPositions() const; + void defaultRotation(); + + /** + * @return The list of positions of the piece + */ + const std::set& getPositions() const; /** * @return The length of the piece diff --git a/src/Pieces/Polyomino.cpp b/src/Pieces/Polyomino.cpp index 016fd62..7ee6118 100644 --- a/src/Pieces/Polyomino.cpp +++ b/src/Pieces/Polyomino.cpp @@ -267,7 +267,7 @@ void Polyomino::tryToInsertPosition(std::set& emptyPositions, const Po tryToInsertPosition(emptyPositions, Position{candidate.x - 1, candidate.y}); } -std::set Polyomino::getPositions() const { +const std::set& Polyomino::getPositions() const { return this->positions; } @@ -293,7 +293,7 @@ bool Polyomino::operator<(const Polyomino& other) const { return false; } -bool Polyomino::operator ==(const Polyomino& other) const { +bool Polyomino::operator==(const Polyomino& other) const { return this->positions == other.positions; } diff --git a/src/Pieces/Polyomino.h b/src/Pieces/Polyomino.h index 6ca4c50..8099195 100644 --- a/src/Pieces/Polyomino.h +++ b/src/Pieces/Polyomino.h @@ -78,9 +78,9 @@ class Polyomino { public: /** - * @return A copy of the positions of the polyomino + * @return The positions of the polyomino */ - std::set getPositions() const; + const std::set& getPositions() const; /** * @return The length of the polyomino @@ -103,7 +103,7 @@ class Polyomino { * Equality operator, two polyominos are equal if their positions are the same, that means two polyominos of the same shape at different places will not be equal * @return If the polyomino is equal to another */ - bool operator ==(const Polyomino& other) const; + bool operator==(const Polyomino& other) const; /** * Stream output operator, adds a 2D grid representing the polyomino diff --git a/src/Pieces/Position.h b/src/Pieces/Position.h index 47ebb3e..710d0ac 100644 --- a/src/Pieces/Position.h +++ b/src/Pieces/Position.h @@ -62,6 +62,14 @@ inline bool operator==(const Position& left, const Position& right) { return (left.x == right.x) && (left.y == right.y); } +/** + * Inequality operator, two positions aren't equal if their coordinates aren't + * @return If the two positions aren't equals + */ +inline bool operator!=(const Position& left, const Position& right) { + return (left.x != right.x) || (left.y != right.y); +} + /** * Stream output operator, adds the coordinates of the position to the stream * @return A reference to the output stream diff --git a/src/Pieces/Rotation.h b/src/Pieces/Rotation.h index 90d4955..f8ef4c4 100644 --- a/src/Pieces/Rotation.h +++ b/src/Pieces/Rotation.h @@ -17,7 +17,7 @@ enum Rotation { * @return A rotation corresponding to doing both rotations */ inline Rotation operator+(const Rotation& left, const Rotation& right) { - return Rotation((left + right) % 4); + return Rotation(((int) left + (int) right) % 4); } /** diff --git a/src/TextUI/TextApp.cpp b/src/TextUI/TextApp.cpp new file mode 100644 index 0000000..9c9cf4a --- /dev/null +++ b/src/TextUI/TextApp.cpp @@ -0,0 +1,323 @@ +#include "TextApp.h" + +#include "../Core/Menu.h" + +#include +#include +#include +#include +#include +#include +#include + +static const int FRAMES_PER_INPUT = FRAMES_PER_SECOND / 2; +static const int MAXIMUM_PIECE_SIZE = 10; +static const int DEFAULT_PIECE_SIZE = 4; +static const int MAXIMUM_BOARD_WIDTH = 30; +static const int MAXIMYM_BOARD_HEIGHT = 40; +static const Gamemode DEFAULT_GAMEMODE = SPRINT; + + +TextApp::TextApp() { + this->defaultKeybinds(); + + this->gameMenu.getPiecesList().loadPieces(MAXIMUM_PIECE_SIZE); + this->gameMenu.getPiecesList().selectAllPieces(DEFAULT_PIECE_SIZE); + + this->gameMenu.getPlayerControls().setDAS(FRAMES_PER_INPUT - 1); + this->gameMenu.getPlayerControls().setARR(FRAMES_PER_INPUT); + this->gameMenu.getPlayerControls().setSDR(0); +} + +void TextApp::run() { + bool quit = false; + std::string answer; + int selectedAnswer = 0; + while (!quit) { + std::cout << "\n\n\n"; + std::cout << "===| WELCOME TO JMINOS! |===" << std::endl; + std::cout << "1- Change pieces" << std::endl; + std::cout << "2- Change board" << std::endl; + std::cout << "3- See controls" << std::endl; + std::cout << "4- Start game" << std::endl; + std::cout << "5- Quit" << std::endl; + std::cout << "Choice: "; + std::getline(std::cin, answer); + try { + selectedAnswer = std::stoi(answer); + } + catch (std::exception ignored) {} + + switch (selectedAnswer) { + case 1 : {this->choosePieces(); break;} + case 2 : {this->chooseBoardSize(); break;} + case 3 : {this->seeKeybinds(); break;} + case 4 : {this->startGame(); break;} + case 5 : {quit = true; break;} + default : std::cout << "Invalid answer!" << std::endl; + } + } + std::cout << "===| SEE YA NEXT TIME! |==="; +} + +void TextApp::choosePieces() { + std::cout << "\n\n\n"; + std::cout << "Choose which piece sizes to play with (from 1 to " << MAXIMUM_PIECE_SIZE << "), separate mutltiple sizes with blank spaces." << std::endl; + std::cout << "Choice: "; + std::string answer; + std::getline(std::cin, answer); + + this->gameMenu.getPiecesList().unselectAll(); + std::cout << "Selected pieces of sizes:"; + std::stringstream answerStream(answer); + for (std::string size; std::getline(answerStream, size, ' ');) { + try { + int selectedSize = std::stoi(size); + if (selectedSize >= 1 && selectedSize <= MAXIMUM_PIECE_SIZE) { + if (this->gameMenu.getPiecesList().selectAllPieces(selectedSize)) { + std::cout << " " << selectedSize; + } + } + } + catch (std::exception ignored) {} + } + + std::string waiting; + std::getline(std::cin, waiting); +} + +void TextApp::chooseBoardSize() { + std::string answer; + + std::cout << "\n\n\n"; + std::cout << "Current board width and height: " << this->gameMenu.getBoardWidth() << "x" << this->gameMenu.getBoardHeight() << std::endl; + + std::cout << "Choose the width of the board (from 1 to " << MAXIMUM_BOARD_WIDTH << ")." << std::endl; + std::cout << "Choice: "; + std::getline(std::cin, answer); + try { + int selectedSize = std::stoi(answer); + if (selectedSize >= 1 && selectedSize <= MAXIMUM_BOARD_WIDTH) { + this->gameMenu.setBoardWidth(selectedSize); + } + } + catch (std::exception ignored) {} + + std::cout << "Choose the height of the board (from 1 to " << MAXIMYM_BOARD_HEIGHT << ")." << std::endl; + std::cout << "Choice: "; + std::getline(std::cin, answer); + try { + int selectedSize = std::stoi(answer); + if (selectedSize >= 1 && selectedSize <= MAXIMYM_BOARD_HEIGHT) { + this->gameMenu.setBoardHeight(selectedSize); + } + } + catch (std::exception ignored) {} + + std::cout << "New board width and height: " << this->gameMenu.getBoardWidth() << "x" << this->gameMenu.getBoardHeight(); + + std::string waiting; + std::getline(std::cin, waiting); +} + +void TextApp::seeKeybinds() const { + std::cout << "\n\n\n"; + std::cout << "Quit/Pause/Retry: quit/pause/retry" << std::endl; + std::cout << "Hold : h" << std::endl; + std::cout << "Soft/Hard drop : sd/hd" << std::endl; + std::cout << "Move left/right : l/r" << std::endl; + std::cout << "Rotate 0/CW/180/CCW: c/cw/cc/ccw" << std::endl; + std::cout << "\n"; + std::cout << "To do several actions at the same time, separe them with blank spaces." << std::endl; + std::cout << "Remember that for certains actions like hard dropping, you need to release it before using it again, even if a different piece spawned."; + std::string waiting; + std::getline(std::cin, waiting); +} + +void TextApp::defaultKeybinds() { + this->keybinds.clear(); + this->keybinds.insert({"quit", QUIT}); + this->keybinds.insert({"pause", PAUSE}); + this->keybinds.insert({"retry", RETRY}); + this->keybinds.insert({"h", HOLD}); + this->keybinds.insert({"sd", SOFT_DROP}); + this->keybinds.insert({"hd", HARD_DROP}); + this->keybinds.insert({"l", MOVE_LEFT}); + this->keybinds.insert({"r", MOVE_RIGHT}); + this->keybinds.insert({"c", ROTATE_0}); + this->keybinds.insert({"cw", ROTATE_CW}); + this->keybinds.insert({"cc", ROTATE_180}); + this->keybinds.insert({"ccw", ROTATE_CCW}); +} + +void TextApp::startGame() const { + Game game = this->gameMenu.startGame(DEFAULT_GAMEMODE); + game.start(); + + std::cout << "\n\n\n"; + this->printGame(game); + + bool quit = false; + bool paused = false; + std::string answer; + while (!quit) { + std::cout << "Actions: "; + std::getline(std::cin, answer); + std::stringstream answerStream(answer); + std::vector actions; + for (std::string action; std::getline(answerStream, action, ' ');) { + actions.push_back(action); + } + + std::set playerActions; + for (std::string action : actions) { + if (this->keybinds.contains(action)) { + playerActions.insert(this->keybinds.at(action)); + } + } + + if (playerActions.contains(PAUSE)) { + paused = (!paused); + } + + if (!paused) { + if (playerActions.contains(QUIT)) { + quit = true; + } + else if (playerActions.contains(RETRY)) { + game.reset(); + game.start(); + } + else { + for (int i = 0; i < FRAMES_PER_INPUT; i++) { + game.nextFrame(playerActions); + } + } + } + + std::cout << "\n\n\n"; + if (paused) { + std::cout << "--<[PAUSED]>--" << std::endl; + } + this->printGame(game); + + if (game.hasLost()) { + quit = true; + std::cout << "You lost!" << std::endl; + } + else if (game.hasWon()) { + quit = true; + std::cout << "You won!" << std::endl; + } + } +} + +void TextApp::printGame(const Game& game) const { + int maxHeight = game.getBoard().getGridHeight(); + if (game.getActivePiece() != nullptr) { + for (const Position& position : game.getActivePiece()->getPositions()) { + maxHeight = std::max(maxHeight, position.y + game.getActivePiecePosition().y); + } + } + + bool lineCountPrinted = false; + bool holdBoxStartedPrinting = false; + bool holdBoxFinishedPrinting = false; + bool nextQueueStartedPrinting = false; + bool nextQueueFinishedPrinting = false; + int nextQueuePrintedPiece; + int printedPieceLineHeight; + + for (int y = maxHeight; y >= 0; y--) { + for (int x = 0; x < game.getBoard().getWidth(); x++) { + bool isActivePieceHere = (game.getActivePiece() != nullptr) && (game.getActivePiece()->getPositions().contains(Position{x, y} - game.getActivePiecePosition())); + bool isGhostPieceHere = (game.getActivePiece() != nullptr) && (game.getActivePiece()->getPositions().contains(Position{x, y} - game.ghostPiecePosition())); + Block block = (isActivePieceHere || isGhostPieceHere) ? game.getActivePiece()->getBlockType() : game.getBoard().getBlock(Position{x, y}); + + if (isActivePieceHere || isGhostPieceHere) { + std::cout << getConsoleColorCode(block); + } + else { + std::cout << getResetConsoleColorCode(); + } + + if (block != NOTHING && (!(isGhostPieceHere && !isActivePieceHere))) { + std::cout << "*"; + } + else { + if (y < game.getBoard().getBaseHeight()) { + if (isGhostPieceHere) { + std::cout << "="; + } + else { + std::cout << "-"; + } + } + else { + std::cout << " "; + } + } + } + + if (y < game.getBoard().getGridHeight()) { + std::cout << " "; + if (!lineCountPrinted) { + std::cout << getResetConsoleColorCode() << "Lines: " << game.getClearedLines(); + lineCountPrinted = true; + } + else if (!holdBoxFinishedPrinting) { + if (!holdBoxStartedPrinting) { + std::cout << getResetConsoleColorCode() << "Hold:"; + printedPieceLineHeight = (game.getHeldPiece() == nullptr) ? -1 : (game.getHeldPiece()->getLength() - 1); + holdBoxStartedPrinting = true; + } + else { + for (int i = 0; i < game.getHeldPiece()->getLength(); i++) { + if (game.getHeldPiece()->getPositions().contains(Position{i, printedPieceLineHeight})) { + std::cout << getConsoleColorCode(game.getHeldPiece()->getBlockType()) << "*"; + } + else { + std::cout << getResetConsoleColorCode() << "-"; + } + } + printedPieceLineHeight--; + } + if (printedPieceLineHeight < 0) { + holdBoxFinishedPrinting = true; + } + } + else if (!nextQueueFinishedPrinting) { + if (!nextQueueStartedPrinting) { + std::cout << getResetConsoleColorCode() << "Next:"; + printedPieceLineHeight = (game.getNextPieces().size() == 0) ? -1 : (game.getNextPieces().at(0).getLength() - 1); + nextQueuePrintedPiece = 0; + nextQueueStartedPrinting = true; + } + else { + for (int i = 0; i < game.getNextPieces().at(nextQueuePrintedPiece).getLength(); i++) { + if (game.getNextPieces().at(nextQueuePrintedPiece).getPositions().contains(Position{i, printedPieceLineHeight})) { + std::cout << getConsoleColorCode(game.getNextPieces().at(nextQueuePrintedPiece).getBlockType()) << "*"; + } + else { + std::cout << getResetConsoleColorCode() << "-"; + } + } + printedPieceLineHeight--; + } + if (printedPieceLineHeight < 0) { + nextQueuePrintedPiece++; + if (nextQueuePrintedPiece >= game.getNextPieces().size()) { + nextQueueFinishedPrinting = true; + } + else { + printedPieceLineHeight = game.getNextPieces().at(nextQueuePrintedPiece).getLength() - 1; + } + } + } + } + + std::cout << "\n"; + } + + std::cout << getResetConsoleColorCode(); +} diff --git a/src/TextUI/TextApp.h b/src/TextUI/TextApp.h new file mode 100644 index 0000000..fca81ad --- /dev/null +++ b/src/TextUI/TextApp.h @@ -0,0 +1,58 @@ +#pragma once + +#include "../Core/Menu.h" + +#include +#include + + +/** + * Textual interface for the app + */ +class TextApp { + private: + Menu gameMenu; // the interface with the core of the app + std::map keybinds; // what the player needs to type to perform in-game actions + + public: + /** + * Initializes the app with default settings + */ + TextApp(); + + /** + * Runs the app + */ + void run(); + + private: + /** + * Sub-menu to select which pieces to play with + */ + void choosePieces(); + + /** + * Sub-menu to change the size of the board + */ + void chooseBoardSize(); + + /** + * Sub-menu to see the in-game controls + */ + void seeKeybinds() const; + + /** + * Sets the controls to their default values + */ + void defaultKeybinds(); + + /** + * Starts a new game with the current settings + */ + void startGame() const; + + /** + * Prints the current state of a game to the console + */ + void printGame(const Game& game) const; +}; diff --git a/src/TextUI/main.cpp b/src/TextUI/main.cpp index ba933d2..5e5f631 100644 --- a/src/TextUI/main.cpp +++ b/src/TextUI/main.cpp @@ -1,6 +1,6 @@ -#include "../Core/Menu.h" #include "../Pieces/Generator.h" #include "../Pieces/PiecesFiles.h" +#include "TextApp.h" #include @@ -18,13 +18,8 @@ void readStatsFromFilesForAllSizes(int amount); int main(int argc, char** argv) { std::srand(std::time(NULL)); - Menu menu; - menu.getPiecesList().loadPieces(4); - menu.getPiecesList().selectAllPieces(4); - Game game = menu.startGame(SPRINT); - game.start(); - - loadFromFilesForOneSize(13); + TextApp UI; + UI.run(); return 0; }