diff --git a/README.md b/README.md new file mode 100644 index 0000000..8514106 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# jminos + +## Manual build and run + +You need to install xmake and have a compiler with c++20 compatibility + +### Install the necessary dependencies + +``xrepo install sfml`` + +Package location (on Linux) for IntelliSense: +``home//.xmake/packages/**`` + +### Build the project + +``cd jminos`` + +``xmake`` + +If you need to change the toolchain (for example using gcc): +``xmake f --toolchain=gcc`` + +### Run the project + +Graphical version: +``xmake run graph`` + +Command line version: +``xmake run text`` diff --git a/data/config/keybinds/custom.bin b/data/config/keybinds/custom.bin new file mode 100644 index 0000000..e69de29 diff --git a/data/config/keybinds/default1.bin b/data/config/keybinds/default1.bin new file mode 100644 index 0000000..e69de29 diff --git a/data/config/keybinds/default2.bin b/data/config/keybinds/default2.bin new file mode 100644 index 0000000..e69de29 diff --git a/data/config/keybinds/default3.bin b/data/config/keybinds/default3.bin new file mode 100644 index 0000000..e69de29 diff --git a/data/config/keybinds/default4.bin b/data/config/keybinds/default4.bin new file mode 100644 index 0000000..e69de29 diff --git a/data/config/settings.bin b/data/config/settings.bin new file mode 100644 index 0000000..e69de29 diff --git a/data/fonts/arial.ttf b/data/fonts/arial.ttf new file mode 100644 index 0000000..ff0815c Binary files /dev/null and b/data/fonts/arial.ttf differ diff --git a/doc/pieces_storage.md b/doc/files_format.md similarity index 54% rename from doc/pieces_storage.md rename to doc/files_format.md index d1b8f74..5c5bcc7 100644 --- a/doc/pieces_storage.md +++ b/doc/files_format.md @@ -1,6 +1,6 @@ -# Pieces storage +# Files format -## What is stored +## Pieces If you don't know what a polyomino is, check [this other file](Pieces_representation.md#what-are-polyominoes). @@ -8,8 +8,6 @@ Generating polyominoes of size n is exponential in regard to n. Because of this, 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 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: @@ -18,3 +16,32 @@ Each piece is stored as follows: The current implementation only allows to generate polyominoes up to size 16, but can be upgraded by storing coordinates on 8 bits instead of 4. It has been currently choosen to use pieces only up to size 15 for this game. + +## Config + +When compiling a release version, default files will be used, theses will then be impacted by the user and used for their next sessions. + +### Keybinds + +The games has 4 keyboard configs by default that never changes, and a modifiable 5th one, but theses are all stored the same. + +Each keybinds files has the the following format: + +- The number of the action (converted from an Enum), stored with 1 byte +- The number of each binded keys (also converted from an Enum), stored with 1 byte +- A separator characters which is 0xFF + +_Repeat for every avaible actions._ + +### Settings + +The settings file has the following format: + +- The number of the chosen keybinds (from 0 to 4), stored with 1 byte +- The size multiplier of the window, 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 + - If custom distribution is set, store the proportion of each size (15x1 byte total) +- Every selected pieces, using 1 byte for the type of selection (once again converted from an Enum) and 1 byte for the actual value \ No newline at end of file diff --git a/doc/game_logic.md b/doc/game_logic.md index ccd0546..15ff687 100644 --- a/doc/game_logic.md +++ b/doc/game_logic.md @@ -26,6 +26,8 @@ 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. diff --git a/src/GraphicalUI/AppMenus/AppMenu.h b/src/GraphicalUI/AppMenus/AppMenu.h new file mode 100644 index 0000000..9435faa --- /dev/null +++ b/src/GraphicalUI/AppMenus/AppMenu.h @@ -0,0 +1,36 @@ +#pragma once + +#include "Settings.h" + +#include +#include +#include + + +class AppMenu { + protected: + std::shared_ptr> menuStack; + std::shared_ptr settings; + std::shared_ptr renderWindow; + + public: + AppMenu(std::shared_ptr> menuStack, std::shared_ptr settings, std::shared_ptr renderWindow) : + menuStack(menuStack), + settings(settings), + renderWindow(renderWindow) + { + + } + + virtual void computeFrame() = 0; + + virtual void drawFrame() const = 0; +}; + + +inline void changeVideoMode(sf::RenderWindow& window, const sf::VideoMode& videoMode) { + 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))); +} diff --git a/src/GraphicalUI/AppMenus/MainAppMenu.cpp b/src/GraphicalUI/AppMenus/MainAppMenu.cpp new file mode 100644 index 0000000..dab1493 --- /dev/null +++ b/src/GraphicalUI/AppMenus/MainAppMenu.cpp @@ -0,0 +1,21 @@ +#include "MainAppMenu.h" + +#include "AppMenu.h" + +#include +#include +#include + + +MainAppMenu::MainAppMenu(std::shared_ptr> menuStack, std::shared_ptr settings, std::shared_ptr renderWindow) : + AppMenu(menuStack, settings, renderWindow) { + +} + +void MainAppMenu::computeFrame() { + +} + +void MainAppMenu::drawFrame() const { + +} diff --git a/src/GraphicalUI/AppMenus/MainAppMenu.h b/src/GraphicalUI/AppMenus/MainAppMenu.h new file mode 100644 index 0000000..1f48733 --- /dev/null +++ b/src/GraphicalUI/AppMenus/MainAppMenu.h @@ -0,0 +1,17 @@ +#pragma once + +#include "AppMenu.h" + +#include +#include +#include + + +class MainAppMenu : public AppMenu { + public: + MainAppMenu(std::shared_ptr> menuStack, std::shared_ptr settings, std::shared_ptr renderWindow); + + void computeFrame(); + + void drawFrame() const; +}; diff --git a/src/GraphicalUI/GraphApp.cpp b/src/GraphicalUI/GraphApp.cpp new file mode 100644 index 0000000..f13d60f --- /dev/null +++ b/src/GraphicalUI/GraphApp.cpp @@ -0,0 +1,49 @@ +#include "GraphApp.h" + +#include "AppMenus/AppMenu.h" +#include "AppMenus/MainAppMenu.h" +#include "Settings.h" + +#include +#include +#include + +static const double TIME_BETWEEN_FRAMES = (1000.f / 60.f); + + +GraphApp::GraphApp() { + this->settings = std::make_shared(); + this->menuStack = std::make_shared>(); + this->window = std::make_shared(); +} + +void GraphApp::startApp() { + changeVideoMode(*this->window, this->settings->getVideoMode()); + this->menuStack->push(MainAppMenu(this->menuStack, this->settings, this->window)); + + bool quit = false; + double timeAtNextFrame = 0; + sf::Clock clock; + while (!quit) { + while (const std::optional event = this->window->pollEvent()) { + if (event->is()) { + quit = true; + } + } + + if (!quit) { + if (clock.getElapsedTime().asMilliseconds() > timeAtNextFrame) { + timeAtNextFrame += TIME_BETWEEN_FRAMES; + this->menuStack->top().computeFrame(); + + if (this->menuStack->empty()) { + quit = true; + } + else { + this->menuStack->top().drawFrame(); + } + } + } + } + window->close(); +} diff --git a/src/GraphicalUI/GraphApp.h b/src/GraphicalUI/GraphApp.h new file mode 100644 index 0000000..4b27c5e --- /dev/null +++ b/src/GraphicalUI/GraphApp.h @@ -0,0 +1,21 @@ +#pragma once + +#include "AppMenus/AppMenu.h" +#include "Settings.h" + +#include +#include +#include + + +class GraphApp { + private: + std::shared_ptr settings; + std::shared_ptr> menuStack; + std::shared_ptr window; + + public: + GraphApp(); + + void startApp(); +}; diff --git a/src/GraphicalUI/Keybinds.h b/src/GraphicalUI/Keybinds.h new file mode 100644 index 0000000..b29639e --- /dev/null +++ b/src/GraphicalUI/Keybinds.h @@ -0,0 +1,32 @@ +#pragma once + +#include "../Core/Action.h" + +#include +#include +#include + +using sfKey = sf::Keyboard::Key; + + +class Keybinds { + private: + std::map> keybinds; + + public: + Keybinds(); + + void loadKeybindsFromFile(); + + void saveKeybindsToFile() const; + + void createDefaultKeybindsFile() const; + + void addKey(Action action, sfKey key); + + void clearKeys(Action action); + + const std::vector& getActions(sfKey key) const; + + const std::vector& getKeybinds(Action action) const; +}; diff --git a/src/GraphicalUI/Settings.cpp b/src/GraphicalUI/Settings.cpp new file mode 100644 index 0000000..d97e222 --- /dev/null +++ b/src/GraphicalUI/Settings.cpp @@ -0,0 +1,87 @@ +#include "Settings.h" + +#include "../Core/Menu.h" +#include "Keybinds.h" + +#include + +static const int NUMBER_OF_KEYBINDS = 5; +static const int CUSTOMIZABLE_KEYBINDS = NUMBER_OF_KEYBINDS - 1; +static const sf::Vector2u BASE_WINDOW_SIZE = {80, 50}; +static const int WINDOW_SIZE_MULTIPLIERS[] = {4, 6, 9, 14, 20}; +static const int WINDOW_SIZE_LAST_MODE = (sizeof(WINDOW_SIZE_MULTIPLIERS) / sizeof(int)) - 1; + + +Settings::Settings() { + for (int i = 1; i <= 15; i++) { + this->menu.getPiecesList().loadPieces(i); + } + + this->loadSettingsFromFile(); +} + +void Settings::loadSettingsFromFile() { + this->menu.getPiecesList().unselectAll(); + this->menu.getPiecesList().selectAllPieces(4); + this->windowSizeMode = 2; +} + +void Settings::saveSettingsToFile() const { + +} + +void Settings::createDefaultSettingsFile() const { + +} + +bool Settings::selectNextKeybinds() { + if (this->chosenKeybinds < NUMBER_OF_KEYBINDS) { + this->chosenKeybinds++; + } +} + +bool Settings::selectPreviousKeybinds() { + if (this->chosenKeybinds > 0) { + this->chosenKeybinds--; + } +} + +bool Settings::canModifyCurrentKeybinds() const { + return (this->chosenKeybinds == CUSTOMIZABLE_KEYBINDS); +} + +bool Settings::widenWindow() { + if (this->windowSizeMode < WINDOW_SIZE_LAST_MODE) { + this->windowSizeMode++; + } +} + +bool Settings::shortenWindow() { + if (this->windowSizeMode > 0) { + this->windowSizeMode--; + } +} + +void Settings::setGamemode(Gamemode gamemode) { + this->gamemode = gamemode; +} + +Menu& Settings::getMenu() { + return this->menu; +} + +Keybinds& Settings::getKeybinds() { + return this->keybinds.at(this->chosenKeybinds); +} + +Gamemode Settings::getGamemode() const { + return this->gamemode; +} + +int Settings::getWindowSizeMultiplier() const { + return WINDOW_SIZE_MULTIPLIERS[this->windowSizeMode]; +} + +const sf::VideoMode& Settings::getVideoMode() const { + return sf::VideoMode(BASE_WINDOW_SIZE * (unsigned int) WINDOW_SIZE_MULTIPLIERS[this->windowSizeMode]); +} diff --git a/src/GraphicalUI/Settings.h b/src/GraphicalUI/Settings.h new file mode 100644 index 0000000..fe7e4fa --- /dev/null +++ b/src/GraphicalUI/Settings.h @@ -0,0 +1,48 @@ +#pragma once + +#include "../Core/Menu.h" +#include "Keybinds.h" + +#include +#include + + +class Settings { + private: + Menu menu; + std::vector keybinds; + int chosenKeybinds; + Gamemode gamemode; + int windowSizeMode; + + public: + Settings(); + + void loadSettingsFromFile(); + + void saveSettingsToFile() const; + + void createDefaultSettingsFile() const; + + bool selectNextKeybinds(); + + bool selectPreviousKeybinds(); + + bool canModifyCurrentKeybinds() const; + + void setGamemode(Gamemode gamemode); + + bool widenWindow(); + + bool shortenWindow(); + + Menu& getMenu(); + + Keybinds& getKeybinds(); + + Gamemode getGamemode() const; + + int getWindowSizeMultiplier() const; + + const sf::VideoMode& getVideoMode() const; +}; diff --git a/src/GraphicalUI/main.cpp b/src/GraphicalUI/main.cpp new file mode 100644 index 0000000..504edd7 --- /dev/null +++ b/src/GraphicalUI/main.cpp @@ -0,0 +1,138 @@ +#include +#include "../Core/Menu.h" +#include "../Pieces/PiecesFiles.h" +#include + +void setToDefaultConfig(); + +int main() { + std::srand(std::time(NULL)); + + sf::RenderWindow window(sf::VideoMode({800, 640}), "My window", sf::Style::Titlebar | sf::Style::Close); + window.setPosition(sf::Vector2i(sf::VideoMode::getDesktopMode().size.x / 2 - 400, sf::VideoMode::getDesktopMode().size.y / 2 - 320)); + + PiecesFiles pf; + for (int i = 1; i <= 10; i++) { + pf.savePieces(i); + } + + Menu m; + m.getPiecesList().loadPieces(10); + m.getPiecesList().selectAllPieces(4); + m.setBoardWidth(10); + m.getPlayerControls().setDAS(6); + m.getPlayerControls().setARR(0); + m.getPlayerControls().setSDR(0); + Game game = m.startGame(SPRINT); + game.start(); + + sf::Clock clock; + + sf::Font font; + if (!font.openFromFile("data/fonts/arial.ttf")) { + std::cout << "aaaaaaaaaaaaaa"; + } + sf::Text text(font); + text.setCharacterSize(20); + text.setFillColor(sf::Color::White); + + while (window.isOpen()) { + while (const std::optional event = window.pollEvent()) { + if (event->is()) + window.close(); + } + + if (clock.getElapsedTime().asMilliseconds() > 16) { + clock.restart(); + + window.clear(sf::Color::Black); + + std::set actions; + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Left)) { + actions.insert(MOVE_LEFT); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Right)) { + actions.insert(MOVE_RIGHT); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Up)) { + actions.insert(HARD_DROP); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Down)) { + actions.insert(SOFT_DROP); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::A)) { + actions.insert(ROTATE_CCW); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::E)) { + actions.insert(ROTATE_CW); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Z)) { + actions.insert(HOLD); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Tab)) { + actions.insert(ROTATE_0); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Num2)) { + actions.insert(ROTATE_180); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::R)) { + game.reset(); + game.start(); + } + game.nextFrame(actions); + + for (int y = game.getBoard().getBaseHeight() + 5; y >= 0; y--) { + for (int x = 0; x < game.getBoard().getWidth(); x++) { + bool isActivePieceHere = (game.getActivePiece() != nullptr) && (game.getActivePiece()->getPositions().contains(Position{x, y} - game.getActivePiecePosition())); + bool isGhostPieceHere = (game.getActivePiece() != nullptr) && (game.getActivePiece()->getPositions().contains(Position{x, y} - game.ghostPiecePosition())); + Block block = (isActivePieceHere || isGhostPieceHere) ? game.getActivePiece()->getBlockType() : game.getBoard().getBlock(Position{x, y}); + + sf::RectangleShape cell(sf::Vector2f(20.f, 20.f)); + cell.setFillColor(sf::Color(BLOCKS_COLOR[block].red, BLOCKS_COLOR[block].green, BLOCKS_COLOR[block].blue, (isGhostPieceHere && !isActivePieceHere) ? 150 : 255)); + cell.setPosition(sf::Vector2f(x*20, (game.getBoard().getBaseHeight() + 10 - y)*20)); + window.draw(cell); + } + } + + if (game.getNextPieces().size() > 0) { + for (int y = 10; y >= 0; y--) { + for (int x = 0; x <= 10; x++) { + Block block = game.getNextPieces().at(0).getBlockType(); + sf::RectangleShape cell(sf::Vector2f(20.f, 20.f)); + cell.setPosition(sf::Vector2f((x + 2 + game.getBoard().getWidth())*20, (game.getBoard().getBaseHeight() - y)*20)); + if (game.getNextPieces().at(0).getPositions().contains(Position({x, y}))) { + cell.setFillColor(sf::Color(BLOCKS_COLOR[block].red, BLOCKS_COLOR[block].green, BLOCKS_COLOR[block].blue)); + } + else { + cell.setFillColor(sf::Color(0, 0, 0)); + } + window.draw(cell); + } + } + } + + if (game.getHeldPiece() != nullptr) { + for (int y = 10; y >= 0; y--) { + for (int x = 0; x <= 10; x++) { + Block block = game.getHeldPiece()->getBlockType(); + sf::RectangleShape cell(sf::Vector2f(20.f, 20.f)); + cell.setPosition(sf::Vector2f((x + 12 + game.getBoard().getWidth())*20, (game.getBoard().getBaseHeight() - y)*20)); + if (game.getHeldPiece()->getPositions().contains(Position({x, y}))) { + cell.setFillColor(sf::Color(BLOCKS_COLOR[block].red, BLOCKS_COLOR[block].green, BLOCKS_COLOR[block].blue)); + } + else { + cell.setFillColor(sf::Color(0, 0, 0)); + } + window.draw(cell); + } + } + } + + text.setPosition(sf::Vector2f(12*20, (game.getBoard().getBaseHeight() - 5)*20)); + text.setString(sf::String(std::to_string(game.getClearedLines()))); + window.draw(text); + + window.display(); + } + } +} diff --git a/xmake.lua b/xmake.lua index 0f205d8..d889008 100644 --- a/xmake.lua +++ b/xmake.lua @@ -1,13 +1,20 @@ set_languages("c++20") +set_optimize("fastest") +set_rundir(".") +add_requires("sfml") -target("main") +target("text") set_kind("binary") - set_rundir(".") add_files("./src/Pieces/*.cpp") add_files("./src/Core/*.cpp") add_files("./src/TextUI/*.cpp") - set_optimize("fastest") +target("graph") + set_kind("binary") + add_files("./src/Pieces/*.cpp") + add_files("./src/Core/*.cpp") + add_files("./src/GraphicalUI/*.cpp") + add_packages("sfml") -- -- If you want to known more usage about xmake, please see https://xmake.io