diff options
| author | fschildt <florian.schildt@protonmail.com> | 2025-07-21 16:07:28 +0200 | 
|---|---|---|
| committer | fschildt <florian.schildt@protonmail.com> | 2025-07-21 16:07:28 +0200 | 
| commit | b46a0d9369fbaa1938f0968ab216bc2d564a9c37 (patch) | |
| tree | c28b75187d01be9642af56a54a6101f51b25e4a7 /src/games/minesweeper | |
first commit
Diffstat (limited to 'src/games/minesweeper')
| -rw-r--r-- | src/games/minesweeper/Minesweeper.cpp | 321 | ||||
| -rw-r--r-- | src/games/minesweeper/Minesweeper.hpp | 81 | 
2 files changed, 402 insertions, 0 deletions
diff --git a/src/games/minesweeper/Minesweeper.cpp b/src/games/minesweeper/Minesweeper.cpp new file mode 100644 index 0000000..60502c6 --- /dev/null +++ b/src/games/minesweeper/Minesweeper.cpp @@ -0,0 +1,321 @@ +#include <renderer/RenderGroup.hpp> +#include <games/minesweeper/Minesweeper.hpp> + +#include <algorithm> +#include <random> + +// Todo: draw a difficulty selection menu at top-mid. + +// Note: How many mines? +// - Beginner: 8x8 or 9x9 grid with 10 mines. +// - Intermediate: 16x16 grid with 40 mines. +// - Expert: 30x16 grid with 99 mines. + +Minesweeper::Minesweeper() +{ +    float map_width = static_cast<float>(m_MapWidth); +    float map_height = static_cast<float>(m_MapHeight); +    float cell_size = 0.8f * std::min(m_WorldHeight / MAX_MAP_HEIGHT, m_WorldWidth / MAX_MAP_WIDTH); +    float cell_size_without_border = 0.8f * cell_size; + +    m_MapViewPos = { +        0.1f * cell_size_without_border + (m_WorldWidth - cell_size * map_width) / 2, +        0.1f * cell_size_without_border + (m_WorldHeight - cell_size * map_height) / 2 +    }; +    m_CellOuterViewSize = {cell_size, cell_size}; +    m_CellInnerViewSize = {cell_size_without_border, cell_size_without_border}; + + +    // Todo: assert various stuff + +    Reinit(); +} + +void Minesweeper::Reinit() { +    int32_t mine_count = 40; +    memset(m_IsCoveredBitmap, 0xff, sizeof(m_IsCoveredBitmap)); +    memset(m_IsFlaggedBitmap, 0 , sizeof(m_IsFlaggedBitmap)); +    InitIsMineBitmap(mine_count); +    InitAdjacentMineCounters(); +} + +void Minesweeper::InitIsMineBitmap(int32_t mine_count) { +    assert(mine_count < m_MapWidth * m_MapHeight); + +    memset(m_IsMineBitmap, 0 , sizeof(m_IsMineBitmap)); + +    std::mt19937 rng((std::random_device()())); +    std::uniform_int_distribution<int32_t> dist(0, m_MapWidth * m_MapHeight - 1); + +    while (mine_count) { +        int32_t random_pos = dist(rng); +        int32_t x = random_pos / m_MapWidth; +        int32_t y = random_pos % m_MapWidth; +        if (!IsMine(x, y)) { +            m_IsMineBitmap[y] |= 1 << x; +            mine_count--; +        } +    } +} + +void Minesweeper::InitAdjacentMineCounters() { +    for (int32_t y = 0; y < m_MapHeight; y++) { +        int32_t y0 = y > 0 ? y-1 : y; +        int32_t y1 = y < m_MapHeight-1 ? y+1 : y; + +        for (int32_t x = 0; x < m_MapHeight; x++) { +            int32_t x0 = x > 0 ? x-1 : x; +            int32_t x1 = x < m_MapWidth-1 ? x+1 : x; + +            int32_t adjacent_mine_counter = 0; +            for (int32_t inner_y = y0; inner_y <= y1; inner_y++) { +                for (int32_t inner_x = x0; inner_x <= x1; inner_x++) { +                    if (IsMine(inner_x, inner_y)) { +                        adjacent_mine_counter++; +                    } +                } +            } +            if (IsMine(x, y)) { +                adjacent_mine_counter = -1; +            } + +            m_AdjacentMineCounters[y * m_MapWidth + x] = adjacent_mine_counter; +        } +    } +} + +bool Minesweeper::Update(std::vector<SDL_Event> &events, RenderGroup &render_group) { +    V3F32 clear_color = {0.3f, 0.2f, 0.3f}; +    render_group.SetCameraSize(4.0f, 3.0f); +    render_group.Clear(clear_color); + +    if (m_RunState == MinesweeperRunState::Restart) { +        Reinit(); +        m_RunState = MinesweeperRunState::Resume; +    } + +    for (SDL_Event &event : events) { +        if (m_RunState == MinesweeperRunState::Exit) { +            return false; +        } +        else if (m_RunState == MinesweeperRunState::Pause) { +            ProcessEventDuringPause(event, render_group); +        } +        else if (m_RunState == MinesweeperRunState::Resume) { +            ProcessEventDuringResume(event, render_group); +        } +    } + +    if (m_RunState == MinesweeperRunState::Pause) { +        DrawPauseMenu(render_group); +    } +    else if (m_RunState == MinesweeperRunState::GameOver) { +        DrawGameOverMenu(render_group); +    } + +    DrawBoard(render_group); + +    bool keep_running = m_RunState != MinesweeperRunState::Exit; +    return keep_running; +} + +void Minesweeper::ProcessEventDuringPause(SDL_Event &event, RenderGroup &render_group) { +    switch (event.type) { +    case SDL_EVENT_KEY_DOWN: { +        if (event.key.key == SDLK_ESCAPE) { +            m_RunState = MinesweeperRunState::Resume; +        } +    } break; +    default:; +    } +} + +void Minesweeper::ProcessEventDuringResume(SDL_Event &event, RenderGroup &render_group) { +    switch (event.type) { +    case SDL_EVENT_KEY_DOWN: { +        if (event.key.key == SDLK_ESCAPE) { +            m_RunState = MinesweeperRunState::Pause; +        } +    } break; + +        case SDL_EVENT_MOUSE_BUTTON_DOWN: { +            V2F32 click_screen_pos = {event.button.x, (float)render_group.m_ScreenHeight -1 - event.button.y}; +            V2F32 click_view_pos = ScreenPosToViewPos(click_screen_pos, render_group); + +        float x_adjusted = click_view_pos.x - m_MapViewPos.x; +        float y_adjusted = click_view_pos.y - m_MapViewPos.y; +        if (x_adjusted < 0.0f) { +            break; +        } +        if (y_adjusted < 0.0f) { +            break; +        } + +        int32_t x = (int32_t)(x_adjusted / m_CellOuterViewSize.x); +        int32_t y = (int32_t)(y_adjusted / m_CellOuterViewSize.y); +        if (x >= m_MapWidth) { +            break; +        } +        if (y >= m_MapHeight) { +            break; +        } + +        if (event.button.button == 1) { +            if (IsCovered(x, y)) { +                if (IsMine(x, y)) { +                    m_IsCoveredBitmap[y] &= ~(1 << x); +                    m_RunState = MinesweeperRunState::GameOver; +                } +                else { +                    Uncover(x, y); +                } +            } +        } +        else if (event.button.button == 3) { +            if (IsCovered(x, y)) { +                ToggleFlag(x ,y); +            } +        } + +    } break; + +    default:; +    } +} + +// Todo: maybe find a more efficient non-naive solution +void Minesweeper::Uncover(int32_t x, int32_t y) { +    if (x < 0)            return; +    if (x >= m_MapWidth)  return; +    if (y < 0)            return; +    if (y >= m_MapHeight) return; +    if (!IsCovered(x, y)) return; + +    m_IsCoveredBitmap[y] &= ~(1 << x); +    if (IsFlagged(x, y)) { +        ToggleFlag(x, y); +    } + +    if (m_AdjacentMineCounters[y*m_MapWidth + x] > 0) { +        return; +    } +    Uncover(x-1, y-1); +    Uncover(x  , y-1); +    Uncover(x+1, y-1); + +    Uncover(x-1, y); +    Uncover(x+1, y); + +    Uncover(x-1, y+1); +    Uncover(x  , y+1); +    Uncover(x+1, y+1); +} + +void Minesweeper::ToggleFlag(int32_t x, int32_t y) { +    m_IsFlaggedBitmap[y] ^= (1 << x); +} + +bool Minesweeper::IsCovered(int32_t x, int32_t y) { +    bool is_covered = m_IsCoveredBitmap[y] & 1 << x; +    return is_covered; +} + +bool Minesweeper::IsFlagged(int32_t x, int32_t y) { +    bool is_flagged = m_IsFlaggedBitmap[y] & 1 << x; +    return is_flagged; +} + +bool Minesweeper::IsMine(int32_t x, int32_t y) { +    bool is_mine = m_IsMineBitmap[y] & 1 << x; +    return is_mine; +} + +V2F32 Minesweeper::ScreenPosToViewPos(V2F32 screen_pos, RenderGroup &render_group) { +    // e.g. [0, 1024] -> [0, 1] -> [0, 4] +    // e.g. [0,  768] -> [0, 1] -> [0, 3] +    float screen_width = (float)render_group.m_ScreenWidth; +    float screen_height = (float)render_group.m_ScreenHeight; + +    V2F32 view_pos; +    view_pos.x = (screen_pos.x / screen_width) * m_WorldWidth; +    view_pos.y = (screen_pos.y / screen_height) * m_WorldHeight; +    return view_pos; +} + +void Minesweeper::DrawPauseMenu(RenderGroup &render_group) { +    ImGui::Begin("MinesweeperPause"); +    if (ImGui::Button("Resume")) { +        m_RunState = MinesweeperRunState::Resume; +    } +    if (ImGui::Button("Exit")) { +        m_RunState = MinesweeperRunState::Exit; +    } +    ImGui::End(); +} + +void Minesweeper::DrawGameOverMenu(RenderGroup &render_group) { +    ImGui::Begin("MinesweeperGameOver"); +    ImGui::Text("Score = ???"); +    if (ImGui::Button("Restart")) { +        m_RunState = MinesweeperRunState::Restart; +    } +    if (ImGui::Button("Exit")) { +        m_RunState = MinesweeperRunState::Exit; +    } +    ImGui::End(); +} + +void Minesweeper::DrawBoard(RenderGroup &render_group) { +    V3F32 covered_cell_color = V3F32(0.4f, 0.4f, 0.4f); +    V3F32 uncovered_cell_color = V3F32(0.2f, 0.2f, 0.2f); +    V3F32 flag_color = {0.6f, 0.3f, 03.f}; +    V3F32 mine_color = {0.8f, 0.2f, 0.2f}; + +    V2F32 flag_draw_size = {m_CellInnerViewSize.x * 0.5f, m_CellInnerViewSize.y * 0.5f}; +    V2F32 flag_draw_offset = { +        (m_CellInnerViewSize.x - flag_draw_size.x) / 2, +        (m_CellInnerViewSize.y - flag_draw_size.y) / 2 +    }; + + + +    // Todo: avoid if-statement by having them in separate contiguous locations? + +    for (int32_t y = 0; y < m_MapHeight; y++) { +        for (int32_t x = 0; x < m_MapWidth; x++) { +            V3F32 world_pos = { +                m_MapViewPos.x + (float)x * m_CellOuterViewSize.x, +                m_MapViewPos.y + (float)y * m_CellOuterViewSize.y, +                0.0f +            }; +            bool is_covered = IsCovered(x, y); +            bool is_flagged = IsFlagged(x, y); +            bool is_mine = IsMine(x, y); + +            if (is_covered) { +                render_group.PushRectangle(world_pos, m_CellInnerViewSize, covered_cell_color); +            } +            if (is_covered && is_flagged) { +                assert(IsCovered(x ,y)); +                V3F32 flag_world_pos = { +                    world_pos.x + flag_draw_offset.x, +                    world_pos.y + flag_draw_offset.y, +                    1.0f +                }; +                render_group.PushRectangle(flag_world_pos, flag_draw_size, flag_color); +            } +            if (!is_covered && !is_mine) { +                render_group.PushRectangle(world_pos, m_CellInnerViewSize, uncovered_cell_color); +            } +            if (!is_covered && is_mine) { +                V3F32 mine_world_pos = { +                    world_pos.x, +                    world_pos.y, +                    2.0f +                }; +                render_group.PushRectangle(mine_world_pos, m_CellInnerViewSize, mine_color); +            } +        } +    } +} + diff --git a/src/games/minesweeper/Minesweeper.hpp b/src/games/minesweeper/Minesweeper.hpp new file mode 100644 index 0000000..4906464 --- /dev/null +++ b/src/games/minesweeper/Minesweeper.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include <games/Game.hpp> + +namespace std { +    template <> +    struct hash<V2ST> { +        size_t operator()(const V2ST& v) const { +            size_t h1 = hash<size_t>{}(v.x); +            size_t h2 = hash<size_t>{}(v.y); +            return h1 ^ (h2 << 1); +        } +    }; +} + +enum class MinesweeperRunState { +    Resume, +    Pause, +    GameOver, +    Restart, +    Exit +}; + +class Minesweeper : public Game { +    public: +        Minesweeper(); +        ~Minesweeper() = default; + +        bool Update(std::vector<SDL_Event> &events, RenderGroup &render_group) override; + +        void ProcessEventDuringPause(SDL_Event &event, RenderGroup &render_group); +        void ProcessEventDuringResume(SDL_Event &event, RenderGroup &render_group); + + +    private: +        void Reinit(); +        void InitIsMineBitmap(int32_t mine_count); +        void InitAdjacentMineCounters(); + +        void Uncover(int32_t x, int32_t y); +        void ToggleFlag(int32_t x, int32_t y); + +        bool IsCovered(int32_t x, int32_t y); +        bool IsFlagged(int32_t x, int32_t y); +        bool IsMine(int32_t x, int32_t y); + +        V2F32 ScreenPosToViewPos(V2F32 screen_pos, RenderGroup &render_group); + + +    private: +        void DrawPauseMenu(RenderGroup &render_group); +        void DrawGameOverMenu(RenderGroup &render_group); +        void DrawBoard(RenderGroup &render_group); + + +    private: +        static constexpr int32_t MAX_MAP_HEIGHT = 32; +        static constexpr int32_t MAX_MAP_WIDTH = 32; +        static constexpr std::string_view s_FontFilepath = "./fonts/dejavu_ttf/DejaVuSans.ttf"; + + +    private: +        MinesweeperRunState m_RunState = MinesweeperRunState::Resume; + +        float m_WorldWidth = 4.0f; +        float m_WorldHeight = 3.0f; + +        int32_t m_MapWidth = 16; +        int32_t m_MapHeight = 16; + +        V2F32 m_MapViewPos; +        V2F32 m_CellOuterViewSize; +        V2F32 m_CellInnerViewSize; + +        uint32_t m_IsCoveredBitmap[MAX_MAP_HEIGHT] {}; +        uint32_t m_IsFlaggedBitmap[MAX_MAP_HEIGHT] {}; +        uint32_t m_IsMineBitmap[MAX_MAP_HEIGHT] {}; +        int32_t m_AdjacentMineCounters[MAX_MAP_WIDTH * MAX_MAP_HEIGHT] {}; +}; + +  | 
