updates and fixes

This commit is contained in:
Bernardo Magri
2026-02-25 13:36:13 +00:00
parent 3bc1645721
commit 23316594f7
7 changed files with 383 additions and 1737 deletions

View File

@@ -11,16 +11,20 @@ res = gnome.compile_resources(
) )
# Dependencies # Dependencies
deps = dependency(['gtkmm-4.0', 'sigc++-3.0']) deps = [
dependency('gtkmm-4.0'),
dependency('sigc++-3.0')
]
# Source files # Source files
src = [ src = [
'src/main.cpp',
'src/window.cpp', 'src/window.cpp',
'src/window.hpp', 'src/window.hpp',
'src/minefield.hpp', 'src/minefield.hpp',
'src/minefield.cpp', 'src/minefield.cpp',
'src/timer.hpp', 'src/board_widget.hpp',
'src/timer.cpp', 'src/board_widget.cpp',
res res
] ]

View File

@@ -1,276 +1,153 @@
#include "minefield.hpp" #include "minefield.hpp"
#include <ctime>
#include <random> #include <random>
#include <algorithm> #include <algorithm>
#include <iostream> #include <queue>
// Define difficulty presets const GameDifficulty Minefield::DifficultyEasy = {"Beginner", 9, 9, 10};
const GameDifficulty MineField::DIFFICULTY_EASY = {"Beginner", 9, 9, 10}; const GameDifficulty Minefield::DifficultyMedium = {"Intermediate", 16, 16, 40};
const GameDifficulty MineField::DIFFICULTY_MEDIUM = {"Intermediate", 16, 16, 40}; const GameDifficulty Minefield::DifficultyHard = {"Expert", 30, 16, 99};
const GameDifficulty MineField::DIFFICULTY_HARD = {"Expert", 30, 16, 99}; const GameDifficulty Minefield::DifficultyExpert = {"Master", 30, 20, 145};
const GameDifficulty MineField::DIFFICULTY_EXPERT = {"Master", 30, 20, 145};
MineField::MineField(int cols, int rows, int mines) : Minefield::Minefield(int cols, int rows, int mines)
m_rows(rows), : cols_(cols), rows_(rows), total_mines_(mines), remaining_flags_(mines) {
m_cols(cols), cells_.resize(cols * rows);
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> cell = std::make_shared<Cell>();
m_cells.push_back(cell);
}
} }
MineField::~MineField() const Cell& Minefield::get_cell(int x, int y) const {
{ static const Cell invalid_cell;
m_cells.clear(); if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return invalid_cell;
return cells_[y * cols_ + x];
} }
void MineField::startTimer() std::chrono::milliseconds Minefield::get_elapsed_time() const {
{ if (state_ == GameState::Ready) return std::chrono::milliseconds(0);
if (!m_timerRunning) auto end = (state_ == GameState::Playing) ? std::chrono::steady_clock::now() : end_time_;
{ return std::chrono::duration_cast<std::chrono::milliseconds>(end - start_time_);
m_startTime = std::chrono::steady_clock::now();
m_timerRunning = true;
}
} }
void MineField::stopTimer() void Minefield::generate_mines(int safe_x, int safe_y) {
{ std::vector<int> indices(cells_.size());
if (m_timerRunning) std::iota(indices.begin(), indices.end(), 0);
{
m_endTime = std::chrono::steady_clock::now();
m_timerRunning = false;
}
}
int MineField::getElapsedTime() const // Remove safe zone (3x3 around click)
{ indices.erase(std::remove_if(indices.begin(), indices.end(), [&](int idx) {
if (m_timerRunning) int cx = idx % cols_;
{ int cy = idx / cols_;
auto now = std::chrono::steady_clock::now(); return std::abs(cx - safe_x) <= 1 && std::abs(cy - safe_y) <= 1;
return std::chrono::duration_cast<std::chrono::milliseconds>(now - m_startTime).count(); }), indices.end());
}
else if (m_gameState == GameState::WON || m_gameState == GameState::LOST)
{
return std::chrono::duration_cast<std::chrono::milliseconds>(m_endTime - m_startTime).count();
}
return 0;
}
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> cell = std::make_shared<Cell>();
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<int> 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::random_device rd;
std::mt19937 gen(rd()); std::mt19937 g(rd());
std::shuffle(indices.begin(), indices.end(), g);
// Shuffle positions int placed = 0;
std::shuffle(positions.begin(), positions.end(), gen); for (int idx : indices) {
if (placed >= total_mines_) break;
cells_[idx].is_bomb = true;
placed++;
}
// Place mines // Precompute neighbors
int minesToPlace = std::min(m_totalMines, static_cast<int>(positions.size())); for (int y = 0; y < rows_; ++y) {
for (int i = 0; i < minesToPlace; i++) for (int x = 0; x < cols_; ++x) {
{ if (!cells_[y * cols_ + x].is_bomb) {
m_cells.at(positions[i])->isBomb = true; cells_[y * cols_ + x].nearby_bombs = count_nearby_bombs(x, y);
}
}
} }
} }
bool MineField::openCell(int x, int y) int Minefield::count_nearby_bombs(int x, int y) const {
{ int count = 0;
// Ignore if game is over or cell is already open/flagged for (int dy = -1; dy <= 1; ++dy) {
if (m_gameState != GameState::PLAYING && m_gameState != GameState::READY) for (int dx = -1; dx <= 1; ++dx) {
{ if (dx == 0 && dy == 0) continue;
return false; int nx = x + dx;
} int ny = y + dy;
if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) {
if (isOpened(x, y) || isFlagged(x, y)) if (cells_[ny * cols_ + nx].is_bomb) count++;
{ }
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();
} }
return false; }
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);
} }
// Open cell if (cell.is_bomb) {
setOpenCell(x, y); cell.is_exploded = true;
cell.is_revealed = true;
state_ = GameState::Lost;
end_time_ = std::chrono::steady_clock::now();
// If no bombs nearby, open surrounding cells // Reveal all other bombs
if (bombsNearby(x, y) == 0) for (auto& c : cells_) {
{ if (c.is_bomb && !c.is_flagged) c.is_revealed = true;
openNeighboorhood(x, y); }
return true;
} }
reveal_recursive(x, y);
check_win_condition();
return true; return true;
} }
void MineField::computeBombsNearby(int x, int y) void Minefield::reveal_recursive(int x, int y) {
{ // Non-recursive BFS to avoid stack overflow
int total = 0; std::vector<std::pair<int, int>> q;
// Check all 8 neighboring cells q.reserve(cols_ * rows_);
for (int i = -1; i <= 1; i++) q.push_back({x, y});
{
for (int j = -1; j <= 1; j++)
{
// Skip the cell itself
if (i == 0 && j == 0)
{
continue;
}
int nx = x + i; // Mark initial as revealed
int ny = y + j; if (!cells_[y * cols_ + x].is_revealed) {
cells_[y * cols_ + x].is_revealed = true;
// Check if within bounds revealed_count_++;
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; size_t head = 0;
} while(head < q.size()){
auto [cx, cy] = q[head++];
Cell& current = cells_[cy * cols_ + cx];
void MineField::openNeighboorhood(int x, int y) if (current.nearby_bombs == 0) {
{ for (int dy = -1; dy <= 1; ++dy) {
// Check all 8 neighboring cells for (int dx = -1; dx <= 1; ++dx) {
for (int i = -1; i <= 1; i++) if (dx == 0 && dy == 0) continue;
{ int nx = cx + dx;
for (int j = -1; j <= 1; j++) int ny = cy + dy;
{ if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) {
// Skip the cell itself Cell& neighbor = cells_[ny * cols_ + nx];
if (i == 0 && j == 0) if (!neighbor.is_revealed && !neighbor.is_flagged) {
{ neighbor.is_revealed = true;
continue; revealed_count_++;
} q.push_back({nx, ny});
}
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);
} }
} }
} }
@@ -278,105 +155,82 @@ void MineField::openNeighboorhood(int x, int y)
} }
} }
bool MineField::isOpened(int x, int y) bool Minefield::toggle_flag(int x, int y) {
{ if (state_ == GameState::Won || state_ == GameState::Lost) return false;
return m_cells.at(x + y * m_cols)->isCleared; if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return false;
}
bool MineField::isFlagged(int x, int y) Cell& cell = cells_[y * cols_ + x];
{ if (cell.is_revealed) return false;
return m_cells.at(x + y * m_cols)->isFlagged;
}
bool MineField::isBomb(int x, int y) if (cell.is_flagged) {
{ cell.is_flagged = false;
return m_cells.at(x + y * m_cols)->isBomb; remaining_flags_++;
} } else {
if (remaining_flags_ > 0) {
int MineField::bombsNearby(int x, int y) cell.is_flagged = true;
{ remaining_flags_--;
// Calculate bombs nearby if not already calculated } else {
if (m_cells.at(x + y * m_cols)->bombsNearby == -1) return false;
{
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;
}
} }
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; 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;
}
}
}

