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/tetris |
Diffstat (limited to 'src/games/tetris')
-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 |
6 files changed, 875 insertions, 0 deletions
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; +}; + |