diff options
Diffstat (limited to 'src/games')
| -rw-r--r-- | src/games/Game.cpp | 39 | ||||
| -rw-r--r-- | src/games/Game.hpp | 26 | ||||
| -rw-r--r-- | src/games/minesweeper/Minesweeper.cpp | 321 | ||||
| -rw-r--r-- | src/games/minesweeper/Minesweeper.hpp | 81 | ||||
| -rw-r--r-- | src/games/snake/Snake.cpp | 345 | ||||
| -rw-r--r-- | src/games/snake/Snake.hpp | 62 | ||||
| -rw-r--r-- | src/games/tetris/Board.cpp | 139 | ||||
| -rw-r--r-- | src/games/tetris/Board.hpp | 30 | ||||
| -rw-r--r-- | src/games/tetris/Tetris.cpp | 368 | ||||
| -rw-r--r-- | src/games/tetris/Tetris.hpp | 67 | ||||
| -rw-r--r-- | src/games/tetris/Tetromino.cpp | 212 | ||||
| -rw-r--r-- | src/games/tetris/Tetromino.hpp | 59 | 
12 files changed, 1749 insertions, 0 deletions
diff --git a/src/games/Game.cpp b/src/games/Game.cpp new file mode 100644 index 0000000..628f5d4 --- /dev/null +++ b/src/games/Game.cpp @@ -0,0 +1,39 @@ +#include <basic/defs.hpp> +#include <games/Game.hpp> +#include <games/tetris/Tetris.hpp> +#include <games/snake/Snake.hpp> +#include <games/minesweeper/Minesweeper.hpp> + +#include <assert.h> +#include <memory> + + +std::unique_ptr<Game> +Game::Select(GameType type) +{ +    switch (type) { +        case NO_GAME: { +            return nullptr; +        } + +        case TETRIS: { +            return std::make_unique<Tetris>(); +        } break; + +        case SNAKE: { +            return std::make_unique<Snake>(); +        } break; + +        case MINESWEEPER: { +            return std::make_unique<Minesweeper>(); +        } break; + +        InvalidDefaultCase; +    } + +    return nullptr; +} + + +Game::~Game() {} + diff --git a/src/games/Game.hpp b/src/games/Game.hpp new file mode 100644 index 0000000..9af98b4 --- /dev/null +++ b/src/games/Game.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include <basic/defs.hpp> +#include <SDL3/SDL.h> +#include <memory> +#include <vector> +#include <renderer/RenderGroup.hpp> + +struct SDL_Window; + +class Game { +public: +    enum GameType { +        NO_GAME, +        TETRIS, +        SNAKE, +        MINESWEEPER +    }; + +    Game() = default; +    virtual ~Game(); +    static std::unique_ptr<Game> Select(GameType type); + +    virtual bool Update(std::vector<SDL_Event> &events, RenderGroup &render_group) = 0; +}; + 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] {}; +}; + + diff --git a/src/games/snake/Snake.cpp b/src/games/snake/Snake.cpp new file mode 100644 index 0000000..9764a9e --- /dev/null +++ b/src/games/snake/Snake.cpp @@ -0,0 +1,345 @@ +#include <games/snake/Snake.hpp> +#include <imgui.h> + + +Snake::Snake () { +    m_IsPaused = false; +    m_IsRunning = true; + +    m_DtInSecondsRemaining = 0.0f; +    m_LastMillisecondsSinceT0 = SDL_GetTicks(); + +    m_TilesPerSecond = 4.0f; +    m_Direction = DIRECTION_RIGHT; +    m_LastAdvancedDirection = DIRECTION_RIGHT; + +    m_MapWidth = 16; +    m_MapHeight = 16; +    assert(MAX_MAP_WIDTH <= 64); // m_BodyBitmap is uint64_t[]. We can't exceed that! +    assert(MAX_MAP_HEIGHT <= 64); +    assert(m_MapWidth <= MAX_MAP_WIDTH); +    assert(m_MapHeight <= MAX_MAP_WIDTH); + +    m_Tail = 0; +    m_Head = 1; +    memset(m_BodyBitmap, 0, sizeof(m_BodyBitmap)); + +    int32_t head_x = m_MapWidth / 2; +    int32_t head_y = m_MapHeight / 2; +    m_BodyPositions[0] = {head_x -1, head_y}; +    m_BodyPositions[1] = {head_x, head_y}; + +    m_Rng = std::mt19937((std::random_device()())); +    m_Dist = std::uniform_int_distribution<int32_t>(0, m_MapWidth * m_MapHeight - 3); + +    SpawnFood(); +    } + +bool Snake::Update(std::vector<SDL_Event> &events, RenderGroup &render_group) { +    uint64_t milliseconds_since_t0 = SDL_GetTicks(); +    uint64_t milliseconds_since_t0_last = m_LastMillisecondsSinceT0; +    uint64_t dt_in_milliseconds = milliseconds_since_t0 - milliseconds_since_t0_last; +    float dt_in_seconds = (float)dt_in_milliseconds / 1000.0f; +    m_LastMillisecondsSinceT0 = milliseconds_since_t0; + + +    V3F32 clear_color = V3F32(0.3f, 0.3f, 0.3f); +    render_group.SetCameraSize(4.0f, 3.0f); +    render_group.Clear(clear_color); + + +    for (SDL_Event &event : events) { +        if (!m_IsRunning) { +            printf("event loop is running just false\n"); +            return false; +        } +        if (m_IsPaused) { +            ProcessEventDuringPause(event); +        } +        else { +            ProcessEventDuringResume(event); +        } +    } + +    if (!m_IsPaused) { +        MaybeMoveSnake(dt_in_seconds); +    } + + +    Draw(render_group); +    DoImgui(); + +    return m_IsRunning; +} + +void Snake::MaybeMoveSnake(float dt_in_seconds) { +    float dt_in_seconds_to_use = m_DtInSecondsRemaining + dt_in_seconds; +    float tiles_per_second = m_TilesPerSecond; +    float seconds_per_tile = 1.0f / tiles_per_second; +    while (dt_in_seconds_to_use > seconds_per_tile) { +        V2I32 head_pos = m_BodyPositions[m_Head]; +        V2I32 tail_pos = m_BodyPositions[m_Tail]; + + +        // find head_pos +        if (m_Direction == DIRECTION_UP) { +            head_pos.y += 1; +        } +        else if (m_Direction == DIRECTION_DOWN) { +            head_pos.y -= 1; +        } +        else if (m_Direction == DIRECTION_RIGHT) { +            head_pos.x += 1; +        } +        else if (m_Direction == DIRECTION_LEFT) { +            head_pos.x -= 1; +        } +        if ((head_pos.x < 0 || head_pos.x >= m_MapWidth) || +            (head_pos.y < 0 || head_pos.y >= m_MapHeight)) +        { +            m_IsRunning = false; +            return; +        } +        uint64_t head_bit = 1 << head_pos.x; +        uint64_t body_bits = m_BodyBitmap[head_pos.y]; +        if (head_pos.y == tail_pos.y) { +            body_bits &= ~(1 << tail_pos.x); +        } +        if (head_bit & body_bits) { +            m_IsRunning = false; +            return; +        } + + +        // advance head +        int32_t max_positions = sizeof(m_BodyPositions) / sizeof(m_BodyPositions[0]); +        m_Head += 1; +        if (m_Head >= max_positions) { +            m_Head = 0; +        } + +        m_BodyPositions[m_Head] = head_pos; +        m_BodyBitmap[head_pos.y] |= (1 << head_pos.x); + + +        if (m_BodyPositions[m_Head] == m_FoodPosition) { +            SpawnFood(); +        } +        else { +            // advance tail +            V2I32 next_tail_pos = m_BodyPositions[m_Tail]; +            m_BodyBitmap[next_tail_pos.y] &= ~(1 << next_tail_pos.x); + +            m_Tail += 1; +            if (m_Tail >= max_positions) { +                m_Tail = 0; +            } +        } + + +        m_LastAdvancedDirection = m_Direction; +        dt_in_seconds_to_use -= seconds_per_tile; +    } + + +    m_DtInSecondsRemaining = dt_in_seconds_to_use; +} + +void Snake::ProcessEventDuringPause(SDL_Event &event) { +    switch (event.type) { +    case SDL_EVENT_KEY_DOWN: { +        if (event.key.key == SDLK_ESCAPE) { +            m_IsPaused = false; +        } +    } +    default:; +    } +} + +void Snake::ProcessEventDuringResume(SDL_Event &event) { +    switch (event.type) { +    case SDL_EVENT_KEY_DOWN: { +        if (event.key.key == SDLK_UP) { +            if (m_LastAdvancedDirection == DIRECTION_RIGHT || +                m_LastAdvancedDirection == DIRECTION_LEFT) +            { +                m_Direction = DIRECTION_UP; +            } +        } +        else if (event.key.key == SDLK_DOWN) { +            if (m_LastAdvancedDirection == DIRECTION_RIGHT || +                m_LastAdvancedDirection == DIRECTION_LEFT) +            { +                m_Direction = DIRECTION_DOWN; +            } +        } +        else if (event.key.key == SDLK_RIGHT) { +            if (m_LastAdvancedDirection == DIRECTION_UP || +                m_LastAdvancedDirection == DIRECTION_DOWN) +            { +                m_Direction = DIRECTION_RIGHT; +            } +        } +        else if (event.key.key == SDLK_LEFT) { +            if (m_LastAdvancedDirection == DIRECTION_UP || +                m_LastAdvancedDirection == DIRECTION_DOWN) +            { +                m_Direction = DIRECTION_LEFT; +            } +        } +        else if (event.key.key == SDLK_ESCAPE) { +            m_IsPaused = true; +        } +    } + +    default:; +    } +} + +void Snake::SpawnFood() { +    int32_t bit0_counts[MAX_MAP_HEIGHT]; +    int32_t bit0_count_total = 0; + +    // count bits +    for (int32_t y = 0; y < m_MapHeight; y++) { +        int32_t bit1_count = 0; + +        uint64_t bitmap_row = m_BodyBitmap[y]; +        while (bitmap_row != 0) { +            bitmap_row = bitmap_row & (bitmap_row - 1); +            bit1_count += 1; +        } + +        int32_t bit0_count = m_MapWidth - bit1_count; +        bit0_counts[y] = bit0_count; +        bit0_count_total += bit0_count; +    } + +    if (bit0_count_total == 0) { +        return; +    } + +    m_Dist.param(std::uniform_int_distribution<int32_t>::param_type(0, bit0_count_total - 1)); +    int32_t bit0_index = m_Dist(m_Rng); +    int32_t bit0_x = 0; +    int32_t bit0_y = 0; + +    // find y +    for (int32_t y = 0; y < m_MapHeight; y++) { +        if (bit0_index < bit0_counts[y]) { +            bit0_y = y; +            break; +        } +        bit0_index -= bit0_counts[y]; +    } + +    // find x +    uint64_t bitmap_row_not = ~m_BodyBitmap[bit0_y]; +    for (int32_t x = 0; x < m_MapWidth; x++) { +        if (bitmap_row_not & 1) { +            if (bit0_index == 0) { +                bit0_x = x; +                break; +            } +            bit0_index--; +        } +        bitmap_row_not >>= 1; +    } + +    m_FoodPosition = {bit0_x, bit0_y}; +} + +void Snake::Draw(RenderGroup &render_group) { +    float world_width = 4.0f; +    float world_height = 3.0f; + +    float tile_size = (world_width / 2) / MAX_MAP_WIDTH; +    float bodypart_size = 0.8f * tile_size; +    float bodypart_offset = (tile_size - bodypart_size) / 2; + +    float map_view_width = tile_size * (float)m_MapWidth; +    float map_view_height = tile_size * (float)m_MapHeight; +    float map_x = (world_width - map_view_width) / 2; +    float map_y = (world_height - map_view_height) / 2; + +    int32_t max_positions = sizeof(m_BodyPositions) / sizeof(m_BodyPositions[0]); + + +    /* draw map background */ +    V3F32 map_world_pos = {map_x, map_y, 0.0f}; +    V2F32 map_world_dim = {map_view_width, map_view_height}; +    V3F32 bg_color = {0.0f, 0.0f, 0.0f}; +    render_group.PushRectangle(map_world_pos, map_world_dim, bg_color); + + +    /* draw snake */ +    // 1) if tail > head: advance to end first +    int32_t tail = m_Tail; +    if (tail > m_Head) { +        while (tail < max_positions) { +            V3F32 local_pos = { +                (float)m_BodyPositions[tail].x * tile_size + bodypart_offset, +                (float)m_BodyPositions[tail].y * tile_size + bodypart_offset, +                1.0f +            }; +            V2F32 local_dim = {bodypart_size, bodypart_size}; + +            V3F32 world_pos = { +                map_world_pos.x + local_pos.x, +                map_world_pos.y + local_pos.y, +                1.0f +            }; +            V2F32 world_dim = local_dim; + +            V3F32 color = {0.3f, 0.3f, 0.3f}; +            render_group.PushRectangle(world_pos, world_dim, color); +            tail++; +        } +        tail = 0; +    } +    // 2) advance to head +    while (tail <= m_Head) { +        V3F32 local_pos = { +            (float)m_BodyPositions[tail].x * tile_size + bodypart_offset, +            (float)m_BodyPositions[tail].y * tile_size + bodypart_offset, +            1.0f +        }; +        V2F32 local_dim = {bodypart_size, bodypart_size}; + +        V3F32 world_pos = { +            map_world_pos.x + local_pos.x, +            map_world_pos.y + local_pos.y, +            1.0f +        }; +        V2F32 world_dim = local_dim; + +        V3F32 color = {0.3f, 0.3f, 0.3f}; +        render_group.PushRectangle(world_pos, world_dim, color); +        tail++; +    } + + +    /* draw food */ +    V3F32 pos = { +        map_world_pos.x + (float)m_FoodPosition.x * tile_size + bodypart_offset, +        map_world_pos.y + (float)m_FoodPosition.y * tile_size + bodypart_offset, +        1.0f +    }; +    V2F32 dim = {bodypart_size, bodypart_size}; +    V3F32 color = {0.3f, 0.6f, 0.4f}; +    render_group.PushRectangle(pos, dim, color); +} + +void Snake::DoImgui() { +    if (m_IsPaused) { +        ImGui::Begin("SnakePause"); +        if (ImGui::Button("Resume")) { +            m_IsPaused = false; +        } +        if (ImGui::Button("Exit")) { +            m_IsRunning = false; +        } +        ImGui::End(); +    } +} + diff --git a/src/games/snake/Snake.hpp b/src/games/snake/Snake.hpp new file mode 100644 index 0000000..f04ad16 --- /dev/null +++ b/src/games/snake/Snake.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include <renderer/RenderGroup.hpp> +#include <games/Game.hpp> + +#include <random> + + +class Snake : public Game { +public: +    enum Direction : int32_t { +        DIRECTION_UP, +        DIRECTION_DOWN, +        DIRECTION_LEFT, +        DIRECTION_RIGHT, +    }; + + +public: +    Snake(); +    bool Update(std::vector<SDL_Event> &events, RenderGroup &render_group) override; + + +private: +    void ProcessEventDuringPause(SDL_Event &event); +    void ProcessEventDuringResume(SDL_Event &event); + +    void MaybeMoveSnake(float dt_in_seconds); +    void SpawnFood(); + +    void Draw(RenderGroup &render_group); +    void DoImgui(); + + + +private: +    static constexpr int32_t MAX_MAP_WIDTH = 16; +    static constexpr int32_t MAX_MAP_HEIGHT = 16; + +    bool m_IsPaused; +    bool m_IsRunning; + +    float m_DtInSecondsRemaining; +    uint64_t m_LastMillisecondsSinceT0; + +    float m_TilesPerSecond; +    Direction m_Direction; +    Direction m_LastAdvancedDirection; + +    int32_t m_MapWidth; +    int32_t m_MapHeight; +    int32_t m_Tail; +    int32_t m_Head; +    uint64_t m_BodyBitmap[MAX_MAP_HEIGHT]; +    V2I32 m_BodyPositions[MAX_MAP_WIDTH * MAX_MAP_HEIGHT]; +    V2I32 m_FoodPosition; + +    std::mt19937 m_Rng; +    std::uniform_int_distribution<int32_t> m_Dist; +}; + + diff --git a/src/games/tetris/Board.cpp b/src/games/tetris/Board.cpp new file mode 100644 index 0000000..9dff135 --- /dev/null +++ b/src/games/tetris/Board.cpp @@ -0,0 +1,139 @@ +#include <renderer/RenderGroup.hpp> +#include <games/tetris/Tetromino.hpp> +#include <games/tetris/Board.hpp> + +Board::Board() { +    for (int y = 0; y < 2; y++) { +        m_Bitmap[y] = 0xffff; // 1111111111111111 +    } +    for (int y = 2; y < 24; y++) { +        m_Bitmap[y] = 0xe007; // 1110000000000111 +    } + +    for (int y = 0; y < 22; y++) { +        for (int x = 0; x < 10; x++) { +            m_Idmap[y][x] = (uint8_t)TetrominoId::TETROMINO_ID_NONE; +        } +    } +} + +int32_t Board::PlaceTetromino(Tetromino &tetromino) { +    BoardPos pos = tetromino.GetPos(); +    TetrominoId id = tetromino.GetId(); +    uint16_t tetromino_bitmap[4]; +    tetromino.GetBitmap(tetromino_bitmap); + + +    // check if Tetromino cannot be placed (Game Over) +    if (tetromino.IsCollisionWithBoard()) { +        return -1; +    } + + +    // place in Board's Bitmap +    m_Bitmap[pos.y+0] |= tetromino_bitmap[0]; +    m_Bitmap[pos.y+1] |= tetromino_bitmap[1]; +    m_Bitmap[pos.y+2] |= tetromino_bitmap[2]; +    m_Bitmap[pos.y+3] |= tetromino_bitmap[3]; + + +    // place in Board's Idmap +    for (int32_t y = 0; y < 4; y++) { +        for (int32_t x = 0; x < 4; x++) { +            int32_t bitmap_x = 0x8000 >> (pos.x + x); +            if (tetromino_bitmap[y] & bitmap_x) { +                int32_t idmap_x = pos.x + x - 3; +                int32_t idmap_y = pos.y + y - 2; +                m_Idmap[idmap_y][idmap_x] = static_cast<uint8_t>(id); +            } +        } +    } + +    int32_t rows_cleared = ClearRows(pos.y); +    return rows_cleared; +} + +int32_t Board::ClearRows(int32_t y0) { +    int32_t rows_cleared = 0; +    int32_t y1 = y0 + 3; + +    // ignore for y = {0,1}. Those bitmap rows are all 1's for collision testing +    if (y0 < 2) { +        y0 += 2 - y0; +    } + +    for (int32_t y = y0; y <= y1; y++) { +        if (m_Bitmap[y] == 0xffff) { +            rows_cleared++; +        } +        else { +            m_Bitmap[y-rows_cleared] = m_Bitmap[y]; +            std::copy(m_Idmap[y-2], m_Idmap[y-2] + 10, m_Idmap[y-2-rows_cleared]); +        } +    } +    for (int32_t y = y1+1; y < 24; y++) { +        m_Bitmap[y-rows_cleared] = m_Bitmap[y]; +        std::copy(m_Idmap[y-2], m_Idmap[y-2] + 10, m_Idmap[y-2-rows_cleared]); +    } +    for (int32_t y = 24-rows_cleared; y < 24; y++) { +        m_Bitmap[y] = 0xe007; +        std::fill(m_Idmap[y-2], m_Idmap[y-2] + 10, (uint8_t)TetrominoId::TETROMINO_ID_NONE); +    } + + +    return rows_cleared; +} + +void Board::Draw(int32_t level, RenderGroup& render_group) { +    float world_width = 4.0f; +    float world_height = 3.0f; +    float tetromino_size_with_border = world_height / 20.0f; +    float tetromino_size = 0.8f * tetromino_size_with_border; +    float tetromino_offset = 0.1f * tetromino_size_with_border; +    V2F32 board_world_pos = { +        (world_width - tetromino_size_with_border*10) / 2.0f, +        0.0f +    }; + + +    // background +    V3F32 bg_world_pos = { +        board_world_pos.x, +        board_world_pos.y, +        0.0f +    }; +    V2F32 bg_world_dim = { +        tetromino_size_with_border * 10, +        tetromino_size_with_border * 20 +    }; +    V3F32 bg_color = {0.0f, 0.0f, 0.0f}; +    render_group.PushRectangle(bg_world_pos, bg_world_dim, bg_color); + + +    // tetromino parts +    for (size_t y = 0; y < 20; y++) { +        for (size_t x = 0; x < 10; x++) { +            uint8_t tetromino_id = m_Idmap[y][x]; +            if (tetromino_id < (uint8_t)TetrominoId::TETROMINO_ID_COUNT) { +                V2F32 local_pos = { +                    (float)x * tetromino_size_with_border + tetromino_offset, +                    (float)y * tetromino_size_with_border + tetromino_offset +                }; +                V2F32 local_dim = {tetromino_size, tetromino_size}; + + +                V3F32 world_pos = { +                    board_world_pos.x + local_pos.x, +                    board_world_pos.y + local_pos.y, +                    1.0f +                }; +                V2F32 world_dim = local_dim; + + +                V3F32 color = Tetromino::GetColor(static_cast<TetrominoId>(tetromino_id)); +                render_group.PushRectangle(world_pos, world_dim, color); +            } +        } +    } +} + diff --git a/src/games/tetris/Board.hpp b/src/games/tetris/Board.hpp new file mode 100644 index 0000000..0019a7f --- /dev/null +++ b/src/games/tetris/Board.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include <basic/defs.hpp> +#include <renderer/RenderGroup.hpp> + +class Tetromino; + +struct BoardPos { +    int32_t x; +    int32_t y; +}; + +class Board { +public: +    Board(); + +    int32_t PlaceTetromino(Tetromino &tetromino); +    void Draw(int32_t level, RenderGroup& render_group); + + +private: +    int32_t ClearRows(int32_t y0); + + +private: +    friend class Tetromino; +    uint16_t m_Bitmap[24]; +    uint8_t m_Idmap[22][10]; +}; + diff --git a/src/games/tetris/Tetris.cpp b/src/games/tetris/Tetris.cpp new file mode 100644 index 0000000..e776fb0 --- /dev/null +++ b/src/games/tetris/Tetris.cpp @@ -0,0 +1,368 @@ +#include <games/tetris/Tetromino.hpp> +#include <SDL3/SDL_events.h> +#include <SDL3/SDL_timer.h> +#include <games/tetris/Tetris.hpp> +#include <imgui.h> + +#include <fstream> + +// Todo: change to new font scaling api in imgui first +// Todo: test text with hardcoded gap + dummy to ensure it gets placed as expected + +Tetris::Tetris() : +    m_ActiveTetromino(m_Board), +    m_NextTetromino(m_Board) +{ +    m_TetrominoCounters[(size_t)m_ActiveTetromino.GetId()] += 1; +} + +void Tetris::Restart() { +    m_RunningState = TetrisRunningState::Resume; +    m_DtInSecondsRemaining = 0.0f; +    m_MillisecondsSinceT0Last = SDL_GetTicks(); + +    // Todo: Don't reconstruct! Make reset methods. +    m_Board = Board(); +    m_ActiveTetromino = Tetromino(m_Board); +    m_NextTetromino = Tetromino(m_Board); + +    memset(m_TetrominoCounters, 0, sizeof(m_TetrominoCounters)); +    m_Score = 0; +    m_LineCounter = 0; +    m_StartingLevel = 0; +    m_Level = 0; +    m_SoftdropCounter = 0; +} + +bool Tetris::Update(std::vector<SDL_Event> &events, RenderGroup &render_group) { +    V3F32 clear_color = V3F32(0.2f, 0.2f, 0.2f); +    render_group.SetCameraSize(4.0f, 3.0f); +    render_group.Clear(clear_color); + +    if (m_RunningState == TetrisRunningState::Restart) { +        Restart(); +    } + +    uint64_t milliseconds_since_t0 = SDL_GetTicks(); +    uint64_t milliseconds_since_t0_last = m_MillisecondsSinceT0Last; +    uint64_t milliseconds_dt = milliseconds_since_t0 - milliseconds_since_t0_last; +    float seconds_dt = static_cast<float>(milliseconds_dt) / 1000.0f; +    m_MillisecondsSinceT0Last = milliseconds_since_t0; + + +    if (m_RunningState == TetrisRunningState::Resume) { +        uint32_t harddrop_count = GetHarddropCount(seconds_dt); +        while (harddrop_count) { +            bool moved_down = m_ActiveTetromino.MaybeMoveDown(); +            if (!moved_down) { +                HandleTetrominoPlacement(); +            } +            harddrop_count--; +        } +    } + + +    for (auto &event : events) { +        using enum TetrisRunningState; +        switch (m_RunningState) { +            case Resume: UpdateResumeState(event); break; +            case Pause: UpdatePauseState(event); break; +            default:; +        } +    } + +    Draw(render_group); + + +    bool keep_running = m_RunningState != TetrisRunningState::Exit; +    return keep_running; +} + +void Tetris::UpdateResumeState(SDL_Event &event) { +    switch (event.type) { +    case SDL_EVENT_KEY_DOWN: { +        auto key = event.key.key; +        if (key == SDLK_RIGHT) { +            m_ActiveTetromino.MaybeMoveHorizontally(TetrominoDirection::Right); +        } else if (key == SDLK_LEFT) { +            m_ActiveTetromino.MaybeMoveHorizontally(TetrominoDirection::Left); +        } else if (key == SDLK_DOWN) { +            bool moved_down = m_ActiveTetromino.MaybeMoveDown(); +            if (!moved_down) { +                HandleTetrominoPlacement(); +            } +            else { +                m_SoftdropCounter++; +            } +        } else if (key == SDLK_X) { +            m_ActiveTetromino.MaybeRotate(TetrominoRotation::Clockwise); +        } else if (key == SDLK_Z || key == SDLK_Y) { +            m_ActiveTetromino.MaybeRotate(TetrominoRotation::CounterClockwise); +        } else if (key == SDLK_ESCAPE) { +            m_RunningState = TetrisRunningState::Pause; +        } +    } +    default:; +    } +} + +void Tetris::UpdatePauseState(SDL_Event &event) { +    switch (event.type) { +    case SDL_EVENT_KEY_DOWN: { +        auto key = event.key.key; +        if (key == SDLK_ESCAPE) { +            m_RunningState = TetrisRunningState::Resume; +        } +    } +    default:; +    } +} + +void Tetris::HandleTetrominoPlacement() { +    int32_t rows_cleared = m_Board.PlaceTetromino(m_ActiveTetromino); + +    m_ActiveTetromino = m_NextTetromino; +    m_NextTetromino = Tetromino(m_Board); + +    if (rows_cleared == -1) { +        HandleGameOver(); +        return; +    } + + +    m_LineCounter += rows_cleared; +    m_TetrominoCounters[(size_t)m_ActiveTetromino.GetId()] += 1; + +    if (rows_cleared == 1) { +        m_Score += 40 * (m_Level + 1); +    } +    else if (rows_cleared == 2) { +        m_Score += 100 * (m_Level + 1); +    } +    else if (rows_cleared == 3) { +        m_Score += 300 * (m_Level + 1); +    } +    else if (rows_cleared == 4) { +        m_Score += 1200 * (m_Level + 1); +    } + +    m_Score += m_SoftdropCounter; +    m_SoftdropCounter = 0; + +    m_Level = m_StartingLevel + m_LineCounter / 10; +} + +uint32_t Tetris::GetHarddropCount(float dt) { +    float nes_frame_time = 1.0f / 60; +    int32_t nes_frames_per_cell; +    if      (m_Level <= 8)  nes_frames_per_cell = 48 - m_Level * 5; +    else if (m_Level == 9)  nes_frames_per_cell = 6; +    else if (m_Level <= 12) nes_frames_per_cell = 5; +    else if (m_Level <= 15) nes_frames_per_cell = 4; +    else if (m_Level <= 18) nes_frames_per_cell = 3; +    else if (m_Level <= 28) nes_frames_per_cell = 2; +    else                    nes_frames_per_cell = 1; + + +    float dt_level = static_cast<float>(nes_frames_per_cell) * nes_frame_time; +    float dt_total = m_DtInSecondsRemaining + dt; + +    uint32_t harddrop_count = 0; +    while (dt_total > dt_level) { +        harddrop_count += 1; +        dt_total -= dt_level; +    } + +    m_DtInSecondsRemaining = dt_total; +    return harddrop_count; +} + +void Tetris::HandleGameOver() { +    m_RunningState = TetrisRunningState::GameOver; +    const char *filepath = "tetris_highscore.txt"; +    int32_t highscore = 0; + + +    std::ifstream highscore_file_in { filepath }; +    if (highscore_file_in) { +        highscore_file_in >> highscore; +        highscore_file_in.close(); +    } +    else { +        SDL_LogInfo(0, "Tetris: cannot open tetris_highscore.txt for reading"); +    } + +    if (highscore > 0 && highscore > m_HighScore) { +        m_HighScore = highscore; +    } + + +    if (m_Score > m_HighScore) { +        m_HighScore = m_Score; +        std::ofstream highscore_file_out { filepath }; +        if (highscore_file_out) { +            highscore_file_out << m_HighScore << std::endl; +            highscore_file_out.close(); +        } +        else { +            SDL_LogInfo(0, "Tetris: cannot open tetris_highscore.txt for writing"); +        } +    } +} + +void Tetris::Draw(RenderGroup &render_group) { +    m_Board.Draw(m_Level, render_group); +    m_ActiveTetromino.Draw(render_group); + +    DrawNextTetromino(render_group); +    DrawStatistics(render_group); +    DrawLineCounter(render_group); +    DrawLevel(render_group); +    DrawScore(render_group); + +    // Todo: Use transparency +    if (m_RunningState  == TetrisRunningState::Pause) { +        DrawPauseMenu(render_group); +    } +    else if (m_RunningState == TetrisRunningState::GameOver) { +        DrawGameOverMenu(render_group); +    } +} + +void Tetris::DrawPauseMenu(RenderGroup &render_group) { +    ImGui::Begin("TetrisPause", nullptr, s_MenuImGuiWindowFlags); +    if (ImGui::Button("Resume")) { +        m_RunningState = TetrisRunningState::Resume; +    } +    if (ImGui::Button("Restart")) { +        m_RunningState = TetrisRunningState::Restart; +    } +    if (ImGui::Button("Exit")) { +        m_RunningState = TetrisRunningState::Exit; +    } +    ImGui::End(); +} + +void Tetris::DrawGameOverMenu(RenderGroup &render_group) { +    ImGui::Begin("TetrisGameOver", nullptr, s_MenuImGuiWindowFlags); +    ImGui::Text("Score = %d", m_Score); +    ImGui::Text("HighScore = %d", m_HighScore); +    if (ImGui::Button("Restart")) { +        m_RunningState = TetrisRunningState::Restart; +    } +    if (ImGui::Button("Exit")) { +        m_RunningState = TetrisRunningState::Exit; +    } +    ImGui::End(); +} + +void Tetris::DrawLineCounter(RenderGroup &render_group) { +    V2F32 view_pos = {0.5f, 2.6f}; +    ImVec2 screen_pos = render_group.ViewPosToScreenPosImGui(view_pos); + +    ImGui::SetNextWindowPos(screen_pos); +    ImGui::Begin("TetrisLines", nullptr, s_DefaultImGuiWindowFlags); +    ImGui::Text("LINES - %d", m_LineCounter); +    ImGui::End(); +} + +void Tetris::DrawStatistics(RenderGroup &render_group) { +    V2F32 view_tetrominoes_pos = {0.4f, 1.8f}; +    V2F32 view_advance = {0.0f, 0.2f}; + +    V2F32 view_text_title_pos = view_tetrominoes_pos + V2F32(0.02f, 0.4f); +    ImVec2 screen_text_title_pos = render_group.ViewPosToScreenPosImGui(view_text_title_pos); + +    V2F32 view_text_pos = view_tetrominoes_pos + V2F32(0.4f, 0.16f); +    V2F32 view_text_gap = {0.0f, 0.124f}; +    ImVec2 screen_text_pos = render_group.ViewPosToScreenPosImGui(view_text_pos); +    ImVec2 screen_text_gap = render_group.ViewSizeToScreenSizeImGui(view_text_gap); + + +    using enum TetrominoId; + +    Tetromino::Draw(TETROMINO_T, 0, view_tetrominoes_pos, 0.5f, render_group); +    view_tetrominoes_pos.y -= view_advance.y; + +    Tetromino::Draw(TETROMINO_J, 0, view_tetrominoes_pos, 0.5f, render_group); +    view_tetrominoes_pos.y -= view_advance.y; + +    Tetromino::Draw(TETROMINO_Z, 0, view_tetrominoes_pos, 0.5f, render_group); +    view_tetrominoes_pos.y -= view_advance.y; + +    Tetromino::Draw(TETROMINO_O, 0, view_tetrominoes_pos, 0.5f, render_group); +    view_tetrominoes_pos.y -= view_advance.y; + +    Tetromino::Draw(TETROMINO_S, 0, view_tetrominoes_pos, 0.5f, render_group); +    view_tetrominoes_pos.y -= view_advance.y; + +    Tetromino::Draw(TETROMINO_L, 0, view_tetrominoes_pos, 0.5f, render_group); +    view_tetrominoes_pos.y -= view_advance.y; + +    Tetromino::Draw(TETROMINO_I, 0, view_tetrominoes_pos, 0.5f, render_group); +    view_tetrominoes_pos.y -= view_advance.y; + + +    ImGui::SetNextWindowPos(screen_text_title_pos); +    ImGui::Begin("TetrisStatisticsTitle", nullptr, s_DefaultImGuiWindowFlags); +    ImGui::Text("STATISTICS"); +    ImGui::End(); + + +    ImGui::SetNextWindowPos(screen_text_pos); +    ImGui::Begin("TetrisStatistics", nullptr, s_DefaultImGuiWindowFlags); + +    ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_T]); +    ImGui::Dummy(screen_text_gap); +    ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_J]); +    ImGui::Dummy(screen_text_gap); +    ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_Z]); +    ImGui::Dummy(screen_text_gap); +    ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_O]); +    ImGui::Dummy(screen_text_gap); +    ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_S]); +    ImGui::Dummy(screen_text_gap); +    ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_L]); +    ImGui::Dummy(screen_text_gap); +    ImGui::Text("%d", m_TetrominoCounters[(size_t)TETROMINO_I]); +    ImGui::Dummy(screen_text_gap); + +    ImGui::End(); +} + +void Tetris::DrawScore(RenderGroup &render_group) { +    V2F32 view_pos = {3.0f, 2.2f}; +    ImVec2 screen_pos = render_group.ViewPosToScreenPosImGui(view_pos); + +    ImGui::SetNextWindowPos(screen_pos); +    ImGui::Begin("TetrisScore", nullptr, s_DefaultImGuiWindowFlags); +    ImGui::Text("Score"); +    ImGui::Text("%d", m_Score); +    ImGui::End(); +} + +void Tetris::DrawNextTetromino(RenderGroup &render_group) { +    V2F32 text_view_pos = {3.0f, 1.8f}; +    ImVec2 text_screen_pos = render_group.ViewPosToScreenPosImGui(text_view_pos); + +    ImGui::SetNextWindowPos(text_screen_pos); +    ImGui::Begin("TetrisNextTetromino", nullptr, s_DefaultImGuiWindowFlags); +    ImGui::Text("Next"); +    ImGui::End(); + + +    V2F32 tetromino_view_pos = {3.0, 1.4f}; +    Tetromino::Draw(m_NextTetromino.GetId(), 0, tetromino_view_pos, 0.5f, render_group); +} + +void Tetris::DrawLevel(RenderGroup &render_group) { +    V2F32 view_pos = {3.0f, 1.2f}; +    ImVec2 screen_pos = render_group.ViewPosToScreenPosImGui(view_pos); + +    ImGui::SetNextWindowPos(screen_pos); +    ImGui::Begin("TetrisLevel", nullptr, s_DefaultImGuiWindowFlags); +    ImGui::Text("Level"); +    ImGui::Text("%d", m_Level); +    ImGui::End(); +} + diff --git a/src/games/tetris/Tetris.hpp b/src/games/tetris/Tetris.hpp new file mode 100644 index 0000000..30167d8 --- /dev/null +++ b/src/games/tetris/Tetris.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include <imgui.h> +#include <games/Game.hpp> +#include <games/tetris/Tetromino.hpp> +#include <games/tetris/Board.hpp> +#include <renderer/RenderGroup.hpp> + +enum class TetrisRunningState { +    Resume, +    Pause, +    GameOver, +    Restart, +    Exit +}; + + +class Tetris : public Game { +public: +    Tetris(); +    bool Update(std::vector<SDL_Event> &events, RenderGroup& render_group) override; +    void HandleTetrominoPlacement(); + +private: +    void Restart(); +    void UpdateResumeState(SDL_Event &event); +    void UpdatePauseState(SDL_Event &event); + +    uint32_t GetHarddropCount(float dt); +    void HandleGameOver(); + +    void Draw(RenderGroup &render_group); +    void DrawLineCounter(RenderGroup &render_group); +    void DrawStatistics(RenderGroup &render_group); +    void DrawScore(RenderGroup &render_group); +    void DrawNextTetromino(RenderGroup &render_group); +    void DrawLevel(RenderGroup &render_group); + +    void DrawPauseMenu(RenderGroup &render_group); +    void DrawGameOverMenu(RenderGroup &render_group); + +private: +    static constexpr ImGuiWindowFlags s_MenuImGuiWindowFlags = ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_AlwaysAutoResize; +    static constexpr ImGuiWindowFlags s_DefaultImGuiWindowFlags = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoScrollbar; + + +private: +    TetrisRunningState m_RunningState = TetrisRunningState::Resume; + +    float m_DtInSecondsRemaining = 0.0f; +    uint64_t m_MillisecondsSinceT0Last = SDL_GetTicks(); + +    Board m_Board; +    Tetromino m_ActiveTetromino; +    Tetromino m_NextTetromino; + +    int32_t m_TetrominoCounters[(size_t)TetrominoId::TETROMINO_ID_COUNT] {}; +    int32_t m_Score = 0; +    int32_t m_LineCounter = 0; +    int32_t m_StartingLevel = 0; +    int32_t m_Level = 0; +    int32_t m_SoftdropCounter = 0; + +    int32_t m_HighScore = 0; +}; + + diff --git a/src/games/tetris/Tetromino.cpp b/src/games/tetris/Tetromino.cpp new file mode 100644 index 0000000..577a869 --- /dev/null +++ b/src/games/tetris/Tetromino.cpp @@ -0,0 +1,212 @@ +#include <games/tetris/Tetromino.hpp> +#include <random> +#include <stdlib.h> + +// layout of a left_aligned_bitmap: xxxx000000000000 +// layout of a board_bitmap is 111xxxxxxxxxx111 +static const uint16_t s_left_aligned_bitmaps[7][4][4] = { +    { +        // O piece +        {0x6000, 0x6000, 0x0000, 0x0000}, // orientation 0 +        {0x6000, 0x6000, 0x0000, 0x0000}, // orientation 1 +        {0x6000, 0x6000, 0x0000, 0x0000}, // orientation 2 +        {0x6000, 0x6000, 0x0000, 0x0000}, // orientation 3 +    }, +    { +        // S piece +        {0x6000, 0x3000, 0x0000, 0x0000}, // orientation 0 +        {0x1000, 0x3000, 0x2000, 0x0000}, // orientation 1 +        {0x6000, 0x3000, 0x0000, 0x0000}, // orientation 2 +        {0x1000, 0x3000, 0x2000, 0x0000}, // orientation 3 +    }, +    { +        // Z piece +        {0x3000, 0x6000, 0x0000, 0x0000}, // orientation 0 +        {0x2000, 0x3000, 0x1000, 0x0000}, // orientation 1 +        {0x3000, 0x6000, 0x0000, 0x0000}, // orientation 2 +        {0x2000, 0x3000, 0x1000, 0x0000}, // orientation 3 +    }, +    { +        // T piece +        {0x2000, 0x7000, 0x0000, 0x0000}, // orientation 0 +        {0x2000, 0x6000, 0x2000, 0x0000}, // orientation 1 +        {0x0000, 0x7000, 0x2000, 0x0000}, // orientation 2 +        {0x2000, 0x3000, 0x2000, 0x0000}, // orientation 3 +    }, +    { +        // L piece +        {0x4000, 0x7000, 0x0000, 0x0000}, // orientation 0 +        {0x2000, 0x2000, 0x6000, 0x0000}, // orientation 1 +        {0x0000, 0x7000, 0x1000, 0x0000}, // orientation 2 +        {0x3000, 0x2000, 0x2000, 0x0000}, // orientation 3 +    }, +    { +        // J piece +        {0x1000, 0x7000, 0x0000, 0x0000}, // orientation 0 +        {0x6000, 0x2000, 0x2000, 0x0000}, // orientation 1 +        {0x0000, 0x7000, 0x4000, 0x0000}, // orientation 2 +        {0x2000, 0x2000, 0x3000, 0x0000}, // orientation 3 +    }, +    { +        // I piece +        {0x0000, 0xf000, 0x0000, 0x0000}, // orientation 0 +        {0x2000, 0x2000, 0x2000, 0x2000}, // orientation 1 +        {0x0000, 0xf000, 0x0000, 0x0000}, // orientation 2 +        {0x2000, 0x2000, 0x2000, 0x2000}, // orientation 3 +    } +}; + + +TetrominoId Tetromino::GetRandomId() { +    static std::uniform_int_distribution<int> s_Dist(0, (int)TetrominoId::TETROMINO_ID_COUNT-1); +    static std::mt19937 s_Rng((std::random_device()())); +    TetrominoId id = static_cast<TetrominoId>(s_Dist(s_Rng)); +    return id; +} + +Tetromino::Tetromino(uint16_t *board_bitmap) : +    m_Id(GetRandomId()), +    m_Pos{6, 20}, +    m_Ori{0}, +    m_BoardBitmap(board_bitmap) +{ +} + +Tetromino::Tetromino(Board &board) : +    Tetromino(board.m_Bitmap) +{ +} + +TetrominoId Tetromino::GetId() { +    return m_Id; +} + +BoardPos Tetromino::GetPos() { +    return m_Pos; +} + +void Tetromino::GetBitmap(uint16_t *bitmap) { +    GetBitmap(m_Id, m_Pos, m_Ori, bitmap); +} + +bool Tetromino::IsCollisionWithBoard() { +    bool is_collision = IsCollisionWithBoard(m_Id, m_Pos, m_Ori, m_BoardBitmap); +    return is_collision; +} + +void Tetromino::MaybeRotate(TetrominoRotation rotation) { +    int32_t rot = static_cast<int32_t>(rotation); +    int32_t ori = (m_Ori + rot) % 4; +    if (!IsCollisionWithBoard(m_Id, m_Pos, ori, m_BoardBitmap)) { +        m_Ori = ori; +    } +} + +void Tetromino::MaybeMoveHorizontally(TetrominoDirection direction) { +    BoardPos pos = m_Pos; +    pos.x += static_cast<int32_t>(direction); +    if (!IsCollisionWithBoard(m_Id, pos, m_Ori, m_BoardBitmap)) { +        m_Pos.x = pos.x; +    } +} + +bool Tetromino::MaybeMoveDown() { +    BoardPos pos = m_Pos; +    pos.y -= 1; +    if (!IsCollisionWithBoard(m_Id, pos, m_Ori, m_BoardBitmap)) { +        m_Pos.y = pos.y; +        return true; +    } +    return false; +} + +void Tetromino::Draw(RenderGroup &render_group) const { +    float world_width = 4.0f; +    float world_height = 3.0f; +    float tetromino_size_with_border = world_height / 20.0f; + +    float x0 = static_cast<float>(m_Pos.x - 3); +    float y0 = static_cast<float>(m_Pos.y - 2); + +    V2F32 world_pos = { +        ((world_width - tetromino_size_with_border*10) / 2.0f) + x0 * tetromino_size_with_border, +        y0 * tetromino_size_with_border +    }; + +    Tetromino::Draw(m_Id, m_Ori, world_pos, 1.0f, render_group); +} + +bool Tetromino::IsCollisionWithBoard(TetrominoId id, BoardPos pos, int32_t ori, uint16_t *board_bitmap) { +    uint16_t tetromino_bitmap[16]; +    GetBitmap(id, pos, ori, tetromino_bitmap); + +    uint64_t tetromino_bits = *(uint64_t*)(tetromino_bitmap); +    uint64_t board_bits = *(uint64_t*)(&board_bitmap[pos.y]); +    bool is_collision = tetromino_bits & board_bits; +    return is_collision; +} + +void Tetromino::GetBitmap(TetrominoId id, BoardPos pos, int32_t ori, uint16_t *bitmap) { +    size_t id_ = static_cast<size_t>(id); +    uint64_t *src  = (uint64_t*)s_left_aligned_bitmaps[id_][ori]; +    uint64_t *dest = (uint64_t*)bitmap; +    *dest = *src >> pos.x; +} + +V3F32 Tetromino::GetColor(TetrominoId id) { +    using enum TetrominoId; + +    V3F32 color; +    switch (id) { +        case TETROMINO_I: +        case TETROMINO_O: +        case TETROMINO_T: { +            color = V3F32(0.8f, 0.8f, 0.8f); +        } break; + +        case TETROMINO_J: +        case TETROMINO_S: { +            color = V3F32(0.8f, 0.2f, 0.2f); +        } break; + +        default: { +            color = V3F32(0.2f, 0.4f, 0.2f); +        } +    } +    return color; +} + +void Tetromino::Draw(TetrominoId id, int32_t ori, V2F32 pos, float scale, RenderGroup &render_group) { +    int32_t id_ = static_cast<int32_t>(id); + +    float world_height = 3.0f; +    float tetromino_size_with_border = scale * world_height / 20.0f; +    float tetromino_size = 0.8f * tetromino_size_with_border; +    float tetromino_offset = 0.1f * tetromino_size_with_border; + +    uint16_t *left_aligned_bitmap = (uint16_t*)s_left_aligned_bitmaps[id_][ori]; +    for (int y = 0; y < 4; y++) { +        for (int x = 0; x < 4; x++) { +            if (left_aligned_bitmap[y] & (0x8000 >> x)) { +                V2F32 local_pos = { +                    (float)x * tetromino_size_with_border + tetromino_offset, +                    (float)y * tetromino_size_with_border + tetromino_offset +                }; +                V2F32 local_dim = {tetromino_size, tetromino_size}; + + +                V3F32 world_pos = { +                    pos.x + local_pos.x, +                    pos.y + local_pos.y, +                    1.0f +                }; +                V2F32 world_dim = local_dim; + + +                V3F32 color = GetColor(id); +                render_group.PushRectangle(world_pos, world_dim, color); +            } +        } +    } +} + diff --git a/src/games/tetris/Tetromino.hpp b/src/games/tetris/Tetromino.hpp new file mode 100644 index 0000000..c3ceeff --- /dev/null +++ b/src/games/tetris/Tetromino.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include <basic/defs.hpp> +#include <basic/math.hpp> +#include <renderer/RenderGroup.hpp> +#include <games/tetris/Board.hpp> + +enum class TetrominoId : uint8_t { +    TETROMINO_O = 0, +    TETROMINO_S, +    TETROMINO_Z, +    TETROMINO_T, +    TETROMINO_L, +    TETROMINO_J, +    TETROMINO_I, +    TETROMINO_ID_COUNT, +    TETROMINO_ID_NONE, +}; +enum class TetrominoRotation {Clockwise = 1, CounterClockwise = 3}; +enum class TetrominoDirection {Left = -1, Right = 1}; + + +class Tetromino { +public: +    Tetromino() = delete; +    Tetromino(Board &board); +    Tetromino(uint16_t *board_bitmap); + +    TetrominoId GetId(); +    BoardPos GetPos(); +    int32_t GetOri(); +    void GetBitmap(uint16_t *bitmap); +    bool IsCollisionWithBoard(); // for last tetromino to check game over + +    bool MaybeMoveDown(); +    void MaybeMoveHorizontally(TetrominoDirection direction); +    void MaybeRotate(TetrominoRotation rotation); + +    void Draw(RenderGroup &render_group) const; + + +public: +    static bool IsCollisionWithBoard(TetrominoId id, BoardPos pos, int32_t ori, uint16_t *board_bitmap); +    static void GetBitmap(TetrominoId id, BoardPos pos, int32_t ori, uint16_t *bitmap); +    static V3F32 GetColor(TetrominoId id); +    static void Draw(TetrominoId id, int32_t ori, V2F32 pos, float scale, RenderGroup &render_group); + + +private: +    static TetrominoId GetRandomId(); + + +private: +    TetrominoId m_Id; +    BoardPos m_Pos; +    int32_t m_Ori; +    uint16_t *m_BoardBitmap; +}; +  | 
