ajouté interface textuelle

This commit is contained in:
2025-03-02 23:36:20 +01:00
parent 857f90d646
commit 1033f3a64c
21 changed files with 646 additions and 113 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -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: 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 - Pause
- Retry - Retry
- Hold - Hold
- Move left - Move left
- Move right - Move right
- Rotate 0°
- Rotate clockwise (CW) - Rotate clockwise (CW)
- Rotate 180° - Rotate 180°
- Rotate counter-clockwise (CCW) - 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`` 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 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`` 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 5. Do the same as step 4 but now we move the piece one line up every time
6. Cancel the rotation 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 ## 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. 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.

View File

@@ -1,10 +1,13 @@
#pragma once #pragma once
#include <iostream>
/** /**
* The list of actions that can be taken by the player * The list of actions that can be taken by the player
*/ */
enum Action { enum Action {
QUIT,
PAUSE, PAUSE,
RETRY, RETRY,
HOLD, HOLD,
@@ -12,7 +15,33 @@ enum Action {
HARD_DROP, HARD_DROP,
MOVE_LEFT, MOVE_LEFT,
MOVE_RIGHT, MOVE_RIGHT,
ROTATE_0,
ROTATE_CW, ROTATE_CW,
ROTATE_180, ROTATE_180,
ROTATE_CCW 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;
}

View File

@@ -87,7 +87,7 @@ Block Board::getBlock(const Position& position) const {
return this->grid.at(position.y).at(position.x); return this->grid.at(position.y).at(position.x);
} }
std::vector<std::vector<Block>> Board::getBlocks() const { const std::vector<std::vector<Block>>& Board::getBlocks() const {
return this->grid; return this->grid;
} }

View File

@@ -44,14 +44,14 @@ class Board {
void clearBoard(); 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; Block getBlock(const Position& position) const;
/** /**
* @return A copy of the grid * @return The grid
*/ */
std::vector<std::vector<Block>> getBlocks() const; const std::vector<std::vector<Block>>& getBlocks() const;
/** /**
* @return The width of the grid * @return The width of the grid

View File

@@ -25,7 +25,7 @@ Game::Game(Gamemode gamemode, const Player& controls, int boardWidth, int boardH
void Game::start() { void Game::start() {
this->started = true; this->started = true;
this->lost = this->board.spawnNextPiece(); this->leftARETime = 1;
} }
void Game::reset() { void Game::reset() {
@@ -48,7 +48,6 @@ void Game::initialize() {
this->heldDAS = 0; this->heldDAS = 0;
this->heldARR = 0; this->heldARR = 0;
this->subVerticalPosition = 0; this->subVerticalPosition = 0;
this->leftARETime = 0;
this->totalLockDelay = 0; this->totalLockDelay = 0;
this->totalForcedLockDelay = 0; this->totalForcedLockDelay = 0;
} }
@@ -64,14 +63,14 @@ void Game::nextFrame(const std::set<Action>& playerActions) {
if (this->leftARETime == 0) { if (this->leftARETime == 0) {
if (AREJustEnded) { if (AREJustEnded) {
this->board.spawnNextPiece(); this->lost = this->board.spawnNextPiece();
} }
/* IRS and IHS */ /* IRS and IHS */
Rotation initialRotation = NONE Rotation initialRotation = NONE
+ (this->initialActions.contains(ROTATE_CW)) ? CLOCKWISE : NONE + ((this->initialActions.contains(ROTATE_CW)) ? CLOCKWISE : NONE)
+ (this->initialActions.contains(ROTATE_180)) ? DOUBLE : NONE + ((this->initialActions.contains(ROTATE_180)) ? DOUBLE : NONE)
+ (this->initialActions.contains(ROTATE_CCW)) ? COUNTERCLOCKWISE : NONE; + ((this->initialActions.contains(ROTATE_CCW)) ? COUNTERCLOCKWISE : NONE);
if (this->initialActions.contains(HOLD)) { if (this->initialActions.contains(HOLD)) {
if (this->board.hold(initialRotation)) { if (this->board.hold(initialRotation)) {
@@ -79,13 +78,35 @@ void Game::nextFrame(const std::set<Action>& playerActions) {
this->totalLockDelay = 0; this->totalLockDelay = 0;
this->heldARR = 0; this->heldARR = 0;
} }
else {
this->lost = true;
}
} }
else { else {
if (initialRotation != NONE) { if (initialRotation != NONE || this->initialActions.contains(ROTATE_0)) {
this->board.rotate(initialRotation); 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 */ /* HOLD */
if (playerActions.contains(HOLD) && (!this->heldActions.contains(HOLD))) { if (playerActions.contains(HOLD) && (!this->heldActions.contains(HOLD))) {
if (this->board.hold()) { if (this->board.hold()) {
@@ -96,25 +117,48 @@ void Game::nextFrame(const std::set<Action>& playerActions) {
} }
/* MOVE LEFT/RIGHT */ /* MOVE LEFT/RIGHT */
Position before = this->board.getActivePiecePosition();
if (playerActions.contains(MOVE_LEFT)) { if (playerActions.contains(MOVE_LEFT)) {
this->movePiece(-1, (this->heldDAS >= 0)); this->movePiece(-1);
} }
if (playerActions.contains(MOVE_RIGHT)) { else if (playerActions.contains(MOVE_RIGHT)) {
this->movePiece(1, (this->heldDAS <= 0)); this->movePiece(1);
} }
else { else {
this->heldDAS = 0; this->heldDAS = 0;
} }
if (before != this->board.getActivePiecePosition()) {
this->totalLockDelay = 0;
}
/* ROTATIONS */ /* 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))) { 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))) { 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))) { 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 */ /* SOFT DROP */
@@ -149,7 +193,7 @@ void Game::nextFrame(const std::set<Action>& playerActions) {
else { else {
/* GRAVITY */ /* GRAVITY */
// parameters.getGravity() gives the gravity for an assumed 20-line high board // 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; this->subVerticalPosition += appliedGravity;
while (this->subVerticalPosition >= SUBPX_PER_ROW) { while (this->subVerticalPosition >= SUBPX_PER_ROW) {
@@ -171,16 +215,18 @@ void Game::nextFrame(const std::set<Action>& playerActions) {
} }
} }
// remove initial actions only once they've been applied
if (AREJustEnded) { if (AREJustEnded) {
this->initialActions.clear(); this->initialActions.clear();
} }
} }
this->framesPassed++; this->framesPassed++;
if (this->lost) {
return;
}
} }
// update remembered actions // update remembered actions for next frame
if ((!this->started) || this->leftARETime > 0) { if ((!this->started) || this->leftARETime > 0) {
for (Action action : playerActions) { for (Action action : playerActions) {
this->initialActions.insert(action); this->initialActions.insert(action);
@@ -189,19 +235,24 @@ void Game::nextFrame(const std::set<Action>& playerActions) {
this->heldActions = playerActions; this->heldActions = playerActions;
if (playerActions.contains(MOVE_LEFT)) { if (this->leftARETime > 0) {
this->heldDAS = std::min(-1, this->heldDAS - 1); 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 if (playerActions.contains(MOVE_RIGHT)) {
} this->heldDAS = std::max(1, this->heldDAS + 1);
else { }
this->heldDAS = 0; else {
this->heldDAS = 0;
}
} }
} }
void Game::movePiece(int movement, bool resetDirection) { void Game::movePiece(int movement) {
if (resetDirection) { int appliedDAS = this->parameters.getDAS();
int appliedARR = this->parameters.getARR();
if ((this->heldDAS * movement) <= 0) {
this->heldDAS = movement; this->heldDAS = movement;
this->heldARR = 0; this->heldARR = 0;
} }
@@ -209,9 +260,12 @@ void Game::movePiece(int movement, bool resetDirection) {
this->heldDAS += movement; this->heldDAS += movement;
} }
if (abs(this->heldDAS) > this->parameters.getDAS()) { if (abs(this->heldDAS) == appliedDAS + 1) {
int appliedARR = this->parameters.getARR(); if (movement == -1) this->board.moveLeft();
if (movement == 1) this->board.moveRight();
}
if (abs(this->heldDAS) > appliedDAS + 1) {
// ARR=0 -> instant movement // ARR=0 -> instant movement
if (appliedARR == 0) { if (appliedARR == 0) {
if (movement == -1) while (this->board.moveLeft()); if (movement == -1) while (this->board.moveLeft());
@@ -233,7 +287,6 @@ void Game::lockPiece() {
LineClear clear = this->board.lockPiece(); LineClear clear = this->board.lockPiece();
this->parameters.clearLines(clear.lines); this->parameters.clearLines(clear.lines);
// update B2B and score
bool B2BConditionsAreMet = ((clear.lines > B2B_MIN_LINE_NUMBER) || clear.isSpin || clear.isMiniSpin); bool B2BConditionsAreMet = ((clear.lines > B2B_MIN_LINE_NUMBER) || clear.isSpin || clear.isMiniSpin);
if (clear.lines > 0) { if (clear.lines > 0) {
/* clearing one more line is worth 2x more /* clearing one more line is worth 2x more
@@ -246,16 +299,14 @@ void Game::lockPiece() {
} }
this->B2BChain = B2BConditionsAreMet; this->B2BChain = B2BConditionsAreMet;
// reset active piece
this->subVerticalPosition = 0; this->subVerticalPosition = 0;
this->totalLockDelay = 0; this->totalLockDelay = 0;
this->totalForcedLockDelay = 0; this->totalForcedLockDelay = 0;
this->heldARR = 0; this->heldARR = 0;
// check for ARE
this->leftARETime = this->parameters.getARE(); this->leftARETime = this->parameters.getARE();
if (this->leftARETime == 0) { if (this->leftARETime == 0) {
this->board.spawnNextPiece(); this->lost = this->board.spawnNextPiece();
} }
} }
@@ -291,22 +342,26 @@ bool Game::areBlocksBones() const {
return this->parameters.getBoneBlocks(); 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(); return this->board.getBoard();
} }
Piece Game::getActivePiece() const { const std::shared_ptr<Piece>& Game::getActivePiece() const {
return this->board.getActivePiece(); return this->board.getActivePiece();
} }
Position Game::getActivePiecePosition() const { const Position& Game::getActivePiecePosition() const {
return this->board.getActivePiecePosition(); return this->board.getActivePiecePosition();
} }
Piece Game::getHeldPiece() const { const std::shared_ptr<Piece>& Game::getHeldPiece() const {
return this->board.getHeldPiece(); return this->board.getHeldPiece();
} }
std::vector<Piece> Game::getNextPieces() const { const std::vector<Piece>& Game::getNextPieces() const {
return this->board.getNextPieces(); return this->board.getNextPieces();
} }

View File

@@ -63,7 +63,7 @@ class Game {
/** /**
* Move the piece in the specified direction * 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 * Locks the piece, updates level and score and spawns the next piece if necessary
@@ -112,27 +112,32 @@ class Game {
bool areBlocksBones() const; 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<Piece>& 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<Piece> getNextPieces() const; const std::shared_ptr<Piece>& getHeldPiece() const;
/**
* @return The next piece queue, can be empty
*/
const std::vector<Piece>& getNextPieces() const;
}; };

View File

@@ -72,15 +72,22 @@ bool GameBoard::moveDown() {
bool GameBoard::rotate(Rotation rotation) { bool GameBoard::rotate(Rotation rotation) {
Piece stored = *this->activePiece; Piece stored = *this->activePiece;
this->rotate(rotation); this->activePiece->rotate(rotation);
// before trying to kick, check if the piece can rotate without kicking // before trying to kick, check if the piece can rotate without kicking
if (!this->activePieceInWall()) { if (rotation == NONE) {
this->isLastMoveKick = false; if (this->moveDown()) {
return true; 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<Position> safePositions; std::set<Position> safePositions;
for (Position position : stored.getPositions()) { for (Position position : stored.getPositions()) {
Position positionInGrid(position + this->activePiecePosition); Position positionInGrid(position + this->activePiecePosition);
@@ -91,7 +98,10 @@ bool GameBoard::rotate(Rotation rotation) {
safePositions.insert(positionInGrid + Position{-1, 0}); 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); bool suceeded = this->tryKicking(true, safePositions);
if (suceeded) { if (suceeded) {
this->isLastMoveKick = true; this->isLastMoveKick = true;
@@ -99,6 +109,9 @@ bool GameBoard::rotate(Rotation rotation) {
} }
// if it doesn't work try kicking the piece up // if it doesn't work try kicking the piece up
if (rotation == NONE) {
this->activePiecePosition.y += 1;
}
suceeded = this->tryKicking(false, safePositions); suceeded = this->tryKicking(false, safePositions);
if (suceeded) { if (suceeded) {
this->isLastMoveKick = true; this->isLastMoveKick = true;
@@ -107,7 +120,10 @@ bool GameBoard::rotate(Rotation rotation) {
// if it still doesn't work, abort the rotation // if it still doesn't work, abort the rotation
this->activePiece = std::make_shared<Piece>(stored); this->activePiece = std::make_shared<Piece>(stored);
return false; if (rotation == NONE) {
this->isLastMoveKick = false;
}
return (rotation == NONE);
} }
bool GameBoard::tryKicking(bool testingBottom, const std::set<Position>& safePositions) { bool GameBoard::tryKicking(bool testingBottom, const std::set<Position>& safePositions) {
@@ -118,9 +134,9 @@ bool GameBoard::tryKicking(bool testingBottom, const std::set<Position>& safePos
// we try from the center to the sides as long as the kicked piece touches the original // we try from the center to the sides as long as the kicked piece touches the original
bool overlapsLeft = true; bool overlapsLeft = true;
bool overlapsRight = true; bool overlapsRight = true;
int i = 0; int i = (j == 0) ? 1 : 0;
do { 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) { if (overlapsRight) {
Position shift{+i, j}; Position shift{+i, j};
if (!this->activePieceOverlaps(safePositions, shift)) { if (!this->activePieceOverlaps(safePositions, shift)) {
@@ -175,21 +191,23 @@ bool GameBoard::hold(Rotation initialRotation) {
} }
} }
this->goToSpawnPosition();
Piece stored = *this->activePiece; Piece stored = *this->activePiece;
Position storedPosition = this->activePiecePosition;
this->goToSpawnPosition();
this->rotate(initialRotation); this->rotate(initialRotation);
// if the piece can't spawn, abort initial rotation // if the piece can't spawn, abort initial rotation
if (this->activePieceInWall()) { if (this->activePieceInWall()) {
this->activePiece = std::make_shared<Piece>(stored); this->activePiece = std::make_shared<Piece>(stored);
this->goToSpawnPosition();
// if the piece still can't spawn, abort holding // if the piece still can't spawn, abort holding
if (this->activePieceInWall()) { if (this->activePieceInWall()) {
if (isFirstTimeHolding) { if (isFirstTimeHolding) {
this->activePiece = nullptr; this->activePiece = nullptr;
} }
std::swap(this->activePiece, this->heldPiece); std::swap(this->activePiece, this->heldPiece);
this->activePiecePosition = storedPosition;
return false; return false;
} }
} }
@@ -200,6 +218,8 @@ bool GameBoard::hold(Rotation initialRotation) {
this->nextQueue.erase(this->nextQueue.begin()); this->nextQueue.erase(this->nextQueue.begin());
} }
this->heldPiece->defaultRotation();
// this piece has done nothing yet // this piece has done nothing yet
this->isLastMoveKick = false; this->isLastMoveKick = false;
@@ -207,10 +227,7 @@ bool GameBoard::hold(Rotation initialRotation) {
} }
bool GameBoard::spawnNextPiece() { bool GameBoard::spawnNextPiece() {
// generate a new piece
this->nextQueue.push_back(this->generator.getNext()); this->nextQueue.push_back(this->generator.getNext());
// get next piece from queue
this->activePiece = std::make_shared<Piece>(this->nextQueue.front()); this->activePiece = std::make_shared<Piece>(this->nextQueue.front());
this->nextQueue.erase(this->nextQueue.begin()); this->nextQueue.erase(this->nextQueue.begin());
@@ -219,13 +236,22 @@ bool GameBoard::spawnNextPiece() {
// this piece has done nothing yet // this piece has done nothing yet
this->isLastMoveKick = false; this->isLastMoveKick = false;
return !this->activePieceInWall(); return this->activePieceInWall();
} }
bool GameBoard::touchesGround() { bool GameBoard::touchesGround() const {
return this->activePieceInWall(Position{0, -1}); 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() { LineClear GameBoard::lockPiece() {
bool isLockedInPlace = (this->activePieceInWall(Position{0, 1}) && this->activePieceInWall(Position{1, 0}) bool isLockedInPlace = (this->activePieceInWall(Position{0, 1}) && this->activePieceInWall(Position{1, 0})
&& this->activePieceInWall(Position{-1, 0}) && this->activePieceInWall(Position{0, -1})); && 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; return this->board;
} }
Piece GameBoard::getActivePiece() const { const std::shared_ptr<Piece>& GameBoard::getActivePiece() const {
return *this->activePiece; return this->activePiece;
} }
Position GameBoard::getActivePiecePosition() const { const Position& GameBoard::getActivePiecePosition() const {
return this->activePiecePosition; return this->activePiecePosition;
} }
Piece GameBoard::getHeldPiece() const { const std::shared_ptr<Piece>& GameBoard::getHeldPiece() const {
return *this->heldPiece; return this->heldPiece;
} }
std::vector<Piece> GameBoard::getNextPieces() const { const std::vector<Piece>& GameBoard::getNextPieces() const {
return this->nextQueue; return this->nextQueue;
} }
@@ -293,6 +319,8 @@ void GameBoard::goToSpawnPosition() {
// center the piece horizontally, biased towards left // center the piece horizontally, biased towards left
this->activePiecePosition.x = (this->board.getWidth() - this->activePiece->getLength()) / 2; this->activePiecePosition.x = (this->board.getWidth() - this->activePiece->getLength()) / 2;
this->activePiece->defaultRotation();
} }
std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard) { std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard) {

View File

@@ -60,7 +60,7 @@ class GameBoard {
bool moveDown(); 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 * @return If it suceeded
*/ */
bool rotate(Rotation rotation); bool rotate(Rotation rotation);
@@ -89,7 +89,13 @@ class GameBoard {
* Checks is the active piece as a wall directly below one of its position * Checks is the active piece as a wall directly below one of its position
* @return If it touches a ground * @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 * Locks the active piece into the board and clears lines if needed
@@ -103,34 +109,34 @@ class GameBoard {
void addGarbageRows(int number); 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<Piece>& 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<Piece>& getHeldPiece() const;
/** /**
* @return A copy of the next piece queue * @return The next piece queue, can be empty
*/ */
std::vector<Piece> getNextPieces() const; const std::vector<Piece>& getNextPieces() const;
private: private:
/** /**
* Checks if one of the active piece's positions touches a wall in the board * 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; bool activePieceInWall(const Position& shift = Position{0, 0}) const;

View File

@@ -118,7 +118,7 @@ void GameParameters::updateStats() {
if (this->level < 0) { if (this->level < 0) {
this->gravity = gravityPerLevel[0]; this->gravity = gravityPerLevel[0];
} }
else if (this->gravity > 20) { else if (this->level > 20) {
this->gravity = gravityPerLevel[20]; this->gravity = gravityPerLevel[20];
} }
else { else {

View File

@@ -77,12 +77,12 @@ class PiecesList {
int getNumberOfPieces(int size) const; int getNumberOfPieces(int size) const;
/** /**
* @return The indexes of all selected pieces * @return A copy of the indexes of all selected pieces
*/ */
std::vector<std::pair<int, int>> getSelectedPieces() const; std::vector<std::pair<int, int>> 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<int, int>& pieceIndex) const; Piece getPiece(const std::pair<int, int>& pieceIndex) const;

View File

@@ -12,6 +12,8 @@
Piece::Piece(const Polyomino& polyomino, Block blockType) : Piece::Piece(const Polyomino& polyomino, Block blockType) :
polyomino(polyomino), polyomino(polyomino),
blockType(blockType) { blockType(blockType) {
this->rotationState = NONE;
} }
void Piece::rotate(Rotation rotation) { void Piece::rotate(Rotation rotation) {
@@ -21,9 +23,22 @@ void Piece::rotate(Rotation rotation) {
this->polyomino.rotate180(); this->polyomino.rotate180();
if (rotation == COUNTERCLOCKWISE) if (rotation == COUNTERCLOCKWISE)
this->polyomino.rotateCCW(); this->polyomino.rotateCCW();
this->rotationState += rotation;
} }
std::set<Position> 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<Position>& Piece::getPositions() const {
return this->polyomino.getPositions(); return this->polyomino.getPositions();
} }

View File

@@ -13,8 +13,9 @@
*/ */
class Piece { class Piece {
private: private:
Polyomino polyomino; // a polyomino representing the piece, (0, 0) is downleft Polyomino polyomino; // a polyomino representing the piece, (0, 0) is downleft
Block blockType; // the block type of the piece Block blockType; // the block type of the piece
Rotation rotationState; // the current rotation of the piece
public: public:
/** /**
@@ -28,9 +29,14 @@ class Piece {
void rotate(Rotation rotation); void rotate(Rotation rotation);
/** /**
* @return A copy of the list of positions of the piece * Rotates the piece to its default rotation
*/ */
std::set<Position> getPositions() const; void defaultRotation();
/**
* @return The list of positions of the piece
*/
const std::set<Position>& getPositions() const;
/** /**
* @return The length of the piece * @return The length of the piece

View File

@@ -267,7 +267,7 @@ void Polyomino::tryToInsertPosition(std::set<Position>& emptyPositions, const Po
tryToInsertPosition(emptyPositions, Position{candidate.x - 1, candidate.y}); tryToInsertPosition(emptyPositions, Position{candidate.x - 1, candidate.y});
} }
std::set<Position> Polyomino::getPositions() const { const std::set<Position>& Polyomino::getPositions() const {
return this->positions; return this->positions;
} }
@@ -293,7 +293,7 @@ bool Polyomino::operator<(const Polyomino& other) const {
return false; return false;
} }
bool Polyomino::operator ==(const Polyomino& other) const { bool Polyomino::operator==(const Polyomino& other) const {
return this->positions == other.positions; return this->positions == other.positions;
} }

View File

@@ -78,9 +78,9 @@ class Polyomino {
public: public:
/** /**
* @return A copy of the positions of the polyomino * @return The positions of the polyomino
*/ */
std::set<Position> getPositions() const; const std::set<Position>& getPositions() const;
/** /**
* @return The length of the polyomino * @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 * 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 * @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 * Stream output operator, adds a 2D grid representing the polyomino

View File

@@ -62,6 +62,14 @@ inline bool operator==(const Position& left, const Position& right) {
return (left.x == right.x) && (left.y == right.y); 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 * Stream output operator, adds the coordinates of the position to the stream
* @return A reference to the output stream * @return A reference to the output stream

View File

@@ -17,7 +17,7 @@ enum Rotation {
* @return A rotation corresponding to doing both rotations * @return A rotation corresponding to doing both rotations
*/ */
inline Rotation operator+(const Rotation& left, const Rotation& right) { inline Rotation operator+(const Rotation& left, const Rotation& right) {
return Rotation((left + right) % 4); return Rotation(((int) left + (int) right) % 4);
} }
/** /**

323
src/TextUI/TextApp.cpp Normal file
View File

@@ -0,0 +1,323 @@
#include "TextApp.h"
#include "../Core/Menu.h"
#include <map>
#include <set>
#include <string>
#include <sstream>
#include <algorithm>
#include <cstdlib>
#include <exception>
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<std::string> actions;
for (std::string action; std::getline(answerStream, action, ' ');) {
actions.push_back(action);
}
std::set<Action> 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();
}

58
src/TextUI/TextApp.h Normal file
View File

@@ -0,0 +1,58 @@
#pragma once
#include "../Core/Menu.h"
#include <map>
#include <string>
/**
* Textual interface for the app
*/
class TextApp {
private:
Menu gameMenu; // the interface with the core of the app
std::map<std::string, Action> 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;
};

View File

@@ -1,6 +1,6 @@
#include "../Core/Menu.h"
#include "../Pieces/Generator.h" #include "../Pieces/Generator.h"
#include "../Pieces/PiecesFiles.h" #include "../Pieces/PiecesFiles.h"
#include "TextApp.h"
#include <chrono> #include <chrono>
@@ -18,13 +18,8 @@ void readStatsFromFilesForAllSizes(int amount);
int main(int argc, char** argv) { int main(int argc, char** argv) {
std::srand(std::time(NULL)); std::srand(std::time(NULL));
Menu menu; TextApp UI;
menu.getPiecesList().loadPieces(4); UI.run();
menu.getPiecesList().selectAllPieces(4);
Game game = menu.startGame(SPRINT);
game.start();
loadFromFilesForOneSize(13);
return 0; return 0;
} }