aboutsummaryrefslogtreecommitdiff
path: root/src/games/tetris/Tetris.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/games/tetris/Tetris.cpp')
-rw-r--r--src/games/tetris/Tetris.cpp368
1 files changed, 368 insertions, 0 deletions
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();
+}
+