From 23316594f794182dd1d4b7e4ad4aa9e46c44e803 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Wed, 25 Feb 2026 13:36:13 +0000 Subject: [PATCH] updates and fixes --- meson.build | 10 +- src/minefield.cpp | 542 ++++++++------------- src/minefield.hpp | 152 +++--- src/timer.cpp | 81 ---- src/timer.hpp | 42 -- src/window.cpp | 1181 ++++----------------------------------------- src/window.hpp | 112 ++--- 7 files changed, 383 insertions(+), 1737 deletions(-) delete mode 100644 src/timer.cpp delete mode 100644 src/timer.hpp diff --git a/meson.build b/meson.build index 46ccacd..4d00418 100644 --- a/meson.build +++ b/meson.build @@ -11,16 +11,20 @@ res = gnome.compile_resources( ) # Dependencies -deps = dependency(['gtkmm-4.0', 'sigc++-3.0']) +deps = [ + dependency('gtkmm-4.0'), + dependency('sigc++-3.0') +] # Source files src = [ + 'src/main.cpp', 'src/window.cpp', 'src/window.hpp', 'src/minefield.hpp', 'src/minefield.cpp', - 'src/timer.hpp', - 'src/timer.cpp', + 'src/board_widget.hpp', + 'src/board_widget.cpp', res ] diff --git a/src/minefield.cpp b/src/minefield.cpp index 6d0369a..f083937 100644 --- a/src/minefield.cpp +++ b/src/minefield.cpp @@ -1,276 +1,153 @@ #include "minefield.hpp" -#include #include #include -#include +#include -// Define difficulty presets -const GameDifficulty MineField::DIFFICULTY_EASY = {"Beginner", 9, 9, 10}; -const GameDifficulty MineField::DIFFICULTY_MEDIUM = {"Intermediate", 16, 16, 40}; -const GameDifficulty MineField::DIFFICULTY_HARD = {"Expert", 30, 16, 99}; -const GameDifficulty MineField::DIFFICULTY_EXPERT = {"Master", 30, 20, 145}; +const GameDifficulty Minefield::DifficultyEasy = {"Beginner", 9, 9, 10}; +const GameDifficulty Minefield::DifficultyMedium = {"Intermediate", 16, 16, 40}; +const GameDifficulty Minefield::DifficultyHard = {"Expert", 30, 16, 99}; +const GameDifficulty Minefield::DifficultyExpert = {"Master", 30, 20, 145}; -MineField::MineField(int cols, int rows, int mines) : - m_rows(rows), - m_cols(cols), - m_totalMines(mines), - m_remainingFlags(mines), - m_openCells(0), - m_gameState(GameState::READY), - m_timerRunning(false) -{ - // Create cells - m_cells.reserve(m_cols * m_rows); - for (int i = 0; i < m_cols * m_rows; i++) - { - std::shared_ptr cell = std::make_shared(); - m_cells.push_back(cell); - } +Minefield::Minefield(int cols, int rows, int mines) + : cols_(cols), rows_(rows), total_mines_(mines), remaining_flags_(mines) { + cells_.resize(cols * rows); } -MineField::~MineField() -{ - m_cells.clear(); +const Cell& Minefield::get_cell(int x, int y) const { + static const Cell invalid_cell; + if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return invalid_cell; + return cells_[y * cols_ + x]; } -void MineField::startTimer() -{ - if (!m_timerRunning) - { - m_startTime = std::chrono::steady_clock::now(); - m_timerRunning = true; - } +std::chrono::milliseconds Minefield::get_elapsed_time() const { + if (state_ == GameState::Ready) return std::chrono::milliseconds(0); + auto end = (state_ == GameState::Playing) ? std::chrono::steady_clock::now() : end_time_; + return std::chrono::duration_cast(end - start_time_); } -void MineField::stopTimer() -{ - if (m_timerRunning) - { - m_endTime = std::chrono::steady_clock::now(); - m_timerRunning = false; - } -} +void Minefield::generate_mines(int safe_x, int safe_y) { + std::vector indices(cells_.size()); + std::iota(indices.begin(), indices.end(), 0); -int MineField::getElapsedTime() const -{ - if (m_timerRunning) - { - auto now = std::chrono::steady_clock::now(); - return std::chrono::duration_cast(now - m_startTime).count(); - } - else if (m_gameState == GameState::WON || m_gameState == GameState::LOST) - { - return std::chrono::duration_cast(m_endTime - m_startTime).count(); - } - return 0; -} + // Remove safe zone (3x3 around click) + indices.erase(std::remove_if(indices.begin(), indices.end(), [&](int idx) { + int cx = idx % cols_; + int cy = idx / cols_; + return std::abs(cx - safe_x) <= 1 && std::abs(cy - safe_y) <= 1; + }), indices.end()); -void MineField::timerTick() -{ - if (m_timerRunning) - { - timerSignal.emit(getElapsedTime()); - } -} - -void MineField::reset() -{ - // Reset all cells - for (auto& cell : m_cells) - { - cell->isBomb = false; - cell->isFlagged = false; - cell->isCleared = false; - cell->bombsNearby = -1; - } - - // Reset game state - m_openCells = 0; - m_remainingFlags = m_totalMines; - m_gameState = GameState::READY; - m_timerRunning = false; - - // Emit signals - resetSignal.emit(); - remainingFlagsSignal.emit(m_remainingFlags); -} - -void MineField::startNewGame(int cols, int rows, int mines) -{ - // Store new dimensions - m_cols = cols; - m_rows = rows; - m_totalMines = mines; - m_remainingFlags = mines; - - // Create new cells - m_cells.clear(); - m_cells.reserve(m_cols * m_rows); - for (int i = 0; i < m_cols * m_rows; i++) - { - std::shared_ptr cell = std::make_shared(); - m_cells.push_back(cell); - } - - // Reset game state - m_openCells = 0; - m_gameState = GameState::READY; - m_timerRunning = false; - - // Emit signals - resetSignal.emit(); - remainingFlagsSignal.emit(m_remainingFlags); -} - -void MineField::initBombs(int firstX, int firstY) -{ - if (m_gameState != GameState::READY) - { - return; - } - - // Start timer when first cell is clicked - startTimer(); - m_gameState = GameState::PLAYING; - - // Create a vector of all possible positions - std::vector positions; - positions.reserve(m_cols * m_rows); - - for (int i = 0; i < m_cols * m_rows; i++) - { - positions.push_back(i); - } - - // Remove first clicked position and surrounding cells from available positions - positions.erase( - std::remove_if(positions.begin(), positions.end(), - [this, firstX, firstY](int pos) { - int x = pos % m_cols; - int y = pos / m_cols; - return std::abs(x - firstX) <= 1 && std::abs(y - firstY) <= 1; - } - ), - positions.end() - ); - - // Use modern random generator std::random_device rd; - std::mt19937 gen(rd()); + std::mt19937 g(rd()); + std::shuffle(indices.begin(), indices.end(), g); - // Shuffle positions - std::shuffle(positions.begin(), positions.end(), gen); - - // Place mines - int minesToPlace = std::min(m_totalMines, static_cast(positions.size())); - for (int i = 0; i < minesToPlace; i++) - { - m_cells.at(positions[i])->isBomb = true; + int placed = 0; + for (int idx : indices) { + if (placed >= total_mines_) break; + cells_[idx].is_bomb = true; + placed++; + } + + // Precompute neighbors + for (int y = 0; y < rows_; ++y) { + for (int x = 0; x < cols_; ++x) { + if (!cells_[y * cols_ + x].is_bomb) { + cells_[y * cols_ + x].nearby_bombs = count_nearby_bombs(x, y); + } + } } } -bool MineField::openCell(int x, int y) -{ - // Ignore if game is over or cell is already open/flagged - if (m_gameState != GameState::PLAYING && m_gameState != GameState::READY) - { - return false; - } - - if (isOpened(x, y) || isFlagged(x, y)) - { - return false; - } - - // Start timer on first click if not already started - if (m_gameState == GameState::READY) - { - initBombs(x, y); - } - - // Check if bomb - if (isBomb(x, y)) - { - // Only emit the game over signal if we haven't already - if (m_gameState != GameState::LOST) { - m_gameState = GameState::LOST; - stopTimer(); - gameOverSignal.emit(); +int Minefield::count_nearby_bombs(int x, int y) const { + int count = 0; + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) continue; + int nx = x + dx; + int ny = y + dy; + if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) { + if (cells_[ny * cols_ + nx].is_bomb) count++; + } } - return false; } - - // Open cell - setOpenCell(x, y); - - // If no bombs nearby, open surrounding cells - if (bombsNearby(x, y) == 0) - { - openNeighboorhood(x, y); + return count; +} + +int Minefield::count_nearby_flags(int x, int y) const { + int count = 0; + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) continue; + int nx = x + dx; + int ny = y + dy; + if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) { + if (cells_[ny * cols_ + nx].is_flagged) count++; + } + } } - + return count; +} + +bool Minefield::open_cell(int x, int y) { + if (state_ == GameState::Won || state_ == GameState::Lost) return false; + if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return false; + + Cell& cell = cells_[y * cols_ + x]; + if (cell.is_flagged || cell.is_revealed) return false; + + if (state_ == GameState::Ready) { + start_time_ = std::chrono::steady_clock::now(); + state_ = GameState::Playing; + generate_mines(x, y); + } + + if (cell.is_bomb) { + cell.is_exploded = true; + cell.is_revealed = true; + state_ = GameState::Lost; + end_time_ = std::chrono::steady_clock::now(); + + // Reveal all other bombs + for (auto& c : cells_) { + if (c.is_bomb && !c.is_flagged) c.is_revealed = true; + } + return true; + } + + reveal_recursive(x, y); + check_win_condition(); return true; } -void MineField::computeBombsNearby(int x, int y) -{ - int total = 0; - // Check all 8 neighboring cells - for (int i = -1; i <= 1; i++) - { - for (int j = -1; j <= 1; j++) - { - // Skip the cell itself - if (i == 0 && j == 0) - { - continue; - } - - int nx = x + i; - int ny = y + j; - - // Check if within bounds - if (nx >= 0 && nx < m_cols && ny >= 0 && ny < m_rows) - { - if (isBomb(nx, ny)) - { - ++total; - } - } - } - } - - m_cells.at(x + y * m_cols)->bombsNearby = total; -} +void Minefield::reveal_recursive(int x, int y) { + // Non-recursive BFS to avoid stack overflow + std::vector> q; + q.reserve(cols_ * rows_); + q.push_back({x, y}); -void MineField::openNeighboorhood(int x, int y) -{ - // Check all 8 neighboring cells - for (int i = -1; i <= 1; i++) - { - for (int j = -1; j <= 1; j++) - { - // Skip the cell itself - if (i == 0 && j == 0) - { - continue; - } - - int nx = x + i; - int ny = y + j; - - // Check if within bounds - if (nx >= 0 && nx < m_cols && ny >= 0 && ny < m_rows) - { - // If cell is not opened and not a bomb, open it - if (!isOpened(nx, ny) && !isBomb(nx, ny)) - { - setOpenCell(nx, ny); - - // If no bombs nearby, recursively open surrounding cells - if (bombsNearby(nx, ny) == 0) - { - openNeighboorhood(nx, ny); + // Mark initial as revealed + if (!cells_[y * cols_ + x].is_revealed) { + cells_[y * cols_ + x].is_revealed = true; + revealed_count_++; + } + + size_t head = 0; + while(head < q.size()){ + auto [cx, cy] = q[head++]; + Cell& current = cells_[cy * cols_ + cx]; + + if (current.nearby_bombs == 0) { + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) continue; + int nx = cx + dx; + int ny = cy + dy; + if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) { + Cell& neighbor = cells_[ny * cols_ + nx]; + if (!neighbor.is_revealed && !neighbor.is_flagged) { + neighbor.is_revealed = true; + revealed_count_++; + q.push_back({nx, ny}); + } } } } @@ -278,105 +155,82 @@ void MineField::openNeighboorhood(int x, int y) } } -bool MineField::isOpened(int x, int y) -{ - return m_cells.at(x + y * m_cols)->isCleared; -} +bool Minefield::toggle_flag(int x, int y) { + if (state_ == GameState::Won || state_ == GameState::Lost) return false; + if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return false; -bool MineField::isFlagged(int x, int y) -{ - return m_cells.at(x + y * m_cols)->isFlagged; -} + Cell& cell = cells_[y * cols_ + x]; + if (cell.is_revealed) return false; -bool MineField::isBomb(int x, int y) -{ - return m_cells.at(x + y * m_cols)->isBomb; -} - -int MineField::bombsNearby(int x, int y) -{ - // Calculate bombs nearby if not already calculated - if (m_cells.at(x + y * m_cols)->bombsNearby == -1) - { - computeBombsNearby(x, y); - } - - return m_cells.at(x + y * m_cols)->bombsNearby; -} - -void MineField::setOpenCell(int x, int y) -{ - m_cells.at(x + y * m_cols)->isCleared = true; - openCellSignal.emit(x, y); - ++m_openCells; - checkGameWon(); -} - -void MineField::checkGameWon() -{ - // Win condition: All non-bomb cells are opened - if (m_openCells == (m_cols * m_rows - m_totalMines) && m_gameState == GameState::PLAYING) - { - m_gameState = GameState::WON; - stopTimer(); - - // Auto-flag all remaining bombs - for (int i = 0; i < m_cols * m_rows; i++) - { - int x = i % m_cols; - int y = i / m_cols; - - if (isBomb(x, y) && !isFlagged(x, y)) - { - m_cells.at(i)->isFlagged = true; - } + if (cell.is_flagged) { + cell.is_flagged = false; + remaining_flags_++; + } else { + if (remaining_flags_ > 0) { + cell.is_flagged = true; + remaining_flags_--; + } else { + return false; } - - m_remainingFlags = 0; - remainingFlagsSignal.emit(m_remainingFlags); - - // Emit game won signal with elapsed time - gameWonSignal.emit(getElapsedTime()); } -} - -bool MineField::toggleFlag(int x, int y) -{ - // Ignore if game is over or cell is already open - if (m_gameState != GameState::PLAYING && m_gameState != GameState::READY) - { - return false; - } - - if (isOpened(x, y)) - { - return false; - } - - if (m_gameState == GameState::READY) - { - // Start timer on first action - startTimer(); - m_gameState = GameState::PLAYING; - } - - // Toggle flag state - if (m_cells.at(x + y * m_cols)->isFlagged) - { - m_cells.at(x + y * m_cols)->isFlagged = false; - ++m_remainingFlags; - } - else if (m_remainingFlags > 0) - { - m_cells.at(x + y * m_cols)->isFlagged = true; - --m_remainingFlags; - } - else - { - // No remaining flags - return false; - } - - remainingFlagsSignal.emit(m_remainingFlags); return true; } + +bool Minefield::chord_cell(int x, int y) { + if (state_ != GameState::Playing) return false; + if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return false; + + Cell& cell = cells_[y * cols_ + x]; + if (!cell.is_revealed) return false; + if (cell.nearby_bombs == 0) return false; + + if (count_nearby_flags(x, y) == cell.nearby_bombs) { + bool changed = false; + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) continue; + int nx = x + dx; + int ny = y + dy; + if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) { + Cell& neighbor = cells_[ny * cols_ + nx]; + if (!neighbor.is_revealed && !neighbor.is_flagged) { + // Standard open logic + if (neighbor.is_bomb) { + neighbor.is_exploded = true; + neighbor.is_revealed = true; + state_ = GameState::Lost; + end_time_ = std::chrono::steady_clock::now(); + // Reveal all bombs + for (auto& c : cells_) { + if (c.is_bomb && !c.is_flagged) c.is_revealed = true; + } + return true; + } else { + reveal_recursive(nx, ny); + changed = true; + } + } + } + } + } + check_win_condition(); + return changed; + } + return false; +} + +void Minefield::check_win_condition() { + if (state_ != GameState::Playing) return; + + // Win if all non-bomb cells are revealed + int safe_cells = (cols_ * rows_) - total_mines_; + if (revealed_count_ == safe_cells) { + state_ = GameState::Won; + end_time_ = std::chrono::steady_clock::now(); + remaining_flags_ = 0; // Visually satisfy flags + // Flag all unflagged bombs + for (auto& c : cells_) { + if (c.is_bomb) c.is_flagged = true; + } + } +} diff --git a/src/minefield.hpp b/src/minefield.hpp index 740117b..3f97f16 100644 --- a/src/minefield.hpp +++ b/src/minefield.hpp @@ -1,13 +1,11 @@ #pragma once -#include -#include -#include #include #include -#include +#include +#include +#include -// Game difficulty presets struct GameDifficulty { std::string name; int cols; @@ -15,104 +13,64 @@ struct GameDifficulty { int mines; }; -// Game score for leaderboard -struct GameScore { - std::string playerName; - std::string difficulty; - int time; - std::string date; - - bool operator<(const GameScore& other) const { - return time < other.time; - } +enum class GameState { + Ready, + Playing, + Won, + Lost }; -class MineField -{ +struct Cell { + bool is_bomb : 1 = false; + bool is_flagged : 1 = false; + bool is_revealed : 1 = false; + bool is_exploded : 1 = false; // The specific bomb that killed you + uint8_t nearby_bombs : 4 = 0; +}; + +class Minefield { public: - enum class GameState { - READY, - PLAYING, - WON, - LOST - }; + Minefield(int cols, int rows, int mines); + + // Core actions + // Returns true if state changed + bool open_cell(int x, int y); + bool toggle_flag(int x, int y); + bool chord_cell(int x, int y); + + // Getters + int cols() const { return cols_; } + int rows() const { return rows_; } + int total_mines() const { return total_mines_; } + int remaining_flags() const { return remaining_flags_; } + GameState state() const { return state_; } + const Cell& get_cell(int x, int y) const; + + // Timing + std::chrono::milliseconds get_elapsed_time() const; + + // Difficulty presets + static const GameDifficulty DifficultyEasy; + static const GameDifficulty DifficultyMedium; + static const GameDifficulty DifficultyHard; + static const GameDifficulty DifficultyExpert; private: - struct Cell - { - bool isFlagged = false; - bool isCleared = false; - bool isBomb = false; - int bombsNearby = -1; - }; + void generate_mines(int safe_x, int safe_y); + void reveal_recursive(int x, int y); + void check_win_condition(); + int count_nearby_flags(int x, int y) const; + int count_nearby_bombs(int x, int y) const; - std::vector> m_cells; - int m_rows; - int m_cols; - int m_totalMines; - int m_remainingFlags; - int m_openCells; - GameState m_gameState; - std::chrono::time_point m_startTime; - std::chrono::time_point m_endTime; - bool m_timerRunning; - - void computeBombsNearby(int x, int y); - void openNeighboorhood(int x, int y); - void setOpenCell(int x, int y); - void checkGameWon(); - void startTimer(); - void stopTimer(); - -public: - MineField(int cols, int rows, int mines); - ~MineField(); + int cols_; + int rows_; + int total_mines_; + int remaining_flags_; + int revealed_count_ = 0; - void initBombs(int x, int y); - bool isBomb(int x, int y); - bool isFlagged(int x, int y); - bool isOpened(int x, int y); - bool openCell(int x, int y); - int bombsNearby(int x, int y); + GameState state_ = GameState::Ready; + std::vector cells_; - GameState getGameState() const { return m_gameState; } - int getCols() const { return m_cols; } - int getRows() const { return m_rows; } - bool toggleFlag(int x, int y); - int getRemainingFlags() const { return m_remainingFlags; } - int getTotalMines() const { return m_totalMines; } - int getOpenCells() const { return m_openCells; } - - void reset(); - void startNewGame(int cols, int rows, int mines); - - // Get elapsed time in milliseconds - int getElapsedTime() const; - - // Timer tick (for UI updates) - void timerTick(); - - // Signal when game is reset - sigc::signal resetSignal; - - // Signal when a cell is opened - sigc::signal openCellSignal; - - // Signal when flags count changes - sigc::signal remainingFlagsSignal; - - // Signal when game is won - sigc::signal gameWonSignal; // int parameter is elapsed time in ms - - // Signal when game is lost - sigc::signal gameOverSignal; - - // Signal for timer updates - sigc::signal timerSignal; // int parameter is elapsed time in ms - - // Predefined difficulty levels - static const GameDifficulty DIFFICULTY_EASY; - static const GameDifficulty DIFFICULTY_MEDIUM; - static const GameDifficulty DIFFICULTY_HARD; - static const GameDifficulty DIFFICULTY_EXPERT; + std::chrono::steady_clock::time_point start_time_; + std::chrono::steady_clock::time_point end_time_; }; diff --git a/src/timer.cpp b/src/timer.cpp deleted file mode 100644 index 65bea5c..0000000 --- a/src/timer.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include "timer.hpp" -#include -#include - -Timer::Timer() : m_running(false) { - reset(); -} - -Timer::~Timer() { - stop(); - - // Wait for timer thread to finish - if (m_timerThread.joinable()) { - m_timerThread.join(); - } -} - -void Timer::start() { - std::lock_guard lock(m_mutex); - - if (!m_running) { - m_running = true; - m_startTime = std::chrono::steady_clock::now(); - - // Start timer thread if not already running - if (!m_timerThread.joinable()) { - m_timerThread = std::thread(&Timer::timerThread, this); - } else { - // Notify thread if already existing - m_condition.notify_one(); - } - } -} - -void Timer::stop() { - std::lock_guard lock(m_mutex); - m_running = false; - m_condition.notify_one(); -} - -void Timer::reset() { - std::lock_guard lock(m_mutex); - m_startTime = std::chrono::steady_clock::now(); -} - -int Timer::getElapsedTime() const { - if (m_running) { - auto now = std::chrono::steady_clock::now(); - return std::chrono::duration_cast(now - m_startTime).count(); - } - return 0; -} - -void Timer::timerThread() { - while (true) { - // Wait for timer to be running - { - std::unique_lock lock(m_mutex); - m_condition.wait(lock, [this] { return m_running || !m_running; }); - - // Exit thread if timer is stopped - if (!m_running) { - return; - } - } - - // Sleep for 100ms (10 updates per second) - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // Check if timer is still running - if (m_running) { - // Get elapsed time - int time = getElapsedTime(); - - // Emit signal through Glib main loop - Glib::signal_idle().connect_once([this, time] { - timerSignal.emit(time); - }); - } - } -} diff --git a/src/timer.hpp b/src/timer.hpp deleted file mode 100644 index c2246a6..0000000 --- a/src/timer.hpp +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -class Timer { -public: - Timer(); - ~Timer(); - - // Start timer - void start(); - - // Stop timer - void stop(); - - // Reset timer - void reset(); - - // Get elapsed time in milliseconds - int getElapsedTime() const; - - // Signal emitted on timer tick - sigc::signal timerSignal; - -private: - std::chrono::time_point m_startTime; - std::atomic m_running; - - // Thread handling - std::thread m_timerThread; - std::mutex m_mutex; - std::condition_variable m_condition; - - // Timer thread function - void timerThread(); -}; diff --git a/src/window.cpp b/src/window.cpp index 9c9deff..a77575c 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -1,1126 +1,137 @@ - #include "window.hpp" #include -#include -#include #include -#include -#include -#include -#include -MainWindow::MainWindow() -{ - // Initialize the game field with default settings - m_field = std::make_unique(MineField::DIFFICULTY_MEDIUM.cols, - MineField::DIFFICULTY_MEDIUM.rows, - MineField::DIFFICULTY_MEDIUM.mines); - m_currentDifficulty = MineField::DIFFICULTY_MEDIUM.name; - m_firstClick = true; +MainWindow::MainWindow() { + set_title("Minesweeper"); + set_default_size(800, 600); + + setup_header_bar(); + setup_board(); - // Setup UI components - setupUI(); - loadResources(); - setupCSSProviders(); - setupHeaderBar(); - setupStatusBar(); - setupGameBoard(); - loadLeaderboard(); - - // Connect signals - m_field->openCellSignal.connect(sigc::mem_fun(*this, &MainWindow::updateCell)); - m_field->remainingFlagsSignal.connect(sigc::mem_fun(*this, &MainWindow::updateFlagsLabel)); - m_field->gameOverSignal.connect(sigc::mem_fun(*this, &MainWindow::onGameOver)); - m_field->gameWonSignal.connect(sigc::mem_fun(*this, &MainWindow::onGameWon)); - m_field->resetSignal.connect(sigc::mem_fun(*this, &MainWindow::resetGame)); - - // Initial update - updateFlagsLabel(m_field->getRemainingFlags()); - updateTimeLabel(); + // Start initial game + start_new_game(Minefield::DifficultyEasy); } -void MainWindow::setupUI() -{ - // Configure main window - set_title("MineSweeper"); - set_default_size(650, 650); - set_resizable(true); - - // Configure main layout - m_boxMain.set_orientation(Gtk::Orientation::VERTICAL); - m_boxMain.set_spacing(8); - m_boxMain.set_margin(10); - - // Set up overlay for animations - m_overlay.set_child(m_grid); - - // Add components to main box - m_boxMain.append(m_statusBox); - m_boxMain.append(m_overlay); - - // Set window content - set_child(m_boxMain); +MainWindow::~MainWindow() { + timer_conn_.disconnect(); } -void MainWindow::setupHeaderBar() -{ - // Create header bar - m_headerBar.set_show_title_buttons(true); - - // Add title widget - auto titleLabel = Gtk::make_managed("MineSweeper"); - titleLabel->add_css_class("title"); - m_headerBar.set_title_widget(*titleLabel); - - // New game button - m_newGameButton.set_label("New Game"); - m_newGameButton.add_css_class("suggested-action"); - m_newGameButton.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::onNewGameClick)); - - // Difficulty button - m_difficultyButton.set_label("Difficulty"); - - // Create menu model - auto difficultyMenu = Gio::Menu::create(); - - // Add difficulty options - auto easyAction = Gio::SimpleAction::create("easy"); - easyAction->signal_activate().connect([this](const Glib::VariantBase&) { - onDifficultySelected(MineField::DIFFICULTY_EASY); +void MainWindow::setup_header_bar() { + set_titlebar(header_bar_); + + // New Game Button + btn_new_game_.set_label("New Game"); + btn_new_game_.signal_clicked().connect([this]() { + start_new_game(current_difficulty_); }); - add_action(easyAction); - difficultyMenu->append("Beginner", "win.easy"); - - auto mediumAction = Gio::SimpleAction::create("medium"); - mediumAction->signal_activate().connect([this](const Glib::VariantBase&) { - onDifficultySelected(MineField::DIFFICULTY_MEDIUM); - }); - add_action(mediumAction); - difficultyMenu->append("Intermediate", "win.medium"); - - auto hardAction = Gio::SimpleAction::create("hard"); - hardAction->signal_activate().connect([this](const Glib::VariantBase&) { - onDifficultySelected(MineField::DIFFICULTY_HARD); - }); - add_action(hardAction); - difficultyMenu->append("Expert", "win.hard"); - - auto expertAction = Gio::SimpleAction::create("expert"); - expertAction->signal_activate().connect([this](const Glib::VariantBase&) { - onDifficultySelected(MineField::DIFFICULTY_EXPERT); - }); - add_action(expertAction); - difficultyMenu->append("Master", "win.expert"); - - auto customAction = Gio::SimpleAction::create("custom"); - customAction->signal_activate().connect([this](const Glib::VariantBase&) { - showDifficultyDialog(); - }); - add_action(customAction); - difficultyMenu->append("Custom...", "win.custom"); - - // Add a separator (using a different item since append_separator is not available) - difficultyMenu->append("───────────", ""); - - // Add leaderboard option - auto leaderboardAction = Gio::SimpleAction::create("leaderboard"); - leaderboardAction->signal_activate().connect([this](const Glib::VariantBase&) { - showLeaderboard(); - }); - add_action(leaderboardAction); - difficultyMenu->append("Leaderboard", "win.leaderboard"); - - m_difficultyButton.set_menu_model(difficultyMenu); - - // Add buttons to header bar - m_headerBar.pack_start(m_newGameButton); - m_headerBar.pack_end(m_difficultyButton); - - // Set header bar as titlebar - set_titlebar(m_headerBar); -} + header_bar_.pack_start(btn_new_game_); -void MainWindow::setupStatusBar() -{ - // Configure status bar - m_statusBox.set_orientation(Gtk::Orientation::HORIZONTAL); - m_statusBox.set_spacing(10); - m_statusBox.set_margin(5); - m_statusBox.set_halign(Gtk::Align::FILL); - m_statusBox.set_hexpand(true); + // Difficulty Menu + btn_difficulty_.set_label("Difficulty"); - // Mines label - m_minesLabel.set_label(Glib::ustring::compose("Total mines: %1", m_field->getTotalMines())); - m_minesLabel.set_halign(Gtk::Align::START); - m_minesLabel.set_hexpand(true); + box_difficulty_.set_orientation(Gtk::Orientation::VERTICAL); + box_difficulty_.set_margin(10); + box_difficulty_.set_spacing(5); - // Timer label - m_timeLabel.set_label("Time: 00:00.0"); - m_timeLabel.set_halign(Gtk::Align::CENTER); - m_timeLabel.set_hexpand(true); - - // Flags label - m_flagsLabel.set_label(Glib::ustring::compose("Flags: %1/%2", - m_field->getRemainingFlags(), - m_field->getTotalMines())); - m_flagsLabel.set_halign(Gtk::Align::END); - m_flagsLabel.set_hexpand(true); - - // Add labels to status bar - m_statusBox.append(m_minesLabel); - m_statusBox.append(m_timeLabel); - m_statusBox.append(m_flagsLabel); -} - -void MainWindow::setupGameBoard() -{ - // Clear existing buttons - m_buttons.clear(); - - // Remove all children from grid - while (auto child = m_grid.get_first_child()) { - m_grid.remove(*child); - } - - // Configure grid - m_grid.set_row_homogeneous(true); - m_grid.set_column_homogeneous(true); - m_grid.set_margin(8); - - // Create cell buttons - int cols = m_field->getCols(); - int rows = m_field->getRows(); - - // Calculate appropriate button size based on window size - int buttonSize = std::max(30, std::min(50, 500 / std::max(cols, rows))); - - // Create buttons for each cell - for (int y = 0; y < rows; y++) { - for (int x = 0; x < cols; x++) { - int index = x + y * cols; - - // Create toggle button - auto button = std::make_shared(); - button->set_size_request(buttonSize, buttonSize); - button->set_has_frame(true); - button->add_css_class("cell-button"); - - // Connect left click - button->signal_clicked().connect( - sigc::bind(sigc::mem_fun(*this, &MainWindow::onCellClick), x, y) - ); - - // Connect right click - auto rightClickGesture = Gtk::GestureClick::create(); - rightClickGesture->set_button(3); // Right mouse button - rightClickGesture->signal_released().connect( - sigc::bind(sigc::mem_fun(*this, &MainWindow::onCellRightClick), index) - ); - button->add_controller(rightClickGesture); - - // Add button to grid and vector - m_grid.attach(*button, x, y); - m_buttons.push_back(button); - } - } - - // Show all widgets - m_grid.show(); -} - -void MainWindow::loadResources() -{ - // Load textures - try { - m_textureBomb = Gdk::Texture::create_from_resource("/minesweeper/bomb-solid"); - m_textureFlag = Gdk::Texture::create_from_resource("/minesweeper/flag-solid"); - m_textureFlagBomb = Gdk::Texture::create_from_resource("/minesweeper/flag-bomb"); - m_textureExplosion = Gdk::Texture::create_from_resource("/minesweeper/explosion-solid"); - } catch (const std::exception& e) { - std::cerr << "Failed to load resources: " << e.what() << std::endl; - } -} - -void MainWindow::setupCSSProviders() -{ - auto css_provider = Gtk::CssProvider::create(); - - // Define CSS styles - css_provider->load_from_data( - "button.cell-button { border-radius: 0; margin: 1px; padding: 0; }" - ".cell-button:checked { background-color: #e0e0e0; }" - ".label-1 { font-weight: bold; font-size: 1.1em; color: blue; }" - ".label-2 { font-weight: bold; font-size: 1.1em; color: green; }" - ".label-3 { font-weight: bold; font-size: 1.1em; color: darkorange; }" - ".label-4 { font-weight: bold; font-size: 1.1em; color: purple; }" - ".label-5 { font-weight: bold; font-size: 1.1em; color: red; }" - ".label-6 { font-weight: bold; font-size: 1.1em; color: salmon; }" - ".label-7 { font-weight: bold; font-size: 1.1em; color: turquoise; }" - ".label-8 { font-weight: bold; font-size: 1.1em; color: magenta; }" - ".confetti { opacity: 0.8; }" - ); - - // Add CSS provider to display - auto display = Gdk::Display::get_default(); - Gtk::StyleContext::add_provider_for_display( - display, css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - ); -} - -void MainWindow::onCellClick(int x, int y) -{ - // Ignore if game is over - if (m_field->getGameState() == MineField::GameState::LOST || - m_field->getGameState() == MineField::GameState::WON) { - return; - } - - // Start timer on first click - if (m_firstClick) { - m_timerConnection = Glib::signal_timeout().connect( - sigc::mem_fun(*this, &MainWindow::updateTimer), 100 - ); - m_firstClick = false; - } - - // Open cell - m_field->openCell(x, y); -} - -void MainWindow::onCellRightClick(int n_press, double n_x, double n_y, int index) -{ - (void)n_press; (void)n_x; (void)n_y; - - // Calculate x and y from index - int cols = m_field->getCols(); - int x = index % cols; - int y = index / cols; - - // Ignore if game is over - if (m_field->getGameState() == MineField::GameState::LOST || - m_field->getGameState() == MineField::GameState::WON) { - return; - } - - // Start timer on first action - if (m_firstClick) { - m_timerConnection = Glib::signal_timeout().connect( - sigc::mem_fun(*this, &MainWindow::updateTimer), 100 - ); - m_firstClick = false; - } - - // Toggle flag - if (m_field->toggleFlag(x, y)) { - if (m_field->isFlagged(x, y)) { - // Set flag image - auto imgFlag = Gtk::make_managed(); - imgFlag->set(m_textureFlag); - m_buttons[index]->set_child(*imgFlag); - m_buttons[index]->set_active(true); - } else { - // Remove flag image - m_buttons[index]->unset_child(); - m_buttons[index]->set_active(false); - } - } -} - -void MainWindow::updateCell(int x, int y) -{ - int cols = m_field->getCols(); - int index = x + y * cols; - - if (index >= 0 && index < static_cast(m_buttons.size())) { - auto button = m_buttons[index]; - - // Cell is opened - if (m_field->isOpened(x, y)) { - // Show bombs nearby - int bombs = m_field->bombsNearby(x, y); - if (bombs > 0) { - button->set_label(Glib::ustring::format(bombs)); - button->get_style_context()->add_class("label-" + Glib::ustring::format(bombs)); - } - - button->set_active(true); - button->set_sensitive(false); - } - } -} - -void MainWindow::updateFlagsLabel(int flags) -{ - m_flagsLabel.set_label(Glib::ustring::compose("Flags: %1/%2", - flags, - m_field->getTotalMines())); -} - -bool MainWindow::updateTimer() -{ - // Update timer every 100ms - if (m_field->getGameState() == MineField::GameState::PLAYING) { - m_field->timerTick(); - updateTimeLabel(); - return true; - } - return false; -} - -void MainWindow::updateTimeLabel() -{ - int time = m_field->getElapsedTime(); - - // Format time as MM:SS.d - int deciseconds = (time / 100) % 10; - int seconds = (time / 1000) % 60; - int minutes = (time / 60000) % 60; - - m_timeLabel.set_label(Glib::ustring::compose("Time: %1:%2.%3", - Glib::ustring::format(std::setfill(L'0'), std::setw(2), minutes), - Glib::ustring::format(std::setfill(L'0'), std::setw(2), seconds), - Glib::ustring::format(deciseconds) - )); -} - -void MainWindow::onGameOver() -{ - // Disconnect timer - m_timerConnection.disconnect(); - - // Show all bombs - revealAllBombs(); - - // Show game over animation - showGameOverAnimation(); -} - -void MainWindow::onGameWon(int time) -{ - // Disconnect timer - m_timerConnection.disconnect(); - - // Update timer display with final time - updateTimeLabel(); - - // Show win animation - showGameWonAnimation(); - - // Show name input dialog for leaderboard - showNameInputDialog(time); -} - -void MainWindow::revealAllBombs() -{ - int cols = m_field->getCols(); - int rows = m_field->getRows(); - - for (int y = 0; y < rows; y++) { - for (int x = 0; x < cols; x++) { - int index = x + y * cols; - - // Disable all buttons - m_buttons[index]->set_sensitive(false); - - if (m_field->isBomb(x, y)) { - // Show bomb or flagged bomb based on state - if (m_field->isFlagged(x, y)) { - auto imgFlagBomb = Gtk::make_managed(); - imgFlagBomb->set(m_textureFlagBomb); - m_buttons[index]->set_child(*imgFlagBomb); - } else { - auto imgBomb = Gtk::make_managed(); - imgBomb->set(m_textureBomb); - m_buttons[index]->set_child(*imgBomb); - } - m_buttons[index]->set_active(true); - } - } - } -} - -void MainWindow::showGameOverAnimation() -{ - // Find first non-flagged bomb - int cols = m_field->getCols(); - int rows = m_field->getRows(); - bool dialogShown = false; // Track if dialog was already shown - - for (int y = 0; y < rows && !dialogShown; y++) { - for (int x = 0; x < cols; x++) { - int index = x + y * cols; - - if (m_field->isBomb(x, y) && !m_field->isFlagged(x, y)) { - // Show explosion on the first bomb - auto imgExplosion = Gtk::make_managed(); - imgExplosion->set(m_textureExplosion); - m_buttons[index]->set_child(*imgExplosion); - - // Only show one dialog - if (!dialogShown) { - // Use a standard Dialog instead of AlertDialog - auto dialog = Gtk::make_managed(*this, "Game Over!", - false, Gtk::MessageType::INFO); - dialog->set_secondary_text("You hit a mine! Better luck next time."); - dialog->add_button("Try Again", 1); - dialog->add_button("Change Difficulty", 2); - dialog->set_default_response(1); - - dialog->signal_response().connect([this, dialog](int response) { - dialog->close(); // Use close() instead of hide() - if (response == 1) { - // Reset game with same settings - onNewGameClick(); - } else { - // Show difficulty dialog - showDifficultyDialog(); - } - }); - - dialog->show(); - dialogShown = true; - } - break; - } - } - } -} - - -void MainWindow::setupConfetti() -{ - // Create confetti pieces - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution colorDist(0, 5); - std::uniform_int_distribution sizeDist(10, 20); - std::uniform_int_distribution xDist(0, m_field->getCols() - 1); - std::uniform_int_distribution yDist(0, m_field->getRows() - 1); - - const std::vector colors = { - "#FF5252", "#FFEB3B", "#4CAF50", "#2196F3", "#9C27B0", "#FF9800" - }; - - // Create 20 confetti pieces - for (int i = 0; i < 20; i++) { - // Create drawing area for confetti - auto confetti = Gtk::make_managed(); - confetti->set_content_width(sizeDist(gen)); - confetti->set_content_height(sizeDist(gen)); - confetti->add_css_class("confetti"); - - // Set confetti color - std::string color = colors[colorDist(gen)]; - - // Draw confetti - confetti->set_draw_func([color](const Cairo::RefPtr& cr, int width, int height) { - cr->set_source_rgba(1, 1, 1, 0.8); - cr->rectangle(0, 0, width, height); - cr->fill(); - - // Parse color using simple RGB format - double r = 0, g = 0, b = 0; - - // Parse hex color format (#RRGGBB) - if (color.length() == 7 && color[0] == '#') { - int ri, gi, bi; - if (sscanf(color.c_str(), "#%02x%02x%02x", &ri, &gi, &bi) == 3) { - r = ri / 255.0; - g = gi / 255.0; - b = bi / 255.0; - } - } - - cr->set_source_rgb(r, g, b); - cr->rectangle(0, 0, width, height); - cr->fill(); + auto add_diff_btn = [&](const GameDifficulty& diff) { + auto* btn = Gtk::make_managed(diff.name); + btn->signal_clicked().connect([this, diff]() { + start_new_game(diff); + btn_difficulty_.popdown(); }); - - // Get random position - int x = xDist(gen); - int y = yDist(gen); - - // Add to overlay at random position - m_overlay.add_overlay(*confetti); - confetti->set_halign(Gtk::Align::START); - confetti->set_valign(Gtk::Align::START); - confetti->set_margin_start(x * 30 + 10); - confetti->set_margin_top(y * 30 + 10); - - // Animate confetti - auto duration = std::uniform_int_distribution(800, 2000)(gen); - Glib::signal_timeout().connect_once([confetti]() { - confetti->unparent(); - }, duration); - } -} - -void MainWindow::showGameWonAnimation() -{ - // Show confetti animation - setupConfetti(); - - // Update all buttons to show flags on bombs - int cols = m_field->getCols(); - int rows = m_field->getRows(); - - for (int y = 0; y < rows; y++) { - for (int x = 0; x < cols; x++) { - if (m_field->isBomb(x, y)) { - int index = x + y * cols; - - // Show flag on bomb - auto imgFlag = Gtk::make_managed(); - imgFlag->set(m_textureFlag); - m_buttons[index]->set_child(*imgFlag); - m_buttons[index]->set_active(true); - } - } - } -} - -void MainWindow::onNewGameClick() -{ - // Reset the current game - m_field->reset(); - - // Clear board UI - clearBoard(); - - // Reset game state - m_firstClick = true; - if (m_timerConnection) { - m_timerConnection.disconnect(); - } - - // Update labels - updateFlagsLabel(m_field->getRemainingFlags()); - m_timeLabel.set_label("Time: 00:00.0"); -} - -void MainWindow::clearBoard() -{ - int cols = m_field->getCols(); - int rows = m_field->getRows(); - - for (int y = 0; y < rows; y++) { - for (int x = 0; x < cols; x++) { - int index = x + y * cols; - - if (index < static_cast(m_buttons.size())) { - auto button = m_buttons[index]; - - // Reset button state - button->set_active(false); - button->set_sensitive(true); - button->set_label(""); - button->unset_child(); - - // Remove style classes - for (int i = 1; i <= 8; i++) { - button->get_style_context()->remove_class("label-" + Glib::ustring::format(i)); - } - } - } - } -} - -void MainWindow::onDifficultySelected(const GameDifficulty& difficulty) -{ - // Start new game with selected difficulty - startNewGame(difficulty.cols, difficulty.rows, difficulty.mines, difficulty.name); -} - -void MainWindow::startNewGame(int cols, int rows, int mines, const std::string& difficulty) -{ - // Update current difficulty - m_currentDifficulty = difficulty; - - // Create new game field - m_field = std::make_unique(cols, rows, mines); - - // Connect signals - m_field->openCellSignal.connect(sigc::mem_fun(*this, &MainWindow::updateCell)); - m_field->remainingFlagsSignal.connect(sigc::mem_fun(*this, &MainWindow::updateFlagsLabel)); - m_field->gameOverSignal.connect(sigc::mem_fun(*this, &MainWindow::onGameOver)); - m_field->gameWonSignal.connect(sigc::mem_fun(*this, &MainWindow::onGameWon)); - m_field->resetSignal.connect(sigc::mem_fun(*this, &MainWindow::resetGame)); - - // Reset game state - m_firstClick = true; - if (m_timerConnection) { - m_timerConnection.disconnect(); - } - - // Update labels - m_minesLabel.set_label(Glib::ustring::compose("Total mines: %1", m_field->getTotalMines())); - updateFlagsLabel(m_field->getRemainingFlags()); - m_timeLabel.set_label("Time: 00:00.0"); - - // Calculate appropriate window size based on grid dimensions - int windowWidth = cols * 30 + 40; // 30px per button + margins - int windowHeight = rows * 30 + 100; // Additional space for header/status bar - - // Resize window - set_default_size(windowWidth, windowHeight); - - // If the window is already visible, we need to queue a resize - queue_resize(); - - // Setup new game board - setupGameBoard(); -} - -void MainWindow::showDifficultyDialog() -{ - // Create custom difficulty dialog - auto dialog = Gtk::make_managed("Custom Difficulty", *this, true); - dialog->set_default_size(300, 200); - - // Add content area - auto contentArea = dialog->get_content_area(); - contentArea->set_margin(10); - contentArea->set_spacing(10); - - // Create input fields - auto widthBox = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 5); - auto widthLabel = Gtk::make_managed("Width:"); - widthLabel->set_halign(Gtk::Align::START); - auto widthSpinner = Gtk::make_managed(); - widthSpinner->set_range(5, 50); - widthSpinner->set_increments(1, 5); - widthSpinner->set_value(16); - widthBox->append(*widthLabel); - widthBox->append(*widthSpinner); - widthSpinner->set_hexpand(true); - - auto heightBox = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 5); - auto heightLabel = Gtk::make_managed("Height:"); - heightLabel->set_halign(Gtk::Align::START); - auto heightSpinner = Gtk::make_managed(); - heightSpinner->set_range(5, 30); - heightSpinner->set_increments(1, 5); - heightSpinner->set_value(16); - heightBox->append(*heightLabel); - heightBox->append(*heightSpinner); - heightSpinner->set_hexpand(true); - - auto minesBox = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 5); - auto minesLabel = Gtk::make_managed("Mines:"); - minesLabel->set_halign(Gtk::Align::START); - auto minesSpinner = Gtk::make_managed(); - minesSpinner->set_range(1, 500); - minesSpinner->set_increments(1, 10); - minesSpinner->set_value(40); - minesBox->append(*minesLabel); - minesBox->append(*minesSpinner); - minesSpinner->set_hexpand(true); - - // Calculate max mines based on board size - auto updateMaxMines = [widthSpinner, heightSpinner, minesSpinner]() { - int width = widthSpinner->get_value_as_int(); - int height = heightSpinner->get_value_as_int(); - int maxMines = static_cast(width * height * 0.8); // Max 80% of cells can be mines - - minesSpinner->set_range(1, maxMines); - - // Adjust mines if necessary - if (minesSpinner->get_value_as_int() > maxMines) { - minesSpinner->set_value(maxMines); - } + box_difficulty_.append(*btn); }; + + add_diff_btn(Minefield::DifficultyEasy); + add_diff_btn(Minefield::DifficultyMedium); + add_diff_btn(Minefield::DifficultyHard); + add_diff_btn(Minefield::DifficultyExpert); + + btn_difficulty_.set_popover(menu_difficulty_); + menu_difficulty_.set_child(box_difficulty_); + header_bar_.pack_start(btn_difficulty_); + + // Status Labels + lbl_flags_.set_margin_end(10); + lbl_time_.set_margin_end(10); - widthSpinner->signal_value_changed().connect(updateMaxMines); - heightSpinner->signal_value_changed().connect(updateMaxMines); - - // Add widgets to dialog - contentArea->append(*widthBox); - contentArea->append(*heightBox); - contentArea->append(*minesBox); - - // Add buttons - dialog->add_button("Cancel", Gtk::ResponseType::CANCEL); - dialog->add_button("Start Game", Gtk::ResponseType::OK); - dialog->set_default_response(Gtk::ResponseType::OK); - - // Show dialog and handle response - dialog->signal_response().connect([this, dialog, widthSpinner, heightSpinner, minesSpinner] - (int response) { - if (response == Gtk::ResponseType::OK) { - int width = widthSpinner->get_value_as_int(); - int height = heightSpinner->get_value_as_int(); - int mines = minesSpinner->get_value_as_int(); - - startNewGame(width, height, mines, "Custom"); - } - dialog->close(); - }); - - dialog->show(); + // Make them monospaced and bold + lbl_flags_.set_markup("🚩 0"); + lbl_time_.set_markup("⏱️ 00:00"); + + header_bar_.pack_end(lbl_time_); + header_bar_.pack_end(lbl_flags_); } -void MainWindow::resetGame() -{ - // Reset UI - clearBoard(); - - // Reset game state - m_firstClick = true; - if (m_timerConnection) { - m_timerConnection.disconnect(); - } - - // Update labels - updateFlagsLabel(m_field->getRemainingFlags()); - m_timeLabel.set_label("Time: 00:00.0"); +void MainWindow::setup_board() { + set_child(board_widget_); + board_widget_.signal_state_changed.connect(sigc::mem_fun(*this, &MainWindow::on_game_state_changed)); } -std::string MainWindow::formatTime(int milliseconds) const -{ - int seconds = milliseconds / 1000; - int minutes = seconds / 60; - seconds %= 60; +void MainWindow::start_new_game(const GameDifficulty& difficulty) { + current_difficulty_ = difficulty; - std::stringstream ss; - ss << std::setfill('0') << std::setw(2) << minutes << ":" - << std::setfill('0') << std::setw(2) << seconds; - return ss.str(); + minefield_ = std::make_shared(difficulty.cols, difficulty.rows, difficulty.mines); + board_widget_.set_minefield(minefield_); + + // Reset timer + timer_conn_.disconnect(); + timer_conn_ = Glib::signal_timeout().connect(sigc::mem_fun(*this, &MainWindow::on_timer_tick), 100); + + on_game_state_changed(); } -std::filesystem::path MainWindow::getConfigDir() const -{ - // Get config directory - std::filesystem::path configDir; - - // Check XDG_CONFIG_HOME environment variable - const char* xdgConfigHome = std::getenv("XDG_CONFIG_HOME"); - if (xdgConfigHome && *xdgConfigHome) { - configDir = xdgConfigHome; - } else { - // Fallback to ~/.config - const char* homeDir = std::getenv("HOME"); - if (homeDir && *homeDir) { - configDir = std::filesystem::path(homeDir) / ".config"; - } else { - // Last resort: use current directory - configDir = "."; - } +void MainWindow::on_game_state_changed() { + if (!minefield_) return; + + // Update Flags + lbl_flags_.set_markup("🚩 " + std::to_string(minefield_->remaining_flags()) + ""); + + // Check Game Over + GameState state = minefield_->state(); + if (state == GameState::Won || state == GameState::Lost) { + timer_conn_.disconnect(); + show_game_over_dialog(state == GameState::Won); } - // Create minesweeper config directory - configDir = configDir / "minesweeper"; - std::filesystem::create_directories(configDir); - - return configDir; + // Update timer immediately to reflect end time if stopped + on_timer_tick(); } -void MainWindow::loadLeaderboard() -{ - m_leaderboard.clear(); +bool MainWindow::on_timer_tick() { + if (!minefield_) return true; - // Get leaderboard file path - std::filesystem::path leaderboardPath = getConfigDir() / "leaderboard.txt"; + auto elapsed = minefield_->get_elapsed_time(); + auto secs = std::chrono::duration_cast(elapsed).count(); - // Open file - std::ifstream file(leaderboardPath); - if (!file.is_open()) { - return; - } + int mm = secs / 60; + int ss = secs % 60; - // Read leaderboard entries - std::string line; - while (std::getline(file, line)) { - std::istringstream iss(line); - GameScore score; - - // Format: name,difficulty,time,date - std::getline(iss, score.playerName, ','); - std::getline(iss, score.difficulty, ','); - std::string timeStr; - std::getline(iss, timeStr, ','); - score.time = std::stoi(timeStr); - std::getline(iss, score.date); - - m_leaderboard.push_back(score); - } + char buf[32]; + snprintf(buf, sizeof(buf), "⏱️ %02d:%02d", mm, ss); + lbl_time_.set_markup(buf); - // Sort leaderboard by time (ascending) - std::sort(m_leaderboard.begin(), m_leaderboard.end()); + return true; // Keep calling } -void MainWindow::saveLeaderboard() -{ - // Get leaderboard file path - std::filesystem::path leaderboardPath = getConfigDir() / "leaderboard.txt"; - - // Open file - std::ofstream file(leaderboardPath); - if (!file.is_open()) { - std::cerr << "Failed to save leaderboard" << std::endl; - return; - } - - // Write leaderboard entries - for (const auto& score : m_leaderboard) { - file << score.playerName << "," - << score.difficulty << "," - << score.time << "," - << score.date << std::endl; - } -} - -void MainWindow::showLeaderboard() -{ - // Create leaderboard dialog - auto dialog = Gtk::make_managed("Leaderboard", *this, true); - dialog->set_default_size(500, 400); - - // Create scrolled window - auto scrolledWindow = Gtk::make_managed(); - scrolledWindow->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC); - scrolledWindow->set_hexpand(true); - scrolledWindow->set_vexpand(true); - scrolledWindow->set_margin(5); - - // Create a GTK4-compatible list view - // Using a simple TreeView-like implementation - // (Since ColumnView is not available or has compatibility issues) - auto listBox = Gtk::make_managed(); - listBox->set_selection_mode(Gtk::SelectionMode::NONE); - listBox->set_show_separators(true); - - // Add header row - auto headerRow = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 10); - headerRow->add_css_class("header-row"); - - auto rankHeader = Gtk::make_managed("Rank"); - rankHeader->set_hexpand(false); - rankHeader->set_width_chars(5); - rankHeader->set_halign(Gtk::Align::START); - rankHeader->add_css_class("header"); - - auto nameHeader = Gtk::make_managed("Player"); - nameHeader->set_hexpand(true); - nameHeader->set_halign(Gtk::Align::START); - nameHeader->add_css_class("header"); - - auto difficultyHeader = Gtk::make_managed("Difficulty"); - difficultyHeader->set_hexpand(true); - difficultyHeader->set_halign(Gtk::Align::START); - difficultyHeader->add_css_class("header"); - - auto timeHeader = Gtk::make_managed("Time"); - timeHeader->set_hexpand(true); - timeHeader->set_halign(Gtk::Align::START); - timeHeader->add_css_class("header"); - - auto dateHeader = Gtk::make_managed("Date"); - dateHeader->set_hexpand(true); - dateHeader->set_halign(Gtk::Align::START); - dateHeader->add_css_class("header"); - - headerRow->append(*rankHeader); - headerRow->append(*nameHeader); - headerRow->append(*difficultyHeader); - headerRow->append(*timeHeader); - headerRow->append(*dateHeader); - - // Add CSS for header - auto css_provider = Gtk::CssProvider::create(); - css_provider->load_from_data( - ".header { font-weight: bold; }" - ".header-row { margin: 5px; }" - ".score-row { margin: 5px; }" +void MainWindow::show_game_over_dialog(bool won) { + auto dialog = Gtk::make_managed( + *this, + won ? "Congratulations!" : "Game Over", + false, + Gtk::MessageType::INFO, + Gtk::ButtonsType::OK, + true ); - auto display = Gdk::Display::get_default(); - Gtk::StyleContext::add_provider_for_display( - display, css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION - ); - - auto headerItem = Gtk::make_managed(); - headerItem->set_child(*headerRow); - listBox->append(*headerItem); - - // Add score rows - for (size_t i = 0; i < m_leaderboard.size(); i++) { - const auto& score = m_leaderboard[i]; - - auto row = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 10); - row->add_css_class("score-row"); - - auto rank = Gtk::make_managed(std::to_string(i + 1)); - rank->set_hexpand(false); - rank->set_width_chars(5); - rank->set_halign(Gtk::Align::START); - - auto name = Gtk::make_managed(score.playerName); - name->set_hexpand(true); - name->set_halign(Gtk::Align::START); - - auto difficulty = Gtk::make_managed(score.difficulty); - difficulty->set_hexpand(true); - difficulty->set_halign(Gtk::Align::START); - - auto time = Gtk::make_managed(formatTime(score.time)); - time->set_hexpand(true); - time->set_halign(Gtk::Align::START); - - auto date = Gtk::make_managed(score.date); - date->set_hexpand(true); - date->set_halign(Gtk::Align::START); - - row->append(*rank); - row->append(*name); - row->append(*difficulty); - row->append(*time); - row->append(*date); - - auto item = Gtk::make_managed(); - item->set_child(*row); - listBox->append(*item); - } - - scrolledWindow->set_child(*listBox); - - // Add to dialog - dialog->get_content_area()->append(*scrolledWindow); - - // Add close button - dialog->add_button("Close", Gtk::ResponseType::CLOSE); - - // Show dialog - dialog->signal_response().connect([dialog](int) { - dialog->close(); - }); - - dialog->show(); -} - -void MainWindow::showNameInputDialog(int time) -{ - // Create name input dialog - auto dialog = Gtk::make_managed("You Won!", *this, true); - dialog->set_default_size(300, 200); - - // Add content area - auto contentArea = dialog->get_content_area(); - contentArea->set_margin(10); - contentArea->set_spacing(10); - - // Add congratulations message - auto congratsLabel = Gtk::make_managed(); - congratsLabel->set_markup("Congratulations!"); - congratsLabel->set_margin(10); - - // Add time message - auto timeLabel = Gtk::make_managed(); - timeLabel->set_markup(Glib::ustring::compose( - "You completed the game in %1", formatTime(time) - )); - - // Add difficulty message - auto difficultyLabel = Gtk::make_managed(); - difficultyLabel->set_markup(Glib::ustring::compose( - "Difficulty: %1", m_currentDifficulty - )); - - // Add name input field - auto nameBox = Gtk::make_managed(Gtk::Orientation::HORIZONTAL, 5); - auto nameLabel = Gtk::make_managed("Your name:"); - nameLabel->set_halign(Gtk::Align::START); - auto nameEntry = Gtk::make_managed(); - nameEntry->set_text("Player"); - nameEntry->set_hexpand(true); - nameBox->append(*nameLabel); - nameBox->append(*nameEntry); - - // Add widgets to dialog - contentArea->append(*congratsLabel); - contentArea->append(*timeLabel); - contentArea->append(*difficultyLabel); - contentArea->append(*nameBox); - - // Add buttons - dialog->add_button("Skip", Gtk::ResponseType::CANCEL); - dialog->add_button("Save Score", Gtk::ResponseType::OK); - dialog->set_default_response(Gtk::ResponseType::OK); - - // Show dialog and handle response - dialog->signal_response().connect([this, dialog, nameEntry, time](int response) { + dialog->set_secondary_text(won ? "You cleared the minefield!" : "Better luck next time."); + dialog->signal_response().connect([dialog, this](int response) { + dialog->hide(); if (response == Gtk::ResponseType::OK) { - std::string playerName = nameEntry->get_text(); - if (playerName.empty()) { - playerName = "Anonymous"; - } - - // Add score to leaderboard - addScoreToLeaderboard(playerName, m_currentDifficulty, time); - - // Show leaderboard - showLeaderboard(); + // Optional: restart? } - - //dialog->close(); - - // Ask if player wants to play again - auto newGameDialog = Gtk::make_managed( - *this, "New Game?", false, Gtk::MessageType::QUESTION, Gtk::ButtonsType::NONE - ); - newGameDialog->set_secondary_text("Would you like to play again?"); - newGameDialog->add_button("Same Difficulty", 1); - newGameDialog->add_button("Change Difficulty", 2); - newGameDialog->set_default_response(1); - - newGameDialog->signal_response().connect([this, newGameDialog](int response) { - newGameDialog->close(); - if (response == 1) { - // Reset game with same settings - onNewGameClick(); - } else { - // Show difficulty dialog - showDifficultyDialog(); - } - }); - - newGameDialog->show(); }); dialog->show(); } - -void MainWindow::addScoreToLeaderboard(const std::string& playerName, const std::string& difficulty, int time) -{ - // Create new score - GameScore score; - score.playerName = playerName; - score.difficulty = difficulty; - score.time = time; - - // Add date - auto now = std::chrono::system_clock::now(); - auto time_t = std::chrono::system_clock::to_time_t(now); - std::stringstream ss; - ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d"); - score.date = ss.str(); - - // Add to leaderboard - m_leaderboard.push_back(score); - - // Sort by time (ascending) - std::sort(m_leaderboard.begin(), m_leaderboard.end()); - - // Limit to top 100 scores - if (m_leaderboard.size() > 100) { - m_leaderboard.resize(100); - } - - // Save leaderboard - saveLeaderboard(); -} - -int main(int argc, char **argv) -{ - auto app = Gtk::Application::create("org.gtkmm.minesweeper"); - return app->make_window_and_run(argc, argv); -} diff --git a/src/window.hpp b/src/window.hpp index 6fc240e..e1a0a3c 100644 --- a/src/window.hpp +++ b/src/window.hpp @@ -1,98 +1,40 @@ #pragma once -#include "minefield.hpp" -#include #include -#include -#include -#include -#include -#include +#include +#include "minefield.hpp" +#include "board_widget.hpp" -class MainWindow : public Gtk::ApplicationWindow -{ +class MainWindow : public Gtk::ApplicationWindow { public: MainWindow(); - virtual ~MainWindow() = default; + ~MainWindow() override; private: - // UI containers - Gtk::Box m_boxMain{Gtk::Orientation::VERTICAL}; - Gtk::HeaderBar m_headerBar; - Gtk::Box m_statusBox{Gtk::Orientation::HORIZONTAL}; - Gtk::Grid m_grid; - Gtk::Overlay m_overlay; + // UI Setup + void setup_header_bar(); + void setup_board(); - // Game status widgets - Gtk::Label m_minesLabel; - Gtk::Label m_flagsLabel; - Gtk::Label m_timeLabel; + // Actions + void start_new_game(const GameDifficulty& difficulty); + void on_game_state_changed(); + bool on_timer_tick(); + void show_game_over_dialog(bool won); + + // Widgets + Gtk::HeaderBar header_bar_; + Gtk::Button btn_new_game_; + Gtk::MenuButton btn_difficulty_; + Gtk::Popover menu_difficulty_; + Gtk::Box box_difficulty_; // Content for popover - // Header bar controls - Gtk::Button m_newGameButton; - Gtk::MenuButton m_difficultyButton; + Gtk::Label lbl_time_; + Gtk::Label lbl_flags_; - // Game field and buttons - std::unique_ptr m_field; - std::vector> m_buttons; + BoardWidget board_widget_; - // Resources - std::shared_ptr m_textureBomb; - std::shared_ptr m_textureFlag; - std::shared_ptr m_textureFlagBomb; - std::shared_ptr m_textureExplosion; - - // Game state tracking - bool m_firstClick; - sigc::connection m_timerConnection; - - // Leaderboard - std::vector m_leaderboard; - Gtk::Dialog* m_leaderboardDialog; - Gtk::Dialog* m_difficultyDialog; - Gtk::Dialog* m_winDialog; - Gtk::Dialog* m_nameDialog; - std::string m_currentDifficulty; - - // Methods - void setupUI(); - void setupHeaderBar(); - void setupStatusBar(); - void setupGameBoard(); - void setupLeaderboard(); - void setupCSSProviders(); - void loadResources(); - - // Event handlers - void onCellClick(int x, int y); - void onCellRightClick(int n_press, double n_x, double n_y, int index); - void onNewGameClick(); - void onDifficultySelected(const GameDifficulty& difficulty); - void showDifficultyDialog(); - - // Game callbacks - void updateCell(int x, int y); - void updateFlagsLabel(int flags); - void updateTimeLabel(); - bool updateTimer(); - void onGameOver(); - void onGameWon(int time); - void showGameOverAnimation(); - void showGameWonAnimation(); - void revealAllBombs(); - - // Leaderboard methods - void loadLeaderboard(); - void saveLeaderboard(); - void showLeaderboard(); - void addScoreToLeaderboard(const std::string& playerName, const std::string& difficulty, int time); - std::string formatTime(int milliseconds) const; - - // Helper methods - void resetGame(); - void startNewGame(int cols, int rows, int mines, const std::string& difficulty); - void clearBoard(); - void setupConfetti(); - void showNameInputDialog(int time); - std::filesystem::path getConfigDir() const; + // Game State + std::shared_ptr minefield_; + sigc::connection timer_conn_; + GameDifficulty current_difficulty_ = Minefield::DifficultyEasy; };