aboutsummaryrefslogtreecommitdiff
path: root/src/games/tetris
diff options
context:
space:
mode:
Diffstat (limited to 'src/games/tetris')
-rw-r--r--src/games/tetris/Board.cpp139
-rw-r--r--src/games/tetris/Board.hpp30
-rw-r--r--src/games/tetris/Tetris.cpp368
-rw-r--r--src/games/tetris/Tetris.hpp67
-rw-r--r--src/games/tetris/Tetromino.cpp212
-rw-r--r--src/games/tetris/Tetromino.hpp59
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;
+};
+