initial commit
This commit is contained in:
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Xmake cache
|
||||||
|
.xmake/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# MacOS Cache
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# personnal documentation
|
||||||
|
doc/*.txt
|
||||||
|
doc/*.violet.html
|
||||||
|
|
||||||
|
# pieces files
|
||||||
|
data/pieces/*.bin
|
||||||
BIN
doc/class_diagramm_core.png
Normal file
BIN
doc/class_diagramm_core.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
BIN
doc/class_diagramm_pieces.png
Normal file
BIN
doc/class_diagramm_pieces.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
67
doc/game_logic.md
Normal file
67
doc/game_logic.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# 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).
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
- Pause
|
||||||
|
- Retry
|
||||||
|
- Hold
|
||||||
|
- Move left
|
||||||
|
- Move right
|
||||||
|
- Rotate clockwise (CW)
|
||||||
|
- Rotate 180°
|
||||||
|
- Rotate counter-clockwise (CCW)
|
||||||
|
- Soft drop
|
||||||
|
- Hard drop
|
||||||
|
|
||||||
|
Pausing and retrying are managed by the UI.
|
||||||
|
IHS and IRS are always tested first (see the [leniency mechanics section](./game_logic.md#leniency-mechanics)).
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
When a piece touches the ground, there is a short time period before she 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 as 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.
|
||||||
|
|
||||||
|
If either holding or rotating happens during frames where no piece is in the board, they will be memorized and immediately applied upon spawning the next piece. This can sometime prevent the player from loosing when the default spawn would have lost the game. This is called IRS and IHS, for Instant Rotation/Hold System.
|
||||||
|
IRS and IHS will fail if they actually loose the player the game when it would have not happened otherwise. In the same sense, holding always fails if it would loose the game.
|
||||||
|
|
||||||
|
## Kicking pieces
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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 cell containing the piece or touching the piece, we will call the set of all theses cells the ``safeCells``
|
||||||
|
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 cell right, then 1 cell left, then 2 cell right, etc. until it fit (and then stop the algorithm), if at one point a position doesn't touch one of the ``safeCells`` 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 cells horizontally) touched none of the ``safeCells``
|
||||||
|
5. Do the same as step 4 but now we move the piece one line up every time
|
||||||
|
6. Cancel the rotation
|
||||||
|
|
||||||
|
## 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, but the rules varies a lot from game to game.
|
||||||
|
|
||||||
|
Since we deal with a great deal of different size and shapes, the rules for spin dectection have been simplified greatly:
|
||||||
|
|
||||||
|
- A move is a _spin_ if the piece is locked in place, that is it can't be moved one cell up, down, left or right without hitting a wall
|
||||||
|
- A move is a _mini spin_ if the move isn't a spin and the last action of the piece was a kick (dropping down because of gravity counts as an action)
|
||||||
|
|
||||||
|
## Score calculation
|
||||||
|
|
||||||
|
- For every cell soft dropped, add 1 to the score
|
||||||
|
- For every cell hard dropped, add 2 to the score
|
||||||
|
- When clearing one line, add 100 to the score, 200 for 2 lines, 400 for 3 lines, 800 for 4 lines, 1600 for 5 lines, etc.
|
||||||
|
- If the line clear is a spin, count the score like a normal clear of 2x more line (200 for 1-line spin, 800 for 2, 3200 for 3, etc.)
|
||||||
|
- When performing a spin, a mini spin, or clearing 4 or more lines, B2B is activated, every subsequent line clear that is a spin, a mini spin, or clear 4 or more lines, scores twice as much
|
||||||
108
doc/pieces_representation.md
Normal file
108
doc/pieces_representation.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Pieces representation
|
||||||
|
|
||||||
|
## What are polyominos ?
|
||||||
|
|
||||||
|
In this game, pieces are represented as a polyomino with a color.
|
||||||
|
Polyominos are mathematical objects consisting of multiple edge-touching squares.
|
||||||
|
There must be a path from every cell to every other cell, going from square to square only through the sides and not the corners.
|
||||||
|
Polyominos can be classified in 3 ways:
|
||||||
|
|
||||||
|
- Fixed polyominos : only translation is allowed
|
||||||
|
- One-sided polyominos : 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).
|
||||||
|
|
||||||
|
Most stacker game uses one-sided polyominos, which results in 7 polyominos of size 4, also known as tetrominos.
|
||||||
|
In this game too, one-sided polyominos will be used since we will only allow to move and rotate the pieces.
|
||||||
|
|
||||||
|
Internally, Polyominos are represented as a set of Cell.
|
||||||
|
This means the cells can be in any order but can't be duplicates.
|
||||||
|
A cell is simply a position on a 2D grid, so a polyomino is determined by the position of its cells.
|
||||||
|
This means however that 2 polyominos of same shape but different positions will be interpreted as different polyominos.
|
||||||
|
To solve this, we normalize the position of the polyominos 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.
|
||||||
|
We will need a way to:
|
||||||
|
|
||||||
|
1. Generate all one-sided polyominos 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 more unplayable.
|
||||||
|
|
||||||
|
## Ordering the polyominos
|
||||||
|
|
||||||
|
For practical reasons, we want to be able to sort all polyominos of the same size.
|
||||||
|
This is done very simply:
|
||||||
|
|
||||||
|
- If one polyomino has an inferior length than another, it is deemed inferior
|
||||||
|
- If two polyomino have the same length, we check all the cells of their square, from left to right, and repeating from up to bottom, for the first position where a polyomino has a cell that another doesn't, the polyomino with the cell is deemed inferior
|
||||||
|
|
||||||
|
Once the polyomino are ordered, it is very simple to attribute them a color, we can simply iterate through the list while looping over the color list.
|
||||||
|
|
||||||
|
## 1. Generating polyominos
|
||||||
|
|
||||||
|
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 algorithm is the following:
|
||||||
|
|
||||||
|
1. Add a single cell at position (0,0), and number it with 0
|
||||||
|
2. Call the generator function
|
||||||
|
1. If we get a polyomino of the size we want:
|
||||||
|
1. We rotate it in its 4 possible rotations and sort them
|
||||||
|
2. If the polyomino was generated in its lowest rotation, we add it to the list, else we discard it
|
||||||
|
3. Stop this instance of the function (the function is recursive, see step 2.3.2)
|
||||||
|
2. Else we number each adjacent cell to the polyomino with a number higher than the last numbered cell, unless:
|
||||||
|
1. If a cell was already numbered then we don't touch it
|
||||||
|
2. If a cell is on top of the polyomino then we don't number it
|
||||||
|
3. If a cell is below y=0, or at exactly x=0 and y<0, then we don't number it
|
||||||
|
3. For each cell with a higher number than the last added one:
|
||||||
|
1. We add this cell to the polyomino
|
||||||
|
2. We call the generator function (recursive function!)
|
||||||
|
3. We remove this cell from the polyomino
|
||||||
|
3. Return the list of polyominos
|
||||||
|
|
||||||
|
The exact number of one-sided polyominos up to size 12 (and higher, but we only generated up to size 12) is known, and this method generated exactly theses numbers, without duplicates.
|
||||||
|
|
||||||
|
By marking cells 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 cells 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.
|
||||||
|
|
||||||
|
## 2. Setting the spawn position of polyominos
|
||||||
|
|
||||||
|
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 square of size ``length``.
|
||||||
|
So now we can assume the polyomino is always inscribed in a square of origin (0,0) and size ``length`` (which should always be the case under normal circumstances). This make the rotation center very easy to find as it is simply the center of this square, and rotating is as simple as rotating a matrix:
|
||||||
|
|
||||||
|
- Clockwise (CW) rotation : ``x, y = y, (length - 1) - x``
|
||||||
|
- 180° rotation : ``x, y = (length - 1) - x, (length - 1) - y``
|
||||||
|
- Counter-clockwise (CCW) rotation : ``x, y = (length - 1) - y, x``
|
||||||
|
|
||||||
|
_Note we set the origin at the bottom-left corner instead of the up-left corner in a matrix, so the formulae aren't exactly the same._
|
||||||
|
|
||||||
|
The second challenge comes in finding a normalized spawn position for pieces. To do this, we first need to find which rotation the piece needs to spawn on, and then center it in the middle of its square.
|
||||||
|
For the rotation, **we want to find the side which is both the widest and the flattest**.
|
||||||
|
**Widest** means that we prefer if the piece is oriented horizontally rather than vertically.
|
||||||
|
**Flattest** means we prefer the side with the most cell at the bottom of the piece.
|
||||||
|
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 widest than the others, and the others will be discarded
|
||||||
|
2. For each potential side check the number of cell 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 cells to their left at spawn
|
||||||
|
4. If there is still no winner, we sort the remaining sides (by simulating having them selectionned and then sort the resulting polyominos) 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 square, 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._
|
||||||
|
|
||||||
|
## 3. Separating the polyominos into categories
|
||||||
|
|
||||||
|
For this game, we want the polyominos 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 cells
|
||||||
|
- Holeless: the polyominos which are neither convex nor have a hole
|
||||||
|
- Others: the polyominos who have a hole (thoses are by definition not convex)
|
||||||
|
|
||||||
|
To check for convexity, we simply iterate trough each row and column, and check cell by cell if they are all contiguous.
|
||||||
|
|
||||||
|
To check for holes, we list every empty cells starting from the exterior of the square of the polyomino, then add every adjacent empty cell recursively. If the cell has an hole then there is at least one empty cell we could not attaign, and since we know the size of the square and of the polyomino, we can compute wheter we have the right number of empty cells.
|
||||||
20
doc/pieces_storage.md
Normal file
20
doc/pieces_storage.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Pieces storage
|
||||||
|
|
||||||
|
## What is stored
|
||||||
|
|
||||||
|
If you don't know what a polyomino is, check [this other file](Pieces_representation.md#what-are-polyominos).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
We want the pieces to be always sorted in the same order, always attributed the same color, 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 wihtout 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
Each piece is stored as follows:
|
||||||
|
- 1 byte for the length of the piece
|
||||||
|
- 1 byte for the other characteristics of the piece: ``ABCCCCCC`` where A indicates if the piece is convex, B indicates if it has a hole, and C is the color number of the piece
|
||||||
|
- 1 byte for each cell: ``XXXXYYYY`` where X is the x coordinate of the cell and Y is the y coordinate of the cell
|
||||||
|
|
||||||
|
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. It has been choosen to use pieces only up to size 15 for this game.
|
||||||
18
src/Core/Action.h
Normal file
18
src/Core/Action.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of actions that can be taken by the player
|
||||||
|
*/
|
||||||
|
enum Action {
|
||||||
|
PAUSE,
|
||||||
|
RETRY,
|
||||||
|
HOLD,
|
||||||
|
SOFT_DROP,
|
||||||
|
HARD_DROP,
|
||||||
|
MOVE_LEFT,
|
||||||
|
MOVE_RIGHT,
|
||||||
|
ROTATE_CW,
|
||||||
|
ROTATE_180,
|
||||||
|
ROTATE_CCW
|
||||||
|
};
|
||||||
50
src/Core/Bag.cpp
Normal file
50
src/Core/Bag.cpp
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#include "Bag.h"
|
||||||
|
|
||||||
|
#include "../Pieces/Piece.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
|
||||||
|
Bag::Bag(const std::vector<Piece>& pieces) : pieces(pieces) {
|
||||||
|
// initialize bags
|
||||||
|
this->currentBag.clear();
|
||||||
|
for (int i = 0; i < this->pieces.size(); i++) {
|
||||||
|
this->currentBag.push_back(i);
|
||||||
|
}
|
||||||
|
this->nextBag.clear();
|
||||||
|
|
||||||
|
// prepare first piece
|
||||||
|
this->prepareNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
Piece Bag::lookNext() {
|
||||||
|
// return the next piece
|
||||||
|
return this->pieces.at(this->next);
|
||||||
|
}
|
||||||
|
|
||||||
|
Piece Bag::getNext() {
|
||||||
|
// get the piece to return
|
||||||
|
int nextIndex = this->next;
|
||||||
|
|
||||||
|
// prepare the piece even after the next
|
||||||
|
this->prepareNext();
|
||||||
|
|
||||||
|
// return the next piece
|
||||||
|
return this->pieces.at(nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bag::prepareNext() {
|
||||||
|
// if the bag is empty switch to the next bag
|
||||||
|
if (this->currentBag.empty()) {
|
||||||
|
std::swap(this->currentBag, this->nextBag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pick a random piece from the current bag
|
||||||
|
int indexIndex = std::rand() % this->currentBag.size();
|
||||||
|
this->next = this->currentBag.at(indexIndex);
|
||||||
|
|
||||||
|
// move the piece over to the next bag
|
||||||
|
this->nextBag.push_back(this->next);
|
||||||
|
this->currentBag.erase(this->currentBag.begin() + indexIndex);
|
||||||
|
}
|
||||||
39
src/Core/Bag.h
Normal file
39
src/Core/Bag.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../Pieces/Piece.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A litteral bag of pieces, in which you take each of its piece randomly one by one then start again with a new bag
|
||||||
|
*/
|
||||||
|
class Bag {
|
||||||
|
private:
|
||||||
|
std::vector<Piece> pieces; // the pieces the bag can dispense
|
||||||
|
int next; // the next piece to give
|
||||||
|
std::vector<int> currentBag; // the list of pieces that are still to be taken out before starting a new bag
|
||||||
|
std::vector<int> nextBag; // the list of pieces that have been taken out of the current bag and have been placed in the next
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Creates a new bag of the specified list of pieces
|
||||||
|
*/
|
||||||
|
Bag(const std::vector<Piece>& pieces);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks at what the next picked piece will be
|
||||||
|
*/
|
||||||
|
Piece lookNext();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks a new piece from the current bag
|
||||||
|
*/
|
||||||
|
Piece getNext();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Prepare the next picked piece in advance
|
||||||
|
*/
|
||||||
|
void prepareNext();
|
||||||
|
};
|
||||||
117
src/Core/Board.cpp
Normal file
117
src/Core/Board.cpp
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
#include "Board.h"
|
||||||
|
|
||||||
|
#include "../Pieces/Piece.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
#include <Set>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
|
||||||
|
Board::Board(int width, int height) : width(width), height(height) {
|
||||||
|
std::vector<Color> emptyRow;
|
||||||
|
for (int i = 0; i < width; i ++) {
|
||||||
|
emptyRow.push_back(NOTHING);
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize grid
|
||||||
|
this->grid.clear();
|
||||||
|
for (int j = 0; j < height; j++) {
|
||||||
|
this->grid.push_back(emptyRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Board::addBlock(const Cell& position, Color block) {
|
||||||
|
// if the block is out of bounds we discard it
|
||||||
|
if (position.x < 0 || position.x >= this->width || position.y < 0) return;
|
||||||
|
|
||||||
|
// resize the grid if needed
|
||||||
|
if (position.y >= this->grid.size()) {
|
||||||
|
std::vector<Color> emptyRow;
|
||||||
|
for (int i = 0; i < width; i ++) {
|
||||||
|
emptyRow.push_back(NOTHING);
|
||||||
|
}
|
||||||
|
for (int j = this->grid.size(); j <= position.y; j++) {
|
||||||
|
this->grid.push_back(emptyRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// change the block in the grid
|
||||||
|
this->grid.at(position.y).at(position.x) = block;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Board::clearRows() {
|
||||||
|
std::vector<Color> emptyRow;
|
||||||
|
for (int i = 0; i < width; i ++) {
|
||||||
|
emptyRow.push_back(NOTHING);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check from top to bottom
|
||||||
|
int clearedLines = 0;
|
||||||
|
for (int j = this->grid.size() - 1; j >= 0; j--) {
|
||||||
|
// check if a line has a block on every column
|
||||||
|
bool isFull = true;
|
||||||
|
for (int i = 0; i < this->width; i++) {
|
||||||
|
if (this->grid.at(j).at(i) == NOTHING) {
|
||||||
|
isFull = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it has, erase it and add a new row at the top
|
||||||
|
if (isFull) {
|
||||||
|
this->grid.erase(this->grid.begin() + j);
|
||||||
|
if(this->grid.size() < height) this->grid.push_back(emptyRow);
|
||||||
|
clearedLines++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clearedLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color Board::getBlock(const Cell& position) const {
|
||||||
|
// if the block is out of bounds
|
||||||
|
if (position.x < 0 || position.x >= this->width || position.y < 0) return OUT_OF_BOUND;
|
||||||
|
|
||||||
|
// if the block is higher than the current grid, since it can grow indefinitely we do as if it was there but empty
|
||||||
|
if (position.y >= this->grid.size()) return NOTHING;
|
||||||
|
|
||||||
|
// else get the color in the grid
|
||||||
|
return this->grid.at(position.y).at(position.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::vector<Color>> Board::getBlocks() const {
|
||||||
|
return this->grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Board::getGridHeight() const {
|
||||||
|
return this->grid.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
int Board::getBaseHeight() const {
|
||||||
|
return this->height;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Board::getWidth() const {
|
||||||
|
return this->width;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& os, const Board& board) {
|
||||||
|
// print the board
|
||||||
|
for (int y = board.grid.size() - 1; y >= 0; y--) {
|
||||||
|
for (int x = 0; x < board.width; x++) {
|
||||||
|
Color block = board.grid.at(y).at(x);
|
||||||
|
os << COLOR_CODES[block];
|
||||||
|
if (block != NOTHING) {
|
||||||
|
os << "*";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
os << "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset console color
|
||||||
|
os << COLOR_RESET;
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
63
src/Core/Board.h
Normal file
63
src/Core/Board.h
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../Pieces/Piece.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A 2D grid of blocks
|
||||||
|
*/
|
||||||
|
class Board {
|
||||||
|
private:
|
||||||
|
std::vector<std::vector<Color>> grid; // the grid, (0,0) is downleft
|
||||||
|
int width; // the width of the grid
|
||||||
|
int height; // the base height of the grid, which can extends indefinitely
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Creates a new board of the specified size
|
||||||
|
*/
|
||||||
|
Board(int width, int height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the color of the specified block, if the block is out of bounds it is simply ignored
|
||||||
|
*/
|
||||||
|
void addBlock(const Cell& position, Color block);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears any complete row and moves down the rows on top, returns the number of cleared rows
|
||||||
|
*/
|
||||||
|
int clearRows();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the color of the block at the specified position
|
||||||
|
*/
|
||||||
|
Color getBlock(const Cell& position) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the grid
|
||||||
|
*/
|
||||||
|
std::vector<std::vector<Color>> getBlocks() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the actual height of the grid
|
||||||
|
*/
|
||||||
|
int getGridHeight() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the base height of the grid
|
||||||
|
*/
|
||||||
|
int getBaseHeight() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the width of the grid
|
||||||
|
*/
|
||||||
|
int getWidth() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream output operator, adds a 2D grid representing the board
|
||||||
|
*/
|
||||||
|
friend std::ostream& operator<<(std::ostream& os, const Board& board);
|
||||||
|
};
|
||||||
300
src/Core/Game.cpp
Normal file
300
src/Core/Game.cpp
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
#include "Game.h"
|
||||||
|
|
||||||
|
#include "GameBoard.h"
|
||||||
|
#include "GameParameters.h"
|
||||||
|
#include "Action.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
|
||||||
|
static const int SUBPX_PER_ROW = 60; // the number of position the active piece can take "between" two rows
|
||||||
|
static const int SOFT_DROP_SCORE = 1; // the score gained by line soft dropped
|
||||||
|
static const int HARD_DROP_SCORE = 2; // the score gained by line hard dropped
|
||||||
|
static const int LINE_CLEAR_BASE_SCORE = 100; // the score value of clearing a single line
|
||||||
|
static const int B2B_SCORE_MULTIPLIER = 2; // by how much havaing B2B on multiplies the score of the line clear
|
||||||
|
static const int B2B_MIN_LINE_NUMBER = 4; // the minimum number of lines needed to be cleared at once to gain B2B (without a spin)
|
||||||
|
|
||||||
|
|
||||||
|
Game::Game(Gamemode gamemode, const Player& controls, int boardWidth, int boardHeight, const std::vector<Piece>& bag) : parameters(gamemode, controls), board(boardWidth, boardHeight, bag, parameters.getNextQueueLength()) {
|
||||||
|
// the game has not yet started
|
||||||
|
this->started = false;
|
||||||
|
this->lost = false;
|
||||||
|
|
||||||
|
// initialize stats
|
||||||
|
this->score = 0;
|
||||||
|
this->framesPassed = 0;
|
||||||
|
this->B2BChain = 0;
|
||||||
|
|
||||||
|
// nothing happened yet
|
||||||
|
this->heldActions.clear();
|
||||||
|
this->initialActions.clear();
|
||||||
|
this->heldDAS = 0;
|
||||||
|
this->heldARR = 0;
|
||||||
|
this->subVerticalPosition = 0;
|
||||||
|
this->leftARETime = 0;
|
||||||
|
this->totalLockDelay = 0;
|
||||||
|
this->totalForcedLockDelay = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::start() {
|
||||||
|
// starts the game
|
||||||
|
this->started = true;
|
||||||
|
this->lost = this->board.spawnNextPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::nextFrame(const std::set<Action>& playerActions) {
|
||||||
|
if (this->lost || this->hasWon()) return;
|
||||||
|
|
||||||
|
if (this->started) {
|
||||||
|
bool AREJustEnded = (this->leftARETime == 1);
|
||||||
|
if (this->leftARETime > 0) {
|
||||||
|
this->leftARETime--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->leftARETime == 0) {
|
||||||
|
if (AREJustEnded) {
|
||||||
|
this->board.spawnNextPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* IRS and IHS */
|
||||||
|
Rotation initialRotation = NONE
|
||||||
|
+ (this->initialActions.contains(ROTATE_CW)) ? CLOCKWISE : NONE
|
||||||
|
+ (this->initialActions.contains(ROTATE_180)) ? DOUBLE : NONE
|
||||||
|
+ (this->initialActions.contains(ROTATE_CCW)) ? COUNTERCLOCKWISE : NONE;
|
||||||
|
|
||||||
|
if (this->initialActions.contains(HOLD)) {
|
||||||
|
if (this->board.hold(initialRotation)) {
|
||||||
|
this->subVerticalPosition = 0;
|
||||||
|
this->totalLockDelay = 0;
|
||||||
|
this->heldARR = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (initialRotation != NONE) {
|
||||||
|
this->board.rotate(initialRotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HOLD */
|
||||||
|
if (playerActions.contains(HOLD) && (!this->heldActions.contains(HOLD))) {
|
||||||
|
if (this->board.hold()) {
|
||||||
|
this->subVerticalPosition = 0;
|
||||||
|
this->totalLockDelay = 0;
|
||||||
|
this->heldARR = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MOVE LEFT/RIGHT */
|
||||||
|
if (playerActions.contains(MOVE_LEFT)) {
|
||||||
|
this->movePiece(-1, (this->heldDAS >= 0));
|
||||||
|
}
|
||||||
|
if (playerActions.contains(MOVE_RIGHT)) {
|
||||||
|
this->movePiece(1, (this->heldDAS <= 0));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this->heldDAS = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ROTATIONS */
|
||||||
|
if (playerActions.contains(ROTATE_CW) && (!this->heldActions.contains(ROTATE_CW))) {
|
||||||
|
this->board.rotate(CLOCKWISE);
|
||||||
|
}
|
||||||
|
if (playerActions.contains(ROTATE_180) && (!this->heldActions.contains(ROTATE_180))) {
|
||||||
|
this->board.rotate(DOUBLE);
|
||||||
|
}
|
||||||
|
if (playerActions.contains(ROTATE_CCW) && (!this->heldActions.contains(ROTATE_CCW))) {
|
||||||
|
this->board.rotate(COUNTERCLOCKWISE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SOFT DROP */
|
||||||
|
if (playerActions.contains(SOFT_DROP)) {
|
||||||
|
int appliedSDR = this->parameters.getSDR();
|
||||||
|
|
||||||
|
// SDR=0 -> instant drop
|
||||||
|
if (appliedSDR == 0) {
|
||||||
|
while (this->board.moveDown()) {
|
||||||
|
this->score += SOFT_DROP_SCORE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// SDR>1 -> move down by specified amount
|
||||||
|
else {
|
||||||
|
this->subVerticalPosition += (SUBPX_PER_ROW / appliedSDR);
|
||||||
|
while (this->subVerticalPosition >= SUBPX_PER_ROW) {
|
||||||
|
this->subVerticalPosition -= SUBPX_PER_ROW;
|
||||||
|
this->score += (this->board.moveDown() * SOFT_DROP_SCORE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HARD DROP */
|
||||||
|
// needs to be done last because we can enter ARE period afterwards
|
||||||
|
if (this->initialActions.contains(HARD_DROP) || (playerActions.contains(HARD_DROP) && (!this->heldActions.contains(HARD_DROP)))) {
|
||||||
|
while (this->board.moveDown()) {
|
||||||
|
this->score += HARD_DROP_SCORE;
|
||||||
|
}
|
||||||
|
this->lockPiece();
|
||||||
|
}
|
||||||
|
// no need to apply gravity and lock delay if the piece was hard dropped
|
||||||
|
else {
|
||||||
|
/* GRAVITY */
|
||||||
|
// parameters.getGravity() gives the gravity for an assumed 20-line high board
|
||||||
|
int appliedGravity = this->parameters.getGravity() * (this->board.getBoard().getBaseHeight() / 20.0);
|
||||||
|
|
||||||
|
this->subVerticalPosition += appliedGravity;
|
||||||
|
while (this->subVerticalPosition >= SUBPX_PER_ROW) {
|
||||||
|
this->subVerticalPosition -= SUBPX_PER_ROW;
|
||||||
|
this->board.moveDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LOCK DELAY */
|
||||||
|
if (this->board.touchesGround()) {
|
||||||
|
this->totalLockDelay++;
|
||||||
|
this->totalForcedLockDelay++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this->totalLockDelay = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this->totalLockDelay > this->parameters.getLockDelay()) || (this->totalForcedLockDelay > this->parameters.getForcedLockDelay())) {
|
||||||
|
this->lockPiece();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove initial actions only once they've been applied
|
||||||
|
if (AREJustEnded) {
|
||||||
|
this->initialActions.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->framesPassed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update remembered actions
|
||||||
|
if ((!this->started) || this->leftARETime > 0) {
|
||||||
|
for (Action action : playerActions) {
|
||||||
|
this->initialActions.insert(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->heldActions = playerActions;
|
||||||
|
|
||||||
|
if (playerActions.contains(MOVE_LEFT)) {
|
||||||
|
this->heldDAS = std::min(-1, this->heldDAS - 1);
|
||||||
|
}
|
||||||
|
if (playerActions.contains(MOVE_RIGHT)) {
|
||||||
|
this->heldDAS = std::max(1, this->heldDAS + 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this->heldDAS = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::movePiece(int movement, bool resetDirection) {
|
||||||
|
if (resetDirection) {
|
||||||
|
this->heldDAS = movement;
|
||||||
|
this->heldARR = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this->heldDAS += movement;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs(this->heldDAS) > this->parameters.getDAS()) {
|
||||||
|
int appliedARR = this->parameters.getARR();
|
||||||
|
|
||||||
|
// ARR=0 -> instant movement
|
||||||
|
if (appliedARR == 0) {
|
||||||
|
if (movement == -1) while (this->board.moveLeft());
|
||||||
|
if (movement == 1) while (this->board.moveRight());
|
||||||
|
}
|
||||||
|
// ARR>1 -> move by specified amount
|
||||||
|
else {
|
||||||
|
this->heldARR++;
|
||||||
|
if (this->heldARR == appliedARR) {
|
||||||
|
this->heldARR = 0;
|
||||||
|
if (movement == -1) this->board.moveLeft();
|
||||||
|
if (movement == 1) this->board.moveRight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Game::lockPiece() {
|
||||||
|
LineClear clear = this->board.lockPiece();
|
||||||
|
this->parameters.clearLines(clear.lines);
|
||||||
|
|
||||||
|
// update B2B and score
|
||||||
|
bool B2BConditions = ((clear.lines > B2B_MIN_LINE_NUMBER) || clear.isSpin || clear.isMiniSpin);
|
||||||
|
if (clear.lines > 0) {
|
||||||
|
/* clearing one more line is worth 2x more
|
||||||
|
clearing with a spin is worth as much as clearing 2x more lines */
|
||||||
|
long int clearScore = LINE_CLEAR_BASE_SCORE;
|
||||||
|
clearScore = clearScore << (clear.lines << (clear.isSpin));
|
||||||
|
|
||||||
|
if (this->B2BChain && B2BConditions) clearScore *= B2B_SCORE_MULTIPLIER;
|
||||||
|
this->score += clearScore;
|
||||||
|
}
|
||||||
|
this->B2BChain = B2BConditions;
|
||||||
|
|
||||||
|
// reset active piece
|
||||||
|
this->subVerticalPosition = 0;
|
||||||
|
this->totalLockDelay = 0;
|
||||||
|
this->totalForcedLockDelay = 0;
|
||||||
|
this->heldARR = 0;
|
||||||
|
|
||||||
|
// check for ARE
|
||||||
|
this->leftARETime = this->parameters.getARE();
|
||||||
|
if (this->leftARETime == 0) {
|
||||||
|
this->board.spawnNextPiece();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Game::hasWon() {
|
||||||
|
return this->parameters.hasWon(this->framesPassed);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Game::hasLost() {
|
||||||
|
return this->lost;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Game::getClearedLines() {
|
||||||
|
return this->parameters.getClearedLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
int Game::getLevel() {
|
||||||
|
return this->parameters.getLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
int Game::getFramesPassed() {
|
||||||
|
return this->framesPassed;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Game::getScore() {
|
||||||
|
return this->score;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Game::isOnB2BChain() {
|
||||||
|
return this->B2BChain;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Game::areBlocksBones() {
|
||||||
|
return this->parameters.getBoneBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
Board Game::getBoard() {
|
||||||
|
return this->board.getBoard();
|
||||||
|
}
|
||||||
|
|
||||||
|
Piece Game::getActivePiece() {
|
||||||
|
return this->board.getActivePiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
Cell Game::getActivePiecePosition() {
|
||||||
|
return this->board.getActivePiecePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
Piece Game::getHeldPiece() {
|
||||||
|
return this->board.getHeldPiece();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Piece> Game::getNextPieces() {
|
||||||
|
return this->board.getNextPieces();
|
||||||
|
}
|
||||||
124
src/Core/Game.h
Normal file
124
src/Core/Game.h
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GameBoard.h"
|
||||||
|
#include "GameParameters.h"
|
||||||
|
#include "Action.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interprets the player action into the game, depending on the state of the board and the current gamemode
|
||||||
|
*/
|
||||||
|
class Game {
|
||||||
|
private:
|
||||||
|
GameParameters parameters; // the current parameters of the game
|
||||||
|
GameBoard board; // the board in which the game is played
|
||||||
|
bool started; // wheter the game has started
|
||||||
|
bool lost; // wheter the game is lost
|
||||||
|
long int score; // the current score
|
||||||
|
int framesPassed; // how many frames have passed since the start of the game
|
||||||
|
bool B2BChain; // wheter the player is currently on a B2B chain
|
||||||
|
std::set<Action> heldActions; // the list of actions that were pressed last frame
|
||||||
|
std::set<Action> initialActions; // the list of actions that have been pressed while there was no active piece
|
||||||
|
int heldDAS; // the number of frames DAS has been held, positive for right or negative for left
|
||||||
|
int heldARR; // the number of frames ARR has been held
|
||||||
|
int subVerticalPosition; // how far the active piece is to go down one line
|
||||||
|
int leftARETime; // how many frames are left before ARE period finishes
|
||||||
|
int totalLockDelay; // how many frames has the active piece touched the ground without moving
|
||||||
|
int totalForcedLockDelay; // how many frames the active piece has touched the ground since the last spawned piece
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Initialize the parameters and creates a new board
|
||||||
|
*/
|
||||||
|
Game(Gamemode gamemode, const Player& controls, int boardWidth, int boardHeight, const std::vector<Piece>& bag);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the game
|
||||||
|
*/
|
||||||
|
void start();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advance to the next frame while excecuting the actions taken by the player,
|
||||||
|
* this is where the main game logic takes place
|
||||||
|
*/
|
||||||
|
void nextFrame(const std::set<Action>& playerActions);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Move the piece in the specified direction
|
||||||
|
*/
|
||||||
|
void movePiece(int movement, bool resetDirection);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locks the piece, updates level and score and spawn the next piece if necessary
|
||||||
|
*/
|
||||||
|
void lockPiece();
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Returns wheter the player has won
|
||||||
|
*/
|
||||||
|
bool hasWon();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns wheter the player has lost
|
||||||
|
*/
|
||||||
|
bool hasLost();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current level
|
||||||
|
*/
|
||||||
|
int getLevel();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current number of cleared lines
|
||||||
|
*/
|
||||||
|
int getClearedLines();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of frames passed since the start of the game
|
||||||
|
*/
|
||||||
|
int getFramesPassed();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current score
|
||||||
|
*/
|
||||||
|
int getScore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns wheter the player is currently on a B2B chain
|
||||||
|
*/
|
||||||
|
bool isOnB2BChain();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns wheter all blocks are currently bone blocks
|
||||||
|
*/
|
||||||
|
bool areBlocksBones();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the board
|
||||||
|
*/
|
||||||
|
Board getBoard();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the active piece
|
||||||
|
*/
|
||||||
|
Piece getActivePiece();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the active piece position
|
||||||
|
*/
|
||||||
|
Cell getActivePiecePosition();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the held piece
|
||||||
|
*/
|
||||||
|
Piece getHeldPiece();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a copy of the next pieces queue
|
||||||
|
*/
|
||||||
|
std::vector<Piece> getNextPieces();
|
||||||
|
};
|
||||||
360
src/Core/GameBoard.cpp
Normal file
360
src/Core/GameBoard.cpp
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
#include "GameBoard.h"
|
||||||
|
|
||||||
|
#include "../Pieces/Piece.h"
|
||||||
|
#include "Board.h"
|
||||||
|
#include "Bag.h"
|
||||||
|
#include "LineClear.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
#include <Set>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
|
||||||
|
GameBoard::GameBoard(int boardWidth, int boardHeight, const std::vector<Piece>& bag, int nextQueueLength) : board(boardWidth, boardHeight), generator(bag), nextQueueLength(nextQueueLength) {
|
||||||
|
// initialize queue
|
||||||
|
this->nextQueue.clear();
|
||||||
|
for (int i = 0; i < nextQueueLength; i++) {
|
||||||
|
this->nextQueue.push_back(this->generator.getNext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameBoard::moveLeft() {
|
||||||
|
// check if the piece can be moved one cell left
|
||||||
|
if (this->isActivePieceInWall(Cell{-1, 0})) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this->activePiecePosition.x -= 1;
|
||||||
|
this->isLastMoveKick = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameBoard::moveRight() {
|
||||||
|
// check if the piece can be moved one cell right
|
||||||
|
if (this->isActivePieceInWall(Cell{1, 0})) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this->activePiecePosition.x += 1;
|
||||||
|
this->isLastMoveKick = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameBoard::moveDown() {
|
||||||
|
// check if the piece can be moved one cell down
|
||||||
|
if (this->isActivePieceInWall(Cell{0, -1})) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this->activePiecePosition.y -= 1;
|
||||||
|
this->isLastMoveKick = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameBoard::rotate(Rotation rotation) {
|
||||||
|
// copy the original piece before rotating it
|
||||||
|
Piece stored = *this->activePiece;
|
||||||
|
this->rotate(rotation);
|
||||||
|
|
||||||
|
// check if the piece can rotate
|
||||||
|
if (!this->isActivePieceInWall()) {
|
||||||
|
this->isLastMoveKick = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the list of cells that touches the original piece
|
||||||
|
std::set<Cell> safeCells;
|
||||||
|
for (Cell cell : stored.getPositions()) {
|
||||||
|
Cell cellInGrid(cell + this->activePiecePosition);
|
||||||
|
safeCells.insert(cellInGrid);
|
||||||
|
safeCells.insert(cellInGrid + Cell{0, 1});
|
||||||
|
safeCells.insert(cellInGrid + Cell{1, 0});
|
||||||
|
safeCells.insert(cellInGrid + Cell{0, -1});
|
||||||
|
safeCells.insert(cellInGrid + Cell{-1, 0});
|
||||||
|
}
|
||||||
|
|
||||||
|
// try kicking the piece down
|
||||||
|
bool suceeded = this->tryKicking(true, safeCells);
|
||||||
|
if (suceeded) {
|
||||||
|
this->isLastMoveKick = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it doesn't work try kicking the piece up
|
||||||
|
suceeded = this->tryKicking(false, safeCells);
|
||||||
|
if (suceeded) {
|
||||||
|
this->isLastMoveKick = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it still doesn't work, abort the rotation
|
||||||
|
this->activePiece = std::make_shared<Piece>(stored);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameBoard::tryKicking(bool testingBottom, const std::set<Cell>& safeCells) {
|
||||||
|
// we try from the original height of the piece, moving vertically as long as the kicked piece touches the original
|
||||||
|
bool overlapsVertically = true;
|
||||||
|
int j = 0;
|
||||||
|
do {
|
||||||
|
// we try from the center to the sides as long as the kicked piece touches the original
|
||||||
|
bool overlapsLeft = true;
|
||||||
|
bool overlapsRight = true;
|
||||||
|
int i = 0;
|
||||||
|
do {
|
||||||
|
// check right before right arbitrarly, we don't decide this with rotations since it would still be arbitrary with 180° rotations
|
||||||
|
if (overlapsRight) {
|
||||||
|
Cell shift{+i, j};
|
||||||
|
// the kicked position must touch the original piece
|
||||||
|
if (!this->activePieceOverlapsOneCell(safeCells, shift)) {
|
||||||
|
overlapsLeft = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// if the position is valid we place the active piece there
|
||||||
|
if (!this->isActivePieceInWall(shift)) {
|
||||||
|
this->activePiecePosition += shift;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the same on the left side
|
||||||
|
if (overlapsLeft) {
|
||||||
|
Cell shift{-i, j};
|
||||||
|
if (!this->activePieceOverlapsOneCell(safeCells, shift)) {
|
||||||
|
overlapsLeft = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!this->isActivePieceInWall(shift)) {
|
||||||
|
this->activePiecePosition += shift;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
} while (overlapsLeft && overlapsRight);
|
||||||
|
|
||||||
|
// test if no position touched the original piece
|
||||||
|
if (i == 1) {
|
||||||
|
overlapsVertically = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// move one line up or down
|
||||||
|
(testingBottom) ? j-- : j++;
|
||||||
|
} while (overlapsVertically);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameBoard::hold(Rotation initialRotation) {
|
||||||
|
// swap with held piece
|
||||||
|
std::swap(this->activePiece, this->heldPiece);
|
||||||
|
|
||||||
|
// if it's the first time holding try the next piece
|
||||||
|
bool isFirstTimeHolding = false;
|
||||||
|
if (this->activePiece == nullptr) {
|
||||||
|
isFirstTimeHolding = true;
|
||||||
|
|
||||||
|
// if no pieces in next queue look at what the next would be
|
||||||
|
if (this->nextQueueLength == 0) {
|
||||||
|
this->activePiece = std::make_shared<Piece>(this->generator.lookNext());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this->activePiece = std::make_shared<Piece>(this->nextQueue.front());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the spawned piece to the correct position
|
||||||
|
this->goToSpawnPosition();
|
||||||
|
|
||||||
|
// apply initial rotation
|
||||||
|
Piece stored = *this->activePiece;
|
||||||
|
this->rotate(initialRotation);
|
||||||
|
|
||||||
|
// if the piece can't spawn, abort initial rotation
|
||||||
|
if (this->isActivePieceInWall()) {
|
||||||
|
this->activePiece = std::make_shared<Piece>(stored);
|
||||||
|
|
||||||
|
// if the piece still can't spawn, abort holding
|
||||||
|
if (this->isActivePieceInWall()) {
|
||||||
|
if (isFirstTimeHolding) {
|
||||||
|
this->activePiece = nullptr;
|
||||||
|
}
|
||||||
|
std::swap(this->activePiece, this->heldPiece);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's the first time holding, confirm we keep this piece
|
||||||
|
if (isFirstTimeHolding) {
|
||||||
|
if (this->nextQueueLength == 0) {
|
||||||
|
this->generator.getNext();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this->spawnNextPiece();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this piece has done nothing yet
|
||||||
|
this->isLastMoveKick = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameBoard::spawnNextPiece() {
|
||||||
|
// add a piece to the queue
|
||||||
|
this->nextQueue.push_back(this->generator.getNext());
|
||||||
|
|
||||||
|
// get next piece from queue
|
||||||
|
this->activePiece = std::make_shared<Piece>(this->nextQueue.front());
|
||||||
|
this->nextQueue.erase(this->nextQueue.begin());
|
||||||
|
|
||||||
|
// set the spawned piece to the correct position
|
||||||
|
this->goToSpawnPosition();
|
||||||
|
|
||||||
|
// this piece has done nothing yet
|
||||||
|
this->isLastMoveKick = false;
|
||||||
|
|
||||||
|
// returns wheter the piece can spawn correctly
|
||||||
|
return !this->isActivePieceInWall();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameBoard::touchesGround() {
|
||||||
|
return this->isActivePieceInWall(Cell{0, -1});
|
||||||
|
}
|
||||||
|
|
||||||
|
LineClear GameBoard::lockPiece() {
|
||||||
|
// check if the piece is locked in place
|
||||||
|
bool isLocked = (this->isActivePieceInWall(Cell{0, 1}) && this->isActivePieceInWall(Cell{1, 0}) &&
|
||||||
|
this->isActivePieceInWall(Cell{-1, 0}) && this->isActivePieceInWall(Cell{0, -1}));
|
||||||
|
|
||||||
|
// put the piece in the board
|
||||||
|
for (Cell cell : this->activePiece->getPositions()) {
|
||||||
|
this->board.addBlock(cell + this->activePiecePosition, this->activePiece->getColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for lines to clear
|
||||||
|
return LineClear{this->board.clearRows(), isLocked, (!isLocked) && this->isLastMoveKick};
|
||||||
|
}
|
||||||
|
|
||||||
|
Board GameBoard::getBoard() const {
|
||||||
|
return this->board;
|
||||||
|
}
|
||||||
|
|
||||||
|
Piece GameBoard::getActivePiece() const {
|
||||||
|
return *this->activePiece;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cell GameBoard::getActivePiecePosition() const {
|
||||||
|
return this->activePiecePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
Piece GameBoard::getHeldPiece() const {
|
||||||
|
return *this->heldPiece;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Piece> GameBoard::getNextPieces() const {
|
||||||
|
return this->nextQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameBoard::isActivePieceInWall(const Cell& shift) const {
|
||||||
|
// check if every cell of the active piece is in an empty spot
|
||||||
|
for (Cell cell : this->activePiece->getPositions()) {
|
||||||
|
if (this->board.getBlock(cell + this->activePiecePosition + shift) != NOTHING) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameBoard::activePieceOverlapsOneCell(const std::set<Cell>& safeCells, const Cell& shift) const {
|
||||||
|
// check if one cell of the translated active piece overlaps with one cell of the given piece set
|
||||||
|
for (Cell cell : this->activePiece->getPositions()) {
|
||||||
|
if (safeCells.contains(cell + shift)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameBoard::goToSpawnPosition() {
|
||||||
|
// get the lowest cell of the piece
|
||||||
|
int lowestCell = this->activePiece->getLength() - 1;
|
||||||
|
for (Cell cell : this->activePiece->getPositions()) {
|
||||||
|
if (cell.y < lowestCell) lowestCell = cell.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the piece one line above the board
|
||||||
|
this->activePiecePosition.y = this->board.getBaseHeight() - lowestCell;
|
||||||
|
|
||||||
|
// center the piece horizontally, biased towards left
|
||||||
|
this->activePiecePosition.x = (this->board.getWidth() - this->activePiece->getLength()) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard) {
|
||||||
|
// print over the board (only the active piece if it is there)
|
||||||
|
if (gameboard.activePiece != nullptr) {
|
||||||
|
|
||||||
|
// change to the color of the active piece
|
||||||
|
Color pieceColor = gameboard.activePiece->getColor();
|
||||||
|
os << COLOR_CODES[pieceColor];
|
||||||
|
|
||||||
|
// print only the cell were the active piece is
|
||||||
|
for (int y = gameboard.activePiecePosition.y + gameboard.activePiece->getLength() - 1; y >= gameboard.board.getBaseHeight(); y--) {
|
||||||
|
for (int x = 0; x < gameboard.board.getWidth(); x++) {
|
||||||
|
bool hasActivePiece = gameboard.activePiece->getPositions().contains(Cell{x, y} - gameboard.activePiecePosition);
|
||||||
|
if (hasActivePiece) {
|
||||||
|
os << "*";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
os << " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// print the board
|
||||||
|
Color pieceColor = (gameboard.activePiece == nullptr) ? NOTHING : gameboard.activePiece->getColor();
|
||||||
|
for (int y = gameboard.board.getBaseHeight() - 1; y >= 0; y--) {
|
||||||
|
for (int x = 0; x < gameboard.board.getWidth(); x++) {
|
||||||
|
bool hasActivePiece = (gameboard.activePiece == nullptr) ? false : gameboard.activePiece->getPositions().contains(Cell{x, y} - gameboard.activePiecePosition);
|
||||||
|
|
||||||
|
// if the active piece is on this cell, print it
|
||||||
|
if (hasActivePiece) {
|
||||||
|
os << COLOR_CODES[pieceColor];
|
||||||
|
os << "*";
|
||||||
|
}
|
||||||
|
|
||||||
|
// else print the cell of the board
|
||||||
|
else {
|
||||||
|
Color block = gameboard.board.getBlock(Cell{x, y});
|
||||||
|
os << COLOR_CODES[block];
|
||||||
|
if (block != NOTHING) {
|
||||||
|
os << "*";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
os << "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// print held piece
|
||||||
|
os << "Hold:" << std::endl;
|
||||||
|
if (!(gameboard.heldPiece == nullptr)) {
|
||||||
|
os << *gameboard.heldPiece;
|
||||||
|
}
|
||||||
|
|
||||||
|
// print next queue
|
||||||
|
os << "Next:" << std::endl;
|
||||||
|
for (const Piece& piece : gameboard.nextQueue) {
|
||||||
|
os << piece;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset console color
|
||||||
|
os << COLOR_RESET;
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
126
src/Core/GameBoard.h
Normal file
126
src/Core/GameBoard.h
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../Pieces/Piece.h"
|
||||||
|
#include "Board.h"
|
||||||
|
#include "Bag.h"
|
||||||
|
#include "LineClear.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Links a board with the pieces moving in it
|
||||||
|
*/
|
||||||
|
class GameBoard {
|
||||||
|
private:
|
||||||
|
Board board; // the board in which pieces moves, (0, 0) is downleft
|
||||||
|
Bag generator; // the piece generator
|
||||||
|
std::shared_ptr<Piece> activePiece; // the piece currently in the board
|
||||||
|
Cell activePiecePosition; // the position of the piece currently in the board
|
||||||
|
std::shared_ptr<Piece> heldPiece; // a piece being holded
|
||||||
|
int nextQueueLength; // the number of next pieces seeable at a time
|
||||||
|
std::vector<Piece> nextQueue; // the list of the next pieces to spawn in the board
|
||||||
|
bool isLastMoveKick; // wheter the last action the piece did was kicking
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Creates a new board, generator, and next queue
|
||||||
|
*/
|
||||||
|
GameBoard(int boardWidth, int boardHeight, const std::vector<Piece>& bag, int nextQueueLength);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try moving the piece one cell to the left, and returns wheter it was sucessfull
|
||||||
|
*/
|
||||||
|
bool moveLeft();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try moving the piece one cell to the right, and returns wheter it was sucessfull
|
||||||
|
*/
|
||||||
|
bool moveRight();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try moving the piece one cell down, and returns wheter it was sucessfull
|
||||||
|
*/
|
||||||
|
bool moveDown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try rotating the piece and kicking it if necessary, and returns wheter it was sucessfull
|
||||||
|
*/
|
||||||
|
bool rotate(Rotation rotation);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Try kicking the piece, testing position either above or below the piece's initial position
|
||||||
|
*/
|
||||||
|
bool tryKicking(bool testingBottom, const std::set<Cell>& safeCells);
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Try holding the active piece or swapping it if one was already stocked, while trying to apply an initial rotation to the newly spawned piece,
|
||||||
|
* and returns wheter it was sucessfull
|
||||||
|
*/
|
||||||
|
bool hold(Rotation initialRotation = NONE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawns the next piece from the queue, and returns wheter it spawns in a wall
|
||||||
|
*/
|
||||||
|
bool spawnNextPiece();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns wheter the active piece is touching walls directly below it
|
||||||
|
*/
|
||||||
|
bool touchesGround();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock the active piece into the board and returns the resulting line clear
|
||||||
|
*/
|
||||||
|
LineClear lockPiece();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the board
|
||||||
|
*/
|
||||||
|
Board getBoard() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the active piece
|
||||||
|
*/
|
||||||
|
Piece getActivePiece() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the position of the active piece
|
||||||
|
*/
|
||||||
|
Cell getActivePiecePosition() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the held piece
|
||||||
|
*/
|
||||||
|
Piece getHeldPiece() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the next piece queue
|
||||||
|
*/
|
||||||
|
std::vector<Piece> getNextPieces() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Returns wheter the translated active piece is in a wall
|
||||||
|
*/
|
||||||
|
bool isActivePieceInWall(const Cell& shift = Cell{0, 0}) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns wheter the translated active piece overlaps with at least one of the cells
|
||||||
|
*/
|
||||||
|
bool activePieceOverlapsOneCell(const std::set<Cell>& safeCells, const Cell& shift = Cell{0, 0}) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active piece to its spawn position
|
||||||
|
*/
|
||||||
|
void goToSpawnPosition();
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Stream output operator, adds the board, the hold box and the next queue
|
||||||
|
*/
|
||||||
|
friend std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard);
|
||||||
|
};
|
||||||
237
src/Core/GameParameters.cpp
Normal file
237
src/Core/GameParameters.cpp
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
#include "GameParameters.h"
|
||||||
|
|
||||||
|
#include "Gamemode.h"
|
||||||
|
#include "Player.h"
|
||||||
|
|
||||||
|
|
||||||
|
GameParameters::GameParameters(Gamemode gamemode, const Player& controls) : gamemode(gamemode), controls(controls) {
|
||||||
|
// initialize lines and level
|
||||||
|
this->clearedLines = 0;
|
||||||
|
switch (this->gamemode) {
|
||||||
|
// lowest gravity
|
||||||
|
case SPRINT : {this->level = 1; break;}
|
||||||
|
// lowest gravity
|
||||||
|
case ULTRA : {this->level = 1; break;}
|
||||||
|
// goes from level 1 to 20
|
||||||
|
case MARATHON : {this->level = 1; break;}
|
||||||
|
// goes from level 20 to 39
|
||||||
|
case MASTER : {this->level = 20; break;}
|
||||||
|
default : this->level = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize stats
|
||||||
|
this->updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameParameters::clearLines(int lineNumber) {
|
||||||
|
// update lines and level
|
||||||
|
switch (this->gamemode) {
|
||||||
|
// modes where level increases
|
||||||
|
case MARATHON :
|
||||||
|
case MASTER : {
|
||||||
|
// update cleared lines
|
||||||
|
int previousLines = this->clearedLines;
|
||||||
|
this->clearedLines += lineNumber;
|
||||||
|
|
||||||
|
// level increments every 10 lines, stats only changes on level up
|
||||||
|
if (previousLines / 10 < this->clearedLines / 10) {
|
||||||
|
this->level = this->clearedLines / 10;
|
||||||
|
this->updateStats();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// other modes
|
||||||
|
default : this->clearedLines += lineNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameParameters::hasWon(int framesPassed) {
|
||||||
|
switch (this->gamemode) {
|
||||||
|
// win once 40 lines have been cleared
|
||||||
|
case SPRINT : return this->clearedLines >= 40;
|
||||||
|
// win once 2mn have passed
|
||||||
|
case ULTRA : return (framesPassed / 60) >= 120;
|
||||||
|
// win once 200 lines have been cleared
|
||||||
|
case MARATHON : return this->clearedLines >= 200;
|
||||||
|
// win once 200 lines have been cleared
|
||||||
|
case MASTER : return this->clearedLines >= 200;
|
||||||
|
default : return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameParameters::updateStats() {
|
||||||
|
/* NEXT QUEUE */
|
||||||
|
switch (this->gamemode) {
|
||||||
|
// 5 for rapidity gamemodes
|
||||||
|
case SPRINT :
|
||||||
|
case ULTRA : {
|
||||||
|
this->nextQueueLength = 5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 3 for endurance gamemodes
|
||||||
|
case MARATHON :
|
||||||
|
case MASTER : {
|
||||||
|
this->nextQueueLength = 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default : this->nextQueueLength = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BONE BLOCKS */
|
||||||
|
switch (this->gamemode) {
|
||||||
|
// blocks turns into bone blocks at level 30
|
||||||
|
case MASTER : this->boneBlocks = (this->level >= 30);
|
||||||
|
default : this->boneBlocks = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GRAVITY */
|
||||||
|
if (level >= 20) {
|
||||||
|
// all levels above 20 are instant gravity
|
||||||
|
this->gravity = 20 * 60;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// get gravity for an assumed 20-rows board
|
||||||
|
switch (this->level) {
|
||||||
|
case 1 : {this->gravity = 1; break;} // 60f/line, 20s total
|
||||||
|
case 2 : {this->gravity = 2; break;} // 30f/line, 10s total
|
||||||
|
case 3 : {this->gravity = 3; break;} // 20f/line, 6.66s total
|
||||||
|
case 4 : {this->gravity = 4; break;} // 15f/line, 5s total
|
||||||
|
case 5 : {this->gravity = 5; break;} // 12f/line, 4s total
|
||||||
|
case 6 : {this->gravity = 6; break;} // 10f/line, 3.33 total
|
||||||
|
case 7 : {this->gravity = 7; break;} // 8.57f/line, 2.85s total
|
||||||
|
case 8 : {this->gravity = 8; break;} // 7.5f/line, 2.5s total
|
||||||
|
case 9 : {this->gravity = 10; break;} // 6f/line, 2s total
|
||||||
|
case 10 : {this->gravity = 12; break;} // 5f/line, 1.66s total
|
||||||
|
case 11 : {this->gravity = 14; break;} // 4.28f/line, 1.42s total
|
||||||
|
case 12 : {this->gravity = 17; break;} // 3.52f/line, 1.17s total
|
||||||
|
case 13 : {this->gravity = 20; break;} // 3f/line, 60f total
|
||||||
|
case 14 : {this->gravity = 24; break;} // 2.5f/line, 50f total
|
||||||
|
case 15 : {this->gravity = 30; break;} // 2f/line, 40f total
|
||||||
|
case 16 : {this->gravity = 40; break;} // 1.5f/line, 30f total
|
||||||
|
case 17 : {this->gravity = 1 * 60; break;} // 1line/f, 20f total
|
||||||
|
case 18 : {this->gravity = 2 * 60; break;} // 2line/f, 10f total
|
||||||
|
case 19 : {this->gravity = 4 * 60; break;} // 4line/f, 5f total
|
||||||
|
default : this->gravity = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LOCK DELAY */
|
||||||
|
switch (this->gamemode) {
|
||||||
|
// starts at 500ms (30f) at lvl 20 and ends at 183ms (11f) at lvl 39
|
||||||
|
case MASTER : {this->lockDelay = 30 - (this->level - 20); break;}
|
||||||
|
// 1s by default
|
||||||
|
default : this->lockDelay = 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FORCED LOCK DELAY */
|
||||||
|
this->forcedLockDelay = this->lockDelay * 10;
|
||||||
|
|
||||||
|
/* ARE */
|
||||||
|
switch (this->gamemode) {
|
||||||
|
// starts at 400ms (24f) at lvl 1 and ends at 083ms (5f) at lvl 20
|
||||||
|
case MARATHON : {this->ARE = 24 - (this->level - 1); break;}
|
||||||
|
// starts at 400ms (24f) at lvl 20 and ends at 083ms (5f) at lvl 39
|
||||||
|
case MASTER : {this->ARE = 24 - (this->level - 20); break;}
|
||||||
|
// no ARE by default
|
||||||
|
default : this->ARE = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LINE ARE */
|
||||||
|
this->lineARE = this->ARE * 2;
|
||||||
|
|
||||||
|
/* DAS */
|
||||||
|
this->DAS = this->controls.getDAS();
|
||||||
|
switch (this->gamemode) {
|
||||||
|
// for modes with reduced lock delay, ensure DAS is lower than lock delay, but at least 1
|
||||||
|
case MASTER : {
|
||||||
|
if (this->lockDelay <= this->DAS) {
|
||||||
|
this->DAS = this->lockDelay - 6; // give 6f (100ms) to change directions
|
||||||
|
}
|
||||||
|
if (this->DAS < 1) {
|
||||||
|
this->DAS = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// modes with no reduced lock delay
|
||||||
|
default : break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ARR */
|
||||||
|
this->ARR = this->controls.getARR();
|
||||||
|
switch (this->gamemode) {
|
||||||
|
// for modes with reduced lock delay, ensure ARR is lower than DAS, but not lower than 1
|
||||||
|
case MASTER : {
|
||||||
|
if (this->DAS <= this->ARR) {
|
||||||
|
this->ARR = this->DAS - 1;
|
||||||
|
}
|
||||||
|
if (this->ARR < 1) {
|
||||||
|
this->ARR = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// modes with no reduced lock delay
|
||||||
|
default : break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SDR */
|
||||||
|
this->SDR = this->controls.getSDR();
|
||||||
|
switch (this->gamemode) {
|
||||||
|
// modes where we don't want instant soft drop to be possible
|
||||||
|
case MARATHON : {
|
||||||
|
if (this->SDR < 1) {
|
||||||
|
this->SDR = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// modes where we don't care
|
||||||
|
default : break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameParameters::getClearedLines() {
|
||||||
|
return this->clearedLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameParameters::getLevel() {
|
||||||
|
return this->level;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameParameters::getNextQueueLength() {
|
||||||
|
return this->nextQueueLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GameParameters::getBoneBlocks() {
|
||||||
|
return this->boneBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameParameters::getGravity() {
|
||||||
|
return this->gravity;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameParameters::getLockDelay() {
|
||||||
|
return this->lockDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameParameters::getForcedLockDelay() {
|
||||||
|
return this->forcedLockDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameParameters::getARE() {
|
||||||
|
return this->ARE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameParameters::getLineARE() {
|
||||||
|
return this->lineARE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameParameters::getDAS() {
|
||||||
|
return this->DAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameParameters::getARR() {
|
||||||
|
return this->ARR;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GameParameters::getSDR() {
|
||||||
|
return this->SDR;
|
||||||
|
}
|
||||||
110
src/Core/GameParameters.h
Normal file
110
src/Core/GameParameters.h
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Gamemode.h"
|
||||||
|
#include "Player.h"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the parameters of the game depending on the current gamemode and the player's controls,
|
||||||
|
* this is basically where all the hard-coded values are stored
|
||||||
|
*/
|
||||||
|
class GameParameters {
|
||||||
|
private:
|
||||||
|
Gamemode gamemode; // the current gamemode
|
||||||
|
Player controls; // the player's controls
|
||||||
|
int clearedLines; // the number of cleared lines
|
||||||
|
int level; // the current level
|
||||||
|
int nextQueueLength; // the number of pieces visibles in the next queue
|
||||||
|
bool boneBlocks; // wheter all blocks are bone blocks
|
||||||
|
int gravity; // the gravity at which pieces drop
|
||||||
|
int lockDelay; // the time before the piece lock in place
|
||||||
|
int forcedLockDelay; // the forced time before the piece lock in place
|
||||||
|
int ARE; // the time before the next piece spawn
|
||||||
|
int lineARE; // the time before the next piece spawn, after clearing a line
|
||||||
|
int DAS; // the time before the piece repeats moving
|
||||||
|
int ARR; // the rate at which the piece repeats moving
|
||||||
|
int SDR; // the rate at which the piece soft drops
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Sets the current gamemode and the player's controls
|
||||||
|
*/
|
||||||
|
GameParameters(Gamemode gamemode, const Player& controls);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the newly cleared lines and update level and stats if needed
|
||||||
|
*/
|
||||||
|
void clearLines(int lineNumber);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns wheter the game ended
|
||||||
|
*/
|
||||||
|
bool hasWon(int framesPassed);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Updates all the parameters
|
||||||
|
*/
|
||||||
|
void updateStats();
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Returns the current number of cleared line
|
||||||
|
*/
|
||||||
|
int getClearedLines();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current level
|
||||||
|
*/
|
||||||
|
int getLevel();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the length of the next queue
|
||||||
|
*/
|
||||||
|
int getNextQueueLength();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns wheter the blocks are currently bone blocks
|
||||||
|
*/
|
||||||
|
bool getBoneBlocks();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current gravity for a 20-line high board
|
||||||
|
*/
|
||||||
|
int getGravity();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current lock delay
|
||||||
|
*/
|
||||||
|
int getLockDelay();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current forced lock delay
|
||||||
|
*/
|
||||||
|
int getForcedLockDelay();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current ARE
|
||||||
|
*/
|
||||||
|
int getARE();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current line ARE
|
||||||
|
*/
|
||||||
|
int getLineARE();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current DAS
|
||||||
|
*/
|
||||||
|
int getDAS();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current ARR
|
||||||
|
*/
|
||||||
|
int getARR();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current SDR
|
||||||
|
*/
|
||||||
|
int getSDR();
|
||||||
|
};
|
||||||
12
src/Core/Gamemode.h
Normal file
12
src/Core/Gamemode.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every gamemode supported by the game
|
||||||
|
*/
|
||||||
|
enum Gamemode {
|
||||||
|
SPRINT,
|
||||||
|
MARATHON,
|
||||||
|
ULTRA,
|
||||||
|
MASTER
|
||||||
|
};
|
||||||
11
src/Core/LineClear.h
Normal file
11
src/Core/LineClear.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify how many lines were cleared and how
|
||||||
|
*/
|
||||||
|
struct LineClear {
|
||||||
|
int lines; // the number of lines cleared
|
||||||
|
bool isSpin; // if the move was a spin
|
||||||
|
bool isMiniSpin; // if the move was a spin mini
|
||||||
|
};
|
||||||
49
src/Core/Player.cpp
Normal file
49
src/Core/Player.cpp
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#include "Player.h"
|
||||||
|
|
||||||
|
static const int DAS_MIN_VALUE = 0;
|
||||||
|
static const int DAS_MAX_VALUE = 30;
|
||||||
|
static const int ARR_MIN_VALUE = 0;
|
||||||
|
static const int ARR_MAX_VALUE = 30;
|
||||||
|
static const int SDR_MIN_VALUE = 0;
|
||||||
|
static const int SDR_MAX_VALUE = 6;
|
||||||
|
|
||||||
|
|
||||||
|
Player::Player() {
|
||||||
|
// default settings
|
||||||
|
this->DAS = 15; // 250ms
|
||||||
|
this->ARR = 3; // 50ms
|
||||||
|
this->SDR = 2; // 33ms
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Player::setDAS(int DAS) {
|
||||||
|
if (DAS < DAS_MIN_VALUE || DAS > DAS_MAX_VALUE) return false;
|
||||||
|
|
||||||
|
this->DAS = DAS;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Player::setARR(int ARR) {
|
||||||
|
if (ARR < ARR_MIN_VALUE || ARR > ARR_MAX_VALUE) return false;
|
||||||
|
|
||||||
|
this->ARR = ARR;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Player::setSDR(int SDR) {
|
||||||
|
if (SDR < SDR_MIN_VALUE || SDR > SDR_MAX_VALUE) return false;
|
||||||
|
|
||||||
|
this->SDR = SDR;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Player::getDAS() {
|
||||||
|
return this->DAS;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Player::getARR() {
|
||||||
|
return this->ARR;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Player::getSDR() {
|
||||||
|
return this->SDR;
|
||||||
|
}
|
||||||
48
src/Core/Player.h
Normal file
48
src/Core/Player.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The controls of a player
|
||||||
|
*/
|
||||||
|
class Player {
|
||||||
|
private:
|
||||||
|
int DAS; // Delayed Auto-Shift, goes from 0 (instant) to 30 (500ms)
|
||||||
|
int ARR; // Auto-Repeat Rate, goes from 0 (instant) to 30 (500ms)
|
||||||
|
int SDR; // Soft Drop Rate, goes from 0 (instant) to 6 (100ms)
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Sets default controls
|
||||||
|
*/
|
||||||
|
Player();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try setting DAS to the desired value, and returns wheter it is possible
|
||||||
|
*/
|
||||||
|
bool setDAS(int DAS);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try setting ARR to the desired value, and returns wheter it is possible
|
||||||
|
*/
|
||||||
|
bool setARR(int ARR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try setting SDR to the desired value, and returns wheter it is possible
|
||||||
|
*/
|
||||||
|
bool setSDR(int SDR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns DAS value
|
||||||
|
*/
|
||||||
|
int getDAS();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns ARR value
|
||||||
|
*/
|
||||||
|
int getARR();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns SDR value
|
||||||
|
*/
|
||||||
|
int getSDR();
|
||||||
|
};
|
||||||
165
src/Core/main.cpp
Normal file
165
src/Core/main.cpp
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#include "../Pieces/PiecesFiles.h"
|
||||||
|
#include "../Pieces/Generator.h"
|
||||||
|
#include "GameBoard.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <string>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
|
||||||
|
void testGeneratorForAllSizes(int amount);
|
||||||
|
void testGeneratorForOneSize(int size);
|
||||||
|
void testGeneratorByprintingAllNminos(int n);
|
||||||
|
void testStoringAndRetrievingPieces(int size);
|
||||||
|
void generateFilesForAllSizes(int amount);
|
||||||
|
void readStatsFromFilesForAllSizes(int amount);
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
std::srand(std::time(NULL));
|
||||||
|
|
||||||
|
using std::chrono::high_resolution_clock;
|
||||||
|
using std::chrono::duration_cast;
|
||||||
|
using std::chrono::duration;
|
||||||
|
using std::chrono::milliseconds;
|
||||||
|
|
||||||
|
PiecesFiles pf;
|
||||||
|
std::vector<Piece> pieces;
|
||||||
|
std::vector<int> convexPieces;
|
||||||
|
std::vector<int> holelessPieces;
|
||||||
|
std::vector<int> otherPieces;
|
||||||
|
pf.loadPieces(13, pieces, convexPieces, holelessPieces, otherPieces);
|
||||||
|
|
||||||
|
auto t1 = high_resolution_clock::now();
|
||||||
|
Bag bg(pieces);
|
||||||
|
auto t2 = high_resolution_clock::now();
|
||||||
|
duration<double, std::milli> ms_double = t2 - t1;
|
||||||
|
std::cout << ms_double.count() << "ms" << std::endl;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void testGeneratorForAllSizes(int amount) {
|
||||||
|
using std::chrono::high_resolution_clock;
|
||||||
|
using std::chrono::duration_cast;
|
||||||
|
using std::chrono::duration;
|
||||||
|
using std::chrono::milliseconds;
|
||||||
|
Generator generator;
|
||||||
|
|
||||||
|
for (int i = 1; i <= amount; i++) {
|
||||||
|
auto t1 = high_resolution_clock::now();
|
||||||
|
std::vector<Polyomino> n_minos = generator.generatePolyominos(i);
|
||||||
|
auto t2 = high_resolution_clock::now();
|
||||||
|
|
||||||
|
duration<double, std::milli> ms_double = t2 - t1;
|
||||||
|
std::cout << "generated " << n_minos.size() << " polyominos of size " << i << " in " << ms_double.count() << "ms" << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void testGeneratorForOneSize(int size) {
|
||||||
|
using std::chrono::high_resolution_clock;
|
||||||
|
using std::chrono::duration_cast;
|
||||||
|
using std::chrono::duration;
|
||||||
|
using std::chrono::milliseconds;
|
||||||
|
Generator generator;
|
||||||
|
|
||||||
|
std::cout << "Generating " << size << "-minos" << std::endl;
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
auto t1 = high_resolution_clock::now();
|
||||||
|
std::vector<Polyomino> n_minos = generator.generatePolyominos(size);
|
||||||
|
auto t2 = high_resolution_clock::now();
|
||||||
|
|
||||||
|
duration<double, std::milli> ms_double = t2 - t1;
|
||||||
|
std::cout << ms_double.count() << "ms" << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void testGeneratorByprintingAllNminos(int n) {
|
||||||
|
Generator generator;
|
||||||
|
std::vector<Polyomino> n_minos = generator.generatePolyominos(n);
|
||||||
|
|
||||||
|
for (Polyomino& n_mino : n_minos) {
|
||||||
|
n_mino.goToSpawnPosition();
|
||||||
|
}
|
||||||
|
std::sort(n_minos.begin(), n_minos.end());
|
||||||
|
|
||||||
|
for (Polyomino& n_mino : n_minos) {
|
||||||
|
|
||||||
|
std::cout << n_mino << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void testStoringAndRetrievingPieces(int size) {
|
||||||
|
PiecesFiles piecesFiles;
|
||||||
|
piecesFiles.savePieces(size);
|
||||||
|
|
||||||
|
std::vector<Piece> pieces;
|
||||||
|
std::vector<int> convexPieces;
|
||||||
|
std::vector<int> holelessPieces;
|
||||||
|
std::vector<int> otherPieces;
|
||||||
|
piecesFiles.loadPieces(size, pieces, convexPieces, holelessPieces, otherPieces);
|
||||||
|
|
||||||
|
std::cout << "Convex " << size << "-minos:" << std::endl;
|
||||||
|
for (int index : convexPieces) {
|
||||||
|
std::cout << pieces.at(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Holeless " << size << "-minos:" << std::endl;
|
||||||
|
for (int index : holelessPieces) {
|
||||||
|
std::cout << pieces.at(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Others " << size << "-minos:" << std::endl;
|
||||||
|
for (int index : otherPieces) {
|
||||||
|
std::cout << pieces.at(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void generateFilesForAllSizes(int amount) {
|
||||||
|
using std::chrono::high_resolution_clock;
|
||||||
|
using std::chrono::duration_cast;
|
||||||
|
using std::chrono::duration;
|
||||||
|
using std::chrono::milliseconds;
|
||||||
|
|
||||||
|
PiecesFiles piecesFiles;
|
||||||
|
for (int i = 1; i <= amount; i++) {
|
||||||
|
auto t1 = high_resolution_clock::now();
|
||||||
|
piecesFiles.savePieces(i);
|
||||||
|
auto t2 = high_resolution_clock::now();
|
||||||
|
|
||||||
|
duration<double, std::milli> ms_double = t2 - t1;
|
||||||
|
std::cout << "Generated pieces files for size " << i << " in " << ms_double.count() << "ms" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 1; i <= amount; i++) {
|
||||||
|
auto t1 = high_resolution_clock::now();
|
||||||
|
std::vector<Piece> pieces;
|
||||||
|
std::vector<int> convexPieces;
|
||||||
|
std::vector<int> holelessPieces;
|
||||||
|
std::vector<int> otherPieces;
|
||||||
|
piecesFiles.loadPieces(i, pieces, convexPieces, holelessPieces, otherPieces);
|
||||||
|
auto t2 = high_resolution_clock::now();
|
||||||
|
|
||||||
|
duration<double, std::milli> ms_double = t2 - t1;
|
||||||
|
std::cout << "Read pieces from files for size " << i << " in " << ms_double.count() << "ms" << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void readStatsFromFilesForAllSizes(int amount) {
|
||||||
|
PiecesFiles piecesFiles;
|
||||||
|
for (int i = 1; i <= amount; i++) {
|
||||||
|
std::vector<Piece> pieces;
|
||||||
|
std::vector<int> convexPieces;
|
||||||
|
std::vector<int> holelessPieces;
|
||||||
|
std::vector<int> otherPieces;
|
||||||
|
piecesFiles.loadPieces(i, pieces, convexPieces, holelessPieces, otherPieces);
|
||||||
|
|
||||||
|
std::cout << i << "-minos : " << pieces.size() << std::endl;
|
||||||
|
std::cout << "Convex " << i << "-minos : " << convexPieces.size() << std::endl;
|
||||||
|
std::cout << "Holeless " << i << "-minos : " << holelessPieces.size() << std::endl;
|
||||||
|
std::cout << "Others " << i << "-minos : " << otherPieces.size() << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
66
src/Pieces/Cell.h
Normal file
66
src/Pieces/Cell.h
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cell on a 2D grid
|
||||||
|
*/
|
||||||
|
struct Cell {
|
||||||
|
int x; // x position
|
||||||
|
int y; // y position
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Addition operator, returns the sums of the coordinates of both cells
|
||||||
|
*/
|
||||||
|
Cell operator+(const Cell& left, const Cell& right) {
|
||||||
|
return Cell{left.x + right.x, left.y + right.y};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additive assignation operator, adds the coordinates of the right cell to the left one
|
||||||
|
*/
|
||||||
|
Cell& operator+=(Cell& left, const Cell& right) {
|
||||||
|
left = left + right;
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Substraction operator, returns the difference of the coordinate between the left and right cell
|
||||||
|
*/
|
||||||
|
Cell operator-(const Cell& left, const Cell& right) {
|
||||||
|
return Cell{left.x - right.x, left.y - right.y};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Substractive assignation operator, substract the coordinates of the right cell from the left one
|
||||||
|
*/
|
||||||
|
Cell& operator-=(Cell& left, const Cell& right) {
|
||||||
|
left = left - right;
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strict inferiority operator, a cell is inferior to another if it is lower or at the same height and more to the left
|
||||||
|
*/
|
||||||
|
bool operator<(const Cell& left, const Cell& right) {
|
||||||
|
return (left.x == right.x) ? (left.y < right.y) : (left.x < right.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equality operator, two cells are equal if they have the same coordinates
|
||||||
|
*/
|
||||||
|
bool operator==(const Cell& left, const Cell& right) {
|
||||||
|
return (left.x == right.x) && (left.y == right.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream output operator, adds the coordinates of the cell to the stream
|
||||||
|
*/
|
||||||
|
std::ostream& operator<<(std::ostream& os, const Cell& cell) {
|
||||||
|
os << "x: " << cell.x << " y: " << cell.y;
|
||||||
|
return os;
|
||||||
|
}
|
||||||
40
src/Pieces/Color.h
Normal file
40
src/Pieces/Color.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <String>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every possible colors a block can take
|
||||||
|
*/
|
||||||
|
enum Color {
|
||||||
|
NOTHING,
|
||||||
|
OUT_OF_BOUND,
|
||||||
|
GARBAGE,
|
||||||
|
PURPLE,
|
||||||
|
ORANGE,
|
||||||
|
CYAN,
|
||||||
|
PINK,
|
||||||
|
YELLOW,
|
||||||
|
RED,
|
||||||
|
BLUE,
|
||||||
|
GREEN
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
static const Color FIRST_PIECE_COLOR = PURPLE; // the first color a piece can be
|
||||||
|
static const Color LAST_PIECE_COLOR = GREEN; // the last color a piece can be
|
||||||
|
|
||||||
|
static const std::string COLOR_RESET = "\033[38;2;255;255;255m"; // color code to reset the console color
|
||||||
|
static const std::string COLOR_CODES[] = { // color codes to change the console color
|
||||||
|
COLOR_RESET, // NOTHING
|
||||||
|
COLOR_RESET, // OUT_OF_BOUND
|
||||||
|
"\033[38;2;150;150;150m", // GARBAGE
|
||||||
|
"\033[38;2;150;0;255m", // PURPLE
|
||||||
|
"\033[38;2;255;150;0m", // ORANGE
|
||||||
|
"\033[38;2;0;255;255m", // CYAN
|
||||||
|
"\033[38;2;255;0;200m", // PINK
|
||||||
|
"\033[38;2;255;255;0m", // YELLOW
|
||||||
|
"\033[38;2;255;0;0m", // RED
|
||||||
|
"\033[38;2;0;100;255m", // BLUE
|
||||||
|
"\033[38;2;0;255;0m" // GREEN
|
||||||
|
};
|
||||||
85
src/Pieces/Generator.cpp
Normal file
85
src/Pieces/Generator.cpp
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#include "Generator.h"
|
||||||
|
|
||||||
|
#include "Polyomino.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
#include <Set>
|
||||||
|
#include <Map>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
|
||||||
|
Generator::Generator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Polyomino> Generator::generatePolyominos(unsigned int order) {
|
||||||
|
// initialization
|
||||||
|
this->validPolyominos.clear();
|
||||||
|
this->currentTestedShape.clear();
|
||||||
|
|
||||||
|
// no polyomino with 0 cells
|
||||||
|
if (order == 0) return this->validPolyominos;
|
||||||
|
|
||||||
|
// start generating from the monomino
|
||||||
|
this->currentTestedShape.insert(Cell{0, 0});
|
||||||
|
|
||||||
|
// generate polyominos
|
||||||
|
std::map<Cell, int> candidateCells;
|
||||||
|
this->generate(order, 0, 1, candidateCells);
|
||||||
|
return this->validPolyominos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Generator::generate(unsigned int order, int lastAddedCellNumber, int nextAvaibleNumber, std::map<Cell, int> candidateCells) {
|
||||||
|
// recursion stop
|
||||||
|
if (order == this->currentTestedShape.size()) {
|
||||||
|
// we test the polyomino formed by the current shape
|
||||||
|
Polyomino candidate(this->currentTestedShape);
|
||||||
|
|
||||||
|
// we sort the rotations of the polyominos
|
||||||
|
std::vector<Polyomino> candidateRotations;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
candidate.normalize();
|
||||||
|
candidateRotations.push_back(candidate);
|
||||||
|
if (!(i==3)) candidate.rotateCW();
|
||||||
|
}
|
||||||
|
std::sort(candidateRotations.begin(), candidateRotations.end());
|
||||||
|
|
||||||
|
// we keep the polyomino only if it was generated in its lowest rotation
|
||||||
|
if (candidate == candidateRotations.at(0)) {
|
||||||
|
this->validPolyominos.push_back(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate the list of candidate cells
|
||||||
|
for (Cell cell : this->currentTestedShape) {
|
||||||
|
this->tryToAddCandidateCell(Cell{cell.x, cell.y + 1}, nextAvaibleNumber, candidateCells);
|
||||||
|
this->tryToAddCandidateCell(Cell{cell.x + 1, cell.y}, nextAvaibleNumber, candidateCells);
|
||||||
|
this->tryToAddCandidateCell(Cell{cell.x, cell.y - 1}, nextAvaibleNumber, candidateCells);
|
||||||
|
this->tryToAddCandidateCell(Cell{cell.x - 1, cell.y}, nextAvaibleNumber, candidateCells);
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate polyominos for all cells with a higher number than the last one
|
||||||
|
for (auto [key, val] : candidateCells) {
|
||||||
|
if (val > lastAddedCellNumber) {
|
||||||
|
this->currentTestedShape.insert(key);
|
||||||
|
this->generate(order, val, nextAvaibleNumber, (order == this->currentTestedShape.size()) ? std::map<Cell, int>() : candidateCells);
|
||||||
|
this->currentTestedShape.erase(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Generator::tryToAddCandidateCell(const Cell& candidate, int& nextAvaibleNumber, std::map<Cell, int>& candidateCells) {
|
||||||
|
// we declared the first cell as the lower-left square, since we always start with a monomino at (0,0) we can test with hard values
|
||||||
|
if (candidate.y < 0 || (candidate.y == 0 && candidate.x < 0)) return;
|
||||||
|
|
||||||
|
// if the cell was already marked then we should not mark it again
|
||||||
|
if (candidateCells.contains(candidate)) return;
|
||||||
|
|
||||||
|
// if the candidate overlaps with the shape there is no reason to add it
|
||||||
|
if (this->currentTestedShape.contains(candidate)) return;
|
||||||
|
|
||||||
|
// once all tests passed we can add the cell
|
||||||
|
candidateCells.insert({candidate, nextAvaibleNumber});
|
||||||
|
nextAvaibleNumber++;
|
||||||
|
}
|
||||||
39
src/Pieces/Generator.h
Normal file
39
src/Pieces/Generator.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Polyomino.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
#include <Set>
|
||||||
|
#include <Map>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generator of one-sided polyominos of any size
|
||||||
|
*/
|
||||||
|
class Generator {
|
||||||
|
private:
|
||||||
|
std::vector<Polyomino> validPolyominos; // the list of already generated polyominos
|
||||||
|
std::set<Cell> currentTestedShape; // the polyomino being created
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Initializes generator
|
||||||
|
*/
|
||||||
|
Generator();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of all one-sided polyominos of the specified size
|
||||||
|
*/
|
||||||
|
std::vector<Polyomino> generatePolyominos(unsigned int order);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Generates all one-sided polyominos of the specified using the current tested shape
|
||||||
|
*/
|
||||||
|
void generate(unsigned int order, int lastAddedCellNumber, int nextAvaibleNumber, std::map<Cell, int> candidateCells);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check wheter a candidate cell can be added to the current tested shape
|
||||||
|
*/
|
||||||
|
void tryToAddCandidateCell(const Cell& candidate, int& nextAvaibleNumber, std::map<Cell, int>& candidateCells);
|
||||||
|
};
|
||||||
38
src/Pieces/Piece.cpp
Normal file
38
src/Pieces/Piece.cpp
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#include "Piece.h"
|
||||||
|
|
||||||
|
#include "Polyomino.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
#include "Color.h"
|
||||||
|
|
||||||
|
#include <Set>
|
||||||
|
#include <String>
|
||||||
|
|
||||||
|
|
||||||
|
Piece::Piece(const Polyomino& polyomino, Color color) : polyomino(polyomino), color(color) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Piece::rotate(Rotation rotation) {
|
||||||
|
if (rotation == CLOCKWISE)
|
||||||
|
this->polyomino.rotateCW();
|
||||||
|
if (rotation == DOUBLE)
|
||||||
|
this->polyomino.rotate180();
|
||||||
|
if (rotation == COUNTERCLOCKWISE)
|
||||||
|
this->polyomino.rotateCCW();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<Cell> Piece::getPositions() const {
|
||||||
|
return this->polyomino.getCells();
|
||||||
|
}
|
||||||
|
|
||||||
|
int Piece::getLength() const {
|
||||||
|
return this->polyomino.getLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
Color Piece::getColor() const {
|
||||||
|
return this->color;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& os, const Piece& piece) {
|
||||||
|
os << COLOR_CODES[piece.color] << piece.polyomino << COLOR_RESET;
|
||||||
|
return os;
|
||||||
|
}
|
||||||
48
src/Pieces/Piece.h
Normal file
48
src/Pieces/Piece.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Polyomino.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
#include "Color.h"
|
||||||
|
|
||||||
|
#include <Set>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstraction of a polyomino as a playable piece in a stacker game
|
||||||
|
*/
|
||||||
|
class Piece {
|
||||||
|
private:
|
||||||
|
Polyomino polyomino; // a polyomino representing the piece, (0, 0) is downleft
|
||||||
|
Color color; // the color of the piece
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Creates a piece with a specified shape and color
|
||||||
|
*/
|
||||||
|
Piece(const Polyomino& piece, Color color);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the piece in the specified direction
|
||||||
|
*/
|
||||||
|
void rotate(Rotation rotation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy of the list of cells of the piece
|
||||||
|
*/
|
||||||
|
std::set<Cell> getPositions() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the length of the piece
|
||||||
|
*/
|
||||||
|
int getLength() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the color of the piece
|
||||||
|
*/
|
||||||
|
Color getColor() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream output operator, adds a 2D grid representing the piece
|
||||||
|
*/
|
||||||
|
friend std::ostream& operator<<(std::ostream& os, const Piece& piece);
|
||||||
|
};
|
||||||
141
src/Pieces/PiecesFiles.cpp
Normal file
141
src/Pieces/PiecesFiles.cpp
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
#include "PiecesFiles.h"
|
||||||
|
|
||||||
|
#include "Generator.h"
|
||||||
|
#include "Piece.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
#include <String>
|
||||||
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
|
||||||
|
PiecesFiles::PiecesFiles() {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PiecesFiles::savePieces(int order) const {
|
||||||
|
// open pieces file
|
||||||
|
std::string filePath;
|
||||||
|
if (!this->getFilePath(order, filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::ofstream piecesFile(filePath, std::ios::trunc | std::ios::binary);
|
||||||
|
if (!piecesFile.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generates the polyominos
|
||||||
|
Generator generator;
|
||||||
|
std::vector<Polyomino> nMinos = generator.generatePolyominos(order);
|
||||||
|
|
||||||
|
// set the polyominos to their spawn position
|
||||||
|
for (Polyomino& nMino : nMinos) {
|
||||||
|
nMino.goToSpawnPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort the polyominos, is done after setting spawn position to ensure the order is always the same
|
||||||
|
std::sort(nMinos.begin(), nMinos.end());
|
||||||
|
|
||||||
|
// write pieces
|
||||||
|
Color pieceColor = Color(FIRST_PIECE_COLOR + (order % (LAST_PIECE_COLOR - FIRST_PIECE_COLOR + 1)));
|
||||||
|
for (const Polyomino& nMino : nMinos) {
|
||||||
|
// write polyomino length
|
||||||
|
char lengthByte = nMino.getLength();
|
||||||
|
piecesFile.write(&lengthByte, 1);
|
||||||
|
|
||||||
|
// write the type and color of the piece
|
||||||
|
char infoByte = (nMino.isConvex() << 7) + (nMino.hasHole() << 6) + pieceColor;
|
||||||
|
pieceColor = (pieceColor == LAST_PIECE_COLOR) ? FIRST_PIECE_COLOR : Color(pieceColor + 1);
|
||||||
|
piecesFile.write(&infoByte, 1);
|
||||||
|
|
||||||
|
// write the cells of the piece
|
||||||
|
char cellByte;
|
||||||
|
for (Cell cell : nMino.getCells()) {
|
||||||
|
cellByte = (cell.x << 4) + cell.y;
|
||||||
|
piecesFile.write(&cellByte, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PiecesFiles::loadPieces(int order, std::vector<Piece>& pieces, std::vector<int>& convexPieces, std::vector<int>& holelessPieces, std::vector<int>& otherPieces) const {
|
||||||
|
// open pieces file
|
||||||
|
std::string filePath;
|
||||||
|
if (!this->getFilePath(order, filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::ifstream piecesFile(filePath, std::ios::binary);
|
||||||
|
if (!piecesFile.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get empty vectors
|
||||||
|
pieces.clear();
|
||||||
|
convexPieces.clear();
|
||||||
|
holelessPieces.clear();
|
||||||
|
otherPieces.clear();
|
||||||
|
|
||||||
|
// set up masks
|
||||||
|
char convexMask = 0b1000'0000;
|
||||||
|
char holeMask = 0b0100'0000;
|
||||||
|
char colorMask = 0b0011'1111;
|
||||||
|
char xMask = 0b1111'0000;
|
||||||
|
char yMask = 0b0000'1111;
|
||||||
|
|
||||||
|
// read the pieces
|
||||||
|
char lengthByte;
|
||||||
|
int i = 0;
|
||||||
|
while (piecesFile.get(lengthByte)) {
|
||||||
|
if (piecesFile.eof()) break;
|
||||||
|
|
||||||
|
// read piece infos
|
||||||
|
char infoByte;
|
||||||
|
piecesFile.get(infoByte);
|
||||||
|
bool isConvex = (infoByte & convexMask) >> 7;
|
||||||
|
bool hasHole = (infoByte & holeMask) >> 6;
|
||||||
|
Color color = Color(infoByte & colorMask);
|
||||||
|
|
||||||
|
// read cells
|
||||||
|
std::set<Cell> pieceCells;
|
||||||
|
char cellByte;
|
||||||
|
for (int i = 0; i < order; i++) {
|
||||||
|
piecesFile.get(cellByte);
|
||||||
|
int x = (cellByte & xMask) >> 4;
|
||||||
|
int y = cellByte & yMask;
|
||||||
|
pieceCells.insert(Cell{x, y});
|
||||||
|
}
|
||||||
|
|
||||||
|
// create piece
|
||||||
|
Piece readPiece(Polyomino(pieceCells, lengthByte), color);
|
||||||
|
pieces.push_back(readPiece);
|
||||||
|
|
||||||
|
// link it to its type
|
||||||
|
if (isConvex) {
|
||||||
|
convexPieces.push_back(i);
|
||||||
|
}
|
||||||
|
else if (hasHole) {
|
||||||
|
otherPieces.push_back(i);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
holelessPieces.push_back(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PiecesFiles::getFilePath(int order, std::string& filePath) const {
|
||||||
|
// verify that the data folder exists
|
||||||
|
std::string dataFolderPath = "data/pieces/";
|
||||||
|
if (!std::filesystem::is_directory(dataFolderPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the file path
|
||||||
|
filePath = dataFolderPath + std::to_string(order) + "minos.bin";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
37
src/Pieces/PiecesFiles.h
Normal file
37
src/Pieces/PiecesFiles.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Piece.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
#include <String>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages creating pieces files and importing the data back from them
|
||||||
|
*/
|
||||||
|
class PiecesFiles {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Initializes file manager
|
||||||
|
*/
|
||||||
|
PiecesFiles();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a file containing all the pieces of the specified size,
|
||||||
|
* returns false if the file couldn't be created
|
||||||
|
*/
|
||||||
|
bool savePieces(int order) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the content of the vectors by the pieces of the specified size, if the file wasn't found the vectors stays untouched,
|
||||||
|
* returns false if the file wasn't found
|
||||||
|
*/
|
||||||
|
bool loadPieces(int order, std::vector<Piece>& pieces, std::vector<int>& convexPieces, std::vector<int>& holelessPieces, std::vector<int>& otherPieces) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Puts the path to the piece file of the specified size in order, if the data folder wasn't found the string stays untouched,
|
||||||
|
* returns false if the data folder wasn't found
|
||||||
|
*/
|
||||||
|
bool getFilePath(int order, std::string& filePath) const;
|
||||||
|
};
|
||||||
322
src/Pieces/Polyomino.cpp
Normal file
322
src/Pieces/Polyomino.cpp
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
#include "Polyomino.h"
|
||||||
|
|
||||||
|
#include "Cell.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
#include <Set>
|
||||||
|
#include <iostream>
|
||||||
|
#include <climits>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
|
||||||
|
Polyomino::Polyomino(const std::set<Cell>& cells) {
|
||||||
|
// find min/max
|
||||||
|
int minX = INT_MAX;
|
||||||
|
int maxX = INT_MIN;
|
||||||
|
int minY = INT_MAX;
|
||||||
|
int maxY = INT_MIN;
|
||||||
|
for (Cell cell : cells) {
|
||||||
|
if (cell.x < minX) minX = cell.x;
|
||||||
|
if (cell.x > maxX) maxX = cell.x;
|
||||||
|
if (cell.y < minY) minY = cell.y;
|
||||||
|
if (cell.y > maxY) maxY = cell.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize
|
||||||
|
std::set<Cell> newCells;
|
||||||
|
for (Cell cell : cells) {
|
||||||
|
newCells.insert(Cell{cell.x - minX, cell.y - minY});
|
||||||
|
}
|
||||||
|
this->cells = newCells;
|
||||||
|
|
||||||
|
// set polyomino length
|
||||||
|
this->length = std::max(maxX - minX + 1, maxY - minY + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Polyomino::Polyomino(const std::set<Cell>& cells, int length) : cells(cells), length(length) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void Polyomino::normalize() {
|
||||||
|
// find min values
|
||||||
|
int minX = INT_MAX;
|
||||||
|
int minY = INT_MAX;
|
||||||
|
for (Cell cell : this->cells) {
|
||||||
|
if (cell.x < minX) minX = cell.x;
|
||||||
|
if (cell.y < minY) minY = cell.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// translate the polyomino to the lowest unsigned values
|
||||||
|
std::set<Cell> newCells;
|
||||||
|
for (Cell cell : this->cells) {
|
||||||
|
newCells.insert(Cell{cell.x - minX, cell.y - minY});
|
||||||
|
}
|
||||||
|
this->cells = newCells;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Polyomino::rotateCW() {
|
||||||
|
// rotate 90° clockwise
|
||||||
|
std::set<Cell> newCells;
|
||||||
|
for (Cell cell : this->cells) {
|
||||||
|
newCells.insert(Cell{cell.y, (length - 1) - (cell.x)});
|
||||||
|
}
|
||||||
|
this->cells = newCells;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Polyomino::rotate180() {
|
||||||
|
// rotate 180°
|
||||||
|
std::set<Cell> newCells;
|
||||||
|
for (Cell cell : this->cells) {
|
||||||
|
newCells.insert(Cell{(length - 1) - (cell.x), (length - 1) - (cell.y)});
|
||||||
|
}
|
||||||
|
this->cells = newCells;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Polyomino::rotateCCW() {
|
||||||
|
// rotate 90° counter-clockwise
|
||||||
|
std::set<Cell> newCells;
|
||||||
|
for (Cell cell : this->cells) {
|
||||||
|
newCells.insert(Cell{(length - 1) - (cell.y), cell.x});
|
||||||
|
}
|
||||||
|
this->cells = newCells;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Polyomino::goToSpawnPosition() {
|
||||||
|
// initialize array
|
||||||
|
std::vector<std::vector<int>> linesCompleteness;
|
||||||
|
std::vector<int> empty;
|
||||||
|
for (int j = 0; j < this->length; j++) {
|
||||||
|
empty.push_back(0);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
linesCompleteness.push_back(empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculates amount of cells per rows and columns
|
||||||
|
for (Cell cell : this->cells) {
|
||||||
|
linesCompleteness.at(0).at(cell.y) += 1; // 0 = bottom to top = no rotation
|
||||||
|
linesCompleteness.at(1).at((length - 1) - cell.x) += 1; // 1 = right to left = CW
|
||||||
|
linesCompleteness.at(2).at((length - 1) - cell.y) += 1; // 2 = top to bottom = 180
|
||||||
|
linesCompleteness.at(3).at(cell.x) += 1; // 3 = left to right = CCW
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks for empty lines
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we ignore the sides which are not the widest
|
||||||
|
bool currentFlattestSides[4] = {true, true, true, true};
|
||||||
|
if (horizontalEmptyLines < verticalEmptyLines) {
|
||||||
|
currentFlattestSides[0] = false;
|
||||||
|
currentFlattestSides[2] = false;
|
||||||
|
}
|
||||||
|
if (verticalEmptyLines < horizontalEmptyLines) {
|
||||||
|
currentFlattestSides[1] = false;
|
||||||
|
currentFlattestSides[3] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks for the flattest sides
|
||||||
|
int sideToBeOn = -1;
|
||||||
|
this->checkForFlattestSide(linesCompleteness, currentFlattestSides, sideToBeOn, false);
|
||||||
|
|
||||||
|
// if all sides are as flat, checks for the most left-biased side
|
||||||
|
if (sideToBeOn == -1) {
|
||||||
|
this->checkForFlattestSide(linesCompleteness, currentFlattestSides, sideToBeOn, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there's still no winner we take the side (amongst the remaining ones) that's the smallest once rotated
|
||||||
|
if (sideToBeOn == -1) {
|
||||||
|
std::vector<Polyomino> candidateRotations;
|
||||||
|
std::vector<int> candidateSides;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
this->normalize();
|
||||||
|
if (currentFlattestSides[i]) {
|
||||||
|
candidateRotations.push_back(*this);
|
||||||
|
candidateSides.push_back(i);
|
||||||
|
}
|
||||||
|
this->rotateCW();
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
|
||||||
|
// find min
|
||||||
|
int minX = INT_MAX;
|
||||||
|
int minY = INT_MAX;
|
||||||
|
for (Cell cell : this->cells) {
|
||||||
|
if (cell.x < minX) minX = cell.x;
|
||||||
|
if (cell.y < minY) minY = cell.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// center the piece with an up bias if it is assymetric
|
||||||
|
if (sideToBeOn % 2 == 1) {
|
||||||
|
std::swap(verticalEmptyLines, horizontalEmptyLines);
|
||||||
|
}
|
||||||
|
std::set<Cell> newCells;
|
||||||
|
for (Cell cell : cells) {
|
||||||
|
newCells.insert(Cell{(cell.x - minX) + (verticalEmptyLines / 2), (cell.y - minY) + ((horizontalEmptyLines + 1) / 2)});
|
||||||
|
}
|
||||||
|
this->cells = newCells;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Polyomino::checkForFlattestSide(const std::vector<std::vector<int>>& linesCompleteness, bool currentFlattestSides[4], int& sideToBeOn, bool checkLeftSide) const {
|
||||||
|
// for each line
|
||||||
|
for (int j = 0; j < this->length; j++) {
|
||||||
|
// we check which sides are the flattest
|
||||||
|
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();
|
||||||
|
maxOwners.insert(i);
|
||||||
|
}
|
||||||
|
else if (linesCompleteness.at(sideToCheck).at(j) == max) {
|
||||||
|
maxOwners.insert(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there's no tie we choose this side
|
||||||
|
if (maxOwners.size() == 1) {
|
||||||
|
sideToBeOn = *maxOwners.begin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// else we only keep the flattest from this round and ignore the others
|
||||||
|
else {
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
currentFlattestSides[i] = currentFlattestSides[i] && maxOwners.contains(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Polyomino::isConvex() const {
|
||||||
|
// for each line and column we check if every cells 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->cells.contains(Cell{i, j})) {
|
||||||
|
if (completedLine) return false;
|
||||||
|
else startedLine = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (startedLine) completedLine = true;
|
||||||
|
}
|
||||||
|
// column check
|
||||||
|
if (this->cells.contains(Cell{j, i})) {
|
||||||
|
if (completedColumn) return false;
|
||||||
|
else startedColumn = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (startedColumn) completedColumn = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Polyomino::hasHole() const {
|
||||||
|
// add every outer cells of the square containing the polyomino
|
||||||
|
std::set<Cell> emptyCells;
|
||||||
|
for (int i = 0; i < this->length - 1; i++) {
|
||||||
|
this->tryToInsertCell(emptyCells, Cell{i, 0}); // up row
|
||||||
|
this->tryToInsertCell(emptyCells, Cell{this->length - 1, i}); // rigth column
|
||||||
|
this->tryToInsertCell(emptyCells, Cell{this->length - 1 - i, this->length - 1}); // bottom row
|
||||||
|
this->tryToInsertCell(emptyCells, Cell{0, this->length - 1 - i}); // left column
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we didn't reached all empty cells in the square then there was some contained within the polyomino, i.e. there was a hole
|
||||||
|
return (emptyCells.size() < (this->length * this->length) - this->cells.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Polyomino::tryToInsertCell(std::set<Cell>& emptyCells, const Cell& candidate) const {
|
||||||
|
// check if the cell is in the square containing the polyomino
|
||||||
|
if (candidate.x >= this->length || candidate.x < 0 || candidate.y >= this->length || candidate.y < 0) return;
|
||||||
|
|
||||||
|
// check if the cell is empty and hasn't already been tested
|
||||||
|
if (this->cells.contains(candidate) || emptyCells.contains(candidate)) return;
|
||||||
|
|
||||||
|
// adds the cell to the list of empty cells and try its neighbors
|
||||||
|
emptyCells.insert(candidate);
|
||||||
|
tryToInsertCell(emptyCells, Cell{candidate.x, candidate.y + 1});
|
||||||
|
tryToInsertCell(emptyCells, Cell{candidate.x + 1, candidate.y});
|
||||||
|
tryToInsertCell(emptyCells, Cell{candidate.x, candidate.y - 1});
|
||||||
|
tryToInsertCell(emptyCells, Cell{candidate.x - 1, candidate.y});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<Cell> Polyomino::getCells() const {
|
||||||
|
return this->cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Polyomino::getLength() const {
|
||||||
|
return this->length;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Polyomino::getPolyominoOrder() const {
|
||||||
|
return this->cells.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Polyomino::operator<(const Polyomino& other) const {
|
||||||
|
// if one has an inferior length then it is deemed inferior
|
||||||
|
if (this->length != other.length) return this->length < other.length;
|
||||||
|
|
||||||
|
// else we check for all cells from left to right and top to bottom, until one has a cell that the other doesn't
|
||||||
|
for (int y = this->length - 1; y >= 0; y--) {
|
||||||
|
for (int x = 0; x < this->length; x++) {
|
||||||
|
bool hasThisCell = this->cells.contains(Cell{x, y});
|
||||||
|
bool hasOtherCell = other.cells.contains(Cell{x, y});
|
||||||
|
if (hasThisCell != hasOtherCell) return hasThisCell;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if they are equal
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Polyomino::operator ==(const Polyomino& other) const {
|
||||||
|
return this->cells == other.cells;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& os, const Polyomino& polyomino) {
|
||||||
|
for (int y = polyomino.length - 1; y >= 0; y--) {
|
||||||
|
for (int x = 0; x < polyomino.length; x++) {
|
||||||
|
if (polyomino.cells.contains(Cell{x, y})) {
|
||||||
|
os << "*";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
os << "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os << std::endl;
|
||||||
|
}
|
||||||
|
return os;
|
||||||
|
}
|
||||||
108
src/Pieces/Polyomino.h
Normal file
108
src/Pieces/Polyomino.h
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Cell.h"
|
||||||
|
|
||||||
|
#include <Vector>
|
||||||
|
#include <Set>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mathematical object consisting of touching squares on a 2D grid
|
||||||
|
*/
|
||||||
|
class Polyomino {
|
||||||
|
private:
|
||||||
|
std::set<Cell> cells; // the squares composing the polyomino, (0,0) is downleft
|
||||||
|
int length; // the size of the smallest square in which the polyomino can fit on any rotation
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Creates a polyomino with the specified cells and normalizes it, wheter it is actually a polyonimo is not checked
|
||||||
|
*/
|
||||||
|
Polyomino(const std::set<Cell>& cells);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a polyomino with the specified cells and length, wheter it is actually a polyonimo of this length is not checked
|
||||||
|
*/
|
||||||
|
Polyomino(const std::set<Cell>& cells, int length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates the polyomino to the lowest unsigned values (lower row on y = 0, and left-most column on x = 0)
|
||||||
|
*/
|
||||||
|
void normalize();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the polyomino 90° clockwise, the center of rotation being the middle of the square going from (0,0) to (length-1, length-1)
|
||||||
|
*/
|
||||||
|
void rotateCW();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the polyomino 180°, the center of rotation being the middle of the square going from (0,0) to (length-1, length-1)
|
||||||
|
*/
|
||||||
|
void rotate180();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the polyomino 90° counter-clockwise, the center of rotation being the middle of the square going from (0,0) to (length-1, length-1)
|
||||||
|
*/
|
||||||
|
void rotateCCW();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the polyomino to a normalized default spawn position
|
||||||
|
*/
|
||||||
|
void goToSpawnPosition();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Auxiliary method of goToSpawnPosition()
|
||||||
|
*/
|
||||||
|
void checkForFlattestSide(const std::vector<std::vector<int>>& linesCompleteness, bool currentFlattestSides[4], int& sideToBeOn, bool checkLeftSide) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Returns wheter the polyomino is convex, that is if every line and column has at most one continuous line of cells
|
||||||
|
*/
|
||||||
|
bool isConvex() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns wheter the polyomino has at least one hole
|
||||||
|
*/
|
||||||
|
bool hasHole() const;
|
||||||
|
|
||||||
|
private :
|
||||||
|
/**
|
||||||
|
* Auxiliary method of hasHole()
|
||||||
|
*/
|
||||||
|
void tryToInsertCell(std::set<Cell>& emptyCells, const Cell& candidate) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Returns a copy of the cells of the polyomino
|
||||||
|
*/
|
||||||
|
std::set<Cell> getCells() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the length of the polyomino
|
||||||
|
*/
|
||||||
|
int getLength() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of squares in the polyomino
|
||||||
|
*/
|
||||||
|
int getPolyominoOrder() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strict inferiority operator, a polyomino is inferior than another if it has a smaller length, or if they are the same length,
|
||||||
|
* while checking from left to right and top to bottom, is the first which has a cell while the other doesn't
|
||||||
|
*/
|
||||||
|
bool operator<(const Polyomino& other) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equality operator, two polyominos are equal if they overlap, that means two polyominos of the same shape but different positions will not be equal
|
||||||
|
*/
|
||||||
|
bool operator ==(const Polyomino& other) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream output operator, adds a 2D grid representing the polyomino
|
||||||
|
*/
|
||||||
|
friend std::ostream& operator<<(std::ostream& os, const Polyomino& polyomino);
|
||||||
|
};
|
||||||
28
src/Pieces/Rotation.h
Normal file
28
src/Pieces/Rotation.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every type of rotation that can be applied to an object in a 2D grid
|
||||||
|
*/
|
||||||
|
enum Rotation {
|
||||||
|
NONE,
|
||||||
|
CLOCKWISE,
|
||||||
|
DOUBLE,
|
||||||
|
COUNTERCLOCKWISE
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Addition operator, returns a rotation corresponding to doing both rotations
|
||||||
|
*/
|
||||||
|
Rotation operator+(const Rotation& left, const Rotation& right) {
|
||||||
|
return Rotation((left + right) % 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additive assignation operator, rotate the left rotation by the right rotation
|
||||||
|
*/
|
||||||
|
Rotation& operator+=(Rotation& left, const Rotation& right) {
|
||||||
|
left = left + right;
|
||||||
|
return left;
|
||||||
|
}
|
||||||
77
xmake.lua
Normal file
77
xmake.lua
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
set_languages("c++20")
|
||||||
|
|
||||||
|
target("main")
|
||||||
|
set_kind("binary")
|
||||||
|
set_rundir(".")
|
||||||
|
add_files("./src/Pieces/*.cpp")
|
||||||
|
add_files("./src/Core/*.cpp")
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- If you want to known more usage about xmake, please see https://xmake.io
|
||||||
|
--
|
||||||
|
-- ## FAQ
|
||||||
|
--
|
||||||
|
-- You can enter the project directory firstly before building project.
|
||||||
|
--
|
||||||
|
-- $ cd projectdir
|
||||||
|
--
|
||||||
|
-- 1. How to build project?
|
||||||
|
--
|
||||||
|
-- $ xmake
|
||||||
|
--
|
||||||
|
-- 2. How to configure project?
|
||||||
|
--
|
||||||
|
-- $ xmake f -p [macosx|linux|iphoneos ..] -a [x86_64|i386|arm64 ..] -m [debug|release]
|
||||||
|
--
|
||||||
|
-- 3. Where is the build output directory?
|
||||||
|
--
|
||||||
|
-- The default output directory is `./build` and you can configure the output directory.
|
||||||
|
--
|
||||||
|
-- $ xmake f -o outputdir
|
||||||
|
-- $ xmake
|
||||||
|
--
|
||||||
|
-- 4. How to run and debug target after building project?
|
||||||
|
--
|
||||||
|
-- $ xmake run [targetname]
|
||||||
|
-- $ xmake run -d [targetname]
|
||||||
|
--
|
||||||
|
-- 5. How to install target to the system directory or other output directory?
|
||||||
|
--
|
||||||
|
-- $ xmake install
|
||||||
|
-- $ xmake install -o installdir
|
||||||
|
--
|
||||||
|
-- 6. Add some frequently-used compilation flags in xmake.lua
|
||||||
|
--
|
||||||
|
-- @code
|
||||||
|
-- -- add debug and release modes
|
||||||
|
-- add_rules("mode.debug", "mode.release")
|
||||||
|
--
|
||||||
|
-- -- add macro definition
|
||||||
|
-- add_defines("NDEBUG", "_GNU_SOURCE=1")
|
||||||
|
--
|
||||||
|
-- -- set warning all as error
|
||||||
|
-- set_warnings("all", "error")
|
||||||
|
--
|
||||||
|
-- -- set language: c99, c++11
|
||||||
|
-- set_languages("c99", "c++11")
|
||||||
|
--
|
||||||
|
-- -- set optimization: none, faster, fastest, smallest
|
||||||
|
-- set_optimize("fastest")
|
||||||
|
--
|
||||||
|
-- -- add include search directories
|
||||||
|
-- add_includedirs("/usr/include", "/usr/local/include")
|
||||||
|
--
|
||||||
|
-- -- add link libraries and search directories
|
||||||
|
-- add_links("tbox")
|
||||||
|
-- add_linkdirs("/usr/local/lib", "/usr/lib")
|
||||||
|
--
|
||||||
|
-- -- add system link libraries
|
||||||
|
-- add_syslinks("z", "pthread")
|
||||||
|
--
|
||||||
|
-- -- add compilation and link flags
|
||||||
|
-- add_cxflags("-stdnolib", "-fno-strict-aliasing")
|
||||||
|
-- add_ldflags("-L/usr/local/lib", "-lpthread", {force = true})
|
||||||
|
--
|
||||||
|
-- @endcode
|
||||||
|
--
|
||||||
Reference in New Issue
Block a user