View File

@@ -1,13 +1,11 @@
#pragma once #pragma once
#include <cstdlib>
#include <memory>
#include <sigc++/signal.h>
#include <vector> #include <vector>
#include <chrono> #include <chrono>
#include <string> #include <random>
#include <algorithm>
#include <optional>
// Game difficulty presets
struct GameDifficulty { struct GameDifficulty {
std::string name; std::string name;
int cols; int cols;
@@ -15,104 +13,64 @@ struct GameDifficulty {
int mines; int mines;
}; };
// Game score for leaderboard enum class GameState {
struct GameScore { Ready,
std::string playerName; Playing,
std::string difficulty; Won,
int time; Lost
std::string date;
bool operator<(const GameScore& other) const {
return time < other.time;
}
}; };
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: public:
enum class GameState { Minefield(int cols, int rows, int mines);
READY,
PLAYING, // Core actions
WON, // Returns true if state changed
LOST 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: private:
struct Cell void generate_mines(int safe_x, int safe_y);
{ void reveal_recursive(int x, int y);
bool isFlagged = false; void check_win_condition();
bool isCleared = false; int count_nearby_flags(int x, int y) const;
bool isBomb = false; int count_nearby_bombs(int x, int y) const;
int bombsNearby = -1;
};
std::vector<std::shared_ptr<Cell>> m_cells; int cols_;
int m_rows; int rows_;
int m_cols; int total_mines_;
int m_totalMines; int remaining_flags_;
int m_remainingFlags; int revealed_count_ = 0;
int m_openCells;
GameState m_gameState;
std::chrono::time_point<std::chrono::steady_clock> m_startTime;
std::chrono::time_point<std::chrono::steady_clock> m_endTime;
bool m_timerRunning;
void computeBombsNearby(int x, int y); GameState state_ = GameState::Ready;
void openNeighboorhood(int x, int y); std::vector<Cell> cells_;
void setOpenCell(int x, int y);
void checkGameWon();
void startTimer();
void stopTimer();
public: std::chrono::steady_clock::time_point start_time_;
MineField(int cols, int rows, int mines); std::chrono::steady_clock::time_point end_time_;
~MineField();
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 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<void()> resetSignal;
// Signal when a cell is opened
sigc::signal<void(int, int)> openCellSignal;
// Signal when flags count changes
sigc::signal<void(int)> remainingFlagsSignal;
// Signal when game is won
sigc::signal<void(int)> gameWonSignal; // int parameter is elapsed time in ms
// Signal when game is lost
sigc::signal<void()> gameOverSignal;
// Signal for timer updates
sigc::signal<void(int)> 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;
}; };

