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; };