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.

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