From b5f75a29b72abda9d08682b4ad2b95b062295502 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Thu, 10 Apr 2025 17:15:14 +0100 Subject: [PATCH 1/4] cleaning up --- meson.build | 4 +- src/main.cpp | 30 ------ src/minefield.cpp | 172 ++++++++++++++++------------------ src/minefield.hpp | 72 +++++++------- src/window.cpp | 232 +++++++++++++++++++++++++--------------------- src/window.hpp | 23 +++-- 6 files changed, 252 insertions(+), 281 deletions(-) delete mode 100644 src/main.cpp diff --git a/meson.build b/meson.build index e0f43b7..860be85 100644 --- a/meson.build +++ b/meson.build @@ -11,6 +11,8 @@ res = gnome.compile_resources( ) deps = dependency(['gtkmm-4.0', 'sigc++-3.0']) -src = ['src/window.cpp', 'src/window.hpp', 'src/minefield.hpp', 'src/minefield.cpp', res] +src = ['src/window.cpp', 'src/window.hpp', 'src/minefield.hpp', 'src/minefield.cpp', + 'src/timer.hpp', 'src/timer.cpp', res] + exe = executable('minesweeper', src, dependencies : deps, install : true) diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index c13ef29..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "MineField.hpp" -#include - -int main() { - - MineField field(160,160, 100); - - srand(time(NULL)); - - int x = rand() % 160; - int y = rand() % 160; - - field.initBombs(x, y); - - printf("Opened cell: %i %i\n", x, y); - printf("Neighboor bombs: %i\n", field.bombsNearby(x,y)); - - while(!field.isGameOver()) { - x = rand() % 160; - y = rand() % 160; - - if(field.clearCell(x, y)) { - printf("Opened cell: %i %i\n", x, y); - printf("Neighboor bombs: %i\n", field.bombsNearby(x,y)); - } - else { - printf("Bomb found in cell: %i %i\n", x, y); - } - } -} diff --git a/src/minefield.cpp b/src/minefield.cpp index 14725bb..8668655 100644 --- a/src/minefield.cpp +++ b/src/minefield.cpp @@ -1,166 +1,158 @@ #include "minefield.hpp" #include -MineField::MineField(int cols, int rows, int mines): m_rows(rows), - m_cols(cols), - m_totalMines(mines), - m_remainingFlags(mines), - m_openCells(0), - m_exploded(false) { - for(int i=0; i< m_cols*m_rows; i++) { +MineField::MineField(int cols, int rows, int mines) : m_rows(rows), + m_cols(cols), + m_totalMines(mines), + m_remainingFlags(mines), + m_openCells(0), + m_gameOver(false) +{ + for (int i = 0; i < m_cols * m_rows; i++) + { std::shared_ptr cell = std::make_shared(); m_cells.push_back(cell); } } -MineField::~MineField() { - if(m_timerRunning) { - stopTimer(); - } +MineField::~MineField() +{ + m_cells.clear(); } -void MineField::timerTick() { - - auto start = std::chrono::system_clock::now(); - - while(m_timerRunning) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - auto now = std::chrono::system_clock::now(); - const auto duration = now - start; - std::chrono::milliseconds ms = std::chrono::duration_cast(duration); - m_time += ms.count(); - timerSignal.emit(m_time); - start = std::chrono::system_clock::now(); - } -} - -void MineField::startTimer() { - m_time = 0; - m_timerRunning = true; - m_timerThread = std::thread(&MineField::timerTick, this); -} - -void MineField::stopTimer() { - m_timerRunning = false; - - if(m_timerThread.joinable()) { - m_timerThread.join(); - } -} - -void MineField::initBombs(int x, int y) { +void MineField::initBombs(int x, int y) +{ int remainingMines = m_totalMines; int startPos = x + y * m_rows; - srand(time(NULL)); //initialize rand() + srand(time(NULL)); // initialize rand() - while(remainingMines > 0) { + while (remainingMines > 0) + { int position = rand() % (m_cols * m_rows); - if(isBomb(position % m_cols, position / m_cols) || position == startPos) { + if (isBomb(position % m_cols, position / m_cols) || position == startPos) + { continue; } m_cells.at(position)->isBomb = true; --remainingMines; } - - startTimer(); - - //init the timer to zero and start the timer thread - //timerThread.detach(); //not sure if this is okay (better to call join() when I set the condition to stop the thread) } -bool MineField::openCell(int x, int y) { - if(isBomb(x, y)) { - m_exploded = true; +bool MineField::openCell(int x, int y) +{ + if (isBomb(x, y)) + { + m_gameOver = true; gameOverSignal.emit(); - stopTimer(); + // stopTimer(); return false; } setOpenCell(x, y); - if (bombsNearby(x, y) == 0) { + if (bombsNearby(x, y) == 0) + { openNeighboorhood(x, y); } return true; } -void MineField::computeBombsNearby(int x, int y) { - int total = 0; - //compute bombs in neighboorhood - for(int i=-1; i<2; i++) { - for(int j=-1; j<2; j++) { - if(x+i >= 0 && x+i < m_cols && y+j >= 0 && y+j < m_rows) { - if(isBomb(x+i, y+j)){ - ++total; - } +void MineField::computeBombsNearby(int x, int y) +{ + int total = 0; + // compute bombs in neighboorhood + for (int i = -1; i < 2; i++) + { + for (int j = -1; j < 2; j++) + { + if (x + i >= 0 && x + i < m_cols && y + j >= 0 && y + j < m_rows) + { + if (isBomb(x + i, y + j)) + { + ++total; + } } } } m_cells.at(x + y * m_rows)->bombsNearby = total; } -void MineField::openNeighboorhood(int x, int y) { - //compute bombs in neighboorhood - for(int i=-1; i<2; i++) { - for(int j=-1; j<2; j++) { - if(x+i >= 0 && x+i < m_cols && y+j >= 0 && y+j < m_rows) { - if((isOpened(x+i, y+j) == false) && (isBomb(x+i, y+j) == false)){ - setOpenCell((x+i), (y+j)); - if(bombsNearby(x+i, y+j) == 0) { - openNeighboorhood(x+i, y+j); - } - } +void MineField::openNeighboorhood(int x, int y) +{ + // compute bombs in neighboorhood + for (int i = -1; i < 2; i++) + { + for (int j = -1; j < 2; j++) + { + if (x + i >= 0 && x + i < m_cols && y + j >= 0 && y + j < m_rows) + { + if ((isOpened(x + i, y + j) == false) && (isBomb(x + i, y + j) == false)) + { + setOpenCell((x + i), (y + j)); + if (bombsNearby(x + i, y + j) == 0) + { + openNeighboorhood(x + i, y + j); + } + } } } } } - -bool MineField::isOpened(int x, int y) { + +bool MineField::isOpened(int x, int y) +{ return m_cells.at(x + y * m_rows)->isCleared; } -bool MineField::isFlagged(int x, int y) { +bool MineField::isFlagged(int x, int y) +{ return m_cells.at(x + y * m_rows)->isFlagged; } -bool MineField::isBomb(int x, int y) { +bool MineField::isBomb(int x, int y) +{ return m_cells.at(x + y * m_rows)->isBomb; } -int MineField::bombsNearby(int x, int y) { - if(m_cells.at(x + y * m_rows)->bombsNearby == -1) { +int MineField::bombsNearby(int x, int y) +{ + if (m_cells.at(x + y * m_rows)->bombsNearby == -1) + { computeBombsNearby(x, y); - } + } return m_cells.at(x + y * m_rows)->bombsNearby; } -void MineField::setOpenCell(int x, int y) { +void MineField::setOpenCell(int x, int y) +{ m_cells.at(x + y * m_rows)->isCleared = true; openCellSignal.emit(x, y); ++m_openCells; checkGameWon(); } -void MineField::checkGameWon() { - if((m_openCells == (m_cols * m_rows - m_totalMines)) && (m_exploded == false) && (m_remainingFlags == 0)) { +void MineField::checkGameWon() +{ + if ((m_openCells == (m_cols * m_rows - m_totalMines)) && (m_gameOver == false) && (m_remainingFlags == 0)) + { m_gameWon = true; - stopTimer(); gameWonSignal.emit(); } - std::cout << "Open cells: " << m_openCells << "\n" << "Remaining Flags: " << m_remainingFlags << "\n"; - } -bool MineField::toggleFlag(int x, int y) { - if(m_cells.at(x + y * m_rows)->isFlagged == true) { +bool MineField::toggleFlag(int x, int y) +{ + if (m_cells.at(x + y * m_rows)->isFlagged == true) + { m_cells.at(x + y * m_rows)->isFlagged = false; ++m_remainingFlags; remainingFlagsSignal.emit(m_remainingFlags); return true; } - else if(m_remainingFlags > 0) { + else if (m_remainingFlags > 0) + { m_cells.at(x + y * m_rows)->isFlagged = true; --m_remainingFlags; remainingFlagsSignal.emit(m_remainingFlags); diff --git a/src/minefield.hpp b/src/minefield.hpp index 45d8384..60f5d06 100644 --- a/src/minefield.hpp +++ b/src/minefield.hpp @@ -1,43 +1,35 @@ #pragma once -#include -#include -#include -#include -#include -#include +#include #include -#include -#include +#include +#include -struct Cell { - bool isFlagged; - bool isCleared; - bool isBomb; - int bombsNearby; - Cell(): isFlagged(false), isCleared(false), isBomb(false), bombsNearby(-1) {}; -}; +class MineField +{ + + struct Cell + { + bool isFlagged = false; + bool isCleared = false; + bool isBomb = false; + int bombsNearby = -1; + }; -class MineField { std::vector> m_cells; - int m_rows; - int m_cols; - int m_totalMines; - int m_remainingFlags; - int m_openCells; - bool m_exploded; - bool m_gameWon; - size_t m_time; - std::atomic_bool m_timerRunning; - std::thread m_timerThread; + int m_rows; + int m_cols; + int m_totalMines; + int m_remainingFlags; + int m_openCells; + bool m_gameOver; + bool m_gameWon; + void computeBombsNearby(int x, int y); void openNeighboorhood(int x, int y); void setOpenCell(int x, int y); void checkGameWon(); - void timerTick(); - void startTimer(); - void stopTimer(); - + public: MineField(int cols, int rows, int mines); ~MineField(); @@ -47,18 +39,16 @@ public: bool isOpened(int x, int y); bool openCell(int x, int y); int bombsNearby(int x, int y); - bool isGameOver() {return m_exploded; }; - int getCols() {return m_cols; }; - int getRows() {return m_rows; }; + bool isGameOver() { return m_gameOver; }; + int getCols() { return m_cols; }; + int getRows() { return m_rows; }; bool toggleFlag(int x, int y); - int getRemainingFlags() {return m_remainingFlags; }; - size_t getCurrentTime() {return m_time; }; - int getTotalMines() {return m_totalMines; }; + int getRemainingFlags() { return m_remainingFlags; }; + int getTotalMines() { return m_totalMines; }; void startNewGame(int cols, int rows, int mines); - sigc::signal openCellSignal; - sigc::signal remainingFlagsSignal; - sigc::signal gameWonSignal; - sigc::signal gameOverSignal; - sigc::signal timerSignal; + sigc::signal openCellSignal; + sigc::signal remainingFlagsSignal; + sigc::signal gameWonSignal; + sigc::signal gameOverSignal; }; diff --git a/src/window.cpp b/src/window.cpp index 7ab40e8..7660f6a 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -3,7 +3,6 @@ #include "sigc++/adaptors/bind.h" #include "sigc++/functors/mem_fun.h" - //} // void MainWindow::ApplyStyles() { // // Load and apply the CSS file @@ -12,21 +11,25 @@ // Gtk::StyleContext::add_provider_for_display(Gdk::Display::get_default(), css_provider, GTK_STYLE_PROVIDER_PRIORITY_USER); // } -void MainWindow::OnCellRightClick(int n_press, double n_x, double n_y, int index) { +void MainWindow::OnCellRightClick(int n_press, double n_x, double n_y, int index) +{ (void)n_press, (void)n_x, (void)n_y; int x = index % field.getCols(); int y = index / field.getCols(); int pos = x + y * field.getRows(); - if(field.isOpened(x, y) == false) { + if (field.isOpened(x, y) == false) + { field.toggleFlag(x, y); - if(field.isFlagged(x, y)) { + if (field.isFlagged(x, y)) + { auto imgflag = Gtk::make_managed(); imgflag->set(m_textureFlag); buttons.at(pos)->set_child(*imgflag); buttons.at(pos)->set_active(true); } - else { + else + { buttons.at(pos)->unset_child(); buttons.at(pos)->queue_draw(); buttons.at(pos)->set_active(false); @@ -34,7 +37,8 @@ void MainWindow::OnCellRightClick(int n_press, double n_x, double n_y, int index } } -void MainWindow::updateFlagsLabel(int flags) { +void MainWindow::updateFlagsLabel(int flags) +{ Glib::ustring msg = Glib::ustring::compose("Remaining flags: %1", flags); flagLabel.set_label(msg); } @@ -57,78 +61,89 @@ void MainWindow::updateFlagsLabel(int flags) { // clockConn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &MainWindow::UpdateClockLabel), 100); // } +void MainWindow::OnCellClick(int x, int y) +{ + if (newGame) + { + field.initBombs(x, y); + newGame = false; + } - - -void MainWindow::OnCellClick(int x, int y) { - if (newGame) { - field.initBombs(x, y); - newGame = false; + if (field.isFlagged(x, y)) + { + buttons.at(x + y * field.getRows())->set_active(true); + } + else + { + field.openCell(x, y); + if (field.isBomb(x, y)) + { + openBombs(); } + } +} - if(field.isFlagged(x, y)) { - buttons.at(x + y * field.getRows())->set_active(true); - } - else { - field.openCell(x, y); - if(field.isBomb(x, y)) { - openBombs(); - } - } -} - -void MainWindow::openBombs() { - for(int i=0; i < field.getCols() * field.getRows(); i++) { +void MainWindow::openBombs() +{ + for (int i = 0; i < field.getCols() * field.getRows(); i++) + { int x = i % field.getCols(); int y = i / field.getCols(); buttons.at(i)->set_sensitive(false); - if(field.isBomb(x, y)) { - if(field.isFlagged(x, y)) { - auto imgFlagBomb = std::make_shared(); - imgFlagBomb->set(m_textureFlagBomb); - buttons.at(i)->set_child(*imgFlagBomb); + if (field.isBomb(x, y)) + { + if (field.isFlagged(x, y)) + { + auto imgFlagBomb = std::make_shared(); + imgFlagBomb->set(m_textureFlagBomb); + buttons.at(i)->set_child(*imgFlagBomb); } - else { - auto imgBomb = std::make_shared(); - imgBomb->set(m_textureBomb); - buttons.at(i)->set_child(*imgBomb); + else + { + auto imgBomb = std::make_shared(); + imgBomb->set(m_textureBomb); + buttons.at(i)->set_child(*imgBomb); } buttons.at(i)->set_active(true); } } } -void MainWindow::updateCell(int x, int y) { +void MainWindow::updateCell(int x, int y) +{ int pos = x + y * field.getRows(); - if(field.isOpened(x, y)) { - if (field.bombsNearby(x, y) > 0) { - switch(field.bombsNearby(x, y)) { + if (field.isOpened(x, y)) + { + if (field.bombsNearby(x, y) > 0) + { + switch (field.bombsNearby(x, y)) + { case 1: - buttons.at(pos)->get_style_context()->add_class("label-1"); - break; + buttons.at(pos)->get_style_context()->add_class("label-1"); + break; case 2: - buttons.at(pos)->get_style_context()->add_class("label-2"); - break; + buttons.at(pos)->get_style_context()->add_class("label-2"); + break; case 3: - buttons.at(pos)->get_style_context()->add_class("label-3"); - break; + buttons.at(pos)->get_style_context()->add_class("label-3"); + break; case 4: - buttons.at(pos)->get_style_context()->add_class("label-4"); - break; + buttons.at(pos)->get_style_context()->add_class("label-4"); + break; case 5: - buttons.at(pos)->get_style_context()->add_class("label-5"); - break; + buttons.at(pos)->get_style_context()->add_class("label-5"); + break; case 6: - buttons.at(pos)->get_style_context()->add_class("label-6"); - break; + buttons.at(pos)->get_style_context()->add_class("label-6"); + break; case 7: - buttons.at(pos)->get_style_context()->add_class("label-7"); - break; + buttons.at(pos)->get_style_context()->add_class("label-7"); + break; case 8: - buttons.at(pos)->get_style_context()->add_class("label-8"); - break; + buttons.at(pos)->get_style_context()->add_class("label-8"); + break; } buttons.at(pos)->set_label(Glib::ustring::format(field.bombsNearby(x, y))); } @@ -150,7 +165,6 @@ void MainWindow::updateCell(int x, int y) { // } // } - // bool MainWindow::AllCellsOpened() // { // for(int i=0; iload_from_data( ".label-1 { font-weight: bold; font-size: 1.5em; color: Blue; }\ @@ -244,11 +259,12 @@ MainWindow::MainWindow() .label-6 { font-weight: bold; font-size: 1.5em; color: Salmon; }\ .label-7 { font-weight: bold; font-size: 1.5em; color: Turquoise; }\ .label-8 { font-weight: bold; font-size: 1.5em; color: Magenta; }"); - + auto display = Gdk::Display::get_default(); Gtk::StyleContext::add_provider_for_display(display, css_provider, GTK_STYLE_PROVIDER_PRIORITY_USER); - - for (int i = 0; i < field.getCols() * field.getRows(); i++) { + + for (int i = 0; i < field.getCols() * field.getRows(); i++) + { auto button = std::make_shared(); button->set_size_request(50, 40); button->set_sensitive(true); @@ -257,55 +273,57 @@ MainWindow::MainWindow() int y = i / field.getRows(); button->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::OnCellClick), x, y)); - //button->get_style_context()->add_class("fixed-button"); + // button->get_style_context()->add_class("fixed-button"); auto gesture = Gtk::GestureClick::create(); gesture->set_button(3); - gesture->signal_released().connect(sigc::bind(sigc::mem_fun(*this, \ - &MainWindow::OnCellRightClick), i)); + gesture->signal_released().connect(sigc::bind(sigc::mem_fun(*this, + &MainWindow::OnCellRightClick), + i)); button->add_controller(gesture); buttons.push_back(button); - + grid.attach(*button, x, y); } field.openCellSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::updateCell))); field.remainingFlagsSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::updateFlagsLabel))); field.gameOverSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::gameOver))); - //newGameButton.set_label("New"); - //newGameButton.add_css_class("suggested-action"); - //newGameButton.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::OnNewButtonClick)); + // newGameButton.set_label("New"); + // newGameButton.add_css_class("suggested-action"); + // newGameButton.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::OnNewButtonClick)); - //optionButton.set_icon_name("open-menu"); + // optionButton.set_icon_name("open-menu"); - field.timerSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::handleClockSig))); + // field.timerSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::handleClockSig))); m_clockDispatch.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::updateClockLabel))); - //field.timerSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::updateClockLabel))); - //if (clockSignalConn.connected()) clockSignalConn.disconnect(); - //elapsedTime = 0; - //clockSignalConn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &MainWindow::updateClockLabel), 100); - //} - //create the minefield - //field = new MineField(COLS, MINES); + // field.timerSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::updateClockLabel))); + // if (clockSignalConn.connected()) clockSignalConn.disconnect(); + // elapsedTime = 0; + // clockSignalConn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &MainWindow::updateClockLabel), 100); + // } + // create the minefield + // field = new MineField(COLS, MINES); - //bar.pack_start(newGameButton); - //bar.pack_end(optionButton); - - //grid.set_row_homogeneous(false); - //grid.set_column_homogeneous(false); + // bar.pack_start(newGameButton); + // bar.pack_end(optionButton); + + // grid.set_row_homogeneous(false); + // grid.set_column_homogeneous(false); grid.set_margin(10); - //grid.set_vexpand(true); - //grid.set_hexpand(true); - // grid.set_fill(false); + // grid.set_vexpand(true); + // grid.set_hexpand(true); + // grid.set_fill(false); boxV.append(grid); - + this->set_titlebar(bar); this->set_child(boxV); } -int main(int argc, char **argv) { - auto app = Gtk::Application::create("eu.minesweeper"); +int main(int argc, char **argv) +{ + auto app = Gtk::Application::create("eu.bernardomagri.minesweeper"); return app->make_window_and_run(argc, argv); } diff --git a/src/window.hpp b/src/window.hpp index 6634384..259e384 100644 --- a/src/window.hpp +++ b/src/window.hpp @@ -12,10 +12,9 @@ #define PROJECT_NAME "minesweeper" - class MainWindow : public Gtk::Window { - Gtk::Box boxV{Gtk::Orientation::VERTICAL}; + Gtk::Box boxV{Gtk::Orientation::VERTICAL}; Gtk::Box boxH{Gtk::Orientation::HORIZONTAL}; std::vector> buttons; Gtk::Grid grid; @@ -24,7 +23,7 @@ class MainWindow : public Gtk::Window Gtk::Button optionButton; Gtk::Label flagLabel; Gtk::Label clockLabel; - MineField field {16, 16, 1}; + MineField field{16, 16, 1}; int m_elapsedTime; bool newGame; std::shared_ptr m_textureBomb; @@ -39,16 +38,16 @@ class MainWindow : public Gtk::Window void gameOver(); sigc::connection clockSignalConn; Glib::Dispatcher m_clockDispatch; -// void OpenNearCells(int index); -// void Explode();xo -// bool AllCellsOpened(); + // void OpenNearCells(int index); + // void Explode();xo + // bool AllCellsOpened(); - public: -MainWindow(); -// void OnNewButtonClick(); +public: + MainWindow(); + // void OnNewButtonClick(); void OnCellClick(int x, int y); void OnCellRightClick(int n_press, double n_x, double n_y, int index); -// void ShowGameWonAnimation(); -// void ApplyStyles(); -// bool UpdateClockLabel(); + // void ShowGameWonAnimation(); + // void ApplyStyles(); + // bool UpdateClockLabel(); }; From 16dac0e1f616658ad4481c2605ad7f7ba5881b04 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Thu, 10 Apr 2025 17:18:29 +0100 Subject: [PATCH 2/4] adding a game screenshot --- README.md | 2 ++ screenshots/screen1.png | Bin 0 -> 52716 bytes 2 files changed, 2 insertions(+) create mode 100644 screenshots/screen1.png diff --git a/README.md b/README.md index aded8ef..48392fc 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Minesweeper game in C++ and GTK4 +![Game Screenshot](scrrenshots/screen1.png) + ## Instructions to build Install dependencies diff --git a/screenshots/screen1.png b/screenshots/screen1.png new file mode 100644 index 0000000000000000000000000000000000000000..5cf7e7db3f8c12bc007fc394eb08bbeb83c29839 GIT binary patch literal 52716 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 C)ce~2 literal 0 HcmV?d00001 From 27e403169989265e6d80330f983f01463f54b70f Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Thu, 10 Apr 2025 17:18:29 +0100 Subject: [PATCH 3/4] adding a game screenshot --- README.md | 2 ++ screenshots/screen1.png | Bin 0 -> 52716 bytes 2 files changed, 2 insertions(+) create mode 100644 screenshots/screen1.png diff --git a/README.md b/README.md index aded8ef..4339e6f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Minesweeper game in C++ and GTK4 +![Game Screenshot](screenshots/screen1.png) + ## Instructions to build Install dependencies diff --git a/screenshots/screen1.png b/screenshots/screen1.png new file mode 100644 index 0000000000000000000000000000000000000000..5cf7e7db3f8c12bc007fc394eb08bbeb83c29839 GIT binary patch literal 52716 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 C)ce~2 literal 0 HcmV?d00001 From a4dccdc2a3217ff9cb65ce50032e530a6faaee86 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Thu, 24 Apr 2025 09:53:09 +0100 Subject: [PATCH 4/4] major refactor --- .gitignore | 122 +- README.md | 94 +- meson.build | 36 +- resources/minesweeper.svg | 118 ++ resources/org.gtkmm.minesweeper.desktop | 11 + src/minefield.cpp | 417 +++++-- src/minefield.hpp | 142 ++- src/timer.cpp | 81 ++ src/timer.hpp | 42 + src/window.cpp | 1349 ++++++++++++++++++----- src/window.hpp | 131 ++- 11 files changed, 2069 insertions(+), 474 deletions(-) create mode 100644 resources/minesweeper.svg create mode 100644 resources/org.gtkmm.minesweeper.desktop create mode 100644 src/timer.cpp create mode 100644 src/timer.hpp diff --git a/.gitignore b/.gitignore index 519f608..31de05d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,13 @@ -# ---> C++ +# Build directories +build/ +_build/ +bin/ +lib/ +obj/ + +# Prerequisites +*.d + # Compiled Object files *.slo *.lo @@ -16,6 +25,7 @@ # Fortran module files *.mod +*.smod # Compiled Static libraries *.lai @@ -27,6 +37,112 @@ *.exe *.out *.app +minesweeper -# Backup files -*~ \ No newline at end of file +# CMake and build system +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +compile_commands.json +Makefile +meson.build.user +meson-build/ +meson-logs/ +meson-private/ + +# IDE and text editor files +.vscode/ +.idea/ +*.kdev4 +.kdev4/ + +# Emacs files +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\#* + +# Emacs directory configuration +.dir-locals.el + +# Emacs backup files +*~ + +# Emacs org-mode +.org-id-locations +*_archive + +# Emacs flymake +*_flymake* + +# Emacs eshell files +/eshell/history +/eshell/lastdir + +# Emacs elpa packages +/elpa/ + +# Emacs projectile files +.projectile + +# macOS specific +.DS_Store +.AppleDouble +.LSOverride +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Linux specific +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +# Windows specific +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.lnk + +# Project specific +.gresource +*.gresource + +# Leaderboard and config files +.config/minesweeper/ + +#MacOS files +.cache/ +.DS_Store diff --git a/README.md b/README.md index 4339e6f..f8f9e7c 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,101 @@ -# minesweeper +# MineSweeper -Minesweeper game in C++ and GTK4 +A modern GTK4/C++ implementation of the classic Minesweeper game with multiple difficulty levels, customizable board sizes, animations, and a leaderboard system. ![Game Screenshot](screenshots/screen1.png) -## Instructions to build +## Features -Install dependencies +- Multiple difficulty levels: Beginner, Intermediate, Expert, and Master +- Custom board size and mine count configuration +- Win/lose animations +- Persistent leaderboard to track best scores +- Modern GTK4 user interface +- Modern C++20 implementation -``` -sudo apt install libgtkmm-4.0-dev libsigc++-3.0-dev +## Building from Source + +### Dependencies + +- GTK 4.0 or later +- gtkmm 4.0 or later +- sigc++ 3.0 or later +- Meson build system +- Ninja build system + +### Ubuntu/Debian + +Install dependencies: + +```bash +sudo apt install build-essential meson ninja-build libgtkmm-4.0-dev libsigc++-3.0-dev ``` -Go to the project folder +### Fedora + +Install dependencies: + +```bash +sudo dnf install gcc-c++ meson ninja-build gtkmm4.0-devel libsigc++3-devel ``` + +### Arch Linux + +Install dependencies: + +```bash +sudo pacman -S base-devel meson ninja gtkmm-4.0 libsigc++-3.0 +``` + +### Building the Project + +Clone the repository: + +```bash +git clone https://github.com/username/minesweeper.git cd minesweeper ``` -Setup meson and compile the project +Configure and build with Meson: -``` +```bash meson setup build meson compile -C build ``` + +Run the game: + +```bash +./build/minesweeper +``` + +### Installing + +To install system-wide: + +```bash +meson install -C build +``` + +## Using Nix + +If you use the Nix package manager, you can build and run the application with: + +```bash +nix-build +./result/bin/minesweeper +``` + +Or install it with: + +```bash +nix-env -i -f . +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/meson.build b/meson.build index 860be85..46ccacd 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('minesweeper', 'cpp', - version : '0.1', + version : '0.2.0', default_options : ['warning_level=3', 'cpp_std=c++20']) gnome = import('gnome') @@ -10,9 +10,35 @@ res = gnome.compile_resources( c_name: 'gresources' ) +# Dependencies deps = dependency(['gtkmm-4.0', 'sigc++-3.0']) -src = ['src/window.cpp', 'src/window.hpp', 'src/minefield.hpp', 'src/minefield.cpp', - 'src/timer.hpp', 'src/timer.cpp', res] - -exe = executable('minesweeper', src, dependencies : deps, install : true) +# Source files +src = [ + 'src/window.cpp', + 'src/window.hpp', + 'src/minefield.hpp', + 'src/minefield.cpp', + 'src/timer.hpp', + 'src/timer.cpp', + res +] + +# Executable +executable('minesweeper', + src, + dependencies : deps, + install : true +) + +# Install icons +install_data( + 'resources/minesweeper.svg', + install_dir: join_paths(get_option('datadir'), 'icons/hicolor/scalable/apps') +) + +# Install desktop file +install_data( + 'resources/org.gtkmm.minesweeper.desktop', + install_dir: join_paths(get_option('datadir'), 'applications') +) diff --git a/resources/minesweeper.svg b/resources/minesweeper.svg new file mode 100644 index 0000000..4e29789 --- /dev/null +++ b/resources/minesweeper.svg @@ -0,0 +1,118 @@ + + diff --git a/resources/org.gtkmm.minesweeper.desktop b/resources/org.gtkmm.minesweeper.desktop new file mode 100644 index 0000000..e991c1b --- /dev/null +++ b/resources/org.gtkmm.minesweeper.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=MineSweeper +GenericName=Minesweeper Game +Comment=Classic minesweeper game with modern GTK UI +Keywords=game;puzzle;mine; +Exec=minesweeper +Icon=minesweeper +Terminal=false +Type=Application +Categories=Game;LogicGame; +StartupNotify=true diff --git a/src/minefield.cpp b/src/minefield.cpp index 8668655..6d0369a 100644 --- a/src/minefield.cpp +++ b/src/minefield.cpp @@ -1,163 +1,382 @@ #include "minefield.hpp" +#include +#include +#include #include -MineField::MineField(int cols, int rows, int mines) : m_rows(rows), - m_cols(cols), - m_totalMines(mines), - m_remainingFlags(mines), - m_openCells(0), - m_gameOver(false) +// 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}; + +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) { - for (int i = 0; i < m_cols * m_rows; i++) - { - std::shared_ptr cell = std::make_shared(); - m_cells.push_back(cell); - } + // 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() { - m_cells.clear(); + m_cells.clear(); } -void MineField::initBombs(int x, int y) +void MineField::startTimer() { - - int remainingMines = m_totalMines; - int startPos = x + y * m_rows; - - srand(time(NULL)); // initialize rand() - - while (remainingMines > 0) - { - int position = rand() % (m_cols * m_rows); - if (isBomb(position % m_cols, position / m_cols) || position == startPos) + if (!m_timerRunning) { - continue; + m_startTime = std::chrono::steady_clock::now(); + m_timerRunning = true; + } +} + +void MineField::stopTimer() +{ + if (m_timerRunning) + { + m_endTime = std::chrono::steady_clock::now(); + m_timerRunning = false; + } +} + +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; +} + +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()); + + // 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; } - m_cells.at(position)->isBomb = true; - --remainingMines; - } } bool MineField::openCell(int x, int y) { - if (isBomb(x, y)) - { - m_gameOver = true; - gameOverSignal.emit(); - // stopTimer(); - return false; - } - - setOpenCell(x, y); - - if (bombsNearby(x, y) == 0) - { - openNeighboorhood(x, y); - } - return true; + // 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(); + } + return false; + } + + // Open cell + setOpenCell(x, y); + + // If no bombs nearby, open surrounding cells + if (bombsNearby(x, y) == 0) + { + openNeighboorhood(x, y); + } + + return true; } void MineField::computeBombsNearby(int x, int y) { - int total = 0; - // compute bombs in neighboorhood - for (int i = -1; i < 2; i++) - { - for (int j = -1; j < 2; j++) + int total = 0; + // Check all 8 neighboring cells + for (int i = -1; i <= 1; i++) { - if (x + i >= 0 && x + i < m_cols && y + j >= 0 && y + j < m_rows) - { - if (isBomb(x + i, y + j)) + for (int j = -1; j <= 1; j++) { - ++total; + // 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_rows)->bombsNearby = total; + + m_cells.at(x + y * m_cols)->bombsNearby = total; } void MineField::openNeighboorhood(int x, int y) { - // compute bombs in neighboorhood - for (int i = -1; i < 2; i++) - { - for (int j = -1; j < 2; j++) + // Check all 8 neighboring cells + for (int i = -1; i <= 1; i++) { - if (x + i >= 0 && x + i < m_cols && y + j >= 0 && y + j < m_rows) - { - if ((isOpened(x + i, y + j) == false) && (isBomb(x + i, y + j) == false)) + for (int j = -1; j <= 1; j++) { - setOpenCell((x + i), (y + j)); - if (bombsNearby(x + i, y + j) == 0) - { - openNeighboorhood(x + i, y + 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); + } + } + } } - } } - } } bool MineField::isOpened(int x, int y) { - return m_cells.at(x + y * m_rows)->isCleared; + return m_cells.at(x + y * m_cols)->isCleared; } bool MineField::isFlagged(int x, int y) { - return m_cells.at(x + y * m_rows)->isFlagged; + return m_cells.at(x + y * m_cols)->isFlagged; } bool MineField::isBomb(int x, int y) { - return m_cells.at(x + y * m_rows)->isBomb; + return m_cells.at(x + y * m_cols)->isBomb; } int MineField::bombsNearby(int x, int y) { - if (m_cells.at(x + y * m_rows)->bombsNearby == -1) - { - computeBombsNearby(x, y); - } - return m_cells.at(x + y * m_rows)->bombsNearby; + // 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_rows)->isCleared = true; - openCellSignal.emit(x, y); - ++m_openCells; - checkGameWon(); + m_cells.at(x + y * m_cols)->isCleared = true; + openCellSignal.emit(x, y); + ++m_openCells; + checkGameWon(); } void MineField::checkGameWon() { - if ((m_openCells == (m_cols * m_rows - m_totalMines)) && (m_gameOver == false) && (m_remainingFlags == 0)) - { - m_gameWon = true; - gameWonSignal.emit(); - } + // 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) { - if (m_cells.at(x + y * m_rows)->isFlagged == true) - { - m_cells.at(x + y * m_rows)->isFlagged = false; - ++m_remainingFlags; + // 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; - } - else if (m_remainingFlags > 0) - { - m_cells.at(x + y * m_rows)->isFlagged = true; - --m_remainingFlags; - remainingFlagsSignal.emit(m_remainingFlags); - checkGameWon(); - return true; - } - return false; } diff --git a/src/minefield.hpp b/src/minefield.hpp index 60f5d06..740117b 100644 --- a/src/minefield.hpp +++ b/src/minefield.hpp @@ -4,51 +4,115 @@ #include #include #include +#include +#include + +// Game difficulty presets +struct GameDifficulty { + std::string name; + int cols; + int rows; + 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; + } +}; class MineField { +public: + enum class GameState { + READY, + PLAYING, + WON, + LOST + }; - struct Cell - { - bool isFlagged = false; - bool isCleared = false; - bool isBomb = false; - int bombsNearby = -1; - }; +private: + struct Cell + { + bool isFlagged = false; + bool isCleared = false; + bool isBomb = false; + int bombsNearby = -1; + }; - std::vector> m_cells; - int m_rows; - int m_cols; - int m_totalMines; - int m_remainingFlags; - int m_openCells; - bool m_gameOver; - bool m_gameWon; + 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 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(); - 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); - bool isGameOver() { return m_gameOver; }; - int getCols() { return m_cols; }; - int getRows() { return m_rows; }; - bool toggleFlag(int x, int y); - int getRemainingFlags() { return m_remainingFlags; }; - int getTotalMines() { return m_totalMines; }; - void startNewGame(int cols, int rows, int mines); - - sigc::signal openCellSignal; - sigc::signal remainingFlagsSignal; - sigc::signal gameWonSignal; - sigc::signal gameOverSignal; + MineField(int cols, int rows, int mines); + ~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 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; }; diff --git a/src/timer.cpp b/src/timer.cpp new file mode 100644 index 0000000..65bea5c --- /dev/null +++ b/src/timer.cpp @@ -0,0 +1,81 @@ +#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 new file mode 100644 index 0000000..c2246a6 --- /dev/null +++ b/src/timer.hpp @@ -0,0 +1,42 @@ +#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 7660f6a..df3a0ab 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -1,329 +1,1126 @@ + #include "window.hpp" -#include "gdkmm/texture.h" -#include "sigc++/adaptors/bind.h" -#include "sigc++/functors/mem_fun.h" +#include +#include +#include +#include +#include +#include +#include +#include -//} -// void MainWindow::ApplyStyles() { -// // Load and apply the CSS file -// auto css_provider = Gtk::CssProvider::create(); -// css_provider->load_from_path("style.css"); -// Gtk::StyleContext::add_provider_for_display(Gdk::Display::get_default(), css_provider, GTK_STYLE_PROVIDER_PRIORITY_USER); -// } - -void MainWindow::OnCellRightClick(int n_press, double n_x, double n_y, int index) +MainWindow::MainWindow() { - (void)n_press, (void)n_x, (void)n_y; - int x = index % field.getCols(); - int y = index / field.getCols(); - int pos = x + y * field.getRows(); - - if (field.isOpened(x, y) == false) - { - field.toggleFlag(x, y); - if (field.isFlagged(x, y)) - { - auto imgflag = Gtk::make_managed(); - imgflag->set(m_textureFlag); - buttons.at(pos)->set_child(*imgflag); - buttons.at(pos)->set_active(true); - } - else - { - buttons.at(pos)->unset_child(); - buttons.at(pos)->queue_draw(); - buttons.at(pos)->set_active(false); - } - } + // 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; + + // 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(); } -void MainWindow::updateFlagsLabel(int flags) +void MainWindow::setupUI() { - Glib::ustring msg = Glib::ustring::compose("Remaining flags: %1", flags); - flagLabel.set_label(msg); -} -// void MainWindow::OnNewButtonClick() { -// newGame = true; -// gameOver = false; - -// for (auto &button : buttons) { -// button->set_active(false); -// button->set_sensitive(true); -// button->set_label(""); -// } - -// //field->remainingFlags = MINES; -// Glib::ustring msg = Glib::ustring::compose("Remaining flags: %1", field->remainingFlags); -// flagLabel.set_label(msg); - -// if (clockConn.connected()) clockConn.disconnect(); -// elapsedTime = 0; -// clockConn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &MainWindow::UpdateClockLabel), 100); -// } - -void MainWindow::OnCellClick(int x, int y) -{ - if (newGame) - { - field.initBombs(x, y); - newGame = false; - } - - if (field.isFlagged(x, y)) - { - buttons.at(x + y * field.getRows())->set_active(true); - } - else - { - field.openCell(x, y); - if (field.isBomb(x, y)) - { - openBombs(); - } - } + // Configure main window + set_title("MineSweeper"); + set_default_size(400, 400); + 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); } -void MainWindow::openBombs() +void MainWindow::setupHeaderBar() { - for (int i = 0; i < field.getCols() * field.getRows(); i++) - { - int x = i % field.getCols(); - int y = i / field.getCols(); + // 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); + }); + 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); +} - buttons.at(i)->set_sensitive(false); +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); + + // 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); + + // 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); +} - if (field.isBomb(x, y)) - { - if (field.isFlagged(x, y)) - { - auto imgFlagBomb = std::make_shared(); - imgFlagBomb->set(m_textureFlagBomb); - buttons.at(i)->set_child(*imgFlagBomb); - } - else - { - auto imgBomb = std::make_shared(); - imgBomb->set(m_textureBomb); - buttons.at(i)->set_child(*imgBomb); - } - buttons.at(i)->set_active(true); +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.2em; color: blue; }" + ".label-2 { font-weight: bold; font-size: 1.2em; color: green; }" + ".label-3 { font-weight: bold; font-size: 1.2em; color: darkorange; }" + ".label-4 { font-weight: bold; font-size: 1.2em; color: purple; }" + ".label-5 { font-weight: bold; font-size: 1.2em; color: red; }" + ".label-6 { font-weight: bold; font-size: 1.2em; color: salmon; }" + ".label-7 { font-weight: bold; font-size: 1.2em; color: turquoise; }" + ".label-8 { font-weight: bold; font-size: 1.2em; 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 pos = x + y * field.getRows(); - if (field.isOpened(x, y)) - { - if (field.bombsNearby(x, y) > 0) - { - switch (field.bombsNearby(x, y)) - { - case 1: - buttons.at(pos)->get_style_context()->add_class("label-1"); - break; - case 2: - buttons.at(pos)->get_style_context()->add_class("label-2"); - break; - case 3: - buttons.at(pos)->get_style_context()->add_class("label-3"); - break; - case 4: - buttons.at(pos)->get_style_context()->add_class("label-4"); - break; - case 5: - buttons.at(pos)->get_style_context()->add_class("label-5"); - break; - case 6: - buttons.at(pos)->get_style_context()->add_class("label-6"); - break; - case 7: - buttons.at(pos)->get_style_context()->add_class("label-7"); - break; - case 8: - buttons.at(pos)->get_style_context()->add_class("label-8"); - break; - } - buttons.at(pos)->set_label(Glib::ustring::format(field.bombsNearby(x, 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); + } } - buttons.at(pos)->set_active(true); - buttons.at(pos)->set_sensitive(false); - } -} -// void MainWindow::ShowGameWonAnimation() { -// // Limit the number of confetti images to 10 -// int confettiCount = 10; -// for (int i = 0; i < confettiCount; ++i) { -// Glib::signal_timeout().connect_once([this]() { -// auto confetti = Gtk::make_managed(); -// confetti->set_from_resource("/mineSweeper/confetti"); -// // Randomize position on the grid or overlay. -// grid->attach(*confetti, rand() % COLS, rand() % COLS); -// grid->queue_draw(); -// }, i * 100); // Add confetti with a delay of 100ms each -// } -// } - -// bool MainWindow::AllCellsOpened() -// { -// for(int i=0; iget_active()) -// return false; -// } -// return true; -// } -void MainWindow::gameOver() -{ - // clockSignalConn.disconnect(); - // std::cout << "Signal gameOver emmited\n"; } -void MainWindow::updateClockLabel() +void MainWindow::updateFlagsLabel(int flags) { - - size_t time = 100; // field.getCurrentTime(); - - int deciseconds = (time / 100) % 10; - int seconds = (time / 1000) % 60; - int minutes = (time / 60000) % 60; - - Glib::ustring msg = Glib::ustring::compose("Elapsed 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(std::setfill(L'0'), std::setw(1), deciseconds)); - clockLabel.set_label(msg); + m_flagsLabel.set_label(Glib::ustring::compose("Flags: %1/%2", + flags, + m_field->getTotalMines())); } -void MainWindow::handleClockSig(size_t time) +bool MainWindow::updateTimer() { - (void)time; - m_clockDispatch.emit(); + // Update timer every 100ms + if (m_field->getGameState() == MineField::GameState::PLAYING) { + m_field->timerTick(); + updateTimeLabel(); + return true; + } + return false; } -MainWindow::MainWindow() +void MainWindow::updateTimeLabel() { - // ApplyStyles(); // Load the CSS file - m_elapsedTime = 0; - newGame = true; - set_title("MineSweeper"); - set_default_size(400, 400); - set_resizable(false); + 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) + )); +} - boxV = Gtk::Box(Gtk::Orientation::VERTICAL); - boxH = Gtk::Box(Gtk::Orientation::HORIZONTAL); +void MainWindow::onGameOver() +{ + // Disconnect timer + m_timerConnection.disconnect(); + + // Show all bombs + revealAllBombs(); + + // Show game over animation + showGameOverAnimation(); +} - boxH.set_hexpand(true); +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); +} - boxV.append(boxH); - boxH.set_expand(true); +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); + } + } + } +} - Gtk::Label labelMines; - labelMines.set_margin_top(12); - labelMines.set_margin_start(12); - labelMines.set_label(Glib::ustring::compose("Total mines: %1", field.getTotalMines())); - // labelMines.set_hexpand(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; + } + } + } +} - Glib::ustring msg = Glib::ustring::compose("Remaining flags: %1", field.getRemainingFlags()); - flagLabel = Gtk::Label(msg); - flagLabel.set_margin_top(12); - flagLabel.set_margin_start(12); - flagLabel.set_margin_end(12); - // flagLabel.set_hexpand(true); - clockLabel.set_margin_top(12); - // clockLabel.set_margin_start(12); - // Clocklabel.set_margin_end(12); - clockLabel.set_hexpand(true); - Glib::ustring clockmsg = Glib::ustring::compose("Elapsed time: 00:00.0"); - clockLabel.set_label(clockmsg); +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(); + }); + + // 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); + } +} - boxH.append(labelMines); - boxH.append(clockLabel); - boxH.append(flagLabel); +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); + } + } + } +} - // TODO check if it's okay to mix std::shared_ptr with Gdk::ptr - 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"); +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"); +} - // bombPix.set_from_resource("/minesweeper/bomb-solid"); +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)); + } + } + } + } +} - auto css_provider = Gtk::CssProvider::create(); - css_provider->load_from_data( - ".label-1 { font-weight: bold; font-size: 1.5em; color: Blue; }\ - .label-2 { font-weight: bold; font-size: 1.5em; color: Green; }\ - .label-3 { font-weight: bold; font-size: 1.5em; color: Darkorange; }\ - .label-4 { font-weight: bold; font-size: 1.5em; color: Purple; }\ - .label-5 { font-weight: bold; font-size: 1.5em; color: Red; }\ - .label-6 { font-weight: bold; font-size: 1.5em; color: Salmon; }\ - .label-7 { font-weight: bold; font-size: 1.5em; color: Turquoise; }\ - .label-8 { font-weight: bold; font-size: 1.5em; color: Magenta; }"); +void MainWindow::onDifficultySelected(const GameDifficulty& difficulty) +{ + // Start new game with selected difficulty + startNewGame(difficulty.cols, difficulty.rows, difficulty.mines, difficulty.name); +} - auto display = Gdk::Display::get_default(); - Gtk::StyleContext::add_provider_for_display(display, css_provider, GTK_STYLE_PROVIDER_PRIORITY_USER); +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"); - for (int i = 0; i < field.getCols() * field.getRows(); i++) - { - auto button = std::make_shared(); - button->set_size_request(50, 40); - button->set_sensitive(true); - button->set_active(false); - int x = i % field.getCols(); - int y = i / field.getRows(); - button->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::OnCellClick), x, y)); + // 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); - // button->get_style_context()->add_class("fixed-button"); + // If the window is already visible, we need to queue a resize + queue_resize(); + + // Setup new game board + setupGameBoard(); +} - auto gesture = Gtk::GestureClick::create(); - gesture->set_button(3); - gesture->signal_released().connect(sigc::bind(sigc::mem_fun(*this, - &MainWindow::OnCellRightClick), - i)); - button->add_controller(gesture); +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); + } + }; + + 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(); +} - buttons.push_back(button); +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"); +} - grid.attach(*button, x, y); - } +std::string MainWindow::formatTime(int milliseconds) const +{ + int seconds = milliseconds / 1000; + int minutes = seconds / 60; + seconds %= 60; + + std::stringstream ss; + ss << std::setfill('0') << std::setw(2) << minutes << ":" + << std::setfill('0') << std::setw(2) << seconds; + return ss.str(); +} - field.openCellSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::updateCell))); - field.remainingFlagsSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::updateFlagsLabel))); - field.gameOverSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::gameOver))); - // newGameButton.set_label("New"); - // newGameButton.add_css_class("suggested-action"); - // newGameButton.signal_clicked().connect(sigc::mem_fun(*this, &MainWindow::OnNewButtonClick)); +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 = "."; + } + } + + // Create minesweeper config directory + configDir = configDir / "minesweeper"; + std::filesystem::create_directories(configDir); + + return configDir; +} - // optionButton.set_icon_name("open-menu"); +void MainWindow::loadLeaderboard() +{ + m_leaderboard.clear(); + + // Get leaderboard file path + std::filesystem::path leaderboardPath = getConfigDir() / "leaderboard.txt"; + + // Open file + std::ifstream file(leaderboardPath); + if (!file.is_open()) { + return; + } + + // 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); + } + + // Sort leaderboard by time (ascending) + std::sort(m_leaderboard.begin(), m_leaderboard.end()); +} - // field.timerSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::handleClockSig))); - m_clockDispatch.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::updateClockLabel))); - // field.timerSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::updateClockLabel))); - // if (clockSignalConn.connected()) clockSignalConn.disconnect(); - // elapsedTime = 0; - // clockSignalConn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &MainWindow::updateClockLabel), 100); - // } - // create the minefield - // field = new MineField(COLS, MINES); +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; + } +} - // bar.pack_start(newGameButton); - // bar.pack_end(optionButton); +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; }" + ); + + 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(); +} - // grid.set_row_homogeneous(false); - // grid.set_column_homogeneous(false); - grid.set_margin(10); - // grid.set_vexpand(true); - // grid.set_hexpand(true); - // grid.set_fill(false); +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) { + 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(); + } + + 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(); +} - boxV.append(grid); - - this->set_titlebar(bar); - this->set_child(boxV); +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("eu.bernardomagri.minesweeper"); - return app->make_window_and_run(argc, 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 259e384..6fc240e 100644 --- a/src/window.hpp +++ b/src/window.hpp @@ -1,53 +1,98 @@ #pragma once -#include "glibmm/dispatcher.h" #include "minefield.hpp" #include #include -#include -#include #include -#include -#include +#include +#include +#include +#include -#define PROJECT_NAME "minesweeper" - -class MainWindow : public Gtk::Window +class MainWindow : public Gtk::ApplicationWindow { - Gtk::Box boxV{Gtk::Orientation::VERTICAL}; - Gtk::Box boxH{Gtk::Orientation::HORIZONTAL}; - std::vector> buttons; - Gtk::Grid grid; - Gtk::HeaderBar bar; - Gtk::Button newGameButton; - Gtk::Button optionButton; - Gtk::Label flagLabel; - Gtk::Label clockLabel; - MineField field{16, 16, 1}; - int m_elapsedTime; - bool newGame; - std::shared_ptr m_textureBomb; - std::shared_ptr m_textureFlag; - std::shared_ptr m_textureFlagBomb; - void updateCell(int x, int y); - void openBombs(); - void updateFlagsLabel(int flags); - void updateClockLabel(); - void handleClockSig(size_t); - void gameWon(); - void gameOver(); - sigc::connection clockSignalConn; - Glib::Dispatcher m_clockDispatch; - // void OpenNearCells(int index); - // void Explode();xo - // bool AllCellsOpened(); - public: - MainWindow(); - // void OnNewButtonClick(); - void OnCellClick(int x, int y); - void OnCellRightClick(int n_press, double n_x, double n_y, int index); - // void ShowGameWonAnimation(); - // void ApplyStyles(); - // bool UpdateClockLabel(); + MainWindow(); + virtual ~MainWindow() = default; + +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; + + // Game status widgets + Gtk::Label m_minesLabel; + Gtk::Label m_flagsLabel; + Gtk::Label m_timeLabel; + + // Header bar controls + Gtk::Button m_newGameButton; + Gtk::MenuButton m_difficultyButton; + + // Game field and buttons + std::unique_ptr m_field; + std::vector> m_buttons; + + // 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; };