From 4537e9bee3d054786857fa92824e2d9e8385bb36 Mon Sep 17 00:00:00 2001 From: fschildt Date: Sun, 28 Sep 2025 11:23:04 +0200 Subject: minesweeper: now playable --- src/games/minesweeper/Minesweeper.cpp | 387 +++++++++++++++++++++------------- 1 file changed, 236 insertions(+), 151 deletions(-) (limited to 'src/games/minesweeper/Minesweeper.cpp') diff --git a/src/games/minesweeper/Minesweeper.cpp b/src/games/minesweeper/Minesweeper.cpp index abb9515..8f20268 100644 --- a/src/games/minesweeper/Minesweeper.cpp +++ b/src/games/minesweeper/Minesweeper.cpp @@ -3,72 +3,94 @@ #include #include +#include -// Todo: draw a difficulty selection menu at top-mid. +// Todo: winning condition (maybe: total_cells - uncovered_cells = mine_count) -// 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(RenderGroup& render_group) + : m_render_group {render_group} +{ + m_font.Init("fonts/dejavu_ttf/DejaVuSansMono.ttf", 16); + for (uint32_t i = 0; i < m_digit_glyphs.size(); ++i) { + m_font.LoadGlyph(m_digit_glyphs[i], i + '0'); + } +} -Minesweeper::Minesweeper() +void +Minesweeper::Reset(Difficulty difficulty) { - float map_width = static_cast(m_MapWidth); - float map_height = static_cast(m_MapHeight); - float cell_size = 0.8f * std::min(m_WorldHeight / MAX_MAP_HEIGHT, m_WorldWidth / MAX_MAP_WIDTH); + int32_t mine_count; + if (difficulty == beginner) { + m_grid_width = 8; + m_grid_height = 8; + mine_count = 10; + } + else if(difficulty == intermediate) { + m_grid_width = 16; + m_grid_height = 16; + mine_count = 40; + } + else { + m_grid_width = 30; + m_grid_height = 16; + mine_count = 99; + } + + + float cell_size = 0.8f * std::min(m_world_height / MAX_MAP_HEIGHT, m_world_width / 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 + V2F32 grid_size = { + (float)m_grid_width * cell_size, + (float)m_grid_height * cell_size + }; + m_grid_pos = { + (m_world_width - grid_size.x) / 2, + (m_world_height - grid_size.y) / 2, }; - m_CellOuterViewSize = {cell_size, cell_size}; - m_CellInnerViewSize = {cell_size_without_border, cell_size_without_border}; - m_Font.Init("fonts/dejavu_ttf/DejaVuSansMono.ttf", 32); - for (uint32_t i = 0; i < m_DigitGlyphs.size(); ++i) { - m_Font.LoadGlyph(m_DigitGlyphs[i], i + '0'); - } + m_cell_outer_size = {cell_size, cell_size}; + m_cell_inner_size = {cell_size_without_border, cell_size_without_border}; - Reinit(); -} - -void Minesweeper::Reinit() { - int32_t mine_count = 40; - memset(m_IsCoveredBitmap, 0xff, sizeof(m_IsCoveredBitmap)); - memset(m_IsFlaggedBitmap, 0 , sizeof(m_IsFlaggedBitmap)); + memset(m_is_covered_bitmap, 0xff, sizeof(m_is_covered_bitmap)); + memset(m_is_flagged_bitmap, 0 , sizeof(m_is_flagged_bitmap)); InitIsMineBitmap(mine_count); InitAdjacentMineCounters(); } -void Minesweeper::InitIsMineBitmap(int32_t mine_count) { - assert(mine_count < m_MapWidth * m_MapHeight); +void +Minesweeper::InitIsMineBitmap(int32_t mine_count) +{ + assert(mine_count < m_grid_width * m_grid_height); - memset(m_IsMineBitmap, 0 , sizeof(m_IsMineBitmap)); + memset(m_is_mine_bitmap, 0 , sizeof(m_is_mine_bitmap)); std::mt19937 rng((std::random_device()())); - std::uniform_int_distribution dist(0, m_MapWidth * m_MapHeight - 1); + std::uniform_int_distribution dist(0, m_grid_width * m_grid_height - 1); + while (mine_count) { int32_t random_pos = dist(rng); - int32_t x = random_pos / m_MapWidth; - int32_t y = random_pos % m_MapWidth; + int32_t y = random_pos / m_grid_width; + int32_t x = random_pos % m_grid_width; if (!IsMine(x, y)) { - m_IsMineBitmap[y] |= 1 << x; + m_is_mine_bitmap[y] |= 1 << x; mine_count--; } } } -void Minesweeper::InitAdjacentMineCounters() { - for (int32_t y = 0; y < m_MapHeight; y++) { +void +Minesweeper::InitAdjacentMineCounters() +{ + for (int32_t y = 0; y < m_grid_height; y++) { int32_t y0 = y > 0 ? y-1 : y; - int32_t y1 = y < m_MapHeight-1 ? y+1 : y; + int32_t y1 = y < m_grid_height-1 ? y+1 : y; - for (int32_t x = 0; x < m_MapHeight; x++) { + for (int32_t x = 0; x < m_grid_width; x++) { int32_t x0 = x > 0 ? x-1 : x; - int32_t x1 = x < m_MapWidth-1 ? x+1 : x; + int32_t x1 = x < m_grid_width-1 ? x+1 : x; int32_t adjacent_mine_counter = 0; for (int32_t inner_y = y0; inner_y <= y1; inner_y++) { @@ -82,71 +104,78 @@ void Minesweeper::InitAdjacentMineCounters() { adjacent_mine_counter = -1; } - m_AdjacentMineCounters[y * m_MapWidth + x] = adjacent_mine_counter; + m_adjacent_mine_counts[y * m_grid_width + x] = adjacent_mine_counter; } } } -bool Minesweeper::Update(std::vector &events, RenderGroup &render_group) { +bool +Minesweeper::Update(std::vector& events) +{ Color 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; - } + m_render_group.SetCameraSize(4.0f, 3.0f); + m_render_group.Clear(clear_color); for (SDL_Event &event : events) { - if (m_RunState == MinesweeperRunState::Exit) { + if (m_run_state == exit) { return false; } - else if (m_RunState == MinesweeperRunState::Pause) { - ProcessEventDuringPause(event, render_group); + else if (m_run_state == pause) { + ProcessEventDuringPause(event); } - else if (m_RunState == MinesweeperRunState::Resume) { - ProcessEventDuringResume(event, render_group); + else if (m_run_state == resume) { + ProcessEventDuringResume(event); } } - if (m_RunState == MinesweeperRunState::Pause) { - DrawPauseMenu(render_group); + if (m_run_state == pause) { + DrawBoard(); + DrawPauseMenu(); } - else if (m_RunState == MinesweeperRunState::GameOver) { - DrawGameOverMenu(render_group); + else if (m_run_state == start_menu) { + DrawStartMenu(); + } + else if (m_run_state == resume) { + DrawBoard(); + } + else if (m_run_state == game_over) { + DrawBoard(); + DrawGameOverMenu(); } - DrawBoard(render_group); - - bool keep_running = m_RunState != MinesweeperRunState::Exit; + bool keep_running = m_run_state != exit; return keep_running; } -void Minesweeper::ProcessEventDuringPause(SDL_Event &event, RenderGroup &render_group) { +void +Minesweeper::ProcessEventDuringPause(SDL_Event &event) +{ switch (event.type) { case SDL_EVENT_KEY_DOWN: { if (event.key.key == SDLK_ESCAPE) { - m_RunState = MinesweeperRunState::Resume; + m_run_state = resume; } } break; default:; } } -void Minesweeper::ProcessEventDuringResume(SDL_Event &event, RenderGroup &render_group) { +void +Minesweeper::ProcessEventDuringResume(SDL_Event &event) +{ switch (event.type) { case SDL_EVENT_KEY_DOWN: { if (event.key.key == SDLK_ESCAPE) { - m_RunState = MinesweeperRunState::Pause; + m_run_state = 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); + V2F32 click_screen_pos = {event.button.x, (float)m_render_group.m_ScreenHeight -1 - event.button.y}; + V2F32 click_view_pos = ScreenPosToViewPos(click_screen_pos, m_render_group); - float x_adjusted = click_view_pos.x - m_MapViewPos.x; - float y_adjusted = click_view_pos.y - m_MapViewPos.y; + float x_adjusted = click_view_pos.x - m_grid_pos.x; + float y_adjusted = click_view_pos.y - m_grid_pos.y; if (x_adjusted < 0.0f) { break; } @@ -154,20 +183,21 @@ void Minesweeper::ProcessEventDuringResume(SDL_Event &event, RenderGroup &render 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) { + int32_t x = (int32_t)(x_adjusted / m_cell_outer_size.x); + int32_t y = (int32_t)(y_adjusted / m_cell_outer_size.y); + if (x >= m_grid_width) { break; } - if (y >= m_MapHeight) { + if (y >= m_grid_height) { break; } if (event.button.button == 1) { if (IsCovered(x, y)) { if (IsMine(x, y)) { - m_IsCoveredBitmap[y] &= ~(1 << x); - m_RunState = MinesweeperRunState::GameOver; + m_is_covered_bitmap[y] &= ~(1 << x); + UncoverMines(); + m_run_state = game_over; } else { Uncover(x, y); @@ -186,20 +216,21 @@ void Minesweeper::ProcessEventDuringResume(SDL_Event &event, RenderGroup &render } } -// Todo: maybe find a more efficient non-naive solution -void Minesweeper::Uncover(int32_t x, int32_t y) { +void +Minesweeper::Uncover(int32_t x, int32_t y) +{ if (x < 0) return; - if (x >= m_MapWidth) return; + if (x >= m_grid_width) return; if (y < 0) return; - if (y >= m_MapHeight) return; + if (y >= m_grid_height) return; if (!IsCovered(x, y)) return; - m_IsCoveredBitmap[y] &= ~(1 << x); + m_is_covered_bitmap[y] &= ~(1 << x); if (IsFlagged(x, y)) { ToggleFlag(x, y); } - if (m_AdjacentMineCounters[y*m_MapWidth + x] > 0) { + if (m_adjacent_mine_counts[y*m_grid_width + x] > 0) { return; } Uncover(x-1, y-1); @@ -214,137 +245,191 @@ void Minesweeper::Uncover(int32_t x, int32_t y) { Uncover(x+1, y+1); } -void Minesweeper::ToggleFlag(int32_t x, int32_t y) { - m_IsFlaggedBitmap[y] ^= (1 << x); +void +Minesweeper::UncoverMines() +{ + for (int32_t y{0}; y < m_grid_height; ++y) { + for (int32_t x{0}; x < m_grid_width; ++x) { + if (IsMine(x, y) && IsCovered(x, y)) { + m_is_covered_bitmap[y] &= ~(1 << x); + } + } + } +} + +void +Minesweeper::ToggleFlag(int32_t x, int32_t y) +{ + m_is_flagged_bitmap[y] ^= (1 << x); } -bool Minesweeper::IsCovered(int32_t x, int32_t y) { - bool is_covered = m_IsCoveredBitmap[y] & 1 << x; +bool +Minesweeper::IsCovered(int32_t x, int32_t y) +{ + bool is_covered = m_is_covered_bitmap[y] & 1 << x; return is_covered; } -bool Minesweeper::IsFlagged(int32_t x, int32_t y) { - bool is_flagged = m_IsFlaggedBitmap[y] & 1 << x; +bool +Minesweeper::IsFlagged(int32_t x, int32_t y) +{ + bool is_flagged = m_is_flagged_bitmap[y] & 1 << x; return is_flagged; } -bool Minesweeper::IsMine(int32_t x, int32_t y) { - bool is_mine = m_IsMineBitmap[y] & 1 << x; +bool +Minesweeper::IsMine(int32_t x, int32_t y) +{ + bool is_mine = m_is_mine_bitmap[y] & 1 << x; return is_mine; } -V2F32 Minesweeper::ScreenPosToViewPos(V2F32 screen_pos, RenderGroup &render_group) { +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; + view_pos.x = (screen_pos.x / screen_width) * m_world_width; + view_pos.y = (screen_pos.y / screen_height) * m_world_height; return view_pos; } -void Minesweeper::DrawPauseMenu(RenderGroup &render_group) { +void +Minesweeper::DrawPauseMenu() +{ ImGui::Begin("MinesweeperPause"); if (ImGui::Button("Resume")) { - m_RunState = MinesweeperRunState::Resume; + m_run_state = resume; + } + if (ImGui::Button("Restart")) { + m_run_state = start_menu; } if (ImGui::Button("Exit")) { - m_RunState = MinesweeperRunState::Exit; + m_run_state = exit; } ImGui::End(); } -void Minesweeper::DrawGameOverMenu(RenderGroup &render_group) { - ImGui::Begin("MinesweeperGameOver"); - ImGui::Text("Score = ???"); - if (ImGui::Button("Restart")) { - m_RunState = MinesweeperRunState::Restart; +void +Minesweeper::DrawStartMenu() +{ + ImGui::Begin("MinesweeperStartMenu"); + if (ImGui::RadioButton("beginner", m_difficulty == beginner ? true : false)) { + m_difficulty = beginner; + } + if (ImGui::RadioButton("intermediate", m_difficulty == intermediate ? true : false)) { + m_difficulty = intermediate; + } + if (ImGui::RadioButton("expert", m_difficulty == expert ? true : false)) { + m_difficulty = expert; + } + if (ImGui::Button("Start")) { + Reset(m_difficulty); + m_run_state = resume; } if (ImGui::Button("Exit")) { - m_RunState = MinesweeperRunState::Exit; + m_run_state = exit; } ImGui::End(); } -void Minesweeper::DrawBoard(RenderGroup &render_group) { - Color covered_cell_color = {0.4f, 0.4f, 0.4f}; - Color uncovered_cell_color = {0.2f, 0.2f, 0.2f}; - Color flag_color = {0.6f, 0.3f, 03.f}; - Color mine_color = {0.8f, 0.2f, 0.2f}; +void +Minesweeper::DrawGameOverMenu() +{ + ImGui::Begin("MinesweeperGameOverMenu"); + if (ImGui::Button("Play Again")) { + m_run_state = start_menu; + } + if (ImGui::Button("Exit")) { + m_run_state = exit; + } + ImGui::End(); +} - 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 +void +Minesweeper::DrawBoard() +{ + Color covered_cell_color {0.6f, 0.6f, 0.6f}; + Color uncovered_cell_color {0.4f, 0.4f, 0.4f}; + Color mine_color {0.8f, 0.2f, 0.2f}; + + Color flag_color {0.6f, 0.3f, 03.f}; + V2F32 flag_size = {m_cell_inner_size.x * 0.5f, m_cell_inner_size.y * 0.5f}; + V2F32 flag_offset = { + (m_cell_inner_size.x - flag_size.x) / 2, + (m_cell_inner_size.y - flag_size.y) / 2 }; - // Temporary: Drawing Glyph Test - render_group.PushBitmap( - {100.0f, 100.0f, 10.0f}, - m_DigitGlyphs[1].bitmap.width, - m_DigitGlyphs[1].bitmap.height, - m_DigitGlyphs[1].bitmap.pixels.get()); - - - for (int32_t y = 0; y < m_MapHeight; y++) { - for (int32_t x = 0; x < m_MapWidth; x++) { - V2F32 world_pos = { - m_MapViewPos.x + (float)x * m_CellOuterViewSize.x, - m_MapViewPos.y + (float)y * m_CellOuterViewSize.y, + for (int32_t y = 0; y < m_grid_height; y++) { + for (int32_t x = 0; x < m_grid_width; x++) { + V2F32 cell_pos = { + m_grid_pos.x + (float)x * m_cell_outer_size.x, + m_grid_pos.y + (float)y * m_cell_outer_size.y, + }; + RectF32 cell_rect = { + cell_pos.x, cell_pos.y, + cell_pos.x + m_cell_inner_size.x, cell_pos.y + m_cell_inner_size.y }; + bool is_covered = IsCovered(x, y); bool is_flagged = IsFlagged(x, y); bool is_mine = IsMine(x, y); if (is_covered) { - RectF32 cell_world_rect = { - world_pos.x, world_pos.y, - world_pos.x + m_CellInnerViewSize.x, world_pos.y + m_CellInnerViewSize.y - }; - render_group.PushRectangle(cell_world_rect, 0.0f, covered_cell_color); + m_render_group.PushRectangle(cell_rect, 0.0f, covered_cell_color); if (is_flagged) { - V3F32 flag_world_pos = { - world_pos.x + flag_draw_offset.x, - world_pos.y + flag_draw_offset.y, + V3F32 flag_pos = { + cell_pos.x + flag_offset.x, + cell_pos.y + flag_offset.y, 1.0f }; - RectF32 flag_world_rect = { - flag_world_pos.x, - flag_world_pos.y, - flag_world_pos.x + m_CellInnerViewSize.x, - flag_world_pos.y + m_CellInnerViewSize.y, + RectF32 flag_rect = { + flag_pos.x, + flag_pos.y, + flag_pos.x + flag_size.x, + flag_pos.y + flag_size.y, }; - render_group.PushRectangle(flag_world_rect, flag_world_pos.z, flag_color); + m_render_group.PushRectangle(flag_rect, flag_pos.z, flag_color); } } else { - V3F32 mine_world_pos = { - world_pos.x, - world_pos.y, - 1.0f - }; if (is_mine) { - RectF32 mine_world_rect = { - mine_world_pos.x, - mine_world_pos.y, - mine_world_pos.x + m_CellInnerViewSize.x, - mine_world_pos.y + m_CellInnerViewSize.y, + V3F32 mine_pos = { + cell_pos.x, + cell_pos.y, + 1.0f + }; + RectF32 mine_rect = { + mine_pos.x, + mine_pos.y, + mine_pos.x + m_cell_inner_size.x, + mine_pos.y + m_cell_inner_size.y, }; - render_group.PushRectangle(mine_world_rect, mine_world_pos.z, mine_color); + m_render_group.PushRectangle(mine_rect, mine_pos.z, mine_color); } else { - RectF32 mine_world_rect = { - mine_world_pos.x, - mine_world_pos.y, - mine_world_pos.x + m_CellInnerViewSize.x, - mine_world_pos.y + m_CellInnerViewSize.y, + m_render_group.PushRectangle(cell_rect, 0.0f, uncovered_cell_color); + + // Todo: Figure out how to scale this properly. + // 256.0f is a random number that just works for now. + V3F32 mine_count_pos = { + 256.0f * cell_pos.x, + 256.0f * cell_pos.y, + 256.0f }; - render_group.PushRectangle(mine_world_rect, mine_world_pos.z, uncovered_cell_color); + size_t mine_count_val = (size_t)m_adjacent_mine_counts[y*m_grid_width + x]; + m_render_group.PushBitmap( + mine_count_pos, + m_digit_glyphs[mine_count_val].bitmap.width, + m_digit_glyphs[mine_count_val].bitmap.height, + m_digit_glyphs[mine_count_val].bitmap.pixels.get()); } } } -- cgit v1.2.3