diff --git a/Board.cpp b/Board.cpp new file mode 100644 index 0000000..9530b49 --- /dev/null +++ b/Board.cpp @@ -0,0 +1,524 @@ +// This is a personal academic project. Dear PVS-Studio, please check it. +// PVS-Studio Static Code Analyzer for C, C++, C#, and Java: https://pvs-studio.com +#include "Board.h" +#include +#include +#include +#include +#include +#include +#include +// IdleState +void IdleState::enter(Board& board) { /* ... */ } +GameStateBase* IdleState::update(Board& board) { + return nullptr; +} +void IdleState::exit(Board& board) { /* ... */ } +bool IdleState::isIdle() const { return true; } + +// GemSelectedState +void GemSelectedState::enter(Board& board) { /* ... */ } +GameStateBase* GemSelectedState::update(Board& board) { + return nullptr; +} +void GemSelectedState::exit(Board& board) { /* ... */ } +// SwappingState +void SwappingState::enter(Board& board) { /* ... */ } +GameStateBase* SwappingState::update(Board& board) { + std::set matchesAfterSwap = board.findMatches(); + if (matchesAfterSwap.empty()) { + board.swapGems(board.getSelectedGemCoords(), board.getSelectedGemCoords()); + return new IdleState(); + } + else { + return new CheckingState(); + } +} +void SwappingState::exit(Board& board) { /* ... */ } + +// CheckingState +void CheckingState::enter(Board& board) { /* ... */ } +GameStateBase* CheckingState::update(Board& board) { + std::set matches = board.findMatches(); + if (!matches.empty()) { + auto destructionResult = board.destroyGems(matches); + board.recentlyDestroyedGems = destructionResult.second; + return new DestroyingState(); + } + else { + return new IdleState(); + } +} +void CheckingState::exit(Board& board) { /* ... */ } + +// DestroyingState +void DestroyingState::enter(Board& board) { /* ... */ } +GameStateBase* DestroyingState::update(Board& board) { + return new ApplyingBonusState(); +} +void DestroyingState::exit(Board& board) { /* ... */ } + +// FallingState +void FallingState::enter(Board& board) { /* ... */ } +GameStateBase* FallingState::update(Board& board) { + if (board.applyGravity()) { + return new RefillingState(); + } + else { + return new CheckingState(); + } +} +void FallingState::exit(Board& board) { /* ... */ } + +// RefillingState +void RefillingState::enter(Board& board) { /* ... */ } +GameStateBase* RefillingState::update(Board& board) { + if (board.refillBoard()) { + return new CheckingState(); + } + else { + return new IdleState(); + } +} +void RefillingState::exit(Board& board) { /* ... */ } + +// ApplyingBonusState +void ApplyingBonusState::enter(Board& board) { /* ... */ } +GameStateBase* ApplyingBonusState::update(Board& board) { + board.trySpawnBonus(board.recentlyDestroyedGems); + board.recentlyDestroyedGems.clear(); + return new FallingState(); +} +void ApplyingBonusState::exit(Board& board) { /* ... */ } + + +Board::Board(int w, int h, sf::Texture& textureSheet) + : width(w), + height(h), + textureSheetRef(textureSheet), + selectedGemCoords(-1, -1), + gemSelected(false), + rng(static_cast(std::chrono::high_resolution_clock::now().time_since_epoch().count())), + gemColorDist(0, NUM_GEM_COLORS - 1), + bonusChanceDist(0.0f, 1.0f), + bonusTypeDist(0, 1), + gridXDist(0, w - 1), + gridYDist(0, h - 1), + currentState(std::make_unique()) +{ + grid.resize(height); + for (int r = 0; r < height; ++r) { + grid[r].resize(width); + } + + initializeBoard(); +} + +void Board::setState(std::unique_ptr newState) { + if (currentState) { + currentState->exit(*this); + } + currentState = std::move(newState); + if (currentState) { + currentState->enter(*this); + } +} + +void Board::initializeBoard() { + for (int r = 0; r < height; ++r) { + for (int c = 0; c < width; ++c) { + do { + createRandomGem(r, c); + } while ( + (c >= 2 && + grid[r][c]->getColor()->getIndex() == grid[r][c - 1]->getColor()->getIndex() && + grid[r][c]->getColor()->getIndex() == grid[r][c - 2]->getColor()->getIndex()) + || + (r >= 2 && + grid[r][c]->getColor()->getIndex() == grid[r - 1][c]->getColor()->getIndex() && + grid[r][c]->getColor()->getIndex() == grid[r - 2][c]->getColor()->getIndex()) + ); + } + } + setState(std::make_unique()); +} +void Board::createGem(int r, int c, std::unique_ptr color) { + if (isValidCoords(r, c)) { + grid[r][c] = std::make_unique(std::move(color), c, r, textureSheetRef); + } +} + +void Board::createRandomGem(int r, int c) { + if (isValidCoords(r, c)) { + int randomColorIndex = gemColorDist(rng); + std::unique_ptr randomColor; + switch (randomColorIndex) { + case 0: randomColor = std::make_unique(); break; + case 1: randomColor = std::make_unique(); break; + case 2: randomColor = std::make_unique(); break; + case 3: randomColor = std::make_unique(); break; + case 4: randomColor = std::make_unique(); break; + case 5: randomColor = std::make_unique(); break; + default: randomColor = std::make_unique(); break; + } + grid[r][c] = std::make_unique(std::move(randomColor), c, r, textureSheetRef); + grid[r][c]->setGridPosition(c, r); + } +} + + +void Board::draw(sf::RenderWindow& window) { + for (int r = 0; r < height; ++r) { + for (int c = 0; c < width; ++c) { + if (grid[r][c]) { + grid[r][c]->draw(window); + } + } + } +} + +bool Board::update() { + GameStateBase* nextState = currentState->update(*this); + if (nextState != nullptr) { + setState(std::unique_ptr(nextState)); + return true; + } + return false; +} + + +bool Board::handleMouseClick(sf::Vector2i pixelCoords) { + if (!isIdle() && !gemSelected) + return false; + + int gridX = pixelCoords.x / GEM_SIZE; + int gridY = pixelCoords.y / GEM_SIZE; + GridCoord clickedCoords(gridX, gridY); + + if (!isValidCoords(clickedCoords)) { + setState(std::make_unique()); + return false; + } + + if (!gemSelected) { + selectedGemCoords = clickedCoords; + gemSelected = true; + setState(std::make_unique()); + return true; + } + else { + GridCoord secondClickCoords = clickedCoords; + + if (secondClickCoords == selectedGemCoords) { + gemSelected = false; + setState(std::make_unique()); + return true; + } + + if (areAdjacent(selectedGemCoords, secondClickCoords)) { + swapGems(selectedGemCoords, secondClickCoords); + gemSelected = false; + setState(std::make_unique()); + return true; + } + else { + selectedGemCoords = secondClickCoords; + setState(std::make_unique()); + return true; + } + } +} + +bool Board::isIdle() const { + return currentState->isIdle(); +} + +bool Board::isValidCoords(int r, int c) const { + return r >= 0 && r < height && c >= 0 && c < width; +} + +bool Board::isValidCoords(GridCoord coords) const { + return isValidCoords(coords.y, coords.x); +} + +bool Board::areAdjacent(GridCoord p1, GridCoord p2) const { + int dx = std::abs(p1.x - p2.x); + int dy = std::abs(p1.y - p2.y); + return (dx == 1 && dy == 0) || (dx == 0 && dy == 1); +} + +void Board::swapGems(GridCoord p1, GridCoord p2) { + if (!isValidCoords(p1) || !isValidCoords(p2)) return; + + auto temp = std::move(grid[p1.y][p1.x]); + grid[p1.y][p1.x] = std::move(grid[p2.y][p2.x]); + grid[p2.y][p2.x] = std::move(temp); + + if (grid[p1.y][p1.x]) { + grid[p1.y][p1.x]->setGridPosition(p1.x, p1.y); + } + if (grid[p2.y][p2.x]) { + grid[p2.y][p2.x]->setGridPosition(p2.x, p2.y); + } +} + +std::set Board::findMatches() { + std::set matches; + for (int r = 0; r < height; ++r) { + int consecutive = 1; + const GemColorBase* currentColor = nullptr; + for (int c = 0; c < width; ++c) { + if (grid[r][c] && grid[r][c]->getColor() && grid[r][c]->getColor()->getIndex() == (currentColor ? currentColor->getIndex() : -2)) { + consecutive++; + } + else { + if (consecutive >= 3) { + for (int i = c - consecutive; i < c; ++i) { + matches.insert({ i, r }); + } + } + currentColor = grid[r][c] ? grid[r][c]->getColor() : nullptr; + consecutive = 1; + } + } + if (consecutive >= 3) { + for (int i = width - consecutive; i < width; ++i) { + matches.insert({ i, r }); + } + } + } + + for (int c = 0; c < width; ++c) { + int consecutive = 1; + const GemColorBase* currentColor = nullptr; + for (int r = 0; r < height; ++r) { + if (grid[r][c] && grid[r][c]->getColor() && grid[r][c]->getColor()->getIndex() == (currentColor ? currentColor->getIndex() : -2)) { + consecutive++; + } + else { + if (consecutive >= 3) { + for (int i = r - consecutive; i < r; ++i) { + matches.insert({ c, i }); + } + } + currentColor = grid[r][c] ? grid[r][c]->getColor() : nullptr; + consecutive = 1; + } + } + if (consecutive >= 3) { + for (int i = height - consecutive; i < height; ++i) { + matches.insert({ c, i }); + } + } + } + + return matches; +} + +std::pair>> Board::destroyGems(const std::set& coordsToDestroy) { + int count = 0; + std::vector> destroyedInfo; + + for (const auto& coord : coordsToDestroy) { + if (isValidCoords(coord) && grid[coord.y][coord.x]) { + destroyedInfo.push_back({ coord, grid[coord.y][coord.x]->getColor()->getIndex() }); + grid[coord.y][coord.x].reset(); + count++; + } + } + return { count, destroyedInfo }; +} + +bool Board::applyGravity() { + bool gemsFell = false; + for (int c = 0; c < width; ++c) { + int emptyRow = height - 1; + for (int r = height - 1; r >= 0; --r) { + if (grid[r][c]) { + if (r != emptyRow) { + grid[emptyRow][c] = std::move(grid[r][c]); + grid[emptyRow][c]->setGridPosition(c, emptyRow); + gemsFell = true; + } + emptyRow--; + } + } + while (emptyRow >= 0) { + grid[emptyRow][c].reset(); + emptyRow--; + } + } + return gemsFell; +} + +bool Board::refillBoard() { + bool newGemsAdded = false; + for (int c = 0; c < width; ++c) { + for (int r = 0; r < height; ++r) { + if (!grid[r][c]) { + createRandomGem(r, c); + grid[r][c]->setGridPosition(c, r); + newGemsAdded = true; + } + else { + break; + } + } + } + return newGemsAdded; +} + +void Board::trySpawnBonus(const std::vector>& destroyedGemsInfo) { + for (const auto& info : destroyedGemsInfo) { + if (bonusChanceDist(rng) <= BONUS_CHANCE) { + int bonusType = bonusTypeDist(rng); + GridCoord bonusOrigin = info.first; + int bonusOriginColorIndex = info.second; + int targetR, targetC; + int attempts = 0; + const int maxAttempts = 20; + do { + int dr = rng() % (2 * REPAINT_BONUS_RADIUS + 1) - REPAINT_BONUS_RADIUS; + int dc = rng() % (2 * REPAINT_BONUS_RADIUS + 1) - REPAINT_BONUS_RADIUS; + targetR = bonusOrigin.y + dr; + targetC = bonusOrigin.x + dc; + attempts++; + } while (!isValidCoords(targetR, targetC) && attempts < maxAttempts); + + if (isValidCoords(targetR, targetC)) { + GridCoord targetPos = { targetC, targetR }; + if (bonusType == 0) { + applyRepaintBonus(targetPos, bonusOriginColorIndex); + } + else { + applyBombBonus(targetPos); + } + } + } + } +} + +void Board::applyRepaintBonus(GridCoord targetPos, int sourceColorIndex) { + if (!isValidCoords(targetPos) || sourceColorIndex == -1) return; + + if (grid[targetPos.y][targetPos.x]) { + std::unique_ptr newColor; + switch (sourceColorIndex) { + case 0: newColor = std::make_unique(); break; + case 1: newColor = std::make_unique(); break; + case 2: newColor = std::make_unique(); break; + case 3: newColor = std::make_unique(); break; + case 4: newColor = std::make_unique(); break; + case 5: newColor = std::make_unique(); break; + default: newColor = std::make_unique(); break; + } + grid[targetPos.y][targetPos.x]->setColor(std::move(newColor)); + } + + std::vector candidates; + for (int dr = -REPAINT_BONUS_RADIUS; dr <= REPAINT_BONUS_RADIUS; ++dr) { + for (int dc = -REPAINT_BONUS_RADIUS; dc <= REPAINT_BONUS_RADIUS; ++dc) { + GridCoord candidatePos(targetPos.x + dc, targetPos.y + dr); + if (isValidCoords(candidatePos) && + grid[candidatePos.y][candidatePos.x] && + !areAdjacent(targetPos, candidatePos)) { + candidates.push_back(candidatePos); + } + } + } + + std::shuffle(candidates.begin(), candidates.end(), rng); + int count = 0; + for (const auto& pos : candidates) { + if (count >= 2) break; + std::unique_ptr newColor; + switch (sourceColorIndex) { + case 0: newColor = std::make_unique(); break; + case 1: newColor = std::make_unique(); break; + case 2: newColor = std::make_unique(); break; + case 3: newColor = std::make_unique(); break; + case 4: newColor = std::make_unique(); break; + case 5: newColor = std::make_unique(); break; + default: newColor = std::make_unique(); break; + } + grid[pos.y][pos.x]->setColor(std::move(newColor)); + count++; + } +} + +void Board::applyBombBonus(GridCoord targetPos) { + std::set gemsToDestroy; + + if (isValidCoords(targetPos) && grid[targetPos.y][targetPos.x]) { + gemsToDestroy.insert(targetPos); + } + + std::vector allGems; + for (int r = 0; r < height; ++r) { + for (int c = 0; c < width; ++c) { + if (grid[r][c] && !gemsToDestroy.count({ c, r })) { + allGems.push_back({ c, r }); + } + } + } + + std::shuffle(allGems.begin(), allGems.end(), rng); + int needed = BOMB_BONUS_COUNT - gemsToDestroy.size(); + for (int i = 0; i < needed && i < allGems.size(); ++i) { + gemsToDestroy.insert(allGems[i]); + } + + if (!gemsToDestroy.empty()) { + destroyGems(gemsToDestroy); + } +} + +bool Board::hasPossibleMoves() const { + for (int r = 0; r < height; ++r) { + for (int c = 0; c < width; ++c) { + if (!grid[r][c]) continue; + + const GemColorBase* current = grid[r][c]->getColor(); + if (!current || current->getIndex() == -1) continue; + if (c + 1 < width && grid[r][c + 1] && grid[r][c + 1]->getColor() && grid[r][c + 1]->getColor()->getIndex() != -1) { + const GemColorBase* right = grid[r][c + 1]->getColor(); + if (c + 2 < width && grid[r][c + 2] && grid[r][c + 2]->getColor() && grid[r][c + 2]->getColor()->getIndex() == current->getIndex()) return true; + if (r - 1 >= 0 && r + 1 < height && grid[r - 1][c + 1] && grid[r - 1][c + 1]->getColor() && grid[r - 1][c + 1]->getColor()->getIndex() == current->getIndex() && + grid[r + 1][c + 1] && grid[r + 1][c + 1]->getColor() && grid[r + 1][c + 1]->getColor()->getIndex() == current->getIndex()) return true; + if (r + 2 < height && grid[r + 1][c + 1] && grid[r + 1][c + 1]->getColor() && grid[r + 1][c + 1]->getColor()->getIndex() == current->getIndex() && + grid[r + 2][c + 1] && grid[r + 2][c + 1]->getColor() && grid[r + 2][c + 1]->getColor()->getIndex() == current->getIndex()) return true; + + if (r - 2 >= 0 && grid[r - 1][c + 1] && grid[r - 1][c + 1]->getColor() && grid[r - 1][c + 1]->getColor()->getIndex() == current->getIndex() && + grid[r - 2][c + 1] && grid[r - 2][c + 1]->getColor() && grid[r - 2][c + 1]->getColor()->getIndex() == current->getIndex()) return true; + if (c - 1 >= 0 && grid[r][c - 1] && grid[r][c - 1]->getColor() && grid[r][c - 1]->getColor()->getIndex() == right->getIndex()) return true; + if (r - 1 >= 0 && r + 1 < height && grid[r - 1][c] && grid[r - 1][c]->getColor() && grid[r - 1][c]->getColor()->getIndex() == right->getIndex() && + grid[r + 1][c] && grid[r + 1][c]->getColor() && grid[r + 1][c]->getColor()->getIndex() == right->getIndex()) return true; + if (r + 2 < height && grid[r + 1][c] && grid[r + 1][c]->getColor() && grid[r + 1][c]->getColor()->getIndex() == right->getIndex() && + grid[r + 2][c] && grid[r + 2][c]->getColor() && grid[r + 2][c]->getColor()->getIndex() == right->getIndex()) return true; + + if (r - 2 >= 0 && grid[r - 1][c] && grid[r - 1][c]->getColor() && grid[r - 1][c]->getColor()->getIndex() == right->getIndex() && + grid[r - 2][c] && grid[r - 2][c]->getColor() && grid[r - 2][c]->getColor()->getIndex() == right->getIndex()) return true; + } + if (r + 1 < height && grid[r + 1][c] && grid[r + 1][c]->getColor() && grid[r + 1][c]->getColor()->getIndex() != -1) { + const GemColorBase* down = grid[r + 1][c]->getColor(); + if (r + 2 < height && grid[r + 2][c] && grid[r + 2][c]->getColor() && grid[r + 2][c]->getColor()->getIndex() == current->getIndex()) return true; + if (c - 1 >= 0 && c + 1 < width && grid[r + 1][c - 1] && grid[r + 1][c - 1]->getColor() && grid[r + 1][c - 1]->getColor()->getIndex() == current->getIndex() && + grid[r + 1][c + 1] && grid[r + 1][c + 1]->getColor() && grid[r + 1][c + 1]->getColor()->getIndex() == current->getIndex()) return true; + if (c + 2 < width && grid[r + 1][c + 1] && grid[r + 1][c + 1]->getColor() && grid[r + 1][c + 1]->getColor()->getIndex() == current->getIndex() && + grid[r + 1][c + 2] && grid[r + 1][c + 2]->getColor() && grid[r + 1][c + 2]->getColor()->getIndex() == current->getIndex()) return true; + + if (c - 2 >= 0 && grid[r + 1][c - 1] && grid[r + 1][c - 1]->getColor() && grid[r + 1][c - 1]->getColor()->getIndex() == current->getIndex() && + grid[r + 1][c - 2] && grid[r + 1][c - 2]->getColor() && grid[r + 1][c - 2]->getColor()->getIndex() == current->getIndex()) return true; + if (r - 1 >= 0 && grid[r - 1][c] && grid[r - 1][c]->getColor() && grid[r - 1][c]->getColor()->getIndex() == down->getIndex()) return true; + if (c - 1 >= 0 && c + 1 < width && grid[r][c - 1] && grid[r][c - 1]->getColor() && grid[r][c - 1]->getColor()->getIndex() == down->getIndex() && + grid[r][c + 1] && grid[r][c + 1]->getColor() && grid[r][c + 1]->getColor()->getIndex() == down->getIndex()) return true; + if (c + 2 < width && grid[r][c + 1] && grid[r][c + 1]->getColor() && grid[r][c + 1]->getColor()->getIndex() == down->getIndex() && + grid[r][c + 2] && grid[r][c + 2]->getColor() && grid[r][c + 2]->getColor()->getIndex() == down->getIndex()) return true; + + if (c - 2 >= 0 && grid[r][c - 1] && grid[r][c - 1]->getColor() && grid[r][c - 1]->getColor()->getIndex() == down->getIndex() && + grid[r][c - 2] && grid[r][c - 2]->getColor() && grid[r][c - 2]->getColor()->getIndex() == down->getIndex()) return true; + } + } + } + return false; +} \ No newline at end of file diff --git a/Board.h b/Board.h new file mode 100644 index 0000000..571bfbf --- /dev/null +++ b/Board.h @@ -0,0 +1,131 @@ +#pragma once + +#include +#include +#include +#include +#include "Gem.h" +#include "Config.h" +#include +class IdleState : public GameStateBase { +public: + void enter(Board& board) override; + GameStateBase* update(Board& board) override; + void exit(Board& board) override; + bool isIdle() const override; +}; + +class GemSelectedState : public GameStateBase { +public: + void enter(Board& board) override; + GameStateBase* update(Board& board) override; + void exit(Board& board) override; +}; + +class SwappingState : public GameStateBase { +public: + void enter(Board& board) override; + GameStateBase* update(Board& board) override; + void exit(Board& board) override; +}; + +class CheckingState : public GameStateBase { +public: + void enter(Board& board) override; + GameStateBase* update(Board& board) override; + void exit(Board& board) override; +}; + +class DestroyingState : public GameStateBase { +public: + void enter(Board& board) override; + GameStateBase* update(Board& board) override; + void exit(Board& board) override; +}; + +class FallingState : public GameStateBase { +public: + void enter(Board& board) override; + GameStateBase* update(Board& board) override; + void exit(Board& board) override; +}; + +class RefillingState : public GameStateBase { +public: + void enter(Board& board) override; + GameStateBase* update(Board& board) override; + void exit(Board& board) override; +}; + +class ApplyingBonusState : public GameStateBase { +public: + void enter(Board& board) override; + GameStateBase* update(Board& board) override; + void exit(Board& board) override; +}; + + +class Board { +public: + Board(int width, int height, sf::Texture& textureSheet); + Board(const Board&) = delete; + Board& operator=(const Board&) = delete; + + void draw(sf::RenderWindow& window); + bool update(); + bool handleMouseClick(sf::Vector2i pixelCoords); + bool isIdle() const; + void setState(std::unique_ptr newState); + GridCoord getSelectedGemCoords() const { return selectedGemCoords; } + void setSelectedGemCoords(GridCoord coords) { selectedGemCoords = coords; } + bool isGemSelected() const { return gemSelected; } + void setGemSelected(bool selected) { gemSelected = selected; } + + bool isValidCoords(int r, int c) const; + bool isValidCoords(GridCoord coords) const; + bool areAdjacent(GridCoord p1, GridCoord p2) const; + void swapGems(GridCoord p1, GridCoord p2); + std::set findMatches(); + std::pair>> destroyGems(const std::set& coordsToDestroy); + bool applyGravity(); + bool refillBoard(); + void trySpawnBonus(const std::vector>& destroyedGemsInfo); + void applyRepaintBonus(GridCoord targetPos, int sourceColorIndex); + void applyBombBonus(GridCoord targetPos); + void createGem(int r, int c, std::unique_ptr color); + void createRandomGem(int r, int c); + std::vector>>& getGrid() { return grid; } + const std::vector>>& getGrid() const { return grid; } + sf::Texture& getTextureSheet() { return textureSheetRef; } + std::mt19937& getRNG() { return rng; } + std::uniform_int_distribution& getGemColorDist() { return gemColorDist; } + std::uniform_real_distribution& getBonusChanceDist() { return bonusChanceDist; } + std::uniform_int_distribution& getBonusTypeDist() { return bonusTypeDist; } + std::uniform_int_distribution& getGridXDist() { return gridXDist; } + std::uniform_int_distribution& getGridYDist() { return gridYDist; } + int getWidth() const { return width; } + int getHeight() const { return height; } + std::vector> recentlyDestroyedGems; + + bool hasPossibleMoves() const; + + +private: + int width; + int height; + sf::Texture& textureSheetRef; + std::vector>> grid; + GridCoord selectedGemCoords; + bool gemSelected; + + std::mt19937 rng; + std::uniform_int_distribution gemColorDist; + std::uniform_real_distribution bonusChanceDist; + std::uniform_int_distribution bonusTypeDist; + std::uniform_int_distribution gridXDist; + std::uniform_int_distribution gridYDist; + + std::unique_ptr currentState; + + void initializeBoard(); +}; \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..30acac8 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,124 @@ +set(PROJECT_NAME GEMS_3) + +################################################################################ +# Source groups +################################################################################ +set(Source_Files + "Board.cpp" + "Board.h" + "Config.h" + "Game.cpp" + "Game.h" + "Gem.cpp" + "Gem.h" + "main.cpp" +) +source_group("Source Files" FILES ${Source_Files}) + +set(ALL_FILES + ${Source_Files} +) + +################################################################################ +# Target +################################################################################ +add_executable(${PROJECT_NAME} ${ALL_FILES}) + +use_props(${PROJECT_NAME} "${CMAKE_CONFIGURATION_TYPES}" "${DEFAULT_CXX_PROPS}") +set(ROOT_NAMESPACE GEMS3) + +set_target_properties(${PROJECT_NAME} PROPERTIES + VS_GLOBAL_KEYWORD "Win32Proj" +) +if("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64") + set_target_properties(${PROJECT_NAME} PROPERTIES + INTERPROCEDURAL_OPTIMIZATION_RELEASE "TRUE" + ) +elseif("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x86") + set_target_properties(${PROJECT_NAME} PROPERTIES + INTERPROCEDURAL_OPTIMIZATION_RELEASE "TRUE" + ) +endif() +################################################################################ +# Compile definitions +################################################################################ +if("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64") + target_compile_definitions(${PROJECT_NAME} PRIVATE + "$<$:" + "_DEBUG" + ">" + "$<$:" + "NDEBUG" + ">" + "_CONSOLE;" + "UNICODE;" + "_UNICODE" + ) +elseif("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x86") + target_compile_definitions(${PROJECT_NAME} PRIVATE + "$<$:" + "_DEBUG" + ">" + "$<$:" + "NDEBUG" + ">" + "WIN32;" + "_CONSOLE;" + "UNICODE;" + "_UNICODE" + ) +endif() + +################################################################################ +# Compile and link options +################################################################################ +if(MSVC) + if("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64") + target_compile_options(${PROJECT_NAME} PRIVATE + $<$: + /std:c++17 + > + $<$: + /Oi; + /Gy + > + /permissive-; + /sdl; + /W3; + ${DEFAULT_CXX_DEBUG_INFORMATION_FORMAT}; + ${DEFAULT_CXX_EXCEPTION_HANDLING} + ) + elseif("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x86") + target_compile_options(${PROJECT_NAME} PRIVATE + $<$: + /Oi; + /Gy + > + /permissive-; + /sdl; + /W3; + ${DEFAULT_CXX_DEBUG_INFORMATION_FORMAT}; + ${DEFAULT_CXX_EXCEPTION_HANDLING} + ) + endif() + if("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64") + target_link_options(${PROJECT_NAME} PRIVATE + $<$: + /OPT:REF; + /OPT:ICF + > + /DEBUG; + /SUBSYSTEM:CONSOLE + ) + elseif("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x86") + target_link_options(${PROJECT_NAME} PRIVATE + $<$: + /OPT:REF; + /OPT:ICF + > + /DEBUG; + /SUBSYSTEM:CONSOLE + ) + endif() +endif() + diff --git a/Config.h b/Config.h new file mode 100644 index 0000000..56bf035 --- /dev/null +++ b/Config.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include +#include + +constexpr int GRID_WIDTH = 8; +constexpr int GRID_HEIGHT = 8; +constexpr int GEM_SIZE = 64; +constexpr unsigned int WINDOW_WIDTH = GRID_WIDTH * GEM_SIZE; +constexpr unsigned int WINDOW_HEIGHT = GRID_HEIGHT * GEM_SIZE; +const int NUM_GEM_COLORS = 6; +const float BONUS_CHANCE = 0.1f; +const int REPAINT_BONUS_RADIUS = 3; +const int BOMB_BONUS_COUNT = 5; +class GemColorBase { +public: + virtual ~GemColorBase() = default; + virtual int getIndex() const = 0; + virtual sf::Color getSFMLColor() const = 0; +}; +class RedGemColor : public GemColorBase { +public: + int getIndex() const override { return 0; } + sf::Color getSFMLColor() const override { return sf::Color::Red; } +}; + +class GreenGemColor : public GemColorBase { +public: + int getIndex() const override { return 1; } + sf::Color getSFMLColor() const override { return sf::Color::Green; } +}; + +class BlueGemColor : public GemColorBase { +public: + int getIndex() const override { return 2; } + sf::Color getSFMLColor() const override { return sf::Color::Blue; } +}; + +class YellowGemColor : public GemColorBase { +public: + int getIndex() const override { return 3; } + sf::Color getSFMLColor() const override { return sf::Color::Yellow; } +}; + +class PurpleGemColor : public GemColorBase { +public: + int getIndex() const override { return 4; } + sf::Color getSFMLColor() const override { return sf::Color::Magenta; } +}; + +class OrangeGemColor : public GemColorBase { +public: + int getIndex() const override { return 5; } + sf::Color getSFMLColor() const override { return sf::Color(255, 165, 0); } +}; + +class NoneGemColor : public GemColorBase { +public: + int getIndex() const override { return -1; } + sf::Color getSFMLColor() const override { return sf::Color::Black; } +}; +class GameStateBase { +public: + virtual ~GameStateBase() = default; + virtual void enter(class Board& board) = 0; + virtual GameStateBase* update(class Board& board) = 0; + virtual void exit(class Board& board) = 0; + virtual bool isIdle() const { return false; } +}; +struct GridCoord { + int x; + int y; + + GridCoord() : x(0), y(0) {} + GridCoord(int x, int y) : x(x), y(y) {} + bool operator<(const GridCoord& other) const { + if (y == other.y) { + return x < other.x; + } + return y < other.y; + } + + bool operator==(const GridCoord& other) const { + return x == other.x && y == other.y; + } +}; +namespace sf { + inline bool operator<(const Vector2i& a, const Vector2i& b) { + if (a.x == b.x) { + return a.y < b.y; + } + return a.x < b.x; + } +} \ No newline at end of file diff --git a/Game.cpp b/Game.cpp new file mode 100644 index 0000000..fadad52 --- /dev/null +++ b/Game.cpp @@ -0,0 +1,62 @@ +#include "Game.h" +#include "Config.h" +#include +#include +#include +#include +#include +#include + +Game::Game() + : window(sf::VideoMode({ WINDOW_WIDTH, WINDOW_HEIGHT }), "SFML Gems Game", sf::Style::Default) +{ + window.setFramerateLimit(60); + loadResources(); + auto newBoard = std::make_unique(GRID_WIDTH, GRID_HEIGHT, gemTextureSheet); + board = std::move(newBoard); +} + +void Game::loadResources() { + if (!gemTextureSheet.loadFromFile("gems.png")) { + throw std::runtime_error("Failed to load gems.png texture!"); + } +} + +void Game::run() { + sf::Clock clock; + while (window.isOpen()) { + sf::Time deltaTime = clock.restart(); + processEvents(); + update(deltaTime); + render(); + } +} + +void Game::processEvents() { + while (const auto event = window.pollEvent()) { + event->visit([this](auto&& e) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + window.close(); + } + else if constexpr (std::is_same_v) { + if (e.button == sf::Mouse::Button::Left) { + board->handleMouseClick({ e.position.x, e.position.y }); + } + } + }); + } +} + +void Game::update(sf::Time deltaTime) { + while (board->update()) {} +} + +void Game::render() { + window.clear(sf::Color::Black); + if (board) { + board->draw(window); + } + window.display(); +} \ No newline at end of file diff --git a/Game.h b/Game.h new file mode 100644 index 0000000..cbb5936 --- /dev/null +++ b/Game.h @@ -0,0 +1,24 @@ +// This is a personal academic project. Dear PVS-Studio, please check it. +// PVS-Studio Static Code Analyzer for C, C++, C#, and Java: https://pvs-studio.com +#pragma once + +#include +#include "Board.h" +#include "Config.h" +#include + +class Game { +public: + Game(); + void run(); + +private: + void processEvents(); + void update(sf::Time deltaTime); + void render(); + sf::RenderWindow window; + sf::Texture gemTextureSheet; + std::unique_ptr board; + void loadResources(); + +}; \ No newline at end of file diff --git a/Gem.cpp b/Gem.cpp new file mode 100644 index 0000000..0c7bffb --- /dev/null +++ b/Gem.cpp @@ -0,0 +1,56 @@ +#include "Gem.h" +#include "Config.h" +#include +#include +Gem::Gem(std::unique_ptr c, int x, int y, sf::Texture& textureSheet) + : color(std::move(c)), gridPosition(x, y), textureSheetRef(textureSheet), sprite(textureSheet) +{ + if (color && color->getIndex() != -1) { + updateTextureRect(); + } + setGridPosition(x, y); +} + +void Gem::setGridPosition(int gridX, int gridY) { + gridPosition.x = gridX; + gridPosition.y = gridY; + sprite.setPosition(sf::Vector2f( + static_cast(gridX * GEM_SIZE), + static_cast(gridY * GEM_SIZE) + )); +} + +void Gem::updateTextureRect() { + if (!color || color->getIndex() == -1) return; + + int colorIndex = color->getIndex(); + if (colorIndex < 0 || colorIndex >= NUM_GEM_COLORS) { + throw std::runtime_error("Invalid GemColor index in updateTextureRect"); + } + sprite.setTexture(textureSheetRef); + sprite.setTextureRect(sf::IntRect( + sf::Vector2i(colorIndex * GEM_SIZE, 0), + sf::Vector2i(GEM_SIZE, GEM_SIZE) + )); +} +const GemColorBase* Gem::getColor() const { + return color.get(); +} + +void Gem::draw(sf::RenderWindow& window) { + if (color && color->getIndex() != -1) { + window.draw(sprite); + } +} +void Gem::setColor(std::unique_ptr newColor) { + color = std::move(newColor); + updateTextureRect(); +} + +sf::Vector2f Gem::getVisualPosition() const { + return sprite.getPosition(); +} + +void Gem::setVisualPosition(float x, float y) { + sprite.setPosition(sf::Vector2f(x, y)); +} \ No newline at end of file diff --git a/Gem.h b/Gem.h new file mode 100644 index 0000000..89ef38e --- /dev/null +++ b/Gem.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include "Config.h" +#include + +class Gem { +public: + Gem(std::unique_ptr c, int gridX, int gridY, sf::Texture& textureSheet); + void draw(sf::RenderWindow& window); + void setGridPosition(int gridX, int gridY); + GridCoord getGridPosition() const; + const GemColorBase* getColor() const; + void setColor(std::unique_ptr newColor); + sf::Vector2f getVisualPosition() const; + void setVisualPosition(float x, float y); + +private: + sf::Sprite sprite; + std::unique_ptr color; + GridCoord gridPosition; + sf::Texture& textureSheetRef; + void updateTextureRect(); +}; \ No newline at end of file diff --git a/gems.png b/gems.png new file mode 100644 index 0000000..574d642 Binary files /dev/null and b/gems.png differ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..77a7052 --- /dev/null +++ b/main.cpp @@ -0,0 +1,25 @@ +#include "Game.h" +#include + +int main() { + try { + Game game; + game.run(); + } + catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; +#ifdef _WIN32 + system("pause"); +#endif + return EXIT_FAILURE; + } + catch (...) { + std::cerr << "An unknown error occurred." << std::endl; +#ifdef _WIN32 + system("pause"); +#endif + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} \ No newline at end of file