#include #include #include #include #include #include #include #include #include static std::u32string int32_to_u32string(int32_t value) { std::string str = std::to_string(value); return std::u32string(str.begin(), str.end()); } Tetris::Tetris() : m_font{s_dejavu_sans_mono_filepath, 22} , m_active_tetromino{m_board.m_bitmap} , m_highscore {ReadHighscore()} { m_frame_strings.reserve(s_frame_strings_capacity); } void Tetris::Start() { m_game_status = game_resuming; m_tlast_milliseconds = SDL_GetTicks(); m_dt_remaining_seconds = 0.0f; m_board.Reset(); m_active_tetromino.Reset(Tetromino::GenerateRandomId()); m_next_tetromino_id = Tetromino::GenerateRandomId(); memset(m_tetromino_counters, 0, sizeof(m_tetromino_counters)); m_tetromino_counters[m_active_tetromino.GetId()] += 1; m_score = 0; m_line_counter = 0; m_starting_level = 0; m_level = 0; m_softdrop_counter = 0; } bool Tetris::Update(std::vector& events) { Color clear_color = {0.2f, 0.2f, 0.2f, 1.0f}; g_renderer.SetCameraSize(4.0f, 3.0f); g_renderer.Clear(clear_color); m_frame_strings.clear(); if (m_game_status == game_starting) { Start(); } float dt = ProcessDt(); if (m_game_status == game_resuming) { uint32_t softdrop_count = GetSoftdropCount(dt); for (uint32_t i{0}; i < softdrop_count; i++) { bool moved_down = m_active_tetromino.MaybeMoveDown(); if (!moved_down) { HandleTetrominoPlacement(); } } } for (auto& event : events) { switch (m_game_status) { case game_resuming: UpdateResumeState(event); break; case game_paused: UpdatePauseState(event); break; default:; } } if (m_game_status == game_exit) { return false; } Draw(); assert((float)m_frame_strings.size() <= 0.8f * s_frame_strings_capacity); return true; } void Tetris::UpdateResumeState(SDL_Event& event) { switch (event.type) { case SDL_EVENT_KEY_DOWN: { auto key = event.key.key; if (key == SDLK_RIGHT) { m_active_tetromino.MaybeMoveHorizontally(Tetromino::right); } else if (key == SDLK_LEFT) { m_active_tetromino.MaybeMoveHorizontally(Tetromino::left); } else if (key == SDLK_DOWN) { bool moved_down = m_active_tetromino.MaybeMoveDown(); if (!moved_down) { HandleTetrominoPlacement(); } else { m_softdrop_counter++; } } else if (key == SDLK_X) { m_active_tetromino.MaybeRotate(Tetromino::rotate_clockwise); } else if (key == SDLK_Z || key == SDLK_Y) { m_active_tetromino.MaybeRotate(Tetromino::rotate_counter_clockwise); } else if (key == SDLK_ESCAPE) { m_game_status = game_paused; } } 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_game_status = game_resuming; } } default:; } } void Tetris::HandleTetrominoPlacement() { int32_t rows_cleared = m_board.PlaceTetromino(m_active_tetromino); m_active_tetromino.Reset(m_next_tetromino_id); m_next_tetromino_id = Tetromino::GenerateRandomId(); if (m_active_tetromino.IsCollisionWithBoard()) { m_game_status = game_over; if (m_score > m_highscore) { m_highscore = m_score; WriteHighscore(); } } m_line_counter += rows_cleared; m_tetromino_counters[m_active_tetromino.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_softdrop_counter; m_softdrop_counter = 0; m_level = m_starting_level + m_line_counter / 10; } uint32_t Tetris::GetSoftdropCount(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(nes_frames_per_cell) * nes_frame_time; uint32_t softdrop_count = 0; while (dt > dt_level) { softdrop_count += 1; dt -= dt_level; } m_dt_remaining_seconds = dt; return softdrop_count; } int32_t Tetris::ReadHighscore() { int32_t highscore = 0; std::ifstream highscore_file_in {s_tetris_highscore_path}; if (highscore_file_in) { highscore_file_in >> highscore; highscore_file_in.close(); } else { SDL_LogInfo(0, "Tetris: cannot open %s for reading", s_tetris_highscore_path); } return highscore; } void Tetris::WriteHighscore() { std::ofstream highscore_file_out {s_tetris_highscore_path}; if (highscore_file_out) { highscore_file_out << m_highscore << std::endl; highscore_file_out.close(); } else { SDL_LogInfo(0, "Tetris: cannot open %s for writing", s_tetris_highscore_path); } } void Tetris::Draw() { m_board.Draw(m_level); m_active_tetromino.Draw(); DrawNextTetromino(); DrawStatistics(); DrawLineCounter(); DrawLevel(); DrawScore(); if (m_game_status == game_paused) { DrawDefaultGamePausedMenu(); } else if (m_game_status == game_over) { DrawGameOverMenu(); } } void Tetris::DrawGameOverMenu() { ImGui::Begin("TetrisGameOver", nullptr, s_imgui_window_flags_menu); ImGui::Text("Score = %d", m_score); ImGui::Text("HighScore = %d", m_highscore); if (ImGui::Button("Restart")) { m_game_status = game_starting; } if (ImGui::Button("Exit")) { m_game_status = game_exit; } ImGui::End(); } void Tetris::DrawLineCounter() { static std::u32string text = U"Lines: xxx"; V3F32 pos = {0.5f, 2.6f, 4.0f}; Color color = {0.9f, 0.9f, 0.9f, 1.0f}; int line_count = std::min(m_line_counter, 999); text[9] = U'0' + char32_t(line_count % 10); line_count /= 10; text[8] = U'0' + char32_t(line_count % 10); line_count /= 10; text[7] = U'0' + char32_t(line_count % 10); line_count /= 10; g_renderer.PushText(text, m_font, pos, color); pos.x += 0.2f; } void Tetris::DrawStatistics() { V3F32 pos = {0.4f, 0.5f, s_text_z}; static std::u32string title_text = U"Statistics"; V3F32 title_pos = {pos.x + 0.02f, pos.y + 1.64f, pos.z}; g_renderer.PushText(title_text, m_font, title_pos, s_text_color); float yadvance = -0.2f; float tetrominoes_x0 = pos.x; float tetrominoes_y0 = pos.y - (float)Tetromino::id_count * yadvance; V2F32 tetromino_pos = {tetrominoes_x0, tetrominoes_y0}; Tetromino::Draw(Tetromino::t_piece, 0, tetromino_pos, 0.5f); tetromino_pos.y += yadvance; Tetromino::Draw(Tetromino::j_piece, 0, tetromino_pos, 0.5f); tetromino_pos.y += yadvance; Tetromino::Draw(Tetromino::z_piece, 0, tetromino_pos, 0.5f); tetromino_pos.y += yadvance; Tetromino::Draw(Tetromino::o_piece, 0, tetromino_pos, 0.5f); tetromino_pos.y += yadvance; Tetromino::Draw(Tetromino::s_piece, 0, tetromino_pos, 0.5f); tetromino_pos.y += yadvance; Tetromino::Draw(Tetromino::l_piece, 0, tetromino_pos, 0.5f); tetromino_pos.y += yadvance; Tetromino::Draw(Tetromino::i_piece, 0, tetromino_pos, 0.5f); tetromino_pos.y += yadvance; float counters_x0 = pos.x + 0.4f; float counters_y0 = pos.y + 0.05f - yadvance * (float)Tetromino::id_count; V3F32 counters_pos = {counters_x0, counters_y0, s_text_z}; std::u32string& t_count = m_frame_strings.emplace_back(int32_to_u32string(m_tetromino_counters[Tetromino::t_piece])); std::u32string& j_count = m_frame_strings.emplace_back(int32_to_u32string(m_tetromino_counters[Tetromino::j_piece])); std::u32string& z_count = m_frame_strings.emplace_back(int32_to_u32string(m_tetromino_counters[Tetromino::z_piece])); std::u32string& o_count = m_frame_strings.emplace_back(int32_to_u32string(m_tetromino_counters[Tetromino::o_piece])); std::u32string& s_count = m_frame_strings.emplace_back(int32_to_u32string(m_tetromino_counters[Tetromino::s_piece])); std::u32string& l_count = m_frame_strings.emplace_back(int32_to_u32string(m_tetromino_counters[Tetromino::l_piece])); std::u32string& i_count = m_frame_strings.emplace_back(int32_to_u32string(m_tetromino_counters[Tetromino::i_piece])); g_renderer.PushText(t_count, m_font, counters_pos, s_text_color); counters_pos.y += yadvance; g_renderer.PushText(j_count, m_font, counters_pos, s_text_color); counters_pos.y += yadvance; g_renderer.PushText(z_count, m_font, counters_pos, s_text_color); counters_pos.y += yadvance; g_renderer.PushText(o_count, m_font, counters_pos, s_text_color); counters_pos.y += yadvance; g_renderer.PushText(s_count, m_font, counters_pos, s_text_color); counters_pos.y += yadvance; g_renderer.PushText(l_count, m_font, counters_pos, s_text_color); counters_pos.y += yadvance; g_renderer.PushText(i_count, m_font, counters_pos, s_text_color); } void Tetris::DrawScore() { V3F32 pos = {3.0f, 2.6f, s_text_z}; std::u32string& top_label = m_frame_strings.emplace_back(U"Top"); std::u32string& top_value = m_frame_strings.emplace_back(int32_to_u32string(m_highscore)); std::u32string& score_label = m_frame_strings.emplace_back(U"Score"); std::u32string& score_value = m_frame_strings.emplace_back(int32_to_u32string(m_score)); g_renderer.PushText(top_label, m_font, pos, s_text_color); pos.y -= 0.1f; g_renderer.PushText(top_value, m_font, pos, s_text_color); pos.y -= 0.2f; g_renderer.PushText(score_label, m_font, pos, s_text_color); pos.y -= 0.1f; g_renderer.PushText(score_value, m_font, pos, s_text_color); } void Tetris::DrawNextTetromino() { V3F32 pos = {3.0f, 1.4f, s_text_z}; V3F32 label_pos = {pos.x, pos.y + 0.4f, pos.z}; std::u32string& label_text = m_frame_strings.emplace_back(U"Next:"); g_renderer.PushText(label_text, m_font, label_pos, s_text_color); V2F32 tetromino_pos = {pos.x, pos.y}; Tetromino::Draw(m_next_tetromino_id, 0, tetromino_pos, 0.5f); } void Tetris::DrawLevel() { V3F32 pos = {3.0f, 1.1f}; std::u32string& label = m_frame_strings.emplace_back(U"Level"); g_renderer.PushText(label, m_font, pos, s_text_color); pos.y -= 0.1f; std::u32string& level = m_frame_strings.emplace_back(int32_to_u32string(m_level)); g_renderer.PushText(level, m_font, pos, s_text_color); }