diff --git a/lab1/CMakeLists.txt b/lab1/CMakeLists.txt new file mode 100644 index 0000000..908d0ba --- /dev/null +++ b/lab1/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.15) +project(gems) + +set(CMAKE_CXX_STANDARD 17) + +include(FetchContent) +FetchContent_Declare( + SDL3 + GIT_REPOSITORY "https://github.com/libsdl-org/SDL.git" + GIT_TAG "main" + EXCLUDE_FROM_ALL +) +FetchContent_MakeAvailable(SDL3) + +add_executable(gems + src/main.cpp + src/Game.cpp + src/Gem.cpp + src/Bonus.cpp +) + +target_include_directories(gems PRIVATE include) +target_link_libraries(gems PRIVATE SDL3::SDL3) \ No newline at end of file diff --git a/lab1/README.md b/lab1/README.md index bbaf8aa..48eb86e 100644 --- a/lab1/README.md +++ b/lab1/README.md @@ -1,2 +1,20 @@ CPP laboratories -sheesh \ No newline at end of file +sheesh + +# Gems + +## How to play: +Use your mouse to change neighbour gems, LMB to select, other buttons to remove selection + +In total game has six colors: +1. Wheat +2. Maroon +3. Orange +4. Olive +5. Light pink +6. Slate gray + +Bonuses can spawn in reward after gem elimination in radius of 3 gems +Bonuses are rendered as squares inside a gem: +1. Recolor (blue) - changes colors randomly of gem where the bonus is and his 2 random non-neighboring gems to color of gem with bonus. +2. Bomb (red) - eliminates 5 random gems in the field including source gem \ No newline at end of file diff --git a/lab1/include/Bonus.hpp b/lab1/include/Bonus.hpp new file mode 100644 index 0000000..009d149 --- /dev/null +++ b/lab1/include/Bonus.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "GameObject.hpp" +#include + +enum class BonusType { RECOLOR, BOMB }; + +class Bonus : public GameObject { +public: + virtual void Activate(class Game* game, size_t index) = 0; + virtual ~Bonus() = default; +}; + +class RecolorBonus : public Bonus { +public: + void Draw(SDL_Renderer* renderer, const SDL_FRect& rect) const override; + void Activate(class Game* game, size_t index) override; +}; + +class BombBonus : public Bonus { +public: + void Draw(SDL_Renderer* renderer, const SDL_FRect& rect) const override; + void Activate(class Game* game, size_t index) override; +}; \ No newline at end of file diff --git a/lab1/include/Colors.hpp b/lab1/include/Colors.hpp new file mode 100644 index 0000000..f9b5266 --- /dev/null +++ b/lab1/include/Colors.hpp @@ -0,0 +1,12 @@ +#pragma once +#include "SDL3/SDL_pixels.h" + +constexpr SDL_Color WHEAT{245, 222, 179, 255}; +constexpr SDL_Color MAROON{128, 0, 0, 255}; +constexpr SDL_Color ORANGE{255, 140, 0, 255}; +constexpr SDL_Color OLIVE{128, 128, 0, 255}; +constexpr SDL_Color LIGHT_PINK{255, 182, 193, 255}; +constexpr SDL_Color SLATE_GRAY{112, 128, 144, 255}; + +constexpr SDL_Color PALETTE[] = {WHEAT, MAROON, ORANGE, OLIVE, LIGHT_PINK, SLATE_GRAY}; +constexpr int PALETTE_SIZE = sizeof(PALETTE) / sizeof(SDL_Color); \ No newline at end of file diff --git a/lab1/include/Game.hpp b/lab1/include/Game.hpp new file mode 100644 index 0000000..39c7d1b --- /dev/null +++ b/lab1/include/Game.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "SDL3/SDL.h" +#include +#include +#include "Colors.hpp" +#include "GameObject.hpp" + +constexpr int SCREEN_WIDTH = 800; +constexpr int SCREEN_HEIGHT = 600; +constexpr int GEM_WIDTH = 40; +constexpr int GEM_HEIGHT = 40; +constexpr int TOTAL_GEMS = SCREEN_WIDTH * SCREEN_HEIGHT / GEM_WIDTH / GEM_HEIGHT; +constexpr int LINE_LENGTH = SCREEN_WIDTH / GEM_WIDTH; +constexpr int ROWS = SCREEN_HEIGHT / GEM_HEIGHT; + +class Game { +public: + Game(); + bool Initialize(); + void Run(); + void ProcessInput(); + void Update(); + void Render() const; + + void HandleClick(const SDL_MouseButtonEvent& e); + bool CheckTriplets(); + void ActivateBonuses(); + void SpawnBonus(size_t destroyed); + + // Для доступа бонусов + std::vector>& GetField() { return field; } + friend class BombBonus; + friend class RecolorBonus; + +private: + bool shouldExit = false; + int selectedGem = -1; + SDL_Window* window = nullptr; + SDL_Renderer* renderer = nullptr; + + std::vector> field; + + void InitField(); + bool canPlace(size_t i, SDL_Color src) const; +}; \ No newline at end of file diff --git a/lab1/include/GameObject.hpp b/lab1/include/GameObject.hpp new file mode 100644 index 0000000..9873d00 --- /dev/null +++ b/lab1/include/GameObject.hpp @@ -0,0 +1,10 @@ +#pragma once +#include "SDL3/SDL_rect.h" +#include "SDL3/SDL_render.h" + +class GameObject { +public: + virtual ~GameObject() = default; + virtual void Draw(SDL_Renderer* renderer, const SDL_FRect& rect) const = 0; + virtual void Update() {} +}; \ No newline at end of file diff --git a/lab1/include/Gem.hpp b/lab1/include/Gem.hpp new file mode 100644 index 0000000..6fa2d26 --- /dev/null +++ b/lab1/include/Gem.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "GameObject.hpp" +#include "SDL3/SDL_pixels.h" +#include + +class Bonus; + +class Gem : public GameObject { +public: + explicit Gem(SDL_Color c); + Gem() = default; + + void Draw(SDL_Renderer* renderer, const SDL_FRect& rect) const override; + + void SetBonus(std::unique_ptr b); + bool HasBonus() const; + void ActivateBonus(class Game* game, size_t index); + + SDL_Color color{255, 255, 255, 255}; + +private: + std::unique_ptr bonus; +}; \ No newline at end of file diff --git a/lab1/src/Bonus.cpp b/lab1/src/Bonus.cpp new file mode 100644 index 0000000..fdd167c --- /dev/null +++ b/lab1/src/Bonus.cpp @@ -0,0 +1,91 @@ +#include "Bonus.hpp" +#include "Game.hpp" +#include "Gem.hpp" +#include "Colors.hpp" +#include +#include + +namespace { + int getRandomInt(int start, int end) { + static std::random_device rd; + static std::mt19937 gen(rd()); + std::uniform_int_distribution dist(start, end - 1); + return dist(gen); + } + + bool isNeighbours(size_t a, size_t b) { + if (a == b) return false; + if (a > b) std::swap(a, b); + if (b - a == 1 && a % LINE_LENGTH != LINE_LENGTH-1) return true; + if (b - a == LINE_LENGTH) return true; + return false; + } +} + +void RecolorBonus::Draw(SDL_Renderer* renderer, const SDL_FRect& rect) const { + SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255); + SDL_FRect bonusRect = { + rect.x + rect.w * 3/8, + rect.y + rect.h * 3/8, + rect.w / 4, + rect.h / 4 + }; + SDL_RenderFillRect(renderer, &bonusRect); +} + +void RecolorBonus::Activate(Game* game, size_t index) { + auto& field = game->GetField(); + auto gem = std::dynamic_pointer_cast(field[index]); + if (!gem) return; + + auto src_color = gem->color; + gem->color = PALETTE[getRandomInt(0, PALETTE_SIZE)]; + + constexpr int RECOLOR_RADIUS = 2; + int x = index % LINE_LENGTH; + int y = index / LINE_LENGTH; + + int x1 = getRandomInt(std::max(0, x - RECOLOR_RADIUS), std::min(x + RECOLOR_RADIUS, LINE_LENGTH)); + int y1 = getRandomInt(std::max(0, y - RECOLOR_RADIUS), std::min(y + RECOLOR_RADIUS, ROWS)); + + size_t i1 = x1 + y1 * LINE_LENGTH; + + while (isNeighbours(index, i1)) { + x1 = getRandomInt(std::max(0, x - RECOLOR_RADIUS), std::min(x + RECOLOR_RADIUS, LINE_LENGTH)); + y1 = getRandomInt(std::max(0, y - RECOLOR_RADIUS), std::min(y + RECOLOR_RADIUS, ROWS)); + i1 = x1 + y1 * LINE_LENGTH; + } + + if (auto other = std::dynamic_pointer_cast(field[i1])) { + other->color = src_color; + } +} + +void BombBonus::Draw(SDL_Renderer* renderer, const SDL_FRect& rect) const { + SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); + SDL_FRect bonusRect = { + rect.x + rect.w * 3/8, + rect.y + rect.h * 3/8, + rect.w / 4, + rect.h / 4 + }; + SDL_RenderFillRect(renderer, &bonusRect); +} + +void BombBonus::Activate(Game* game, size_t index) { + auto& field = game->GetField(); + for (int i = 0; i < 4; i++) { + int x = getRandomInt(0, LINE_LENGTH); + int y = getRandomInt(0, ROWS); + size_t j = x + y * LINE_LENGTH; + + if (j < field.size()) { + for (int row = y; row > 0; row--) { + size_t current = x + row * LINE_LENGTH; + size_t above = x + (row-1) * LINE_LENGTH; + field[current] = field[above]; + } + field[x] = std::make_shared(PALETTE[getRandomInt(0, PALETTE_SIZE)]); + } + } +} \ No newline at end of file diff --git a/lab1/src/Game.cpp b/lab1/src/Game.cpp new file mode 100644 index 0000000..36bae73 --- /dev/null +++ b/lab1/src/Game.cpp @@ -0,0 +1,300 @@ +#include "Game.hpp" +#include "Gem.hpp" +#include "Bonus.hpp" +#include +#include +#include + +Game::Game() { + field.reserve(TOTAL_GEMS); +} + +bool Game::Initialize() { + if (!SDL_Init(SDL_INIT_VIDEO)) { + SDL_Log("Failed to initialize SDL: %s", SDL_GetError()); + return false; + } + + window = SDL_CreateWindow("Gems", SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_RESIZABLE); + if (!window) { + SDL_Log("Failed to create window: %s", SDL_GetError()); + SDL_Quit(); + return false; + } + + renderer = SDL_CreateRenderer(window, nullptr); + if (!renderer) { + SDL_Log("Failed to create renderer: %s", SDL_GetError()); + SDL_DestroyWindow(window); + SDL_Quit(); + return false; + } + + InitField(); + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + return true; +} + +int getRandomInt(int start, int end) { + static std::random_device rd; + static std::mt19937 gen(rd()); + std::uniform_int_distribution dist(start, end - 1); + return dist(gen); +} + +bool operator==(const SDL_Color& a, const SDL_Color& b) { + return a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a; +} + +bool Game::canPlace(size_t i, SDL_Color src) const { + if (i >= 2 * LINE_LENGTH) { + auto gem1 = std::dynamic_pointer_cast(field[i - LINE_LENGTH]); + auto gem2 = std::dynamic_pointer_cast(field[i - 2 * LINE_LENGTH]); + if (gem1 && gem2 && gem1->color == src && gem2->color == src) + return false; + } + + if (i % LINE_LENGTH >= 2) { + auto gem1 = std::dynamic_pointer_cast(field[i - 1]); + auto gem2 = std::dynamic_pointer_cast(field[i - 2]); + if (gem1 && gem2 && gem1->color == src && gem2->color == src) + return false; + } + return true; +} + +void Game::InitField() { + for (int i = 0; i < TOTAL_GEMS; i++) { + auto color = PALETTE[getRandomInt(0, PALETTE_SIZE)]; + while (!canPlace(i, color)) { + color = PALETTE[getRandomInt(0, PALETTE_SIZE)]; + } + field.push_back(std::make_shared(color)); + } +} + +void Game::Run() { + while (!shouldExit) { + ProcessInput(); + Update(); + Render(); + SDL_Delay(100); + } +} + +void Game::ProcessInput() { + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_EVENT_QUIT) { + shouldExit = true; + } + else if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { + HandleClick(e.button); + } + } +} + +bool isNeighbours(size_t a, size_t b) { + if (a == b) return false; + if (a > b) std::swap(a, b); + if (b - a == 1 && a % LINE_LENGTH != LINE_LENGTH-1) return true; + if (b - a == LINE_LENGTH) return true; + return false; +} + +void Game::HandleClick(const SDL_MouseButtonEvent& e) { + if (e.button != SDL_BUTTON_LEFT || !e.down) { + selectedGem = -1; + return; + } + + const int x = e.x / GEM_WIDTH; + const int y = e.y / GEM_HEIGHT; + + size_t clicked = x + y * LINE_LENGTH; + if (selectedGem != -1 && isNeighbours(selectedGem, clicked)) { + std::swap(field[selectedGem], field[clicked]); + selectedGem = -1; + } + else { + selectedGem = clicked; + } +} + +void Game::Render() const { + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderClear(renderer); + + for (int i = 0; i < field.size(); i++) { + int gemX = i % LINE_LENGTH; + int gemY = i / LINE_LENGTH; + + SDL_FRect gemRect{ + static_cast(gemX * GEM_WIDTH), + static_cast(gemY * GEM_HEIGHT), + static_cast(GEM_WIDTH), + static_cast(GEM_HEIGHT) + }; + + field[i]->Draw(renderer, gemRect); + } + + if (selectedGem != -1 && selectedGem < field.size()) { + int gemX = selectedGem % LINE_LENGTH; + int gemY = selectedGem / LINE_LENGTH; + SDL_FRect gemRect{ + static_cast(gemX * GEM_WIDTH), + static_cast(gemY * GEM_HEIGHT), + static_cast(GEM_WIDTH), + static_cast(GEM_HEIGHT) + }; + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 96); + SDL_RenderFillRect(renderer, &gemRect); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderRect(renderer, &gemRect); + } + + SDL_RenderPresent(renderer); +} + +bool Game::CheckTriplets() { + bool located = false; + for (int i = 2; i < TOTAL_GEMS; i++) { + auto gem_i = std::dynamic_pointer_cast(field[i]); + if (!gem_i) continue; + + const auto& src = gem_i->color; + + // Проверка вертикальных троек + if (i >= 2 * LINE_LENGTH) { + auto gem_up1 = std::dynamic_pointer_cast(field[i - LINE_LENGTH]); + auto gem_up2 = std::dynamic_pointer_cast(field[i - 2 * LINE_LENGTH]); + if (gem_up1 && gem_up2 && gem_up1->color == src && gem_up2->color == src) { + located = true; + int additional = 1; + while (i + additional * LINE_LENGTH < TOTAL_GEMS) { + auto gem_next = std::dynamic_pointer_cast(field[i + additional * LINE_LENGTH]); + if (gem_next && gem_next->color == src) + additional++; + else + break; + } + additional--; + + // Определяем столбец + int col = i % LINE_LENGTH; + int startRow = i / LINE_LENGTH - 2; // начальная строка тройки + int endRow = startRow + 2 + additional; // последняя строка в цепочке + + // Спавним бонусы для всех гемов в цепочке + for (int row = startRow; row <= endRow; row++) { + int idx = col + row * LINE_LENGTH; + SpawnBonus(idx); + } + + // Количество удаляемых строк + int rowsToRemove = 3 + additional; + + // Сдвигаем гемы в столбце сверху вниз + for (int row = endRow; row >= rowsToRemove; row--) { + int idx = col + row * LINE_LENGTH; + field[idx] = field[idx - rowsToRemove * LINE_LENGTH]; + } + + // Генерируем новые гемы наверху + for (int row = 0; row < rowsToRemove; row++) { + int idx = col + row * LINE_LENGTH; + field[idx] = std::make_shared(PALETTE[getRandomInt(0, PALETTE_SIZE)]); + } + + break; + } + } + + // Проверка горизонтальных троек + if (i % LINE_LENGTH >= 2) { + auto gem_left1 = std::dynamic_pointer_cast(field[i - 1]); + auto gem_left2 = std::dynamic_pointer_cast(field[i - 2]); + if (gem_left1 && gem_left2 && gem_left1->color == src && gem_left2->color == src) { + located = true; + int additional = 1; + int row = i / LINE_LENGTH; + int endLineIndex = (row + 1) * LINE_LENGTH; + while (i + additional < endLineIndex) { + auto gem_next = std::dynamic_pointer_cast(field[i + additional]); + if (gem_next && gem_next->color == src) + additional++; + else + break; + } + additional--; + + int startCol = i % LINE_LENGTH - 2; + int endCol = startCol + 2 + additional; + + // Спавним бонусы для всех гемов в цепочке + for (int col = startCol; col <= endCol; col++) { + int idx = col + row * LINE_LENGTH; + SpawnBonus(idx); + } + + // Для каждого столбца в цепочке сдвигаем гемы сверху вниз + for (int col = startCol; col <= endCol; col++) { + int idx = col + row * LINE_LENGTH; + + // Сдвигаем все гемы выше вниз на одну позицию + for (int r = row; r > 0; r--) { + int currentIdx = col + r * LINE_LENGTH; + int aboveIdx = col + (r-1) * LINE_LENGTH; + field[currentIdx] = field[aboveIdx]; + } + + // Генерируем новый гем наверху + field[col] = std::make_shared(PALETTE[getRandomInt(0, PALETTE_SIZE)]); + } + + break; + } + } + } + return located; +} + +void Game::Update() { + ActivateBonuses(); + CheckTriplets(); +} + +void Game::ActivateBonuses() { + for (size_t i = 0; i < field.size(); i++) { + if (auto gem = std::dynamic_pointer_cast(field[i])) { + if (gem->HasBonus()) { + gem->ActivateBonus(this, i); + } + } + } +} + +void Game::SpawnBonus(size_t destroyed) { + constexpr float SPAWN_CHANCE = 0.5; + if (static_cast(getRandomInt(0, 101)) / 100 > SPAWN_CHANCE) return; + + constexpr int SPAWN_RADIUS = 3; + int x = destroyed % LINE_LENGTH; + int y = destroyed / LINE_LENGTH; + + int xNew = getRandomInt(std::max(0, x - SPAWN_RADIUS), std::min(LINE_LENGTH, x + SPAWN_RADIUS)); + int yNew = getRandomInt(std::max(0, y - SPAWN_RADIUS), std::min(ROWS, y + SPAWN_RADIUS)); + size_t idx = xNew + yNew * LINE_LENGTH; + + if (idx >= field.size()) return; + if (auto gem = std::dynamic_pointer_cast(field[idx])) { + std::unique_ptr bonus; + if (getRandomInt(0, 2) == 0) { + bonus = std::make_unique(); + } else { + bonus = std::make_unique(); + } + gem->SetBonus(std::move(bonus)); + } +} \ No newline at end of file diff --git a/lab1/src/Gem.cpp b/lab1/src/Gem.cpp new file mode 100644 index 0000000..8411a13 --- /dev/null +++ b/lab1/src/Gem.cpp @@ -0,0 +1,28 @@ +#include "Gem.hpp" +#include "Bonus.hpp" + +Gem::Gem(SDL_Color c) : color(c) {} + +void Gem::Draw(SDL_Renderer* renderer, const SDL_FRect& rect) const { + SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); + SDL_RenderFillRect(renderer, &rect); + + if (bonus) { + bonus->Draw(renderer, rect); + } +} + +void Gem::SetBonus(std::unique_ptr b) { + bonus = std::move(b); +} + +bool Gem::HasBonus() const { + return bonus != nullptr; +} + +void Gem::ActivateBonus(Game* game, size_t index) { + if (bonus) { + bonus->Activate(game, index); + bonus.reset(); + } +} \ No newline at end of file diff --git a/lab1/src/main.cpp b/lab1/src/main.cpp new file mode 100644 index 0000000..7f23f68 --- /dev/null +++ b/lab1/src/main.cpp @@ -0,0 +1,11 @@ +#include "Game.hpp" + +int main() { + Game gems{}; + + if (gems.Initialize()) { + gems.Run(); + } + + return 0; +}