Compare commits
7 Commits
f525c00662
...
v0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 021620acef | |||
| 74797e935a | |||
| 2fbe4a6052 | |||
| d029589c21 | |||
| 47d3d929db | |||
| 1033f3a64c | |||
| 857f90d646 |
Binary file not shown.
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 164 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 54 KiB |
@@ -1,17 +1,19 @@
|
||||
# Game logic
|
||||
|
||||
This section will detail how the player's action are interpreted into the game.
|
||||
We will only talk about pieces and not polyominos. In this project, pieces are an abstraction of a polyomino. Though if you want to know more the polyominos in this project, check [this other file](Pieces_representation.md).
|
||||
We will only talk about pieces and not polyominoes. In this project, pieces are an abstraction of a polyomino. Though if you want to know more the polyominoes in this project, check [this other file](Pieces_representation.md).
|
||||
|
||||
## Order of operations
|
||||
|
||||
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)
|
||||
@@ -24,9 +26,34 @@ Then the rest of actions will be tested in the list's order.
|
||||
Finally, gravity and lock delay are applied last.
|
||||
Moving and soft dropping can be held but hard dropping, holding and rotating needs to be released and pressed again to happen.
|
||||
|
||||
## ARR and DAS
|
||||
|
||||
The sidewise movement of the piece is defined by two parameters: DAS and ARR.
|
||||
**DAS** stands for Delayed Auto Shift and **ARR** for Auto Repeat Rate.
|
||||
Theses can be changed by the player.
|
||||
The parameters will be refered to as DAS and ARR, while the in-game values will be refered to as the piece's DAS ans ARR.
|
||||
|
||||
- Initially, both the piece DAS and piece ARR are at 0. When moving the piece in one direction,
|
||||
the piece will move one position and then wait for the player to hold that direction as long as the DAS value.
|
||||
_So for exemple if DAS is set to 20, on the first frame piece DAS will be set to 1 and the piece will move once.
|
||||
The next movement will be delayed by 20 frames, so the piece will move again 20 frames later, on the 21th frame, or when the piece DAS = (DAS +1)._
|
||||
- Then, if the player still holds the same direction, ARR takes on.
|
||||
The piece will move every ARR frames.
|
||||
_So in our exemple if ARR is set to 5, this means the piece will move every 5 frames after the 21th frame.
|
||||
So on the 21th frame piece DAS = 21 and piece ARR = 0.
|
||||
On the 26th frame, piece ARR = 5 so the piece moves and piece ARR is set back to 0._
|
||||
|
||||
Now DAS can be buffered, that is if a direction is held before the piece spawn, it will have an initial DAS value corresponding to the number of frames held.
|
||||
_So in our example where DAS = 20 and ARR = 5, if we held a direction during 30f then hold without releasing this direction, the piece will move instantly on the frame it spawns.
|
||||
Then, ARR will start counting on the next frame. So the piece will move on frames 1, 6, 11, etc._
|
||||
|
||||
When trying to hold two directions at the same time, only the direction held last will be applied.
|
||||
So for example if you were holding left and you suddendly start holding the two directions at the same time, the piece will go right.
|
||||
In the two directions are first pressed at the same frame, left will take priority.
|
||||
|
||||
## Leniency mechanics
|
||||
|
||||
In a general sense, we try to be kind to the players. Some of the following mechanics are just standards in a lot of stacker games, while other have been added because of this game using polyominos of high sizes and thus being way harder to play.
|
||||
In a general sense, we try to be kind to the players. Some of the following mechanics are just standards in a lot of stacker games, while other have been added because of this game using polyominoes of high sizes and thus being way harder to play.
|
||||
|
||||
When a piece touches the ground, there is a short time period before it automatically locks. This period is called _lock delay_. Lock delay is reset everytime the piece move. To not allow for infinite stalling, there is another longer period called _forced lock delay_ that does not reset when moving the piece.
|
||||
The player has a hold box in which they can temporarly store a piece. In this game, we allow the player to swap between the held piece and the active piece as much as they want. Once again to not allow for infinite stalling, forced lock delay does not reset when holding a piece.
|
||||
@@ -38,17 +65,20 @@ IRS and IHS will fail if they actually loose the player the game when it would h
|
||||
|
||||
A common mechanic of stacker games is kicking. This happen when rotating a piece makes it collide with a wall. The game will try to move the piece in a few predetermined spot close to the current position until one of them allow the piece to not be in a wall again. If no positions allow for that, the rotation is simply cancelled.
|
||||
|
||||
This concept works very well for games with up to tetrominos or pentominos, but when using polyominos of any size we can't choose a few predetermined spots for each piece.
|
||||
This concept works very well for games with up to tetrominos or pentominos, but when using polyominoes of any size we can't choose a few predetermined spots for each piece.
|
||||
|
||||
Since this game uses polyomino of high sizes which are very unplayable, we will try to be complaisant to the player and allow as much kicking spots as possible, while trying not to make him feel like the piece is going through walls. To solve this problem, this game introduce a new Rotation System called **AutoRS**, which does not have a predetermined list of spots to fit the piece but instead adapt to its shape. Its algorithm goes as follow:
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
# Pieces representation
|
||||
|
||||
## What are polyominos ?
|
||||
## What are polyominoes ?
|
||||
|
||||
In this game, pieces are represented as a polyomino and a block type.
|
||||
Polyominos are mathematical objects consisting of multiple edge-touching squares.
|
||||
Polyominoes are mathematical objects consisting of multiple edge-touching squares.
|
||||
There must be a path from every square to every other square, going from square to square only through the sides and not the corners.
|
||||
Polyominos can be classified in 3 ways:
|
||||
Polyominoes can be classified in 3 ways:
|
||||
|
||||
- Fixed polyominos : only translation is allowed
|
||||
- One-sided polyominos : only translation and rotation are allowed
|
||||
- Fixed polyominoes : only translation is allowed
|
||||
- One-sided polyominoes : only translation and rotation are allowed
|
||||
- Free polyomins : translation, rotation, and reflection are allowed
|
||||
|
||||
For more detailed informations about polyominos, check the [Wikipedia page](https://en.wikipedia.org/wiki/Polyomino).
|
||||
For more detailed informations about polyominoes, check the [Wikipedia page](https://en.wikipedia.org/wiki/Polyomino).
|
||||
|
||||
Most stacker game uses all one-sided polyominos of size 4 (called tetrominos), which results in 7 distinct polyominos.
|
||||
In this game too, one-sided polyominos will be used since we will only allow to move and rotate the pieces.
|
||||
Most stacker game uses all one-sided polyominoes of size 4 (called tetrominos), which results in 7 distinct polyominoes.
|
||||
In this game too, one-sided polyominoes will be used since we will only allow to move and rotate the pieces.
|
||||
|
||||
Internally, polyominos are represented as a set of positions on a 2D grid.
|
||||
This means that 2 polyominos of same shape but at different places will be interpreted as different polyominos.
|
||||
To solve this, when doing equality checks, we normalize the polyominos so that their left-most column is at x=0 and their bottom-most row at y=0.
|
||||
Internally, polyominoes are represented as a set of positions on a 2D grid.
|
||||
This means that 2 polyominoes of same shape but at different places will be interpreted as different polyominoes.
|
||||
To solve this, when doing equality checks, we normalize the polyominoes so that their left-most column is at x=0 and their bottom-most row at y=0.
|
||||
|
||||
Even if there is only 7 one-sided 4-minos, there is already more than 9,000 one-sided 10-minos.
|
||||
Since the aim of this game is to be playable with polyominos of any size, listing all polyominos manually isn't viable.
|
||||
Since the aim of this game is to be playable with polyominoes of any size, listing all polyominoes manually isn't viable.
|
||||
We will need a way to automatically:
|
||||
|
||||
1. Generate all one-sided polyominos of size n
|
||||
1. Generate all one-sided polyominoes of size n
|
||||
2. Set their spawn positions and rotation centers
|
||||
|
||||
Aditionally, for this game we will also a want a way to separate the polyominos into multiple categories, specifically to allow removing those with holes who are harder to play with.
|
||||
Aditionally, for this game we will also a want a way to separate the polyominoes into multiple categories, specifically to allow removing those with holes who are harder to play with.
|
||||
|
||||
## Ordering the polyominos
|
||||
## Ordering the polyominoes
|
||||
|
||||
For practical reasons, we want to be able to sort all polyominos of the same size.
|
||||
For practical reasons, we want to be able to sort all polyominoes of the same size.
|
||||
But to sort objects we need a way to compare them.
|
||||
|
||||
When a polyomino is created, an attribute named its length is computed. This is simply the max between its width and its height. Thus the polyomino can be inscribed in a box of size ``length``.
|
||||
We will now assume that our polyominos are always inscribed in a box of origin (0,0) and size ``length`` (which should always be the case under normal circumstances).
|
||||
We will now assume that our polyominoes are always inscribed in a box of origin (0,0) and size ``length`` (which should always be the case under normal circumstances).
|
||||
|
||||
We can now compare polyominos using this method:
|
||||
We can now compare polyominoes using this method:
|
||||
|
||||
- If one polyomino has an inferior length than another, it is deemed inferior
|
||||
- If two polyomino have the same length, we check all the positions of their box, from left to right, and repeating from up to bottom, for the first position where a polyomino has a square and the another doesn't, the present polyomino which has a square is deemed inferior
|
||||
|
||||
A nice side-effect is that, once the polyomino are ordered, it is very simple to attribute them a block type, we can simply iterate through the list while looping over the block list.
|
||||
|
||||
## 1. Generating polyominos
|
||||
## 1. Generating polyominoes
|
||||
|
||||
The method used to generate polyominos is similar to the [inductive method](https://en.wikipedia.org/wiki/Polyomino#Inductive_algorithms) described in the previously mentionned Wikipedia page.
|
||||
The method used to generate polyominoes is similar to the [inductive method](https://en.wikipedia.org/wiki/Polyomino#Inductive_algorithms) described in the previously mentionned Wikipedia page.
|
||||
|
||||
The algorithm is the following:
|
||||
|
||||
@@ -64,15 +64,15 @@ The algorithm is the following:
|
||||
1. We add this square to the polyomino
|
||||
2. We call the generator function (recursive function!)
|
||||
3. We remove this square from the polyomino
|
||||
3. Return the list of polyominos
|
||||
3. Return the list of polyominoes
|
||||
|
||||
The exact number of one-sided polyominos up to size 15 (and higher, but we only generated up to size 15) is known, and this method generated exactly theses numbers, without duplicates.
|
||||
The exact number of one-sided polyominoes up to size 15 (and higher, but we only generated up to size 15) is known, and this method generated exactly theses numbers, without duplicates.
|
||||
|
||||
By marking squares and adding only ones that have a higher number than the last one everytime, we generate each fixed polyomino exactly n times. By ignoring the squares below y=0, or at exactly x=0 and y<0, we generate each fixed polyomino exactly 1 time.
|
||||
An one-sided polyomino has 4 rotations, some of which can be the same if the polyomino has symmetries. 2 rotations that are the same corresponds to 1 fixed polyomino, we will refer to theses 2 rotation as 1 "unique" rotation.
|
||||
Because we generate every fixed polyomino exactly 1 time, that means each "unique" rotation of an one-sided polyomino is generated exactly one-time, which includes its lowest rotation. That's how we can get away with only checking the rotation of the polyomino and not comparing it to the rest of the generated polyominos.
|
||||
Because we generate every fixed polyomino exactly 1 time, that means each "unique" rotation of an one-sided polyomino is generated exactly one-time, which includes its lowest rotation. That's how we can get away with only checking the rotation of the polyomino and not comparing it to the rest of the generated polyominoes.
|
||||
|
||||
## 2. Setting the spawn position of polyominos
|
||||
## 2. Setting the spawn position of polyominoes
|
||||
|
||||
Since we assume the polyomino is always inscribed in a box at origin (0,0), we can use formulae close to matrix rotations to rotate the piece:
|
||||
|
||||
@@ -91,19 +91,19 @@ The current algorithm for doing so is the following:
|
||||
1. Check if the polyomino has more lines horizontally or vertically, this will determine either 2 or 4 sides which are wider than the others, and the others will be discarded
|
||||
2. For each potential side check the number of square on the first line, if one has more than every others, then this side is choosed, else we only keep the sides that tied and repeat for the next line
|
||||
3. If we still have at least 2 sides tied, we do the same again but check the flatness of the side to the left instead, this will make the pieces overall have more squares to their left at spawn
|
||||
4. If there is still no winner, we sort the remaining sides (by simulating having them selectionned and then sorting the resulting polyominos) and keep the lowest
|
||||
4. If there is still no winner, we sort the remaining sides (by simulating having them selectionned and then sorting the resulting polyominoes) and keep the lowest
|
||||
5. We rotate the piece so that the chosen side ends up at the bottom of the polyomino
|
||||
6. We center the polyomino inside its box, since we chose the widest side it will always fill the width of the square, but for the height it can be asymmetric, in that case we place it one line closer to the top than the bottom
|
||||
|
||||
_Note we could actually just skip straight to step 4 because it always give the same orientation, but we want the spawn rotations to follow somewhat of a logic. Step 1 to 3 actually already works for 99% of polyominos and they constrain step 4 too by preselecting certain sides._
|
||||
_Note we could actually just skip straight to step 4 because it always give the same orientation, but we want the spawn rotations to follow somewhat of a logic. Step 1 to 3 actually already works for 99% of polyominoes and they constrain step 4 too by preselecting certain sides._
|
||||
|
||||
## 3. Separating the polyominos into categories
|
||||
## 3. Separating the polyominoes into categories
|
||||
|
||||
For this game, we want the polyominos to be broken down into 3 categories:
|
||||
For this game, we want the polyominoes to be broken down into 3 categories:
|
||||
|
||||
- Convex: this is said of a polyomino where every row and column is formed of at most one continous line of squares
|
||||
- Holeless: the polyominos which are neither convex nor have a hole
|
||||
- Others: the polyominos who have a hole (thoses are by definition not convex)
|
||||
- Holeless: the polyominoes which are neither convex nor have a hole
|
||||
- Others: the polyominoes who have a hole (thoses are by definition not convex)
|
||||
|
||||
To check for convexity, we simply iterate trough each row and column, and check if all the squares are contiguous.
|
||||
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
## What is stored
|
||||
|
||||
If you don't know what a polyomino is, check [this other file](Pieces_representation.md#what-are-polyominos).
|
||||
If you don't know what a polyomino is, check [this other file](Pieces_representation.md#what-are-polyominoes).
|
||||
|
||||
Generating polyominos of size n is exponential in regard to n. Because of this, we will store the pieces beforehand and load them upon launching the game.
|
||||
Generating polyominoes of size n is exponential in regard to n. Because of this, we will store the pieces beforehand and load them upon launching the game.
|
||||
|
||||
We want the pieces to be always sorted in the same order, always attributed the same block type, and always set at the same spawn position, no matter how they were generated. We also want them to be separated in 3 categories : convex, not convex but without a hole, and with a hole. Theses problematics are already resolved internally, but will be calculated before storage as to not need extra calculcations upon load (except for the block type which is trivially computed).
|
||||
|
||||
## How is it stored
|
||||
|
||||
Pieces are stored in binary files. Each file simply contains every polyomino of one size, one after the other. Since each file contains all polyominos of the same size, we know how much stuff to read and don't need delimiters. We know we've read all pieces simply when we reach the end of file character.
|
||||
Pieces are stored in binary files. Each file simply contains every polyomino of one size, one after the other. Since each file contains all polyominoes of the same size, we know how much stuff to read and don't need delimiters. We know we've read all pieces simply when we reach the end of file character.
|
||||
|
||||
Each piece is stored as follows:
|
||||
- 1 byte for the characteristics of the piece: ``ABCCCCCC`` where A indicates if the piece is convex, B indicates if it has a hole, and C is the length of the piece
|
||||
- 1 byte for each position: ``XXXXYYYY`` where X is the x coordinate of the position and Y is the y coordinate of the position
|
||||
|
||||
The current implementation only allows to generate polyominos up to size 16, but can be upgraded by storing coordinates on 8 bits instead of 4.
|
||||
The current implementation only allows to generate polyominoes up to size 16, but can be upgraded by storing coordinates on 8 bits instead of 4.
|
||||
It has been currently choosen to use pieces only up to size 15 for this game.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -8,28 +8,26 @@
|
||||
#include <cstdlib>
|
||||
|
||||
|
||||
Bag::Bag(std::shared_ptr<PiecesList> piecesList) :
|
||||
Bag::Bag(const std::shared_ptr<PiecesList>& piecesList) :
|
||||
piecesList(piecesList) {
|
||||
|
||||
// initialize bags
|
||||
this->currentBag = this->piecesList->getSelectedPieces();
|
||||
this->nextBag.clear();
|
||||
|
||||
// prepare first piece
|
||||
this->prepareNext();
|
||||
}
|
||||
|
||||
void Bag::jumpToNextBag() {
|
||||
// if the bag is empty switch to the next bag
|
||||
if (this->currentBag.empty()) {
|
||||
if (this->currentBag.size() < this->nextBag.size()) {
|
||||
std::swap(this->currentBag, this->nextBag);
|
||||
}
|
||||
|
||||
// get the already used pieces back to the current bag
|
||||
for (const std::pair<int, int>& pieceIndex : this->nextBag) {
|
||||
this->currentBag.emplace_back(pieceIndex);
|
||||
this->currentBag.push_back(pieceIndex);
|
||||
}
|
||||
this->nextBag.clear();
|
||||
|
||||
this->prepareNext();
|
||||
}
|
||||
|
||||
Piece Bag::lookNext() {
|
||||
@@ -37,27 +35,21 @@ Piece Bag::lookNext() {
|
||||
}
|
||||
|
||||
Piece Bag::getNext() {
|
||||
// get the piece to return
|
||||
std::pair<int, int> nextIndex = this->next;
|
||||
|
||||
// prepare the piece even after the next
|
||||
this->prepareNext();
|
||||
|
||||
// return the next piece
|
||||
return this->piecesList->getPiece(nextIndex);
|
||||
}
|
||||
|
||||
void Bag::prepareNext() {
|
||||
// if the bag is empty switch to the next bag
|
||||
if (this->currentBag.empty()) {
|
||||
std::swap(this->currentBag, this->nextBag);
|
||||
}
|
||||
|
||||
// pick a random piece from the current bag
|
||||
int indexIndex = std::rand() % this->currentBag.size();
|
||||
this->next = this->currentBag.at(indexIndex);
|
||||
|
||||
// move the piece over to the next bag
|
||||
this->nextBag.push_back(this->next);
|
||||
this->currentBag.erase(this->currentBag.begin() + indexIndex);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class Bag {
|
||||
/**
|
||||
* Creates a new bag with the pieces currently selected in the piece list
|
||||
*/
|
||||
Bag(std::shared_ptr<PiecesList> piecesList);
|
||||
Bag(const std::shared_ptr<PiecesList>& piecesList);
|
||||
|
||||
/**
|
||||
* Ignores the remaining pieces in the current bag and startd fresh from a new bag
|
||||
|
||||
@@ -20,7 +20,6 @@ Board::Board(int width, int height) :
|
||||
}
|
||||
|
||||
void Board::changeBlock(const Position& position, Block block) {
|
||||
// if the block is out of bounds we discard it
|
||||
if (position.x < 0 || position.x >= this->width || position.y < 0) return;
|
||||
|
||||
// resize the grid if needed
|
||||
@@ -62,7 +61,7 @@ int Board::clearRows() {
|
||||
|
||||
if (lineIsFull) {
|
||||
this->grid.erase(this->grid.begin() + j);
|
||||
if(this->grid.size() < height) {
|
||||
if(this->grid.size() < this->height) {
|
||||
this->grid.push_back(this->emptyRow);
|
||||
}
|
||||
clearedLines++;
|
||||
@@ -74,7 +73,7 @@ int Board::clearRows() {
|
||||
|
||||
void Board::clearBoard() {
|
||||
this->grid.clear();
|
||||
for (int j = 0; j < height; j++) {
|
||||
for (int j = 0; j < this->height; j++) {
|
||||
this->grid.push_back(this->emptyRow);
|
||||
}
|
||||
}
|
||||
@@ -87,7 +86,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,57 +63,72 @@ void Game::nextFrame(const std::set<Action>& playerActions) {
|
||||
|
||||
if (this->leftARETime == 0) {
|
||||
if (AREJustEnded) {
|
||||
this->board.spawnNextPiece();
|
||||
this->lost = this->board.spawnNextPiece();
|
||||
this->resetPiece(true);
|
||||
}
|
||||
|
||||
/* 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)) {
|
||||
this->subVerticalPosition = 0;
|
||||
this->totalLockDelay = 0;
|
||||
this->heldARR = 0;
|
||||
}
|
||||
this->lost = (!this->board.hold(initialRotation));
|
||||
}
|
||||
else {
|
||||
if (initialRotation != NONE) {
|
||||
this->board.rotate(initialRotation);
|
||||
if ((initialRotation != NONE) || this->initialActions.contains(ROTATE_0)) {
|
||||
this->lost = (!this->board.rotate(initialRotation));
|
||||
}
|
||||
}
|
||||
|
||||
if (this->lost) {
|
||||
if (initialRotation == NONE) {
|
||||
this->lost = (!this->board.rotate(initialRotation));
|
||||
}
|
||||
}
|
||||
if (this->lost) {
|
||||
this->framesPassed++;
|
||||
return;
|
||||
}
|
||||
|
||||
/* HOLD */
|
||||
if (playerActions.contains(HOLD) && (!this->heldActions.contains(HOLD))) {
|
||||
if (this->board.hold()) {
|
||||
this->subVerticalPosition = 0;
|
||||
this->totalLockDelay = 0;
|
||||
this->heldARR = 0;
|
||||
this->resetPiece(false);
|
||||
}
|
||||
}
|
||||
|
||||
/* MOVE LEFT/RIGHT */
|
||||
if (playerActions.contains(MOVE_LEFT)) {
|
||||
this->movePiece(-1, (this->heldDAS >= 0));
|
||||
Position before = this->board.getActivePiecePosition();
|
||||
|
||||
if (this->heldDAS >= 0) {
|
||||
if (playerActions.contains(MOVE_LEFT) && (!heldActions.contains(MOVE_LEFT))) this->movePiece(-1);
|
||||
else if (playerActions.contains(MOVE_RIGHT)) this->movePiece(1);
|
||||
else this->heldDAS = 0;
|
||||
}
|
||||
if (playerActions.contains(MOVE_RIGHT)) {
|
||||
this->movePiece(1, (this->heldDAS <= 0));
|
||||
else if (this->heldDAS < 0) {
|
||||
if (playerActions.contains(MOVE_RIGHT) && (!heldActions.contains(MOVE_RIGHT))) this->movePiece(1);
|
||||
else if (playerActions.contains(MOVE_LEFT)) this->movePiece(-1);
|
||||
else this->heldDAS = 0;
|
||||
}
|
||||
else {
|
||||
this->heldDAS = 0;
|
||||
|
||||
if (before != this->board.getActivePiecePosition()) {
|
||||
this->totalLockDelay = 0;
|
||||
}
|
||||
|
||||
/* ROTATIONS */
|
||||
if (playerActions.contains(ROTATE_0) && (!this->heldActions.contains(ROTATE_0))) {
|
||||
this->rotatePiece(NONE);
|
||||
}
|
||||
if (playerActions.contains(ROTATE_CW) && (!this->heldActions.contains(ROTATE_CW))) {
|
||||
this->board.rotate(CLOCKWISE);
|
||||
this->rotatePiece(CLOCKWISE);
|
||||
}
|
||||
if (playerActions.contains(ROTATE_180) && (!this->heldActions.contains(ROTATE_180))) {
|
||||
this->board.rotate(DOUBLE);
|
||||
this->rotatePiece(DOUBLE);
|
||||
}
|
||||
if (playerActions.contains(ROTATE_CCW) && (!this->heldActions.contains(ROTATE_CCW))) {
|
||||
this->board.rotate(COUNTERCLOCKWISE);
|
||||
this->rotatePiece(COUNTERCLOCKWISE);
|
||||
}
|
||||
|
||||
/* SOFT DROP */
|
||||
@@ -148,15 +162,20 @@ void Game::nextFrame(const std::set<Action>& playerActions) {
|
||||
// no need to apply gravity and lock delay if the piece was hard dropped
|
||||
else {
|
||||
/* GRAVITY */
|
||||
// parameters.getGravity() gives the gravity for an assumed 20-line high board
|
||||
int appliedGravity = this->parameters.getGravity() * (this->board.getBoard().getBaseHeight() / 20.0);
|
||||
|
||||
this->subVerticalPosition += appliedGravity;
|
||||
while (this->subVerticalPosition >= SUBPX_PER_ROW) {
|
||||
this->subVerticalPosition -= SUBPX_PER_ROW;
|
||||
this->board.moveDown();
|
||||
if (this->parameters.getLevel() >= 20) {
|
||||
while (this->board.moveDown());
|
||||
}
|
||||
else {
|
||||
// parameters.getGravity() gives the gravity for an assumed 20-line high board
|
||||
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) {
|
||||
this->subVerticalPosition -= SUBPX_PER_ROW;
|
||||
this->board.moveDown();
|
||||
}
|
||||
}
|
||||
|
||||
/* LOCK DELAY */
|
||||
if (this->board.touchesGround()) {
|
||||
this->totalLockDelay++;
|
||||
@@ -171,56 +190,78 @@ 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
|
||||
|
||||
this->heldActions = playerActions;
|
||||
|
||||
if ((!this->started) || this->leftARETime > 0) {
|
||||
for (Action action : playerActions) {
|
||||
this->initialActions.insert(action);
|
||||
}
|
||||
}
|
||||
|
||||
this->heldActions = playerActions;
|
||||
|
||||
if (playerActions.contains(MOVE_LEFT)) {
|
||||
this->heldDAS = std::min(-1, this->heldDAS - 1);
|
||||
}
|
||||
if (playerActions.contains(MOVE_RIGHT)) {
|
||||
this->heldDAS = std::max(1, this->heldDAS + 1);
|
||||
}
|
||||
else {
|
||||
this->heldDAS = 0;
|
||||
if (this->heldDAS >= 0) {
|
||||
if (playerActions.contains(MOVE_LEFT)) this->heldDAS = -1;
|
||||
else if (playerActions.contains(MOVE_RIGHT)) this->heldDAS++;
|
||||
else this->heldDAS = 0;
|
||||
}
|
||||
else if (this->heldDAS < 0) {
|
||||
if (playerActions.contains(MOVE_RIGHT)) this->heldDAS = +1;
|
||||
else if (playerActions.contains(MOVE_LEFT)) this->heldDAS--;
|
||||
else this->heldDAS = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Game::movePiece(int movement, bool resetDirection) {
|
||||
if (resetDirection) {
|
||||
void Game::resetPiece(bool newPiece) {
|
||||
int appliedDAS = this->parameters.getDAS();
|
||||
|
||||
this->subVerticalPosition = 0;
|
||||
this->totalLockDelay = 0;
|
||||
if (newPiece) {
|
||||
this->totalForcedLockDelay = 0;
|
||||
}
|
||||
if (abs(this->heldDAS) > appliedDAS) {
|
||||
this->heldDAS = (this->heldDAS > 0) ? (+appliedDAS) : (-appliedDAS);
|
||||
}
|
||||
this->heldARR = 0;
|
||||
}
|
||||
|
||||
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;
|
||||
if (movement == -1) this->board.moveLeft();
|
||||
if (movement == 1) this->board.moveRight();
|
||||
}
|
||||
else {
|
||||
this->heldDAS += movement;
|
||||
}
|
||||
|
||||
if (abs(this->heldDAS) > this->parameters.getDAS()) {
|
||||
int appliedARR = this->parameters.getARR();
|
||||
|
||||
if (abs(this->heldDAS) > appliedDAS) {
|
||||
// ARR=0 -> instant movement
|
||||
if (appliedARR == 0) {
|
||||
if (movement == -1) while (this->board.moveLeft());
|
||||
if (movement == 1) while (this->board.moveRight());
|
||||
}
|
||||
|
||||
// ARR>1 -> move by specified amount
|
||||
else {
|
||||
this->heldARR++;
|
||||
if (this->heldARR == appliedARR) {
|
||||
if (abs(this->heldDAS) > appliedDAS + 1) {
|
||||
this->heldARR++;
|
||||
}
|
||||
if ((this->heldARR == appliedARR) || (abs(this->heldDAS) == (appliedDAS + 1))) {
|
||||
this->heldARR = 0;
|
||||
if (movement == -1) this->board.moveLeft();
|
||||
if (movement == 1) this->board.moveRight();
|
||||
@@ -229,11 +270,21 @@ void Game::movePiece(int movement, bool resetDirection) {
|
||||
}
|
||||
}
|
||||
|
||||
void Game::rotatePiece(Rotation rotation) {
|
||||
Position before = this->board.getActivePiecePosition();
|
||||
|
||||
if (this->board.rotate(rotation)) {
|
||||
this->totalLockDelay = 0;
|
||||
if (before != this->board.getActivePiecePosition()) {
|
||||
this->subVerticalPosition = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -245,18 +296,14 @@ void Game::lockPiece() {
|
||||
this->score += clearScore;
|
||||
}
|
||||
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();
|
||||
}
|
||||
if (!this->hasWon()) {
|
||||
this->leftARETime = this->parameters.getARE();
|
||||
if (this->leftARETime == 0) {
|
||||
this->lost = this->board.spawnNextPiece();
|
||||
this->resetPiece(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Game::hasWon() const {
|
||||
@@ -291,22 +338,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();
|
||||
}
|
||||
|
||||
@@ -61,9 +61,19 @@ class Game {
|
||||
|
||||
private:
|
||||
/**
|
||||
* Move the piece in the specified direction
|
||||
* Resets the piece's parameter
|
||||
*/
|
||||
void movePiece(int movement, bool resetDirection);
|
||||
void resetPiece(bool newPiece);
|
||||
|
||||
/**
|
||||
* Moves the piece in the specified direction (1 for right and -1 for left)
|
||||
*/
|
||||
void movePiece(int movement);
|
||||
|
||||
/**
|
||||
* Rotates the piece with the specified rotation
|
||||
*/
|
||||
void rotatePiece(Rotation rotation);
|
||||
|
||||
/**
|
||||
* Locks the piece, updates level and score and spawns the next piece if necessary
|
||||
@@ -112,27 +122,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;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <cstdlib>
|
||||
|
||||
|
||||
@@ -38,7 +39,7 @@ void GameBoard::initialize() {
|
||||
}
|
||||
|
||||
bool GameBoard::moveLeft() {
|
||||
if (this->isActivePieceInWall(Position{-1, 0})) {
|
||||
if (this->activePieceInWall(Position{-1, 0})) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
@@ -49,7 +50,7 @@ bool GameBoard::moveLeft() {
|
||||
}
|
||||
|
||||
bool GameBoard::moveRight() {
|
||||
if (this->isActivePieceInWall(Position{1, 0})) {
|
||||
if (this->activePieceInWall(Position{1, 0})) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
@@ -60,7 +61,7 @@ bool GameBoard::moveRight() {
|
||||
}
|
||||
|
||||
bool GameBoard::moveDown() {
|
||||
if (this->isActivePieceInWall(Position{0, -1})) {
|
||||
if (this->activePieceInWall(Position{0, -1})) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
@@ -72,15 +73,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->isActivePieceInWall()) {
|
||||
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 +99,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 +110,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 +121,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,30 +135,29 @@ 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->activePieceOverlapsOnePosition(safePositions, shift)) {
|
||||
if (!this->activePieceOverlaps(safePositions, shift)) {
|
||||
overlapsLeft = false;
|
||||
}
|
||||
else {
|
||||
if (!this->isActivePieceInWall(shift)) {
|
||||
if (!this->activePieceInWall(shift)) {
|
||||
this->activePiecePosition += shift;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// do the same on the left side
|
||||
if (overlapsLeft) {
|
||||
Position shift{-i, j};
|
||||
if (!this->activePieceOverlapsOnePosition(safePositions, shift)) {
|
||||
if (!this->activePieceOverlaps(safePositions, shift)) {
|
||||
overlapsLeft = false;
|
||||
}
|
||||
else {
|
||||
if (!this->isActivePieceInWall(shift)) {
|
||||
if (!this->activePieceInWall(shift)) {
|
||||
this->activePiecePosition += shift;
|
||||
return true;
|
||||
}
|
||||
@@ -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->isActivePieceInWall()) {
|
||||
if (this->activePieceInWall()) {
|
||||
this->activePiece = std::make_shared<Piece>(stored);
|
||||
|
||||
this->goToSpawnPosition();
|
||||
|
||||
// if the piece still can't spawn, abort holding
|
||||
if (this->isActivePieceInWall()) {
|
||||
if (this->activePieceInWall()) {
|
||||
if (isFirstTimeHolding) {
|
||||
this->activePiece = nullptr;
|
||||
}
|
||||
std::swap(this->activePiece, this->heldPiece);
|
||||
this->activePiecePosition = storedPosition;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -200,35 +218,40 @@ bool GameBoard::hold(Rotation initialRotation) {
|
||||
this->nextQueue.erase(this->nextQueue.begin());
|
||||
}
|
||||
|
||||
// this piece has done nothing yet
|
||||
this->heldPiece->defaultRotation();
|
||||
|
||||
this->isLastMoveKick = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
this->goToSpawnPosition();
|
||||
|
||||
// this piece has done nothing yet
|
||||
this->isLastMoveKick = false;
|
||||
|
||||
return !this->isActivePieceInWall();
|
||||
return this->activePieceInWall();
|
||||
}
|
||||
|
||||
bool GameBoard::touchesGround() {
|
||||
return this->isActivePieceInWall(Position{0, -1});
|
||||
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->isActivePieceInWall(Position{0, 1}) && this->isActivePieceInWall(Position{1, 0})
|
||||
&& this->isActivePieceInWall(Position{-1, 0}) && this->isActivePieceInWall(Position{0, -1}));
|
||||
bool isLockedInPlace = (this->activePieceInWall(Position{0, 1}) && this->activePieceInWall(Position{1, 0})
|
||||
&& this->activePieceInWall(Position{-1, 0}) && this->activePieceInWall(Position{0, -1}));
|
||||
|
||||
for (Position position : this->activePiece->getPositions()) {
|
||||
this->board.changeBlock(position + this->activePiecePosition, this->activePiece->getBlockType());
|
||||
@@ -248,34 +271,34 @@ 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;
|
||||
}
|
||||
|
||||
bool GameBoard::isActivePieceInWall(const Position& shift) const {
|
||||
bool GameBoard::activePieceInWall(const Position& shift) const {
|
||||
for (Position position : this->activePiece->getPositions()) {
|
||||
if (this->board.getBlock(position + this->activePiecePosition + shift) != NOTHING) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GameBoard::activePieceOverlapsOnePosition(const std::set<Position>& safePositions, const Position& shift) const {
|
||||
bool GameBoard::activePieceOverlaps(const std::set<Position>& safePositions, const Position& shift) const {
|
||||
for (Position position : this->activePiece->getPositions()) {
|
||||
if (safePositions.contains(position + this->activePiecePosition + shift)) return true;
|
||||
}
|
||||
@@ -293,6 +316,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) {
|
||||
|
||||
@@ -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,42 +109,42 @@ 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 isActivePieceInWall(const Position& shift = Position{0, 0}) const;
|
||||
bool activePieceInWall(const Position& shift = Position{0, 0}) const;
|
||||
|
||||
/**
|
||||
* Check if one of the active piece's positions shifted by a specified position would overlap with a set of positions
|
||||
* @return If the shifted active piece overlaps with one of the position
|
||||
*/
|
||||
bool activePieceOverlapsOnePosition(const std::set<Position>& safePositions, const Position& shift = Position{0, 0}) const;
|
||||
bool activePieceOverlaps(const std::set<Position>& safePositions, const Position& shift = Position{0, 0}) const;
|
||||
|
||||
/**
|
||||
* Sets the active piece to its spawn position
|
||||
|
||||
@@ -12,7 +12,6 @@ GameParameters::GameParameters(Gamemode gamemode, const Player& controls) :
|
||||
}
|
||||
|
||||
void GameParameters::reset() {
|
||||
// initialize lines and level
|
||||
this->clearedLines = 0;
|
||||
switch (this->gamemode) {
|
||||
// lowest gravity
|
||||
@@ -26,17 +25,14 @@ void GameParameters::reset() {
|
||||
default : this->level = 1;
|
||||
}
|
||||
|
||||
// initialize stats
|
||||
this->updateStats();
|
||||
}
|
||||
|
||||
void GameParameters::clearLines(int lineNumber) {
|
||||
// update lines and level
|
||||
switch (this->gamemode) {
|
||||
// modes where level increases
|
||||
case MARATHON :
|
||||
case MASTER : {
|
||||
// update cleared lines
|
||||
int previousLines = this->clearedLines;
|
||||
this->clearedLines += lineNumber;
|
||||
|
||||
@@ -94,7 +90,7 @@ void GameParameters::updateStats() {
|
||||
/* GRAVITY */
|
||||
// get gravity for an assumed 20-rows board
|
||||
static const int gravityPerLevel[] = {
|
||||
0, // LVL0
|
||||
0, // lvl0 = no gravity
|
||||
1, // 60f/line, 20s total
|
||||
2, // 30f/line, 10s total
|
||||
3, // 20f/line, 6.66s total
|
||||
@@ -113,12 +109,13 @@ void GameParameters::updateStats() {
|
||||
40, // 1.5f/line, 30f total
|
||||
1 * 60, // 1line/f, 20f total
|
||||
2 * 60, // 2line/f, 10f total
|
||||
4 * 60 // 4line/f, 5f total
|
||||
4 * 60, // 4line/f, 5f total
|
||||
20 * 60 // lvl20 = instant gravity
|
||||
};
|
||||
if (this->level < 0) {
|
||||
this->gravity = gravityPerLevel[0];
|
||||
}
|
||||
else if (this->gravity > 20) {
|
||||
else if (this->level > 20) {
|
||||
this->gravity = gravityPerLevel[20];
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -4,15 +4,18 @@
|
||||
#include "Player.h"
|
||||
#include "Game.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
|
||||
Menu::Menu() {
|
||||
// default board size
|
||||
this->boardHeight = 20;
|
||||
this->boardWidth = 10;
|
||||
this->piecesList = std::make_shared<PiecesList>(PiecesList());
|
||||
|
||||
this->boardWidth = DEFAULT_BOARD_WIDTH;
|
||||
this->boardHeight = DEFAULT_BOARD_HEIGHT;
|
||||
}
|
||||
|
||||
Game Menu::startGame(Gamemode gamemode) const {
|
||||
return Game(gamemode, this->playerControls, this->boardWidth, this->boardHeight, std::make_shared<PiecesList>(this->piecesList));
|
||||
return Game(gamemode, this->playerControls, this->boardWidth, this->boardHeight, this->piecesList);
|
||||
}
|
||||
|
||||
bool Menu::setBoardWidth(int width) {
|
||||
@@ -42,5 +45,5 @@ Player& Menu::getPlayerControls() {
|
||||
}
|
||||
|
||||
PiecesList& Menu::getPiecesList() {
|
||||
return this->piecesList;
|
||||
return *this->piecesList;
|
||||
}
|
||||
|
||||
@@ -4,18 +4,20 @@
|
||||
#include "Player.h"
|
||||
#include "Game.h"
|
||||
|
||||
static const int FRAMES_PER_SECOND = 60; // the number of frames per second, all the values in the game were choosen with this number in mind
|
||||
static const int FRAMES_PER_SECOND = 60; // the number of frames per second, all the values in the app were choosen with this number in mind
|
||||
static const int DEFAULT_BOARD_WIDTH = 10; // the default width of the board when starting the menu
|
||||
static const int DEFAULT_BOARD_HEIGHT = 20; // the default height of the board when starting the menu
|
||||
|
||||
|
||||
/**
|
||||
* The interface between the UI and the core of the game
|
||||
* The interface between an UI and the core of the app
|
||||
*/
|
||||
class Menu {
|
||||
private:
|
||||
PiecesList piecesList; // the list of pieces in the game
|
||||
Player playerControls; // the controls of the player
|
||||
int boardHeight; // the height of the board for the next game
|
||||
int boardWidth; // the width of the board for the next game
|
||||
std::shared_ptr<PiecesList> piecesList; // the list of pieces used by the app
|
||||
Player playerControls; // the controls of the player
|
||||
int boardWidth; // the width of the board for the next game
|
||||
int boardHeight; // the height of the board for the next game
|
||||
|
||||
public:
|
||||
/**
|
||||
|
||||
@@ -101,7 +101,7 @@ std::vector<std::pair<int, int>> PiecesList::getSelectedPieces() const {
|
||||
return this->selectedPieces;
|
||||
}
|
||||
|
||||
Piece PiecesList::getPiece(std::pair<int, int> pieceIndex) const {
|
||||
Piece PiecesList::getPiece(const std::pair<int, int>& pieceIndex) const {
|
||||
return this->loadedPieces.at(pieceIndex.first).at(pieceIndex.second);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,14 +77,14 @@ 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(std::pair<int, int> pieceIndex) const;
|
||||
Piece getPiece(const std::pair<int, int>& pieceIndex) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
#include "Player.h"
|
||||
|
||||
static const int DAS_MIN_VALUE = 0; // 0ms
|
||||
static const int DAS_MAX_VALUE = 30; // 500ms
|
||||
static const int ARR_MIN_VALUE = 0; // 0ms
|
||||
static const int ARR_MAX_VALUE = 30; // 500ms
|
||||
static const int SDR_MIN_VALUE = 0; // 0ms
|
||||
static const int SDR_MAX_VALUE = 6; // 100ms
|
||||
|
||||
|
||||
Player::Player() {
|
||||
// default settings
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
static const int DAS_MIN_VALUE = 0; // the minimal selectable DAS value, equals to 0ms
|
||||
static const int DAS_MAX_VALUE = 30; // the maximal selectable DAS value, equals to 500ms
|
||||
static const int ARR_MIN_VALUE = 0; // the minimal selectable ARR value, equals to 0ms
|
||||
static const int ARR_MAX_VALUE = 30; // the maximal selectable ARR value, equals to 500ms
|
||||
static const int SDR_MIN_VALUE = 0; // the minimal selectable SDR value, equals to 0ms
|
||||
static const int SDR_MAX_VALUE = 6; // the maximal selectable SDR value, equals to 100ms
|
||||
|
||||
|
||||
/**
|
||||
* The controls of a player
|
||||
|
||||
@@ -11,19 +11,19 @@
|
||||
Generator::Generator() {
|
||||
}
|
||||
|
||||
std::vector<Polyomino> Generator::generatePolyominos(int polyominoSize) {
|
||||
this->validPolyominos.clear();
|
||||
std::vector<Polyomino> Generator::generatePolyominoes(int polyominoSize) {
|
||||
this->validPolyominoes.clear();
|
||||
this->currentTestedShape.clear();
|
||||
|
||||
// a polyomino has at least 1 square
|
||||
if (polyominoSize < 1) return this->validPolyominos;
|
||||
if (polyominoSize < 1) return this->validPolyominoes;
|
||||
|
||||
// start generating from the monomino
|
||||
// always place the first cell at (0, 0)
|
||||
this->currentTestedShape.insert(Position{0, 0});
|
||||
|
||||
std::map<Position, int> candidatePositions;
|
||||
this->generate(polyominoSize, 0, 1, candidatePositions);
|
||||
return this->validPolyominos;
|
||||
return this->validPolyominoes;
|
||||
}
|
||||
|
||||
void Generator::generate(int polyominoSize, int lastAddedPositionNumber, int nextAvaibleNumber, std::map<Position, int> candidatePositions) {
|
||||
@@ -31,7 +31,7 @@ void Generator::generate(int polyominoSize, int lastAddedPositionNumber, int nex
|
||||
if (polyominoSize == this->currentTestedShape.size()) {
|
||||
Polyomino candidate(this->currentTestedShape);
|
||||
|
||||
// we sort the rotations of the polyominos
|
||||
// we sort the rotations of the polyominoes
|
||||
std::vector<Polyomino> candidateRotations;
|
||||
candidateRotations.reserve(4);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -43,7 +43,7 @@ void Generator::generate(int polyominoSize, int lastAddedPositionNumber, int nex
|
||||
|
||||
// we keep the polyomino only if it was generated in its lowest rotation
|
||||
if (candidate == candidateRotations.at(0)) {
|
||||
this->validPolyominos.push_back(candidate);
|
||||
this->validPolyominoes.push_back(candidate);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -57,7 +57,7 @@ void Generator::generate(int polyominoSize, int lastAddedPositionNumber, int nex
|
||||
this->tryToAddCandidatePosition(Position{position.x - 1, position.y}, nextAvaibleNumber, candidatePositions);
|
||||
}
|
||||
|
||||
// generate polyominos for all positions with a higher number than the last one
|
||||
// try adding a square only to positions with a higher number than the last one
|
||||
for (auto [key, val] : candidatePositions) {
|
||||
if (val > lastAddedPositionNumber) {
|
||||
this->currentTestedShape.insert(key);
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
|
||||
/**
|
||||
* A generator of one-sided polyominos of any size
|
||||
* A generator of one-sided polyominoes of any size
|
||||
*/
|
||||
class Generator {
|
||||
private:
|
||||
std::vector<Polyomino> validPolyominos; // the list of already generated polyominos
|
||||
std::vector<Polyomino> validPolyominoes; // the list of already generated polyominoes
|
||||
std::set<Position> currentTestedShape; // the polyomino being created
|
||||
|
||||
public:
|
||||
@@ -22,14 +22,14 @@ class Generator {
|
||||
Generator();
|
||||
|
||||
/**
|
||||
* Generates the list of all one-sided polyominos of the specified size
|
||||
* @return The list of polyominos
|
||||
* Generates the list of all one-sided polyominoes of the specified size
|
||||
* @return The list of polyominoes
|
||||
*/
|
||||
std::vector<Polyomino> generatePolyominos(int polyominoSize);
|
||||
std::vector<Polyomino> generatePolyominoes(int polyominoSize);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Generates all one-sided polyominos of the specified size using the current tested shape
|
||||
* Generates all one-sided polyominoes of the specified size using the current tested shape
|
||||
*/
|
||||
void generate(int polyominoSize, int lastAddedPositionNumber, int nextAvaibleNumber, std::map<Position, int> candidatePositions);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,9 +25,9 @@ bool PiecesFiles::savePieces(int polyominoSize) const {
|
||||
}
|
||||
|
||||
Generator generator;
|
||||
std::vector<Polyomino> nMinos = generator.generatePolyominos(polyominoSize);
|
||||
std::vector<Polyomino> nMinos = generator.generatePolyominoes(polyominoSize);
|
||||
|
||||
// sorting the polyominos is done after setting spawn position to ensure the order is always the same
|
||||
// sorting the polyominoes is done after setting spawn position to ensure the order is always the same
|
||||
for (Polyomino& nMino : nMinos) {
|
||||
nMino.goToSpawnPosition();
|
||||
}
|
||||
@@ -64,6 +64,7 @@ bool PiecesFiles::loadPieces(int polyominoSize, std::vector<Piece>& pieces, std:
|
||||
holelessPieces.clear();
|
||||
otherPieces.clear();
|
||||
|
||||
// we shift the first color of each size so that the small polyominoes (size 1-2-3) don't all have the same color
|
||||
Block pieceBlock = firstPieceBlockType();
|
||||
for (int i = 0; i < polyominoSize; i++) {
|
||||
nextPieceBlockType(pieceBlock);
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
#include <iostream>
|
||||
#include <climits>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
|
||||
Polyomino::Polyomino(const std::set<Position>& positions) {
|
||||
// find min/max
|
||||
int minX = INT_MAX;
|
||||
int maxX = INT_MIN;
|
||||
int minY = INT_MAX;
|
||||
@@ -22,15 +22,14 @@ Polyomino::Polyomino(const std::set<Position>& positions) {
|
||||
if (position.y > maxY) maxY = position.y;
|
||||
}
|
||||
|
||||
// normalize
|
||||
this->length = std::max(maxX - minX + 1, maxY - minY + 1);
|
||||
|
||||
// we normalize here instead of calling this->normalize() to reduce the number of calculations for the generation algorithm
|
||||
std::set<Position> newPositions;
|
||||
for (Position position : positions) {
|
||||
newPositions.insert(Position{position.x - minX, position.y - minY});
|
||||
}
|
||||
this->positions = std::move(newPositions);
|
||||
|
||||
// set polyomino length
|
||||
this->length = std::max(maxX - minX + 1, maxY - minY + 1);
|
||||
}
|
||||
|
||||
Polyomino::Polyomino(const std::set<Position>& positions, int length) :
|
||||
@@ -39,7 +38,6 @@ Polyomino::Polyomino(const std::set<Position>& positions, int length) :
|
||||
}
|
||||
|
||||
void Polyomino::normalize() {
|
||||
// find min values
|
||||
int minX = INT_MAX;
|
||||
int minY = INT_MAX;
|
||||
for (Position position : this->positions) {
|
||||
@@ -47,7 +45,6 @@ void Polyomino::normalize() {
|
||||
if (position.y < minY) minY = position.y;
|
||||
}
|
||||
|
||||
// translate the polyomino to the lowest unsigned values
|
||||
std::set<Position> newPositions;
|
||||
for (Position position : this->positions) {
|
||||
newPositions.insert(Position{position.x - minX, position.y - minY});
|
||||
@@ -82,6 +79,7 @@ void Polyomino::rotateCCW() {
|
||||
void Polyomino::goToSpawnPosition() {
|
||||
// initialize array
|
||||
std::vector<std::vector<int>> linesCompleteness;
|
||||
linesCompleteness.reserve(4);
|
||||
std::vector<int> empty;
|
||||
for (int j = 0; j < this->length; j++) {
|
||||
empty.push_back(0);
|
||||
@@ -98,15 +96,15 @@ void Polyomino::goToSpawnPosition() {
|
||||
linesCompleteness.at(3).at(position.x) += 1; // 3 = left to right = CCW
|
||||
}
|
||||
|
||||
// count empty lines
|
||||
// count empty lines and push the non-empty lines to the start of each vector
|
||||
int horizontalEmptyLines = 0;
|
||||
int verticalEmptyLines = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = this->length - 1; j >= 0; j--) {
|
||||
if (linesCompleteness.at(i).at(j) == 0) {
|
||||
// push the non-empty lines to the start of each vector
|
||||
linesCompleteness.at(i).erase(linesCompleteness.at(i).begin() + j);
|
||||
linesCompleteness.at(i).push_back(0);
|
||||
|
||||
if (i == 0) horizontalEmptyLines++;
|
||||
if (i == 1) verticalEmptyLines++;
|
||||
}
|
||||
@@ -124,11 +122,11 @@ void Polyomino::goToSpawnPosition() {
|
||||
currentFlattestSides[3] = false;
|
||||
}
|
||||
|
||||
// checks for the flattest sides
|
||||
// checks for the flattest side
|
||||
int sideToBeOn = -1;
|
||||
this->checkForFlattestSide(linesCompleteness, currentFlattestSides, sideToBeOn, false);
|
||||
|
||||
// if all sides are as flat, checks for the most left-biased side
|
||||
// if ther's no winner, checks for the side which has the flattest side to its left
|
||||
if (sideToBeOn == -1) {
|
||||
this->checkForFlattestSide(linesCompleteness, currentFlattestSides, sideToBeOn, true);
|
||||
}
|
||||
@@ -148,12 +146,16 @@ void Polyomino::goToSpawnPosition() {
|
||||
sideToBeOn = candidateSides.at(std::distance(candidateRotations.begin(), std::min_element(candidateRotations.begin(), candidateRotations.end())));
|
||||
}
|
||||
|
||||
// do the correct rotation
|
||||
if (sideToBeOn == 1) this->rotateCW();
|
||||
if (sideToBeOn == 2) this->rotate180();
|
||||
if (sideToBeOn == 3) this->rotateCCW();
|
||||
switch (sideToBeOn) {
|
||||
case 1 : {this->rotateCW(); break;}
|
||||
case 2 : {this->rotate180(); break;}
|
||||
case 3 : {this->rotateCCW(); break;}
|
||||
default: break;
|
||||
}
|
||||
if (sideToBeOn % 2 == 1) {
|
||||
std::swap(verticalEmptyLines, horizontalEmptyLines);
|
||||
}
|
||||
|
||||
// find min
|
||||
int minX = INT_MAX;
|
||||
int minY = INT_MAX;
|
||||
for (Position position : this->positions) {
|
||||
@@ -161,10 +163,7 @@ void Polyomino::goToSpawnPosition() {
|
||||
if (position.y < minY) minY = position.y;
|
||||
}
|
||||
|
||||
// center the piece with an up bias if it is assymetric
|
||||
if (sideToBeOn % 2 == 1) {
|
||||
std::swap(verticalEmptyLines, horizontalEmptyLines);
|
||||
}
|
||||
// center the piece with an up bias
|
||||
std::set<Position> newPositions;
|
||||
for (Position position : positions) {
|
||||
newPositions.insert(Position{(position.x - minX) + (verticalEmptyLines / 2), (position.y - minY) + ((horizontalEmptyLines + 1) / 2)});
|
||||
@@ -174,19 +173,17 @@ void Polyomino::goToSpawnPosition() {
|
||||
|
||||
void Polyomino::checkForFlattestSide(const std::vector<std::vector<int>>& linesCompleteness, bool currentFlattestSides[4], int& sideToBeOn, bool checkLeftSide) const {
|
||||
for (int j = 0; j < this->length; j++) {
|
||||
// we check which sides are the flattest on this line
|
||||
int max = 0;
|
||||
std::set<int> maxOwners;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
// only the candidate sides are compared
|
||||
if (!currentFlattestSides[i]) continue;
|
||||
|
||||
// if we need to check the flatness of the side to the left
|
||||
int sideToCheck = i;
|
||||
if (checkLeftSide) {
|
||||
sideToCheck = (i + 3) % 4;
|
||||
}
|
||||
|
||||
// we check which sides are the flattest
|
||||
if (linesCompleteness.at(sideToCheck).at(j) > max) {
|
||||
max = linesCompleteness.at(sideToCheck).at(j);
|
||||
maxOwners.clear();
|
||||
@@ -203,7 +200,7 @@ void Polyomino::checkForFlattestSide(const std::vector<std::vector<int>>& linesC
|
||||
return;
|
||||
}
|
||||
|
||||
// else we only keep the flattest from this round and ignore the others
|
||||
// else we only keep the flattest on this line and ignore the others
|
||||
else {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
currentFlattestSides[i] = currentFlattestSides[i] && maxOwners.contains(i);
|
||||
@@ -213,14 +210,12 @@ void Polyomino::checkForFlattestSide(const std::vector<std::vector<int>>& linesC
|
||||
}
|
||||
|
||||
bool Polyomino::isConvex() const {
|
||||
// for each line and column we check if every squares are adjacent to each others
|
||||
for (int j = 0; j < this->length; j++) {
|
||||
bool startedLine = false;
|
||||
bool completedLine = false;
|
||||
bool startedColumn = false;
|
||||
bool completedColumn = false;
|
||||
for (int i = 0; i < this->length; i++) {
|
||||
// line check
|
||||
if (this->positions.contains(Position{i, j})) {
|
||||
if (completedLine) return false;
|
||||
else startedLine = true;
|
||||
@@ -228,7 +223,7 @@ bool Polyomino::isConvex() const {
|
||||
else {
|
||||
if (startedLine) completedLine = true;
|
||||
}
|
||||
// column check
|
||||
|
||||
if (this->positions.contains(Position{j, i})) {
|
||||
if (completedColumn) return false;
|
||||
else startedColumn = true;
|
||||
@@ -267,7 +262,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;
|
||||
}
|
||||
|
||||
@@ -282,7 +277,6 @@ int Polyomino::getPolyominoSize() const {
|
||||
bool Polyomino::operator<(const Polyomino& other) const {
|
||||
if (this->length != other.length) return this->length < other.length;
|
||||
|
||||
// we check for all positions from left to right and top to bottom, until one has a square that the other doesn't
|
||||
for (int y = this->length - 1; y >= 0; y--) {
|
||||
for (int x = 0; x < this->length; x++) {
|
||||
bool hasThisPosition = this->positions.contains(Position{x, y});
|
||||
@@ -293,7 +287,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -100,10 +100,10 @@ class Polyomino {
|
||||
bool operator<(const Polyomino& other) const;
|
||||
|
||||
/**
|
||||
* 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 polyominoes are equal if their positions are the same, that means two polyominoes 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
346
src/TextUI/TextApp.cpp
Normal file
346
src/TextUI/TextApp.cpp
Normal file
@@ -0,0 +1,346 @@
|
||||
#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; // the number of frames that will pass everytime the player gives an input
|
||||
static const int MAXIMUM_PIECE_SIZE = 10; // the maximum size of pieces that will be loaded and utilizable
|
||||
static const int DEFAULT_PIECE_SIZE = 4; // the default size of pieces that will be selected when first running the app
|
||||
static const int MAXIMUM_BOARD_WIDTH = 30; // the maximum selectable width of the board
|
||||
static const int MAXIMUM_BOARD_HEIGHT = 40; // the maximum selectable height of the board
|
||||
static const Gamemode DEFAULT_GAMEMODE = SPRINT; // the gamemode that will be used when starting a new game
|
||||
|
||||
|
||||
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);
|
||||
this->gameMenu.getPlayerControls().setARR(FRAMES_PER_INPUT);
|
||||
this->gameMenu.getPlayerControls().setSDR(0);
|
||||
}
|
||||
|
||||
void TextApp::run() {
|
||||
bool quit = false;
|
||||
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::string answer;
|
||||
std::getline(std::cin, answer);
|
||||
int selectedAnswer = 0;
|
||||
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 " << MAXIMUM_BOARD_HEIGHT << ")." << std::endl;
|
||||
std::cout << "Choice: ";
|
||||
std::getline(std::cin, answer);
|
||||
try {
|
||||
int selectedSize = std::stoi(answer);
|
||||
if (selectedSize >= 1 && selectedSize <= MAXIMUM_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::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;
|
||||
std::set<Action> lastFrameActions;
|
||||
std::set<Action> metaActions;
|
||||
for (std::string action : actions) {
|
||||
try {
|
||||
Action playerAction = this->keybinds.at(action);
|
||||
if (playerAction == PAUSE || playerAction == RETRY || playerAction == quit) {
|
||||
metaActions.insert(playerAction);
|
||||
}
|
||||
else {
|
||||
if (playerAction == SOFT_DROP || playerAction == MOVE_LEFT || playerAction == MOVE_RIGHT) {
|
||||
playerActions.insert(playerAction);
|
||||
lastFrameActions.insert(playerAction);
|
||||
}
|
||||
else if (playerAction == HOLD || playerAction == HARD_DROP) {
|
||||
lastFrameActions.insert(playerAction);
|
||||
}
|
||||
else {
|
||||
playerActions.insert(playerAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception ignored) {}
|
||||
}
|
||||
|
||||
if (metaActions.contains(PAUSE)) {
|
||||
paused = (!paused);
|
||||
}
|
||||
|
||||
if (!paused) {
|
||||
if (metaActions.contains(QUIT)) {
|
||||
quit = true;
|
||||
}
|
||||
else if (metaActions.contains(RETRY)) {
|
||||
game.reset();
|
||||
game.start();
|
||||
}
|
||||
else {
|
||||
for (int i = 0; i < (FRAMES_PER_INPUT - 1); i++) {
|
||||
game.nextFrame(playerActions);
|
||||
}
|
||||
game.nextFrame(lastFrameActions);
|
||||
}
|
||||
}
|
||||
|
||||
if (!quit) {
|
||||
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++) {
|
||||
/* BOARD PRINTING */
|
||||
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()) {
|
||||
/* SIDEBAR PRINTING */
|
||||
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
58
src/TextUI/TextApp.h
Normal 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;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "Menu.h"
|
||||
#include "../Pieces/Generator.h"
|
||||
#include "../Pieces/PiecesFiles.h"
|
||||
#include "TextApp.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
@@ -18,11 +18,11 @@ 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();
|
||||
// dev: generate files if it hasn't been done before, UI will NOT generate the files
|
||||
//generateFilesForAllSizes(10);
|
||||
|
||||
TextApp UI;
|
||||
UI.run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -37,11 +37,11 @@ void testGeneratorForAllSizes(int amount) {
|
||||
|
||||
for (int i = 1; i <= amount; i++) {
|
||||
auto t1 = high_resolution_clock::now();
|
||||
std::vector<Polyomino> n_minos = generator.generatePolyominos(i);
|
||||
std::vector<Polyomino> n_minos = generator.generatePolyominoes(i);
|
||||
auto t2 = high_resolution_clock::now();
|
||||
|
||||
duration<double, std::milli> ms_double = t2 - t1;
|
||||
std::cout << "generated " << n_minos.size() << " polyominos of size " << i << " in " << ms_double.count() << "ms" << std::endl;
|
||||
std::cout << "generated " << n_minos.size() << " polyominoes of size " << i << " in " << ms_double.count() << "ms" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ void testGeneratorForOneSize(int size) {
|
||||
std::cout << "Generating " << size << "-minos" << std::endl;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
auto t1 = high_resolution_clock::now();
|
||||
std::vector<Polyomino> n_minos = generator.generatePolyominos(size);
|
||||
std::vector<Polyomino> n_minos = generator.generatePolyominoes(size);
|
||||
auto t2 = high_resolution_clock::now();
|
||||
|
||||
duration<double, std::milli> ms_double = t2 - t1;
|
||||
@@ -65,7 +65,7 @@ void testGeneratorForOneSize(int size) {
|
||||
|
||||
void testGeneratorByprintingAllNminos(int n) {
|
||||
Generator generator;
|
||||
std::vector<Polyomino> n_minos = generator.generatePolyominos(n);
|
||||
std::vector<Polyomino> n_minos = generator.generatePolyominoes(n);
|
||||
|
||||
for (Polyomino& n_mino : n_minos) {
|
||||
n_mino.goToSpawnPosition();
|
||||
Reference in New Issue
Block a user