61 Commits

Author SHA1 Message Date
f4b58fb67e omg omg 2025-03-30 22:43:41 +02:00
0271b56542 better piece scrolling 2025-03-30 20:16:14 +02:00
5d73e751d7 début de qqch 2025-03-30 13:07:17 +02:00
314b7a8488 added distribution menu 2025-03-29 21:57:27 +01:00
7151be0b1a better cursor 2025-03-29 19:28:04 +01:00
d124205a71 ok fix ig 2025-03-29 18:52:06 +01:00
87920548e5 Merge branch 'main' of https://git.ale-pri.com/TetrisNerd/jminos 2025-03-29 18:50:54 +01:00
57620c70a2 added distribution modes 2025-03-29 18:48:37 +01:00
3dac18c821 fix linux build 2025-03-29 18:14:14 +01:00
3538403f40 finalized gamemode select menu 2025-03-29 12:31:54 +01:00
ec40495328 change volume to start timer 2025-03-29 11:49:19 +01:00
e0de2b5f90 aaaa 2025-03-28 21:51:30 +01:00
df7c9bd211 fixes B2B 2025-03-28 21:45:50 +01:00
f883bd5bab ajout board menu 2025-03-28 21:31:24 +01:00
0f026635f6 stats in-game 2025-03-28 21:00:46 +01:00
be071bd606 ajout holdbox 2025-03-28 16:32:44 +01:00
3320545465 next queue fixed 2025-03-28 15:53:21 +01:00
d1646d0fb5 next queue lezgo 2025-03-28 14:58:52 +01:00
3d74ef7cd5 added ZEN gamemode 2025-03-27 21:50:36 +01:00
88cb44c5fe update readme.md 2025-03-27 21:03:32 +01:00
009ed8edc3 ajout menu info 2025-03-26 00:39:39 +01:00
b2567844fc fini menu keybinds 2025-03-25 20:06:02 +01:00
de8a5e6e34 keybinds menu 2025-03-25 17:17:36 +01:00
c168cd68d7 refactoring 2025-03-24 22:39:16 +01:00
321271b748 ?? 2025-03-24 16:25:34 +01:00
c601424481 on track pas les settings wtf 2025-03-24 16:25:30 +01:00
fd9fd4586a on peut changer les settings wtf 2025-03-24 16:20:37 +01:00
8a4c4201fe taille board adaptative 2025-03-24 14:59:02 +01:00
c08cfc2255 ingame moins moche (mais pas fini) 2025-03-24 13:28:38 +01:00
38008e00bc fixed game logic 2025-03-23 21:41:37 +01:00
507bc9cc86 omg on a un deuxième menu 2025-03-23 20:15:05 +01:00
e721a71894 omg on a un menu 2025-03-23 18:45:34 +01:00
1781b85332 update readme 2025-03-23 16:53:58 +01:00
92b58c4b98 l'espoir renaît???? 2025-03-23 11:50:55 +01:00
8635d4b853 abandon 2025-03-23 00:16:59 +01:00
9780a36af4 ff 2025-03-22 23:30:52 +01:00
6b16abda6a fichier settings 2025-03-22 22:03:57 +01:00
30dd323e22 trop de trucs oscours 2025-03-22 17:41:33 +01:00
d87ddcdc22 Merge branch 'main' of https://git.ale-pri.com/TetrisNerd/jminos 2025-03-22 11:00:34 +01:00
8aaced68d0 merging aftermath 2025-03-22 11:00:31 +01:00
be6c8d9f77 Actualiser .vscode/c_cpp_properties.json 2025-03-22 09:56:42 +00:00
d9ccecfdd8 Merge branch 'main' of https://git.ale-pri.com/TetrisNerd/jminos 2025-03-22 10:53:30 +01:00
02bab6ed87 Merge pull request 'Better xmake.lua + Intellisense' (#2) from xmake into main
Reviewed-on: https://127.0.0.1:3000/TetrisNerd/jminos/pulls/2
2025-03-22 09:51:36 +00:00
0e17996c35 toujours plus de settings 2025-03-22 10:49:55 +01:00
ea8478275b add vscode Intellisense 2025-03-22 10:02:46 +01:00
29177bc94d refactor xmake.lua 2025-03-22 10:02:35 +01:00
c25abec6ba on commence l'interface là ouais 2025-03-21 22:52:29 +01:00
021620acef enlever le debug 2025-03-10 09:49:18 +01:00
74797e935a fixed game logic 2025-03-05 19:02:51 +01:00
2fbe4a6052 meilleurs coms ig 2025-03-03 22:34:46 +01:00
d029589c21 smarter & pointier 2025-03-03 12:00:39 +01:00
47d3d929db typo 2025-03-03 00:26:05 +01:00
1033f3a64c ajouté interface textuelle 2025-03-02 23:36:20 +01:00
857f90d646 màj diagrammes 2025-03-01 23:15:58 +01:00
f525c00662 const everywhere + légèrement changer format fichiers 2025-03-01 22:21:43 +01:00
8088894073 ajouté classe Menu 2025-03-01 20:27:36 +01:00
d443b25de1 allow resetting games 2025-03-01 19:13:49 +01:00
03281a6d70 ajoute la possiblilité d'ajouter des lignes de garbage 2025-03-01 18:41:09 +01:00
66c2cf1013 fini PiecesList 2025-03-01 18:24:09 +01:00
13ee43167e début de la classe PiecesFile 2025-02-28 22:32:51 +01:00
26f501f7e8 mis en place la PR de simon sur les couleurs 2025-02-28 18:42:04 +01:00
93 changed files with 5173 additions and 1039 deletions

5
.gitignore vendored
View File

@@ -11,6 +11,9 @@ build/
# personnal documentation
doc/*.txt
doc/*.violet.html
doc/mockups/*
# pieces files
# data files
data/pieces/*.bin
data/config/*.bin
data/config/keybinds/*.bin

10
.vscode/c_cpp_properties.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"configurations": [
{
"name": "jminos",
"cppStandard": "c++20",
"compileCommands": ".vscode/compile_commands.json"
}
],
"version": 4
}

59
README.md Normal file
View File

@@ -0,0 +1,59 @@
# jminos
Modern stacker game with every polyominos from size 1 to 15, made in C++ with [SFML 3](https://www.sfml-dev.org/)!
## Download
// to bo included when release version is done //
This game has been tested on and provides executables for Windows 11 and Linux under Ubuntu only.
If your OS isn't compactible with either of theses two, you can try [manually building the project](#manual-build-and-run).
## How to play
### General
You can see and change in-game keybinds in the **SETTINGS** section of the main menu!
All of in-menu navigation is done with the arrow keys, the Enter key and the Escape key. Theses are unmutable keybinds.
You will find some more infos about the Rotation System and the scoring in the **INFO** section of the main menu.
If you want to know more details about the generation and classification of polyominoes, [check the documentation](/doc/)!
### Available gamemodes
- SPRINT : clear 40 lines as fast as possible!
- MARATHON : clear 200 lines with increasing gravity!
- ULTRA : scores as much as possible in only 2 minutes!
- MASTER : clear 200 lines at levels higher than maximum gravity!
- ZEN : practice indefinitely in this mode with no gravity!
## Manual build
This project uses xmake for compiling, xmake is cross-platform and works in most OS, xmake also automatically install supported librairies.
To be able to build this project, you need to [have xmake installed](https://xmake.io) and have a compiler with C++20 compatibility.
### Build the project
``cd jminos``
``xmake``
If you need to change the toolchain (for example using gcc):
``xmake f --toolchain=gcc``
### Run the project
``xmake run``
Note that the program will generate the polyomino files for you the first time. This lasts several minutes so it only does it up to size 10. If you want to use the full range up to size 15, you will need to uncomment the ``#define`` at line 13 of file ``src/GraphicalUI/Settings.h``.
If for some reasons you wanna run the command line version:
``xmake run text``
### Create a release version (xmake packaging ???)
## Credits
Library used: [SFML 3](https://www.sfml-dev.org/).
Font used: [Press Start](https://www.zone38.net/font/#pressstart).
Inspired by other modern stacker games such as Techmino, jstris, tetr.io, etc.
This game isn't affiliated with any of them.

0
data/config/.gitkeep Normal file
View File

View File

View File

@@ -0,0 +1,17 @@
Thanks for downloading one of codeman38's retro video game fonts, as seen on Memepool, BoingBoing, and all around the blogosphere.
So, you're wondering what the license is for these fonts? Pretty simple; it's based upon that used for Bitstream's Vera font set <http://www.gnome.org/fonts/>.
Basically, here are the key points summarized, in as little legalese as possible; I hate reading license agreements as much as you probably do:
With one specific exception, you have full permission to bundle these fonts in your own free or commercial projects-- and by projects, I'm referring to not just software but also electronic documents and print publications.
So what's the exception? Simple: you can't re-sell these fonts in a commercial font collection. I've seen too many font CDs for sale in stores that are just a repackaging of thousands of freeware fonts found on the internet, and in my mind, that's quite a bit like highway robbery. Note that this *only* applies to products that are font collections in and of themselves; you may freely bundle these fonts with an operating system, application program, or the like.
Feel free to modify these fonts and even to release the modified versions, as long as you change the original font names (to ensure consistency among people with the font installed) and as long as you give credit somewhere in the font file to codeman38 or zone38.net. I may even incorporate these changes into a later version of my fonts if you wish to send me the modifed fonts via e-mail.
Also, feel free to mirror these fonts on your own site, as long as you make it reasonably clear that these fonts are not your own work. I'm not asking for much; linking to zone38.net or even just mentioning the nickname codeman38 should be enough.
Well, that pretty much sums it up... so without further ado, install and enjoy these fonts from the golden age of video games.
[ codeman38 | cody@zone38.net | http://www.zone38.net/ ]

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 54 KiB

51
doc/files_format.md Normal file
View File

@@ -0,0 +1,51 @@
# Files format
## Pieces
If you don't know what a polyomino is, check [this other file](Pieces_representation.md#what-are-polyominoes).
Generating polyominoes of size n is exponential in regard to n. Because of this, we will store the pieces beforehand and load them upon launching the game.
We want the pieces to be always sorted in the same order, always attributed the same block type, and always set at the same spawn position, no matter how they were generated. We also want them to be separated in 3 categories : convex, not convex but without a hole, and with a hole. Theses problematics are already resolved internally, but will be calculated before storage as to not need extra calculcations upon load (except for the block type which is trivially computed).
Pieces are stored in binary files. Each file simply contains every polyomino of one size, one after the other. Since each file contains all polyominoes of the same size, we know how much stuff to read and don't need delimiters. We know we've read all pieces simply when we reach the end of file character.
Each piece is stored as follows:
- 1 byte for the characteristics of the piece: ``ABCCCCCC`` where A indicates if the piece is convex, B indicates if it has a hole, and C is the length of the piece
- 1 byte for each position: ``XXXXYYYY`` where X is the x coordinate of the position and Y is the y coordinate of the position
The current implementation only allows to generate polyominoes up to size 16, but can be upgraded by storing coordinates on 8 bits instead of 4.
It has been currently choosen to use pieces only up to size 15 for this game.
## Config
When compiling a release version, default files will be used, theses will then be impacted by the user and used for their next sessions.
### Keybinds
The games has 4 keyboard configs by default that never changes, and a modifiable 5th one, but theses are all stored the same.
Each keybinds files has the the following format:
- The number of the action (converted from an Enum), stored with 1 byte
- The number of each binded keys (also converted from an Enum), stored with 1 byte
- A separator characters which is 0xFF
_Repeat for every avaible actions._
### Settings
The settings file has the following format:
- The number of the chosen keybinds (from 0 to 4), stored with 1 byte
- The DAS of the player, stored with 1 byte
- The ARR of the player, stored with 1 byte
- The SDR of the player, stored with 1 byte
- The window size mode, stored with 1 byte
- The master volume, stored with 1 byte
- The number of the last selected gamemode (converted from an Enum), stored with 1 byte
- The last selected width of the board, stored with 1 byte
- The last selected height of the board, stored with 1 byte
- The uniformity mode (0 for default distribution, 1 for uniformous distribution, 2 for custom distribution), stored with 1 byte
- For each size, store the custom proportion (from 0 to 10) (15x1 byte total)
- Every selected pieces, using 1 byte for the type of selection (once again converted from an Enum) and 1 byte for the actual value

View File

@@ -1,7 +1,7 @@
# Game logic
This section will detail how the player's action are interpreted into the game.
We will only talk about pieces and not polyominos. In this project, pieces are an abstraction of a polyomino. Though if you want to know more the polyominos in this project, check [this other file](Pieces_representation.md).
We will only talk about pieces and not polyominoes. In this project, pieces are an abstraction of a polyomino. Though if you want to know more the polyominoes in this project, check [this other file](Pieces_representation.md).
## Order of operations
@@ -12,6 +12,7 @@ Each frame, the UI will translate the user's input into a series of action to ap
- Hold
- Move left
- Move right
- Rotate 0°
- Rotate clockwise (CW)
- Rotate 180°
- Rotate counter-clockwise (CCW)
@@ -24,12 +25,39 @@ 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.
Menu navigation is managed by the UI and uses static keybinds to prevent the user from softlocking themselves.
## ARR and DAS
The sidewise movement of the piece is defined by two parameters: DAS and ARR.
**DAS** stands for Delayed Auto Shift and **ARR** for Auto Repeat Rate.
Theses can be changed by the player.
The parameters will be refered to as DAS and ARR, while the in-game values will be refered to as the piece's DAS ans ARR.
- Initially, both the piece DAS and piece ARR are at 0. When moving the piece in one direction,
the piece will move one position and then wait for the player to hold that direction as long as the DAS value.
_So for exemple if DAS is set to 20, on the first frame piece DAS will be set to 1 and the piece will move once.
The next movement will be delayed by 20 frames, so the piece will move again 20 frames later, on the 21th frame, or when the piece DAS = (DAS +1)._
- Then, if the player still holds the same direction, ARR takes on.
The piece will move every ARR frames.
_So in our exemple if ARR is set to 5, this means the piece will move every 5 frames after the 21th frame.
So on the 21th frame piece DAS = 21 and piece ARR = 0.
On the 26th frame, piece ARR = 5 so the piece moves and piece ARR is set back to 0._
Now DAS can be buffered, that is if a direction is held before the piece spawn, it will have an initial DAS value corresponding to the number of frames held.
_So in our example where DAS = 20 and ARR = 5, if we held a direction during 30f then hold without releasing this direction, the piece will move instantly on the frame it spawns.
Then, ARR will start counting on the next frame. So the piece will move on frames 1, 6, 11, etc._
When trying to hold two directions at the same time, only the direction held last will be applied.
So for example if you were holding left and you suddendly start holding the two directions at the same time, the piece will go right.
In the two directions are first pressed at the same frame, left will take priority.
## Leniency mechanics
In a general sense, we try to be kind to the players. Some of the following mechanics are just standards in a lot of stacker games, while other have been added because of this game using polyominos of high sizes and thus being way harder to play.
In a general sense, we try to be kind to the players. Some of the following mechanics are just standards in a lot of stacker games, while other have been added because of this game using polyominoes of high sizes and thus being way harder to play.
When a piece touches the ground, there is a short time period before 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.
When a piece touches the ground, there is a short time period before it automatically locks. This period is called _lock delay_. Lock delay is reset everytime the piece move. To not allow for infinite stalling, there is another longer period called _forced lock delay_ that does not reset when moving the piece.
The player has a hold box in which they can temporarly store a piece. In this game, we allow the player to swap between the held piece and the active piece as much as they want. Once again to not allow for infinite stalling, forced lock delay does not reset when holding a piece.
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.
@@ -38,30 +66,57 @@ IRS and IHS will fail if they actually loose the player the game when it would h
A common mechanic of stacker games is kicking. This happen when rotating a piece makes it collide with a wall. The game will try to move the piece in a few predetermined spot close to the current position until one of them allow the piece to not be in a wall again. If no positions allow for that, the rotation is simply cancelled.
This concept works very well for games with up to tetrominos or pentominos, but when using polyominos of any size we can't choose a few predetermined spots for each piece.
This concept works very well for games with up to tetrominos or pentominos, but when using polyominoes of any size we can't choose a few predetermined spots for each piece.
Since this game uses polyomino of high sizes which are very unplayable, we will try to be complaisant to the player and allow as much kicking spots as possible, while trying not to make him feel like the piece is going through walls. To solve this problem, this game introduce a new Rotation System called **AutoRS**, which does not have a predetermined list of spots to fit the piece but instead adapt to its shape. Its algorithm goes as follow:
1. Before rotating, mark every cell containing the piece or touching the piece, we will call the set of all theses cells the ``safeCells``
1. Before rotating, mark every position containing the piece or touching the piece, we will call the set of all theses positions the ``safePositions``
2. Rotate the piece, if it fit stop the algorithm
3. Try fitting the piece, going from the center to the sides, that means we try to move the piece 1 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``
3. Try fitting the piece, going from the center to the sides, that means we try to move the piece 1 position right, then 1 position left, then 2 position right, etc. until it fit (and then stop the algorithm), if at one point none of the squares of the pieces are in one of the ``safePositions`` we stop trying in this direction
4. Move the piece one line down, and repeat step 3 again, until we hit a line were the first position (shifted by 0 positions horizontally) touched none of the ``safePositions``
5. Do the same as step 4 but now we move the piece one line up every time
6. Cancel the rotation
_Note: at step 3, the direction which is checked first is actually the last movement done by the player._
Kicking is primarly designed for rotating, but there is also a 0° rotation applied when a piece spawns into a wall.
0° rotations will first try to move the piece one position down, and if it can't, try kicking the piece, only registering a kick if the piece couldn't move down.
## Detecting spins
Another common mechanic of stacker games that goes alongside kicking is spinning. A spin is a special move (a move is calculated once a piece has been locked to the board) which usually happen when the last move a piece did was a kick or a rotation and the piece is locked in place, but the rules varies a lot from game to game.
Another common mechanic of stacker games that goes alongside kicking is spinning. A spin is a special move (a move is calculated once a piece has been locked to the board) which usually happen when the last move a piece did was a kick or a rotation and the piece is locked in place or is locked in certain corners, but the rules varies a lot from game to game.
Since we deal with a great deal of different size and shapes, the rules for spin dectection have been simplified greatly:
Since we work 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 _spin_ if the piece is locked in place, that is it can't be moved one position 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 locking a piece, add 1 to the score if it is due to lock delay, or 10 if it was hard dropped.
- 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
## Grade calculation
Grade is an alternate system to line clears.
- Each time a piece is dropped, 1 point is added to the total.
- For each cleared line, 1 point is added to the total.
The only exception occurs when the total ends in '99', a line must be cleared to progress.
When this line is cleared, the point of the piece is also counted towards the total.
## Pieces distribution
A bag is an object which contains a set of pieces.
The principle of a bag generator is to put every available pieces into a bag and take them out one by one until the bag is empty, to then start with a new full bag.
It's a way to have equal distribution of pieces while still allowing for a random order of pieces.
The game has 3 modes of pieces distribution:
1. Default. The simplest of them, all selected pieces are put in a single bag.
2. Uniformous. The pieces are now separated by size and put in different bags. Each size will now appear as often as any other.
3. Custom. The pieces are once again separated by size, but now the player specifies how likely each size is to appear.
Both system 2 and 3 uses a system analogous to a bag to determine which size of piece to take next.

View File

@@ -1,108 +1,110 @@
# Pieces representation
## What are polyominos ?
## What are polyominoes ?
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:
In this game, pieces are represented as a polyomino and a block type.
Polyominoes are mathematical objects consisting of multiple edge-touching squares.
There must be a path from every square to every other square, going from square to square only through the sides and not the corners.
Polyominoes can be classified in 3 ways:
- Fixed polyominos : only translation is allowed
- One-sided polyominos : only translation and rotation are allowed
- Fixed polyominoes : only translation is allowed
- One-sided polyominoes : only translation and rotation are allowed
- Free polyomins : translation, rotation, and reflection are allowed
For more detailed informations about polyominos, check the [Wikipedia page](https://en.wikipedia.org/wiki/Polyomino).
For more detailed informations about polyominoes, check the [Wikipedia page](https://en.wikipedia.org/wiki/Polyomino).
Most stacker game uses 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.
Most stacker game uses all one-sided polyominoes of size 4 (called tetrominos), which results in 7 distinct polyominoes.
In this game too, one-sided polyominoes will be used since we will only allow to move and rotate the pieces.
Internally, Polyominos are represented as a set of 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.
Internally, polyominoes are represented as a set of positions on a 2D grid.
This means that 2 polyominoes of same shape but at different places will be interpreted as different polyominoes.
To solve this, when doing equality checks, we normalize the polyominoes so that their left-most column is at x=0 and their bottom-most row at y=0.
Even if there is only 7 one-sided 4-minos, there is already more than 9,000 one-sided 10-minos.
Since the aim of this game is to be playable with polyominos of any size, listing all polyominos manually isn't viable.
We will need a way to:
Since the aim of this game is to be playable with polyominoes of any size, listing all polyominoes manually isn't viable.
We will need a way to automatically:
1. Generate all one-sided polyominos of size n
1. Generate all one-sided polyominoes of size n
2. Set their spawn positions and rotation centers
Aditionally, for this game we will also a want a way to separate the polyominos into multiple categories, specifically to allow removing those with holes who are more unplayable.
Aditionally, for this game we will also a want a way to separate the polyominoes into multiple categories, specifically to allow removing those with holes who are harder to play with.
## Ordering the polyominos
## Ordering the polyominoes
For practical reasons, we want to be able to sort all polyominos of the same size.
This is done very simply:
For practical reasons, we want to be able to sort all polyominoes of the same size.
But to sort objects we need a way to compare them.
When a polyomino is created, an attribute named its length is computed. This is simply the max between its width and its height. Thus the polyomino can be inscribed in a box of size ``length``.
We will now assume that our polyominoes are always inscribed in a box of origin (0,0) and size ``length`` (which should always be the case under normal circumstances).
We can now compare polyominoes using this method:
- If one polyomino has an inferior length than another, it is deemed inferior
- If two polyomino have the same length, we check all the 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
- If two polyomino have the same length, we check all the positions of their box, from left to right, and repeating from up to bottom, for the first position where a polyomino has a square and the another doesn't, the present polyomino which has a square is deemed inferior
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.
A nice side-effect is that, once the polyomino are ordered, it is very simple to attribute them a block type, we can simply iterate through the list while looping over the block list.
## 1. Generating polyominos
## 1. Generating polyominoes
The method used to generate polyominos is similar to the [inductive method](https://en.wikipedia.org/wiki/Polyomino#Inductive_algorithms) described in the previously mentionned Wikipedia page.
The method used to generate polyominoes is similar to the [inductive method](https://en.wikipedia.org/wiki/Polyomino#Inductive_algorithms) described in the previously mentionned Wikipedia page.
The algorithm is the following:
1. Add a single cell at position (0,0), and number it with 0
1. Add a single square 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. Else we number each adjacent square to the polyomino with a number higher than the last numbered square, unless:
1. If a square was already numbered then we don't touch it
2. If a square is on top of the polyomino then we don't number it
3. If a square is below y=0, or at exactly x=0 and y<0, then we don't number it
3. For each square with a higher number than the last added one:
1. We add this square 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
3. We remove this square from the polyomino
3. Return the list of polyominoes
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.
The exact number of one-sided polyominoes up to size 15 (and higher, but we only generated up to size 15) is known, and this method generated exactly theses numbers, without duplicates.
By marking 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.
By marking squares and adding only ones that have a higher number than the last one everytime, we generate each fixed polyomino exactly n times. By ignoring the squares below y=0, or at exactly x=0 and y<0, we generate each fixed polyomino exactly 1 time.
An one-sided polyomino has 4 rotations, some of which can be the same if the polyomino has symmetries. 2 rotations that are the same corresponds to 1 fixed polyomino, we will refer to theses 2 rotation as 1 "unique" rotation.
Because we generate every fixed polyomino exactly 1 time, that means each "unique" rotation of an one-sided polyomino is generated exactly one-time, which includes its lowest rotation. That's how we can get away with only checking the rotation of the polyomino and not comparing it to the rest of the generated polyominos.
Because we generate every fixed polyomino exactly 1 time, that means each "unique" rotation of an one-sided polyomino is generated exactly one-time, which includes its lowest rotation. That's how we can get away with only checking the rotation of the polyomino and not comparing it to the rest of the generated polyominoes.
## 2. Setting the spawn position of polyominos
## 2. Setting the spawn position of polyominoes
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:
Since we assume the polyomino is always inscribed in a box at origin (0,0), we can use formulae close to matrix rotations to rotate the piece:
- 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._
_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**.
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 box.
The very arbitrary rules used in this game for finding the spawn rotation is the following: **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.
**Flattest** means we prefer the side with the most square 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
1. Check if the polyomino has more lines horizontally or vertically, this will determine either 2 or 4 sides which are wider than the others, and the others will be discarded
2. For each potential side check the number of square on the first line, if one has more than every others, then this side is choosed, else we only keep the sides that tied and repeat for the next line
3. If we still have at least 2 sides tied, we do the same again but check the flatness of the side to the left instead, this will make the pieces overall have more squares to their left at spawn
4. If there is still no winner, we sort the remaining sides (by simulating having them selectionned and then sorting the resulting polyominoes) and keep the lowest
5. We rotate the piece so that the chosen side ends up at the bottom of the polyomino
6. We center the polyomino inside its 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
6. We center the polyomino inside its box, since we chose the widest side it will always fill the width of the square, but for the height it can be asymmetric, in that case we place it one line closer to the top than the bottom
_Note we could actually just skip straight to step 4 because it always give the same orientation, but we want the spawn rotations to follow somewhat of a logic. Step 1 to 3 actually already works for 99% of polyominos and they constrain step 4 too by preselecting certain sides._
_Note we could actually just skip straight to step 4 because it always give the same orientation, but we want the spawn rotations to follow somewhat of a logic. Step 1 to 3 actually already works for 99% of polyominoes and they constrain step 4 too by preselecting certain sides._
## 3. Separating the polyominos into categories
## 3. Separating the polyominoes into categories
For this game, we want the polyominos to be broken down into 3 categories:
For this game, we want the polyominoes to be broken down into 3 categories:
- Convex: this is said of a polyomino where every row and column is formed of at most one continous line of 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)
- Convex: this is said of a polyomino where every row and column is formed of at most one continous line of squares
- Holeless: the polyominoes which are neither convex nor have a hole
- Others: the polyominoes who have a hole (thoses are by definition not convex)
To check for convexity, we simply iterate trough each row and column, and check cell by cell if they are all contiguous.
To check for convexity, we simply iterate trough each row and column, and check if all the squares are 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.
To check for holes, we list every empty squares starting from the exterior of the box of the polyomino, then add every adjacent empty square recursively. If the polyomino has an hole then there is at least one empty square 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 squares.

View File

@@ -1,20 +0,0 @@
# 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.

View File

@@ -1,8 +1,10 @@
#pragma once
#include <iostream>
/**
* The list of actions that can be taken by the player
* The list of in-game actions that can be taken by the player
*/
enum Action {
PAUSE,
@@ -12,7 +14,45 @@ enum Action {
HARD_DROP,
MOVE_LEFT,
MOVE_RIGHT,
ROTATE_0,
ROTATE_CW,
ROTATE_180,
ROTATE_CCW
};
static const Action ACTION_LIST_IN_ORDER[] = { // the list of possible actions in a sorted order
MOVE_LEFT,
MOVE_RIGHT,
SOFT_DROP,
HARD_DROP,
ROTATE_CW,
ROTATE_CCW,
ROTATE_180,
ROTATE_0,
HOLD,
PAUSE,
RETRY
};
static const std::string ACTION_NAMES[] = { // name representation for each actions
"Pause",
"Retry",
"Hold",
"Soft drop",
"Hard drop",
"Move left",
"Move right",
"Rotate 0",
"Rotate CW",
"Rotate 180",
"Rotate CCW"
};
/**
* Stream output operator, adds the name of the action
* @return A reference to the output stream
*/
inline std::ostream& operator<<(std::ostream& os, const Action action) {
os << ACTION_NAMES[action];
return os;
}

View File

@@ -1,50 +1,110 @@
#include "Bag.h"
#include "../Pieces/Piece.h"
#include "PiecesList.h"
#include <Vector>
#include <vector>
#include <utility>
#include <cstdlib>
static const double SMALLEST_CONSIDERED_PROPORTION = 0.01; // the smallest a proportion can get before it is considered equal to 0
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);
Bag::Bag(const std::shared_ptr<PiecesList>& piecesList) :
piecesList(piecesList) {
this->highestSize = this->piecesList->getHighestLoadedSize();
this->selectedPieces = this->piecesList->getSelectedPieces();
this->distributionMode = this->piecesList->getDistributionMode();
this->propotionsPerSize = this->piecesList->getProportionsPerSize();
this->currentBags.clear();
this->nextBags.clear();
this->sizesBag.clear();
this->sizesProgression.clear();
if (this->distributionMode == DEFAULT) {
this->currentBags.push_back(this->selectedPieces);
this->nextBags.push_back({});
}
else {
for (int i = 0; i <= this->highestSize; i++) {
this->currentBags.push_back(PieceBag());
this->nextBags.push_back(PieceBag());
this->sizesProgression.push_back(0);
}
for (const auto& piece : this->selectedPieces) {
int pieceSize = this->piecesList->lookAtPiece(piece).getPositions().size();
this->currentBags.at(pieceSize).push_back(piece);
}
}
this->prepareNext();
}
void Bag::jumpToNextBag() {
for (int i = 0; i < this->currentBags.size(); i++) {
if (this->currentBags.at(i).size() < this->nextBags.at(i).size()) {
std::swap(this->currentBags.at(i), this->nextBags.at(i));
}
for (const auto& piece : this->nextBags.at(i)) {
this->currentBags.at(i).push_back(piece);
}
this->nextBags.at(i).clear();
}
this->nextBag.clear();
// prepare first piece
this->prepareNext();
}
Piece Bag::lookNext() {
// return the next piece
return this->pieces.at(this->next);
return this->piecesList->getPiece(this->next);
}
Piece Bag::getNext() {
// get the piece to return
int nextIndex = this->next;
std::pair<int, int> nextIndex = this->next;
// prepare the piece even after the next
this->prepareNext();
// return the next piece
return this->pieces.at(nextIndex);
return this->piecesList->getPiece(nextIndex);
}
void Bag::prepareNext() {
// if the bag is empty switch to the next bag
if (this->currentBag.empty()) {
std::swap(this->currentBag, this->nextBag);
if (this->distributionMode == DEFAULT) {
this->getNextPieceFromBag(0);
}
else {
if (this->sizesBag.empty()) {
for (int i = 0; i <= this->highestSize; i++) {
if (this->propotionsPerSize.at(i) >= SMALLEST_CONSIDERED_PROPORTION
&& !(this->currentBags.at(i).empty() && this->nextBags.at(i).empty())) {
while (this->sizesProgression.at(i) < 1) {
this->sizesBag.push_back(i);
this->sizesProgression.at(i) += this->propotionsPerSize.at(i);
}
this->sizesProgression.at(i) -= 1;
}
}
}
int nextSizeIndex = std::rand() % this->sizesBag.size();
int nextSize = this->sizesBag.at(nextSizeIndex);
this->sizesBag.erase(this->sizesBag.begin() + nextSizeIndex);
this->getNextPieceFromBag(nextSize);
}
}
void Bag::getNextPieceFromBag(int bagIndex) {
if (this->currentBags.at(bagIndex).empty()) {
std::swap(this->currentBags.at(bagIndex), this->nextBags.at(bagIndex));
}
// pick a random piece from the current bag
int indexIndex = std::rand() % this->currentBag.size();
this->next = this->currentBag.at(indexIndex);
int indexIndex = std::rand() % this->currentBags.at(bagIndex).size();
this->next = this->currentBags.at(bagIndex).at(indexIndex);
// move the piece over to the next bag
this->nextBag.push_back(this->next);
this->currentBag.erase(this->currentBag.begin() + indexIndex);
this->nextBags.at(bagIndex).push_back(this->next);
this->currentBags.at(bagIndex).erase(this->currentBags.at(bagIndex).begin() + indexIndex);
}

View File

@@ -1,8 +1,14 @@
#pragma once
#include "../Pieces/Piece.h"
#include "PiecesList.h"
#include "DistributionMode.h"
#include <Vector>
#include <vector>
#include <memory>
#include <utility>
using PieceBag = std::vector<std::pair<int, int>>;
/**
@@ -10,30 +16,48 @@
*/
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
std::shared_ptr<PiecesList> piecesList; // the list of loaded pieces
int highestSize; // the highest size of piece in the bag
std::vector<std::pair<int, int>> selectedPieces; // the list of pieces that can be given to the player
DistributionMode distributionMode; // the distribution mode
std::vector<double> propotionsPerSize; // the proportion of pieces for each size
std::pair<int, int> next; // the next piece to give
std::vector<PieceBag> currentBags; // for each size, the list of pieces that are still to be taken out before starting a new bag
std::vector<PieceBag> nextBags; // for each size, the list of pieces that have been taken out of the current bag and have been placed in the next
std::vector<int> sizesBag; // the list each of bags that are still to have a piece taken out of them
std::vector<double> sizesProgression; // how close each size is to meet its quota of pieces
public:
/**
* Creates a new bag of the specified list of pieces
* Creates a new bag with the pieces currently selected in the piece list
*/
Bag(const std::vector<Piece>& pieces);
Bag(const std::shared_ptr<PiecesList>& piecesList);
/**
* Looks at what the next picked piece will be
* Ignores the remaining pieces in the current bag and start fresh from a new bag
*/
void jumpToNextBag();
/**
* Looks at what the next picked piece will be, without removing it from the bag
* @return The next piece
*/
Piece lookNext();
/**
* Picks a new piece from the current bag
* Picks a new piece from the current bag, removing it from the bag
* @return The next piece
*/
Piece getNext();
private:
/**
* Prepare the next picked piece in advance
* Prepares the next picked piece in advance
*/
void prepareNext();
/**
* Gets the next picked piece from the specified bag
*/
void getNextPieceFromBag(int bagIndex);
};

View File

@@ -2,64 +2,68 @@
#include "../Pieces/Piece.h"
#include <Vector>
#include <Set>
#include <vector>
#include <set>
#include <iostream>
Board::Board(int width, int height) : width(width), height(height) {
std::vector<Color> emptyRow;
Board::Board(int width, int height) :
width(width),
height(height) {
this->emptyRow = std::vector<Block>(width);
for (int i = 0; i < width; i ++) {
emptyRow.push_back(NOTHING);
this->emptyRow.push_back(NOTHING);
}
// initialize grid
this->grid.clear();
for (int j = 0; j < height; j++) {
this->grid.push_back(emptyRow);
}
this->clearBoard();
}
void Board::addBlock(const Cell& position, Color block) {
// if the block is out of bounds we discard it
void Board::changeBlock(const Position& position, Block block) {
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);
this->grid.push_back(this->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);
void Board::insertRow(int height, int holePosition, Block blockType) {
std::vector<Block> insertedRow;
for (int i = 0; i < this->width; i++) {
if (i == holePosition) {
insertedRow.push_back(NOTHING);
}
else {
insertedRow.push_back(blockType);
}
}
// check from top to bottom
this->grid.insert(this->grid.begin() + height, insertedRow);
}
int Board::clearRows() {
// check from top to bottom, so that erasing lines don't screw up the looping
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++) {
bool lineIsFull = true;
int i = 0;
while (lineIsFull && (i < width)) {
if (this->grid.at(j).at(i) == NOTHING) {
isFull = false;
lineIsFull = false;
}
i++;
}
// if it has, erase it and add a new row at the top
if (isFull) {
if (lineIsFull) {
this->grid.erase(this->grid.begin() + j);
if(this->grid.size() < height) this->grid.push_back(emptyRow);
if(this->grid.size() < this->height) {
this->grid.push_back(this->emptyRow);
}
clearedLines++;
}
}
@@ -67,21 +71,29 @@ int Board::clearRows() {
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;
void Board::clearBoard() {
this->grid.clear();
for (int j = 0; j < this->height; j++) {
this->grid.push_back(this->emptyRow);
}
}
Block Board::getBlock(const Position& position) const {
if (position.x < 0 || position.x >= this->width || position.y < 0) return OUT_OF_BOUNDS;
// 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 {
const std::vector<std::vector<Block>>& Board::getBlocks() const {
return this->grid;
}
int Board::getWidth() const {
return this->width;
}
int Board::getGridHeight() const {
return this->grid.size();
}
@@ -90,16 +102,11 @@ 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];
Block block = board.grid.at(y).at(x);
os << getConsoleColorCode(block);
if (block != NOTHING) {
os << "*";
}
@@ -110,8 +117,7 @@ std::ostream& operator<<(std::ostream& os, const Board& board) {
os << std::endl;
}
// reset console color
os << COLOR_RESET;
os << getResetConsoleColorCode();
return os;
}

View File

@@ -2,7 +2,7 @@
#include "../Pieces/Piece.h"
#include <Vector>
#include <vector>
#include <iostream>
@@ -11,9 +11,10 @@
*/
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
std::vector<std::vector<Block>> grid; // the grid, (0,0) is downleft
std::vector<Block> emptyRow; // an empty row of blocks
int width; // the width of the grid
int height; // the base height of the grid, which can extend indefinitely
public:
/**
@@ -22,42 +23,54 @@ class Board {
Board(int width, int height);
/**
* Change the color of the specified block, if the block is out of bounds it is simply ignored
* Changes the block at the specified position, if the block is out of bounds it is simply ignored
*/
void addBlock(const Cell& position, Color block);
void changeBlock(const Position& position, Block block);
/**
* Clears any complete row and moves down the rows on top, returns the number of cleared rows
* Inserts a row of the specified block type (unless on the specified column that has a hole), at the specified height
*/
void insertRow(int height, int holePosition, Block blockType);
/**
* Clears any complete row and moves down the rows on top
* @return The number of cleared rows
*/
int clearRows();
/**
* Returns the color of the block at the specified position
* Deletes any block currently on the board
*/
Color getBlock(const Cell& position) const;
void clearBoard();
/**
* Returns a copy of the grid
* @return The block at the specified position
*/
std::vector<std::vector<Color>> getBlocks() const;
Block getBlock(const Position& position) const;
/**
* Returns the actual height of the grid
* @return The grid
*/
int getGridHeight() const;
const std::vector<std::vector<Block>>& getBlocks() const;
/**
* Returns the base height of the grid
*/
int getBaseHeight() const;
/**
* Returns the width of the grid
* @return The width of the grid
*/
int getWidth() const;
/**
* @return The actual height of the grid
*/
int getGridHeight() const;
/**
* @return The base height of the grid
*/
int getBaseHeight() const;
/**
* Stream output operator, adds a 2D grid representing the board
* @return A reference to the output stream
*/
friend std::ostream& operator<<(std::ostream& os, const Board& board);
};

View File

@@ -0,0 +1,27 @@
#pragma once
#include <string>
/**
* The modes of pieces distribution managed by the app
*/
enum DistributionMode {
DEFAULT,
UNIFORM,
CUSTOM
};
/**
* @return A string containing the name of the given distribution mode
*/
inline std::string getPiecesDistributionName(DistributionMode distributionMode) {
static const std::string DISTRIBUTION_NAMES[] = {
"DEFAULT",
"UNIFORM",
"CUSTOM"
};
return DISTRIBUTION_NAMES[distributionMode];
}

View File

@@ -4,45 +4,59 @@
#include "GameParameters.h"
#include "Action.h"
#include <Vector>
#include <set>
#include <algorithm>
#include <memory>
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 SUBPX_PER_ROW = 60; // the number of position the active piece can take "between" two rows
static const int LOCK_DELAY_SCORE = 1; // the score gained when the piece locks due to lock delay
static const int HARD_DROP_SCORE = 10; // the score gained when the piece locks due to an hard drop
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)
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
Game::Game(Gamemode gamemode, const Player& controls, int boardWidth, int boardHeight, const std::shared_ptr<PiecesList>& piecesList) :
parameters(gamemode, controls),
board(boardWidth, boardHeight, piecesList, parameters.getNextQueueLength()) {
this->initialize();
}
void Game::start() {
this->started = true;
this->leftARETime = 1;
}
void Game::reset() {
this->initialize();
this->parameters.reset();
this->board.reset();
}
void Game::initialize() {
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() {
this->started = true;
this->lost = this->board.spawnNextPiece();
}
void Game::nextFrame(const std::set<Action>& playerActions) {
if (this->lost || this->hasWon()) return;
bool pieceJustLocked = false;
if (this->started) {
bool AREJustEnded = (this->leftARETime == 1);
if (this->leftARETime > 0) {
@@ -51,57 +65,73 @@ void Game::nextFrame(const std::set<Action>& playerActions) {
if (this->leftARETime == 0) {
if (AREJustEnded) {
this->board.spawnNextPiece();
this->lost = this->board.spawnNextPiece();
this->resetPiece(true);
}
/* IRS and IHS */
Rotation initialRotation = NONE
+ (this->initialActions.contains(ROTATE_CW)) ? CLOCKWISE : NONE
+ (this->initialActions.contains(ROTATE_180)) ? DOUBLE : NONE
+ (this->initialActions.contains(ROTATE_CCW)) ? COUNTERCLOCKWISE : NONE;
+ ((this->initialActions.contains(ROTATE_CW)) ? CLOCKWISE : NONE)
+ ((this->initialActions.contains(ROTATE_180)) ? DOUBLE : NONE)
+ ((this->initialActions.contains(ROTATE_CCW)) ? COUNTERCLOCKWISE : NONE);
if (this->initialActions.contains(HOLD)) {
if (this->board.hold(initialRotation)) {
this->subVerticalPosition = 0;
this->totalLockDelay = 0;
this->heldARR = 0;
}
this->lost = (!this->board.hold(initialRotation));
}
else {
if (initialRotation != NONE) {
this->board.rotate(initialRotation);
if ((initialRotation != NONE) || this->initialActions.contains(ROTATE_0)) {
this->lost = (!this->board.rotate(initialRotation));
}
}
if (this->lost) {
if (initialRotation == NONE) {
this->board.rotate(NONE);
this->lost = this->board.activePieceInWall();
if (this->lost) {
this->framesPassed++;
return;
}
}
}
/* HOLD */
if (playerActions.contains(HOLD) && (!this->heldActions.contains(HOLD))) {
if (this->board.hold()) {
this->subVerticalPosition = 0;
this->totalLockDelay = 0;
this->heldARR = 0;
this->resetPiece(false);
}
}
/* MOVE LEFT/RIGHT */
if (playerActions.contains(MOVE_LEFT)) {
this->movePiece(-1, (this->heldDAS >= 0));
Position before = this->board.getActivePiecePosition();
if (this->heldDAS >= 0) {
if (playerActions.contains(MOVE_LEFT) && (!heldActions.contains(MOVE_LEFT))) this->movePiece(-1);
else if (playerActions.contains(MOVE_RIGHT)) this->movePiece(1);
else this->heldDAS = 0;
}
if (playerActions.contains(MOVE_RIGHT)) {
this->movePiece(1, (this->heldDAS <= 0));
else if (this->heldDAS < 0) {
if (playerActions.contains(MOVE_RIGHT) && (!heldActions.contains(MOVE_RIGHT))) this->movePiece(1);
else if (playerActions.contains(MOVE_LEFT)) this->movePiece(-1);
else this->heldDAS = 0;
}
else {
this->heldDAS = 0;
if (before != this->board.getActivePiecePosition()) {
this->totalLockDelay = 0;
}
/* ROTATIONS */
if (playerActions.contains(ROTATE_0) && (!this->heldActions.contains(ROTATE_0))) {
this->rotatePiece(NONE);
}
if (playerActions.contains(ROTATE_CW) && (!this->heldActions.contains(ROTATE_CW))) {
this->board.rotate(CLOCKWISE);
this->rotatePiece(CLOCKWISE);
}
if (playerActions.contains(ROTATE_180) && (!this->heldActions.contains(ROTATE_180))) {
this->board.rotate(DOUBLE);
this->rotatePiece(DOUBLE);
}
if (playerActions.contains(ROTATE_CCW) && (!this->heldActions.contains(ROTATE_CCW))) {
this->board.rotate(COUNTERCLOCKWISE);
this->rotatePiece(COUNTERCLOCKWISE);
}
/* SOFT DROP */
@@ -110,16 +140,14 @@ void Game::nextFrame(const std::set<Action>& playerActions) {
// SDR=0 -> instant drop
if (appliedSDR == 0) {
while (this->board.moveDown()) {
this->score += SOFT_DROP_SCORE;
}
while (this->board.moveDown());
}
// SDR>1 -> move down by specified amount
else {
this->subVerticalPosition += (SUBPX_PER_ROW / appliedSDR);
while (this->subVerticalPosition >= SUBPX_PER_ROW) {
this->board.moveDown();
this->subVerticalPosition -= SUBPX_PER_ROW;
this->score += (this->board.moveDown() * SOFT_DROP_SCORE);
}
}
}
@@ -127,23 +155,28 @@ void Game::nextFrame(const std::set<Action>& playerActions) {
/* 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;
}
while (this->board.moveDown());
this->lockPiece();
pieceJustLocked = true;
this->score += HARD_DROP_SCORE;
}
// no need to apply gravity and lock delay if the piece was hard dropped
else {
/* GRAVITY */
// parameters.getGravity() gives the gravity for an assumed 20-line high board
int appliedGravity = this->parameters.getGravity() * (this->board.getBoard().getBaseHeight() / 20.0);
this->subVerticalPosition += appliedGravity;
while (this->subVerticalPosition >= SUBPX_PER_ROW) {
this->subVerticalPosition -= SUBPX_PER_ROW;
this->board.moveDown();
if (this->parameters.getLevel() >= 20) {
while (this->board.moveDown());
}
else {
// parameters.getGravity() gives the gravity for an assumed 20-line high board
int appliedGravity = this->parameters.getGravity() * std::max((double) this->board.getBoard().getBaseHeight() / 20.0, 1.0);
this->subVerticalPosition += appliedGravity;
while (this->subVerticalPosition >= SUBPX_PER_ROW) {
this->subVerticalPosition -= SUBPX_PER_ROW;
this->board.moveDown();
}
}
/* LOCK DELAY */
if (this->board.touchesGround()) {
this->totalLockDelay++;
@@ -155,59 +188,85 @@ void Game::nextFrame(const std::set<Action>& playerActions) {
if ((this->totalLockDelay > this->parameters.getLockDelay()) || (this->totalForcedLockDelay > this->parameters.getForcedLockDelay())) {
this->lockPiece();
pieceJustLocked = true;
this->score += LOCK_DELAY_SCORE;
}
}
// remove initial actions only once they've been applied
if (AREJustEnded) {
this->initialActions.clear();
}
}
this->framesPassed++;
if (this->lost) {
return;
}
}
// update remembered actions
if ((!this->started) || this->leftARETime > 0) {
for (Action action : playerActions) {
this->initialActions.insert(action);
if ((!pieceJustLocked) && (!heldActions.contains(action))) {
this->initialActions.insert(action);
}
}
if (this->heldDAS >= 0) {
if (playerActions.contains(MOVE_LEFT)) this->heldDAS = -1;
else if (playerActions.contains(MOVE_RIGHT)) this->heldDAS++;
else this->heldDAS = 0;
}
else if (this->heldDAS < 0) {
if (playerActions.contains(MOVE_RIGHT)) this->heldDAS = +1;
else if (playerActions.contains(MOVE_LEFT)) this->heldDAS--;
else this->heldDAS = 0;
}
}
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) {
void Game::resetPiece(bool newPiece) {
int appliedDAS = this->parameters.getDAS();
this->subVerticalPosition = 0;
this->totalLockDelay = 0;
if (newPiece) {
this->totalForcedLockDelay = 0;
}
if (abs(this->heldDAS) > appliedDAS) {
this->heldDAS = (this->heldDAS > 0) ? (+appliedDAS) : (-appliedDAS);
}
this->heldARR = 0;
}
void Game::movePiece(int movement) {
int appliedDAS = this->parameters.getDAS();
int appliedARR = this->parameters.getARR();
if ((this->heldDAS * movement) <= 0) {
this->heldDAS = movement;
this->heldARR = 0;
if (movement == -1) this->board.moveLeft();
if (movement == 1) this->board.moveRight();
}
else {
this->heldDAS += movement;
}
if (abs(this->heldDAS) > this->parameters.getDAS()) {
int appliedARR = this->parameters.getARR();
if (abs(this->heldDAS) > appliedDAS) {
// ARR=0 -> instant movement
if (appliedARR == 0) {
if (movement == -1) while (this->board.moveLeft());
if (movement == 1) while (this->board.moveRight());
}
// ARR>1 -> move by specified amount
else {
this->heldARR++;
if (this->heldARR == appliedARR) {
if (abs(this->heldDAS) > appliedDAS + 1) {
this->heldARR++;
}
if ((this->heldARR == appliedARR) || (abs(this->heldDAS) == (appliedDAS + 1))) {
this->heldARR = 0;
if (movement == -1) this->board.moveLeft();
if (movement == 1) this->board.moveRight();
@@ -216,84 +275,115 @@ void Game::movePiece(int movement, bool resetDirection) {
}
}
void Game::rotatePiece(Rotation rotation) {
Position before = this->board.getActivePiecePosition();
if (this->board.rotate(rotation)) {
this->totalLockDelay = 0;
if (before != this->board.getActivePiecePosition()) {
this->subVerticalPosition = 0;
}
}
}
void Game::lockPiece() {
LineClear clear = this->board.lockPiece();
this->parameters.clearLines(clear.lines);
this->parameters.lockedPiece(clear);
// update B2B and score
bool B2BConditions = ((clear.lines > B2B_MIN_LINE_NUMBER) || clear.isSpin || clear.isMiniSpin);
if (clear.lines > 0) {
bool B2BConditionsAreMet = ((clear.lines >= B2B_MIN_LINE_NUMBER) || clear.isSpin || clear.isMiniSpin);
/* clearing one more line is worth 2x more
clearing with a spin is worth as much as clearing 2x more lines */
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;
if (this->B2BChain && B2BConditionsAreMet) {
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();
this->B2BChain = B2BConditionsAreMet;
}
if (!this->hasWon()) {
this->leftARETime = this->parameters.getARE();
if (this->leftARETime == 0) {
this->lost = this->board.spawnNextPiece();
this->resetPiece(true);
if (this->lost) {
this->board.rotate(NONE);
this->lost = this->board.activePieceInWall();
}
}
}
}
bool Game::hasWon() {
bool Game::hasWon() const {
return this->parameters.hasWon(this->framesPassed);
}
bool Game::hasLost() {
bool Game::hasLost() const {
return this->lost;
}
int Game::getClearedLines() {
int Game::getClearedLines() const {
return this->parameters.getClearedLines();
}
int Game::getLevel() {
int Game::getGrade() const {
return this->parameters.getGrade();
}
int Game::getLevel() const {
return this->parameters.getLevel();
}
int Game::getFramesPassed() {
int Game::getFramesPassed() const {
return this->framesPassed;
}
int Game::getScore() {
int Game::getScore() const {
return this->score;
}
bool Game::isOnB2BChain() {
bool Game::isOnB2BChain() const {
return this->B2BChain;
}
bool Game::areBlocksBones() {
float Game::getLockDelayProgression() const {
return (float) this->totalLockDelay / this->parameters.getLockDelay();
}
float Game::getForcedLockDelayProgression() const {
return (float) this->totalForcedLockDelay / this->parameters.getForcedLockDelay();
}
bool Game::areBlocksBones() const {
return this->parameters.getBoneBlocks();
}
Board Game::getBoard() {
const Board& Game::getBoard() const {
return this->board.getBoard();
}
Piece Game::getActivePiece() {
const std::shared_ptr<Piece>& Game::getActivePiece() const {
return this->board.getActivePiece();
}
Cell Game::getActivePiecePosition() {
const Position& Game::getActivePiecePosition() const {
return this->board.getActivePiecePosition();
}
Piece Game::getHeldPiece() {
Position Game::getGhostPiecePosition() const {
return this->board.lowestPosition();
}
const std::shared_ptr<Piece>& Game::getHeldPiece() const {
return this->board.getHeldPiece();
}
std::vector<Piece> Game::getNextPieces() {
const std::vector<Piece>& Game::getNextPieces() const {
return this->board.getNextPieces();
}

View File

@@ -4,7 +4,8 @@
#include "GameParameters.h"
#include "Action.h"
#include <Vector>
#include <vector>
#include <memory>
/**
@@ -12,27 +13,27 @@
*/
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
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
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);
Game(Gamemode gamemode, const Player& controls, int boardWidth, int boardHeight, const std::shared_ptr<PiecesList>& piecesList);
/**
* Starts the game
@@ -40,85 +41,128 @@ class Game {
void start();
/**
* Advance to the next frame while excecuting the actions taken by the player,
* Resets the game
*/
void reset();
private:
/**
* Initializes the game
*/
void initialize();
public:
/**
* Advances 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
* Resets the piece's parameter
*/
void movePiece(int movement, bool resetDirection);
void resetPiece(bool newPiece);
/**
* Locks the piece, updates level and score and spawn the next piece if necessary
* Moves the piece in the specified direction (1 for right and -1 for left)
*/
void movePiece(int movement);
/**
* Rotates the piece with the specified rotation
*/
void rotatePiece(Rotation rotation);
/**
* Locks the piece, updates level and score and spawns the next piece if necessary
*/
void lockPiece();
public:
/**
* Returns wheter the player has won
* @return If the player has won
*/
bool hasWon();
bool hasWon() const;
/**
* Returns wheter the player has lost
* @return If the player has lost
*/
bool hasLost();
bool hasLost() const;
/**
* Returns the current level
* @return The current level
*/
int getLevel();
int getLevel() const;
/**
* Returns the current number of cleared lines
* @return The current number of cleared lines
*/
int getClearedLines();
int getClearedLines() const;
/**
* Returns the number of frames passed since the start of the game
* @return The current grade
*/
int getFramesPassed();
int getGrade() const;
/**
* Returns the current score
* @return The number of frames passed since the start of the game
*/
int getScore();
int getFramesPassed() const;
/**
* Returns wheter the player is currently on a B2B chain
* @return The current score
*/
bool isOnB2BChain();
int getScore() const;
/**
* Returns wheter all blocks are currently bone blocks
* @return If the player is currently on a B2B chain
*/
bool areBlocksBones();
bool isOnB2BChain() const;
/**
* Returns a copy of the board
* @return How close the active piece's lock delay is to the maximum allowed, betwwen 0 and 1
*/
Board getBoard();
float getLockDelayProgression() const;
/**
* Returns a copy of the active piece
* @return How close the active piece's forced lock delay is to the maximum allowed, betwwen 0 and 1
*/
Piece getActivePiece();
float getForcedLockDelayProgression() const;
/**
* Returns a copy of the active piece position
* @return If all blocks are currently bone blocks
*/
Cell getActivePiecePosition();
bool areBlocksBones() const;
/**
* Returns a copy of the held piece
* @return The board
*/
Piece getHeldPiece();
const Board& getBoard() const;
/**
* Return a copy of the next pieces queue
* @return A pointer to the active piece, can be null
*/
std::vector<Piece> getNextPieces();
const std::shared_ptr<Piece>& getActivePiece() const;
/**
* @return The position of the active piece
*/
const Position& getActivePiecePosition() const;
/**
* @return The position of the ghost piece
*/
Position getGhostPiecePosition() const;
/**
* @return A pointer to the held piece, can be null
*/
const std::shared_ptr<Piece>& getHeldPiece() const;
/**
* @return The next piece queue, can be empty
*/
const std::vector<Piece>& getNextPieces() const;
};

View File

@@ -5,22 +5,44 @@
#include "Bag.h"
#include "LineClear.h"
#include <Vector>
#include <Set>
#include <vector>
#include <set>
#include <memory>
#include <utility>
#include <cstdlib>
GameBoard::GameBoard(int boardWidth, int boardHeight, const std::vector<Piece>& bag, int nextQueueLength) : board(boardWidth, boardHeight), generator(bag), nextQueueLength(nextQueueLength) {
// initialize queue
GameBoard::GameBoard(int boardWidth, int boardHeight, const std::shared_ptr<PiecesList>& piecesList, int nextQueueLength) :
board(boardWidth, boardHeight),
generator(piecesList),
nextQueueLength(nextQueueLength) {
this->initialize();
}
void GameBoard::reset() {
this->board.clearBoard();
this->generator.jumpToNextBag();
this->initialize();
}
void GameBoard::initialize() {
this->nextQueue.clear();
for (int i = 0; i < nextQueueLength; i++) {
this->nextQueue.push_back(this->generator.getNext());
}
this->activePiece = nullptr;
this->heldPiece = nullptr;
this->isLastMoveKick = false;
this->movedLeftLast = false;
}
bool GameBoard::moveLeft() {
// check if the piece can be moved one cell left
if (this->isActivePieceInWall(Cell{-1, 0})) {
this->movedLeftLast = true;
if (this->activePieceInWall(Position{-1, 0})) {
return false;
}
else {
@@ -31,8 +53,9 @@ bool GameBoard::moveLeft() {
}
bool GameBoard::moveRight() {
// check if the piece can be moved one cell right
if (this->isActivePieceInWall(Cell{1, 0})) {
this->movedLeftLast = false;
if (this->activePieceInWall(Position{1, 0})) {
return false;
}
else {
@@ -43,8 +66,7 @@ bool GameBoard::moveRight() {
}
bool GameBoard::moveDown() {
// check if the piece can be moved one cell down
if (this->isActivePieceInWall(Cell{0, -1})) {
if (this->activePieceInWall(Position{0, -1})) {
return false;
}
else {
@@ -55,36 +77,48 @@ bool GameBoard::moveDown() {
}
bool GameBoard::rotate(Rotation rotation) {
// copy the original piece before rotating it
Piece stored = *this->activePiece;
this->rotate(rotation);
this->activePiece->rotate(rotation);
// check if the piece can rotate
if (!this->isActivePieceInWall()) {
this->isLastMoveKick = false;
return true;
// before trying to kick, check if the piece can rotate without kicking
if (rotation == NONE) {
if (this->moveDown()) {
this->isLastMoveKick = false;
return true;
}
}
else {
if (!this->activePieceInWall()) {
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});
std::set<Position> safePositions;
for (Position position : stored.getPositions()) {
Position positionInGrid(position + this->activePiecePosition);
safePositions.insert(positionInGrid);
safePositions.insert(positionInGrid + Position{0, 1});
safePositions.insert(positionInGrid + Position{1, 0});
safePositions.insert(positionInGrid + Position{0, -1});
safePositions.insert(positionInGrid + Position{-1, 0});
}
// try kicking the piece down
bool suceeded = this->tryKicking(true, safeCells);
// first try kicking the piece down
if (rotation == NONE) {
this->activePiecePosition.y -= 1;
}
bool suceeded = this->tryKicking(true, safePositions);
if (suceeded) {
this->isLastMoveKick = true;
return true;
}
// if it doesn't work try kicking the piece up
suceeded = this->tryKicking(false, safeCells);
if (rotation == NONE) {
this->activePiecePosition.y += 1;
}
suceeded = this->tryKicking(false, safePositions);
if (suceeded) {
this->isLastMoveKick = true;
return true;
@@ -92,74 +126,79 @@ bool GameBoard::rotate(Rotation rotation) {
// if it still doesn't work, abort the rotation
this->activePiece = std::make_shared<Piece>(stored);
return false;
if (rotation == NONE) {
this->isLastMoveKick = false;
}
return (rotation == NONE);
}
bool GameBoard::tryKicking(bool testingBottom, const std::set<Cell>& safeCells) {
// we try from the original height of the piece, moving vertically as long as the kicked piece touches the original
bool GameBoard::tryKicking(bool testingBottom, const std::set<Position>& safePositions) {
// we try from the original height of the piece, moving vertically as long as the kicked piece touches the original at least once on this row
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;
int i = (j == 0) ? 1 : 0;
do {
// check right before right arbitrarly, we don't decide this with rotations since it would still be arbitrary with 180° rotations
if (overlapsRight) {
Cell shift{+i, j};
// the kicked position must touch the original piece
if (!this->activePieceOverlapsOneCell(safeCells, shift)) {
overlapsLeft = false;
// we first check the side to which the player moved last
if (movedLeftLast) {
if (overlapsLeft) {
if (this->tryFittingKickedPiece(safePositions, Position({-i, j}), overlapsLeft)) return true;
}
else {
// if the position is valid we place the active piece there
if (!this->isActivePieceInWall(shift)) {
this->activePiecePosition += shift;
return true;
}
if (overlapsRight) {
if (this->tryFittingKickedPiece(safePositions, Position({+i, j}), overlapsRight)) return true;
}
}
// do the same on the left side
if (overlapsLeft) {
Cell shift{-i, j};
if (!this->activePieceOverlapsOneCell(safeCells, shift)) {
overlapsLeft = false;
else {
if (overlapsRight) {
if (this->tryFittingKickedPiece(safePositions, Position({+i, j}), overlapsRight)) return true;
}
else {
if (!this->isActivePieceInWall(shift)) {
this->activePiecePosition += shift;
return true;
}
if (overlapsLeft) {
if (this->tryFittingKickedPiece(safePositions, Position({-i, j}), overlapsLeft)) 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::tryFittingKickedPiece(const std::set<Position>& safePositions, const Position& shift, bool& overlaps) {
if (!this->activePieceOverlaps(safePositions, shift)) {
overlaps = false;
}
else {
if (!this->activePieceInWall(shift)) {
this->activePiecePosition += shift;
return true;
}
}
return false;
}
bool GameBoard::activePieceOverlaps(const std::set<Position>& safePositions, const Position& shift) const {
for (Position position : this->activePiece->getPositions()) {
if (safePositions.contains(position + this->activePiecePosition + shift)) return true;
}
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
bool isFirstTimeHolding = (this->activePiece == nullptr);
if (isFirstTimeHolding) {
// try with the next piece in queue since there is no piece in the hold box yet
if (this->nextQueueLength == 0) {
this->activePiece = std::make_shared<Piece>(this->generator.lookNext());
}
@@ -168,141 +207,140 @@ bool GameBoard::hold(Rotation initialRotation) {
}
}
// set the spawned piece to the correct position
this->goToSpawnPosition();
// apply initial rotation
Piece stored = *this->activePiece;
Position storedPosition = this->activePiecePosition;
this->goToSpawnPosition();
this->rotate(initialRotation);
// if the piece can't spawn, abort initial rotation
if (this->isActivePieceInWall()) {
if (this->activePieceInWall()) {
this->activePiece = std::make_shared<Piece>(stored);
this->goToSpawnPosition();
// if the piece still can't spawn, abort holding
if (this->isActivePieceInWall()) {
if (this->activePieceInWall()) {
if (isFirstTimeHolding) {
this->activePiece = nullptr;
}
std::swap(this->activePiece, this->heldPiece);
this->activePiecePosition = storedPosition;
return false;
}
}
// if it's the first time holding, confirm we keep this piece
if (isFirstTimeHolding) {
if (this->nextQueueLength == 0) {
this->generator.getNext();
}
else {
this->spawnNextPiece();
}
// confirm we keep the piece we tried with
this->nextQueue.push_back(this->generator.getNext());
this->nextQueue.erase(this->nextQueue.begin());
}
// this piece has done nothing yet
this->heldPiece->defaultRotation();
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();
return this->activePieceInWall();
}
bool GameBoard::touchesGround() {
return this->isActivePieceInWall(Cell{0, -1});
bool GameBoard::activePieceInWall(const Position& shift) const {
for (Position position : this->activePiece->getPositions()) {
if (this->board.getBlock(position + this->activePiecePosition + shift) != NOTHING) return true;
}
return false;
}
bool GameBoard::touchesGround() const {
return this->activePieceInWall(Position{0, -1});
}
Position GameBoard::lowestPosition() const {
Position shift = Position{0, -1};
while (!activePieceInWall(shift)) {
shift.y -= 1;
}
shift.y += 1;
return (this->activePiecePosition + shift);
}
LineClear GameBoard::lockPiece() {
// 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}));
bool isLockedInPlace = (this->activePieceInWall(Position{0, 1}) && this->activePieceInWall(Position{1, 0})
&& this->activePieceInWall(Position{-1, 0}) && this->activePieceInWall(Position{0, -1}));
// put the piece in the board
for (Cell cell : this->activePiece->getPositions()) {
this->board.addBlock(cell + this->activePiecePosition, this->activePiece->getColor());
for (Position position : this->activePiece->getPositions()) {
this->board.changeBlock(position + this->activePiecePosition, this->activePiece->getBlockType());
}
// check for lines to clear
return LineClear{this->board.clearRows(), isLocked, (!isLocked) && this->isLastMoveKick};
this->activePiece = nullptr;
return LineClear{this->board.clearRows(), isLockedInPlace, (!isLockedInPlace) && this->isLastMoveKick};
}
Board GameBoard::getBoard() const {
void GameBoard::addGarbageRows(int number) {
int holePosition = std::rand() % this->board.getWidth();
for (int i = 0; i < number; i++) {
this->board.insertRow(0, holePosition, GARBAGE);
if (this->touchesGround()) {
this->activePiecePosition.y += 1;
}
}
}
const Board& GameBoard::getBoard() const {
return this->board;
}
Piece GameBoard::getActivePiece() const {
return *this->activePiece;
const std::shared_ptr<Piece>& GameBoard::getActivePiece() const {
return this->activePiece;
}
Cell GameBoard::getActivePiecePosition() const {
const Position& GameBoard::getActivePiecePosition() const {
return this->activePiecePosition;
}
Piece GameBoard::getHeldPiece() const {
return *this->heldPiece;
const std::shared_ptr<Piece>& GameBoard::getHeldPiece() const {
return this->heldPiece;
}
std::vector<Piece> GameBoard::getNextPieces() const {
const std::vector<Piece>& GameBoard::getNextPieces() const {
return this->nextQueue;
}
bool GameBoard::isActivePieceInWall(const 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;
int lowestPosition = this->activePiece->getLength() - 1;
for (Position position : this->activePiece->getPositions()) {
if (position.y < lowestPosition) lowestPosition = position.y;
}
// set the piece one line above the board
this->activePiecePosition.y = this->board.getBaseHeight() - lowestCell;
this->activePiecePosition.y = this->board.getBaseHeight() - lowestPosition;
// center the piece horizontally, biased towards left
this->activePiecePosition.x = (this->board.getWidth() - this->activePiece->getLength()) / 2;
this->activePiece->defaultRotation();
}
std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard) {
// print over the board (only the active piece if it is there)
if (gameboard.activePiece != nullptr) {
Block pieceBlockType = gameboard.activePiece->getBlockType();
os << getConsoleColorCode(pieceBlockType);
// 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
// print only the position 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);
bool hasActivePiece = gameboard.activePiece->getPositions().contains(Position{x, y} - gameboard.activePiecePosition);
if (hasActivePiece) {
os << "*";
}
@@ -315,21 +353,19 @@ std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard) {
}
// print the board
Color pieceColor = (gameboard.activePiece == nullptr) ? NOTHING : gameboard.activePiece->getColor();
Block pieceBlockType = (gameboard.activePiece == nullptr) ? NOTHING : gameboard.activePiece->getBlockType();
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);
bool hasActivePiece = (gameboard.activePiece == nullptr) ? false : gameboard.activePiece->getPositions().contains(Position{x, y} - gameboard.activePiecePosition);
// if the active piece is on this cell, print it
// the active piece takes visual priority over the board
if (hasActivePiece) {
os << COLOR_CODES[pieceColor];
os << getConsoleColorCode(pieceBlockType);
os << "*";
}
// else print the cell of the board
else {
Color block = gameboard.board.getBlock(Cell{x, y});
os << COLOR_CODES[block];
Block block = gameboard.board.getBlock(Position{x, y});
os << getConsoleColorCode(block);
if (block != NOTHING) {
os << "*";
}
@@ -341,7 +377,7 @@ std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard) {
os << std::endl;
}
// print held piece
// print hold box
os << "Hold:" << std::endl;
if (!(gameboard.heldPiece == nullptr)) {
os << *gameboard.heldPiece;
@@ -353,8 +389,7 @@ std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard) {
os << piece;
}
// reset console color
os << COLOR_RESET;
os << getResetConsoleColorCode();
return os;
}

View File

@@ -5,7 +5,7 @@
#include "Bag.h"
#include "LineClear.h"
#include <Vector>
#include <vector>
#include <memory>
@@ -14,105 +14,145 @@
*/
class GameBoard {
private:
Board board; // the board in which pieces moves, (0, 0) is downleft
Bag generator; // the piece generator
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
Position 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
bool movedLeftLast; // wheter the last sideway movement was a left one
public:
/**
* Creates a new board, generator, and next queue
*/
GameBoard(int boardWidth, int boardHeight, const std::vector<Piece>& bag, int nextQueueLength);
GameBoard(int boardWidth, int boardHeight, const std::shared_ptr<PiecesList>& piecesList, int nextQueueLength);
/**
* Try moving the piece one cell to the left, and returns wheter it was sucessfull
* Resets the board as if it was newly created
*/
void reset();
private:
/**
* Initializes the board
*/
void initialize();
public:
/**
* Tries moving the piece one position to the left
* @return If it suceeded
*/
bool moveLeft();
/**
* Try moving the piece one cell to the right, and returns wheter it was sucessfull
* Tries moving the piece one position to the right
* @return If it suceeded
*/
bool moveRight();
/**
* Try moving the piece one cell down, and returns wheter it was sucessfull
* Tries moving the piece one position down
* @return If it suceeded
*/
bool moveDown();
/**
* Try rotating the piece and kicking it if necessary, and returns wheter it was sucessfull
* Tries rotating the piece and kicking it if necessary, if it's a 0° rotation, it will forcefully try kicking
* @return If it suceeded
*/
bool rotate(Rotation rotation);
private:
/**
* Try kicking the piece, testing position either above or below the piece's initial position
* Tries kicking the piece, testing position either above or below the piece's initial position
* @return If it suceeded
*/
bool tryKicking(bool testingBottom, const std::set<Cell>& safeCells);
bool tryKicking(bool testingBottom, const std::set<Position>& safePositions);
/**
* Tries fitting the kicked active piece at the specified position
* @return If it suceeded
*/
bool tryFittingKickedPiece(const std::set<Position>& safePositions, const Position& shift, bool& overlaps);
/**
* Check if one of the active piece's positions shifted by a specified position would overlap with a set of positions
* @return If the shifted active piece overlaps with one of the position
*/
bool activePieceOverlaps(const std::set<Position>& safePositions, const Position& shift = Position{0, 0}) const;
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
* Tries holding the active piece or swapping it if one was already stocked, while trying to apply an initial rotation to the newly spawned piece
* @return If it suceeded
*/
bool hold(Rotation initialRotation = NONE);
/**
* Spawns the next piece from the queue, and returns wheter it spawns in a wall
* Spawns the next piece from the queue
* @return If it spawned in a wall
*/
bool spawnNextPiece();
/**
* Returns wheter the active piece is touching walls directly below it
* Checks if one of the active piece's positions touches a wall in the board
* @return If the active piece is in a wall
*/
bool touchesGround();
bool activePieceInWall(const Position& shift = Position{0, 0}) const;
/**
* Lock the active piece into the board and returns the resulting line clear
* Checks is the active piece as a wall directly below one of its position
* @return If it touches a ground
*/
bool touchesGround() const;
/**
* Computes what the piece position would be if it were to be dropped down as much as possible
* @return The lowest position before hitting a wall
*/
Position lowestPosition() const;
/**
* Locks the active piece into the board and clears lines if needed
* @return The resulting line clear
*/
LineClear lockPiece();
/**
* Returns a copy of the board
* Adds a specified number of garbage rows to the bottom of the board, the hole position being random but the same for all of them
*/
Board getBoard() const;
void addGarbageRows(int number);
/**
* Returns a copy of the active piece
* @return The board
*/
Piece getActivePiece() const;
const Board& getBoard() const;
/**
* Returns a copy of the position of the active piece
* @return A pointer to the active piece, can be null
*/
Cell getActivePiecePosition() const;
const std::shared_ptr<Piece>& getActivePiece() const;
/**
* Returns a copy of the held piece
* @return The position of the active piece
*/
Piece getHeldPiece() const;
const Position& getActivePiecePosition() const;
/**
* Returns a copy of the next piece queue
* @return A pointer to the held piece, can be null
*/
std::vector<Piece> getNextPieces() const;
const std::shared_ptr<Piece>& getHeldPiece() const;
/**
* @return The next piece queue, can be empty
*/
const std::vector<Piece>& getNextPieces() const;
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
*/
@@ -121,6 +161,7 @@ class GameBoard {
public:
/**
* Stream output operator, adds the board, the hold box and the next queue
* @return A reference to the output stream
*/
friend std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard);
};

View File

@@ -4,9 +4,17 @@
#include "Player.h"
GameParameters::GameParameters(Gamemode gamemode, const Player& controls) : gamemode(gamemode), controls(controls) {
// initialize lines and level
GameParameters::GameParameters(Gamemode gamemode, const Player& controls) :
gamemode(gamemode),
controls(controls) {
this->reset();
}
void GameParameters::reset() {
this->clearedLines = 0;
this->grade = 0;
switch (this->gamemode) {
// lowest gravity
case SPRINT : {this->level = 1; break;}
@@ -16,36 +24,39 @@ GameParameters::GameParameters(Gamemode gamemode, const Player& controls) : game
case MARATHON : {this->level = 1; break;}
// goes from level 20 to 39
case MASTER : {this->level = 20; break;}
// no gravity
case ZEN : {this->level = 0; break;}
default : this->level = 1;
}
// initialize stats
this->updateStats();
}
void GameParameters::clearLines(int lineNumber) {
// update lines and level
void GameParameters::lockedPiece(const LineClear& lineClear) {
switch (this->gamemode) {
// modes where level increases
case MARATHON :
case MASTER : {
// update cleared lines
int previousLines = this->clearedLines;
this->clearedLines += lineNumber;
this->clearedLines += lineClear.lines;
// level increments every 10 lines, stats only changes on level up
if (previousLines / 10 < this->clearedLines / 10) {
this->level = this->clearedLines / 10;
this->level += (this->clearedLines / 10 - previousLines / 10);
this->updateStats();
}
break;
}
// other modes
default : this->clearedLines += lineNumber;
default : this->clearedLines += lineClear.lines;
}
if (!((lineClear.lines == 0) && ((this->grade % 100) == 99))) {
this->grade += (1 + lineClear.lines);
}
}
bool GameParameters::hasWon(int framesPassed) {
bool GameParameters::hasWon(int framesPassed) const {
switch (this->gamemode) {
// win once 40 lines have been cleared
case SPRINT : return this->clearedLines >= 40;
@@ -55,6 +66,8 @@ bool GameParameters::hasWon(int framesPassed) {
case MARATHON : return this->clearedLines >= 200;
// win once 200 lines have been cleared
case MASTER : return this->clearedLines >= 200;
// infinite mode
case ZEN :
default : return false;
}
}
@@ -62,13 +75,14 @@ bool GameParameters::hasWon(int framesPassed) {
void GameParameters::updateStats() {
/* NEXT QUEUE */
switch (this->gamemode) {
// 5 for rapidity gamemodes
// 5 for fast-controls gamemodes
case SPRINT :
case ULTRA : {
case ULTRA :
case ZEN : {
this->nextQueueLength = 5;
break;
}
// 3 for endurance gamemodes
// 3 for slow-controls gamemodes
case MARATHON :
case MASTER : {
this->nextQueueLength = 3;
@@ -85,40 +99,46 @@ void GameParameters::updateStats() {
}
/* GRAVITY */
if (level >= 20) {
// all levels above 20 are instant gravity
this->gravity = 20 * 60;
// get gravity for an assumed 20-rows board
static const int gravityPerLevel[] = {
0, // lvl0 = no gravity
1, // 60f/line, 20s total
2, // 30f/line, 10s total
3, // 20f/line, 6.66s total
4, // 15f/line, 5s total
5, // 12f/line, 4s total
6, // 10f/line, 3.33 total
7, // 8.57f/line, 2.85s total
8, // 7.5f/line, 2.5s total
10, // 6f/line, 2s total
12, // 5f/line, 1.66s total
14, // 4.28f/line, 1.42s total
17, // 3.52f/line, 1.17s total
20, // 3f/line, 60f total
24, // 2.5f/line, 50f total
30, // 2f/line, 40f total
40, // 1.5f/line, 30f total
1 * 60, // 1line/f, 20f total
2 * 60, // 2line/f, 10f total
4 * 60, // 4line/f, 5f total
20 * 60 // lvl20 = instant gravity
};
if (this->level < 0) {
this->gravity = gravityPerLevel[0];
}
else if (this->level > 20) {
this->gravity = gravityPerLevel[20];
}
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;
}
this->gravity = gravityPerLevel[this->level];
}
/* 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;}
// 10s
case ZEN : {this->lockDelay = 60 * 10; break;}
// 1s by default
default : this->lockDelay = 60;
}
@@ -188,50 +208,54 @@ void GameParameters::updateStats() {
}
}
int GameParameters::getClearedLines() {
int GameParameters::getClearedLines() const {
return this->clearedLines;
}
int GameParameters::getLevel() {
int GameParameters::getLevel() const {
return this->level;
}
int GameParameters::getNextQueueLength() {
int GameParameters::getGrade() const {
return this->grade;
}
int GameParameters::getNextQueueLength() const {
return this->nextQueueLength;
}
bool GameParameters::getBoneBlocks() {
bool GameParameters::getBoneBlocks() const {
return this->boneBlocks;
}
int GameParameters::getGravity() {
int GameParameters::getGravity() const {
return this->gravity;
}
int GameParameters::getLockDelay() {
int GameParameters::getLockDelay() const {
return this->lockDelay;
}
int GameParameters::getForcedLockDelay() {
int GameParameters::getForcedLockDelay() const {
return this->forcedLockDelay;
}
int GameParameters::getARE() {
int GameParameters::getARE() const {
return this->ARE;
}
int GameParameters::getLineARE() {
int GameParameters::getLineARE() const {
return this->lineARE;
}
int GameParameters::getDAS() {
int GameParameters::getDAS() const {
return this->DAS;
}
int GameParameters::getARR() {
int GameParameters::getARR() const {
return this->ARR;
}
int GameParameters::getSDR() {
int GameParameters::getSDR() const {
return this->SDR;
}

View File

@@ -2,6 +2,7 @@
#include "Gamemode.h"
#include "Player.h"
#include "LineClear.h"
/**
@@ -10,20 +11,21 @@
*/
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
Gamemode gamemode; // the current gamemode
Player controls; // the player's controls
int clearedLines; // the number of cleared lines
int level; // the current level
int grade; // the current amount of points
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
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
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:
/**
@@ -32,14 +34,20 @@ class GameParameters {
GameParameters(Gamemode gamemode, const Player& controls);
/**
* Count the newly cleared lines and update level and stats if needed
* Resets all stats and parameters
*/
void clearLines(int lineNumber);
void reset();
/**
* Returns wheter the game ended
* Counts the newly cleared lines and update level and stats if needed
*/
bool hasWon(int framesPassed);
void lockedPiece(const LineClear& lineClear);
/**
* Checks if the game ended based on the current states and time passed, accorind to the gamemode
* @return If the player has won
*/
bool hasWon(int framesPassed) const;
private:
/**
@@ -49,62 +57,67 @@ class GameParameters {
public:
/**
* Returns the current number of cleared line
* @return The current number of cleared line
*/
int getClearedLines();
int getClearedLines() const;
/**
* Returns the current level
* @return The current level
*/
int getLevel();
int getLevel() const;
/**
* @return The current grade
*/
int getGrade() const;
/**
* Returns the length of the next queue
* @return The length of the next queue
*/
int getNextQueueLength();
int getNextQueueLength() const;
/**
* Returns wheter the blocks are currently bone blocks
*/
bool getBoneBlocks();
bool getBoneBlocks() const;
/**
* Returns the current gravity for a 20-line high board
* @return The current gravity for a 20-line high board
*/
int getGravity();
int getGravity() const;
/**
* Returns the current lock delay
* @return The current lock delay
*/
int getLockDelay();
int getLockDelay() const;
/**
* Returns the current forced lock delay
* @return The current forced lock delay
*/
int getForcedLockDelay();
int getForcedLockDelay() const;
/**
* Returns the current ARE
* @return The current ARE
*/
int getARE();
int getARE() const;
/**
* Returns the current line ARE
* @return The current line ARE
*/
int getLineARE();
int getLineARE() const;
/**
* Returns the current DAS
* @return The current DAS
*/
int getDAS();
int getDAS() const;
/**
* Returns the current ARR
* @return The current ARR
*/
int getARR();
int getARR() const;
/**
* Returns the current SDR
* @return The current SDR
*/
int getSDR();
int getSDR() const;
};

View File

@@ -1,5 +1,7 @@
#pragma once
#include <string>
/**
* Every gamemode supported by the game
@@ -8,5 +10,37 @@ enum Gamemode {
SPRINT,
MARATHON,
ULTRA,
MASTER
MASTER,
ZEN
};
/**
* @return A string containing the name of the gamemode
*/
inline std::string getGamemodeName(Gamemode gamemode) {
static const std::string GAMEMODE_NAMES[] = {
"SPRINT",
"MARATHON",
"ULTRA",
"MASTER",
"ZEN"
};
return GAMEMODE_NAMES[gamemode];
}
/**
* @return A tiny string containing the goal of the gamemode
*/
inline std::string getGamemodeGoal(Gamemode gamemode) {
static const std::string GAMEMODE_DESCRIPTIONS[] = {
"40 lines",
"200 lines",
"2 minutes",
"200 lines",
"Chill"
};
return GAMEMODE_DESCRIPTIONS[gamemode];
}

View File

@@ -5,7 +5,7 @@
* 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
int lines; // the number of lines cleared
bool isSpin; // if the move was a spin
bool isMiniSpin; // if the move was a spin mini
};

60
src/Core/Menu.cpp Normal file
View File

@@ -0,0 +1,60 @@
#include "Menu.h"
#include "PiecesList.h"
#include "Player.h"
#include "Game.h"
#include <memory>
static const int DEFAULT_BOARD_WIDTH = 10; // the default width of the board when starting the menu
static const int DEFAULT_BOARD_HEIGHT = 20; // the default height of the board when starting the menu
Menu::Menu() {
this->piecesList = std::make_shared<PiecesList>(PiecesList());
this->boardWidth = DEFAULT_BOARD_WIDTH;
this->boardHeight = DEFAULT_BOARD_HEIGHT;
}
Game Menu::startGame(Gamemode gamemode) const {
return Game(gamemode, this->playerControls, this->boardWidth, this->boardHeight, this->piecesList);
}
bool Menu::setBoardWidth(int width) {
if (width < 1) return false;
this->boardWidth = width;
return true;
}
bool Menu::setBoardHeight(int height) {
if (height < 1) return false;
this->boardHeight = height;
return true;
}
int Menu::getBoardWidth() const {
return this->boardWidth;
}
int Menu::getBoardHeight() const {
return this->boardHeight;
}
Player& Menu::getPlayerControls() {
return this->playerControls;
}
const Player& Menu::readPlayerControls() const {
return this->playerControls;
}
PiecesList& Menu::getPiecesList() {
return *this->piecesList;
}
const PiecesList& Menu::readPiecesList() const {
return *this->piecesList;
}

73
src/Core/Menu.h Normal file
View File

@@ -0,0 +1,73 @@
#pragma once
#include "PiecesList.h"
#include "Player.h"
#include "Game.h"
static const int FRAMES_PER_SECOND = 60; // the number of frames per second, all the values in the app were choosen with this number in mind
/**
* The interface between an UI and the core of the app
*/
class Menu {
private:
std::shared_ptr<PiecesList> piecesList; // the list of pieces used by the app
Player playerControls; // the controls of the player
int boardWidth; // the width of the board for the next game
int boardHeight; // the height of the board for the next game
public:
/**
* Initializes the board size and player controls to their default values
*/
Menu();
/**
* Starts a new game with the current settings
* @return The game that has been created
*/
Game startGame(Gamemode gamemode) const;
/**
* Sets the width of the board, which must be greater than 0
* @return If the width has been changed
*/
bool setBoardWidth(int width);
/**
* Sets the height of the board, which must be greater than 0
* @return If the height has been changed
*/
bool setBoardHeight(int height);
/**
* @return The width of the board
*/
int getBoardWidth() const;
/**
* @return The height of the board
*/
int getBoardHeight() const;
/**
* @return A reference to the player's controls
*/
Player& getPlayerControls();
/**
* @return A reference to the player's controls
*/
const Player& readPlayerControls() const;
/**
* @return A reference to the pieces list
*/
PiecesList& getPiecesList();
/**
* @return A reference to the pieces list
*/
const PiecesList& readPiecesList() const;
};

155
src/Core/PiecesList.cpp Normal file
View File

@@ -0,0 +1,155 @@
#include "PiecesList.h"
#include "../Pieces/Piece.h"
#include "../Pieces/PiecesFiles.h"
#include "DistributionMode.h"
#include <vector>
#include <utility>
PiecesList::PiecesList() {
this->highestLoadedSize = 0;
this->selectedPieces.clear();
this->distributionMode = DEFAULT;
this->proportionsPerSize.clear();
this->customProportionsPerSize.clear();
// we need to have something at index 0 even if there is no pieces of size 0
this->loadedPieces.clear();
this->convexPieces.clear();
this->holelessPieces.clear();
this->otherPieces.clear();
this->pushBackEmptyVectors();
// we always prepare vectors of the next size to be generated
this->pushBackEmptyVectors();
}
bool PiecesList::loadPieces(int size) {
if (size < 1) return false;
PiecesFiles piecesFiles;
for (int i = this->highestLoadedSize + 1; i <= size; i++) {
if (!piecesFiles.loadPieces(i, this->loadedPieces.at(i), this->convexPieces.at(i), this->holelessPieces.at(i), this->otherPieces.at(i))) {
return false;
}
else {
this->highestLoadedSize++;
this->pushBackEmptyVectors();
}
}
return true;
}
bool PiecesList::selectPiece(int size, int number) {
if (size < 1 || size > this->highestLoadedSize || number >= this->loadedPieces.at(size).size()) return false;
this->selectedPieces.push_back(std::pair<int, int>(size, number));
return true;
}
bool PiecesList::selectAllPieces(int size) {
if (size < 1 || size > this->highestLoadedSize) return false;
for (int i = 0; i < this->loadedPieces.at(size).size(); i++) {
this->selectedPieces.push_back(std::pair<int, int>(size, i));
}
return true;
}
bool PiecesList::selectConvexPieces(int size) {
if (size < 1 || size > this->highestLoadedSize) return false;
for (int index : this->convexPieces.at(size)) {
this->selectedPieces.push_back(std::pair<int, int>(size, index));
}
return true;
}
bool PiecesList::selectHolelessPieces(int size) {
if (size < 1 || size > this->highestLoadedSize) return false;
for (int index : this->holelessPieces.at(size)) {
this->selectedPieces.push_back(std::pair<int, int>(size, index));
}
return true;
}
bool PiecesList::selectOtherPieces(int size) {
if (size < 1 || size > this->highestLoadedSize) return false;
for (int index : this->otherPieces.at(size)) {
this->selectedPieces.push_back(std::pair<int, int>(size, index));
}
return true;
}
void PiecesList::unselectAll() {
this->selectedPieces.clear();
}
bool PiecesList::setDistributionMode(DistributionMode distributionMode) {
if (distributionMode == DEFAULT || distributionMode == UNIFORM || distributionMode == CUSTOM) {
this->distributionMode = distributionMode;
return true;
}
return false;
}
int PiecesList::getHighestLoadedSize() const {
return this->highestLoadedSize;
}
int PiecesList::getNumberOfPieces(int size) const {
if (size < 1 || size > this->highestLoadedSize) return 0;
return this->loadedPieces.at(size).size();
}
std::vector<std::pair<int, int>> PiecesList::getSelectedPieces() const {
return this->selectedPieces;
}
DistributionMode PiecesList::getDistributionMode() const {
return this->distributionMode;
}
bool PiecesList::changeCustomDistribution(int size, double distribution) {
if (size < 1 || size > this->highestLoadedSize || distribution > 1) {
return false;
}
this->customProportionsPerSize.at(size) = distribution;
return true;
}
std::vector<double> PiecesList::getProportionsPerSize() const {
if (this->distributionMode == CUSTOM) {
return this->customProportionsPerSize;
}
else {
return this->proportionsPerSize;
}
}
Piece PiecesList::getPiece(const std::pair<int, int>& pieceIndex) const {
return this->loadedPieces.at(pieceIndex.first).at(pieceIndex.second);
}
const Piece& PiecesList::lookAtPiece(const std::pair<int, int>& pieceIndex) const {
return this->loadedPieces.at(pieceIndex.first).at(pieceIndex.second);
}
void PiecesList::pushBackEmptyVectors() {
this->loadedPieces.push_back(std::vector<Piece>());
this->convexPieces.push_back(std::vector<int>());
this->holelessPieces.push_back(std::vector<int>());
this->otherPieces.push_back(std::vector<int>());
this->proportionsPerSize.push_back(1);
this->customProportionsPerSize.push_back(1);
}

126
src/Core/PiecesList.h Normal file
View File

@@ -0,0 +1,126 @@
#pragma once
#include "../Pieces/Piece.h"
#include "DistributionMode.h"
#include <vector>
#include <utility>
/**
* A container for all loaded pieces to prevent loading and copying them multiple times,
* also allows for the player to select a list of pieces to be used in a game
*/
class PiecesList {
private:
int highestLoadedSize; // the highest size of pieces currently loaded
std::vector<std::vector<Piece>> loadedPieces; // every loaded pieces by size
std::vector<std::vector<int>> convexPieces; // the list of convex loaded pieces by size
std::vector<std::vector<int>> holelessPieces; // the list of holeless loaded pieces by size
std::vector<std::vector<int>> otherPieces; // the list of other loaded pieces by size
std::vector<std::pair<int, int>> selectedPieces; // the list of all currently selected pieces
DistributionMode distributionMode; // the current pieces distribution mode
std::vector<double> proportionsPerSize; // the proportion of piece for each sizes
std::vector<double> customProportionsPerSize; // the proportion of piece for each sizes when the distribution mode is set to custom
public:
/**
* Initializes a list of pieces up to size 0 (so no pieces)
*/
PiecesList();
/**
* Makes the list load all pieces of the specified size
* @return If it sucessfully loaded the pieces
*/
bool loadPieces(int size);
/**
* Selects the specified piece
* @return If the piece could be selected
*/
bool selectPiece(int size, int number);
/**
* Selects all pieces of the specified size
* @return If the pieces could be selected
*/
bool selectAllPieces(int size);
/**
* Selects all convex pieces of the specified size
* @return If the pieces could be selected
*/
bool selectConvexPieces(int size);
/**
* Selects all holeless pieces of the specified size
* @return If the pieces could be selected
*/
bool selectHolelessPieces(int size);
/**
* Selects all other pieces of the specified size
* @return If the pieces could be selected
*/
bool selectOtherPieces(int size);
/**
* Unselects all previously selected pieces
*/
void unselectAll();
/**
* Changes the current pieces distribution mode
* @return If the mode is supported
*/
bool setDistributionMode(DistributionMode distributionMode);
/**
* Changes the distribution of the specified size for when the distribution mode is set to custom,
* the specified distribution must lower or equal to 1
* @return If the new distribution was applied
*/
bool changeCustomDistribution(int size, double distribution);
/**
* @return The highest loaded size of pieces
*/
int getHighestLoadedSize() const;
/**
* @return The number of pieces of the specified size
*/
int getNumberOfPieces(int size) const;
/**
* @return A copy of the indexes of all selected pieces
*/
std::vector<std::pair<int, int>> getSelectedPieces() const;
/**
* @return The current distribution mode
*/
DistributionMode getDistributionMode() const;
/**
* @return The proportion of pieces for each loaded size
*/
std::vector<double> getProportionsPerSize() const;
/**
* @return A copy of the piece corresponding to the specified index
*/
Piece getPiece(const std::pair<int, int>& pieceIndex) const;
/**
* @return The piece corresponding to the specified index
*/
const Piece& lookAtPiece(const std::pair<int, int>& pieceIndex) const;
private:
/**
* Adds empty vectors at the end of every pieces list
*/
void pushBackEmptyVectors();
};

View File

@@ -1,12 +1,5 @@
#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
@@ -36,14 +29,14 @@ bool Player::setSDR(int SDR) {
return true;
}
int Player::getDAS() {
int Player::getDAS() const {
return this->DAS;
}
int Player::getARR() {
int Player::getARR() const {
return this->ARR;
}
int Player::getSDR() {
int Player::getSDR() const {
return this->SDR;
}

View File

@@ -1,5 +1,12 @@
#pragma once
static const int DAS_MIN_VALUE = 0; // the minimal selectable DAS value, equals to 0ms
static const int DAS_MAX_VALUE = 30; // the maximal selectable DAS value, equals to 500ms
static const int ARR_MIN_VALUE = 0; // the minimal selectable ARR value, equals to 0ms
static const int ARR_MAX_VALUE = 30; // the maximal selectable ARR value, equals to 500ms
static const int SDR_MIN_VALUE = 0; // the minimal selectable SDR value, equals to 0ms
static const int SDR_MAX_VALUE = 6; // the maximal selectable SDR value, equals to 100ms
/**
* The controls of a player
@@ -17,32 +24,35 @@ class Player {
Player();
/**
* Try setting DAS to the desired value, and returns wheter it is possible
* Try setting DAS to the desired value
* @return If it is possible
*/
bool setDAS(int DAS);
/**
* Try setting ARR to the desired value, and returns wheter it is possible
* Try setting ARR to the desired value
* @return If it is possible
*/
bool setARR(int ARR);
/**
* Try setting SDR to the desired value, and returns wheter it is possible
* Try setting SDR to the desired value
* @return If it is possible
*/
bool setSDR(int SDR);
/**
* Returns DAS value
* @return DAS value
*/
int getDAS();
int getDAS() const;
/**
* Returns ARR value
* @return ARR value
*/
int getARR();
int getARR() const;
/**
* Returns SDR value
* @return SDR value
*/
int getSDR();
int getSDR() const;
};

View File

@@ -0,0 +1,90 @@
#pragma once
#include "../Settings.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <optional>
#include <SFML/Graphics.hpp>
class AppMenu;
using MenuStack = std::stack<std::shared_ptr<AppMenu>>;
class AppMenu {
protected:
std::shared_ptr<MenuStack> menuStack;
std::shared_ptr<Settings> settings;
std::shared_ptr<sf::RenderWindow> renderWindow;
bool enterPressed = false;
bool enterReleased = false;
bool escPressed = false;
bool escReleased = false;
sf::Font pressStartFont = sf::Font("data/fonts/pressstart/prstartk.ttf");
public:
AppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) :
menuStack(menuStack),
settings(settings),
renderWindow(renderWindow)
{
}
virtual void computeFrame() = 0;
virtual void drawFrame() const = 0;
protected:
void updateMetaBinds() {
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Enter)) {
enterPressed = true;
enterReleased = false;
}
else {
enterReleased = enterPressed;
enterPressed = false;
}
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Escape)) {
escPressed = true;
escReleased = false;
}
else {
escReleased = escPressed;
escPressed = false;
}
}
void placeText(sf::Text& text, const std::optional<PlayerCursor>& playerCursor, const sf::String& string, float xPos, float yPos, const std::optional<sf::Vector2u>& cursorPos) const {
float sizeMultiplier = this->settings->getWindowSizeMultiplier();
text.setString(string);
if (playerCursor.has_value() && cursorPos.has_value()) {
text.setOutlineThickness((playerCursor.value().getPosition() == cursorPos.value()) ? (sizeMultiplier / 2) : 0);
}
text.setOrigin(sf::Vector2f({0, text.getLocalBounds().size.y / 2}));
text.setPosition(sf::Vector2f({sizeMultiplier * xPos, sizeMultiplier * yPos}));
this->renderWindow->draw(text);
}
void placeTitle(sf::Text& text, const std::optional<PlayerCursor>& playerCursor, const sf::String& string, float yPos, const std::optional<sf::Vector2u>& cursorPos) const {
float sizeMultiplier = this->settings->getWindowSizeMultiplier();
text.setString(string);
if (playerCursor.has_value() && cursorPos.has_value()) {
text.setOutlineThickness((playerCursor.value().getPosition() == cursorPos.value()) ? (sizeMultiplier / 2) : 0);
}
text.setOrigin({text.getLocalBounds().getCenter().x, text.getLocalBounds().size.y / 2});
text.setPosition(sf::Vector2f({sizeMultiplier * 40.f, sizeMultiplier * yPos}));
this->renderWindow->draw(text);
}
sf::Color getColorOfBlock(Block block, int luminosityShift) const {
Color rgbColor = BLOCKS_COLOR[block];
return sf::Color(std::clamp(rgbColor.red + luminosityShift, 0, 255),
std::clamp(rgbColor.green + luminosityShift, 0, 255),
std::clamp(rgbColor.blue + luminosityShift, 0, 255));
}
};

View File

@@ -0,0 +1,64 @@
#include "GameBoardAppMenu.h"
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
GameBoardAppMenu::GameBoardAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) :
AppMenu(menuStack, settings, renderWindow),
playerCursor({1, 1}) {
}
void GameBoardAppMenu::computeFrame() {
this->updateMetaBinds();
this->playerCursor.updatePosition();
Menu& menu = this->settings->getMenu();
switch (this->playerCursor.getPosition().y) {
case 0 : {
if (this->playerCursor.movedLeft()) {
menu.setBoardWidth(std::max(1, menu.getBoardWidth() - 1));
}
if (this->playerCursor.movedRight()) {
menu.setBoardWidth(std::min(MAXIMUM_BOARD_WIDTH, menu.getBoardWidth() + 1));
}
break;
}
case 1 : {
if (this->playerCursor.movedLeft()) {
menu.setBoardHeight(std::max(1, menu.getBoardHeight() - 1));
}
if (this->playerCursor.movedRight()) {
menu.setBoardHeight(std::min(MAXIMUM_BOARD_HEIGHT, menu.getBoardHeight() + 1));
}
break;
}
}
if (this->escReleased) {
this->menuStack->pop();
}
}
void GameBoardAppMenu::drawFrame() const {
this->renderWindow->clear(sf::Color(200, 200, 200));
const Menu& menu = this->settings->getMenu();
sf::Text text(this->pressStartFont, "", this->settings->getWindowSizeMultiplier() * 2);
text.setFillColor(sf::Color(0, 0, 0));
text.setOutlineColor(sf::Color(255, 255, 255));
this->placeTitle(text, {}, "BOARD SETTINGS", 5.f, {});
this->placeText(text, this->playerCursor, "< BOARD WIDTH: " + std::to_string(menu.getBoardWidth()) + " >", 5.f, 15.f, sf::Vector2u{0, 0});
this->placeText(text, this->playerCursor, "< BOARD HEIGHT: " + std::to_string(menu.getBoardHeight()) + " >", 5.f, 25.f, sf::Vector2u{0, 1});
this->renderWindow->display();
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
class GameBoardAppMenu : public AppMenu {
private:
PlayerCursor playerCursor;
public:
GameBoardAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow);
void computeFrame() override;
void drawFrame() const override;
};

View File

@@ -0,0 +1,85 @@
#include "GameDistributionAppMenu.h"
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
GameDistributionAppMenu::GameDistributionAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) :
AppMenu(menuStack, settings, renderWindow),
playerCursor({1}) {
for (int i = 1; i <= MAXIMUM_PIECES_SIZE; i++) {
this->playerCursor.addRow(i, 1);
}
}
void GameDistributionAppMenu::computeFrame() {
this->updateMetaBinds();
this->playerCursor.updatePosition();
PiecesList& piecesList = this->settings->getMenu().getPiecesList();
if (this->playerCursor.getPosition().y == 0) {
if (this->playerCursor.movedLeft()) {
piecesList.setDistributionMode(DistributionMode((int) piecesList.getDistributionMode() - 1));
}
if (this->playerCursor.movedRight()) {
piecesList.setDistributionMode(DistributionMode((int) piecesList.getDistributionMode() + 1));
}
}
else {
if (piecesList.getDistributionMode() != CUSTOM) {
this->playerCursor.goToPosition({0, 0});
}
else {
if (this->playerCursor.movedLeft()) {
this->settings->decreaseDistribution(this->playerCursor.getPosition().y);
}
if (this->playerCursor.movedRight()) {
this->settings->increaseDistribution(this->playerCursor.getPosition().y);
}
}
}
if (this->escReleased) {
this->settings->confirmDistribution();
this->menuStack->pop();
}
}
void GameDistributionAppMenu::drawFrame() const {
this->renderWindow->clear(sf::Color(200, 200, 200));
const Menu& menu = this->settings->getMenu();
sf::Text text(this->pressStartFont, "", this->settings->getWindowSizeMultiplier() * 2);
text.setFillColor(sf::Color(0, 0, 0));
text.setOutlineColor(sf::Color(255, 255, 255));
this->placeTitle(text, {}, "DISTRIBUTION SETTINGS", 5.f, {});
const DistributionMode distributionMode = this->settings->getMenu().readPiecesList().getDistributionMode();
const std::vector<int>& distributions = this->settings->getDistributions();
int firstElem = std::clamp(((int) this->playerCursor.getPosition().y) - 1, 0, MAXIMUM_PIECES_SIZE - 3);
if (firstElem == 0) {
this->placeText(text, this->playerCursor, "< DISTRIBUTION MODE: " + getPiecesDistributionName(distributionMode) + " >", 5.f, 15.f, sf::Vector2u{0, 0});
}
else {
this->placeText(text, this->playerCursor, "< SIZE " + std::to_string(firstElem) + " PROBABILITY: " + std::to_string(distributions.at(firstElem)) + " >", 5.f, 15.f, sf::Vector2u{(unsigned int) firstElem, 0});
}
if (distributionMode != CUSTOM) {
text.setFillColor(sf::Color(100, 100, 100));
}
this->placeText(text, this->playerCursor, "< SIZE " + std::to_string(firstElem + 1) + " PROBABILITY: " + std::to_string(distributions.at(firstElem + 1)) + " >", 5.f, 25.f, sf::Vector2u{0, (unsigned int) firstElem + 1});
this->placeText(text, this->playerCursor, "< SIZE " + std::to_string(firstElem + 2) + " PROBABILITY: " + std::to_string(distributions.at(firstElem + 2)) + " >", 5.f, 35.f, sf::Vector2u{0, (unsigned int) firstElem + 2});
this->placeText(text, this->playerCursor, "< SIZE " + std::to_string(firstElem + 3) + " PROBABILITY: " + std::to_string(distributions.at(firstElem + 3)) + " >", 5.f, 45.f, sf::Vector2u{0, (unsigned int) firstElem + 3});
this->renderWindow->display();
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
class GameDistributionAppMenu : public AppMenu {
private:
PlayerCursor playerCursor;
public:
GameDistributionAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow);
void computeFrame() override;
void drawFrame() const override;
};

View File

@@ -0,0 +1,182 @@
#include "GamePiecesAppMenu.h"
#include "AppMenu.h"
#include "GameDistributionAppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
GamePiecesAppMenu::GamePiecesAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) :
AppMenu(menuStack, settings, renderWindow),
playerCursor({1, (unsigned int) this->settings->getSelectedPieces().size() + 1u}) {
for (int i = 1; i <= MAXIMUM_PIECES_SIZE; i++) {
this->playerCursor.addRow(i + 1, this->settings->getMenu().readPiecesList().getNumberOfPieces(i) + 4);
}
}
void GamePiecesAppMenu::computeFrame() {
this->updateMetaBinds();
this->playerCursor.updatePosition();
if (this->playerCursor.movedDown() && this->playerCursor.getPosition().y == 2) {
this->playerCursor.goToPosition({0, 2});
}
if (this->enterReleased) {
if (this->playerCursor.getPosition().y == 0) {
this->menuStack->push(std::make_shared<GameDistributionAppMenu>(this->menuStack, this->settings, this->renderWindow));
}
if (this->playerCursor.getPosition().y > 1) {
if (this->playerCursor.getPosition().x >= 4) {
this->settings->selectPieces(createSinglePieceType(this->playerCursor.getPosition().y - 1), this->playerCursor.getPosition().x - 4);
}
else {
switch (this->playerCursor.getPosition().x) {
case 0 : {this->settings->selectPieces(ALL_PIECES, this->playerCursor.getPosition().y - 1); break;}
case 1 : {this->settings->selectPieces(CONVEX_PIECES, this->playerCursor.getPosition().y - 1); break;}
case 2 : {this->settings->selectPieces(HOLELESS_PIECES, this->playerCursor.getPosition().y - 1); break;}
case 3 : {this->settings->selectPieces(OTHER_PIECES, this->playerCursor.getPosition().y - 1); break;}
}
}
this->playerCursor.addPosition(0, 1);
}
}
if (this->escReleased) {
if (this->playerCursor.getPosition().y == 1) {
if (this->playerCursor.getPosition().x > 0) {
this->settings->unselectPieces(this->playerCursor.getPosition().x - 1);
this->playerCursor.removePosition(this->playerCursor.getPosition().x - 1, 1);
}
}
else {
if (this->settings->getSelectedPieces().size() == 0) {
this->settings->selectPieces(ALL_PIECES, 4);
}
this->settings->confirmSelectedPieces();
this->menuStack->pop();
}
}
}
void GamePiecesAppMenu::drawFrame() const {
this->renderWindow->clear(sf::Color(200, 200, 200));
sf::Text text(this->pressStartFont, "", this->settings->getWindowSizeMultiplier() * 2);
text.setFillColor(sf::Color(0, 0, 0));
text.setOutlineColor(sf::Color(255, 255, 255));
this->placeTitle(text, {}, "PIECES SELECT", 5.f, {});
if (this->playerCursor.getPosition().y == 0) {
this->placeText(text, this->playerCursor, "PIECES DISTRIBUTION", 5.f, 15.f, sf::Vector2u{0, 0});
this->drawSelectedPiecesRow(25.f);
this->drawRow(1, 35.f, true);
this->drawRow(2, 45.f, true);
}
else {
this->drawSelectedPiecesRow(15.f);
bool drawFromFirstElem = (this->playerCursor.getPosition().y == 1);
int firstElem = std::clamp(((int) this->playerCursor.getPosition().y) - 2, 1, MAXIMUM_PIECES_SIZE - 2);
this->drawRow(firstElem, 25.f, drawFromFirstElem);
this->drawRow(firstElem + 1, 35.f, drawFromFirstElem);
this->drawRow(firstElem + 2, 45.f, drawFromFirstElem);
}
this->renderWindow->display();
}
void GamePiecesAppMenu::drawSelectedPiecesRow(float yPos) const {
sf::RectangleShape rect({(float) this->renderWindow->getSize().x, 8.f * this->settings->getWindowSizeMultiplier()});
rect.setPosition({0.f, (yPos - 4.f) * this->settings->getWindowSizeMultiplier()});
rect.setFillColor({240, 240, 240});
this->renderWindow->draw(rect);
sf::Text text(this->pressStartFont, "", this->settings->getWindowSizeMultiplier());
text.setFillColor({0, 0, 0});
int elem = (this->playerCursor.getPosition().y == 1) ? std::max(((int) this->playerCursor.getPosition().x) - 4, 0) : 0;
float xProgress = 1.f;
bool first = true;
while (true) {
if ((this->playerCursor.getPosition().y == 1) && (elem == this->playerCursor.getPosition().x)) {
this->placeText(text, {}, "|", xProgress, yPos, {});
xProgress += (1.f + (text.getGlobalBounds().size.x / this->settings->getWindowSizeMultiplier()));
}
if (elem >= this->settings->getSelectedPieces().size()) return;
const auto& [pieceType, value] = this->settings->getSelectedPieces().at(elem);
int pieceSize = getSizeOfPieces(pieceType);
if (pieceSize > 0) {
const Piece& piece = this->settings->getMenu().readPiecesList().lookAtPiece({pieceSize, value});
int cellSize = (8 * this->settings->getWindowSizeMultiplier()) / (piece.getLength());
sf::FloatRect piecePosition(sf::Vector2f(xProgress, yPos - 4.f) * (float) this->settings->getWindowSizeMultiplier(), sf::Vector2f(8 , 8) * (float) this->settings->getWindowSizeMultiplier());
this->drawPiece(piece, cellSize, piecePosition, false);
xProgress += (1.f + 8.f);
}
else {
this->placeText(text, {}, ((first) ? "" : " ") + getPiecesTypeName(pieceType) + "_" + std::to_string(value), xProgress, yPos, {});
xProgress += (1.f + (text.getGlobalBounds().size.x / this->settings->getWindowSizeMultiplier()));
}
elem++;
}
}
void GamePiecesAppMenu::drawRow(int piecesSize, float yPos, bool drawFromFirstElem) const {
int numberOfPieces = this->settings->getMenu().readPiecesList().getNumberOfPieces(piecesSize);
int firstElem = (drawFromFirstElem) ? -4 : std::max(((int) this->playerCursor.getPosition().x) - 7, -4);
sf::Text text(this->pressStartFont, "", this->settings->getWindowSizeMultiplier());
text.setFillColor({0, 0, 0});
text.setOutlineColor({255, 255, 255});
this->placeText(text, {}, "SIZE " + std::to_string(piecesSize), 1.f, yPos, {});
for (int i = 0; i < 7; i++) {
if (i + firstElem >= numberOfPieces) return;
if ((i + firstElem) < 0) {
switch (i + firstElem) {
case -4 : {this->placeText(text, this->playerCursor, "ALL", 10.f + (i * 10.f), yPos, sf::Vector2u{0, piecesSize + 1u}); break;}
case -3 : {this->placeText(text, this->playerCursor, "CONVEX", 10.f + (i * 10.f), yPos, sf::Vector2u{1, piecesSize + 1u}); break;}
case -2 : {this->placeText(text, this->playerCursor, "HOLELESS", 10.f + (i * 10.f), yPos, sf::Vector2u{2, piecesSize + 1u}); break;}
case -1 : {this->placeText(text, this->playerCursor, "OTHER", 10.f + (i * 10.f), yPos, sf::Vector2u{3, piecesSize + 1u}); break;}
}
}
else {
const Piece& piece = this->settings->getMenu().readPiecesList().lookAtPiece({piecesSize, firstElem + i});
int cellSize = (8 * this->settings->getWindowSizeMultiplier()) / (piece.getLength());
sf::FloatRect piecePosition(sf::Vector2f(10.f + (i * 10.f), yPos - 4.f) * (float) this->settings->getWindowSizeMultiplier(), sf::Vector2f(8 , 8) * (float) this->settings->getWindowSizeMultiplier());
this->drawPiece(piece, cellSize, piecePosition, this->playerCursor.getPosition() == sf::Vector2u{i + firstElem + 4u, piecesSize + 1u});
}
}
}
void GamePiecesAppMenu::drawPiece(const Piece& piece, int cellSize, const sf::FloatRect& piecePosition, bool selected) const {
sf::RectangleShape rect(piecePosition.size);
rect.setPosition(piecePosition.position);
rect.setFillColor({180, 180, 180});
if (selected) {
rect.setOutlineColor({0, 0, 0});
rect.setOutlineThickness(this->settings->getWindowSizeMultiplier() / 2);
}
this->renderWindow->draw(rect);
sf::RectangleShape cell(sf::Vector2f(cellSize, cellSize));
cell.setFillColor(this->getColorOfBlock(piece.getBlockType(), 0));
for (const Position& cellPosition : piece.getPositions()) {
cell.setPosition(sf::Vector2f(piecePosition.position.x + (cellPosition.x * cellSize),
piecePosition.position.y + ((piece.getLength() - cellPosition.y - 1) * cellSize) ));
this->renderWindow->draw(cell);
}
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <vector>
#include <SFML/Graphics.hpp>
class GamePiecesAppMenu : public AppMenu {
private:
PlayerCursor playerCursor;
public:
GamePiecesAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow);
void computeFrame() override;
void drawFrame() const override;
private:
void drawSelectedPiecesRow(float yPos) const;
void drawRow(int piecesSize, float yPos, bool drawFromFirstElem) const;
void drawPiece(const Piece& piece, int cellSize, const sf::FloatRect& pos, bool selected) const;
};

View File

@@ -0,0 +1,277 @@
#include "GamePlayingAppMenu.h"
#include "AppMenu.h"
#include <stack>
#include <memory>
#include <algorithm>
#include <cmath>
#include <string>
#include <SFML/Graphics.hpp>
GamePlayingAppMenu::GamePlayingAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) :
AppMenu(menuStack, settings, renderWindow),
game(this->settings->getMenu().startGame(this->settings->getGamemode())) {
this->startTimer = this->settings->getStartTimerLength() * FRAMES_PER_SECOND;
if (this->startTimer == 0) {
this->game.start();
}
this->paused = false;
this->pausePressed = false;
this->retryPressed = false;
int maxWidthMultiplier = (this->settings->getWindowSizeMultiplier() * 40) / (this->game.getBoard().getWidth());
int maxHeightMultiplier = (this->settings->getWindowSizeMultiplier() * 50) / (this->game.getBoard().getBaseHeight() + 10);
this->cellSizeZoom = std::min(maxWidthMultiplier, maxHeightMultiplier);
float boardWidth = this->game.getBoard().getWidth() * this->cellSizeZoom;
float boardHeight = (this->game.getBoard().getBaseHeight() + 10) * this->cellSizeZoom;
this->boardPosition = sf::FloatRect(sf::Vector2f((this->settings->getWindowSizeMultiplier() * 40) - (boardWidth / 2),
(this->settings->getWindowSizeMultiplier() * 25) - (boardHeight / 2)),
sf::Vector2f(boardWidth, boardHeight));
for (int i = 0; i < 5; i++) {
this->nextQueuePosition[i] = sf::FloatRect(sf::Vector2f(this->boardPosition.position.x + boardWidth + (5.f * this->settings->getWindowSizeMultiplier()), (10.f + (10.f * i)) * this->settings->getWindowSizeMultiplier()),
sf::Vector2f(8.f * this->settings->getWindowSizeMultiplier(), 8.f * this->settings->getWindowSizeMultiplier()));
}
this->nextCellSizeZoom = this->nextQueuePosition[0].size.y;
for (const auto& piece : this->settings->getMenu().getPiecesList().getSelectedPieces()) {
float nextPieceCellSizeZoom = ((int) this->nextQueuePosition[0].size.y) / this->settings->getMenu().getPiecesList().lookAtPiece(piece).getLength();
this->nextCellSizeZoom = std::min(this->nextCellSizeZoom, nextPieceCellSizeZoom);
}
this->holdBoxPosition = sf::FloatRect(sf::Vector2f(this->boardPosition.position.x - ((8.f + 5.f) * this->settings->getWindowSizeMultiplier()), (10.f) * this->settings->getWindowSizeMultiplier()),
sf::Vector2f(8.f * this->settings->getWindowSizeMultiplier(), 8.f * this->settings->getWindowSizeMultiplier()));
this->holdCellSizeZoom = this->nextCellSizeZoom;
}
void GamePlayingAppMenu::computeFrame() {
this->updateMetaBinds();
if (this->startTimer > 0) {
this->startTimer--;
if (this->startTimer == 0) {
this->game.start();
}
}
if (this->escReleased) {
this->menuStack->pop();
}
else {
std::set<Action> actions;
for (Action action : ACTION_LIST_IN_ORDER) {
for (sfKey key : this->settings->getKeybinds().getKeybinds(action)) {
if (sf::Keyboard::isKeyPressed(key)) {
actions.insert(action);
}
}
}
if (actions.contains(RETRY)) {
this->retryPressed = true;
}
else {
if (this->retryPressed) {
this->game.reset();
this->startTimer = this->settings->getStartTimerLength() * FRAMES_PER_SECOND;
if (this->startTimer == 0) {
this->game.start();
}
}
this->retryPressed = false;
}
if (actions.contains(PAUSE)) {
this->pausePressed = true;
}
else {
if (this->pausePressed) {
this->paused = (!this->paused);
}
this->pausePressed = false;
}
if (!paused) {
this->game.nextFrame(actions);
}
}
}
void GamePlayingAppMenu::drawFrame() const {
this->renderWindow->clear(sf::Color(200, 200, 200));
sf::Vector2f cellSize(this->cellSizeZoom, this->cellSizeZoom);
bool drawActivePiece = (this->game.getActivePiece() != nullptr) && (!this->game.hasLost());
// board
for (int y = this->game.getBoard().getBaseHeight() + 9; y >= 0; y--) {
for (int x = 0; x < this->game.getBoard().getWidth(); x++) {
Block block = this->game.getBoard().getBlock(Position{x, y});
sf::RectangleShape cell(cellSize);
cell.setFillColor(this->getColorOfBlock(block, (block == NOTHING) ? 0 : -30));
cell.setPosition(this->getBoardBlockPosition(x, y));
this->renderWindow->draw(cell);
}
}
// end coutdown
if (drawActivePiece) {
// ghost piece
sf::Color ghostColor = this->getColorOfBlock(this->game.getActivePiece()->getBlockType(), 100);
for (const Position& position : this->game.getActivePiece()->getPositions()) {
Position cellPosition = (this->game.getGhostPiecePosition() + position);
sf::RectangleShape cell(cellSize);
cell.setFillColor(ghostColor);
cell.setPosition(this->getBoardBlockPosition(cellPosition.x, cellPosition.y));
this->renderWindow->draw(cell);
}
// active piece outline
float pieceOutlineSize = std::roundf(this->cellSizeZoom / 4);
sf::Color pieceOultlineColor = sf::Color(255, 255 - (255 * this->game.getForcedLockDelayProgression()), 255 - (255 * this->game.getForcedLockDelayProgression()));
for (const Position& position : this->game.getActivePiece()->getPositions()) {
Position cellPosition = (this->game.getActivePiecePosition() + position);
sf::RectangleShape cell(cellSize);
cell.setOutlineThickness(pieceOutlineSize);
cell.setOutlineColor(pieceOultlineColor);
cell.setPosition(this->getBoardBlockPosition(cellPosition.x, cellPosition.y));
this->renderWindow->draw(cell);
}
}
// top out line
sf::RectangleShape topOutLine(sf::Vector2f(this->cellSizeZoom * this->game.getBoard().getWidth(), std::roundf(this->cellSizeZoom / 4)));
topOutLine.setPosition(this->getBoardBlockPosition(0, this->game.getBoard().getBaseHeight() - 1));
topOutLine.setFillColor(sf::Color(255, 0, 0));
this->renderWindow->draw(topOutLine);
if (drawActivePiece) {
// active piece
sf::Color pieceColor = this->getColorOfBlock(this->game.getActivePiece()->getBlockType(), -200 * (this->game.getLockDelayProgression()));
for (const Position& position : this->game.getActivePiece()->getPositions()) {
Position cellPosition = (this->game.getActivePiecePosition() + position);
sf::RectangleShape cell(cellSize);
cell.setFillColor(pieceColor);
cell.setPosition(this->getBoardBlockPosition(cellPosition.x, cellPosition.y));
this->renderWindow->draw(cell);
}
}
// next queue
int upShift = 0;
for (int i = 0; i < std::min((int) this->game.getNextPieces().size(), 5); i++) {
sf::FloatRect nextBox = this->nextQueuePosition[i];
nextBox.position.y -= upShift;
sf::Vector2f nextCellSize(this->nextCellSizeZoom, this->nextCellSizeZoom);
sf::Color color = this->getColorOfBlock(this->game.getNextPieces().at(i).getBlockType(), 0);
sf::Color boxColor = sf::Color(180, 180, 180);
int lowestRank = 0;
for (int y = 0; y < this->game.getNextPieces().at(i).getLength(); y++) {
for (int x = 0; x < this->game.getNextPieces().at(i).getLength(); x++) {
sf::RectangleShape cell(nextCellSize);
if (this->game.getNextPieces().at(i).getPositions().contains(Position{x, y})) {
cell.setFillColor(color);
lowestRank = y;
}
else {
cell.setFillColor(boxColor);
}
cell.setPosition(sf::Vector2f(nextBox.position.x + (x * this->nextCellSizeZoom),
nextBox.position.y + ((this->game.getNextPieces().at(i).getLength() - y - 1) * this->nextCellSizeZoom)));
this->renderWindow->draw(cell);
}
}
upShift += nextBox.size.y - (this->game.getNextPieces().at(i).getLength() * this->nextCellSizeZoom);
}
// hold box
if (this->game.getHeldPiece() != nullptr) {
sf::Vector2f holdCellSize(this->holdCellSizeZoom, this->holdCellSizeZoom);
sf::Color color = this->getColorOfBlock(this->game.getHeldPiece()->getBlockType(), 0);
sf::Color boxColor = sf::Color(180, 180, 180);
for (int y = 0; y < this->game.getHeldPiece()->getLength(); y++) {
for (int x = 0; x < this->game.getHeldPiece()->getLength(); x++) {
sf::RectangleShape cell(holdCellSize);
if (this->game.getHeldPiece()->getPositions().contains(Position{x, y})) {
cell.setFillColor(color);
}
else {
cell.setFillColor(boxColor);
}
cell.setPosition(sf::Vector2f(this->holdBoxPosition.position.x + (x * this->nextCellSizeZoom),
this->holdBoxPosition.position.y + ((this->game.getHeldPiece()->getLength() - y - 1) * this->holdCellSizeZoom)));
this->renderWindow->draw(cell);
}
}
}
// stats
int windowSizeMultiplier = this->settings->getWindowSizeMultiplier();
int fontSize = (this->boardPosition.size.x > (windowSizeMultiplier * 30.f)) ? (windowSizeMultiplier) : (windowSizeMultiplier * 2);
sf::Text text(this->pressStartFont, "", fontSize);
text.setFillColor(sf::Color(0, 0, 0));
int millisecondes = this->game.getFramesPassed() * (1000.f / FRAMES_PER_SECOND);
std::string showedMillisecondes = std::to_string(millisecondes % 1000);
while (showedMillisecondes.size() < 3) {
showedMillisecondes = "0" + showedMillisecondes;
}
std::string showedSecondes = std::to_string((millisecondes / 1000) % 60);
while (showedSecondes.size() < 2) {
showedSecondes = "0" + showedSecondes;
}
std::string showedMinutes = std::to_string((millisecondes / (60 * 1000)));
std::string showedTime = showedMinutes + ":" + showedSecondes + "." + showedMillisecondes;
this->placeText(text, {}, getGamemodeName(this->settings->getGamemode()), 1.f, 3.f, {});
this->placeText(text, {}, getGamemodeGoal(this->settings->getGamemode()), 1.f, 6.f, {});
if (this->game.isOnB2BChain()) {
this->placeText(text, {}, "B2B", 1.f, 22.f, {});
}
this->placeText(text, {}, "LINES:" + std::to_string(this->game.getClearedLines()), 1.f, 27.f, {});
this->placeText(text, {}, "LEVEL:" + std::to_string(this->game.getLevel()), 1.f, 32.f, {});
this->placeText(text, {}, "SCORE:" + std::to_string(this->game.getScore()), 1.f, 37.f, {});
this->placeText(text, {}, "GRADE:" + std::to_string(this->game.getGrade()), 1.f, 42.f, {});
this->placeText(text, {}, showedTime, 1.f, 47.f, {});
// game state
text.setOutlineColor(sf::Color(255, 255, 255));
text.setOutlineThickness(windowSizeMultiplier / 2.f);
text.setCharacterSize(windowSizeMultiplier * 4);
if (this->game.hasWon()) {
this->placeTitle(text, {}, "WIN", 25.f, {});
}
else if (this->game.hasLost()) {
this->placeTitle(text, {}, "LOSE", 25.f, {});
}
else if (this->paused) {
this->placeTitle(text, {}, "PAUSE", 25.f, {});
}
else if (this->startTimer > 0) {
this->placeTitle(text, {}, std::to_string(((this->startTimer - 1) / ((this->settings->getStartTimerLength() * FRAMES_PER_SECOND) / 4))), 25.f, {});
}
this->renderWindow->display();
}
sf::Vector2f GamePlayingAppMenu::getBoardBlockPosition(int x, int y) const {
return sf::Vector2f(this->boardPosition.position.x + (x * this->cellSizeZoom),
this->boardPosition.position.y + ((this->game.getBoard().getBaseHeight() + 9 - y) * this->cellSizeZoom));
}

View File

@@ -0,0 +1,32 @@
#pragma once
#include "AppMenu.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
class GamePlayingAppMenu : public AppMenu {
private:
Game game;
int startTimer;
bool paused;
bool pausePressed;
bool retryPressed;
sf::FloatRect boardPosition;
float cellSizeZoom;
sf::FloatRect holdBoxPosition;
float holdCellSizeZoom;
sf::FloatRect nextQueuePosition[5];
float nextCellSizeZoom;
public:
GamePlayingAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow);
void computeFrame() override;
void drawFrame() const override;
sf::Vector2f getBoardBlockPosition(int x, int y) const;
};

View File

@@ -0,0 +1,82 @@
#include "GameSettingsAppMenu.h"
#include "AppMenu.h"
#include "GamePiecesAppMenu.h"
#include "GameBoardAppMenu.h"
#include "GamePlayingAppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
GameSettingsAppMenu::GameSettingsAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) :
AppMenu(menuStack, settings, renderWindow),
playerCursor({2, 3, 2}) {
}
void GameSettingsAppMenu::computeFrame() {
this->updateMetaBinds();
this->playerCursor.updatePosition();
switch (this->playerCursor.getPosition().y) {
case 1 : {
switch (this->playerCursor.getPosition().x) {
case 0 : {this->settings->setGamemode(SPRINT); break;}
case 1 : {this->settings->setGamemode(MARATHON); break;}
case 2 : {this->settings->setGamemode(ULTRA); break;}
}
break;
}
case 2 : {
switch (this->playerCursor.getPosition().x) {
case 0 : {this->settings->setGamemode(MASTER); break;}
case 1 : {this->settings->setGamemode(ZEN); break;}
}
break;
}
}
if (this->enterReleased) {
if (this->playerCursor.getPosition().y == 0) {
if (this->playerCursor.getPosition().x == 0) {
this->menuStack->push(std::make_shared<GamePiecesAppMenu>(this->menuStack, this->settings, this->renderWindow));
}
if (this->playerCursor.getPosition().x == 1) {
this->menuStack->push(std::make_shared<GameBoardAppMenu>(this->menuStack, this->settings, this->renderWindow));
}
}
if (this->playerCursor.getPosition().y > 0) {
this->menuStack->push(std::make_shared<GamePlayingAppMenu>(this->menuStack, this->settings, this->renderWindow));
}
}
if (this->escReleased) {
this->menuStack->pop();
}
}
void GameSettingsAppMenu::drawFrame() const {
this->renderWindow->clear(sf::Color(200, 200, 200));
sf::Text text(this->pressStartFont, "", this->settings->getWindowSizeMultiplier() * 2);
text.setFillColor(sf::Color(0, 0, 0));
text.setOutlineColor(sf::Color(255, 255, 255));
this->placeTitle(text, {}, "GAME SETTINGS", 5.f, {});
this->placeText(text, this->playerCursor, "PIECES SELECT", 5.f, 15.f, sf::Vector2u{0, 0});
this->placeText(text, this->playerCursor, "BOARD SELECT", 40.f, 15.f, sf::Vector2u{1, 0});
text.setOutlineThickness(0);
this->placeTitle(text, {}, "GAMEMODE SELECT", 25.f, {});
this->placeText(text, this->playerCursor, "SPRINT", 5.f, 35.f, sf::Vector2u{0, 1});
this->placeText(text, this->playerCursor, "MARATHON", 25.f, 35.f, sf::Vector2u{1, 1});
this->placeText(text, this->playerCursor, "ULTRA", 50.f, 35.f, sf::Vector2u{2, 1});
this->placeText(text, this->playerCursor, "MASTER", 5.f, 45.f, sf::Vector2u{0, 2});
this->placeText(text, this->playerCursor, "ZEN", 25.f, 45.f, sf::Vector2u{1, 2});
this->renderWindow->display();
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
class GameSettingsAppMenu : public AppMenu {
private:
PlayerCursor playerCursor;
public:
GameSettingsAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow);
void computeFrame() override;
void drawFrame() const override;
};

View File

@@ -0,0 +1,87 @@
#include "InfoAppMenu.h"
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
InfoAppMenu::InfoAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) :
AppMenu(menuStack, settings, renderWindow),
playerCursor({4}),
sectionsName(
"< ABOUT >",
"< ROTATION SYSTEM >",
"< SCORING >",
"< 0 DEGREES ROTATIONS >"
),
sectionsContent(
"This game is written in C++,\n"
"using SFML 3 for the GUI.\n"
"It has been inspired by other\n"
"stacker games, such as\n"
"Techmino, jstris, tetr.io, etc.\n"
"This project isn't affiliated\n"
"to them in any ways.\n"
"Current version: beta.",
"This game uses its own\n"
"Rotation Sytem, called AutoRS.\n"
"The rotation center is always the\n"
"center of the piece by default.\n"
"When kicking the piece, it will\n"
"compute and try (most) position that\n"
"touches the original piece,\n"
"prioritizing sides over depth and\n"
"firstly going down before going up.",
"The score gained from a line clear\n"
"doubles when clearing one more line.\n"
"Clearing with a spin scores as much\n"
"as clearing 2x more lines normally.\n"
"B2B is granted by clearing at least\n"
"4 lines or doing a spin or mini-spin,\n"
"and doubles the score gained.\n"
"A spin is detected when the piece is\n"
"locked in place, a mini-spin simply\n"
"when the last move was a kick.",
"This games introduces 0 degrees\n"
"rotations, which work by simpling\n"
"moving the piece down and kicking\n"
"it as is, allowing for new kinds\n"
"of kicks.\n"
"As a leniency mechanic, when a\n"
"piece spawns it will automatically\n"
"try a 0 degrees rotations if it\n"
"spawned inside a wall."
) {
}
void InfoAppMenu::computeFrame() {
this->updateMetaBinds();
this->playerCursor.updatePosition();
if (this->escReleased) {
this->menuStack->pop();
}
}
void InfoAppMenu::drawFrame() const {
this->renderWindow->clear(sf::Color(200, 200, 200));
sf::Text text(this->pressStartFont, "", this->settings->getWindowSizeMultiplier() * 2);
text.setFillColor(sf::Color(0, 0, 0));
text.setOutlineColor(sf::Color(255, 255, 255));
this->placeTitle(text, this->playerCursor, this->sectionsName[this->playerCursor.getPosition().x], 10.f, this->playerCursor.getPosition());
text.setLineSpacing((float) this->settings->getWindowSizeMultiplier() / 8);
text.setOutlineThickness(0);
this->placeText(text, {}, this->sectionsContent[this->playerCursor.getPosition().x], 5.f, 30.f, {});
this->renderWindow->display();
}

View File

@@ -0,0 +1,23 @@
#pragma once
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
class InfoAppMenu : public AppMenu {
private:
PlayerCursor playerCursor;
sf::String sectionsName[4];
sf::String sectionsContent[4];
public:
InfoAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow);
void computeFrame() override;
void drawFrame() const override;
};

View File

@@ -0,0 +1,54 @@
#include "MainAppMenu.h"
#include "AppMenu.h"
#include "GameSettingsAppMenu.h"
#include "SettingsMainAppMenu.h"
#include "InfoAppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
MainAppMenu::MainAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) :
AppMenu(menuStack, settings, renderWindow),
playerCursor({1, 1, 1}) {
}
void MainAppMenu::computeFrame() {
this->updateMetaBinds();
this->playerCursor.updatePosition();
if (this->enterReleased) {
if (this->playerCursor.getPosition().y == 0) {
this->menuStack->push(std::make_shared<GameSettingsAppMenu>(this->menuStack, this->settings, this->renderWindow));
}
if (this->playerCursor.getPosition().y == 1) {
this->menuStack->push(std::make_shared<SettingsMainAppMenu>(this->menuStack, this->settings, this->renderWindow));
}
if (this->playerCursor.getPosition().y == 2) {
this->menuStack->push(std::make_shared<InfoAppMenu>(this->menuStack, this->settings, this->renderWindow));
}
}
if (this->escReleased) {
this->menuStack->pop();
}
}
void MainAppMenu::drawFrame() const {
this->renderWindow->clear(sf::Color(200, 200, 200));
sf::Text text(this->pressStartFont, "", this->settings->getWindowSizeMultiplier() * 2);
text.setFillColor(sf::Color(0, 0, 0));
text.setOutlineColor(sf::Color(255, 255, 255));
this->placeTitle(text, {}, "JMINOS", 10.f, {});
this->placeTitle(text, this->playerCursor, "PLAY", 20.f, sf::Vector2u{0, 0});
this->placeTitle(text, this->playerCursor, "SETTINGS", 30.f, sf::Vector2u{0, 1});
this->placeTitle(text, this->playerCursor, "INFO", 40.f, sf::Vector2u{0, 2});
this->renderWindow->display();
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
class MainAppMenu : public AppMenu {
private:
PlayerCursor playerCursor;
public:
MainAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow);
void computeFrame() override;
void drawFrame() const override;
};

View File

@@ -0,0 +1,76 @@
#include "SettingsControlsAppMenu.h"
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
SettingsControlsAppMenu::SettingsControlsAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) :
AppMenu(menuStack, settings, renderWindow),
playerCursor({1, 1, 1}) {
}
void SettingsControlsAppMenu::computeFrame() {
this->updateMetaBinds();
this->playerCursor.updatePosition();
Player& playerControls = this->settings->getMenu().getPlayerControls();
switch (this->playerCursor.getPosition().y) {
case 0 : {
if (this->playerCursor.movedLeft()) {
playerControls.setDAS(playerControls.getDAS() - 1);
}
if (this->playerCursor.movedRight()) {
playerControls.setDAS(playerControls.getDAS() + 1);
}
break;
}
case 1 : {
if (this->playerCursor.movedLeft()) {
playerControls.setARR(playerControls.getARR() - 1);
}
if (this->playerCursor.movedRight()) {
playerControls.setARR(playerControls.getARR() + 1);
}
break;
}
case 2 : {
if (this->playerCursor.movedLeft()) {
playerControls.setSDR(playerControls.getSDR() - 1);
}
if (this->playerCursor.movedRight()) {
playerControls.setSDR(playerControls.getSDR() + 1);
}
break;
}
}
if (this->escReleased) {
this->menuStack->pop();
}
}
void SettingsControlsAppMenu::drawFrame() const {
this->renderWindow->clear(sf::Color(200, 200, 200));
const Player& playerControls = this->settings->getMenu().readPlayerControls();
sf::Text text(this->pressStartFont, "", this->settings->getWindowSizeMultiplier() * 2);
text.setFillColor(sf::Color(0, 0, 0));
text.setOutlineColor(sf::Color(255, 255, 255));
this->placeTitle(text, {}, "CONTROLS SETTINGS", 5.f, {});
sf::Vector2u windowSize = this->renderWindow->getSize();
this->placeText(text, this->playerCursor, "< DAS: " + std::to_string(playerControls.getDAS()) + " >", 5.f, 15.f, sf::Vector2u{0, 0});
this->placeText(text, this->playerCursor, "< ARR: " + std::to_string(playerControls.getARR()) + " >", 5.f, 25.f, sf::Vector2u{0, 1});
this->placeText(text, this->playerCursor, "< SDR: " + std::to_string(playerControls.getSDR()) + " >", 5.f, 35.f, sf::Vector2u{0, 2});
this->renderWindow->display();
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
class SettingsControlsAppMenu : public AppMenu {
private:
PlayerCursor playerCursor;
public:
SettingsControlsAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow);
void computeFrame() override;
void drawFrame() const override;
};

View File

@@ -0,0 +1,127 @@
#include "SettingsKeybindsAppMenu.h"
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <string>
#include <regex>
#include <filesystem>
#include <algorithm>
#include <SFML/Graphics.hpp>
SettingsKeybindsAppMenu::SettingsKeybindsAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) :
AppMenu(menuStack, settings, renderWindow),
playerCursor({1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}) {
this->selectedAnAction = false;
for (Action action : ACTION_LIST_IN_ORDER) {
std::string textureName = ACTION_NAMES[action];
textureName = std::regex_replace(textureName, std::regex(" "), "");
std::filesystem::path texturePath("data/images/keybinds/" + textureName + ".png");
this->iconTextures[action] = sf::Texture(texturePath, false, {{0, 0}, {16, 16}});
}
}
void SettingsKeybindsAppMenu::computeFrame() {
this->updateMetaBinds();
if (!this->selectedAnAction) {
this->playerCursor.updatePosition();
if (this->playerCursor.movedLeft()) {
this->settings->selectPreviousKeybinds();
}
if (this->playerCursor.movedRight()) {
this->settings->selectNextKeybinds();
}
}
else {
bool addedKeybind = false;
for (const auto& [key, string] : KEYS_TO_STRING) {
if (sf::Keyboard::isKeyPressed(key) && (key != sfKey::Enter) && (key != sfKey::Escape)) {
this->settings->getKeybinds().addKey(this->actionSelected, key);
addedKeybind = true;
}
if (addedKeybind) {
this->selectedAnAction = false;
break;
}
}
}
if (this->enterReleased && this->settings->getKeybinds().isModifiable()) {
this->selectedAnAction = !selectedAnAction;
this->actionSelected = ACTION_LIST_IN_ORDER[this->playerCursor.getPosition().y - 1];
}
if (this->escReleased) {
if (this->selectedAnAction) {
this->settings->getKeybinds().clearKeys(this->actionSelected);
this->selectedAnAction = false;
}
else {
this->menuStack->pop();
}
}
}
void SettingsKeybindsAppMenu::drawFrame() const {
this->renderWindow->clear(sf::Color(200, 200, 200));
sf::Text text(this->pressStartFont, "", this->settings->getWindowSizeMultiplier() * 2);
text.setFillColor(sf::Color(0, 0, 0));
text.setOutlineColor(sf::Color(255, 255, 255));
this->placeTitle(text, {}, "KEYBINDS SETTINGS", 5.f, {});
if (this->settings->getKeybindsLayout() == CUSTOMIZABLE_KEYBINDS) {
this->placeText(text, this->playerCursor, "< CUSTOM >", 5.f, 15.f, sf::Vector2u{0, 0});
}
else {
this->placeText(text, this->playerCursor, "< DEFAULT " + std::to_string(this->settings->getKeybindsLayout() + 1) + " >", 5.f, 15.f, sf::Vector2u{0, 0});
}
if (this->selectedAnAction) {
text.setOutlineColor(sf::Color(255, 0, 0));
}
int i = 0;
int firstElem = std::clamp(((int) this->playerCursor.getPosition().y) - 2, 0, 8);
for (Action action : ACTION_LIST_IN_ORDER) {
if (i >= firstElem && i < (firstElem + 3)) {
sf::String string;
bool firstKey = true;
for (sfKey key : this->settings->getKeybinds().getKeybinds(action)) {
if (KEYS_TO_STRING.contains(key)) {
std::string keyString = KEYS_TO_STRING.at(key);
if (firstKey) {
string += keyString;
firstKey = false;
}
else {
string += ", " + keyString;
}
}
}
this->placeText(text, this->playerCursor, setStringToUpperCase(ACTION_NAMES[action]), 15.f, ((i - firstElem) * 10) + 25.f, sf::Vector2u{0, (unsigned int) i + 1});
text.setOutlineThickness(0);
this->placeText(text, {}, string, 40.f, ((i - firstElem) * 10) + 25.f, {});
sf::Sprite sprite(this->iconTextures[action]);
sprite.setOrigin(sprite.getLocalBounds().getCenter());
sprite.setPosition(sf::Vector2f(8.f, ((i - firstElem) * 10) + 25.f) * (float) this->settings->getWindowSizeMultiplier());
sprite.setScale(sprite.getScale() * ((float) this->settings->getWindowSizeMultiplier() / 2));
this->renderWindow->draw(sprite);
}
i++;
}
this->renderWindow->display();
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
class SettingsKeybindsAppMenu : public AppMenu {
private:
PlayerCursor playerCursor;
sf::Texture iconTextures[11];
bool selectedAnAction;
Action actionSelected;
public:
SettingsKeybindsAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow);
void computeFrame() override;
void drawFrame() const override;
};

View File

@@ -0,0 +1,78 @@
#include "SettingsMainAppMenu.h"
#include "AppMenu.h"
#include "SettingsKeybindsAppMenu.h"
#include "SettingsControlsAppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
SettingsMainAppMenu::SettingsMainAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) :
AppMenu(menuStack, settings, renderWindow),
playerCursor({1, 1, 1, 1}) {
}
void SettingsMainAppMenu::computeFrame() {
this->updateMetaBinds();
this->playerCursor.updatePosition();
switch (this->playerCursor.getPosition().y) {
case 2 : {
if (this->playerCursor.movedLeft()) {
if (this->settings->shortenWindow()) {
this->settings->changeVideoMode(*this->renderWindow);
}
}
if (this->playerCursor.movedRight()) {
if (this->settings->widenWindow()) {
this->settings->changeVideoMode(*this->renderWindow);
}
}
break;
}
case 3 : {
if (this->playerCursor.movedLeft()) {
this->settings->shortenStartTimer();
}
if (this->playerCursor.movedRight()) {
this->settings->lengthenStartTimer();
}
break;
}
}
if (this->enterReleased) {
if (this->playerCursor.getPosition().y == 0) {
this->menuStack->push(std::make_shared<SettingsKeybindsAppMenu>(this->menuStack, this->settings, this->renderWindow));
}
if (this->playerCursor.getPosition().y == 1) {
this->menuStack->push(std::make_shared<SettingsControlsAppMenu>(this->menuStack, this->settings, this->renderWindow));
}
}
if (this->escReleased) {
this->menuStack->pop();
}
}
void SettingsMainAppMenu::drawFrame() const {
this->renderWindow->clear(sf::Color(200, 200, 200));
sf::Text text(this->pressStartFont, "", this->settings->getWindowSizeMultiplier() * 2);
text.setFillColor(sf::Color(0, 0, 0));
text.setOutlineColor(sf::Color(255, 255, 255));
this->placeTitle(text, {}, "SETTINGS", 5.f, {});
sf::Vector2u windowSize = this->renderWindow->getSize();
this->placeText(text, this->playerCursor, "CHANGE KEYBINDS", 5.f, 15.f, sf::Vector2u{0, 0});
this->placeText(text, this->playerCursor, "CHANGE CONTROLS", 5.f, 25.f, sf::Vector2u{0, 1});
this->placeText(text, this->playerCursor, "< WINDOW SIZE: " + std::to_string(windowSize.x) + "x" + std::to_string(windowSize.y) + " >", 5.f, 35.f, sf::Vector2u{0, 2});
this->placeText(text, this->playerCursor, "< START TIMER: " + std::to_string(this->settings->getStartTimerLength()) + "s >", 5.f, 45.f, sf::Vector2u{0, 3});
this->renderWindow->display();
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "AppMenu.h"
#include "../PlayerCursor.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
class SettingsMainAppMenu : public AppMenu {
private:
PlayerCursor playerCursor;
public:
SettingsMainAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow);
void computeFrame() override;
void drawFrame() const override;
};

View File

@@ -0,0 +1,53 @@
#include "GraphApp.h"
#include "AppMenus/AppMenu.h"
#include "AppMenus/MainAppMenu.h"
#include "Settings.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
static const double TIME_BETWEEN_FRAMES = (1000.f / FRAMES_PER_SECOND);
GraphApp::GraphApp() {
this->settings = std::make_shared<Settings>();
this->menuStack = std::make_shared<MenuStack>();
this->renderWindow = std::make_shared<sf::RenderWindow>();
}
void GraphApp::run() {
this->settings->changeVideoMode(*this->renderWindow);
this->menuStack->push(std::make_shared<MainAppMenu>(this->menuStack, this->settings, this->renderWindow));
bool quit = false;
double timeAtNextFrame = 0;
sf::Clock clock;
while (!quit) {
while (const std::optional event = this->renderWindow->pollEvent()) {
if (event->is<sf::Event::Closed>()) {
quit = true;
}
}
if (!quit) {
if (clock.getElapsedTime().asMilliseconds() > timeAtNextFrame) {
this->menuStack->top()->computeFrame();
if (this->menuStack->empty()) {
quit = true;
}
else {
this->menuStack->top()->drawFrame();
}
while (clock.getElapsedTime().asMilliseconds() > timeAtNextFrame) {
timeAtNextFrame += TIME_BETWEEN_FRAMES;
}
}
}
}
this->settings->saveSettingsToFile();
renderWindow->close();
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "AppMenus/AppMenu.h"
#include "Settings.h"
#include <stack>
#include <memory>
#include <SFML/Graphics.hpp>
class GraphApp {
private:
std::shared_ptr<Settings> settings;
std::shared_ptr<MenuStack> menuStack;
std::shared_ptr<sf::RenderWindow> renderWindow;
public:
GraphApp();
void run();
};

View File

@@ -0,0 +1,98 @@
#include "Keybinds.h"
#include "../Core/Action.h"
#include <map>
#include <set>
#include <fstream>
#include <SFML/Graphics.hpp>
Keybinds::Keybinds(int layoutNumber) :
layoutNumber(layoutNumber) {
for (Action action : ACTION_LIST_IN_ORDER) {
this->keybinds.insert({action, std::set<sfKey>()});
}
this->modifiable = (layoutNumber == CUSTOMIZABLE_KEYBINDS);
this->loadKeybindsFromFile();
}
void Keybinds::loadKeybindsFromFile() {
std::ifstream layoutFile("data/config/keybinds/layout" + std::to_string(this->layoutNumber) + ".bin", std::ios::binary);
for (Action action : ACTION_LIST_IN_ORDER) {
this->keybinds.at(action).clear();
}
char byte;
while (layoutFile.peek() != EOF) {
layoutFile.get(byte);
Action action = Action(byte);
bool separatorMet = false;
while (!separatorMet) {
layoutFile.get(byte);
if (byte == (char) 0xFF) {
separatorMet = true;
}
else {
this->keybinds.at(action).insert(sfKey(byte));
}
}
}
}
void Keybinds::saveKeybindsToFile() const {
if (!this->modifiable) return;
std::ofstream layoutFile("data/config/keybinds/layout" + std::to_string(this->layoutNumber) + ".bin", std::ios::trunc | std::ios::binary);
char byte;
for (Action action : ACTION_LIST_IN_ORDER) {
byte = action;
layoutFile.write(&byte, 1);
for (sfKey key : this->keybinds.at(action)) {
byte = (int) key;
layoutFile.write(&byte, 1);
}
byte = 0xFF;
layoutFile.write(&byte, 1);
}
}
void Keybinds::addKey(Action action, sfKey key) {
if (!this->modifiable) return;
if (!KEYS_TO_STRING.contains(key)) return;
this->keybinds.at(action).insert(key);
}
void Keybinds::clearKeys(Action action) {
if (!this->modifiable) return;
this->keybinds.at(action).clear();
}
bool Keybinds::isModifiable() const {
return this->modifiable;
}
const std::set<Action> Keybinds::getActions(sfKey key) const {
std::set<Action> actions;
for (const auto& [action, keys] : this->keybinds) {
if (keys.contains(key)) {
actions.insert(action);
}
}
return actions;
}
const std::set<sfKey>& Keybinds::getKeybinds(Action action) const {
return this->keybinds.at(action);
}

156
src/GraphicalUI/Keybinds.h Normal file
View File

@@ -0,0 +1,156 @@
#pragma once
#include "../Core/Action.h"
#include <map>
#include <set>
#include <algorithm>
#include <SFML/Graphics.hpp>
using sfKey = sf::Keyboard::Key;
static const int NUMBER_OF_KEYBINDS = 5;
static const int CUSTOMIZABLE_KEYBINDS = NUMBER_OF_KEYBINDS - 1;
class Keybinds {
private:
std::map<Action, std::set<sfKey>> keybinds;
int layoutNumber;
bool modifiable;
public:
Keybinds(int layoutNumber);
void loadKeybindsFromFile();
void saveKeybindsToFile() const;
void addKey(Action action, sfKey key);
void clearKeys(Action action);
bool isModifiable() const;
const std::set<Action> getActions(sfKey key) const;
const std::set<sfKey>& getKeybinds(Action action) const;
};
inline std::string setStringToUpperCase(std::string&& str) {
std::transform(str.begin(), str.end(), str.begin(), ::toupper);
return str;
}
inline std::string setStringToUpperCase(const std::string& str) {
std::string result = str;
std::transform(result.begin(), result.end(), result.begin(), ::toupper);
return result;
}
#define INSERT_MAPPING(identifier) {sfKey::identifier, setStringToUpperCase(#identifier)}
static const std::map<sfKey, sf::String> KEYS_TO_STRING = {
INSERT_MAPPING(A),
INSERT_MAPPING(B),
INSERT_MAPPING(C),
INSERT_MAPPING(D),
INSERT_MAPPING(E),
INSERT_MAPPING(F),
INSERT_MAPPING(G),
INSERT_MAPPING(H),
INSERT_MAPPING(I),
INSERT_MAPPING(J),
INSERT_MAPPING(K),
INSERT_MAPPING(L),
INSERT_MAPPING(M),
INSERT_MAPPING(N),
INSERT_MAPPING(O),
INSERT_MAPPING(P),
INSERT_MAPPING(Q),
INSERT_MAPPING(R),
INSERT_MAPPING(S),
INSERT_MAPPING(T),
INSERT_MAPPING(U),
INSERT_MAPPING(V),
INSERT_MAPPING(W),
INSERT_MAPPING(X),
INSERT_MAPPING(Y),
INSERT_MAPPING(Z),
INSERT_MAPPING(Num0),
INSERT_MAPPING(Num1),
INSERT_MAPPING(Num2),
INSERT_MAPPING(Num3),
INSERT_MAPPING(Num4),
INSERT_MAPPING(Num5),
INSERT_MAPPING(Num6),
INSERT_MAPPING(Num7),
INSERT_MAPPING(Num8),
INSERT_MAPPING(Num9),
INSERT_MAPPING(Escape),
INSERT_MAPPING(LControl),
INSERT_MAPPING(LShift),
INSERT_MAPPING(LAlt),
INSERT_MAPPING(LSystem),
INSERT_MAPPING(RControl),
INSERT_MAPPING(RShift),
INSERT_MAPPING(RAlt),
INSERT_MAPPING(RSystem),
INSERT_MAPPING(Menu),
INSERT_MAPPING(LBracket),
INSERT_MAPPING(RBracket),
INSERT_MAPPING(Semicolon),
INSERT_MAPPING(Comma),
INSERT_MAPPING(Period),
INSERT_MAPPING(Apostrophe),
INSERT_MAPPING(Slash),
INSERT_MAPPING(Backslash),
INSERT_MAPPING(Grave),
INSERT_MAPPING(Equal),
INSERT_MAPPING(Hyphen),
INSERT_MAPPING(Space),
INSERT_MAPPING(Enter),
INSERT_MAPPING(Backspace),
INSERT_MAPPING(Tab),
INSERT_MAPPING(PageUp),
INSERT_MAPPING(PageDown),
INSERT_MAPPING(End),
INSERT_MAPPING(Home),
INSERT_MAPPING(Insert),
INSERT_MAPPING(Delete),
INSERT_MAPPING(Add),
INSERT_MAPPING(Subtract),
INSERT_MAPPING(Multiply),
INSERT_MAPPING(Divide),
INSERT_MAPPING(Left),
INSERT_MAPPING(Right),
INSERT_MAPPING(Up),
INSERT_MAPPING(Down),
INSERT_MAPPING(Numpad0),
INSERT_MAPPING(Numpad1),
INSERT_MAPPING(Numpad2),
INSERT_MAPPING(Numpad3),
INSERT_MAPPING(Numpad4),
INSERT_MAPPING(Numpad5),
INSERT_MAPPING(Numpad6),
INSERT_MAPPING(Numpad7),
INSERT_MAPPING(Numpad8),
INSERT_MAPPING(Numpad9),
INSERT_MAPPING(F1),
INSERT_MAPPING(F2),
INSERT_MAPPING(F3),
INSERT_MAPPING(F4),
INSERT_MAPPING(F5),
INSERT_MAPPING(F6),
INSERT_MAPPING(F7),
INSERT_MAPPING(F8),
INSERT_MAPPING(F9),
INSERT_MAPPING(F10),
INSERT_MAPPING(F11),
INSERT_MAPPING(F12),
INSERT_MAPPING(F13),
INSERT_MAPPING(F14),
INSERT_MAPPING(F15),
INSERT_MAPPING(Pause)
};
#undef INSERT_MAPPING

View File

@@ -0,0 +1,35 @@
#pragma once
#include <string>
enum PiecesType {
CONVEX_PIECES,
HOLELESS_PIECES,
OTHER_PIECES,
ALL_PIECES,
SINGLE_PIECE
};
inline int getSizeOfPieces(PiecesType type) {
if (type < SINGLE_PIECE) return 0;
else return (type - SINGLE_PIECE + 1);
}
inline PiecesType createSinglePieceType(int size) {
return PiecesType(SINGLE_PIECE + size - 1);
}
inline std::string getPiecesTypeName(PiecesType piecesType) {
static const std::string PIECES_TYPE_NAME[] = {
"CONVEX",
"HOLELESS",
"OTHER",
"ALL",
"SINGLE"
};
return PIECES_TYPE_NAME[piecesType];
}

View File

@@ -0,0 +1,163 @@
#include "PlayerCursor.h"
#include "Keybinds.h"
#include "Settings.h"
#include <vector>
#include <algorithm>
#include <SFML/Graphics.hpp>
static const int MENU_DAS = FRAMES_PER_SECOND / 2;
PlayerCursor::PlayerCursor(std::vector<unsigned int> rows) :
rows(rows) {
this->position = sf::Vector2u({0, 0});
this->leftDAS = 0;
this->rightDAS = 0;
this->upDAS = 0;
this->downDAS = 0;
}
void PlayerCursor::updatePosition() {
(sf::Keyboard::isKeyPressed(sfKey::Left)) ? (this->leftDAS++) : (this->leftDAS = 0);
if (this->shouldMove(this->leftDAS)) {
this->moveLeft();
}
(sf::Keyboard::isKeyPressed(sfKey::Right)) ? (this->rightDAS++) : (this->rightDAS = 0);
if (this->shouldMove(this->rightDAS)) {
this->moveRight();
}
(sf::Keyboard::isKeyPressed(sfKey::Up)) ? (this->upDAS++) : (this->upDAS = 0);
if (this->shouldMove(this->upDAS)) {
this->moveUp();
}
(sf::Keyboard::isKeyPressed(sfKey::Down)) ? (this->downDAS++) : (this->downDAS = 0);
if (this->shouldMove(this->downDAS)) {
this->moveDown();
}
}
bool PlayerCursor::movedLeft() const {
return this->shouldMove(this->leftDAS);
}
bool PlayerCursor::movedRight() const {
return this->shouldMove(this->rightDAS);
}
bool PlayerCursor::movedUp() const {
return this->shouldMove(this->upDAS);
}
bool PlayerCursor::movedDown() const {
return this->shouldMove(this->downDAS);
}
void PlayerCursor::goToPosition(const sf::Vector2u& newPosition) {
if (this->rows.size() > newPosition.y) {
if (this->rows.at(newPosition.y) > newPosition.x) {
this->position = newPosition;
}
}
}
bool PlayerCursor::addPosition(unsigned int x, unsigned int y) {
if (y >= this->rows.size()) return false;
if (x > this->rows.at(y)) return false;
this->rows.at(y)++;
if ((y == this->position.y) && (x <= this->position.x)) {
this->position.x++;
}
return true;
}
bool PlayerCursor::removePosition(unsigned int x, unsigned int y) {
if (y >= this->rows.size()) return false;
if (x >= this->rows.at(y)) return false;
this->rows.at(y)--;
if ((y == this->position.y) && (x < this->position.x)) {
this->position.x--;
}
return true;
}
bool PlayerCursor::addRow(unsigned int position, unsigned int width) {
if (position > this->rows.size()) return false;
this->rows.insert(this->rows.begin() + position, width);
if (position <= this->position.y) {
this->position.y++;
}
return true;
}
bool PlayerCursor::removeRow(unsigned int position) {
if (position >= this->rows.size()) return false;
this->rows.erase(this->rows.begin() + position);
if (position < this->position.y) {
this->position.y--;
}
return true;
}
const sf::Vector2u& PlayerCursor::getPosition() const {
return this->position;
}
bool PlayerCursor::shouldMove(int DAS) const {
return (DAS == 1
|| (DAS > MENU_DAS && (DAS % 5) == 0)
|| (DAS > (FRAMES_PER_SECOND * 2)));
}
void PlayerCursor::moveLeft() {
if (this->position.x == 0) {
this->position.x = this->rows.at(this->position.y) - 1;
}
else {
this->position.x--;
}
}
void PlayerCursor::moveRight() {
if (this->position.x == this->rows.at(this->position.y) - 1) {
this->position.x = 0;
}
else {
this->position.x++;
}
}
void PlayerCursor::moveUp() {
if (this->position.y == 0) {
this->position.y = this->rows.size() - 1;
}
else {
this->position.y--;
}
if (this->position.x >= this->rows.at(this->position.y)) {
this->position.x = this->rows.at(this->position.y) - 1;
}
}
void PlayerCursor::moveDown() {
if (this->position.y == this->rows.size() - 1) {
this->position.y = 0;
}
else {
this->position.y++;
}
if (this->position.x >= this->rows.at(this->position.y)) {
this->position.x = this->rows.at(this->position.y) - 1;
}
}

View File

@@ -0,0 +1,51 @@
#pragma once
#include <vector>
#include <SFML/Graphics.hpp>
class PlayerCursor {
private:
std::vector<unsigned int> rows;
sf::Vector2u position;
int leftDAS;
int rightDAS;
int upDAS;
int downDAS;
public:
PlayerCursor(std::vector<unsigned int> rows);
void updatePosition();
bool movedLeft() const;
bool movedRight() const;
bool movedUp() const;
bool movedDown() const;
void goToPosition(const sf::Vector2u& newPosition);
bool addPosition(unsigned int x, unsigned int y);
bool removePosition(unsigned int x, unsigned int y);
bool addRow(unsigned int position, unsigned int width);
bool removeRow(unsigned int position);
const sf::Vector2u& getPosition() const;
private:
bool shouldMove(int DAS) const;
void moveLeft();
void moveRight();
void moveUp();
void moveDown();
};

View File

@@ -0,0 +1,308 @@
#include "Settings.h"
#include "../Core/Menu.h"
#include "Keybinds.h"
#include <fstream>
#include <algorithm>
#include <SFML/Graphics.hpp>
static const sf::Vector2u BASE_WINDOW_SIZE = {80, 50};
static const int WINDOW_SIZE_MULTIPLIERS[] = {4, 6, 10, 14, 20, 30, 40};
static const int WINDOW_SIZE_LAST_MODE = (sizeof(WINDOW_SIZE_MULTIPLIERS) / sizeof(int)) - 1;
static const int START_TIMER_MAX = 4;
static const int DISTRIBUTION_MAX = 10;
Settings::Settings() {
for (int i = 1; i <= MAXIMUM_PIECES_SIZE; i++) {
this->menu.getPiecesList().loadPieces(i);
}
this->keybinds.reserve(NUMBER_OF_KEYBINDS);
for (int i = 0; i < NUMBER_OF_KEYBINDS; i++) {
this->keybinds.emplace_back(i);
}
this->loadSettingsFromFile();
}
void Settings::loadSettingsFromFile() {
std::ifstream settingsFile("data/config/settings.bin", std::ios::binary);
char byte;
// keybind layout
settingsFile.get(byte);
this->chosenKeybinds = byte;
// DAS tuning
settingsFile.get(byte);
this->menu.getPlayerControls().setDAS(byte);
// ARR tuning
settingsFile.get(byte);
this->menu.getPlayerControls().setARR(byte);
// SDR tuning
settingsFile.get(byte);
this->menu.getPlayerControls().setSDR(byte);
// window size mode
settingsFile.get(byte);
this->windowSizeMode = byte;
// start timer length
settingsFile.get(byte);
this->startTimerLength = byte;
// gamemode
settingsFile.get(byte);
this->gamemode = Gamemode(byte);
// board width
settingsFile.get(byte);
this->menu.setBoardWidth(byte);
// board height
settingsFile.get(byte);
this->menu.setBoardHeight(byte);
// piece distribution
settingsFile.get(byte);
this->menu.getPiecesList().setDistributionMode(DistributionMode(byte));
this->distributions.clear();
this->distributions.push_back(0);
for (int i = 1; i <= 15; i++) {
settingsFile.get(byte);
this->distributions.push_back(byte);
}
this->confirmDistribution();
// selected pieces
char pieceType;
char pieceValue;
this->selectedPieces.clear();
while (settingsFile.get(pieceType)) {
if (settingsFile.eof()) break;
settingsFile.get(pieceValue);
this->selectedPieces.push_back({PiecesType(pieceType), pieceValue});
}
this->confirmSelectedPieces();
}
void Settings::saveSettingsToFile() const {
this->keybinds.at(CUSTOMIZABLE_KEYBINDS).saveKeybindsToFile();
std::ofstream settingsFile("data/config/settings.bin", std::ios::trunc | std::ios::binary);
char byte;
// keybind layout
byte = this->chosenKeybinds;
settingsFile.write(&byte, 1);
// DAS tuning
byte = this->menu.readPlayerControls().getDAS();
settingsFile.write(&byte, 1);
// ARR tuning
byte = this->menu.readPlayerControls().getARR();
settingsFile.write(&byte, 1);
// SDR tuning
byte = this->menu.readPlayerControls().getSDR();
settingsFile.write(&byte, 1);
// window size mode
byte = this->windowSizeMode;
settingsFile.write(&byte, 1);
// start timer length
byte = this->startTimerLength;
settingsFile.write(&byte, 1);
// gamemode
byte = this->gamemode;
settingsFile.write(&byte, 1);
// board width
byte = this->menu.getBoardWidth();
settingsFile.write(&byte, 1);
// board height
byte = this->menu.getBoardHeight();
settingsFile.write(&byte, 1);
// piece distribution
byte = this->menu.readPiecesList().getDistributionMode();
settingsFile.write(&byte, 1);
for (int i = 1; i <= 15; i++) {
byte = this->distributions.at(i);
settingsFile.write(&byte, 1);
}
// selected pieces
for (const auto& [type, value] : this->selectedPieces) {
byte = type;
settingsFile.write(&byte, 1);
byte = value;
settingsFile.write(&byte, 1);
}
}
bool Settings::selectNextKeybinds() {
if (this->chosenKeybinds < (NUMBER_OF_KEYBINDS - 1)) {
this->chosenKeybinds++;
return true;
}
return false;
}
bool Settings::selectPreviousKeybinds() {
if (this->chosenKeybinds > 0) {
this->chosenKeybinds--;
return true;
}
return false;
}
bool Settings::canModifyCurrentKeybinds() const {
return (this->chosenKeybinds == CUSTOMIZABLE_KEYBINDS);
}
bool Settings::widenWindow() {
if (this->windowSizeMode < WINDOW_SIZE_LAST_MODE) {
this->windowSizeMode++;
return true;
}
return false;
}
bool Settings::shortenWindow() {
if (this->windowSizeMode > 0) {
this->windowSizeMode--;
return true;
}
return false;
}
void Settings::changeVideoMode(sf::RenderWindow& window) const {
sf::VideoMode videoMode(BASE_WINDOW_SIZE * (unsigned int) WINDOW_SIZE_MULTIPLIERS[this->windowSizeMode]);
window.create(videoMode, "jminos", sf::Style::Close | sf::Style::Titlebar);
sf::Vector2u desktopSize = sf::VideoMode::getDesktopMode().size;
sf::Vector2u windowSize = window.getSize();
window.setPosition(sf::Vector2i((desktopSize.x / 2) - (windowSize.x / 2), (desktopSize.y / 2) - (windowSize.y / 2)));
}
bool Settings::lengthenStartTimer() {
if (this->startTimerLength < START_TIMER_MAX) {
this->startTimerLength++;
return true;
}
return false;
}
bool Settings::shortenStartTimer() {
if (this->startTimerLength > 0) {
this->startTimerLength--;
return true;
}
return false;
}
void Settings::setGamemode(Gamemode gamemode) {
this->gamemode = gamemode;
}
void Settings::selectPieces(PiecesType type, int value) {
this->selectedPieces.emplace_back(type, value);
}
void Settings::unselectPieces(int index) {
if (index >= this->selectedPieces.size()) return;
this->selectedPieces.erase(this->selectedPieces.begin() + index);
}
void Settings::confirmSelectedPieces() {
this->menu.getPiecesList().unselectAll();
for (const auto& [type, value] : this->selectedPieces) {
int size = getSizeOfPieces(type);
if (size == 0) {
switch (type) {
case CONVEX_PIECES : {this->menu.getPiecesList().selectConvexPieces(value); break;}
case HOLELESS_PIECES : {this->menu.getPiecesList().selectHolelessPieces(value); break;}
case OTHER_PIECES : {this->menu.getPiecesList().selectOtherPieces(value); break;}
case ALL_PIECES : {this->menu.getPiecesList().selectAllPieces(value); break;}
}
}
else {
if (size > MAXIMUM_PIECES_SIZE) return;
this->menu.getPiecesList().selectPiece(size, value);
}
}
}
bool Settings::increaseDistribution(int size) {
if (size < 1 || size > MAXIMUM_PIECES_SIZE) return false;
if (this->distributions.at(size) < DISTRIBUTION_MAX) {
this->distributions.at(size)++;
return true;
}
return false;
}
bool Settings::decreaseDistribution(int size) {
if (size < 1 || size > MAXIMUM_PIECES_SIZE) return false;
if (this->distributions.at(size) > 0) {
this->distributions.at(size)--;
return true;
}
return false;
}
void Settings::confirmDistribution() {
for (int i = 1; i <= 15; i++) {
this->menu.getPiecesList().changeCustomDistribution(i, (double) 1 / (this->distributions.at(i) + 0.001));
}
}
Menu& Settings::getMenu() {
return this->menu;
}
Keybinds& Settings::getKeybinds() {
return this->keybinds.at(this->chosenKeybinds);
}
int Settings::getKeybindsLayout() const {
return this->chosenKeybinds;
}
Gamemode Settings::getGamemode() const {
return this->gamemode;
}
int Settings::getWindowSizeMultiplier() const {
return WINDOW_SIZE_MULTIPLIERS[this->windowSizeMode];
}
int Settings::getStartTimerLength() const {
return this->startTimerLength;
}
const std::vector<std::pair<PiecesType, int>>& Settings::getSelectedPieces() const {
return this->selectedPieces;
}
const std::vector<int>& Settings::getDistributions() const {
return this->distributions;
}

View File

@@ -0,0 +1,84 @@
#pragma once
#include "../Core/Menu.h"
#include "Keybinds.h"
#include "PiecesType.h"
#include <SFML/Graphics.hpp>
#include <vector>
static const int MAXIMUM_BOARD_WIDTH = 40;
static const int MAXIMUM_BOARD_HEIGHT = 40;
//#define __JMINOS_RELEASE__
#ifdef __JMINOS_RELEASE__
static const int MAXIMUM_PIECES_SIZE = 15;
#else
static const int MAXIMUM_PIECES_SIZE = 10;
#endif
class Settings {
private:
Menu menu;
std::vector<Keybinds> keybinds;
int chosenKeybinds;
int windowSizeMode;
int startTimerLength;
Gamemode gamemode;
std::vector<std::pair<PiecesType, int>> selectedPieces;
std::vector<int> distributions;
public:
Settings();
void loadSettingsFromFile();
void saveSettingsToFile() const;
bool selectNextKeybinds();
bool selectPreviousKeybinds();
bool canModifyCurrentKeybinds() const;
bool widenWindow();
bool shortenWindow();
void changeVideoMode(sf::RenderWindow& window) const;
bool lengthenStartTimer();
bool shortenStartTimer();
void setGamemode(Gamemode gamemode);
void selectPieces(PiecesType type, int value);
void unselectPieces(int index);
void confirmSelectedPieces();
bool increaseDistribution(int size);
bool decreaseDistribution(int size);
void confirmDistribution();
Menu& getMenu();
Keybinds& getKeybinds();
int getKeybindsLayout() const;
Gamemode getGamemode() const;
int getWindowSizeMultiplier() const;
int getStartTimerLength() const;
const std::vector<std::pair<PiecesType, int>>& getSelectedPieces() const;
const std::vector<int>& getDistributions() const;
};

166
src/GraphicalUI/main.cpp Normal file
View File

@@ -0,0 +1,166 @@
#include "GraphApp.h"
#include "../Pieces/PiecesFiles.h"
#include <filesystem>
#include <fstream>
void resetSettingsFile();
void resetKeybindFile(int layout);
int main() {
std::srand(std::time(NULL));
PiecesFiles pf;
for (int i = 1; i <= MAXIMUM_PIECES_SIZE; i++) {
if (!std::filesystem::exists("data/pieces/" + std::to_string(i) + "minos.bin")) {
std::cout << "pieces files for size " << i << " not found, generating..." << std::endl;
pf.savePieces(i);
}
}
if (!std::filesystem::exists("data/config/settings.bin")) {
std::cout << "settings file not found, generating..." << std::endl;
resetSettingsFile();
}
for (int i = 0; i < NUMBER_OF_KEYBINDS; i++) {
if (!std::filesystem::exists("data/config/keybinds/layout" + std::to_string(i) + ".bin")) {
std::cout << "keybind file n°" << (i + 1) << "/" << NUMBER_OF_KEYBINDS << " not found, generating..." << std::endl;
resetKeybindFile(i);
}
}
GraphApp UI;
UI.run();
return 0;
}
void resetSettingsFile() {
std::ofstream settingsFile("data/config/settings.bin", std::ios::trunc | std::ios::binary);
char byte;
Menu menu;
// keybind layout
byte = 0;
settingsFile.write(&byte, 1);
// DAS tuning
byte = menu.getPlayerControls().getDAS();
settingsFile.write(&byte, 1);
// ARR tuning
byte = menu.getPlayerControls().getARR();
settingsFile.write(&byte, 1);
// SDR tuning
byte = menu.getPlayerControls().getSDR();
settingsFile.write(&byte, 1);
// window size mode
byte = 2;
settingsFile.write(&byte, 1);
// start timer length
byte = 2;
settingsFile.write(&byte, 1);
// gamemode
byte = Gamemode(0);
settingsFile.write(&byte, 1);
// board width
byte = menu.getBoardWidth();
settingsFile.write(&byte, 1);
// board height
byte = menu.getBoardHeight();
settingsFile.write(&byte, 1);
// piece distribution
byte = DEFAULT;
settingsFile.write(&byte, 1);
for (int i = 1; i <= 15; i++) {
byte = 1;
settingsFile.write(&byte, 1);
}
// selected pieces
byte = ALL_PIECES;
settingsFile.write(&byte, 1);
byte = 4;
settingsFile.write(&byte, 1);
}
void resetKeybindFile(int layout) {
if (layout < 0 || layout > 4) return;
std::ofstream layoutFile("data/config/keybinds/layout" + std::to_string(layout) + ".bin", std::ios::trunc | std::ios::binary);
std::map<Action, sfKey> keybinds;
if (layout != 4) {
keybinds.insert({PAUSE, sfKey::P});
keybinds.insert({RETRY, sfKey::R});
}
if (layout == 0) {
keybinds.insert({MOVE_LEFT, sfKey::Left});
keybinds.insert({MOVE_RIGHT, sfKey::Right});
keybinds.insert({SOFT_DROP, sfKey::Down});
keybinds.insert({HARD_DROP, sfKey::Space});
keybinds.insert({ROTATE_CW, sfKey::Up});
keybinds.insert({ROTATE_CCW, sfKey::Z});
keybinds.insert({ROTATE_180, sfKey::X});
keybinds.insert({ROTATE_0, sfKey::LShift});
keybinds.insert({HOLD, sfKey::C});
}
if (layout == 1) {
keybinds.insert({MOVE_LEFT, sfKey::Z});
keybinds.insert({MOVE_RIGHT, sfKey::C});
keybinds.insert({SOFT_DROP, sfKey::X});
keybinds.insert({HARD_DROP, sfKey::S});
keybinds.insert({ROTATE_CW, sfKey::M});
keybinds.insert({ROTATE_CCW, sfKey::Comma});
keybinds.insert({ROTATE_180, sfKey::J});
keybinds.insert({ROTATE_0, sfKey::K});
keybinds.insert({HOLD, sfKey::LShift});
}
if (layout == 2) {
keybinds.insert({MOVE_LEFT, sfKey::A});
keybinds.insert({MOVE_RIGHT, sfKey::D});
keybinds.insert({SOFT_DROP, sfKey::W});
keybinds.insert({HARD_DROP, sfKey::S});
keybinds.insert({ROTATE_CW, sfKey::Left});
keybinds.insert({ROTATE_CCW, sfKey::Right});
keybinds.insert({ROTATE_180, sfKey::Up});
keybinds.insert({ROTATE_0, sfKey::Down});
keybinds.insert({HOLD, sfKey::RShift});
}
if (layout == 3) {
keybinds.insert({MOVE_LEFT, sfKey::Left});
keybinds.insert({MOVE_RIGHT, sfKey::Right});
keybinds.insert({SOFT_DROP, sfKey::Down});
keybinds.insert({HARD_DROP, sfKey::Up});
keybinds.insert({ROTATE_CW, sfKey::E});
keybinds.insert({ROTATE_CCW, sfKey::A});
keybinds.insert({ROTATE_180, sfKey::Num2});
keybinds.insert({ROTATE_0, sfKey::Tab});
keybinds.insert({HOLD, sfKey::Z});
}
char byte;
for (Action action : ACTION_LIST_IN_ORDER) {
byte = action;
layoutFile.write(&byte, 1);
if (keybinds.contains(action)) {
byte = (int) keybinds.at(action);
layoutFile.write(&byte, 1);
}
byte = 0xFF;
layoutFile.write(&byte, 1);
}
}

37
src/Pieces/Block.h Normal file
View File

@@ -0,0 +1,37 @@
#pragma once
#include <string>
/**
* Every possible block type
*/
enum Block {
NOTHING,
OUT_OF_BOUNDS,
GARBAGE,
PURPLE,
ORANGE,
CYAN,
PINK,
YELLOW,
RED,
BLUE,
GREEN
};
/**
* Gets the first block type a piece can be
* @return The block type
*/
inline Block firstPieceBlockType() {
return Block(PURPLE);
}
/**
* Sets the block to the next available piece block type
*/
inline void nextPieceBlockType(Block& block) {
block = (block == GREEN) ? PURPLE : Block(block + 1);
}

View File

@@ -1,66 +0,0 @@
#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
*/
inline 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
*/
inline 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
*/
inline 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
*/
inline 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
*/
inline 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
*/
inline 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
*/
inline std::ostream& operator<<(std::ostream& os, const Cell& cell) {
os << "x: " << cell.x << " y: " << cell.y;
return os;
}

View File

@@ -1,51 +1,55 @@
#pragma once
#include <String>
#include "Block.h"
#include "string"
/**
* Every possible colors a block can take
* A color encoded in RGB
*/
enum Color {
NOTHING,
OUT_OF_BOUND,
GARBAGE,
PURPLE,
ORANGE,
CYAN,
PINK,
YELLOW,
RED,
BLUE,
GREEN
struct Color {
unsigned char red; // the red component of the color
unsigned char green; // the green component of the color
unsigned char blue; // the blue component of the color
};
/**
* Returns the first color a piece can take
*/
inline Color firstPieceColor() {
return Color(PURPLE);
}
/**
* Sets the color to the next available piece color
*/
inline void nextPieceColor(Color& color) {
color = (color == GREEN) ? PURPLE : Color(color + 1);
}
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
static const Color EMPTY_BLOCK_COLOR = {255, 255, 255}; // color of an empty block
static const Color BLOCKS_COLOR[] = { // color for each block type
EMPTY_BLOCK_COLOR, // NOTHING
EMPTY_BLOCK_COLOR, // OUT_OF_BOUNDS
{150, 150, 150}, // GARBAGE
{150, 0, 255}, // PURPLE
{255, 150, 0}, // ORANGE
{0, 255, 255}, // CYAN
{255, 0, 200}, // PINK
{255, 255, 0}, // YELLOW
{255, 0, 0}, // RED
{0, 100, 255}, // BLUE
{0, 255, 0} // GREEN
};
/**
* Translates the color into a color code to change the console's color
* @return A string to print in the console
*/
inline std::string getConsoleColorCode(const Color& color) {
return "\033[38;2;" + std::to_string(color.red) + ";" + std::to_string(color.green) + ";" + std::to_string(color.blue) + "m";
}
/**
* Translates the color into a color code to change the console's color
* @return A string to print in the console
*/
inline std::string getConsoleColorCode(const Block block) {
return getConsoleColorCode(BLOCKS_COLOR[block]);
}
/**
* Gets a color code to reset the console's color
* @return A string to print in the console
*/
inline std::string getResetConsoleColorCode() {
return getConsoleColorCode(EMPTY_BLOCK_COLOR);
}

View File

@@ -2,40 +2,38 @@
#include "Polyomino.h"
#include <Vector>
#include <Set>
#include <Map>
#include <vector>
#include <set>
#include <map>
#include <algorithm>
Generator::Generator() {
}
std::vector<Polyomino> Generator::generatePolyominos(unsigned int order) {
// initialization
this->validPolyominos.clear();
std::vector<Polyomino> Generator::generatePolyominoes(int polyominoSize) {
this->validPolyominoes.clear();
this->currentTestedShape.clear();
// no polyomino with 0 cells
if (order == 0) return this->validPolyominos;
// a polyomino has at least 1 square
if (polyominoSize < 1) return this->validPolyominoes;
// start generating from the monomino
this->currentTestedShape.insert(Cell{0, 0});
// always place the first cell at (0, 0)
this->currentTestedShape.insert(Position{0, 0});
// generate polyominos
std::map<Cell, int> candidateCells;
this->generate(order, 0, 1, candidateCells);
return this->validPolyominos;
std::map<Position, int> candidatePositions;
this->generate(polyominoSize, 0, 1, candidatePositions);
return this->validPolyominoes;
}
void Generator::generate(unsigned int order, int lastAddedCellNumber, int nextAvaibleNumber, std::map<Cell, int> candidateCells) {
void Generator::generate(int polyominoSize, int lastAddedPositionNumber, int nextAvaibleNumber, std::map<Position, int> candidatePositions) {
// recursion stop
if (order == this->currentTestedShape.size()) {
// we test the polyomino formed by the current shape
if (polyominoSize == this->currentTestedShape.size()) {
Polyomino candidate(this->currentTestedShape);
// we sort the rotations of the polyominos
// we sort the rotations of the polyominoes
std::vector<Polyomino> candidateRotations;
candidateRotations.reserve(4);
for (int i = 0; i < 4; i++) {
candidate.normalize();
candidateRotations.push_back(candidate);
@@ -45,41 +43,41 @@ void Generator::generate(unsigned int order, int lastAddedCellNumber, int nextAv
// we keep the polyomino only if it was generated in its lowest rotation
if (candidate == candidateRotations.at(0)) {
this->validPolyominos.push_back(candidate);
this->validPolyominoes.push_back(candidate);
}
return;
}
// 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 the list of candidate positions
for (Position position : this->currentTestedShape) {
this->tryToAddCandidatePosition(Position{position.x, position.y + 1}, nextAvaibleNumber, candidatePositions);
this->tryToAddCandidatePosition(Position{position.x + 1, position.y}, nextAvaibleNumber, candidatePositions);
this->tryToAddCandidatePosition(Position{position.x, position.y - 1}, nextAvaibleNumber, candidatePositions);
this->tryToAddCandidatePosition(Position{position.x - 1, position.y}, nextAvaibleNumber, candidatePositions);
}
// generate polyominos for all cells with a higher number than the last one
for (auto [key, val] : candidateCells) {
if (val > lastAddedCellNumber) {
// try adding a square only to positions with a higher number than the last one
for (auto [key, val] : candidatePositions) {
if (val > lastAddedPositionNumber) {
this->currentTestedShape.insert(key);
this->generate(order, val, nextAvaibleNumber, (order == this->currentTestedShape.size()) ? std::map<Cell, int>() : candidateCells);
this->generate(polyominoSize, val, nextAvaibleNumber, (polyominoSize == this->currentTestedShape.size()) ? std::map<Position, int>() : candidatePositions);
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
void Generator::tryToAddCandidatePosition(const Position& candidate, int& nextAvaibleNumber, std::map<Position, int>& candidatepositions) {
// we declared the first position as the lower-left square, since we always start with a monomino at (0,0) we can test with 0 directly
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 position was already marked then we should not mark it again
if (candidatepositions.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});
// once all tests passed we can add the position
candidatepositions.insert({candidate, nextAvaibleNumber});
nextAvaibleNumber++;
}

View File

@@ -2,38 +2,39 @@
#include "Polyomino.h"
#include <Vector>
#include <Set>
#include <Map>
#include <vector>
#include <set>
#include <map>
/**
* A generator of one-sided polyominos of any size
* A generator of one-sided polyominoes of any size
*/
class Generator {
private:
std::vector<Polyomino> validPolyominos; // the list of already generated polyominos
std::set<Cell> currentTestedShape; // the polyomino being created
std::vector<Polyomino> validPolyominoes; // the list of already generated polyominoes
std::set<Position> currentTestedShape; // the polyomino being created
public:
/**
* Initializes generator
* Default constructor
*/
Generator();
/**
* Returns the list of all one-sided polyominos of the specified size
* Generates the list of all one-sided polyominoes of the specified size
* @return The list of polyominoes
*/
std::vector<Polyomino> generatePolyominos(unsigned int order);
std::vector<Polyomino> generatePolyominoes(int polyominoSize);
private:
/**
* Generates all one-sided polyominos of the specified using the current tested shape
* Generates all one-sided polyominoes of the specified size using the current tested shape
*/
void generate(unsigned int order, int lastAddedCellNumber, int nextAvaibleNumber, std::map<Cell, int> candidateCells);
void generate(int polyominoSize, int lastAddedPositionNumber, int nextAvaibleNumber, std::map<Position, int> candidatePositions);
/**
* Check wheter a candidate cell can be added to the current tested shape
* Checks wheter a candidate position can be added to the current tested shape
*/
void tryToAddCandidateCell(const Cell& candidate, int& nextAvaibleNumber, std::map<Cell, int>& candidateCells);
void tryToAddCandidatePosition(const Position& candidate, int& nextAvaibleNumber, std::map<Position, int>& candidatePositions);
};

View File

@@ -2,13 +2,18 @@
#include "Polyomino.h"
#include "Rotation.h"
#include "Block.h"
#include "Color.h"
#include <Set>
#include <String>
#include <set>
#include <string>
Piece::Piece(const Polyomino& polyomino, Color color) : polyomino(polyomino), color(color) {
Piece::Piece(const Polyomino& polyomino, Block blockType) :
polyomino(polyomino),
blockType(blockType) {
this->rotationState = NONE;
}
void Piece::rotate(Rotation rotation) {
@@ -18,21 +23,34 @@ void Piece::rotate(Rotation rotation) {
this->polyomino.rotate180();
if (rotation == COUNTERCLOCKWISE)
this->polyomino.rotateCCW();
this->rotationState += rotation;
}
std::set<Cell> Piece::getPositions() const {
return this->polyomino.getCells();
void Piece::defaultRotation() {
if (this->rotationState == CLOCKWISE)
this->polyomino.rotateCCW();
if (this->rotationState == DOUBLE)
this->polyomino.rotate180();
if (this->rotationState == COUNTERCLOCKWISE)
this->polyomino.rotateCW();
this->rotationState = NONE;
}
const std::set<Position>& Piece::getPositions() const {
return this->polyomino.getPositions();
}
int Piece::getLength() const {
return this->polyomino.getLength();
}
Color Piece::getColor() const {
return this->color;
Block Piece::getBlockType() const {
return this->blockType;
}
std::ostream& operator<<(std::ostream& os, const Piece& piece) {
os << COLOR_CODES[piece.color] << piece.polyomino << COLOR_RESET;
os << getConsoleColorCode(piece.blockType) << piece.polyomino << getResetConsoleColorCode();
return os;
}

View File

@@ -2,9 +2,10 @@
#include "Polyomino.h"
#include "Rotation.h"
#include "Block.h"
#include "Color.h"
#include <Set>
#include <set>
/**
@@ -12,14 +13,15 @@
*/
class Piece {
private:
Polyomino polyomino; // a polyomino representing the piece, (0, 0) is downleft
Color color; // the color of the piece
Polyomino polyomino; // a polyomino representing the piece, (0, 0) is downleft
Block blockType; // the block type of the piece
Rotation rotationState; // the current rotation of the piece
public:
/**
* Creates a piece with a specified shape and color
* Creates a piece with a specified shape and block type
*/
Piece(const Polyomino& piece, Color color);
Piece(const Polyomino& piece, Block blockType);
/**
* Rotates the piece in the specified direction
@@ -27,22 +29,28 @@ class Piece {
void rotate(Rotation rotation);
/**
* Returns a copy of the list of cells of the piece
* Rotates the piece to its default rotation
*/
std::set<Cell> getPositions() const;
void defaultRotation();
/**
* Returns the length of the piece
* @return The list of positions of the piece
*/
const std::set<Position>& getPositions() const;
/**
* @return The length of the piece
*/
int getLength() const;
/**
* Returns the color of the piece
* @return The block type of the piece
*/
Color getColor() const;
Block getBlockType() const;
/**
* Stream output operator, adds a 2D grid representing the piece
* @return A reference to the output stream
*/
friend std::ostream& operator<<(std::ostream& os, const Piece& piece);
};

View File

@@ -3,8 +3,8 @@
#include "Generator.h"
#include "Piece.h"
#include <Vector>
#include <String>
#include <vector>
#include <string>
#include <iostream>
#include <fstream>
#include <filesystem>
@@ -14,10 +14,9 @@
PiecesFiles::PiecesFiles() {
}
bool PiecesFiles::savePieces(int order) const {
// open pieces file
bool PiecesFiles::savePieces(int polyominoSize) const {
std::string filePath;
if (!this->getFilePath(order, filePath)) {
if (!this->getFilePath(polyominoSize, filePath)) {
return false;
}
std::ofstream piecesFile(filePath, std::ios::trunc | std::ios::binary);
@@ -25,46 +24,34 @@ bool PiecesFiles::savePieces(int order) const {
return false;
}
// generates the polyominos
Generator generator;
std::vector<Polyomino> nMinos = generator.generatePolyominos(order);
std::vector<Polyomino> nMinos = generator.generatePolyominoes(polyominoSize);
// set the polyominos to their spawn position
// sorting the polyominoes is done after setting spawn position to ensure the order is always the same
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 = firstPieceColor();
for (int i = 0; i < order; i++) nextPieceColor(pieceColor);
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;
nextPieceColor(pieceColor);
for (const Polyomino& nMino : nMinos) {
// write the characteristics of the piece
char infoByte = (nMino.isConvex() << 7) + (nMino.hasHole() << 6) + nMino.getLength();
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);
// write the positions of the piece
char positionByte;
for (Position position : nMino.getPositions()) {
positionByte = (position.x << 4) + position.y;
piecesFile.write(&positionByte, 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
bool PiecesFiles::loadPieces(int polyominoSize, std::vector<Piece>& pieces, std::vector<int>& convexPieces, std::vector<int>& holelessPieces, std::vector<int>& otherPieces) const {
std::string filePath;
if (!this->getFilePath(order, filePath)) {
if (!this->getFilePath(polyominoSize, filePath)) {
return false;
}
std::ifstream piecesFile(filePath, std::ios::binary);
@@ -72,47 +59,48 @@ bool PiecesFiles::loadPieces(int order, std::vector<Piece>& pieces, std::vector<
return false;
}
// get empty vectors
pieces.clear();
convexPieces.clear();
holelessPieces.clear();
otherPieces.clear();
// set up masks
// we shift the first color of each size so that the small polyominoes (size 1-2-3) don't all have the same color
Block pieceBlock = firstPieceBlockType();
for (int i = 0; i < polyominoSize; i++) {
nextPieceBlockType(pieceBlock);
}
char convexMask = 0b1000'0000;
char holeMask = 0b0100'0000;
char colorMask = 0b0011'1111;
char lengthMask = 0b0011'1111;
char xMask = 0b1111'0000;
char yMask = 0b0000'1111;
// read the pieces
char lengthByte;
char infoByte;
int i = 0;
while (piecesFile.get(lengthByte)) {
while (piecesFile.get(infoByte)) {
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);
int length = (infoByte & lengthMask);
// 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});
// read positions
std::set<Position> piecePositions;
char positionByte;
for (int i = 0; i < polyominoSize; i++) {
piecesFile.get(positionByte);
int x = ((unsigned char) positionByte & xMask) >> 4;
int y = positionByte & yMask;
piecePositions.insert(Position{x, y});
}
// create piece
Piece readPiece(Polyomino(pieceCells, lengthByte), color);
Piece readPiece(Polyomino(piecePositions, length), pieceBlock);
nextPieceBlockType(pieceBlock);
pieces.push_back(readPiece);
// link it to its type
if (isConvex) {
convexPieces.push_back(i);
}
@@ -129,14 +117,12 @@ bool PiecesFiles::loadPieces(int order, std::vector<Piece>& pieces, std::vector<
return true;
}
bool PiecesFiles::getFilePath(int order, std::string& filePath) const {
// verify that the data folder exists
bool PiecesFiles::getFilePath(int polyominoSize, std::string& filePath) const {
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";
filePath = dataFolderPath + std::to_string(polyominoSize) + "minos.bin";
return true;
}

View File

@@ -2,8 +2,8 @@
#include "Piece.h"
#include <Vector>
#include <String>
#include <vector>
#include <string>
/**
@@ -12,26 +12,26 @@
class PiecesFiles {
public:
/**
* Initializes file manager
* Default constructor
*/
PiecesFiles();
/**
* Generate a file containing all the pieces of the specified size,
* returns false if the file couldn't be created
* Generate a file containing all the pieces of the specified size
* @return If the file could be created
*/
bool savePieces(int order) const;
bool savePieces(int polyominoSize) 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
* Replace the content of the vectors by the pieces of the specified size, if the file wasn't found the vectors stays untouched
* @return If the file was found
*/
bool loadPieces(int order, std::vector<Piece>& pieces, std::vector<int>& convexPieces, std::vector<int>& holelessPieces, std::vector<int>& otherPieces) const;
bool loadPieces(int polyominoSize, 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
* Puts the path to the piece file of the specified size in order, if the data folder wasn't found the string stays untouched
* @return If the data folder was found
*/
bool getFilePath(int order, std::string& filePath) const;
bool getFilePath(int polyominoSize, std::string& filePath) const;
};

View File

@@ -1,88 +1,85 @@
#include "Polyomino.h"
#include "Cell.h"
#include "Position.h"
#include <Vector>
#include <Set>
#include <vector>
#include <set>
#include <iostream>
#include <climits>
#include <algorithm>
#include <utility>
Polyomino::Polyomino(const std::set<Cell>& cells) {
// find min/max
Polyomino::Polyomino(const std::set<Position>& positions) {
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;
for (Position position : positions) {
if (position.x < minX) minX = position.x;
if (position.x > maxX) maxX = position.x;
if (position.y < minY) minY = position.y;
if (position.y > maxY) maxY = position.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);
// we normalize here instead of calling this->normalize() to reduce the number of calculations for the generation algorithm
std::set<Position> newPositions;
for (Position position : positions) {
newPositions.insert(Position{position.x - minX, position.y - minY});
}
this->positions = std::move(newPositions);
}
Polyomino::Polyomino(const std::set<Cell>& cells, int length) : cells(cells), length(length) {
Polyomino::Polyomino(const std::set<Position>& positions, int length) :
positions(positions),
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;
for (Position position : this->positions) {
if (position.x < minX) minX = position.x;
if (position.y < minY) minY = position.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});
std::set<Position> newPositions;
for (Position position : this->positions) {
newPositions.insert(Position{position.x - minX, position.y - minY});
}
this->cells = newCells;
this->positions = std::move(newPositions);
}
void Polyomino::rotateCW() {
// rotate 90° clockwise
std::set<Cell> newCells;
for (Cell cell : this->cells) {
newCells.insert(Cell{cell.y, (length - 1) - (cell.x)});
std::set<Position> newPositions;
for (Position position : this->positions) {
newPositions.insert(Position{position.y, (length - 1) - (position.x)});
}
this->cells = newCells;
this->positions = std::move(newPositions);
}
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)});
std::set<Position> newPositions;
for (Position position : this->positions) {
newPositions.insert(Position{(length - 1) - (position.x), (length - 1) - (position.y)});
}
this->cells = newCells;
this->positions = std::move(newPositions);
}
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});
std::set<Position> newPositions;
for (Position position : this->positions) {
newPositions.insert(Position{(length - 1) - (position.y), position.x});
}
this->cells = newCells;
this->positions = std::move(newPositions);
}
void Polyomino::goToSpawnPosition() {
// initialize array
std::vector<std::vector<int>> linesCompleteness;
linesCompleteness.reserve(4);
std::vector<int> empty;
for (int j = 0; j < this->length; j++) {
empty.push_back(0);
@@ -91,23 +88,23 @@ void Polyomino::goToSpawnPosition() {
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
// calculates amount of squares per rows and columns
for (Position position : this->positions) {
linesCompleteness.at(0).at(position.y) += 1; // 0 = bottom to top = no rotation
linesCompleteness.at(1).at((length - 1) - position.x) += 1; // 1 = right to left = CW
linesCompleteness.at(2).at((length - 1) - position.y) += 1; // 2 = top to bottom = 180
linesCompleteness.at(3).at(position.x) += 1; // 3 = left to right = CCW
}
// checks for empty lines
// count empty lines and push the non-empty lines to the start of each vector
int horizontalEmptyLines = 0;
int verticalEmptyLines = 0;
for (int i = 0; i < 4; i++) {
for (int j = this->length - 1; j >= 0; j--) {
if (linesCompleteness.at(i).at(j) == 0) {
// push the non-empty lines to the start of each vector
linesCompleteness.at(i).erase(linesCompleteness.at(i).begin() + j);
linesCompleteness.at(i).push_back(0);
if (i == 0) horizontalEmptyLines++;
if (i == 1) verticalEmptyLines++;
}
@@ -125,11 +122,11 @@ void Polyomino::goToSpawnPosition() {
currentFlattestSides[3] = false;
}
// checks for the flattest sides
// checks for the flattest side
int sideToBeOn = -1;
this->checkForFlattestSide(linesCompleteness, currentFlattestSides, sideToBeOn, false);
// if all sides are as flat, checks for the most left-biased side
// if ther's no winner, checks for the side which has the flattest side to its left
if (sideToBeOn == -1) {
this->checkForFlattestSide(linesCompleteness, currentFlattestSides, sideToBeOn, true);
}
@@ -149,47 +146,44 @@ void Polyomino::goToSpawnPosition() {
sideToBeOn = candidateSides.at(std::distance(candidateRotations.begin(), std::min_element(candidateRotations.begin(), candidateRotations.end())));
}
// do the correct rotation
if (sideToBeOn == 1) this->rotateCW();
if (sideToBeOn == 2) this->rotate180();
if (sideToBeOn == 3) this->rotateCCW();
// 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;
switch (sideToBeOn) {
case 1 : {this->rotateCW(); break;}
case 2 : {this->rotate180(); break;}
case 3 : {this->rotateCCW(); break;}
default: break;
}
// 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)});
int minX = INT_MAX;
int minY = INT_MAX;
for (Position position : this->positions) {
if (position.x < minX) minX = position.x;
if (position.y < minY) minY = position.y;
}
this->cells = newCells;
// center the piece with an up bias
std::set<Position> newPositions;
for (Position position : positions) {
newPositions.insert(Position{(position.x - minX) + (verticalEmptyLines / 2), (position.y - minY) + ((horizontalEmptyLines + 1) / 2)});
}
this->positions = std::move(newPositions);
}
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
// we check which sides are the flattest on this line
int max = 0;
std::set<int> maxOwners;
for (int i = 0; i < 4; i++) {
// only the candidate sides are compared
if (!currentFlattestSides[i]) continue;
// if we need to check the flatness of the side to the left
int sideToCheck = i;
if (checkLeftSide) {
sideToCheck = (i + 3) % 4;
}
// we check which sides are the flattest
if (linesCompleteness.at(sideToCheck).at(j) > max) {
max = linesCompleteness.at(sideToCheck).at(j);
maxOwners.clear();
@@ -200,13 +194,13 @@ void Polyomino::checkForFlattestSide(const std::vector<std::vector<int>>& linesC
}
}
// if there's no tie we choose this side
// if there's no tie we choose the only side
if (maxOwners.size() == 1) {
sideToBeOn = *maxOwners.begin();
return;
}
// else we only keep the flattest from this round and ignore the others
// else we only keep the flattest on this line and ignore the others
else {
for (int i = 0; i < 4; i++) {
currentFlattestSides[i] = currentFlattestSides[i] && maxOwners.contains(i);
@@ -216,23 +210,21 @@ void Polyomino::checkForFlattestSide(const std::vector<std::vector<int>>& linesC
}
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 (this->positions.contains(Position{i, j})) {
if (completedLine) return false;
else startedLine = true;
}
else {
if (startedLine) completedLine = true;
}
// column check
if (this->cells.contains(Cell{j, i})) {
if (this->positions.contains(Position{j, i})) {
if (completedColumn) return false;
else startedColumn = true;
}
@@ -245,71 +237,64 @@ bool Polyomino::isConvex() const {
}
bool Polyomino::hasHole() const {
// add every outer cells of the square containing the polyomino
std::set<Cell> emptyCells;
// add every empty square on the outer of the box containing the polyomino
std::set<Position> emptyPositions;
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
this->tryToInsertPosition(emptyPositions, Position{i, 0}); // up row
this->tryToInsertPosition(emptyPositions, Position{this->length - 1, i}); // rigth column
this->tryToInsertPosition(emptyPositions, Position{this->length - 1 - i, this->length - 1}); // bottom row
this->tryToInsertPosition(emptyPositions, Position{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());
// if we didn't reached all empty squares in the box then there was some contained within the polyomino, i.e. there was a hole
return (emptyPositions.size() < (this->length * this->length) - this->positions.size());
}
void Polyomino::tryToInsertCell(std::set<Cell>& emptyCells, const Cell& candidate) const {
// check if the cell is in the square containing the polyomino
void Polyomino::tryToInsertPosition(std::set<Position>& emptyPositions, const Position& candidate) const {
if (candidate.x >= this->length || candidate.x < 0 || candidate.y >= this->length || candidate.y < 0) return;
if (this->positions.contains(candidate) || emptyPositions.contains(candidate)) 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});
// if it's a new empty square, try its neighbors
emptyPositions.insert(candidate);
tryToInsertPosition(emptyPositions, Position{candidate.x, candidate.y + 1});
tryToInsertPosition(emptyPositions, Position{candidate.x + 1, candidate.y});
tryToInsertPosition(emptyPositions, Position{candidate.x, candidate.y - 1});
tryToInsertPosition(emptyPositions, Position{candidate.x - 1, candidate.y});
}
std::set<Cell> Polyomino::getCells() const {
return this->cells;
const std::set<Position>& Polyomino::getPositions() const {
return this->positions;
}
int Polyomino::getLength() const {
return this->length;
}
int Polyomino::getPolyominoOrder() const {
return this->cells.size();
int Polyomino::getPolyominoSize() const {
return this->positions.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;
bool hasThisPosition = this->positions.contains(Position{x, y});
bool hasOtherPosition = other.positions.contains(Position{x, y});
if (hasThisPosition != hasOtherPosition) return hasThisPosition;
}
}
// if they are equal
return false;
}
bool Polyomino::operator ==(const Polyomino& other) const {
return this->cells == other.cells;
bool Polyomino::operator==(const Polyomino& other) const {
return this->positions == other.positions;
}
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})) {
if (polyomino.positions.contains(Position{x, y})) {
os << "*";
}
else {

View File

@@ -1,9 +1,9 @@
#pragma once
#include "Cell.h"
#include "Position.h"
#include <Vector>
#include <Set>
#include <vector>
#include <set>
#include <iostream>
@@ -12,19 +12,19 @@
*/
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
std::set<Position> positions; // the squares composing the polyomino, (0,0) is downleft
int length; // the size of the smallest square box 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
* Creates a polyomino with the specified positions and normalizes it, wheter it is actually a polyonimo is not checked
*/
Polyomino(const std::set<Cell>& cells);
Polyomino(const std::set<Position>& positions);
/**
* Creates a polyomino with the specified cells and length, wheter it is actually a polyonimo of this length is not checked
* Creates a polyomino with the specified positions and length, wheter it is actually a polyonimo of this length is not checked
*/
Polyomino(const std::set<Cell>& cells, int length);
Polyomino(const std::set<Position>& positions, int length);
/**
* Translates the polyomino to the lowest unsigned values (lower row on y = 0, and left-most column on x = 0)
@@ -32,17 +32,17 @@ class Polyomino {
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)
* Rotates the polyomino 90° clockwise, the center of rotation being the middle of the box 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)
* Rotates the polyomino 180°, the center of rotation being the middle of the box 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)
* Rotates the polyomino 90° counter-clockwise, the center of rotation being the middle of the box going from (0,0) to (length-1, length-1)
*/
void rotateCCW();
@@ -59,12 +59,14 @@ class Polyomino {
public:
/**
* Returns wheter the polyomino is convex, that is if every line and column has at most one continuous line of cells
* Check if the polyomino is convex, that is if every line and column has at most one continuous line of positions
* @return If the polyomino is convex
*/
bool isConvex() const;
/**
* Returns wheter the polyomino has at least one hole
* Check if the polyomino has at least one hole
* @return If the polyomino has at least one hole
*/
bool hasHole() const;
@@ -72,37 +74,40 @@ class Polyomino {
/**
* Auxiliary method of hasHole()
*/
void tryToInsertCell(std::set<Cell>& emptyCells, const Cell& candidate) const;
void tryToInsertPosition(std::set<Position>& emptypositions, const Position& candidate) const;
public:
/**
* Returns a copy of the cells of the polyomino
* @return The positions of the polyomino
*/
std::set<Cell> getCells() const;
const std::set<Position>& getPositions() const;
/**
* Returns the length of the polyomino
* @return The length of the polyomino
*/
int getLength() const;
/**
* Returns the number of squares in the polyomino
* @return The number of squares in the polyomino
*/
int getPolyominoOrder() const;
int getPolyominoSize() 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
* while checking from left to right and top to bottom, is the first which has a square while the other don't
* @return If the polyomino is inferior than another
*/
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
* Equality operator, two polyominoes are equal if their positions are the same, that means two polyominoes of the same shape at different places will not be equal
* @return If the polyomino is equal to another
*/
bool operator ==(const Polyomino& other) const;
bool operator==(const Polyomino& other) const;
/**
* Stream output operator, adds a 2D grid representing the polyomino
* @return A reference to the output stream
*/
friend std::ostream& operator<<(std::ostream& os, const Polyomino& polyomino);
};

80
src/Pieces/Position.h Normal file
View File

@@ -0,0 +1,80 @@
#pragma once
#include <iostream>
/**
* A position on a 2D grid
*/
struct Position {
int x; // x position
int y; // y position
};
/**
* Addition operator
* @return The sums of the coordinates of both positions
*/
inline Position operator+(const Position& left, const Position& right) {
return Position{left.x + right.x, left.y + right.y};
}
/**
* Additive assignation operator, adds the coordinates of the right position to the left one
* @return A reference to the left position
*/
inline Position& operator+=(Position& left, const Position& right) {
left = left + right;
return left;
}
/**
* Substraction operator
* @return The difference of the coordinate between the left and right position
*/
inline Position operator-(const Position& left, const Position& right) {
return Position{left.x - right.x, left.y - right.y};
}
/**
* Substractive assignation operator, substracts the coordinates of the right position from the left one
* @return A reference to the left position
*/
inline Position& operator-=(Position& left, const Position& right) {
left = left + right;
return left;
}
/**
* Strict inferiority operator, a position is inferior to another if it is lower or at the same height and more to the left
* @return If the left position is inferior to the right position
*/
inline bool operator<(const Position& left, const Position& right) {
return (left.x == right.x) ? (left.y < right.y) : (left.x < right.x);
}
/**
* Equality operator, two positions are equal if they have the same coordinates
* @return If the two positions are equals
*/
inline bool operator==(const Position& left, const Position& right) {
return (left.x == right.x) && (left.y == right.y);
}
/**
* Inequality operator, two positions aren't equal if their coordinates aren't
* @return If the two positions aren't equals
*/
inline bool operator!=(const Position& left, const Position& right) {
return (left.x != right.x) || (left.y != right.y);
}
/**
* Stream output operator, adds the coordinates of the position to the stream
* @return A reference to the output stream
*/
inline std::ostream& operator<<(std::ostream& os, const Position& position) {
os << "x: " << position.x << " y: " << position.y;
return os;
}

View File

@@ -13,14 +13,16 @@ enum Rotation {
/**
* Addition operator, returns a rotation corresponding to doing both rotations
* Addition operator
* @return A rotation corresponding to doing both rotations
*/
inline Rotation operator+(const Rotation& left, const Rotation& right) {
return Rotation((left + right) % 4);
return Rotation(((int) left + (int) right) % 4);
}
/**
* Additive assignation operator, rotate the left rotation by the right rotation
* Additive assignation operator, rotates the left rotation by the right rotation
* @return A reference to the left rotation
*/
inline Rotation& operator+=(Rotation& left, const Rotation& right) {
left = left + right;

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

@@ -0,0 +1,344 @@
#include "TextApp.h"
#include "../Core/Menu.h"
#include <map>
#include <set>
#include <string>
#include <sstream>
#include <algorithm>
#include <cstdlib>
#include <exception>
static const int FRAMES_PER_INPUT = FRAMES_PER_SECOND / 2; // the number of frames that will pass everytime the player gives an input
static const int MAXIMUM_PIECE_SIZE = 10; // the maximum size of pieces that will be loaded and utilizable
static const int DEFAULT_PIECE_SIZE = 4; // the default size of pieces that will be selected when first running the app
static const int MAXIMUM_BOARD_WIDTH = 30; // the maximum selectable width of the board
static const int MAXIMUM_BOARD_HEIGHT = 40; // the maximum selectable height of the board
static const Gamemode DEFAULT_GAMEMODE = SPRINT; // the gamemode that will be used when starting a new game
TextApp::TextApp() {
this->defaultKeybinds();
this->gameMenu.getPiecesList().loadPieces(MAXIMUM_PIECE_SIZE);
this->gameMenu.getPiecesList().selectAllPieces(DEFAULT_PIECE_SIZE);
this->gameMenu.getPlayerControls().setDAS(FRAMES_PER_INPUT);
this->gameMenu.getPlayerControls().setARR(FRAMES_PER_INPUT);
this->gameMenu.getPlayerControls().setSDR(0);
}
void TextApp::run() {
bool quit = false;
while (!quit) {
std::cout << "\n\n\n";
std::cout << "===| WELCOME TO JMINOS! |===" << std::endl;
std::cout << "1- Change pieces" << std::endl;
std::cout << "2- Change board" << std::endl;
std::cout << "3- See controls" << std::endl;
std::cout << "4- Start game" << std::endl;
std::cout << "5- Quit" << std::endl;
std::cout << "Choice: ";
std::string answer;
std::getline(std::cin, answer);
int selectedAnswer = 0;
try {
selectedAnswer = std::stoi(answer);
}
catch (std::exception ignored) {}
switch (selectedAnswer) {
case 1 : {this->choosePieces(); break;}
case 2 : {this->chooseBoardSize(); break;}
case 3 : {this->seeKeybinds(); break;}
case 4 : {this->startGame(); break;}
case 5 : {quit = true; break;}
default : std::cout << "Invalid answer!" << std::endl;
}
}
std::cout << "===| SEE YA NEXT TIME! |===" << std::endl;
}
void TextApp::choosePieces() {
std::cout << "\n\n\n";
std::cout << "Choose which piece sizes to play with (from 1 to " << MAXIMUM_PIECE_SIZE << "), separate mutltiple sizes with blank spaces." << std::endl;
std::cout << "Choice: ";
std::string answer;
std::getline(std::cin, answer);
this->gameMenu.getPiecesList().unselectAll();
std::cout << "Selected pieces of sizes:";
std::stringstream answerStream(answer);
for (std::string size; std::getline(answerStream, size, ' ');) {
try {
int selectedSize = std::stoi(size);
if (selectedSize >= 1 && selectedSize <= MAXIMUM_PIECE_SIZE) {
if (this->gameMenu.getPiecesList().selectAllPieces(selectedSize)) {
std::cout << " " << selectedSize;
}
}
}
catch (std::exception ignored) {}
}
std::string waiting;
std::getline(std::cin, waiting);
}
void TextApp::chooseBoardSize() {
std::string answer;
std::cout << "\n\n\n";
std::cout << "Current board width and height: " << this->gameMenu.getBoardWidth() << "x" << this->gameMenu.getBoardHeight() << std::endl;
std::cout << "Choose the width of the board (from 1 to " << MAXIMUM_BOARD_WIDTH << ")." << std::endl;
std::cout << "Choice: ";
std::getline(std::cin, answer);
try {
int selectedSize = std::stoi(answer);
if (selectedSize >= 1 && selectedSize <= MAXIMUM_BOARD_WIDTH) {
this->gameMenu.setBoardWidth(selectedSize);
}
}
catch (std::exception ignored) {}
std::cout << "Choose the height of the board (from 1 to " << MAXIMUM_BOARD_HEIGHT << ")." << std::endl;
std::cout << "Choice: ";
std::getline(std::cin, answer);
try {
int selectedSize = std::stoi(answer);
if (selectedSize >= 1 && selectedSize <= MAXIMUM_BOARD_HEIGHT) {
this->gameMenu.setBoardHeight(selectedSize);
}
}
catch (std::exception ignored) {}
std::cout << "New board width and height: " << this->gameMenu.getBoardWidth() << "x" << this->gameMenu.getBoardHeight();
std::string waiting;
std::getline(std::cin, waiting);
}
void TextApp::seeKeybinds() const {
std::cout << "\n\n\n";
std::cout << "Quit/Pause/Retry: quit/pause/retry" << std::endl;
std::cout << "Hold : h" << std::endl;
std::cout << "Soft/Hard drop : sd/hd" << std::endl;
std::cout << "Move left/right : l/r" << std::endl;
std::cout << "Rotate 0/CW/180/CCW: c/cw/cc/ccw" << std::endl;
std::cout << "\n";
std::cout << "To do several actions at the same time, separe them with blank spaces." << std::endl;
std::string waiting;
std::getline(std::cin, waiting);
}
void TextApp::defaultKeybinds() {
this->keybinds.clear();
this->keybinds.insert({"pause", PAUSE});
this->keybinds.insert({"retry", RETRY});
this->keybinds.insert({"h", HOLD});
this->keybinds.insert({"sd", SOFT_DROP});
this->keybinds.insert({"hd", HARD_DROP});
this->keybinds.insert({"l", MOVE_LEFT});
this->keybinds.insert({"r", MOVE_RIGHT});
this->keybinds.insert({"c", ROTATE_0});
this->keybinds.insert({"cw", ROTATE_CW});
this->keybinds.insert({"cc", ROTATE_180});
this->keybinds.insert({"ccw", ROTATE_CCW});
}
void TextApp::startGame() const {
Game game = this->gameMenu.startGame(DEFAULT_GAMEMODE);
game.start();
std::cout << "\n\n\n";
this->printGame(game);
bool quit = false;
bool paused = false;
std::string answer;
while (!quit) {
std::cout << "Actions: ";
std::getline(std::cin, answer);
std::stringstream answerStream(answer);
std::vector<std::string> actions;
for (std::string action; std::getline(answerStream, action, ' ');) {
actions.push_back(action);
}
std::set<Action> playerActions;
std::set<Action> lastFrameActions;
bool retrying = false;
for (std::string action : actions) {
if (action == "quit") {
quit = true;
}
else {
try {
Action playerAction = this->keybinds.at(action);
if (playerAction == RETRY) {
retrying = true;
}
else if (playerAction == PAUSE) {
paused = (!paused);
}
else if (playerAction == SOFT_DROP || playerAction == MOVE_LEFT || playerAction == MOVE_RIGHT) {
playerActions.insert(playerAction);
lastFrameActions.insert(playerAction);
}
else if (playerAction == HOLD || playerAction == HARD_DROP) {
lastFrameActions.insert(playerAction);
}
else {
playerActions.insert(playerAction);
}
}
catch (std::exception ignored) {}
}
}
if (!paused && !quit) {
if (retrying) {
game.reset();
game.start();
}
else {
for (int i = 0; i < (FRAMES_PER_INPUT - 1); i++) {
game.nextFrame(playerActions);
}
game.nextFrame(lastFrameActions);
}
}
if (!quit) {
std::cout << "\n\n\n";
if (paused) {
std::cout << "--<[PAUSED]>--" << std::endl;
}
this->printGame(game);
}
if (game.hasLost()) {
quit = true;
std::cout << "You lost!" << std::endl;
}
else if (game.hasWon()) {
quit = true;
std::cout << "You won!" << std::endl;
}
}
}
void TextApp::printGame(const Game& game) const {
int maxHeight = game.getBoard().getGridHeight();
if (game.getActivePiece() != nullptr) {
for (const Position& position : game.getActivePiece()->getPositions()) {
maxHeight = std::max(maxHeight, position.y + game.getActivePiecePosition().y);
}
}
bool lineCountPrinted = false;
bool holdBoxStartedPrinting = false;
bool holdBoxFinishedPrinting = false;
bool nextQueueStartedPrinting = false;
bool nextQueueFinishedPrinting = false;
int nextQueuePrintedPiece;
int printedPieceLineHeight;
for (int y = maxHeight; y >= 0; y--) {
for (int x = 0; x < game.getBoard().getWidth(); x++) {
/* BOARD PRINTING */
bool isActivePieceHere = (game.getActivePiece() != nullptr) && (game.getActivePiece()->getPositions().contains(Position{x, y} - game.getActivePiecePosition()));
bool isGhostPieceHere = (game.getActivePiece() != nullptr) && (game.getActivePiece()->getPositions().contains(Position{x, y} - game.getGhostPiecePosition()));
Block block = (isActivePieceHere || isGhostPieceHere) ? game.getActivePiece()->getBlockType() : game.getBoard().getBlock(Position{x, y});
if (isActivePieceHere || isGhostPieceHere) {
std::cout << getConsoleColorCode(block);
}
else {
std::cout << getResetConsoleColorCode();
}
if (block != NOTHING && (!(isGhostPieceHere && !isActivePieceHere))) {
std::cout << "*";
}
else {
if (y < game.getBoard().getBaseHeight()) {
if (isGhostPieceHere) {
std::cout << "=";
}
else {
std::cout << "-";
}
}
else {
std::cout << " ";
}
}
}
if (y < game.getBoard().getGridHeight()) {
/* SIDEBAR PRINTING */
std::cout << " ";
if (!lineCountPrinted) {
std::cout << getResetConsoleColorCode() << "Lines: " << game.getClearedLines();
lineCountPrinted = true;
}
else if (!holdBoxFinishedPrinting) {
if (!holdBoxStartedPrinting) {
std::cout << getResetConsoleColorCode() << "Hold:";
printedPieceLineHeight = (game.getHeldPiece() == nullptr) ? -1 : (game.getHeldPiece()->getLength() - 1);
holdBoxStartedPrinting = true;
}
else {
for (int i = 0; i < game.getHeldPiece()->getLength(); i++) {
if (game.getHeldPiece()->getPositions().contains(Position{i, printedPieceLineHeight})) {
std::cout << getConsoleColorCode(game.getHeldPiece()->getBlockType()) << "*";
}
else {
std::cout << getResetConsoleColorCode() << "-";
}
}
printedPieceLineHeight--;
}
if (printedPieceLineHeight < 0) {
holdBoxFinishedPrinting = true;
}
}
else if (!nextQueueFinishedPrinting) {
if (!nextQueueStartedPrinting) {
std::cout << getResetConsoleColorCode() << "Next:";
printedPieceLineHeight = (game.getNextPieces().size() == 0) ? -1 : (game.getNextPieces().at(0).getLength() - 1);
nextQueuePrintedPiece = 0;
nextQueueStartedPrinting = true;
}
else {
for (int i = 0; i < game.getNextPieces().at(nextQueuePrintedPiece).getLength(); i++) {
if (game.getNextPieces().at(nextQueuePrintedPiece).getPositions().contains(Position{i, printedPieceLineHeight})) {
std::cout << getConsoleColorCode(game.getNextPieces().at(nextQueuePrintedPiece).getBlockType()) << "*";
}
else {
std::cout << getResetConsoleColorCode() << "-";
}
}
printedPieceLineHeight--;
}
if (printedPieceLineHeight < 0) {
nextQueuePrintedPiece++;
if (nextQueuePrintedPiece >= game.getNextPieces().size()) {
nextQueueFinishedPrinting = true;
}
else {
printedPieceLineHeight = game.getNextPieces().at(nextQueuePrintedPiece).getLength() - 1;
}
}
}
}
std::cout << "\n";
}
std::cout << getResetConsoleColorCode();
}

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

@@ -0,0 +1,58 @@
#pragma once
#include "../Core/Menu.h"
#include <map>
#include <string>
/**
* Textual interface for the app
*/
class TextApp {
private:
Menu gameMenu; // the interface with the core of the app
std::map<std::string, Action> keybinds; // what the player needs to type to perform in-game actions
public:
/**
* Initializes the app with default settings
*/
TextApp();
/**
* Runs the app
*/
void run();
private:
/**
* Sub-menu to select which pieces to play with
*/
void choosePieces();
/**
* Sub-menu to change the size of the board
*/
void chooseBoardSize();
/**
* Sub-menu to see the in-game controls
*/
void seeKeybinds() const;
/**
* Sets the controls to their default values
*/
void defaultKeybinds();
/**
* Starts a new game with the current settings
*/
void startGame() const;
/**
* Prints the current state of a game to the console
*/
void printGame(const Game& game) const;
};

View File

@@ -1,12 +1,8 @@
#include "../Pieces/PiecesFiles.h"
#include "../Pieces/Generator.h"
#include "GameBoard.h"
#include "../Pieces/PiecesFiles.h"
#include "TextApp.h"
#include <chrono>
#include <string>
#include <algorithm>
#include <filesystem>
#include <fstream>
void testGeneratorForAllSizes(int amount);
@@ -14,12 +10,19 @@ void testGeneratorForOneSize(int size);
void testGeneratorByprintingAllNminos(int n);
void testStoringAndRetrievingPieces(int size);
void generateFilesForAllSizes(int amount);
void generateFilesForOneSize(int size);
void loadFromFilesForOneSize(int size);
void readStatsFromFilesForAllSizes(int amount);
int main(int argc, char** argv) {
std::srand(std::time(NULL));
// dev: generate files if it hasn't been done before, UI will NOT generate the files
//generateFilesForAllSizes(10);
TextApp UI;
UI.run();
return 0;
}
@@ -34,11 +37,11 @@ void testGeneratorForAllSizes(int amount) {
for (int i = 1; i <= amount; i++) {
auto t1 = high_resolution_clock::now();
std::vector<Polyomino> n_minos = generator.generatePolyominos(i);
std::vector<Polyomino> n_minos = generator.generatePolyominoes(i);
auto t2 = high_resolution_clock::now();
duration<double, std::milli> ms_double = t2 - t1;
std::cout << "generated " << n_minos.size() << " polyominos of size " << i << " in " << ms_double.count() << "ms" << std::endl;
std::cout << "generated " << n_minos.size() << " polyominoes of size " << i << " in " << ms_double.count() << "ms" << std::endl;
}
}
@@ -52,7 +55,7 @@ void testGeneratorForOneSize(int size) {
std::cout << "Generating " << size << "-minos" << std::endl;
for (int i = 0; i < 10; i++) {
auto t1 = high_resolution_clock::now();
std::vector<Polyomino> n_minos = generator.generatePolyominos(size);
std::vector<Polyomino> n_minos = generator.generatePolyominoes(size);
auto t2 = high_resolution_clock::now();
duration<double, std::milli> ms_double = t2 - t1;
@@ -62,7 +65,7 @@ void testGeneratorForOneSize(int size) {
void testGeneratorByprintingAllNminos(int n) {
Generator generator;
std::vector<Polyomino> n_minos = generator.generatePolyominos(n);
std::vector<Polyomino> n_minos = generator.generatePolyominoes(n);
for (Polyomino& n_mino : n_minos) {
n_mino.goToSpawnPosition();
@@ -106,8 +109,8 @@ void generateFilesForAllSizes(int amount) {
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);
@@ -117,12 +120,12 @@ void generateFilesForAllSizes(int amount) {
std::cout << "Generated pieces files for size " << i << " in " << ms_double.count() << "ms" << std::endl;
}
std::vector<Piece> pieces;
std::vector<int> convexPieces;
std::vector<int> holelessPieces;
std::vector<int> otherPieces;
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();
@@ -131,6 +134,46 @@ void generateFilesForAllSizes(int amount) {
}
}
void generateFilesForOneSize(int size) {
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::duration;
using std::chrono::milliseconds;
PiecesFiles piecesFiles;
std::cout << "Generating " << size << "-minos files" << std::endl;
for (int i = 0; i < 10; i++) {
auto t1 = high_resolution_clock::now();
piecesFiles.savePieces(size);
auto t2 = high_resolution_clock::now();
duration<double, std::milli> ms_double = t2 - t1;
std::cout << ms_double.count() << "ms" << std::endl;
}
}
void loadFromFilesForOneSize(int size) {
using std::chrono::high_resolution_clock;
using std::chrono::duration_cast;
using std::chrono::duration;
using std::chrono::milliseconds;
PiecesFiles piecesFiles;
std::vector<Piece> pieces;
std::vector<int> convexPieces;
std::vector<int> holelessPieces;
std::vector<int> otherPieces;
std::cout << "Loading " << size << "-minos from files" << std::endl;
for (int i = 0; i < 10; i++) {
auto t1 = high_resolution_clock::now();
piecesFiles.loadPieces(size, pieces, convexPieces, holelessPieces, otherPieces);
auto t2 = high_resolution_clock::now();
duration<double, std::milli> ms_double = t2 - t1;
std::cout << ms_double.count() << "ms" << std::endl;
}
}
void readStatsFromFilesForAllSizes(int amount) {
PiecesFiles piecesFiles;
for (int i = 1; i <= amount; i++) {

View File

@@ -1,12 +1,27 @@
add_rules("mode.debug", "mode.release")
add_requires("sfml 3.0.0")
set_languages("c++20")
target("main")
set_rundir(".")
target("core")
set_kind("$(kind)")
add_files("src/Pieces/*.cpp")
add_files("src/Core/*.cpp")
target("text")
set_kind("binary")
set_rundir(".")
add_files("./src/Pieces/*.cpp")
add_files("./src/Core/*.cpp")
set_optimize("fastest")
set_default(false)
add_files("./src/TextUI/*.cpp")
add_deps("core")
target("graph")
set_kind("binary")
add_files("./src/GraphicalUI/**.cpp")
add_deps("core")
add_packages("sfml")
--
-- If you want to known more usage about xmake, please see https://xmake.io