View File

@@ -1,81 +0,0 @@
#include "timer.hpp"
#include <iostream>
#include <glibmm/main.h>
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<std::mutex> 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<std::mutex> lock(m_mutex);
m_running = false;
m_condition.notify_one();
}
void Timer::reset() {
std::lock_guard<std::mutex> 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<std::chrono::milliseconds>(now - m_startTime).count();
}
return 0;
}
void Timer::timerThread() {
while (true) {
// Wait for timer to be running
{
std::unique_lock<std::mutex> 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);
});
}
}
}

View File

@@ -1,42 +0,0 @@
#pragma once
#include <chrono>
#include <functional>
#include <sigc++/signal.h>
#include <thread>
#include <atomic>
#include <mutex>
#include <condition_variable>
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<void(int)> timerSignal;
private:
std::chrono::time_point<std::chrono::steady_clock> m_startTime;
std::atomic<bool> m_running;
// Thread handling
std::thread m_timerThread;
std::mutex m_mutex;
std::condition_variable m_condition;
// Timer thread function
void timerThread();
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,98 +1,40 @@
#pragma once #pragma once
#include "minefield.hpp"
#include <memory>
#include <gtkmm.h> #include <gtkmm.h>
#include <sigc++/sigc++.h> #include <memory>
#include <vector> #include "minefield.hpp"
#include <string> #include "board_widget.hpp"
#include <fstream>
#include <filesystem>
class MainWindow : public Gtk::ApplicationWindow class MainWindow : public Gtk::ApplicationWindow {
{
public: public:
MainWindow(); MainWindow();
virtual ~MainWindow() = default; ~MainWindow() override;
private: private:
// UI containers // UI Setup
Gtk::Box m_boxMain{Gtk::Orientation::VERTICAL}; void setup_header_bar();
Gtk::HeaderBar m_headerBar; void setup_board();
Gtk::Box m_statusBox{Gtk::Orientation::HORIZONTAL};
Gtk::Grid m_grid;
Gtk::Overlay m_overlay;
// Game status widgets // Actions
Gtk::Label m_minesLabel; void start_new_game(const GameDifficulty& difficulty);
Gtk::Label m_flagsLabel; void on_game_state_changed();
Gtk::Label m_timeLabel; bool on_timer_tick();
void show_game_over_dialog(bool won);
// Header bar controls // Widgets
Gtk::Button m_newGameButton; Gtk::HeaderBar header_bar_;
Gtk::MenuButton m_difficultyButton; Gtk::Button btn_new_game_;
Gtk::MenuButton btn_difficulty_;
Gtk::Popover menu_difficulty_;
Gtk::Box box_difficulty_; // Content for popover
// Game field and buttons Gtk::Label lbl_time_;
std::unique_ptr<MineField> m_field; Gtk::Label lbl_flags_;
std::vector<std::shared_ptr<Gtk::ToggleButton>> m_buttons;
// Resources BoardWidget board_widget_;
std::shared_ptr<Gdk::Texture> m_textureBomb;
std::shared_ptr<Gdk::Texture> m_textureFlag;
std::shared_ptr<Gdk::Texture> m_textureFlagBomb;
std::shared_ptr<Gdk::Texture> m_textureExplosion;
// Game state tracking // Game State
bool m_firstClick; std::shared_ptr<Minefield> minefield_;
sigc::connection m_timerConnection; sigc::connection timer_conn_;
GameDifficulty current_difficulty_ = Minefield::DifficultyEasy;
// Leaderboard
std::vector<GameScore> 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;
}; };