Compare commits
71 Commits
f525c00662
...
assets
| Author | SHA1 | Date | |
|---|---|---|---|
| d50714ef8c | |||
| 3d1feb6295 | |||
| 7a96136631 | |||
| fc6348cebc | |||
| 0bb25d4628 | |||
| 69c07abcec | |||
| 774aa1422f | |||
| 4dfeda0957 | |||
| 05bb79d4a9 | |||
| e0ab6a4828 | |||
| a62b3c018d | |||
| 46ebb88ef2 | |||
| de14978b01 | |||
| d5ac79559e | |||
| 5ea47ddd25 | |||
| 1a95765877 | |||
| 6bff555cbc | |||
| f4b58fb67e | |||
| 0271b56542 | |||
| 5d73e751d7 | |||
| 314b7a8488 | |||
| 7151be0b1a | |||
| d124205a71 | |||
| 87920548e5 | |||
| 57620c70a2 | |||
| 3dac18c821 | |||
| 3538403f40 | |||
| ec40495328 | |||
| e0de2b5f90 | |||
| df7c9bd211 | |||
| f883bd5bab | |||
| 0f026635f6 | |||
| be071bd606 | |||
| 3320545465 | |||
| d1646d0fb5 | |||
| 3d74ef7cd5 | |||
| 88cb44c5fe | |||
| 009ed8edc3 | |||
| b2567844fc | |||
| de8a5e6e34 | |||
| c168cd68d7 | |||
| 321271b748 | |||
| c601424481 | |||
| fd9fd4586a | |||
| 8a4c4201fe | |||
| c08cfc2255 | |||
| 38008e00bc | |||
| 507bc9cc86 | |||
| e721a71894 | |||
| 1781b85332 | |||
| 92b58c4b98 | |||
| 8635d4b853 | |||
| 9780a36af4 | |||
| 6b16abda6a | |||
| 30dd323e22 | |||
| d87ddcdc22 | |||
| 8aaced68d0 | |||
| be6c8d9f77 | |||
| d9ccecfdd8 | |||
| 02bab6ed87 | |||
| 0e17996c35 | |||
| ea8478275b | |||
| 29177bc94d | |||
| c25abec6ba | |||
| 021620acef | |||
| 74797e935a | |||
| 2fbe4a6052 | |||
| d029589c21 | |||
| 47d3d929db | |||
| 1033f3a64c | |||
| 857f90d646 |
36
.github/workflows/ubuntu.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Linux arm64
|
||||
run-name: Build And Test
|
||||
|
||||
on: [push]
|
||||
|
||||
|
||||
env:
|
||||
XMAKE_ROOT: y
|
||||
|
||||
jobs:
|
||||
Build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare XMake
|
||||
uses: xmake-io/github-action-setup-xmake@v1
|
||||
with:
|
||||
xmake-version: latest
|
||||
actions-cache-folder: '.xmake-cache'
|
||||
actions-cache-key: 'ubuntu'
|
||||
|
||||
- name: Install SFML
|
||||
run: |
|
||||
apt update
|
||||
apt install -y libsfml-dev
|
||||
|
||||
- name: XMake config
|
||||
run: xmake f -p linux -y
|
||||
|
||||
- name: Build
|
||||
run: xmake
|
||||
|
||||
- name: Test
|
||||
run: xmake test
|
||||
7
.gitignore
vendored
@@ -10,7 +10,10 @@ build/
|
||||
|
||||
# personnal documentation
|
||||
doc/*.txt
|
||||
doc/*.violet.html
|
||||
doc/diagrams/*.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
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "jminos",
|
||||
"cppStandard": "c++20",
|
||||
"compileCommands": ".vscode/compile_commands.json"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
123
README.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# jminos
|
||||
|
||||
Modern stacker game with every polyominoes from size 1 to 15, made in C++ with [SFML 3](https://www.sfml-dev.org/)!
|
||||
|
||||
## Download
|
||||
|
||||
// TODO when the game is finished //
|
||||
|
||||
This game has been tested on and built for Windows 11 and WSL2 Ubuntu.
|
||||
If your OS isn't compactible with either of theses two, you can try [manually building the project](#manual-build).
|
||||
|
||||
## How to play
|
||||
|
||||
Choose which pieces you want to play with and stack them up until you either win or top out!
|
||||
Make full lines to make them dissapear.
|
||||
Use the different spins to kick the pieces in spot you couldn't imagine were attaignable!
|
||||
Each gamemode has its own objective, but you can play as you wish.
|
||||
|
||||
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 unchangeable keybinds.
|
||||
You will find more infos about the Rotation System, the scoring system, or the different pieces type in the **INFO** section of the main menu.
|
||||
|
||||
## Features
|
||||
|
||||
- Every polyominoes up to pentedecaminoes!
|
||||
- 7bag with proportionnality for each polyomino size!
|
||||
- Customizable board size!
|
||||
- Customizable keybinds!
|
||||
- 0° rotations!
|
||||
- AutoRS as the Rotation System!
|
||||
- IRS, IHS, infinite hold, and other leniency mechanics!
|
||||
- Very bland interface!! (i'm not a designer)
|
||||
|
||||
### 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!
|
||||
|
||||
### Screenshots
|
||||
|
||||
Pentedecamino jumpscare
|
||||

|
||||
|
||||
Pieces select screen
|
||||

|
||||
|
||||
AutoRS demonstration
|
||||

|
||||
|
||||
0° spins demonstration
|
||||

|
||||
|
||||
## 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``
|
||||
|
||||
The program will generate the polyomino files for you if you don't have them.
|
||||
As this is a lengthy process, debug mode limits pieces size to 10.
|
||||
To switch between debug and release mode:
|
||||
``xmake f -m debug``
|
||||
``xmake f -m release``
|
||||
|
||||
If for some reasons you wanna run the command line version (not updated):
|
||||
``xmake build text``
|
||||
``xmake run text``
|
||||
|
||||
### Package the project
|
||||
|
||||
// TODO when the game is finished //
|
||||
|
||||
## Benchmarks
|
||||
|
||||
### One-sided n-polyominoes
|
||||
|
||||
| n | Number | Generation | File storing | File retrieving | File size |
|
||||
| - | - | - | - | - | - |
|
||||
| 1 | 1 | 0s 0.005622ms | 0s 0.750955ms | 0s 0.014016ms | 2 bytes |
|
||||
| 2 | 1 | 0s 0.005758ms | 0s 0.181323ms | 0s 0.012256ms | 3 bytes |
|
||||
| 3 | 2 | 0s 0.017525ms | 0s 0.054497ms | 0s 0.006431ms | 8 bytes |
|
||||
| 4 | 7 | 0s 0.050554ms | 0s 0.067617ms | 0s 0.010984ms | 35 bytes |
|
||||
| 5 | 18 | 0s 0.191971ms | 0s 0.109905ms | 0s 0.021234ms | 108 bytes |
|
||||
| 6 | 60 | 0s 0.651757ms | 0s 0.327465ms | 0s 0.04558ms | 420 bytes |
|
||||
| 7 | 196 | 0s 3.38847ms | 0s 1.94434ms | 0s 0.258777ms | 1568 bytes |
|
||||
| 8 | 704 | 0s 22.7411ms | 0s 10.0103ms | 0s 1.34813ms | 6336 bytes |
|
||||
| 9 | 2500 | 0s 66.2949ms | 0s 20.6137ms | 0s 2.56374ms | 25000 bytes |
|
||||
| 10 | 9189 | 0s 194.764ms | 0s 84.5884ms | 0s 9.64467ms | 101079 bytes |
|
||||
| 11 | 33896 | 0s 759.182ms | 0s 378.494ms | 0s 44.1424ms | 406752 bytes |
|
||||
| 12 | 126759 | 2s 709.277ms | 1s 530.34ms | 0s 155ms | 1647867 bytes |
|
||||
| 13 | 476270 | 10s 668.308ms | 7s 395.512ms | 0s 765.601ms | 6667780 bytes |
|
||||
| 14 | 1802312 | 45s 606.597ms | 32s 28.7977ms | 2s 919.653ms | 27034680 bytes |
|
||||
| 15 | ~6M | ~5mn | ~5mn | ~10s | ~100 MB |
|
||||
|
||||
_File storing includes normalizing and sorting all polyominoes before writing them to the file._
|
||||
If you want to know more details about the generation and storage of polyominoes, [check the documentation](/doc/)!
|
||||
|
||||
Run it yourself by typing:
|
||||
``xmake build benchmark``
|
||||
``xmake run benchmark``
|
||||
|
||||
## 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
0
data/config/keybinds/.gitkeep
Normal file
17
data/fonts/pressstart/license.txt
Normal 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/ ]
|
||||
BIN
data/fonts/pressstart/prstart.ttf
Normal file
BIN
data/fonts/pressstart/prstartk.ttf
Normal file
BIN
data/images/keybinds/Harddrop.png
Normal file
|
After Width: | Height: | Size: 619 B |
BIN
data/images/keybinds/Hold.png
Normal file
|
After Width: | Height: | Size: 618 B |
BIN
data/images/keybinds/Moveleft.png
Normal file
|
After Width: | Height: | Size: 604 B |
BIN
data/images/keybinds/Moveright.png
Normal file
|
After Width: | Height: | Size: 603 B |
BIN
data/images/keybinds/Pause.png
Normal file
|
After Width: | Height: | Size: 591 B |
BIN
data/images/keybinds/Retry.png
Normal file
|
After Width: | Height: | Size: 600 B |
BIN
data/images/keybinds/Rotate0.png
Normal file
|
After Width: | Height: | Size: 624 B |
BIN
data/images/keybinds/Rotate180.png
Normal file
|
After Width: | Height: | Size: 627 B |
BIN
data/images/keybinds/RotateCCW.png
Normal file
|
After Width: | Height: | Size: 622 B |
BIN
data/images/keybinds/RotateCW.png
Normal file
|
After Width: | Height: | Size: 623 B |
BIN
data/images/keybinds/Softdrop.png
Normal file
|
After Width: | Height: | Size: 606 B |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 46 KiB |
BIN
doc/diagrams/class_diagramm_core.png
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
doc/diagrams/class_diagramm_pieces.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
59
doc/files_format.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 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 versions of the file format, stored with 1 byte
|
||||
- The previous maximum pieces size selected, stored with 1 byte
|
||||
- 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 20) (15x1 byte total)
|
||||
- Every selected pieces
|
||||
- For every groupe of piece (ALL, CONVEX, HOLELESS, OTHER), use 1 byte for the type (once again converted from an Enum) and 1 byte for the size
|
||||
- For every single piece, use 1 byte for (the size + encoding that it is a single piece),
|
||||
and 3 bytes to store the number of the piece (allows number up to 16M, size 15 has 6M pieces).
|
||||
|
||||
The current file format version is 11.
|
||||
If the file starts with a number lower than that, it will be regenerated upon launching the game.
|
||||
@@ -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,9 +25,36 @@ 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 it automatically locks. This period is called _lock delay_. Lock delay is reset everytime the piece move. To not allow for infinite stalling, there is another longer period called _forced lock delay_ that does not reset when moving the piece.
|
||||
The player has a hold box in which they can temporarly store a piece. In this game, we allow the player to swap between the held piece and the active piece as much as they want. Once again to not allow for infinite stalling, forced lock delay does not reset when holding a piece.
|
||||
@@ -38,17 +66,21 @@ IRS and IHS will fail if they actually loose the player the game when it would h
|
||||
|
||||
A common mechanic of stacker games is kicking. This happen when rotating a piece makes it collide with a wall. The game will try to move the piece in a few predetermined spot close to the current position until one of them allow the piece to not be in a wall again. If no positions allow for that, the rotation is simply cancelled.
|
||||
|
||||
This concept works very well for games with up to tetrominos or pentominos, but when using polyominos of any size we can't choose a few predetermined spots for each piece.
|
||||
This concept works very well for games with up to tetrominos or pentominos, but when using polyominoes of any size we can't choose a few predetermined spots for each piece.
|
||||
|
||||
Since this game uses polyomino of high sizes which are very unplayable, we will try to be complaisant to the player and allow as much kicking spots as possible, while trying not to make him feel like the piece is going through walls. To solve this problem, this game introduce a new Rotation System called **AutoRS**, which does not have a predetermined list of spots to fit the piece but instead adapt to its shape. Its algorithm goes as follow:
|
||||
|
||||
1. Before rotating, mark every position containing the piece or touching the piece, we will call the set of all theses positions the ``safePositions``
|
||||
2. Rotate the piece, if it fit stop the algorithm
|
||||
3. Try fitting the piece, going from the center to the sides, that means we try to move the piece 1 position right, then 1 position left, then 2 position right, etc. until it fit (and then stop the algorithm), if at one point a position doesn't touch one of the ``safePositions`` we stop trying in this direction
|
||||
3. Try fitting the piece, going from the center to the sides, that means we try to move the piece 1 position right, then 1 position left, then 2 position right, etc. until it fit (and then stop the algorithm), if at one point none of the squares of the pieces are in one of the ``safePositions`` we stop trying in this direction
|
||||
4. Move the piece one line down, and repeat step 3 again, until we hit a line were the first position (shifted by 0 positions horizontally) touched none of the ``safePositions``
|
||||
5. Do the same as step 4 but now we move the piece one line up every time
|
||||
6. Cancel the rotation
|
||||
|
||||
_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 or is locked in certain corners, but the rules varies a lot from game to game.
|
||||
@@ -60,8 +92,31 @@ Since we work with a great deal of different size and shapes, the rules for spin
|
||||
|
||||
## Score calculation
|
||||
|
||||
- For every position soft dropped, add 1 to the score
|
||||
- For every position 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.
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
# Pieces representation
|
||||
|
||||
## What are polyominos ?
|
||||
## What are polyominoes ?
|
||||
|
||||
In this game, pieces are represented as a polyomino and a block type.
|
||||
Polyominos are mathematical objects consisting of multiple edge-touching squares.
|
||||
Polyominoes are mathematical objects consisting of multiple edge-touching squares.
|
||||
There must be a path from every square to every other square, going from square to square only through the sides and not the corners.
|
||||
Polyominos can be classified in 3 ways:
|
||||
Polyominoes can be classified in 3 ways:
|
||||
|
||||
- Fixed polyominos : only translation is allowed
|
||||
- One-sided polyominos : only translation and rotation are allowed
|
||||
- Fixed polyominoes : only translation is allowed
|
||||
- One-sided polyominoes : only translation and rotation are allowed
|
||||
- Free polyomins : translation, rotation, and reflection are allowed
|
||||
|
||||
For more detailed informations about polyominos, check the [Wikipedia page](https://en.wikipedia.org/wiki/Polyomino).
|
||||
For more detailed informations about polyominoes, check the [Wikipedia page](https://en.wikipedia.org/wiki/Polyomino).
|
||||
|
||||
Most stacker game uses all one-sided polyominos of size 4 (called tetrominos), which results in 7 distinct polyominos.
|
||||
In this game too, one-sided polyominos will be used since we will only allow to move and rotate the pieces.
|
||||
Most stacker game uses all one-sided polyominoes of size 4 (called tetrominos), which results in 7 distinct polyominoes.
|
||||
In this game too, one-sided polyominoes will be used since we will only allow to move and rotate the pieces.
|
||||
|
||||
Internally, polyominos are represented as a set of positions on a 2D grid.
|
||||
This means that 2 polyominos of same shape but at different places will be interpreted as different polyominos.
|
||||
To solve this, when doing equality checks, we normalize the polyominos so that their left-most column is at x=0 and their bottom-most row at y=0.
|
||||
Internally, polyominoes are represented as a set of positions on a 2D grid.
|
||||
This means that 2 polyominoes of same shape but at different places will be interpreted as different polyominoes.
|
||||
To solve this, when doing equality checks, we normalize the polyominoes so that their left-most column is at x=0 and their bottom-most row at y=0.
|
||||
|
||||
Even if there is only 7 one-sided 4-minos, there is already more than 9,000 one-sided 10-minos.
|
||||
Since the aim of this game is to be playable with polyominos of any size, listing all polyominos manually isn't viable.
|
||||
Since the aim of this game is to be playable with polyominoes of any size, listing all polyominoes manually isn't viable.
|
||||
We will need a way to automatically:
|
||||
|
||||
1. Generate all one-sided polyominos of size n
|
||||
1. Generate all one-sided polyominoes of size n
|
||||
2. Set their spawn positions and rotation centers
|
||||
|
||||
Aditionally, for this game we will also a want a way to separate the polyominos into multiple categories, specifically to allow removing those with holes who are harder to play with.
|
||||
Aditionally, for this game we will also a want a way to separate the polyominoes into multiple categories, specifically to allow removing those with holes who are harder to play with.
|
||||
|
||||
## Ordering the polyominos
|
||||
## Ordering the polyominoes
|
||||
|
||||
For practical reasons, we want to be able to sort all polyominos of the same size.
|
||||
For practical reasons, we want to be able to sort all polyominoes of the same size.
|
||||
But to sort objects we need a way to compare them.
|
||||
|
||||
When a polyomino is created, an attribute named its length is computed. This is simply the max between its width and its height. Thus the polyomino can be inscribed in a box of size ``length``.
|
||||
We will now assume that our polyominos are always inscribed in a box of origin (0,0) and size ``length`` (which should always be the case under normal circumstances).
|
||||
We will now assume that our polyominoes are always inscribed in a box of origin (0,0) and size ``length`` (which should always be the case under normal circumstances).
|
||||
|
||||
We can now compare polyominos using this method:
|
||||
We can now compare polyominoes using this method:
|
||||
|
||||
- If one polyomino has an inferior length than another, it is deemed inferior
|
||||
- If two polyomino have the same length, we check all the positions of their box, from left to right, and repeating from up to bottom, for the first position where a polyomino has a square and the another doesn't, the present polyomino which has a square is deemed inferior
|
||||
|
||||
A nice side-effect is that, once the polyomino are ordered, it is very simple to attribute them a block type, we can simply iterate through the list while looping over the block list.
|
||||
|
||||
## 1. Generating polyominos
|
||||
## 1. Generating polyominoes
|
||||
|
||||
The method used to generate polyominos is similar to the [inductive method](https://en.wikipedia.org/wiki/Polyomino#Inductive_algorithms) described in the previously mentionned Wikipedia page.
|
||||
The method used to generate polyominoes is similar to the [inductive method](https://en.wikipedia.org/wiki/Polyomino#Inductive_algorithms) described in the previously mentionned Wikipedia page.
|
||||
|
||||
The algorithm is the following:
|
||||
|
||||
@@ -64,15 +64,15 @@ The algorithm is the following:
|
||||
1. We add this square to the polyomino
|
||||
2. We call the generator function (recursive function!)
|
||||
3. We remove this square from the polyomino
|
||||
3. Return the list of polyominos
|
||||
3. Return the list of polyominoes
|
||||
|
||||
The exact number of one-sided polyominos up to size 15 (and higher, but we only generated up to size 15) is known, and this method generated exactly theses numbers, without duplicates.
|
||||
The exact number of one-sided polyominoes up to size 15 (and higher, but we only generated up to size 15) is known, and this method generated exactly theses numbers, without duplicates.
|
||||
|
||||
By marking squares and adding only ones that have a higher number than the last one everytime, we generate each fixed polyomino exactly n times. By ignoring the squares below y=0, or at exactly x=0 and y<0, we generate each fixed polyomino exactly 1 time.
|
||||
An one-sided polyomino has 4 rotations, some of which can be the same if the polyomino has symmetries. 2 rotations that are the same corresponds to 1 fixed polyomino, we will refer to theses 2 rotation as 1 "unique" rotation.
|
||||
Because we generate every fixed polyomino exactly 1 time, that means each "unique" rotation of an one-sided polyomino is generated exactly one-time, which includes its lowest rotation. That's how we can get away with only checking the rotation of the polyomino and not comparing it to the rest of the generated polyominos.
|
||||
Because we generate every fixed polyomino exactly 1 time, that means each "unique" rotation of an one-sided polyomino is generated exactly one-time, which includes its lowest rotation. That's how we can get away with only checking the rotation of the polyomino and not comparing it to the rest of the generated polyominoes.
|
||||
|
||||
## 2. Setting the spawn position of polyominos
|
||||
## 2. Setting the spawn position of polyominoes
|
||||
|
||||
Since we assume the polyomino is always inscribed in a box at origin (0,0), we can use formulae close to matrix rotations to rotate the piece:
|
||||
|
||||
@@ -91,19 +91,19 @@ The current algorithm for doing so is the following:
|
||||
1. Check if the polyomino has more lines horizontally or vertically, this will determine either 2 or 4 sides which are wider than the others, and the others will be discarded
|
||||
2. For each potential side check the number of square on the first line, if one has more than every others, then this side is choosed, else we only keep the sides that tied and repeat for the next line
|
||||
3. If we still have at least 2 sides tied, we do the same again but check the flatness of the side to the left instead, this will make the pieces overall have more squares to their left at spawn
|
||||
4. If there is still no winner, we sort the remaining sides (by simulating having them selectionned and then sorting the resulting polyominos) and keep the lowest
|
||||
4. If there is still no winner, we sort the remaining sides (by simulating having them selectionned and then sorting the resulting polyominoes) and keep the lowest
|
||||
5. We rotate the piece so that the chosen side ends up at the bottom of the polyomino
|
||||
6. We center the polyomino inside its box, since we chose the widest side it will always fill the width of the square, but for the height it can be asymmetric, in that case we place it one line closer to the top than the bottom
|
||||
|
||||
_Note we could actually just skip straight to step 4 because it always give the same orientation, but we want the spawn rotations to follow somewhat of a logic. Step 1 to 3 actually already works for 99% of polyominos and they constrain step 4 too by preselecting certain sides._
|
||||
_Note we could actually just skip straight to step 4 because it always give the same orientation, but we want the spawn rotations to follow somewhat of a logic. Step 1 to 3 actually already works for 99% of polyominoes and they constrain step 4 too by preselecting certain sides._
|
||||
|
||||
## 3. Separating the polyominos into categories
|
||||
## 3. Separating the polyominoes into categories
|
||||
|
||||
For this game, we want the polyominos to be broken down into 3 categories:
|
||||
For this game, we want the polyominoes to be broken down into 3 categories:
|
||||
|
||||
- Convex: this is said of a polyomino where every row and column is formed of at most one continous line of squares
|
||||
- Holeless: the polyominos which are neither convex nor have a hole
|
||||
- Others: the polyominos who have a hole (thoses are by definition not convex)
|
||||
- Holeless: the polyominoes which are neither convex nor have a hole
|
||||
- Others: the polyominoes who have a hole (thoses are by definition not convex)
|
||||
|
||||
To check for convexity, we simply iterate trough each row and column, and check if all the squares are contiguous.
|
||||
|
||||
|
||||
@@ -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 block type, and always set at the same spawn position, no matter how they were generated. We also want them to be separated in 3 categories : convex, not convex but without a hole, and with a hole. Theses problematics are already resolved internally, but will be calculated before storage as to not need extra calculcations upon load (except for the block type which is trivially computed).
|
||||
|
||||
## How is it stored
|
||||
|
||||
Pieces are stored in binary files. Each file simply contains every polyomino of one size, one after the other. Since each file contains all polyominos of the same size, we know how much stuff to read and don't need delimiters. We know we've read all pieces simply when we reach the end of file character.
|
||||
|
||||
Each piece is stored as follows:
|
||||
- 1 byte for the characteristics of the piece: ``ABCCCCCC`` where A indicates if the piece is convex, B indicates if it has a hole, and C is the length of the piece
|
||||
- 1 byte for each position: ``XXXXYYYY`` where X is the x coordinate of the position and Y is the y coordinate of the position
|
||||
|
||||
The current implementation only allows to generate polyominos up to size 16, but can be upgraded by storing coordinates on 8 bits instead of 4.
|
||||
It has been currently choosen to use pieces only up to size 15 for this game.
|
||||
BIN
doc/readme/big_piece.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
doc/readme/pieces_selection.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
doc/readme/rotation_0.gif
Normal file
|
After Width: | Height: | Size: 614 KiB |
BIN
doc/readme/rotations.gif
Normal file
|
After Width: | Height: | Size: 779 KiB |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -7,29 +7,55 @@
|
||||
#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(std::shared_ptr<PiecesList> piecesList) :
|
||||
|
||||
Bag::Bag(const std::shared_ptr<PiecesList>& piecesList) :
|
||||
piecesList(piecesList) {
|
||||
|
||||
// initialize bags
|
||||
this->currentBag = this->piecesList->getSelectedPieces();
|
||||
this->nextBag.clear();
|
||||
|
||||
// prepare first piece
|
||||
this->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() {
|
||||
// if the bag is empty switch to the next bag
|
||||
if (this->currentBag.empty()) {
|
||||
std::swap(this->currentBag, this->nextBag);
|
||||
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();
|
||||
}
|
||||
|
||||
// get the already used pieces back to the current bag
|
||||
for (const std::pair<int, int>& pieceIndex : this->nextBag) {
|
||||
this->currentBag.emplace_back(pieceIndex);
|
||||
}
|
||||
this->nextBag.clear();
|
||||
this->prepareNext();
|
||||
}
|
||||
|
||||
Piece Bag::lookNext() {
|
||||
@@ -37,27 +63,48 @@ Piece Bag::lookNext() {
|
||||
}
|
||||
|
||||
Piece Bag::getNext() {
|
||||
// get the piece to return
|
||||
std::pair<int, int> nextIndex = this->next;
|
||||
|
||||
// prepare the piece even after the next
|
||||
this->prepareNext();
|
||||
|
||||
// return the next piece
|
||||
return this->piecesList->getPiece(nextIndex);
|
||||
}
|
||||
|
||||
void Bag::prepareNext() {
|
||||
// if the bag is empty switch to the next bag
|
||||
if (this->currentBag.empty()) {
|
||||
std::swap(this->currentBag, this->nextBag);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
#include "../Pieces/Piece.h"
|
||||
#include "PiecesList.h"
|
||||
#include "DistributionMode.h"
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
using PieceBag = std::vector<std::pair<int, int>>;
|
||||
|
||||
|
||||
/**
|
||||
* A litteral bag of pieces, in which you take each of its piece randomly one by one then start again with a new bag
|
||||
@@ -14,19 +17,24 @@
|
||||
class Bag {
|
||||
private:
|
||||
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<std::pair<int, int>> currentBag; // the list of pieces that are still to be taken out before starting a new bag
|
||||
std::vector<std::pair<int, int>> nextBag; // the list of pieces that have been taken out of the current bag and have been placed in the next
|
||||
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 with the pieces currently selected in the piece list
|
||||
*/
|
||||
Bag(std::shared_ptr<PiecesList> piecesList);
|
||||
Bag(const std::shared_ptr<PiecesList>& piecesList);
|
||||
|
||||
/**
|
||||
* Ignores the remaining pieces in the current bag and startd fresh from a new bag
|
||||
* Ignores the remaining pieces in the current bag and start fresh from a new bag
|
||||
*/
|
||||
void jumpToNextBag();
|
||||
|
||||
@@ -44,7 +52,12 @@ class Bag {
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -20,7 +20,6 @@ Board::Board(int width, int height) :
|
||||
}
|
||||
|
||||
void Board::changeBlock(const Position& position, Block block) {
|
||||
// if the block is out of bounds we discard it
|
||||
if (position.x < 0 || position.x >= this->width || position.y < 0) return;
|
||||
|
||||
// resize the grid if needed
|
||||
@@ -62,7 +61,7 @@ int Board::clearRows() {
|
||||
|
||||
if (lineIsFull) {
|
||||
this->grid.erase(this->grid.begin() + j);
|
||||
if(this->grid.size() < height) {
|
||||
if(this->grid.size() < this->height) {
|
||||
this->grid.push_back(this->emptyRow);
|
||||
}
|
||||
clearedLines++;
|
||||
@@ -74,7 +73,7 @@ int Board::clearRows() {
|
||||
|
||||
void Board::clearBoard() {
|
||||
this->grid.clear();
|
||||
for (int j = 0; j < height; j++) {
|
||||
for (int j = 0; j < this->height; j++) {
|
||||
this->grid.push_back(this->emptyRow);
|
||||
}
|
||||
}
|
||||
@@ -87,7 +86,7 @@ Block Board::getBlock(const Position& position) const {
|
||||
return this->grid.at(position.y).at(position.x);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Block>> Board::getBlocks() const {
|
||||
const std::vector<std::vector<Block>>& Board::getBlocks() const {
|
||||
return this->grid;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,14 +44,14 @@ class Board {
|
||||
void clearBoard();
|
||||
|
||||
/**
|
||||
* @return A copy of the block at the specified position
|
||||
* @return The block at the specified position
|
||||
*/
|
||||
Block getBlock(const Position& position) const;
|
||||
|
||||
/**
|
||||
* @return A copy of the grid
|
||||
* @return The grid
|
||||
*/
|
||||
std::vector<std::vector<Block>> getBlocks() const;
|
||||
const std::vector<std::vector<Block>>& getBlocks() const;
|
||||
|
||||
/**
|
||||
* @return The width of the grid
|
||||
|
||||
27
src/Core/DistributionMode.h
Normal 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];
|
||||
}
|
||||
@@ -9,8 +9,8 @@
|
||||
#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 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)
|
||||
@@ -25,7 +25,7 @@ Game::Game(Gamemode gamemode, const Player& controls, int boardWidth, int boardH
|
||||
|
||||
void Game::start() {
|
||||
this->started = true;
|
||||
this->lost = this->board.spawnNextPiece();
|
||||
this->leftARETime = 1;
|
||||
}
|
||||
|
||||
void Game::reset() {
|
||||
@@ -48,7 +48,6 @@ void Game::initialize() {
|
||||
this->heldDAS = 0;
|
||||
this->heldARR = 0;
|
||||
this->subVerticalPosition = 0;
|
||||
this->leftARETime = 0;
|
||||
this->totalLockDelay = 0;
|
||||
this->totalForcedLockDelay = 0;
|
||||
}
|
||||
@@ -56,6 +55,8 @@ void Game::initialize() {
|
||||
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) {
|
||||
@@ -64,57 +65,75 @@ 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;
|
||||
/* IRS and IHS */
|
||||
bool initialRotated = (this->initialActions.contains(ROTATE_0) || this->initialActions.contains(ROTATE_CW)
|
||||
|| this->initialActions.contains(ROTATE_180) || this->initialActions.contains(ROTATE_CCW));
|
||||
Rotation initialRotation = NONE
|
||||
+ ((this->initialActions.contains(ROTATE_CW)) ? CLOCKWISE : NONE)
|
||||
+ ((this->initialActions.contains(ROTATE_180)) ? DOUBLE : NONE)
|
||||
+ ((this->initialActions.contains(ROTATE_CCW)) ? COUNTERCLOCKWISE : NONE);
|
||||
|
||||
if (this->initialActions.contains(HOLD)) {
|
||||
if (this->board.hold(initialRotation)) {
|
||||
this->subVerticalPosition = 0;
|
||||
this->totalLockDelay = 0;
|
||||
this->heldARR = 0;
|
||||
if (this->initialActions.contains(HOLD)) {
|
||||
this->board.hold(initialRotation);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (initialRotation != NONE) {
|
||||
this->board.rotate(initialRotation);
|
||||
else {
|
||||
if (initialRotated) {
|
||||
this->board.rotate(initialRotation);
|
||||
}
|
||||
}
|
||||
|
||||
this->lost = this->board.activePieceInWall();
|
||||
if (this->lost) {
|
||||
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;
|
||||
if (this->board.hold({})) {
|
||||
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 */
|
||||
@@ -123,16 +142,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,23 +157,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++;
|
||||
@@ -168,59 +190,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();
|
||||
@@ -229,34 +277,49 @@ 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 B2BConditionsAreMet = ((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 && B2BConditionsAreMet) clearScore *= B2B_SCORE_MULTIPLIER;
|
||||
if (this->B2BChain && B2BConditionsAreMet) {
|
||||
clearScore *= B2B_SCORE_MULTIPLIER;
|
||||
}
|
||||
this->score += clearScore;
|
||||
}
|
||||
this->B2BChain = B2BConditionsAreMet;
|
||||
|
||||
// reset active piece
|
||||
this->subVerticalPosition = 0;
|
||||
this->totalLockDelay = 0;
|
||||
this->totalForcedLockDelay = 0;
|
||||
this->heldARR = 0;
|
||||
|
||||
// check for ARE
|
||||
this->leftARETime = this->parameters.getARE();
|
||||
if (this->leftARETime == 0) {
|
||||
this->board.spawnNextPiece();
|
||||
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() const {
|
||||
@@ -271,6 +334,10 @@ int Game::getClearedLines() const {
|
||||
return this->parameters.getClearedLines();
|
||||
}
|
||||
|
||||
int Game::getGrade() const {
|
||||
return this->parameters.getGrade();
|
||||
}
|
||||
|
||||
int Game::getLevel() const {
|
||||
return this->parameters.getLevel();
|
||||
}
|
||||
@@ -287,26 +354,38 @@ bool Game::isOnB2BChain() const {
|
||||
return this->B2BChain;
|
||||
}
|
||||
|
||||
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 {
|
||||
const Board& Game::getBoard() const {
|
||||
return this->board.getBoard();
|
||||
}
|
||||
|
||||
Piece Game::getActivePiece() const {
|
||||
const std::shared_ptr<Piece>& Game::getActivePiece() const {
|
||||
return this->board.getActivePiece();
|
||||
}
|
||||
|
||||
Position Game::getActivePiecePosition() const {
|
||||
const Position& Game::getActivePiecePosition() const {
|
||||
return this->board.getActivePiecePosition();
|
||||
}
|
||||
|
||||
Piece Game::getHeldPiece() const {
|
||||
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 {
|
||||
const std::vector<Piece>& Game::getNextPieces() const {
|
||||
return this->board.getNextPieces();
|
||||
}
|
||||
|
||||
@@ -61,9 +61,19 @@ class Game {
|
||||
|
||||
private:
|
||||
/**
|
||||
* Move the piece in the specified direction
|
||||
* Resets the piece's parameter
|
||||
*/
|
||||
void movePiece(int movement, bool resetDirection);
|
||||
void resetPiece(bool newPiece);
|
||||
|
||||
/**
|
||||
* Moves the piece in the specified direction (1 for right and -1 for left)
|
||||
*/
|
||||
void movePiece(int movement);
|
||||
|
||||
/**
|
||||
* Rotates the piece with the specified rotation
|
||||
*/
|
||||
void rotatePiece(Rotation rotation);
|
||||
|
||||
/**
|
||||
* Locks the piece, updates level and score and spawns the next piece if necessary
|
||||
@@ -91,6 +101,11 @@ class Game {
|
||||
*/
|
||||
int getClearedLines() const;
|
||||
|
||||
/**
|
||||
* @return The current grade
|
||||
*/
|
||||
int getGrade() const;
|
||||
|
||||
/**
|
||||
* @return The number of frames passed since the start of the game
|
||||
*/
|
||||
@@ -106,33 +121,48 @@ class Game {
|
||||
*/
|
||||
bool isOnB2BChain() const;
|
||||
|
||||
/**
|
||||
* @return How close the active piece's lock delay is to the maximum allowed, betwwen 0 and 1
|
||||
*/
|
||||
float getLockDelayProgression() const;
|
||||
|
||||
/**
|
||||
* @return How close the active piece's forced lock delay is to the maximum allowed, betwwen 0 and 1
|
||||
*/
|
||||
float getForcedLockDelayProgression() const;
|
||||
|
||||
/**
|
||||
* @return If all blocks are currently bone blocks
|
||||
*/
|
||||
bool areBlocksBones() const;
|
||||
|
||||
/**
|
||||
* @return A copy of the board
|
||||
* @return The board
|
||||
*/
|
||||
Board getBoard() const;
|
||||
const Board& getBoard() const;
|
||||
|
||||
/**
|
||||
* @return A copy of the active piece
|
||||
* @return A pointer to the active piece, can be null
|
||||
*/
|
||||
Piece getActivePiece() const;
|
||||
const std::shared_ptr<Piece>& getActivePiece() const;
|
||||
|
||||
/**
|
||||
* @return A copy of the active piece position
|
||||
* @return The position of the active piece
|
||||
*/
|
||||
Position getActivePiecePosition() const;
|
||||
const Position& getActivePiecePosition() const;
|
||||
|
||||
/**
|
||||
* @return A copy of the held piece
|
||||
* @return The position of the ghost piece
|
||||
*/
|
||||
Piece getHeldPiece() const;
|
||||
Position getGhostPiecePosition() const;
|
||||
|
||||
/**
|
||||
* @return A copy of the next pieces queue
|
||||
* @return A pointer to the held piece, can be null
|
||||
*/
|
||||
std::vector<Piece> getNextPieces() const;
|
||||
const std::shared_ptr<Piece>& getHeldPiece() const;
|
||||
|
||||
/**
|
||||
* @return The next piece queue, can be empty
|
||||
*/
|
||||
const std::vector<Piece>& getNextPieces() const;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
|
||||
|
||||
GameBoard::GameBoard(int boardWidth, int boardHeight, const std::shared_ptr<PiecesList>& piecesList, int nextQueueLength) :
|
||||
@@ -35,10 +38,13 @@ void GameBoard::initialize() {
|
||||
this->activePiece = nullptr;
|
||||
this->heldPiece = nullptr;
|
||||
this->isLastMoveKick = false;
|
||||
this->movedLeftLast = false;
|
||||
}
|
||||
|
||||
bool GameBoard::moveLeft() {
|
||||
if (this->isActivePieceInWall(Position{-1, 0})) {
|
||||
this->movedLeftLast = true;
|
||||
|
||||
if (this->activePieceInWall(Position{-1, 0})) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
@@ -49,7 +55,9 @@ bool GameBoard::moveLeft() {
|
||||
}
|
||||
|
||||
bool GameBoard::moveRight() {
|
||||
if (this->isActivePieceInWall(Position{1, 0})) {
|
||||
this->movedLeftLast = false;
|
||||
|
||||
if (this->activePieceInWall(Position{1, 0})) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
@@ -60,7 +68,7 @@ bool GameBoard::moveRight() {
|
||||
}
|
||||
|
||||
bool GameBoard::moveDown() {
|
||||
if (this->isActivePieceInWall(Position{0, -1})) {
|
||||
if (this->activePieceInWall(Position{0, -1})) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
@@ -72,15 +80,22 @@ bool GameBoard::moveDown() {
|
||||
|
||||
bool GameBoard::rotate(Rotation rotation) {
|
||||
Piece stored = *this->activePiece;
|
||||
this->rotate(rotation);
|
||||
this->activePiece->rotate(rotation);
|
||||
|
||||
// before trying to kick, check if the piece can rotate without kicking
|
||||
if (!this->isActivePieceInWall()) {
|
||||
this->isLastMoveKick = false;
|
||||
return true;
|
||||
if (rotation == NONE) {
|
||||
if (this->moveDown()) {
|
||||
this->isLastMoveKick = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!this->activePieceInWall()) {
|
||||
this->isLastMoveKick = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// get the list of positions that touches the original piece
|
||||
std::set<Position> safePositions;
|
||||
for (Position position : stored.getPositions()) {
|
||||
Position positionInGrid(position + this->activePiecePosition);
|
||||
@@ -91,7 +106,10 @@ bool GameBoard::rotate(Rotation rotation) {
|
||||
safePositions.insert(positionInGrid + Position{-1, 0});
|
||||
}
|
||||
|
||||
// try kicking the piece down
|
||||
// first try kicking the piece down
|
||||
if (rotation == NONE) {
|
||||
this->activePiecePosition.y -= 1;
|
||||
}
|
||||
bool suceeded = this->tryKicking(true, safePositions);
|
||||
if (suceeded) {
|
||||
this->isLastMoveKick = true;
|
||||
@@ -99,6 +117,9 @@ bool GameBoard::rotate(Rotation rotation) {
|
||||
}
|
||||
|
||||
// if it doesn't work try kicking the piece up
|
||||
if (rotation == NONE) {
|
||||
this->activePiecePosition.y += 1;
|
||||
}
|
||||
suceeded = this->tryKicking(false, safePositions);
|
||||
if (suceeded) {
|
||||
this->isLastMoveKick = true;
|
||||
@@ -107,7 +128,10 @@ bool GameBoard::rotate(Rotation rotation) {
|
||||
|
||||
// if it still doesn't work, abort the rotation
|
||||
this->activePiece = std::make_shared<Piece>(stored);
|
||||
return false;
|
||||
if (rotation == NONE) {
|
||||
this->isLastMoveKick = false;
|
||||
}
|
||||
return (rotation == NONE);
|
||||
}
|
||||
|
||||
bool GameBoard::tryKicking(bool testingBottom, const std::set<Position>& safePositions) {
|
||||
@@ -118,33 +142,23 @@ bool GameBoard::tryKicking(bool testingBottom, const std::set<Position>& safePos
|
||||
// we try from the center to the sides as long as the kicked piece touches the original
|
||||
bool overlapsLeft = true;
|
||||
bool overlapsRight = true;
|
||||
int i = 0;
|
||||
int i = (j == 0) ? 1 : 0;
|
||||
do {
|
||||
// check right before right arbitrarly, we don't decide this with rotations since it would still be arbitrary with 180° rotations
|
||||
if (overlapsRight) {
|
||||
Position shift{+i, j};
|
||||
if (!this->activePieceOverlapsOnePosition(safePositions, 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 (!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) {
|
||||
Position shift{-i, j};
|
||||
if (!this->activePieceOverlapsOnePosition(safePositions, 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,12 +175,32 @@ bool GameBoard::tryKicking(bool testingBottom, const std::set<Position>& safePos
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GameBoard::hold(Rotation initialRotation) {
|
||||
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(std::optional<Rotation> initialRotation) {
|
||||
Position storedPosition = this->activePiecePosition;
|
||||
std::swap(this->activePiece, this->heldPiece);
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -175,65 +209,84 @@ bool GameBoard::hold(Rotation initialRotation) {
|
||||
}
|
||||
}
|
||||
|
||||
// try initial rotation
|
||||
this->goToSpawnPosition();
|
||||
if (initialRotation.has_value()) {
|
||||
std::shared_ptr<Piece> storedPiece(this->activePiece);
|
||||
this->rotate(initialRotation.value());
|
||||
|
||||
Piece stored = *this->activePiece;
|
||||
this->rotate(initialRotation);
|
||||
if (this->activePieceInWall()) {
|
||||
this->activePiece = storedPiece;
|
||||
}
|
||||
}
|
||||
|
||||
// if the piece can't spawn, try 0° rotation
|
||||
if (this->activePieceInWall()) {
|
||||
std::shared_ptr<Piece> storedPiece(this->activePiece);
|
||||
this->rotate(NONE);
|
||||
|
||||
// if the piece can't spawn, abort initial rotation
|
||||
if (this->isActivePieceInWall()) {
|
||||
this->activePiece = std::make_shared<Piece>(stored);
|
||||
|
||||
// if the piece still can't spawn, abort holding
|
||||
if (this->isActivePieceInWall()) {
|
||||
if (isFirstTimeHolding) {
|
||||
this->activePiece = nullptr;
|
||||
}
|
||||
if (this->activePieceInWall()) {
|
||||
this->activePiece = (isFirstTimeHolding) ? nullptr : storedPiece;
|
||||
std::swap(this->activePiece, this->heldPiece);
|
||||
this->activePiecePosition = storedPosition;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isFirstTimeHolding) {
|
||||
// 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() {
|
||||
// generate a new piece
|
||||
this->nextQueue.push_back(this->generator.getNext());
|
||||
|
||||
// get next piece from queue
|
||||
this->activePiece = std::make_shared<Piece>(this->nextQueue.front());
|
||||
this->nextQueue.erase(this->nextQueue.begin());
|
||||
|
||||
this->goToSpawnPosition();
|
||||
|
||||
// this piece has done nothing yet
|
||||
this->isLastMoveKick = false;
|
||||
|
||||
return !this->isActivePieceInWall();
|
||||
return this->activePieceInWall();
|
||||
}
|
||||
|
||||
bool GameBoard::touchesGround() {
|
||||
return this->isActivePieceInWall(Position{0, -1});
|
||||
bool GameBoard::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() {
|
||||
bool isLockedInPlace = (this->isActivePieceInWall(Position{0, 1}) && this->isActivePieceInWall(Position{1, 0})
|
||||
&& this->isActivePieceInWall(Position{-1, 0}) && this->isActivePieceInWall(Position{0, -1}));
|
||||
bool isLockedInPlace = (this->activePieceInWall(Position{0, 1}) && this->activePieceInWall(Position{1, 0})
|
||||
&& this->activePieceInWall(Position{-1, 0}) && this->activePieceInWall(Position{0, -1}));
|
||||
|
||||
for (Position position : this->activePiece->getPositions()) {
|
||||
this->board.changeBlock(position + this->activePiecePosition, this->activePiece->getBlockType());
|
||||
}
|
||||
|
||||
this->activePiece = nullptr;
|
||||
|
||||
return LineClear{this->board.clearRows(), isLockedInPlace, (!isLockedInPlace) && this->isLastMoveKick};
|
||||
}
|
||||
|
||||
@@ -248,40 +301,26 @@ void GameBoard::addGarbageRows(int number) {
|
||||
}
|
||||
}
|
||||
|
||||
Board GameBoard::getBoard() const {
|
||||
const Board& GameBoard::getBoard() const {
|
||||
return this->board;
|
||||
}
|
||||
|
||||
Piece GameBoard::getActivePiece() const {
|
||||
return *this->activePiece;
|
||||
const std::shared_ptr<Piece>& GameBoard::getActivePiece() const {
|
||||
return this->activePiece;
|
||||
}
|
||||
|
||||
Position GameBoard::getActivePiecePosition() const {
|
||||
const Position& GameBoard::getActivePiecePosition() const {
|
||||
return this->activePiecePosition;
|
||||
}
|
||||
|
||||
Piece GameBoard::getHeldPiece() const {
|
||||
return *this->heldPiece;
|
||||
const std::shared_ptr<Piece>& GameBoard::getHeldPiece() const {
|
||||
return this->heldPiece;
|
||||
}
|
||||
|
||||
std::vector<Piece> GameBoard::getNextPieces() const {
|
||||
const std::vector<Piece>& GameBoard::getNextPieces() const {
|
||||
return this->nextQueue;
|
||||
}
|
||||
|
||||
bool GameBoard::isActivePieceInWall(const Position& shift) const {
|
||||
for (Position position : this->activePiece->getPositions()) {
|
||||
if (this->board.getBlock(position + this->activePiecePosition + shift) != NOTHING) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GameBoard::activePieceOverlapsOnePosition(const std::set<Position>& safePositions, const Position& shift) const {
|
||||
for (Position position : this->activePiece->getPositions()) {
|
||||
if (safePositions.contains(position + this->activePiecePosition + shift)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void GameBoard::goToSpawnPosition() {
|
||||
int lowestPosition = this->activePiece->getLength() - 1;
|
||||
for (Position position : this->activePiece->getPositions()) {
|
||||
@@ -293,6 +332,8 @@ void GameBoard::goToSpawnPosition() {
|
||||
|
||||
// center the piece horizontally, biased towards left
|
||||
this->activePiecePosition.x = (this->board.getWidth() - this->activePiece->getLength()) / 2;
|
||||
|
||||
this->activePiece->defaultRotation();
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
|
||||
/**
|
||||
@@ -22,6 +23,7 @@ class GameBoard {
|
||||
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:
|
||||
/**
|
||||
@@ -60,7 +62,7 @@ class GameBoard {
|
||||
bool moveDown();
|
||||
|
||||
/**
|
||||
* Tries rotating the piece and kicking it if necessary
|
||||
* Tries rotating the piece and kicking it if necessary, if it's a 0° rotation, it will forcefully try kicking
|
||||
* @return If it suceeded
|
||||
*/
|
||||
bool rotate(Rotation rotation);
|
||||
@@ -72,12 +74,24 @@ class GameBoard {
|
||||
*/
|
||||
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:
|
||||
/**
|
||||
* 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);
|
||||
bool hold(std::optional<Rotation> initialRotation);
|
||||
|
||||
/**
|
||||
* Spawns the next piece from the queue
|
||||
@@ -85,11 +99,23 @@ class GameBoard {
|
||||
*/
|
||||
bool spawnNextPiece();
|
||||
|
||||
/**
|
||||
* 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 activePieceInWall(const Position& shift = Position{0, 0}) const;
|
||||
|
||||
/**
|
||||
* Checks is the active piece as a wall directly below one of its position
|
||||
* @return If it touches a ground
|
||||
*/
|
||||
bool touchesGround();
|
||||
bool touchesGround() const;
|
||||
|
||||
/**
|
||||
* Computes what the piece position would be if it were to be dropped down as much as possible
|
||||
* @return The lowest position before hitting a wall
|
||||
*/
|
||||
Position lowestPosition() const;
|
||||
|
||||
/**
|
||||
* Locks the active piece into the board and clears lines if needed
|
||||
@@ -103,43 +129,31 @@ class GameBoard {
|
||||
void addGarbageRows(int number);
|
||||
|
||||
/**
|
||||
* @return A copy of the board
|
||||
* @return The board
|
||||
*/
|
||||
Board getBoard() const;
|
||||
const Board& getBoard() const;
|
||||
|
||||
/**
|
||||
* @return A copy of the active piece
|
||||
* @return A pointer to the active piece, can be null
|
||||
*/
|
||||
Piece getActivePiece() const;
|
||||
const std::shared_ptr<Piece>& getActivePiece() const;
|
||||
|
||||
/**
|
||||
* @return A copy of the position of the active piece
|
||||
* @return The position of the active piece
|
||||
*/
|
||||
Position getActivePiecePosition() const;
|
||||
const Position& getActivePiecePosition() const;
|
||||
|
||||
/**
|
||||
* @return A copy of the held piece
|
||||
* @return A pointer to the held piece, can be null
|
||||
*/
|
||||
Piece getHeldPiece() const;
|
||||
const std::shared_ptr<Piece>& getHeldPiece() const;
|
||||
|
||||
/**
|
||||
* @return A copy of the next piece queue
|
||||
* @return The next piece queue, can be empty
|
||||
*/
|
||||
std::vector<Piece> getNextPieces() const;
|
||||
const std::vector<Piece>& getNextPieces() const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Checks if one of the active piece's positions touches a wall in the board
|
||||
* @return If the active piece is in a wall
|
||||
*/
|
||||
bool isActivePieceInWall(const Position& shift = Position{0, 0}) const;
|
||||
|
||||
/**
|
||||
* Check if one of the active piece's positions shifted by a specified position would overlap with a set of positions
|
||||
* @return If the shifted active piece overlaps with one of the position
|
||||
*/
|
||||
bool activePieceOverlapsOnePosition(const std::set<Position>& safePositions, const Position& shift = Position{0, 0}) const;
|
||||
|
||||
/**
|
||||
* Sets the active piece to its spawn position
|
||||
*/
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
GameParameters::GameParameters(Gamemode gamemode, const Player& controls) :
|
||||
gamemode(gamemode),
|
||||
controls(controls) {
|
||||
|
||||
|
||||
this->reset();
|
||||
}
|
||||
|
||||
void GameParameters::reset() {
|
||||
// initialize lines and level
|
||||
this->clearedLines = 0;
|
||||
this->grade = 0;
|
||||
|
||||
switch (this->gamemode) {
|
||||
// lowest gravity
|
||||
case SPRINT : {this->level = 1; break;}
|
||||
@@ -23,32 +24,35 @@ void GameParameters::reset() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +66,8 @@ bool GameParameters::hasWon(int framesPassed) const {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -69,13 +75,14 @@ bool GameParameters::hasWon(int framesPassed) const {
|
||||
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;
|
||||
@@ -94,7 +101,7 @@ void GameParameters::updateStats() {
|
||||
/* GRAVITY */
|
||||
// get gravity for an assumed 20-rows board
|
||||
static const int gravityPerLevel[] = {
|
||||
0, // LVL0
|
||||
0, // lvl0 = no gravity
|
||||
1, // 60f/line, 20s total
|
||||
2, // 30f/line, 10s total
|
||||
3, // 20f/line, 6.66s total
|
||||
@@ -113,12 +120,13 @@ void GameParameters::updateStats() {
|
||||
40, // 1.5f/line, 30f total
|
||||
1 * 60, // 1line/f, 20f total
|
||||
2 * 60, // 2line/f, 10f total
|
||||
4 * 60 // 4line/f, 5f total
|
||||
4 * 60, // 4line/f, 5f total
|
||||
20 * 60 // lvl20 = instant gravity
|
||||
};
|
||||
if (this->level < 0) {
|
||||
this->gravity = gravityPerLevel[0];
|
||||
}
|
||||
else if (this->gravity > 20) {
|
||||
else if (this->level > 20) {
|
||||
this->gravity = gravityPerLevel[20];
|
||||
}
|
||||
else {
|
||||
@@ -129,6 +137,8 @@ void GameParameters::updateStats() {
|
||||
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;
|
||||
}
|
||||
@@ -206,6 +216,10 @@ int GameParameters::getLevel() const {
|
||||
return this->level;
|
||||
}
|
||||
|
||||
int GameParameters::getGrade() const {
|
||||
return this->grade;
|
||||
}
|
||||
|
||||
int GameParameters::getNextQueueLength() const {
|
||||
return this->nextQueueLength;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "Gamemode.h"
|
||||
#include "Player.h"
|
||||
#include "LineClear.h"
|
||||
|
||||
|
||||
/**
|
||||
@@ -14,6 +15,7 @@ class GameParameters {
|
||||
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
|
||||
@@ -39,7 +41,7 @@ class GameParameters {
|
||||
/**
|
||||
* Counts the newly cleared lines and update level and stats if needed
|
||||
*/
|
||||
void clearLines(int lineNumber);
|
||||
void lockedPiece(const LineClear& lineClear);
|
||||
|
||||
/**
|
||||
* Checks if the game ended based on the current states and time passed, accorind to the gamemode
|
||||
@@ -63,6 +65,11 @@ class GameParameters {
|
||||
* @return The current level
|
||||
*/
|
||||
int getLevel() const;
|
||||
|
||||
/**
|
||||
* @return The current grade
|
||||
*/
|
||||
int getGrade() const;
|
||||
|
||||
/**
|
||||
* @return The length of the next queue
|
||||
|
||||
@@ -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",
|
||||
"Infinite"
|
||||
};
|
||||
|
||||
return GAMEMODE_DESCRIPTIONS[gamemode];
|
||||
}
|
||||
|
||||
@@ -4,15 +4,21 @@
|
||||
#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() {
|
||||
// default board size
|
||||
this->boardHeight = 20;
|
||||
this->boardWidth = 10;
|
||||
this->piecesList = std::make_shared<PiecesList>(PiecesList());
|
||||
|
||||
this->boardWidth = DEFAULT_BOARD_WIDTH;
|
||||
this->boardHeight = DEFAULT_BOARD_HEIGHT;
|
||||
}
|
||||
|
||||
Game Menu::startGame(Gamemode gamemode) const {
|
||||
return Game(gamemode, this->playerControls, this->boardWidth, this->boardHeight, std::make_shared<PiecesList>(this->piecesList));
|
||||
return Game(gamemode, this->playerControls, this->boardWidth, this->boardHeight, this->piecesList);
|
||||
}
|
||||
|
||||
bool Menu::setBoardWidth(int width) {
|
||||
@@ -41,6 +47,14 @@ Player& Menu::getPlayerControls() {
|
||||
return this->playerControls;
|
||||
}
|
||||
|
||||
PiecesList& Menu::getPiecesList() {
|
||||
return this->piecesList;
|
||||
const Player& Menu::readPlayerControls() const {
|
||||
return this->playerControls;
|
||||
}
|
||||
|
||||
PiecesList& Menu::getPiecesList() {
|
||||
return *this->piecesList;
|
||||
}
|
||||
|
||||
const PiecesList& Menu::readPiecesList() const {
|
||||
return *this->piecesList;
|
||||
}
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
#include "Player.h"
|
||||
#include "Game.h"
|
||||
|
||||
static const int FRAMES_PER_SECOND = 60; // the number of frames per second, all the values in the game were choosen with this number in mind
|
||||
static const int FRAMES_PER_SECOND = 60; // the number of frames per second, all the values in the app were choosen with this number in mind
|
||||
|
||||
|
||||
/**
|
||||
* The interface between the UI and the core of the game
|
||||
* The interface between an UI and the core of the app
|
||||
*/
|
||||
class Menu {
|
||||
private:
|
||||
PiecesList piecesList; // the list of pieces in the game
|
||||
Player playerControls; // the controls of the player
|
||||
int boardHeight; // the height of the board for the next game
|
||||
int boardWidth; // the width of the board for the next game
|
||||
std::shared_ptr<PiecesList> piecesList; // the list of pieces used by the app
|
||||
Player playerControls; // the controls of the player
|
||||
int boardWidth; // the width of the board for the next game
|
||||
int boardHeight; // the height of the board for the next game
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -50,14 +50,24 @@ class Menu {
|
||||
* @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;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "../Pieces/Piece.h"
|
||||
#include "../Pieces/PiecesFiles.h"
|
||||
#include "DistributionMode.h"
|
||||
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
@@ -11,6 +12,10 @@ 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();
|
||||
@@ -24,6 +29,7 @@ PiecesList::PiecesList() {
|
||||
|
||||
bool PiecesList::loadPieces(int size) {
|
||||
if (size < 1) return false;
|
||||
if (size <= this->highestLoadedSize) return true;
|
||||
|
||||
PiecesFiles piecesFiles;
|
||||
for (int i = this->highestLoadedSize + 1; i <= size; i++) {
|
||||
@@ -87,6 +93,14 @@ 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;
|
||||
}
|
||||
@@ -101,7 +115,33 @@ std::vector<std::pair<int, int>> PiecesList::getSelectedPieces() const {
|
||||
return this->selectedPieces;
|
||||
}
|
||||
|
||||
Piece PiecesList::getPiece(std::pair<int, int> pieceIndex) const {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -110,4 +150,7 @@ void PiecesList::pushBackEmptyVectors() {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../Pieces/Piece.h"
|
||||
#include "DistributionMode.h"
|
||||
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
@@ -18,6 +19,9 @@ class PiecesList {
|
||||
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:
|
||||
/**
|
||||
@@ -66,6 +70,19 @@ class PiecesList {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -77,14 +94,29 @@ class PiecesList {
|
||||
int getNumberOfPieces(int size) const;
|
||||
|
||||
/**
|
||||
* @return The indexes of all selected pieces
|
||||
* @return A copy of the indexes of all selected pieces
|
||||
*/
|
||||
std::vector<std::pair<int, int>> getSelectedPieces() const;
|
||||
|
||||
/**
|
||||
* @return The 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
|
||||
*/
|
||||
Piece getPiece(std::pair<int, int> pieceIndex) const;
|
||||
const Piece& lookAtPiece(const std::pair<int, int>& pieceIndex) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
#include "Player.h"
|
||||
|
||||
static const int DAS_MIN_VALUE = 0; // 0ms
|
||||
static const int DAS_MAX_VALUE = 30; // 500ms
|
||||
static const int ARR_MIN_VALUE = 0; // 0ms
|
||||
static const int ARR_MAX_VALUE = 30; // 500ms
|
||||
static const int SDR_MIN_VALUE = 0; // 0ms
|
||||
static const int SDR_MAX_VALUE = 6; // 100ms
|
||||
|
||||
|
||||
Player::Player() {
|
||||
// default settings
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
static const int DAS_MIN_VALUE = 0; // the minimal selectable DAS value, equals to 0ms
|
||||
static const int DAS_MAX_VALUE = 30; // the maximal selectable DAS value, equals to 500ms
|
||||
static const int ARR_MIN_VALUE = 0; // the minimal selectable ARR value, equals to 0ms
|
||||
static const int ARR_MAX_VALUE = 30; // the maximal selectable ARR value, equals to 500ms
|
||||
static const int SDR_MIN_VALUE = 0; // the minimal selectable SDR value, equals to 0ms
|
||||
static const int SDR_MAX_VALUE = 6; // the maximal selectable SDR value, equals to 100ms
|
||||
|
||||
|
||||
/**
|
||||
* The controls of a player
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
#include "Menu.h"
|
||||
#include "../Pieces/Generator.h"
|
||||
#include "../Pieces/PiecesFiles.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
|
||||
void testGeneratorForAllSizes(int amount);
|
||||
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));
|
||||
|
||||
Menu menu;
|
||||
menu.getPiecesList().loadPieces(4);
|
||||
menu.getPiecesList().selectAllPieces(4);
|
||||
Game game = menu.startGame(SPRINT);
|
||||
game.start();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void testGeneratorForAllSizes(int amount) {
|
||||
using std::chrono::high_resolution_clock;
|
||||
using std::chrono::duration_cast;
|
||||
using std::chrono::duration;
|
||||
using std::chrono::milliseconds;
|
||||
Generator generator;
|
||||
|
||||
for (int i = 1; i <= amount; i++) {
|
||||
auto t1 = high_resolution_clock::now();
|
||||
std::vector<Polyomino> n_minos = generator.generatePolyominos(i);
|
||||
auto t2 = high_resolution_clock::now();
|
||||
|
||||
duration<double, std::milli> ms_double = t2 - t1;
|
||||
std::cout << "generated " << n_minos.size() << " polyominos of size " << i << " in " << ms_double.count() << "ms" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void testGeneratorForOneSize(int size) {
|
||||
using std::chrono::high_resolution_clock;
|
||||
using std::chrono::duration_cast;
|
||||
using std::chrono::duration;
|
||||
using std::chrono::milliseconds;
|
||||
Generator generator;
|
||||
|
||||
std::cout << "Generating " << size << "-minos" << std::endl;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
auto t1 = high_resolution_clock::now();
|
||||
std::vector<Polyomino> n_minos = generator.generatePolyominos(size);
|
||||
auto t2 = high_resolution_clock::now();
|
||||
|
||||
duration<double, std::milli> ms_double = t2 - t1;
|
||||
std::cout << ms_double.count() << "ms" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void testGeneratorByprintingAllNminos(int n) {
|
||||
Generator generator;
|
||||
std::vector<Polyomino> n_minos = generator.generatePolyominos(n);
|
||||
|
||||
for (Polyomino& n_mino : n_minos) {
|
||||
n_mino.goToSpawnPosition();
|
||||
}
|
||||
std::sort(n_minos.begin(), n_minos.end());
|
||||
|
||||
for (Polyomino& n_mino : n_minos) {
|
||||
|
||||
std::cout << n_mino << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void testStoringAndRetrievingPieces(int size) {
|
||||
PiecesFiles piecesFiles;
|
||||
piecesFiles.savePieces(size);
|
||||
|
||||
std::vector<Piece> pieces;
|
||||
std::vector<int> convexPieces;
|
||||
std::vector<int> holelessPieces;
|
||||
std::vector<int> otherPieces;
|
||||
piecesFiles.loadPieces(size, pieces, convexPieces, holelessPieces, otherPieces);
|
||||
|
||||
std::cout << "Convex " << size << "-minos:" << std::endl;
|
||||
for (int index : convexPieces) {
|
||||
std::cout << pieces.at(index);
|
||||
}
|
||||
|
||||
std::cout << "Holeless " << size << "-minos:" << std::endl;
|
||||
for (int index : holelessPieces) {
|
||||
std::cout << pieces.at(index);
|
||||
}
|
||||
|
||||
std::cout << "Others " << size << "-minos:" << std::endl;
|
||||
for (int index : otherPieces) {
|
||||
std::cout << pieces.at(index);
|
||||
}
|
||||
}
|
||||
|
||||
void generateFilesForAllSizes(int amount) {
|
||||
using std::chrono::high_resolution_clock;
|
||||
using std::chrono::duration_cast;
|
||||
using std::chrono::duration;
|
||||
using std::chrono::milliseconds;
|
||||
PiecesFiles piecesFiles;
|
||||
|
||||
for (int i = 1; i <= amount; i++) {
|
||||
auto t1 = high_resolution_clock::now();
|
||||
piecesFiles.savePieces(i);
|
||||
auto t2 = high_resolution_clock::now();
|
||||
|
||||
duration<double, std::milli> ms_double = t2 - t1;
|
||||
std::cout << "Generated pieces files for size " << i << " in " << ms_double.count() << "ms" << std::endl;
|
||||
}
|
||||
|
||||
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();
|
||||
piecesFiles.loadPieces(i, pieces, convexPieces, holelessPieces, otherPieces);
|
||||
auto t2 = high_resolution_clock::now();
|
||||
|
||||
duration<double, std::milli> ms_double = t2 - t1;
|
||||
std::cout << "Read pieces from files for size " << i << " in " << ms_double.count() << "ms" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void 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++) {
|
||||
std::vector<Piece> pieces;
|
||||
std::vector<int> convexPieces;
|
||||
std::vector<int> holelessPieces;
|
||||
std::vector<int> otherPieces;
|
||||
piecesFiles.loadPieces(i, pieces, convexPieces, holelessPieces, otherPieces);
|
||||
|
||||
std::cout << i << "-minos : " << pieces.size() << std::endl;
|
||||
std::cout << "Convex " << i << "-minos : " << convexPieces.size() << std::endl;
|
||||
std::cout << "Holeless " << i << "-minos : " << holelessPieces.size() << std::endl;
|
||||
std::cout << "Others " << i << "-minos : " << otherPieces.size() << std::endl;
|
||||
}
|
||||
}
|
||||
12
src/GraphicalUI/AppMenus/AppMenu.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "AppMenu.h"
|
||||
|
||||
#include "../../Utils/AssetManager.h"
|
||||
|
||||
AppMenu::AppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) : menuStack(menuStack),
|
||||
settings(settings),
|
||||
renderWindow(renderWindow)
|
||||
{
|
||||
const Asset& file = getResource(AssetName::data_fonts_pressstart_prstartk_ttf);
|
||||
|
||||
this->pressStartFont = sf::Font(file.data, file.size);
|
||||
}
|
||||
84
src/GraphicalUI/AppMenus/AppMenu.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#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;
|
||||
|
||||
public:
|
||||
AppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::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));
|
||||
}
|
||||
};
|
||||
64
src/GraphicalUI/AppMenus/GameBoardAppMenu.cpp
Normal 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();
|
||||
}
|
||||
21
src/GraphicalUI/AppMenus/GameBoardAppMenu.h
Normal 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;
|
||||
};
|
||||
85
src/GraphicalUI/AppMenus/GameDistributionAppMenu.cpp
Normal 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 <= this->settings->getMaximumPiecesSize(); 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, this->settings->getMaximumPiecesSize() - 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();
|
||||
}
|
||||
21
src/GraphicalUI/AppMenus/GameDistributionAppMenu.h
Normal 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;
|
||||
};
|
||||
234
src/GraphicalUI/AppMenus/GamePiecesAppMenu.cpp
Normal file
@@ -0,0 +1,234 @@
|
||||
#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 <= this->settings->getMaximumPiecesSize(); i++) {
|
||||
this->playerCursor.addRow(i + 1, this->settings->getMenu().readPiecesList().getNumberOfPieces(i) + 4);
|
||||
}
|
||||
|
||||
if (this->settings->getMaximumPiecesSize() < MAXIMUM_PIECES_SIZE) {
|
||||
this->playerCursor.addRow(this->settings->getMaximumPiecesSize() + 2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
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 == (this->settings->getMaximumPiecesSize() + 2)) {
|
||||
int newMaxSize = this->settings->getMaximumPiecesSize() + 1;
|
||||
this->settings->loadPieces(newMaxSize);
|
||||
|
||||
this->playerCursor.removeRow(newMaxSize + 1);
|
||||
this->playerCursor.addRow(newMaxSize + 1, this->settings->getMenu().readPiecesList().getNumberOfPieces(newMaxSize) + 4);
|
||||
this->playerCursor.goToPosition({0u, newMaxSize + 1u});
|
||||
|
||||
if (newMaxSize < MAXIMUM_PIECES_SIZE) {
|
||||
this->playerCursor.addRow(newMaxSize + 2, 1);
|
||||
}
|
||||
}
|
||||
else 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 {
|
||||
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);
|
||||
bool addExtraLine = (this->settings->getMaximumPiecesSize() < MAXIMUM_PIECES_SIZE);
|
||||
int firstElem = std::clamp(((int) this->playerCursor.getPosition().y) - 2, 1, this->settings->getMaximumPiecesSize() - 2 + (addExtraLine ? 1 : 0));
|
||||
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) {
|
||||
if (!(pieceSize > this->settings->getMaximumPiecesSize())) {
|
||||
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, {}, "UNLOADED_" + std::to_string(pieceSize), xProgress, yPos, {});
|
||||
xProgress += (1.f + (text.getGlobalBounds().size.x / this->settings->getWindowSizeMultiplier()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!(value > this->settings->getMaximumPiecesSize())) {
|
||||
this->placeText(text, {}, ((first) ? "" : " ") + getPiecesTypeName(pieceType) + "_" + std::to_string(value), xProgress, yPos, {});
|
||||
xProgress += (1.f + (text.getGlobalBounds().size.x / this->settings->getWindowSizeMultiplier()));
|
||||
}
|
||||
else {
|
||||
this->placeText(text, {}, "UNLOADED_" + std::to_string(pieceSize), xProgress, yPos, {});
|
||||
xProgress += (1.f + (text.getGlobalBounds().size.x / this->settings->getWindowSizeMultiplier()));
|
||||
}
|
||||
}
|
||||
|
||||
elem++;
|
||||
}
|
||||
}
|
||||
|
||||
void GamePiecesAppMenu::drawRow(int piecesSize, float yPos, bool drawFromFirstElem) const {
|
||||
if (piecesSize > this->settings->getMaximumPiecesSize()) {
|
||||
sf::Text text(this->pressStartFont, "", this->settings->getWindowSizeMultiplier() * 2);
|
||||
text.setOutlineThickness(this->settings->getWindowSizeMultiplier() / 2);
|
||||
if (this->playerCursor.getPosition().y == (piecesSize + 1)) {
|
||||
text.setOutlineColor({255, 255, 255});
|
||||
}
|
||||
else {
|
||||
text.setOutlineColor({0, 0, 0});
|
||||
}
|
||||
|
||||
std::string sizeString = "LOAD SIZE " + std::to_string(piecesSize) + "? ";
|
||||
if (piecesSize <= 10) {
|
||||
text.setFillColor({0, 255, 0});
|
||||
this->placeText(text, {}, sizeString + "(LOW LOAD TIME)", 1.f, yPos, {});
|
||||
}
|
||||
else if (piecesSize <= 13) {
|
||||
text.setFillColor({255, 255, 0});
|
||||
this->placeText(text, {}, sizeString + "(MEDIUM LOAD TIME)", 1.f, yPos, {});
|
||||
}
|
||||
else {
|
||||
text.setFillColor({255, 0, 0});
|
||||
this->placeText(text, {}, sizeString + "(LONG LOAD TIME)", 1.f, yPos, {});
|
||||
}
|
||||
}
|
||||
else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
29
src/GraphicalUI/AppMenus/GamePiecesAppMenu.h
Normal 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;
|
||||
};
|
||||
275
src/GraphicalUI/AppMenus/GamePlayingAppMenu.cpp
Normal file
@@ -0,0 +1,275 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
32
src/GraphicalUI/AppMenus/GamePlayingAppMenu.h
Normal 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;
|
||||
};
|
||||
82
src/GraphicalUI/AppMenus/GameSettingsAppMenu.cpp
Normal 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();
|
||||
}
|
||||
21
src/GraphicalUI/AppMenus/GameSettingsAppMenu.h
Normal 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;
|
||||
};
|
||||
97
src/GraphicalUI/AppMenus/InfoAppMenu.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#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({INFO_SECTIONS_COUNT}),
|
||||
sectionsName(
|
||||
"< ABOUT >",
|
||||
"< PIECES TYPES >",
|
||||
"< 0 DEGREES ROTATIONS >",
|
||||
"< ROTATION SYSTEM >",
|
||||
"< SCORING >"
|
||||
),
|
||||
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.",
|
||||
|
||||
"There is multiple pieces type in\n"
|
||||
"the selection screen. Use theses\n"
|
||||
"categories for size of at least 7.\n"
|
||||
"Convex, Holeless and Others are\n"
|
||||
"all mutually exclusive.\n"
|
||||
"Others have holes inside them, and\n"
|
||||
"Convex are presumably easier to\n"
|
||||
"play with than Holeless.",
|
||||
|
||||
"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.",
|
||||
|
||||
"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."
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
25
src/GraphicalUI/AppMenus/InfoAppMenu.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppMenu.h"
|
||||
#include "../PlayerCursor.h"
|
||||
|
||||
#include <stack>
|
||||
#include <memory>
|
||||
#include <SFML/Graphics.hpp>
|
||||
|
||||
static const int INFO_SECTIONS_COUNT = 5;
|
||||
|
||||
|
||||
class InfoAppMenu : public AppMenu {
|
||||
private:
|
||||
PlayerCursor playerCursor;
|
||||
sf::String sectionsName[INFO_SECTIONS_COUNT];
|
||||
sf::String sectionsContent[INFO_SECTIONS_COUNT];
|
||||
|
||||
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;
|
||||
};
|
||||
54
src/GraphicalUI/AppMenus/MainAppMenu.cpp
Normal 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();
|
||||
}
|
||||
21
src/GraphicalUI/AppMenus/MainAppMenu.h
Normal 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;
|
||||
};
|
||||
76
src/GraphicalUI/AppMenus/SettingsControlsAppMenu.cpp
Normal 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();
|
||||
}
|
||||
21
src/GraphicalUI/AppMenus/SettingsControlsAppMenu.h
Normal 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;
|
||||
};
|
||||
129
src/GraphicalUI/AppMenus/SettingsKeybindsAppMenu.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "SettingsKeybindsAppMenu.h"
|
||||
|
||||
#include "AppMenu.h"
|
||||
#include "../PlayerCursor.h"
|
||||
#include "../../Utils/AssetManager.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(" "), "");
|
||||
|
||||
const Asset& textureData = getResource("data/images/keybinds/" + textureName + ".png");
|
||||
|
||||
this->iconTextures[action] = sf::Texture(textureData.data, textureData.size, 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();
|
||||
}
|
||||
24
src/GraphicalUI/AppMenus/SettingsKeybindsAppMenu.h
Normal 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;
|
||||
};
|
||||
78
src/GraphicalUI/AppMenus/SettingsMainAppMenu.cpp
Normal 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();
|
||||
}
|
||||
21
src/GraphicalUI/AppMenus/SettingsMainAppMenu.h
Normal 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;
|
||||
};
|
||||
75
src/GraphicalUI/AppMenus/StartUpAppMenu.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "StartUpAppMenu.h"
|
||||
|
||||
#include "AppMenu.h"
|
||||
#include "MainAppMenu.h"
|
||||
#include "../PlayerCursor.h"
|
||||
|
||||
#include <stack>
|
||||
#include <memory>
|
||||
#include <algorithm>
|
||||
#include <SFML/Graphics.hpp>
|
||||
|
||||
|
||||
StartUpAppMenu::StartUpAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow) :
|
||||
AppMenu(menuStack, settings, renderWindow),
|
||||
playerCursor({MAXIMUM_PIECES_SIZE + 1}) {
|
||||
|
||||
this->playerCursor.goToPosition({(unsigned int) std::clamp(this->settings->getMaximumPiecesSize(), MINIMUM_PIECES_SIZE, MAXIMUM_PIECES_SIZE), 0u});
|
||||
}
|
||||
|
||||
void StartUpAppMenu::computeFrame() {
|
||||
this->updateMetaBinds();
|
||||
this->playerCursor.updatePosition();
|
||||
|
||||
if (this->playerCursor.getPosition().x < MINIMUM_PIECES_SIZE) {
|
||||
if (this->playerCursor.movedLeft()) {
|
||||
this->playerCursor.goToPosition({MAXIMUM_PIECES_SIZE, 0});
|
||||
}
|
||||
else {
|
||||
this->playerCursor.goToPosition({MINIMUM_PIECES_SIZE, 0});
|
||||
}
|
||||
}
|
||||
|
||||
if (this->enterReleased) {
|
||||
this->settings->loadSettingsFromFile(true, {this->playerCursor.getPosition().x});
|
||||
this->menuStack->pop();
|
||||
|
||||
if (this->settings->hasLoadedPieces()) {
|
||||
this->menuStack->push(std::make_shared<MainAppMenu>(this->menuStack, this->settings, this->renderWindow));
|
||||
}
|
||||
else {
|
||||
std::cout << "ERROR: COULD NOT LOAD PIECES" << std::endl;
|
||||
std::cout << "ARGUMENT WAS: " << this->playerCursor.getPosition().x << std::endl;
|
||||
}
|
||||
}
|
||||
else if (this->escReleased) {
|
||||
this->menuStack->pop();
|
||||
}
|
||||
}
|
||||
|
||||
void StartUpAppMenu::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, {}, "SELECT THE LOADED PIECES MAXIMUM SIZE", 10.f, {});
|
||||
this->placeTitle(text, this->playerCursor, "< " + std::to_string(this->playerCursor.getPosition().x) + " >", 25.f, this->playerCursor.getPosition());
|
||||
|
||||
text.setOutlineColor({0, 0, 0});
|
||||
if (this->playerCursor.getPosition().x <= 10) {
|
||||
text.setFillColor({0, 255, 0});
|
||||
this->placeTitle(text, {}, "LOW LOAD TIME", 40.f, {});
|
||||
}
|
||||
else if (this->playerCursor.getPosition().x <= 13) {
|
||||
text.setFillColor({255, 255, 0});
|
||||
this->placeTitle(text, {}, "MEDIUM LOAD TIME", 40.f, {});
|
||||
}
|
||||
else {
|
||||
text.setFillColor({255, 0, 0});
|
||||
this->placeTitle(text, {}, "LONG LOAD TIME", 40.f, {});
|
||||
}
|
||||
|
||||
this->renderWindow->display();
|
||||
}
|
||||
21
src/GraphicalUI/AppMenus/StartUpAppMenu.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppMenu.h"
|
||||
#include "../PlayerCursor.h"
|
||||
|
||||
#include <stack>
|
||||
#include <memory>
|
||||
#include <SFML/Graphics.hpp>
|
||||
|
||||
|
||||
class StartUpAppMenu : public AppMenu {
|
||||
private:
|
||||
PlayerCursor playerCursor;
|
||||
|
||||
public:
|
||||
StartUpAppMenu(std::shared_ptr<MenuStack> menuStack, std::shared_ptr<Settings> settings, std::shared_ptr<sf::RenderWindow> renderWindow);
|
||||
|
||||
void computeFrame() override;
|
||||
|
||||
void drawFrame() const override;
|
||||
};
|
||||
53
src/GraphicalUI/GraphApp.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "GraphApp.h"
|
||||
|
||||
#include "AppMenus/AppMenu.h"
|
||||
#include "AppMenus/StartUpAppMenu.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>(false);
|
||||
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<StartUpAppMenu>(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();
|
||||
}
|
||||
21
src/GraphicalUI/GraphApp.h
Normal 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();
|
||||
};
|
||||
98
src/GraphicalUI/Keybinds.cpp
Normal 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
@@ -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
|
||||
35
src/GraphicalUI/PiecesType.h
Normal 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];
|
||||
}
|
||||
163
src/GraphicalUI/PlayerCursor.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
51
src/GraphicalUI/PlayerCursor.h
Normal 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();
|
||||
};
|
||||
416
src/GraphicalUI/Settings.cpp
Normal file
@@ -0,0 +1,416 @@
|
||||
#include "Settings.h"
|
||||
|
||||
#include "../Core/Menu.h"
|
||||
#include "Keybinds.h"
|
||||
#include "PiecesType.h"
|
||||
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#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 = 20;
|
||||
|
||||
|
||||
Settings::Settings(bool loadPieces) {
|
||||
this->keybinds.clear();
|
||||
this->keybinds.reserve(NUMBER_OF_KEYBINDS);
|
||||
for (int i = 0; i < NUMBER_OF_KEYBINDS; i++) {
|
||||
this->keybinds.emplace_back(i);
|
||||
}
|
||||
|
||||
this->loadSettingsFromFile(loadPieces, {});
|
||||
}
|
||||
|
||||
void Settings::loadPieces(int maximumPiecesSizeRequest) {
|
||||
if (maximumPiecesSizeRequest < MINIMUM_PIECES_SIZE) {
|
||||
maximumPiecesSizeRequest = MINIMUM_PIECES_SIZE;
|
||||
}
|
||||
else if (maximumPiecesSizeRequest > MAXIMUM_PIECES_SIZE) {
|
||||
maximumPiecesSizeRequest = MAXIMUM_PIECES_SIZE;
|
||||
}
|
||||
|
||||
bool succeeded = true;
|
||||
int i = 1;
|
||||
while (succeeded && (i <= maximumPiecesSizeRequest)) {
|
||||
succeeded = this->menu.getPiecesList().loadPieces(i);
|
||||
i++;
|
||||
}
|
||||
|
||||
if (succeeded) {
|
||||
this->maximumPiecesSize = maximumPiecesSizeRequest;
|
||||
}
|
||||
this->loadedPieces = succeeded;
|
||||
}
|
||||
|
||||
void Settings::loadSettingsFromFile(bool loadPieces, std::optional<int> maximumPiecesSizeRequest) {
|
||||
std::ifstream settingsFile("data/config/settings.bin", std::ios::binary);
|
||||
char byte;
|
||||
|
||||
// file format version
|
||||
settingsFile.get(byte);
|
||||
|
||||
// maximum pieces size
|
||||
settingsFile.get(byte);
|
||||
this->maximumPiecesSize = byte;
|
||||
|
||||
if (loadPieces) {
|
||||
if (maximumPiecesSizeRequest.has_value()) {
|
||||
this->loadPieces(maximumPiecesSizeRequest.value());
|
||||
}
|
||||
else {
|
||||
this->loadPieces(byte);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this->loadedPieces = false;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
if (this->loadedPieces) {
|
||||
// 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 pieceSize;
|
||||
char lowByte;
|
||||
char midByte;
|
||||
char highByte;
|
||||
|
||||
this->selectedPieces.clear();
|
||||
while (settingsFile.get(pieceType)) {
|
||||
if (settingsFile.eof()) break;
|
||||
|
||||
if (getSizeOfPieces(PiecesType(pieceType)) == 0) {
|
||||
settingsFile.get(pieceSize);
|
||||
|
||||
this->selectedPieces.emplace_back(PiecesType(pieceType), pieceSize);
|
||||
}
|
||||
else {
|
||||
settingsFile.get(lowByte);
|
||||
settingsFile.get(midByte);
|
||||
settingsFile.get(highByte);
|
||||
|
||||
int pieceNumber = ((unsigned char) lowByte) + ((unsigned char) midByte << 8) + ((unsigned char) highByte << 16);
|
||||
this->selectedPieces.emplace_back(PiecesType(pieceType), pieceNumber);
|
||||
}
|
||||
}
|
||||
this->confirmSelectedPieces();
|
||||
}
|
||||
else {
|
||||
this->distributions.clear();
|
||||
this->selectedPieces.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void Settings::saveSettingsToFile() const {
|
||||
if (!this->loadedPieces) return;
|
||||
|
||||
this->keybinds.at(CUSTOMIZABLE_KEYBINDS).saveKeybindsToFile();
|
||||
|
||||
std::ofstream settingsFile("data/config/settings.bin", std::ios::trunc | std::ios::binary);
|
||||
char byte;
|
||||
|
||||
// file format version
|
||||
byte = CURRENT_FILE_FORMAT_VERSION;
|
||||
settingsFile.write(&byte, 1);
|
||||
|
||||
// maximum pieces size
|
||||
byte = this->maximumPiecesSize;
|
||||
settingsFile.write(&byte, 1);
|
||||
|
||||
// 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);
|
||||
if (getSizeOfPieces(type) == 0) {
|
||||
byte = value;
|
||||
settingsFile.write(&byte, 1);
|
||||
}
|
||||
else {
|
||||
int number = value;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
byte = (number % 256);
|
||||
settingsFile.write(&byte, 1);
|
||||
number = (number >> 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!this->loadedPieces) return;
|
||||
|
||||
this->selectedPieces.emplace_back(type, value);
|
||||
}
|
||||
|
||||
void Settings::unselectPieces(int index) {
|
||||
if (!this->loadedPieces) return;
|
||||
if (index >= this->selectedPieces.size()) return;
|
||||
|
||||
this->selectedPieces.erase(this->selectedPieces.begin() + index);
|
||||
}
|
||||
|
||||
void Settings::confirmSelectedPieces() {
|
||||
if (!this->loadedPieces) return;
|
||||
|
||||
this->menu.getPiecesList().unselectAll();
|
||||
|
||||
bool selectedNone = true;
|
||||
for (const auto& [type, value] : this->selectedPieces) {
|
||||
int size = getSizeOfPieces(type);
|
||||
|
||||
if (size == 0) {
|
||||
if (!(value > this->maximumPiecesSize)) {
|
||||
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;}
|
||||
}
|
||||
selectedNone = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!(getSizeOfPieces(type) > this->maximumPiecesSize)) {
|
||||
this->menu.getPiecesList().selectPiece(size, value);
|
||||
selectedNone = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedNone) {
|
||||
this->selectedPieces.push_back(DEFAULT_SELECTION);
|
||||
this->confirmSelectedPieces();
|
||||
}
|
||||
}
|
||||
|
||||
bool Settings::increaseDistribution(int size) {
|
||||
if (!this->loadedPieces) return false;
|
||||
if (size < 1 || size > this->maximumPiecesSize) return false;
|
||||
|
||||
if (this->distributions.at(size) < DISTRIBUTION_MAX) {
|
||||
this->distributions.at(size)++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Settings::decreaseDistribution(int size) {
|
||||
if (!this->loadedPieces) return false;
|
||||
if (size < 1 || size > this->maximumPiecesSize) return false;
|
||||
|
||||
if (this->distributions.at(size) > 0) {
|
||||
this->distributions.at(size)--;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Settings::confirmDistribution() {
|
||||
if (!this->loadedPieces) return;
|
||||
|
||||
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::getMaximumPiecesSize() const {
|
||||
return this->maximumPiecesSize;
|
||||
}
|
||||
|
||||
bool Settings::hasLoadedPieces() const {
|
||||
return this->loadedPieces;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
100
src/GraphicalUI/Settings.h
Normal file
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include "../Core/Menu.h"
|
||||
#include "Keybinds.h"
|
||||
#include "PiecesType.h"
|
||||
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <SFML/Graphics.hpp>
|
||||
|
||||
static const int CURRENT_FILE_FORMAT_VERSION = 11;
|
||||
|
||||
static const int MAXIMUM_BOARD_WIDTH = 40;
|
||||
static const int MAXIMUM_BOARD_HEIGHT = 40;
|
||||
|
||||
static const int RELEASE_PIECES_SIZE = 15;
|
||||
static const int DEBUG_PIECES_SIZE = 10;
|
||||
|
||||
static const int MINIMUM_PIECES_SIZE = 4;
|
||||
#ifdef NDEBUG
|
||||
static const int MAXIMUM_PIECES_SIZE = RELEASE_PIECES_SIZE;
|
||||
#else
|
||||
static const int MAXIMUM_PIECES_SIZE = DEBUG_PIECES_SIZE;
|
||||
#endif
|
||||
|
||||
static const std::pair<PiecesType, int> DEFAULT_SELECTION = {ALL_PIECES, MINIMUM_PIECES_SIZE};
|
||||
|
||||
|
||||
class Settings {
|
||||
private:
|
||||
Menu menu;
|
||||
int maximumPiecesSize;
|
||||
bool loadedPieces;
|
||||
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(bool loadPieces);
|
||||
|
||||
void loadPieces(int maximumPiecesSizeRequest);
|
||||
|
||||
void loadSettingsFromFile(bool loadPieces, std::optional<int> maximumPiecesSizeRequest);
|
||||
|
||||
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 getMaximumPiecesSize() const;
|
||||
|
||||
bool hasLoadedPieces() const;
|
||||
|
||||
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;
|
||||
};
|
||||
208
src/GraphicalUI/main.cpp
Normal file
@@ -0,0 +1,208 @@
|
||||
#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")) {
|
||||
#ifdef NDEBUG
|
||||
std::cout << "IMPORTANT: You are currently in release mode, if you do not wish to generate big pieces (can take several minutes), type 'xmake f -m debug'." << std::endl;
|
||||
#endif
|
||||
|
||||
std::cout << "INFO: Pieces files for size " << i << " not found, generating..." << std::endl;
|
||||
pf.savePieces(i);
|
||||
}
|
||||
}
|
||||
#ifndef NDEBUG
|
||||
std::cout << "IMPORTANT: you are currently in debug mode, if you wish to use bigger pieces, type 'xmake f -m release'." << std::endl;
|
||||
|
||||
bool everythingGenerated = true;
|
||||
for (int i = DEBUG_PIECES_SIZE; i <= RELEASE_PIECES_SIZE; i++) {
|
||||
if (!std::filesystem::exists("data/pieces/" + std::to_string(i) + "minos.bin")) {
|
||||
everythingGenerated = false;
|
||||
}
|
||||
}
|
||||
if (!everythingGenerated) {
|
||||
std::cout << "NOTE : you do not have all pieces generated, generating can take several minutes." << std::endl;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!std::filesystem::exists("data/config/settings.bin")) {
|
||||
std::cout << "INFO: Settings file not found, generating..." << std::endl;
|
||||
resetSettingsFile();
|
||||
}
|
||||
else {
|
||||
std::ifstream settingsFile("data/config/settings.bin", std::ios::binary);
|
||||
char byte;
|
||||
|
||||
settingsFile.get(byte);
|
||||
if ((unsigned char) byte < CURRENT_FILE_FORMAT_VERSION) {
|
||||
std::cout << "INFO: Files format changed, regenerating..." << std::endl;
|
||||
resetSettingsFile();
|
||||
for (int i = 0; i < NUMBER_OF_KEYBINDS; i++) {
|
||||
resetKeybindFile(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < NUMBER_OF_KEYBINDS; i++) {
|
||||
if (!std::filesystem::exists("data/config/keybinds/layout" + std::to_string(i) + ".bin")) {
|
||||
std::cout << "INFO: Keybind file n°" << (i + 1) << "/" << NUMBER_OF_KEYBINDS << " not found, generating..." << std::endl;
|
||||
resetKeybindFile(i);
|
||||
}
|
||||
}
|
||||
|
||||
GraphApp UI;
|
||||
UI.run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void resetSettingsFile() {
|
||||
std::filesystem::create_directories("data/config");
|
||||
std::ofstream settingsFile("data/config/settings.bin", std::ios::trunc | std::ios::binary);
|
||||
char byte;
|
||||
|
||||
Menu menu;
|
||||
|
||||
// file format version
|
||||
byte = CURRENT_FILE_FORMAT_VERSION;
|
||||
settingsFile.write(&byte, 1);
|
||||
|
||||
// maximum pieces size
|
||||
byte = MINIMUM_PIECES_SIZE;
|
||||
settingsFile.write(&byte, 1);
|
||||
|
||||
// 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::filesystem::create_directories("data/config/keybinds/layout");
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -11,19 +11,19 @@
|
||||
Generator::Generator() {
|
||||
}
|
||||
|
||||
std::vector<Polyomino> Generator::generatePolyominos(int polyominoSize) {
|
||||
this->validPolyominos.clear();
|
||||
std::vector<Polyomino> Generator::generatePolyominoes(int polyominoSize) {
|
||||
this->validPolyominoes.clear();
|
||||
this->currentTestedShape.clear();
|
||||
|
||||
// a polyomino has at least 1 square
|
||||
if (polyominoSize < 1) return this->validPolyominos;
|
||||
if (polyominoSize < 1) return this->validPolyominoes;
|
||||
|
||||
// start generating from the monomino
|
||||
// always place the first cell at (0, 0)
|
||||
this->currentTestedShape.insert(Position{0, 0});
|
||||
|
||||
std::map<Position, int> candidatePositions;
|
||||
this->generate(polyominoSize, 0, 1, candidatePositions);
|
||||
return this->validPolyominos;
|
||||
return this->validPolyominoes;
|
||||
}
|
||||
|
||||
void Generator::generate(int polyominoSize, int lastAddedPositionNumber, int nextAvaibleNumber, std::map<Position, int> candidatePositions) {
|
||||
@@ -31,7 +31,7 @@ void Generator::generate(int polyominoSize, int lastAddedPositionNumber, int nex
|
||||
if (polyominoSize == this->currentTestedShape.size()) {
|
||||
Polyomino candidate(this->currentTestedShape);
|
||||
|
||||
// we sort the rotations of the polyominos
|
||||
// we sort the rotations of the polyominoes
|
||||
std::vector<Polyomino> candidateRotations;
|
||||
candidateRotations.reserve(4);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -43,7 +43,7 @@ void Generator::generate(int polyominoSize, int lastAddedPositionNumber, int nex
|
||||
|
||||
// we keep the polyomino only if it was generated in its lowest rotation
|
||||
if (candidate == candidateRotations.at(0)) {
|
||||
this->validPolyominos.push_back(candidate);
|
||||
this->validPolyominoes.push_back(candidate);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -57,7 +57,7 @@ void Generator::generate(int polyominoSize, int lastAddedPositionNumber, int nex
|
||||
this->tryToAddCandidatePosition(Position{position.x - 1, position.y}, nextAvaibleNumber, candidatePositions);
|
||||
}
|
||||
|
||||
// generate polyominos for all positions with a higher number than the last one
|
||||
// try adding a square only to positions with a higher number than the last one
|
||||
for (auto [key, val] : candidatePositions) {
|
||||
if (val > lastAddedPositionNumber) {
|
||||
this->currentTestedShape.insert(key);
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
|
||||
|
||||
/**
|
||||
* A generator of one-sided polyominos of any size
|
||||
* A generator of one-sided polyominoes of any size
|
||||
*/
|
||||
class Generator {
|
||||
private:
|
||||
std::vector<Polyomino> validPolyominos; // the list of already generated polyominos
|
||||
std::vector<Polyomino> validPolyominoes; // the list of already generated polyominoes
|
||||
std::set<Position> currentTestedShape; // the polyomino being created
|
||||
|
||||
public:
|
||||
@@ -22,14 +22,14 @@ class Generator {
|
||||
Generator();
|
||||
|
||||
/**
|
||||
* Generates the list of all one-sided polyominos of the specified size
|
||||
* @return The list of polyominos
|
||||
* Generates the list of all one-sided polyominoes of the specified size
|
||||
* @return The list of polyominoes
|
||||
*/
|
||||
std::vector<Polyomino> generatePolyominos(int polyominoSize);
|
||||
std::vector<Polyomino> generatePolyominoes(int polyominoSize);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Generates all one-sided polyominos of the specified size using the current tested shape
|
||||
* Generates all one-sided polyominoes of the specified size using the current tested shape
|
||||
*/
|
||||
void generate(int polyominoSize, int lastAddedPositionNumber, int nextAvaibleNumber, std::map<Position, int> candidatePositions);
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
Piece::Piece(const Polyomino& polyomino, Block blockType) :
|
||||
polyomino(polyomino),
|
||||
blockType(blockType) {
|
||||
|
||||
this->rotationState = NONE;
|
||||
}
|
||||
|
||||
void Piece::rotate(Rotation rotation) {
|
||||
@@ -21,9 +23,22 @@ void Piece::rotate(Rotation rotation) {
|
||||
this->polyomino.rotate180();
|
||||
if (rotation == COUNTERCLOCKWISE)
|
||||
this->polyomino.rotateCCW();
|
||||
|
||||
this->rotationState += rotation;
|
||||
}
|
||||
|
||||
std::set<Position> Piece::getPositions() const {
|
||||
void Piece::defaultRotation() {
|
||||
if (this->rotationState == CLOCKWISE)
|
||||
this->polyomino.rotateCCW();
|
||||
if (this->rotationState == DOUBLE)
|
||||
this->polyomino.rotate180();
|
||||
if (this->rotationState == COUNTERCLOCKWISE)
|
||||
this->polyomino.rotateCW();
|
||||
|
||||
this->rotationState = NONE;
|
||||
}
|
||||
|
||||
const std::set<Position>& Piece::getPositions() const {
|
||||
return this->polyomino.getPositions();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
*/
|
||||
class Piece {
|
||||
private:
|
||||
Polyomino polyomino; // a polyomino representing the piece, (0, 0) is downleft
|
||||
Block blockType; // the block type of the piece
|
||||
Polyomino polyomino; // a polyomino representing the piece, (0, 0) is downleft
|
||||
Block blockType; // the block type of the piece
|
||||
Rotation rotationState; // the current rotation of the piece
|
||||
|
||||
public:
|
||||
/**
|
||||
@@ -28,9 +29,14 @@ class Piece {
|
||||
void rotate(Rotation rotation);
|
||||
|
||||
/**
|
||||
* @return A copy of the list of positions of the piece
|
||||
* Rotates the piece to its default rotation
|
||||
*/
|
||||
std::set<Position> getPositions() const;
|
||||
void defaultRotation();
|
||||
|
||||
/**
|
||||
* @return The list of positions of the piece
|
||||
*/
|
||||
const std::set<Position>& getPositions() const;
|
||||
|
||||
/**
|
||||
* @return The length of the piece
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
|
||||
PiecesFiles::PiecesFiles() {
|
||||
}
|
||||
@@ -25,22 +27,35 @@ bool PiecesFiles::savePieces(int polyominoSize) const {
|
||||
}
|
||||
|
||||
Generator generator;
|
||||
std::vector<Polyomino> nMinos = generator.generatePolyominos(polyominoSize);
|
||||
std::vector<Polyomino> polyominoes = generator.generatePolyominoes(polyominoSize);
|
||||
|
||||
// sorting the polyominos is done after setting spawn position to ensure the order is always the same
|
||||
for (Polyomino& nMino : nMinos) {
|
||||
return this->savePieces(polyominoSize, polyominoes);
|
||||
}
|
||||
|
||||
bool PiecesFiles::savePieces(int polyominoSize, std::vector<Polyomino>& polyominoes) const {
|
||||
std::string filePath;
|
||||
if (!this->getFilePath(polyominoSize, filePath)) {
|
||||
return false;
|
||||
}
|
||||
std::ofstream piecesFile(filePath, std::ios::trunc | std::ios::binary);
|
||||
if (!piecesFile.good()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// sorting the polyominoes is done after setting spawn position to ensure the order is always the same
|
||||
for (Polyomino& nMino : polyominoes) {
|
||||
nMino.goToSpawnPosition();
|
||||
}
|
||||
std::sort(nMinos.begin(), nMinos.end());
|
||||
std::sort(polyominoes.begin(), polyominoes.end());
|
||||
|
||||
for (const Polyomino& nMino : nMinos) {
|
||||
for (const Polyomino& polyomino : polyominoes) {
|
||||
// write the characteristics of the piece
|
||||
char infoByte = (nMino.isConvex() << 7) + (nMino.hasHole() << 6) + nMino.getLength();
|
||||
char infoByte = (polyomino.isConvex() << 7) + (polyomino.hasHole() << 6) + polyomino.getLength();
|
||||
piecesFile.write(&infoByte, 1);
|
||||
|
||||
// write the positions of the piece
|
||||
char positionByte;
|
||||
for (Position position : nMino.getPositions()) {
|
||||
for (const Position position : polyomino.getPositions()) {
|
||||
positionByte = (position.x << 4) + position.y;
|
||||
piecesFile.write(&positionByte, 1);
|
||||
}
|
||||
@@ -64,6 +79,7 @@ bool PiecesFiles::loadPieces(int polyominoSize, std::vector<Piece>& pieces, std:
|
||||
holelessPieces.clear();
|
||||
otherPieces.clear();
|
||||
|
||||
// we shift the first color of each size so that the small polyominoes (size 1-2-3) don't all have the same color
|
||||
Block pieceBlock = firstPieceBlockType();
|
||||
for (int i = 0; i < polyominoSize; i++) {
|
||||
nextPieceBlockType(pieceBlock);
|
||||
@@ -90,7 +106,7 @@ bool PiecesFiles::loadPieces(int polyominoSize, std::vector<Piece>& pieces, std:
|
||||
char positionByte;
|
||||
for (int i = 0; i < polyominoSize; i++) {
|
||||
piecesFile.get(positionByte);
|
||||
int x = (positionByte & xMask) >> 4;
|
||||
int x = ((unsigned char) positionByte & xMask) >> 4;
|
||||
int y = positionByte & yMask;
|
||||
piecePositions.insert(Position{x, y});
|
||||
}
|
||||
@@ -118,7 +134,12 @@ bool PiecesFiles::loadPieces(int polyominoSize, std::vector<Piece>& pieces, std:
|
||||
|
||||
bool PiecesFiles::getFilePath(int polyominoSize, std::string& filePath) const {
|
||||
std::string dataFolderPath = "data/pieces/";
|
||||
if (!std::filesystem::is_directory(dataFolderPath)) {
|
||||
|
||||
if (!fs::exists(dataFolderPath)) {
|
||||
fs::create_directories(dataFolderPath);
|
||||
}
|
||||
|
||||
if (!fs::is_directory(dataFolderPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,13 +22,18 @@ class PiecesFiles {
|
||||
*/
|
||||
bool savePieces(int polyominoSize) const;
|
||||
|
||||
/**
|
||||
* Generate a file containing all the pieces of the specified size, assuming they have been correctly generated
|
||||
* @return If the file could be created
|
||||
*/
|
||||
bool savePieces(int polyominoSize, std::vector<Polyomino>& polyominoes) const;
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
* @return If the data folder was found
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
#include <iostream>
|
||||
#include <climits>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
|
||||
Polyomino::Polyomino(const std::set<Position>& positions) {
|
||||
// find min/max
|
||||
int minX = INT_MAX;
|
||||
int maxX = INT_MIN;
|
||||
int minY = INT_MAX;
|
||||
@@ -22,15 +22,14 @@ Polyomino::Polyomino(const std::set<Position>& positions) {
|
||||
if (position.y > maxY) maxY = position.y;
|
||||
}
|
||||
|
||||
// normalize
|
||||
this->length = std::max(maxX - minX + 1, maxY - minY + 1);
|
||||
|
||||
// we normalize here instead of calling this->normalize() to reduce the number of calculations for the generation algorithm
|
||||
std::set<Position> newPositions;
|
||||
for (Position position : positions) {
|
||||
newPositions.insert(Position{position.x - minX, position.y - minY});
|
||||
}
|
||||
this->positions = std::move(newPositions);
|
||||
|
||||
// set polyomino length
|
||||
this->length = std::max(maxX - minX + 1, maxY - minY + 1);
|
||||
}
|
||||
|
||||
Polyomino::Polyomino(const std::set<Position>& positions, int length) :
|
||||
@@ -39,7 +38,6 @@ Polyomino::Polyomino(const std::set<Position>& positions, int length) :
|
||||
}
|
||||
|
||||
void Polyomino::normalize() {
|
||||
// find min values
|
||||
int minX = INT_MAX;
|
||||
int minY = INT_MAX;
|
||||
for (Position position : this->positions) {
|
||||
@@ -47,7 +45,6 @@ void Polyomino::normalize() {
|
||||
if (position.y < minY) minY = position.y;
|
||||
}
|
||||
|
||||
// translate the polyomino to the lowest unsigned values
|
||||
std::set<Position> newPositions;
|
||||
for (Position position : this->positions) {
|
||||
newPositions.insert(Position{position.x - minX, position.y - minY});
|
||||
@@ -82,6 +79,7 @@ void Polyomino::rotateCCW() {
|
||||
void Polyomino::goToSpawnPosition() {
|
||||
// initialize array
|
||||
std::vector<std::vector<int>> linesCompleteness;
|
||||
linesCompleteness.reserve(4);
|
||||
std::vector<int> empty;
|
||||
for (int j = 0; j < this->length; j++) {
|
||||
empty.push_back(0);
|
||||
@@ -98,15 +96,15 @@ void Polyomino::goToSpawnPosition() {
|
||||
linesCompleteness.at(3).at(position.x) += 1; // 3 = left to right = CCW
|
||||
}
|
||||
|
||||
// count empty lines
|
||||
// count empty lines and push the non-empty lines to the start of each vector
|
||||
int horizontalEmptyLines = 0;
|
||||
int verticalEmptyLines = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = this->length - 1; j >= 0; j--) {
|
||||
if (linesCompleteness.at(i).at(j) == 0) {
|
||||
// push the non-empty lines to the start of each vector
|
||||
linesCompleteness.at(i).erase(linesCompleteness.at(i).begin() + j);
|
||||
linesCompleteness.at(i).push_back(0);
|
||||
|
||||
if (i == 0) horizontalEmptyLines++;
|
||||
if (i == 1) verticalEmptyLines++;
|
||||
}
|
||||
@@ -124,11 +122,11 @@ void Polyomino::goToSpawnPosition() {
|
||||
currentFlattestSides[3] = false;
|
||||
}
|
||||
|
||||
// checks for the flattest sides
|
||||
// checks for the flattest side
|
||||
int sideToBeOn = -1;
|
||||
this->checkForFlattestSide(linesCompleteness, currentFlattestSides, sideToBeOn, false);
|
||||
|
||||
// if all sides are as flat, checks for the most left-biased side
|
||||
// if ther's no winner, checks for the side which has the flattest side to its left
|
||||
if (sideToBeOn == -1) {
|
||||
this->checkForFlattestSide(linesCompleteness, currentFlattestSides, sideToBeOn, true);
|
||||
}
|
||||
@@ -148,12 +146,16 @@ void Polyomino::goToSpawnPosition() {
|
||||
sideToBeOn = candidateSides.at(std::distance(candidateRotations.begin(), std::min_element(candidateRotations.begin(), candidateRotations.end())));
|
||||
}
|
||||
|
||||
// do the correct rotation
|
||||
if (sideToBeOn == 1) this->rotateCW();
|
||||
if (sideToBeOn == 2) this->rotate180();
|
||||
if (sideToBeOn == 3) this->rotateCCW();
|
||||
switch (sideToBeOn) {
|
||||
case 1 : {this->rotateCW(); break;}
|
||||
case 2 : {this->rotate180(); break;}
|
||||
case 3 : {this->rotateCCW(); break;}
|
||||
default: break;
|
||||
}
|
||||
if (sideToBeOn % 2 == 1) {
|
||||
std::swap(verticalEmptyLines, horizontalEmptyLines);
|
||||
}
|
||||
|
||||
// find min
|
||||
int minX = INT_MAX;
|
||||
int minY = INT_MAX;
|
||||
for (Position position : this->positions) {
|
||||
@@ -161,10 +163,7 @@ void Polyomino::goToSpawnPosition() {
|
||||
if (position.y < minY) minY = position.y;
|
||||
}
|
||||
|
||||
// center the piece with an up bias if it is assymetric
|
||||
if (sideToBeOn % 2 == 1) {
|
||||
std::swap(verticalEmptyLines, horizontalEmptyLines);
|
||||
}
|
||||
// center the piece with an up bias
|
||||
std::set<Position> newPositions;
|
||||
for (Position position : positions) {
|
||||
newPositions.insert(Position{(position.x - minX) + (verticalEmptyLines / 2), (position.y - minY) + ((horizontalEmptyLines + 1) / 2)});
|
||||
@@ -174,19 +173,17 @@ void Polyomino::goToSpawnPosition() {
|
||||
|
||||
void Polyomino::checkForFlattestSide(const std::vector<std::vector<int>>& linesCompleteness, bool currentFlattestSides[4], int& sideToBeOn, bool checkLeftSide) const {
|
||||
for (int j = 0; j < this->length; j++) {
|
||||
// we check which sides are the flattest on this line
|
||||
int max = 0;
|
||||
std::set<int> maxOwners;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
// only the candidate sides are compared
|
||||
if (!currentFlattestSides[i]) continue;
|
||||
|
||||
// if we need to check the flatness of the side to the left
|
||||
int sideToCheck = i;
|
||||
if (checkLeftSide) {
|
||||
sideToCheck = (i + 3) % 4;
|
||||
}
|
||||
|
||||
// we check which sides are the flattest
|
||||
if (linesCompleteness.at(sideToCheck).at(j) > max) {
|
||||
max = linesCompleteness.at(sideToCheck).at(j);
|
||||
maxOwners.clear();
|
||||
@@ -203,7 +200,7 @@ void Polyomino::checkForFlattestSide(const std::vector<std::vector<int>>& linesC
|
||||
return;
|
||||
}
|
||||
|
||||
// else we only keep the flattest from this round and ignore the others
|
||||
// else we only keep the flattest on this line and ignore the others
|
||||
else {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
currentFlattestSides[i] = currentFlattestSides[i] && maxOwners.contains(i);
|
||||
@@ -213,14 +210,12 @@ void Polyomino::checkForFlattestSide(const std::vector<std::vector<int>>& linesC
|
||||
}
|
||||
|
||||
bool Polyomino::isConvex() const {
|
||||
// for each line and column we check if every squares are adjacent to each others
|
||||
for (int j = 0; j < this->length; j++) {
|
||||
bool startedLine = false;
|
||||
bool completedLine = false;
|
||||
bool startedColumn = false;
|
||||
bool completedColumn = false;
|
||||
for (int i = 0; i < this->length; i++) {
|
||||
// line check
|
||||
if (this->positions.contains(Position{i, j})) {
|
||||
if (completedLine) return false;
|
||||
else startedLine = true;
|
||||
@@ -228,7 +223,7 @@ bool Polyomino::isConvex() const {
|
||||
else {
|
||||
if (startedLine) completedLine = true;
|
||||
}
|
||||
// column check
|
||||
|
||||
if (this->positions.contains(Position{j, i})) {
|
||||
if (completedColumn) return false;
|
||||
else startedColumn = true;
|
||||
@@ -267,7 +262,7 @@ void Polyomino::tryToInsertPosition(std::set<Position>& emptyPositions, const Po
|
||||
tryToInsertPosition(emptyPositions, Position{candidate.x - 1, candidate.y});
|
||||
}
|
||||
|
||||
std::set<Position> Polyomino::getPositions() const {
|
||||
const std::set<Position>& Polyomino::getPositions() const {
|
||||
return this->positions;
|
||||
}
|
||||
|
||||
@@ -282,7 +277,6 @@ int Polyomino::getPolyominoSize() const {
|
||||
bool Polyomino::operator<(const Polyomino& other) const {
|
||||
if (this->length != other.length) return this->length < other.length;
|
||||
|
||||
// we check for all positions from left to right and top to bottom, until one has a square that the other doesn't
|
||||
for (int y = this->length - 1; y >= 0; y--) {
|
||||
for (int x = 0; x < this->length; x++) {
|
||||
bool hasThisPosition = this->positions.contains(Position{x, y});
|
||||
@@ -293,7 +287,7 @@ bool Polyomino::operator<(const Polyomino& other) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Polyomino::operator ==(const Polyomino& other) const {
|
||||
bool Polyomino::operator==(const Polyomino& other) const {
|
||||
return this->positions == other.positions;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,9 +78,9 @@ class Polyomino {
|
||||
|
||||
public:
|
||||
/**
|
||||
* @return A copy of the positions of the polyomino
|
||||
* @return The positions of the polyomino
|
||||
*/
|
||||
std::set<Position> getPositions() const;
|
||||
const std::set<Position>& getPositions() const;
|
||||
|
||||
/**
|
||||
* @return The length of the polyomino
|
||||
@@ -100,10 +100,10 @@ class Polyomino {
|
||||
bool operator<(const Polyomino& other) const;
|
||||
|
||||
/**
|
||||
* Equality operator, two polyominos are equal if their positions are the same, that means two polyominos of the same shape at different places will not be equal
|
||||
* Equality operator, two polyominoes are equal if their positions are the same, that means two polyominoes of the same shape at different places will not be equal
|
||||
* @return If the polyomino is equal to another
|
||||
*/
|
||||
bool operator ==(const Polyomino& other) const;
|
||||
bool operator==(const Polyomino& other) const;
|
||||
|
||||
/**
|
||||
* Stream output operator, adds a 2D grid representing the polyomino
|
||||
|
||||
@@ -62,6 +62,14 @@ inline bool operator==(const Position& left, const Position& right) {
|
||||
return (left.x == right.x) && (left.y == right.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inequality operator, two positions aren't equal if their coordinates aren't
|
||||
* @return If the two positions aren't equals
|
||||
*/
|
||||
inline bool operator!=(const Position& left, const Position& right) {
|
||||
return (left.x != right.x) || (left.y != right.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream output operator, adds the coordinates of the position to the stream
|
||||
* @return A reference to the output stream
|
||||
|
||||
@@ -17,7 +17,7 @@ enum Rotation {
|
||||
* @return A rotation corresponding to doing both rotations
|
||||
*/
|
||||
inline Rotation operator+(const Rotation& left, const Rotation& right) {
|
||||
return Rotation((left + right) % 4);
|
||||
return Rotation(((int) left + (int) right) % 4);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
344
src/TextUI/TextApp.cpp
Normal 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
@@ -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;
|
||||
};
|
||||
183
src/TextUI/main.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
#include "../Pieces/Generator.h"
|
||||
#include "../Pieces/PiecesFiles.h"
|
||||
#include "TextApp.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <cmath>
|
||||
|
||||
static const int MAXIMUM_PIECES_SIZE = 10;
|
||||
static const int BENCHMARK_PIECES_SIZE = 15;
|
||||
|
||||
|
||||
void testGeneratorForAllSizes(int max_size);
|
||||
void testGeneratorForOneSize(int size);
|
||||
void printPiecesByTypesForOneSize(int size);
|
||||
void readStatsFromFilesForAllSizes(int max_size);
|
||||
|
||||
void benchmarking(int min_size, int max_size);
|
||||
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
std::srand(std::time(NULL));
|
||||
|
||||
#ifdef BENCHMARK
|
||||
benchmarking(1, BENCHMARK_PIECES_SIZE);
|
||||
#else
|
||||
// dev: generate files if it hasn't been done before, UI will NOT generate the files
|
||||
//generateFilesForAllSizes(10);
|
||||
|
||||
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 << "INFO: Pieces files for size " << i << " not found, generating..." << std::endl;
|
||||
pf.savePieces(i);
|
||||
}
|
||||
}
|
||||
|
||||
TextApp UI;
|
||||
UI.run();
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void testGeneratorForAllSizes(int max_size) {
|
||||
using std::chrono::high_resolution_clock;
|
||||
using std::chrono::duration_cast;
|
||||
using std::chrono::duration;
|
||||
using std::chrono::milliseconds;
|
||||
Generator generator;
|
||||
|
||||
for (int i = 1; i <= max_size; i++) {
|
||||
auto t1 = high_resolution_clock::now();
|
||||
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() << " polyominoes of size " << i << " in " << ms_double.count() << "ms" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void testGeneratorForOneSize(int size) {
|
||||
using std::chrono::high_resolution_clock;
|
||||
using std::chrono::duration_cast;
|
||||
using std::chrono::duration;
|
||||
using std::chrono::milliseconds;
|
||||
Generator generator;
|
||||
|
||||
std::cout << "Generating " << size << "-minos" << std::endl;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
auto t1 = high_resolution_clock::now();
|
||||
std::vector<Polyomino> n_minos = generator.generatePolyominoes(size);
|
||||
auto t2 = high_resolution_clock::now();
|
||||
|
||||
duration<double, std::milli> ms_double = t2 - t1;
|
||||
std::cout << ms_double.count() << "ms" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void printPiecesByTypesForOneSize(int size) {
|
||||
PiecesFiles piecesFiles;
|
||||
|
||||
std::vector<Piece> pieces;
|
||||
std::vector<int> convexPieces;
|
||||
std::vector<int> holelessPieces;
|
||||
std::vector<int> otherPieces;
|
||||
piecesFiles.loadPieces(size, pieces, convexPieces, holelessPieces, otherPieces);
|
||||
|
||||
std::cout << "Convex " << size << "-minos:" << std::endl;
|
||||
for (int index : convexPieces) {
|
||||
std::cout << pieces.at(index);
|
||||
}
|
||||
|
||||
std::cout << "Holeless " << size << "-minos:" << std::endl;
|
||||
for (int index : holelessPieces) {
|
||||
std::cout << pieces.at(index);
|
||||
}
|
||||
|
||||
std::cout << "Others " << size << "-minos:" << std::endl;
|
||||
for (int index : otherPieces) {
|
||||
std::cout << pieces.at(index);
|
||||
}
|
||||
}
|
||||
|
||||
void readStatsFromFilesForAllSizes(int max_size) {
|
||||
PiecesFiles piecesFiles;
|
||||
for (int i = 1; i <= max_size; i++) {
|
||||
std::vector<Piece> pieces;
|
||||
std::vector<int> convexPieces;
|
||||
std::vector<int> holelessPieces;
|
||||
std::vector<int> otherPieces;
|
||||
piecesFiles.loadPieces(i, pieces, convexPieces, holelessPieces, otherPieces);
|
||||
|
||||
std::cout << i << "-minos : " << pieces.size() << std::endl;
|
||||
std::cout << "Convex " << i << "-minos : " << convexPieces.size() << std::endl;
|
||||
std::cout << "Holeless " << i << "-minos : " << holelessPieces.size() << std::endl;
|
||||
std::cout << "Others " << i << "-minos : " << otherPieces.size() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void benchmarking(int min_size, int max_size) {
|
||||
using std::chrono::high_resolution_clock;
|
||||
using std::chrono::duration_cast;
|
||||
using std::chrono::duration;
|
||||
using std::chrono::milliseconds;
|
||||
|
||||
std::cout << "| n | Number | Generation | File storing | File retrieving | File size |" << std::endl;
|
||||
std::cout << "| - | - | - | - | - | - |" << std::endl;
|
||||
|
||||
Generator gen;
|
||||
PiecesFiles pf;
|
||||
|
||||
for (int i = min_size; i <= max_size; i++) {
|
||||
std::cout << "| " << i;
|
||||
|
||||
auto t1 = high_resolution_clock::now();
|
||||
std::vector<Polyomino> polyominoes = gen.generatePolyominoes(i);
|
||||
auto t2 = high_resolution_clock::now();
|
||||
duration<double, std::milli> ms_double = t2 - t1;
|
||||
std::cout << " | " << polyominoes.size();
|
||||
std::flush(std::cout);
|
||||
std::cout << " | " << (int) ms_double.count() / 1000 << "s " << std::fmod(ms_double.count(), 1000) << "ms ";
|
||||
std::flush(std::cout);
|
||||
|
||||
t1 = high_resolution_clock::now();
|
||||
pf.savePieces(i, polyominoes);
|
||||
t2 = high_resolution_clock::now();
|
||||
ms_double = t2 - t1;
|
||||
std::cout << " | " << (int) ms_double.count() / 1000 << "s " << std::fmod(ms_double.count(), 1000) << "ms ";
|
||||
std::flush(std::cout);
|
||||
|
||||
polyominoes.clear();
|
||||
polyominoes.shrink_to_fit();
|
||||
|
||||
std::vector<Piece> pieces;
|
||||
std::vector<int> convexPieces;
|
||||
std::vector<int> holelessPieces;
|
||||
std::vector<int> otherPieces;
|
||||
|
||||
t1 = high_resolution_clock::now();
|
||||
pf.loadPieces(i, pieces, convexPieces, holelessPieces, otherPieces);
|
||||
t2 = high_resolution_clock::now();
|
||||
ms_double = t2 - t1;
|
||||
std::cout << " | " << (int) ms_double.count() / 1000 << "s " << std::fmod(ms_double.count(), 1000) << "ms ";
|
||||
std::flush(std::cout);
|
||||
|
||||
pieces.clear();
|
||||
pieces.shrink_to_fit();
|
||||
convexPieces.clear();
|
||||
convexPieces.shrink_to_fit();
|
||||
holelessPieces.clear();
|
||||
holelessPieces.shrink_to_fit();
|
||||
otherPieces.clear();
|
||||
otherPieces.shrink_to_fit();
|
||||
|
||||
std::string filePath;
|
||||
pf.getFilePath(i, filePath);
|
||||
int fileSize = std::filesystem::file_size(filePath);
|
||||
std::cout << " | " << fileSize << " bytes |" << std::endl;
|
||||
std::flush(std::cout);
|
||||
}
|
||||
}
|
||||
97
src/Utils/AssetManager.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#include "./AssetManager.h"
|
||||
|
||||
#include <map>
|
||||
|
||||
static const unsigned char data_fonts_pressstart_prstartk_ttf[] = {
|
||||
#include <data/fonts/pressstart/prstartk.ttf.h>
|
||||
};
|
||||
|
||||
static const unsigned char data_fonts_pressstart_prstart_ttf[] = {
|
||||
#include <data/fonts/pressstart/prstart.ttf.h>
|
||||
};
|
||||
|
||||
static const unsigned char data_images_keybinds_Rotate0_png[] = {
|
||||
#include <data/images/keybinds/Rotate0.png.h>
|
||||
};
|
||||
|
||||
static const unsigned char data_images_keybinds_Moveright_png[] = {
|
||||
#include <data/images/keybinds/Moveright.png.h>
|
||||
};
|
||||
|
||||
static const unsigned char data_images_keybinds_RotateCW_png[] = {
|
||||
#include <data/images/keybinds/RotateCW.png.h>
|
||||
};
|
||||
|
||||
static const unsigned char data_images_keybinds_Pause_png[] = {
|
||||
#include <data/images/keybinds/Pause.png.h>
|
||||
};
|
||||
|
||||
static const unsigned char data_images_keybinds_Hold_png[] = {
|
||||
#include <data/images/keybinds/Hold.png.h>
|
||||
};
|
||||
|
||||
static const unsigned char data_images_keybinds_Softdrop_png[] = {
|
||||
#include <data/images/keybinds/Softdrop.png.h>
|
||||
};
|
||||
|
||||
static const unsigned char data_images_keybinds_RotateCCW_png[] = {
|
||||
#include <data/images/keybinds/RotateCCW.png.h>
|
||||
};
|
||||
|
||||
static const unsigned char data_images_keybinds_Moveleft_png[] = {
|
||||
#include <data/images/keybinds/Moveleft.png.h>
|
||||
};
|
||||
|
||||
static const unsigned char data_images_keybinds_Rotate180_png[] = {
|
||||
#include <data/images/keybinds/Rotate180.png.h>
|
||||
};
|
||||
|
||||
static const unsigned char data_images_keybinds_Retry_png[] = {
|
||||
#include <data/images/keybinds/Retry.png.h>
|
||||
};
|
||||
|
||||
static const unsigned char data_images_keybinds_Harddrop_png[] = {
|
||||
#include <data/images/keybinds/Harddrop.png.h>
|
||||
};
|
||||
|
||||
static const Asset assets[] = {
|
||||
{data_fonts_pressstart_prstartk_ttf, sizeof(data_fonts_pressstart_prstartk_ttf)},
|
||||
{data_fonts_pressstart_prstart_ttf, sizeof(data_fonts_pressstart_prstart_ttf)},
|
||||
{data_images_keybinds_Rotate0_png, sizeof(data_images_keybinds_Rotate0_png)},
|
||||
{data_images_keybinds_Moveright_png, sizeof(data_images_keybinds_Moveright_png)},
|
||||
{data_images_keybinds_RotateCW_png, sizeof(data_images_keybinds_RotateCW_png)},
|
||||
{data_images_keybinds_Pause_png, sizeof(data_images_keybinds_Pause_png)},
|
||||
{data_images_keybinds_Hold_png, sizeof(data_images_keybinds_Hold_png)},
|
||||
{data_images_keybinds_Softdrop_png, sizeof(data_images_keybinds_Softdrop_png)},
|
||||
{data_images_keybinds_RotateCCW_png, sizeof(data_images_keybinds_RotateCCW_png)},
|
||||
{data_images_keybinds_Moveleft_png, sizeof(data_images_keybinds_Moveleft_png)},
|
||||
{data_images_keybinds_Rotate180_png, sizeof(data_images_keybinds_Rotate180_png)},
|
||||
{data_images_keybinds_Retry_png, sizeof(data_images_keybinds_Retry_png)},
|
||||
{data_images_keybinds_Harddrop_png, sizeof(data_images_keybinds_Harddrop_png)},
|
||||
|
||||
};
|
||||
|
||||
static const std::map<std::string, AssetName> assetMap = {
|
||||
{"data/fonts/pressstart/prstartk.ttf", AssetName::data_fonts_pressstart_prstartk_ttf},
|
||||
{"data/fonts/pressstart/prstart.ttf", AssetName::data_fonts_pressstart_prstart_ttf},
|
||||
{"data/images/keybinds/Rotate0.png", AssetName::data_images_keybinds_Rotate0_png},
|
||||
{"data/images/keybinds/Moveright.png", AssetName::data_images_keybinds_Moveright_png},
|
||||
{"data/images/keybinds/RotateCW.png", AssetName::data_images_keybinds_RotateCW_png},
|
||||
{"data/images/keybinds/Pause.png", AssetName::data_images_keybinds_Pause_png},
|
||||
{"data/images/keybinds/Hold.png", AssetName::data_images_keybinds_Hold_png},
|
||||
{"data/images/keybinds/Softdrop.png", AssetName::data_images_keybinds_Softdrop_png},
|
||||
{"data/images/keybinds/RotateCCW.png", AssetName::data_images_keybinds_RotateCCW_png},
|
||||
{"data/images/keybinds/Moveleft.png", AssetName::data_images_keybinds_Moveleft_png},
|
||||
{"data/images/keybinds/Rotate180.png", AssetName::data_images_keybinds_Rotate180_png},
|
||||
{"data/images/keybinds/Retry.png", AssetName::data_images_keybinds_Retry_png},
|
||||
{"data/images/keybinds/Harddrop.png", AssetName::data_images_keybinds_Harddrop_png},
|
||||
|
||||
};
|
||||
|
||||
const Asset& getResource(AssetName fileName) {
|
||||
return assets[static_cast<std::size_t>(fileName)];
|
||||
}
|
||||
|
||||
const Asset& getResource(const std::string& fileName) {
|
||||
return getResource(assetMap.at(fileName));
|
||||
}
|
||||