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:
- 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.

View File

@@ -1,10 +1,13 @@
#pragma once
#include <iostream>
/**
* 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;
}

View File

@@ -87,7 +87,7 @@ Block Board::getBlock(const Position& position) const {
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;
}

View File

@@ -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<std::vector<Block>> getBlocks() const;
const std::vector<std::vector<Block>>& getBlocks() const;
/**
* @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() {
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<Action>& 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<Action>& 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<Action>& 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<Action>& 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<Action>& 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<Action>& 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<Piece>& 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<Piece>& Game::getHeldPiece() const {
return this->board.getHeldPiece();
}
std::vector<Piece> Game::getNextPieces() const {
const std::vector<Piece>& Game::getNextPieces() const {
return this->board.getNextPieces();
}

View File

@@ -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<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) {
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<Position> 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<Piece>(stored);
return false;
if (rotation == NONE) {
this->isLastMoveKick = false;
}
return (rotation == NONE);
}
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
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,14 +191,15 @@ 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<Piece>(stored);
this->goToSpawnPosition();
// if the piece still can't spawn, abort holding
if (this->activePieceInWall()) {
@@ -190,6 +207,7 @@ bool GameBoard::hold(Rotation initialRotation) {
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<Piece>(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<Piece>& 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<Piece>& GameBoard::getHeldPiece() const {
return this->heldPiece;
}
std::vector<Piece> GameBoard::getNextPieces() const {
const std::vector<Piece>& 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) {

View File

@@ -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<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:
/**
* 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;

View File

@@ -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 {

View File

@@ -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<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;

View File

@@ -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<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();
}

View File

@@ -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<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

View File

@@ -267,7 +267,7 @@ void Polyomino::tryToInsertPosition(std::set<Position>& emptyPositions, const Po
tryToInsertPosition(emptyPositions, Position{candidate.x - 1, candidate.y});
}
std::set<Position> Polyomino::getPositions() const {
const std::set<Position>& 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;
}

View File

@@ -78,9 +78,9 @@ class Polyomino {
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
@@ -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

View File

@@ -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

View File

@@ -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);
}
/**

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/PiecesFiles.h"
#include "TextApp.h"
#include <chrono>
@@ -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;
}