Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2456814295 | ||
|
|
23316594f7 | ||
|
|
3bc1645721 | ||
|
|
05bc82a5c1 | ||
|
|
039de24da8 | ||
|
|
fe0b39a49d | ||
|
|
c61173308a | ||
|
|
7312e473b8 | ||
|
|
a4dccdc2a3 | ||
|
|
0cdc668629 | ||
|
|
27e4031699 | ||
|
|
16dac0e1f6 | ||
|
|
b5f75a29b7 | ||
|
|
0399aaf94f | ||
|
|
2a1d45f998 | ||
|
|
4ff86eb027 | ||
|
|
b6a6e16d3a | ||
|
|
8c1cf20228 | ||
|
|
8afb29b680 |
120
.gitignore
vendored
120
.gitignore
vendored
@@ -1,4 +1,13 @@
|
|||||||
# ---> C++
|
# Build directories
|
||||||
|
build/
|
||||||
|
_build/
|
||||||
|
bin/
|
||||||
|
lib/
|
||||||
|
obj/
|
||||||
|
|
||||||
|
# Prerequisites
|
||||||
|
*.d
|
||||||
|
|
||||||
# Compiled Object files
|
# Compiled Object files
|
||||||
*.slo
|
*.slo
|
||||||
*.lo
|
*.lo
|
||||||
@@ -16,6 +25,7 @@
|
|||||||
|
|
||||||
# Fortran module files
|
# Fortran module files
|
||||||
*.mod
|
*.mod
|
||||||
|
*.smod
|
||||||
|
|
||||||
# Compiled Static libraries
|
# Compiled Static libraries
|
||||||
*.lai
|
*.lai
|
||||||
@@ -27,6 +37,112 @@
|
|||||||
*.exe
|
*.exe
|
||||||
*.out
|
*.out
|
||||||
*.app
|
*.app
|
||||||
|
minesweeper
|
||||||
|
|
||||||
# Backup files
|
# 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
|
||||||
|
|||||||
96
README.md
96
README.md
@@ -1,23 +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
|

|
||||||
|
|
||||||
Install dependencies
|
## Features
|
||||||
|
|
||||||
```
|
- Multiple difficulty levels: Beginner, Intermediate, Expert, and Master
|
||||||
sudo apt install libgtkmm-4.0-dev libsigc++-3.0-dev
|
- Custom board size and mine count configuration
|
||||||
|
- Win/lose animations
|
||||||
|
- Persistent leaderboard to track best scores
|
||||||
|
- Modern GTK4 user interface
|
||||||
|
- Modern C++20 implementation
|
||||||
|
|
||||||
|
## 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
|
cd minesweeper
|
||||||
```
|
```
|
||||||
|
|
||||||
Setup meson and compile the project
|
Configure and build with Meson:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
meson setup build
|
meson setup build
|
||||||
meson compile -C 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.
|
||||||
|
|||||||
48
default.nix
Normal file
48
default.nix
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{ lib
|
||||||
|
, stdenv
|
||||||
|
, fetchFromGitHub
|
||||||
|
, meson
|
||||||
|
, ninja
|
||||||
|
, pkg-config
|
||||||
|
, wrapGAppsHook4
|
||||||
|
, gtk4
|
||||||
|
, gtkmm4
|
||||||
|
, libsigcxx
|
||||||
|
}:
|
||||||
|
|
||||||
|
stdenv.mkDerivation rec {
|
||||||
|
pname = "minesweeper";
|
||||||
|
version = "0.2.0";
|
||||||
|
|
||||||
|
src = ./.;
|
||||||
|
|
||||||
|
# If you want to use fetchFromGitHub instead:
|
||||||
|
# src = fetchFromGitHub {
|
||||||
|
# owner = "username";
|
||||||
|
# repo = "minesweeper";
|
||||||
|
# rev = "v${version}";
|
||||||
|
# hash = "sha256-0000000000000000000000000000000000000000000=";
|
||||||
|
# };
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
meson
|
||||||
|
ninja
|
||||||
|
pkg-config
|
||||||
|
wrapGAppsHook4
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
gtk4
|
||||||
|
gtkmm4
|
||||||
|
libsigcxx
|
||||||
|
];
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "Modern Minesweeper game with GTK4 interface";
|
||||||
|
homepage = "https://github.com/username/minesweeper";
|
||||||
|
license = licenses.mit;
|
||||||
|
platforms = platforms.linux;
|
||||||
|
maintainers = with maintainers; [ /* your name here */ ];
|
||||||
|
mainProgram = "minesweeper";
|
||||||
|
};
|
||||||
|
}
|
||||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1735563628,
|
||||||
|
"narHash": "sha256-OnSAY7XDSx7CtDoqNh8jwVwh4xNL/2HaJxGjryLWzX8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "b134951a4c9f3c995fd7be05f3243f8ecd65d798",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-24.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
92
flake.nix
Normal file
92
flake.nix
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
description = "GTK4 Minesweeper game";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
|
||||||
|
# Robust fallbacks across channels
|
||||||
|
wrapGApps = pkgs.wrapGAppsHook4 or pkgs.wrapGAppsHook;
|
||||||
|
adwaitaTheme = pkgs.gnome.adwaita-icon-theme or pkgs.adwaita-icon-theme;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# Main package
|
||||||
|
packages.default = pkgs.stdenv.mkDerivation rec {
|
||||||
|
pname = "minesweeper";
|
||||||
|
version = "0.1";
|
||||||
|
src = self;
|
||||||
|
|
||||||
|
strictDeps = true;
|
||||||
|
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
meson
|
||||||
|
ninja
|
||||||
|
pkg-config
|
||||||
|
gobject-introspection
|
||||||
|
wrapGApps
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
gtk4
|
||||||
|
gtkmm4
|
||||||
|
glibmm
|
||||||
|
libsigcxx30
|
||||||
|
gdk-pixbuf
|
||||||
|
librsvg # SVG loader
|
||||||
|
gvfs # GIO modules
|
||||||
|
gsettings-desktop-schemas
|
||||||
|
hicolor-icon-theme
|
||||||
|
adwaitaTheme
|
||||||
|
];
|
||||||
|
|
||||||
|
# Ensure we use Nix-provided GIO modules, not host ones
|
||||||
|
preFixup = ''
|
||||||
|
gappsWrapperArgs+=(
|
||||||
|
--set GIO_EXTRA_MODULES ${pkgs.gvfs}/lib/gio/modules
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = with pkgs.lib; {
|
||||||
|
description = "A simple GTKmm4 Minesweeper game";
|
||||||
|
homepage = "https://example.org/minesweeper";
|
||||||
|
license = licenses.gpl3Plus;
|
||||||
|
platforms = platforms.linux;
|
||||||
|
mainProgram = "minesweeper";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# nix run support
|
||||||
|
apps.${system}.default = {
|
||||||
|
type = "app";
|
||||||
|
program = "${self.packages.${system}.default}/bin/minesweeper";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Dev shell for hacking
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
meson
|
||||||
|
ninja
|
||||||
|
pkg-config
|
||||||
|
gobject-introspection
|
||||||
|
];
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
gtk4
|
||||||
|
gtkmm4
|
||||||
|
glibmm
|
||||||
|
libsigcxx30
|
||||||
|
gdk-pixbuf
|
||||||
|
librsvg
|
||||||
|
gvfs
|
||||||
|
gsettings-desktop-schemas
|
||||||
|
hicolor-icon-theme
|
||||||
|
adwaitaTheme
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
42
meson.build
42
meson.build
@@ -1,6 +1,6 @@
|
|||||||
project('minesweeper', 'cpp',
|
project('minesweeper', 'cpp',
|
||||||
version : '0.1',
|
version : '0.2.0',
|
||||||
default_options : ['warning_level=3'])
|
default_options : ['warning_level=3', 'cpp_std=c++20'])
|
||||||
|
|
||||||
gnome = import('gnome')
|
gnome = import('gnome')
|
||||||
|
|
||||||
@@ -10,7 +10,39 @@ res = gnome.compile_resources(
|
|||||||
c_name: 'gresources'
|
c_name: 'gresources'
|
||||||
)
|
)
|
||||||
|
|
||||||
deps = dependency(['gtkmm-4.0', 'sigc++-3.0'])
|
# Dependencies
|
||||||
src = ['src/window.cpp', 'src/window.hpp', 'src/minefield.hpp', 'src/minefield.cpp', res]
|
deps = [
|
||||||
exe = executable('minesweeper', src, dependencies : deps, install : true)
|
dependency('gtkmm-4.0'),
|
||||||
|
dependency('sigc++-3.0')
|
||||||
|
]
|
||||||
|
|
||||||
|
# Source files
|
||||||
|
src = [
|
||||||
|
'src/main.cpp',
|
||||||
|
'src/window.cpp',
|
||||||
|
'src/window.hpp',
|
||||||
|
'src/minefield.hpp',
|
||||||
|
'src/minefield.cpp',
|
||||||
|
'src/board_widget.hpp',
|
||||||
|
'src/board_widget.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')
|
||||||
|
)
|
||||||
|
|||||||
118
resources/minesweeper.svg
Normal file
118
resources/minesweeper.svg
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
version="1.1"
|
||||||
|
id="svg9"
|
||||||
|
sodipodi:docname="minesweeper.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs9" /><sodipodi:namedview
|
||||||
|
id="namedview9"
|
||||||
|
pagecolor="#4c524e"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="10.337901"
|
||||||
|
inkscape:cx="34.097831"
|
||||||
|
inkscape:cy="41.207591"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1387"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="25"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg9" /><rect
|
||||||
|
x="4.573761"
|
||||||
|
y="5.6444345"
|
||||||
|
width="90"
|
||||||
|
height="90"
|
||||||
|
rx="15"
|
||||||
|
fill="#f9db9b"
|
||||||
|
stroke="#6d4c41"
|
||||||
|
stroke-width="4"
|
||||||
|
id="rect1" /><rect
|
||||||
|
x="9.4985218"
|
||||||
|
y="10.021999"
|
||||||
|
width="25"
|
||||||
|
height="25"
|
||||||
|
fill="#f44336"
|
||||||
|
rx="5"
|
||||||
|
id="rect2" /><circle
|
||||||
|
cx="54.573761"
|
||||||
|
cy="55.644436"
|
||||||
|
r="18"
|
||||||
|
fill="#4e342e"
|
||||||
|
id="circle2" /><path
|
||||||
|
d="m 59.573761,40.644434 q 5,-10 10,-5"
|
||||||
|
stroke="#4e342e"
|
||||||
|
stroke-width="3"
|
||||||
|
fill="none"
|
||||||
|
id="path2" /><line
|
||||||
|
x1="69.34153"
|
||||||
|
y1="29.115864"
|
||||||
|
x2="64.542198"
|
||||||
|
y2="27.71357"
|
||||||
|
stroke="#ff9800"
|
||||||
|
stroke-width="2"
|
||||||
|
id="line2" /><line
|
||||||
|
x1="72.705803"
|
||||||
|
y1="25.333492"
|
||||||
|
x2="70.999474"
|
||||||
|
y2="20.633656"
|
||||||
|
stroke="#ff9800"
|
||||||
|
stroke-width="2"
|
||||||
|
id="line3" /><line
|
||||||
|
x1="79.007629"
|
||||||
|
y1="27.03516"
|
||||||
|
x2="82.793045"
|
||||||
|
y2="23.768559"
|
||||||
|
stroke="#ff9800"
|
||||||
|
stroke-width="2"
|
||||||
|
id="line4" /><line
|
||||||
|
x1="79.630043"
|
||||||
|
y1="33.17028"
|
||||||
|
x2="84.25367"
|
||||||
|
y2="35.073471"
|
||||||
|
stroke="#ff9800"
|
||||||
|
stroke-width="2"
|
||||||
|
id="line5" /><line
|
||||||
|
x1="75.123314"
|
||||||
|
y1="35.76083"
|
||||||
|
x2="76.230385"
|
||||||
|
y2="40.63673"
|
||||||
|
stroke="#ff9800"
|
||||||
|
stroke-width="2"
|
||||||
|
id="line6" /><line
|
||||||
|
x1="72.073761"
|
||||||
|
y1="31.314306"
|
||||||
|
x2="74.573761"
|
||||||
|
y2="26.98418"
|
||||||
|
stroke="#ff9800"
|
||||||
|
stroke-width="2"
|
||||||
|
id="line7" /><circle
|
||||||
|
cx="29.573761"
|
||||||
|
cy="75.644432"
|
||||||
|
r="4"
|
||||||
|
fill="#4e342e"
|
||||||
|
id="circle7" /><circle
|
||||||
|
cx="74.573761"
|
||||||
|
cy="75.644432"
|
||||||
|
r="4"
|
||||||
|
fill="#4e342e"
|
||||||
|
id="circle8" /><circle
|
||||||
|
cx="74.573761"
|
||||||
|
cy="30.644434"
|
||||||
|
r="4"
|
||||||
|
fill="#4e342e"
|
||||||
|
id="circle9" /><path
|
||||||
|
d="m 17.36416,15.724382 c 0,-0.571957 -0.462088,-1.034051 -1.034044,-1.034051 -0.571963,0 -1.03405,0.462094 -1.03405,1.034051 v 1.034043 9.823461 3.619167 c 0,0.571963 0.462087,1.03405 1.03405,1.03405 0.571956,0 1.034044,-0.462087 1.034044,-1.03405 v -4.136188 l 2.077796,-0.52026 c 1.328101,-0.332834 2.73376,-0.177725 3.958464,0.433007 1.428274,0.714144 3.085984,0.80139 4.578889,0.239123 l 1.121298,-0.420079 c 0.403926,-0.151878 0.672137,-0.536414 0.672137,-0.969421 v -8.000947 c 0,-0.743225 -0.782001,-1.227935 -1.447674,-0.895095 l -0.31021,0.155103 c -1.496138,0.749688 -3.257253,0.749688 -4.75339,0 -1.134225,-0.568725 -2.436478,-0.710906 -3.667644,-0.403921 l -2.229666,0.559029 z"
|
||||||
|
id="path1"
|
||||||
|
style="fill:#6d4c41;fill-opacity:1;stroke-width:0.032314" /></svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
11
resources/org.gtkmm.minesweeper.desktop
Normal file
11
resources/org.gtkmm.minesweeper.desktop
Normal file
@@ -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
|
||||||
BIN
screenshots/screen1.png
Normal file
BIN
screenshots/screen1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
332
src/board_widget.cpp
Normal file
332
src/board_widget.cpp
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
#include "board_widget.hpp"
|
||||||
|
#include <iostream>
|
||||||
|
#include <cmath>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
|
BoardWidget::BoardWidget() {
|
||||||
|
set_hexpand(true);
|
||||||
|
set_vexpand(true);
|
||||||
|
|
||||||
|
// Setup Draw Function
|
||||||
|
set_draw_func(sigc::mem_fun(*this, &BoardWidget::on_draw));
|
||||||
|
|
||||||
|
// Left Click
|
||||||
|
left_click_controller_ = Gtk::GestureClick::create();
|
||||||
|
left_click_controller_->set_button(GDK_BUTTON_PRIMARY);
|
||||||
|
left_click_controller_->signal_pressed().connect(sigc::mem_fun(*this, &BoardWidget::on_click_pressed));
|
||||||
|
add_controller(left_click_controller_);
|
||||||
|
|
||||||
|
// Right Click
|
||||||
|
right_click_controller_ = Gtk::GestureClick::create();
|
||||||
|
right_click_controller_->set_button(GDK_BUTTON_SECONDARY);
|
||||||
|
right_click_controller_->signal_pressed().connect(sigc::mem_fun(*this, &BoardWidget::on_right_click_pressed));
|
||||||
|
add_controller(right_click_controller_);
|
||||||
|
|
||||||
|
// Motion (Hover)
|
||||||
|
motion_controller_ = Gtk::EventControllerMotion::create();
|
||||||
|
motion_controller_->signal_enter().connect(sigc::mem_fun(*this, &BoardWidget::on_motion));
|
||||||
|
motion_controller_->signal_motion().connect(sigc::mem_fun(*this, &BoardWidget::on_motion));
|
||||||
|
motion_controller_->signal_leave().connect(sigc::mem_fun(*this, &BoardWidget::on_leave));
|
||||||
|
add_controller(motion_controller_);
|
||||||
|
}
|
||||||
|
|
||||||
|
BoardWidget::~BoardWidget() {
|
||||||
|
if (tick_id_ > 0) remove_tick_callback(tick_id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::set_minefield(std::shared_ptr<Minefield> field) {
|
||||||
|
field_ = field;
|
||||||
|
particles_.clear();
|
||||||
|
if (tick_id_ > 0) {
|
||||||
|
remove_tick_callback(tick_id_);
|
||||||
|
tick_id_ = 0;
|
||||||
|
}
|
||||||
|
queue_draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::start_confetti() {
|
||||||
|
particles_.clear();
|
||||||
|
std::random_device rd;
|
||||||
|
std::mt19937 gen(rd());
|
||||||
|
std::uniform_real_distribution<> pos_dist(0.0, 1.0); // Relative to window
|
||||||
|
std::uniform_real_distribution<> vel_dist(-2.0, 2.0);
|
||||||
|
std::uniform_real_distribution<> color_dist(0.0, 1.0);
|
||||||
|
std::uniform_real_distribution<> size_dist(3.0, 8.0);
|
||||||
|
|
||||||
|
int w = get_width();
|
||||||
|
int h = get_height();
|
||||||
|
|
||||||
|
for (int i = 0; i < 200; ++i) {
|
||||||
|
particles_.push_back({
|
||||||
|
pos_dist(gen) * w,
|
||||||
|
pos_dist(gen) * h * 0.5, // Start from top half
|
||||||
|
vel_dist(gen),
|
||||||
|
vel_dist(gen) + 2.0, // Fall down
|
||||||
|
1.0, // Life
|
||||||
|
color_dist(gen), color_dist(gen), color_dist(gen),
|
||||||
|
size_dist(gen)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tick_id_ == 0) {
|
||||||
|
tick_id_ = add_tick_callback(
|
||||||
|
sigc::mem_fun(*this, &BoardWidget::on_animation_tick)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BoardWidget::on_animation_tick(const Glib::RefPtr<Gdk::FrameClock>& clock) {
|
||||||
|
return process_animation(); // Overload
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BoardWidget::process_animation() {
|
||||||
|
if (particles_.empty()) return false;
|
||||||
|
|
||||||
|
bool alive = false;
|
||||||
|
for (auto& p : particles_) {
|
||||||
|
p.x += p.vx;
|
||||||
|
p.y += p.vy;
|
||||||
|
p.vy += 0.1; // Gravity
|
||||||
|
p.life -= 0.01;
|
||||||
|
|
||||||
|
if (p.life > 0) alive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
queue_draw();
|
||||||
|
return alive;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<int, int> BoardWidget::get_cell_at(double x, double y) {
|
||||||
|
if (!field_) return {-1, -1};
|
||||||
|
|
||||||
|
// Use stored metrics from last draw
|
||||||
|
if (x < offset_x_ || x >= offset_x_ + field_->cols() * cell_size_ ||
|
||||||
|
y < offset_y_ || y >= offset_y_ + field_->rows() * cell_size_) {
|
||||||
|
return {-1, -1};
|
||||||
|
}
|
||||||
|
|
||||||
|
int cx = static_cast<int>((x - offset_x_) / cell_size_);
|
||||||
|
int cy = static_cast<int>((y - offset_y_) / cell_size_);
|
||||||
|
return {cx, cy};
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::on_click_pressed(int n_press, double x, double y) {
|
||||||
|
if (!field_) return;
|
||||||
|
auto [cx, cy] = get_cell_at(x, y);
|
||||||
|
if (cx == -1) return;
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
if (field_->get_cell(cx, cy).is_revealed) {
|
||||||
|
changed = field_->chord_cell(cx, cy);
|
||||||
|
} else {
|
||||||
|
changed = field_->open_cell(cx, cy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
signal_state_changed.emit();
|
||||||
|
if (field_->state() == GameState::Won) {
|
||||||
|
start_confetti();
|
||||||
|
}
|
||||||
|
queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::on_right_click_pressed(int n_press, double x, double y) {
|
||||||
|
if (!field_) return;
|
||||||
|
auto [cx, cy] = get_cell_at(x, y);
|
||||||
|
if (cx == -1) return;
|
||||||
|
|
||||||
|
if (field_->toggle_flag(cx, cy)) {
|
||||||
|
signal_state_changed.emit();
|
||||||
|
queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::on_motion(double x, double y) {
|
||||||
|
auto [cx, cy] = get_cell_at(x, y);
|
||||||
|
if (cx != hover_x_ || cy != hover_y_) {
|
||||||
|
hover_x_ = cx;
|
||||||
|
hover_y_ = cy;
|
||||||
|
queue_draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::on_leave() {
|
||||||
|
hover_x_ = -1;
|
||||||
|
hover_y_ = -1;
|
||||||
|
queue_draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
|
||||||
|
// Background
|
||||||
|
cr->set_source_rgb(0.95, 0.95, 0.95);
|
||||||
|
cr->paint();
|
||||||
|
|
||||||
|
if (field_) {
|
||||||
|
// Calculate scaling
|
||||||
|
int cols = field_->cols();
|
||||||
|
int rows = field_->rows();
|
||||||
|
|
||||||
|
double pad = 20.0;
|
||||||
|
double avail_w = width - 2 * pad;
|
||||||
|
double avail_h = height - 2 * pad;
|
||||||
|
|
||||||
|
cell_size_ = std::min(avail_w / cols, avail_h / rows);
|
||||||
|
cell_size_ = std::min(cell_size_, 48.0);
|
||||||
|
cell_size_ = std::max(cell_size_, 16.0);
|
||||||
|
|
||||||
|
double board_w_px = cols * cell_size_;
|
||||||
|
double board_h_px = rows * cell_size_;
|
||||||
|
offset_x_ = (width - board_w_px) / 2.0;
|
||||||
|
offset_y_ = (height - board_h_px) / 2.0;
|
||||||
|
|
||||||
|
cr->save();
|
||||||
|
cr->translate(offset_x_, offset_y_);
|
||||||
|
|
||||||
|
// Shadow for the board
|
||||||
|
cr->set_source_rgba(0.0, 0.0, 0.0, 0.1);
|
||||||
|
cr->rectangle(5, 5, board_w_px, board_h_px);
|
||||||
|
cr->fill();
|
||||||
|
|
||||||
|
for (int y = 0; y < rows; ++y) {
|
||||||
|
for (int x = 0; x < cols; ++x) {
|
||||||
|
draw_cell(cr, x, y, cell_size_, field_->get_cell(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cr->restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!particles_.empty()) {
|
||||||
|
draw_particles(cr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::draw_cell(const Cairo::RefPtr<Cairo::Context>& cr, int x, int y, double size, const Cell& cell) {
|
||||||
|
double px = x * size;
|
||||||
|
double py = y * size;
|
||||||
|
|
||||||
|
// Base shape
|
||||||
|
cr->rectangle(px, py, size, size);
|
||||||
|
|
||||||
|
bool is_hover = (x == hover_x_ && y == hover_y_ && field_->state() == GameState::Playing);
|
||||||
|
bool revealed = cell.is_revealed;
|
||||||
|
|
||||||
|
if (revealed) {
|
||||||
|
if (cell.is_exploded) {
|
||||||
|
cr->set_source_rgb(0.9, 0.3, 0.3); // Red background
|
||||||
|
} else {
|
||||||
|
cr->set_source_rgb(0.92, 0.92, 0.92); // Revealed background (lighter gray)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (is_hover) {
|
||||||
|
cr->set_source_rgb(0.75, 0.85, 1.0); // Hover light blue
|
||||||
|
} else {
|
||||||
|
// Gradient for unrevealed cells
|
||||||
|
auto pat = Cairo::LinearGradient::create(px, py, px + size, py + size);
|
||||||
|
pat->add_color_stop_rgba(0, 0.8, 0.8, 0.8, 1);
|
||||||
|
pat->add_color_stop_rgba(1, 0.7, 0.7, 0.7, 1);
|
||||||
|
cr->set_source(pat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!revealed && !is_hover) cr->fill();
|
||||||
|
else cr->fill_preserve();
|
||||||
|
|
||||||
|
if (revealed || is_hover) {
|
||||||
|
// Border
|
||||||
|
cr->set_source_rgb(0.6, 0.6, 0.6);
|
||||||
|
cr->set_line_width(1.0);
|
||||||
|
cr->stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content
|
||||||
|
if (cell.is_flagged) {
|
||||||
|
draw_flag(cr, px, py, size);
|
||||||
|
} else if (revealed) {
|
||||||
|
if (cell.is_bomb) {
|
||||||
|
draw_bomb(cr, px, py, size, cell.is_exploded);
|
||||||
|
} else if (cell.nearby_bombs > 0) {
|
||||||
|
draw_digit(cr, cell.nearby_bombs, px, py, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::draw_flag(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size) {
|
||||||
|
double cx = x + size / 2.0;
|
||||||
|
double cy = y + size / 2.0;
|
||||||
|
|
||||||
|
// Pole
|
||||||
|
cr->set_source_rgb(0.2, 0.2, 0.2);
|
||||||
|
cr->set_line_width(2.0);
|
||||||
|
cr->move_to(cx - size * 0.1, cy + size * 0.3);
|
||||||
|
cr->line_to(cx - size * 0.1, cy - size * 0.3);
|
||||||
|
cr->stroke();
|
||||||
|
|
||||||
|
// Flag
|
||||||
|
cr->set_source_rgb(0.9, 0.2, 0.2);
|
||||||
|
cr->move_to(cx - size * 0.1, cy - size * 0.3);
|
||||||
|
cr->line_to(cx + size * 0.25, cy - size * 0.15);
|
||||||
|
cr->line_to(cx - size * 0.1, cy);
|
||||||
|
cr->close_path();
|
||||||
|
cr->fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::draw_bomb(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size, bool exploded) {
|
||||||
|
double cx = x + size / 2.0;
|
||||||
|
double cy = y + size / 2.0;
|
||||||
|
double r = size * 0.25;
|
||||||
|
|
||||||
|
// Bomb body
|
||||||
|
auto pat = Cairo::RadialGradient::create(cx - r*0.3, cy - r*0.3, r*0.1, cx, cy, r);
|
||||||
|
pat->add_color_stop_rgba(0, 0.4, 0.4, 0.4, 1);
|
||||||
|
pat->add_color_stop_rgba(1, 0.1, 0.1, 0.1, 1);
|
||||||
|
cr->set_source(pat);
|
||||||
|
cr->arc(cx, cy, r, 0, 2 * M_PI);
|
||||||
|
cr->fill();
|
||||||
|
|
||||||
|
// Spikes/Fuse
|
||||||
|
cr->set_source_rgb(0.1, 0.1, 0.1);
|
||||||
|
cr->set_line_width(2.0);
|
||||||
|
cr->move_to(cx, cy - r);
|
||||||
|
cr->line_to(cx, cy - r - size * 0.1);
|
||||||
|
cr->stroke();
|
||||||
|
|
||||||
|
// Spark if exploded?
|
||||||
|
if (exploded) {
|
||||||
|
cr->set_source_rgb(1.0, 0.5, 0.0);
|
||||||
|
cr->arc(cx, cy - r - size * 0.1, size * 0.05, 0, 2 * M_PI);
|
||||||
|
cr->fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::draw_digit(const Cairo::RefPtr<Cairo::Context>& cr, int number, double x, double y, double size) {
|
||||||
|
switch (number) {
|
||||||
|
case 1: cr->set_source_rgb(0.2, 0.4, 1.0); break; // Blue
|
||||||
|
case 2: cr->set_source_rgb(0.2, 0.6, 0.2); break; // Green
|
||||||
|
case 3: cr->set_source_rgb(0.9, 0.2, 0.2); break; // Red
|
||||||
|
case 4: cr->set_source_rgb(0.1, 0.1, 0.6); break; // Dark Blue
|
||||||
|
case 5: cr->set_source_rgb(0.6, 0.1, 0.1); break; // Maroon
|
||||||
|
case 6: cr->set_source_rgb(0.1, 0.6, 0.6); break; // Cyan
|
||||||
|
case 7: cr->set_source_rgb(0.1, 0.1, 0.1); break; // Black
|
||||||
|
case 8: cr->set_source_rgb(0.5, 0.5, 0.5); break; // Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
cr->select_font_face("Sans", Cairo::ToyFontFace::Slant::NORMAL, Cairo::ToyFontFace::Weight::BOLD);
|
||||||
|
cr->set_font_size(size * 0.7); // Bigger font
|
||||||
|
|
||||||
|
Cairo::TextExtents extents;
|
||||||
|
std::string text = std::to_string(number);
|
||||||
|
cr->get_text_extents(text, extents);
|
||||||
|
|
||||||
|
cr->move_to(x + (size - extents.width) / 2 - extents.x_bearing,
|
||||||
|
y + (size - extents.height) / 2 - extents.y_bearing);
|
||||||
|
cr->show_text(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BoardWidget::draw_particles(const Cairo::RefPtr<Cairo::Context>& cr) {
|
||||||
|
for (const auto& p : particles_) {
|
||||||
|
if (p.life <= 0) continue;
|
||||||
|
cr->set_source_rgba(p.r, p.g, p.b, p.life);
|
||||||
|
cr->rectangle(p.x, p.y, p.size, p.size);
|
||||||
|
cr->fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/board_widget.hpp
Normal file
69
src/board_widget.hpp
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtkmm/drawingarea.h>
|
||||||
|
#include <gtkmm/gestureclick.h>
|
||||||
|
#include <gtkmm/eventcontrollermotion.h>
|
||||||
|
#include <gdkmm/texture.h>
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
#include <random>
|
||||||
|
#include "minefield.hpp"
|
||||||
|
|
||||||
|
struct Particle {
|
||||||
|
double x, y;
|
||||||
|
double vx, vy;
|
||||||
|
double life;
|
||||||
|
double r, g, b;
|
||||||
|
double size;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BoardWidget : public Gtk::DrawingArea {
|
||||||
|
public:
|
||||||
|
BoardWidget();
|
||||||
|
virtual ~BoardWidget();
|
||||||
|
|
||||||
|
void set_minefield(std::shared_ptr<Minefield> field);
|
||||||
|
|
||||||
|
// Signal when state changes (for UI updates)
|
||||||
|
sigc::signal<void()> signal_state_changed;
|
||||||
|
|
||||||
|
// Animation
|
||||||
|
void start_confetti();
|
||||||
|
bool on_animation_tick(const Glib::RefPtr<Gdk::FrameClock>& clock);
|
||||||
|
bool process_animation(); // Internal helper
|
||||||
|
|
||||||
|
private:
|
||||||
|
void on_draw(const Cairo::RefPtr<Cairo::Context>& cr, int width, int height);
|
||||||
|
|
||||||
|
// Input handlers
|
||||||
|
void on_click_pressed(int n_press, double x, double y);
|
||||||
|
void on_right_click_pressed(int n_press, double x, double y);
|
||||||
|
void on_motion(double x, double y);
|
||||||
|
void on_leave();
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
std::pair<int, int> get_cell_at(double x, double y);
|
||||||
|
void draw_cell(const Cairo::RefPtr<Cairo::Context>& cr, int x, int y, double size, const Cell& cell);
|
||||||
|
void draw_digit(const Cairo::RefPtr<Cairo::Context>& cr, int number, double x, double y, double size);
|
||||||
|
void draw_flag(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size);
|
||||||
|
void draw_bomb(const Cairo::RefPtr<Cairo::Context>& cr, double x, double y, double size, bool exploded);
|
||||||
|
void draw_particles(const Cairo::RefPtr<Cairo::Context>& cr);
|
||||||
|
|
||||||
|
std::shared_ptr<Minefield> field_;
|
||||||
|
|
||||||
|
// State
|
||||||
|
double cell_size_ = 32.0;
|
||||||
|
double offset_x_ = 0;
|
||||||
|
double offset_y_ = 0;
|
||||||
|
int hover_x_ = -1;
|
||||||
|
int hover_y_ = -1;
|
||||||
|
|
||||||
|
// Particles
|
||||||
|
std::vector<Particle> particles_;
|
||||||
|
guint tick_id_ = 0;
|
||||||
|
|
||||||
|
// Controllers
|
||||||
|
Glib::RefPtr<Gtk::GestureClick> left_click_controller_;
|
||||||
|
Glib::RefPtr<Gtk::GestureClick> right_click_controller_;
|
||||||
|
Glib::RefPtr<Gtk::EventControllerMotion> motion_controller_;
|
||||||
|
};
|
||||||
32
src/main.cpp
32
src/main.cpp
@@ -1,30 +1,8 @@
|
|||||||
#include "MineField.hpp"
|
#include "window.hpp"
|
||||||
#include <iostream>
|
#include <gtkmm/application.h>
|
||||||
|
|
||||||
int main() {
|
int main(int argc, char* argv[]) {
|
||||||
|
auto app = Gtk::Application::create("org.gtkmm.minesweeper");
|
||||||
|
|
||||||
MineField field(160,160, 100);
|
return app->make_window_and_run<MainWindow>(argc, argv);
|
||||||
|
|
||||||
srand(time(NULL));
|
|
||||||
|
|
||||||
int x = rand() % 160;
|
|
||||||
int y = rand() % 160;
|
|
||||||
|
|
||||||
field.initBombs(x, y);
|
|
||||||
|
|
||||||
printf("Opened cell: %i %i\n", x, y);
|
|
||||||
printf("Neighboor bombs: %i\n", field.bombsNearby(x,y));
|
|
||||||
|
|
||||||
while(!field.isGameOver()) {
|
|
||||||
x = rand() % 160;
|
|
||||||
y = rand() % 160;
|
|
||||||
|
|
||||||
if(field.clearCell(x, y)) {
|
|
||||||
printf("Opened cell: %i %i\n", x, y);
|
|
||||||
printf("Neighboor bombs: %i\n", field.bombsNearby(x,y));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
printf("Bomb found in cell: %i %i\n", x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,141 +1,236 @@
|
|||||||
#include "minefield.hpp"
|
#include "minefield.hpp"
|
||||||
|
#include <random>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
MineField::MineField(int cols, int rows, int mines): m_rows(rows),
|
const GameDifficulty Minefield::DifficultyEasy = {"Beginner", 9, 9, 10};
|
||||||
m_cols(cols),
|
const GameDifficulty Minefield::DifficultyMedium = {"Intermediate", 16, 16, 40};
|
||||||
m_totalMines(mines),
|
const GameDifficulty Minefield::DifficultyHard = {"Expert", 30, 16, 99};
|
||||||
m_remainingFlags(mines),
|
const GameDifficulty Minefield::DifficultyExpert = {"Master", 30, 20, 145};
|
||||||
m_openCells(0),
|
|
||||||
m_exploded(false) {
|
Minefield::Minefield(int cols, int rows, int mines)
|
||||||
for(int i=0; i< m_cols*m_rows; i++) {
|
: cols_(cols), rows_(rows), total_mines_(mines), remaining_flags_(mines) {
|
||||||
std::shared_ptr<Cell> cell = std::make_shared<Cell>();
|
cells_.resize(cols * rows);
|
||||||
m_cells.push_back(cell);
|
}
|
||||||
|
|
||||||
|
const Cell& Minefield::get_cell(int x, int y) const {
|
||||||
|
static const Cell invalid_cell;
|
||||||
|
if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return invalid_cell;
|
||||||
|
return cells_[y * cols_ + x];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::chrono::milliseconds Minefield::get_elapsed_time() const {
|
||||||
|
if (state_ == GameState::Ready) return std::chrono::milliseconds(0);
|
||||||
|
auto end = (state_ == GameState::Playing) ? std::chrono::steady_clock::now() : end_time_;
|
||||||
|
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start_time_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Minefield::generate_mines(int safe_x, int safe_y) {
|
||||||
|
std::vector<int> indices(cells_.size());
|
||||||
|
std::iota(indices.begin(), indices.end(), 0);
|
||||||
|
|
||||||
|
// Remove safe zone (3x3 around click)
|
||||||
|
indices.erase(std::remove_if(indices.begin(), indices.end(), [&](int idx) {
|
||||||
|
int cx = idx % cols_;
|
||||||
|
int cy = idx / cols_;
|
||||||
|
return std::abs(cx - safe_x) <= 1 && std::abs(cy - safe_y) <= 1;
|
||||||
|
}), indices.end());
|
||||||
|
|
||||||
|
std::random_device rd;
|
||||||
|
std::mt19937 g(rd());
|
||||||
|
std::shuffle(indices.begin(), indices.end(), g);
|
||||||
|
|
||||||
|
int placed = 0;
|
||||||
|
for (int idx : indices) {
|
||||||
|
if (placed >= total_mines_) break;
|
||||||
|
cells_[idx].is_bomb = true;
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precompute neighbors
|
||||||
|
for (int y = 0; y < rows_; ++y) {
|
||||||
|
for (int x = 0; x < cols_; ++x) {
|
||||||
|
if (!cells_[y * cols_ + x].is_bomb) {
|
||||||
|
cells_[y * cols_ + x].nearby_bombs = count_nearby_bombs(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MineField::timerTick() {
|
int Minefield::count_nearby_bombs(int x, int y) const {
|
||||||
|
int count = 0;
|
||||||
auto start = std::chrono::system_clock::now();
|
for (int dy = -1; dy <= 1; ++dy) {
|
||||||
|
for (int dx = -1; dx <= 1; ++dx) {
|
||||||
while((m_exploded == false) && (m_gameWon == false)) {
|
if (dx == 0 && dy == 0) continue;
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
int nx = x + dx;
|
||||||
auto now = std::chrono::system_clock::now();
|
int ny = y + dy;
|
||||||
const auto duration = now - start;
|
if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) {
|
||||||
std::chrono::milliseconds ms = std::chrono::duration_cast<std::chrono::milliseconds>(duration);
|
if (cells_[ny * cols_ + nx].is_bomb) count++;
|
||||||
m_time += ms.count();
|
|
||||||
timerSignal.emit(m_time);
|
|
||||||
start = std::chrono::system_clock::now();
|
|
||||||
}
|
}
|
||||||
//I should use std::duration to represent the time instead
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MineField::initBombs(int x, int y) {
|
int Minefield::count_nearby_flags(int x, int y) const {
|
||||||
|
int count = 0;
|
||||||
int remainingMines = m_totalMines;
|
for (int dy = -1; dy <= 1; ++dy) {
|
||||||
int startPos = x + y * m_rows;
|
for (int dx = -1; dx <= 1; ++dx) {
|
||||||
|
if (dx == 0 && dy == 0) continue;
|
||||||
srand(time(NULL)); //initialize rand()
|
int nx = x + dx;
|
||||||
|
int ny = y + dy;
|
||||||
while(remainingMines > 0) {
|
if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) {
|
||||||
int position = rand() % (m_cols * m_rows);
|
if (cells_[ny * cols_ + nx].is_flagged) count++;
|
||||||
if(isBomb(position % m_cols, position / m_cols) || position == startPos) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
m_cells.at(position)->isBomb = true;
|
|
||||||
--remainingMines;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
//init the timer to zero and start the timer thread
|
return count;
|
||||||
m_time = 0;
|
|
||||||
timerThread = std::thread(&MineField::timerTick, this);
|
|
||||||
timerThread.detach(); //not sure if this is okay (better to call join() when I set the condition to stop the thread)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MineField::openCell(int x, int y) {
|
bool Minefield::open_cell(int x, int y) {
|
||||||
if(isBomb(x, y)) {
|
if (state_ == GameState::Won || state_ == GameState::Lost) return false;
|
||||||
m_exploded = true;
|
if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return false;
|
||||||
gameOverSignal.emit();
|
|
||||||
|
Cell& cell = cells_[y * cols_ + x];
|
||||||
|
if (cell.is_flagged || cell.is_revealed) return false;
|
||||||
|
|
||||||
|
if (state_ == GameState::Ready) {
|
||||||
|
start_time_ = std::chrono::steady_clock::now();
|
||||||
|
state_ = GameState::Playing;
|
||||||
|
generate_mines(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cell.is_bomb) {
|
||||||
|
cell.is_exploded = true;
|
||||||
|
cell.is_revealed = true;
|
||||||
|
state_ = GameState::Lost;
|
||||||
|
end_time_ = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
// Reveal all other bombs
|
||||||
|
for (auto& c : cells_) {
|
||||||
|
if (c.is_bomb && !c.is_flagged) c.is_revealed = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
reveal_recursive(x, y);
|
||||||
|
check_win_condition();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Minefield::reveal_recursive(int x, int y) {
|
||||||
|
// Non-recursive BFS to avoid stack overflow
|
||||||
|
std::vector<std::pair<int, int>> q;
|
||||||
|
q.reserve(cols_ * rows_);
|
||||||
|
q.push_back({x, y});
|
||||||
|
|
||||||
|
// Mark initial as revealed
|
||||||
|
if (!cells_[y * cols_ + x].is_revealed) {
|
||||||
|
cells_[y * cols_ + x].is_revealed = true;
|
||||||
|
revealed_count_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t head = 0;
|
||||||
|
while(head < q.size()){
|
||||||
|
auto [cx, cy] = q[head++];
|
||||||
|
Cell& current = cells_[cy * cols_ + cx];
|
||||||
|
|
||||||
|
if (current.nearby_bombs == 0) {
|
||||||
|
for (int dy = -1; dy <= 1; ++dy) {
|
||||||
|
for (int dx = -1; dx <= 1; ++dx) {
|
||||||
|
if (dx == 0 && dy == 0) continue;
|
||||||
|
int nx = cx + dx;
|
||||||
|
int ny = cy + dy;
|
||||||
|
if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) {
|
||||||
|
Cell& neighbor = cells_[ny * cols_ + nx];
|
||||||
|
if (!neighbor.is_revealed && !neighbor.is_flagged) {
|
||||||
|
neighbor.is_revealed = true;
|
||||||
|
revealed_count_++;
|
||||||
|
q.push_back({nx, ny});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Minefield::toggle_flag(int x, int y) {
|
||||||
|
if (state_ == GameState::Won || state_ == GameState::Lost) return false;
|
||||||
|
if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return false;
|
||||||
|
|
||||||
|
Cell& cell = cells_[y * cols_ + x];
|
||||||
|
if (cell.is_revealed) return false;
|
||||||
|
|
||||||
|
if (cell.is_flagged) {
|
||||||
|
cell.is_flagged = false;
|
||||||
|
remaining_flags_++;
|
||||||
|
} else {
|
||||||
|
if (remaining_flags_ > 0) {
|
||||||
|
cell.is_flagged = true;
|
||||||
|
remaining_flags_--;
|
||||||
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setOpenCell(x, y);
|
|
||||||
|
|
||||||
if (bombsNearby(x, y) == 0) {
|
|
||||||
openNeighboorhood(x, y);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MineField::computeBombsNearby(int x, int y) {
|
bool Minefield::chord_cell(int x, int y) {
|
||||||
int total = 0;
|
if (state_ != GameState::Playing) return false;
|
||||||
//compute bombs in neighboorhood
|
if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return false;
|
||||||
for(int i=-1; i<2; i++) {
|
|
||||||
for(int j=-1; j<2; j++) {
|
|
||||||
if(x+i >= 0 && x+i < m_cols && y+j >= 0 && y+j < m_rows) {
|
|
||||||
if(isBomb(x+i, y+j)){
|
|
||||||
++total;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_cells.at(x + y * m_rows)->bombsNearby = total;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MineField::openNeighboorhood(int x, int y) {
|
Cell& cell = cells_[y * cols_ + x];
|
||||||
//compute bombs in neighboorhood
|
if (!cell.is_revealed) return false;
|
||||||
for(int i=-1; i<2; i++) {
|
if (cell.nearby_bombs == 0) return false;
|
||||||
for(int j=-1; j<2; j++) {
|
|
||||||
if(x+i >= 0 && x+i < m_cols && y+j >= 0 && y+j < m_rows) {
|
|
||||||
if((isOpened(x+i, y+j) == false) && (isBomb(x+i, y+j) == false)){
|
|
||||||
setOpenCell((x+i), (y+j));
|
|
||||||
if(bombsNearby(x+i, y+j) == 0) {
|
|
||||||
openNeighboorhood(x+i, y+j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MineField::isOpened(int x, int y) {
|
if (count_nearby_flags(x, y) == cell.nearby_bombs) {
|
||||||
return m_cells.at(x + y * m_rows)->isCleared;
|
bool changed = false;
|
||||||
}
|
for (int dy = -1; dy <= 1; ++dy) {
|
||||||
|
for (int dx = -1; dx <= 1; ++dx) {
|
||||||
bool MineField::isFlagged(int x, int y) {
|
if (dx == 0 && dy == 0) continue;
|
||||||
return m_cells.at(x + y * m_rows)->isFlagged;
|
int nx = x + dx;
|
||||||
}
|
int ny = y + dy;
|
||||||
|
if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) {
|
||||||
bool MineField::isBomb(int x, int y) {
|
Cell& neighbor = cells_[ny * cols_ + nx];
|
||||||
return m_cells.at(x + y * m_rows)->isBomb;
|
if (!neighbor.is_revealed && !neighbor.is_flagged) {
|
||||||
}
|
// Standard open logic
|
||||||
|
if (neighbor.is_bomb) {
|
||||||
int MineField::bombsNearby(int x, int y) {
|
neighbor.is_exploded = true;
|
||||||
if(m_cells.at(x + y * m_rows)->bombsNearby == -1) {
|
neighbor.is_revealed = true;
|
||||||
computeBombsNearby(x, y);
|
state_ = GameState::Lost;
|
||||||
|
end_time_ = std::chrono::steady_clock::now();
|
||||||
|
// Reveal all bombs
|
||||||
|
for (auto& c : cells_) {
|
||||||
|
if (c.is_bomb && !c.is_flagged) c.is_revealed = true;
|
||||||
}
|
}
|
||||||
return m_cells.at(x + y * m_rows)->bombsNearby;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MineField::setOpenCell(int x, int y) {
|
|
||||||
m_cells.at(x + y * m_rows)->isCleared = true;
|
|
||||||
openCellSignal.emit(x, y);
|
|
||||||
if((++m_openCells == (m_cols * m_rows - m_totalMines)) && (m_exploded == false)) {
|
|
||||||
m_gameWon = true;
|
|
||||||
gameWonSignal.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MineField::toggleFlag(int x, int y) {
|
|
||||||
if(m_cells.at(x + y * m_rows)->isFlagged == true) {
|
|
||||||
m_cells.at(x + y * m_rows)->isFlagged = false;
|
|
||||||
++m_remainingFlags;
|
|
||||||
remainingFlagsSignal.emit(m_remainingFlags);
|
|
||||||
return true;
|
return true;
|
||||||
|
} else {
|
||||||
|
reveal_recursive(nx, ny);
|
||||||
|
changed = true;
|
||||||
}
|
}
|
||||||
else if(m_remainingFlags > 0) {
|
}
|
||||||
m_cells.at(x + y * m_rows)->isFlagged = true;
|
}
|
||||||
--m_remainingFlags;
|
}
|
||||||
remainingFlagsSignal.emit(m_remainingFlags);
|
}
|
||||||
return true;
|
check_win_condition();
|
||||||
|
return changed;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Minefield::check_win_condition() {
|
||||||
|
if (state_ != GameState::Playing) return;
|
||||||
|
|
||||||
|
// Win if all non-bomb cells are revealed
|
||||||
|
int safe_cells = (cols_ * rows_) - total_mines_;
|
||||||
|
if (revealed_count_ == safe_cells) {
|
||||||
|
state_ = GameState::Won;
|
||||||
|
end_time_ = std::chrono::steady_clock::now();
|
||||||
|
remaining_flags_ = 0; // Visually satisfy flags
|
||||||
|
// Flag all unflagged bombs
|
||||||
|
for (auto& c : cells_) {
|
||||||
|
if (c.is_bomb) c.is_flagged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,57 +1,76 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <sigc++/signal.h>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstdlib>
|
|
||||||
#include <ctime>
|
|
||||||
#include <memory>
|
|
||||||
#include <thread>
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <random>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
struct GameDifficulty {
|
||||||
|
std::string name;
|
||||||
|
int cols;
|
||||||
|
int rows;
|
||||||
|
int mines;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class GameState {
|
||||||
|
Ready,
|
||||||
|
Playing,
|
||||||
|
Won,
|
||||||
|
Lost
|
||||||
|
};
|
||||||
|
|
||||||
struct Cell {
|
struct Cell {
|
||||||
bool isFlagged;
|
bool is_bomb : 1 = false;
|
||||||
bool isCleared;
|
bool is_flagged : 1 = false;
|
||||||
bool isBomb;
|
bool is_revealed : 1 = false;
|
||||||
int bombsNearby;
|
bool is_exploded : 1 = false; // The specific bomb that killed you
|
||||||
Cell(): isFlagged(false), isCleared(false), isBomb(false), bombsNearby(-1) {};
|
uint8_t nearby_bombs : 4 = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MineField {
|
class Minefield {
|
||||||
std::vector<std::shared_ptr<Cell>> m_cells;
|
|
||||||
int m_rows;
|
|
||||||
int m_cols;
|
|
||||||
int m_totalMines;
|
|
||||||
int m_remainingFlags;
|
|
||||||
int m_openCells;
|
|
||||||
bool m_exploded;
|
|
||||||
bool m_gameWon;
|
|
||||||
size_t m_time;
|
|
||||||
std::thread timerThread;
|
|
||||||
void computeBombsNearby(int x, int y);
|
|
||||||
void openNeighboorhood(int x, int y);
|
|
||||||
void setOpenCell(int x, int y);
|
|
||||||
void timerTick();
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
MineField(int cols, int rows, int mines);
|
Minefield(int cols, int rows, int mines);
|
||||||
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_exploded; };
|
|
||||||
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<void(int, int)> openCellSignal;
|
// Core actions
|
||||||
sigc::signal<void(int)> remainingFlagsSignal;
|
// Returns true if state changed
|
||||||
sigc::signal<void(void)> gameWonSignal;
|
bool open_cell(int x, int y);
|
||||||
sigc::signal<void(void)> gameOverSignal;
|
bool toggle_flag(int x, int y);
|
||||||
sigc::signal<void(unsigned int)> timerSignal;
|
bool chord_cell(int x, int y);
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
int cols() const { return cols_; }
|
||||||
|
int rows() const { return rows_; }
|
||||||
|
int total_mines() const { return total_mines_; }
|
||||||
|
int remaining_flags() const { return remaining_flags_; }
|
||||||
|
GameState state() const { return state_; }
|
||||||
|
const Cell& get_cell(int x, int y) const;
|
||||||
|
|
||||||
|
// Timing
|
||||||
|
std::chrono::milliseconds get_elapsed_time() const;
|
||||||
|
|
||||||
|
// Difficulty presets
|
||||||
|
static const GameDifficulty DifficultyEasy;
|
||||||
|
static const GameDifficulty DifficultyMedium;
|
||||||
|
static const GameDifficulty DifficultyHard;
|
||||||
|
static const GameDifficulty DifficultyExpert;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void generate_mines(int safe_x, int safe_y);
|
||||||
|
void reveal_recursive(int x, int y);
|
||||||
|
void check_win_condition();
|
||||||
|
int count_nearby_flags(int x, int y) const;
|
||||||
|
int count_nearby_bombs(int x, int y) const;
|
||||||
|
|
||||||
|
int cols_;
|
||||||
|
int rows_;
|
||||||
|
int total_mines_;
|
||||||
|
int remaining_flags_;
|
||||||
|
int revealed_count_ = 0;
|
||||||
|
|
||||||
|
GameState state_ = GameState::Ready;
|
||||||
|
std::vector<Cell> cells_;
|
||||||
|
|
||||||
|
std::chrono::steady_clock::time_point start_time_;
|
||||||
|
std::chrono::steady_clock::time_point end_time_;
|
||||||
};
|
};
|
||||||
|
|||||||
389
src/window.cpp
389
src/window.cpp
@@ -1,302 +1,137 @@
|
|||||||
#include "window.hpp"
|
#include "window.hpp"
|
||||||
#include "gdkmm/texture.h"
|
#include <iostream>
|
||||||
#include "sigc++/functors/mem_fun.h"
|
#include <iomanip>
|
||||||
|
|
||||||
|
MainWindow::MainWindow() {
|
||||||
|
set_title("Minesweeper");
|
||||||
|
set_default_size(800, 600);
|
||||||
|
|
||||||
//}
|
setup_header_bar();
|
||||||
// void MainWindow::ApplyStyles() {
|
setup_board();
|
||||||
// // 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) {
|
// Start initial game
|
||||||
(void)n_press, (void)n_x, (void)n_y;
|
start_new_game(Minefield::DifficultyEasy);
|
||||||
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<Gtk::Image>();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::updateFlagsLabel(int flags) {
|
MainWindow::~MainWindow() {
|
||||||
Glib::ustring msg = Glib::ustring::compose("Remaining flags: %1", flags);
|
timer_conn_.disconnect();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::openBombs() {
|
void MainWindow::setup_header_bar() {
|
||||||
for(int i=0; i < field.getCols() * field.getRows(); i++) {
|
set_titlebar(header_bar_);
|
||||||
int x = i % field.getCols();
|
|
||||||
int y = i / field.getCols();
|
|
||||||
|
|
||||||
buttons.at(i)->set_sensitive(false);
|
// New Game Button
|
||||||
|
btn_new_game_.set_label("New Game");
|
||||||
|
btn_new_game_.signal_clicked().connect([this]() {
|
||||||
|
start_new_game(current_difficulty_);
|
||||||
|
});
|
||||||
|
header_bar_.pack_start(btn_new_game_);
|
||||||
|
|
||||||
if(field.isBomb(x, y)) {
|
// Difficulty Menu
|
||||||
if(field.isFlagged(x, y)) {
|
btn_difficulty_.set_label("Difficulty");
|
||||||
auto imgFlagBomb = std::make_shared<Gtk::Image>();
|
|
||||||
imgFlagBomb->set(m_textureFlagBomb);
|
box_difficulty_.set_orientation(Gtk::Orientation::VERTICAL);
|
||||||
buttons.at(i)->set_child(*imgFlagBomb);
|
box_difficulty_.set_margin(10);
|
||||||
}
|
box_difficulty_.set_spacing(5);
|
||||||
else {
|
|
||||||
auto imgBomb = std::make_shared<Gtk::Image>();
|
auto add_diff_btn = [&](const GameDifficulty& diff) {
|
||||||
imgBomb->set(m_textureBomb);
|
auto* btn = Gtk::make_managed<Gtk::Button>(diff.name);
|
||||||
buttons.at(i)->set_child(*imgBomb);
|
btn->signal_clicked().connect([this, diff]() {
|
||||||
}
|
start_new_game(diff);
|
||||||
buttons.at(i)->set_active(true);
|
btn_difficulty_.popdown();
|
||||||
}
|
});
|
||||||
}
|
box_difficulty_.append(*btn);
|
||||||
|
};
|
||||||
|
|
||||||
|
add_diff_btn(Minefield::DifficultyEasy);
|
||||||
|
add_diff_btn(Minefield::DifficultyMedium);
|
||||||
|
add_diff_btn(Minefield::DifficultyHard);
|
||||||
|
add_diff_btn(Minefield::DifficultyExpert);
|
||||||
|
|
||||||
|
btn_difficulty_.set_popover(menu_difficulty_);
|
||||||
|
menu_difficulty_.set_child(box_difficulty_);
|
||||||
|
header_bar_.pack_start(btn_difficulty_);
|
||||||
|
|
||||||
|
// Status Labels
|
||||||
|
lbl_flags_.set_margin_end(10);
|
||||||
|
lbl_time_.set_margin_end(10);
|
||||||
|
|
||||||
|
// Make them monospaced and bold
|
||||||
|
lbl_flags_.set_markup("<b>🚩 0</b>");
|
||||||
|
lbl_time_.set_markup("<b>⏱️ 00:00</b>");
|
||||||
|
|
||||||
|
header_bar_.pack_end(lbl_time_);
|
||||||
|
header_bar_.pack_end(lbl_flags_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::updateCell(int x, int y) {
|
void MainWindow::setup_board() {
|
||||||
int pos = x + y * field.getRows();
|
set_child(board_widget_);
|
||||||
if(field.isOpened(x, y)) {
|
board_widget_.signal_state_changed.connect(sigc::mem_fun(*this, &MainWindow::on_game_state_changed));
|
||||||
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)));
|
|
||||||
}
|
|
||||||
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<Gtk::Image>();
|
|
||||||
// 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; i<COLS * COLS; i++) {
|
|
||||||
// if (!buttons[i]->get_active())
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
void MainWindow::gameOver() {
|
|
||||||
clockSignalConn.disconnect();
|
|
||||||
//std::cout << "Signal gameOver emmited\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MainWindow::updateClockLabel()
|
void MainWindow::start_new_game(const GameDifficulty& difficulty) {
|
||||||
{
|
current_difficulty_ = difficulty;
|
||||||
++m_elapsedTime;
|
|
||||||
|
|
||||||
int deciseconds = m_elapsedTime % 10;
|
minefield_ = std::make_shared<Minefield>(difficulty.cols, difficulty.rows, difficulty.mines);
|
||||||
int seconds = (m_elapsedTime / 10) % 60;
|
board_widget_.set_minefield(minefield_);
|
||||||
int minutes = (m_elapsedTime /600) % 60;
|
|
||||||
|
|
||||||
Glib::ustring msg = Glib::ustring::compose("Elapsed time: %1:%2.%3", \
|
// Reset timer
|
||||||
Glib::ustring::format(std::setfill(L'0'), std::setw(2), minutes), \
|
timer_conn_.disconnect();
|
||||||
Glib::ustring::format(std::setfill(L'0'), std::setw(2), seconds), \
|
timer_conn_ = Glib::signal_timeout().connect(sigc::mem_fun(*this, &MainWindow::on_timer_tick), 100);
|
||||||
Glib::ustring::format(std::setfill(L'0'), std::setw(1), deciseconds));
|
|
||||||
clockLabel.set_label(msg);
|
on_game_state_changed();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::MainWindow()
|
void MainWindow::on_game_state_changed() {
|
||||||
{
|
if (!minefield_) return;
|
||||||
// ApplyStyles(); // Load the CSS file
|
|
||||||
m_elapsedTime = 0;
|
|
||||||
newGame = true;
|
|
||||||
set_title("MineSweeper");
|
|
||||||
set_default_size(400, 400);
|
|
||||||
set_resizable(false);
|
|
||||||
|
|
||||||
boxV = Gtk::Box(Gtk::Orientation::VERTICAL);
|
// Update Flags
|
||||||
boxH = Gtk::Box(Gtk::Orientation::HORIZONTAL);
|
lbl_flags_.set_markup("<b>🚩 " + std::to_string(minefield_->remaining_flags()) + "</b>");
|
||||||
|
|
||||||
boxH.set_hexpand(true);
|
// Check Game Over
|
||||||
|
GameState state = minefield_->state();
|
||||||
boxV.append(boxH);
|
if (state == GameState::Won || state == GameState::Lost) {
|
||||||
boxH.set_expand(true);
|
timer_conn_.disconnect();
|
||||||
|
show_game_over_dialog(state == GameState::Won);
|
||||||
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);
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
boxH.append(labelMines);
|
|
||||||
boxH.append(clockLabel);
|
|
||||||
boxH.append(flagLabel);
|
|
||||||
|
|
||||||
|
|
||||||
//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");
|
|
||||||
|
|
||||||
// bombPix.set_from_resource("/minesweeper/bomb-solid");
|
|
||||||
|
|
||||||
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; }");
|
|
||||||
|
|
||||||
auto display = Gdk::Display::get_default();
|
|
||||||
Gtk::StyleContext::add_provider_for_display(display, css_provider, GTK_STYLE_PROVIDER_PRIORITY_USER);
|
|
||||||
|
|
||||||
for (int i = 0; i < field.getCols() * field.getRows(); i++) {
|
|
||||||
auto button = std::make_shared<Gtk::ToggleButton>();
|
|
||||||
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));
|
|
||||||
|
|
||||||
//button->get_style_context()->add_class("fixed-button");
|
|
||||||
|
|
||||||
auto gesture = Gtk::GestureClick::create();
|
|
||||||
gesture->set_button(3);
|
|
||||||
gesture->signal_released().connect(sigc::bind(sigc::mem_fun(*this, \
|
|
||||||
&MainWindow::OnCellRightClick), i));
|
|
||||||
button->add_controller(gesture);
|
|
||||||
|
|
||||||
buttons.push_back(button);
|
|
||||||
|
|
||||||
grid.attach(*button, x, y);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
field.openCellSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::updateCell)));
|
// Update timer immediately to reflect end time if stopped
|
||||||
field.remainingFlagsSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::updateFlagsLabel)));
|
on_timer_tick();
|
||||||
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));
|
|
||||||
|
|
||||||
//optionButton.set_icon_name("open-menu");
|
|
||||||
|
|
||||||
//if (clockSignalConn.connected()) clockSignalConn.disconnect();
|
|
||||||
//elapsedTime = 0;
|
|
||||||
clockSignalConn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &MainWindow::updateClockLabel), 100);
|
|
||||||
//}
|
|
||||||
//create the minefield
|
|
||||||
//field = new MineField(COLS, MINES);
|
|
||||||
|
|
||||||
//bar.pack_start(newGameButton);
|
|
||||||
//bar.pack_end(optionButton);
|
|
||||||
|
|
||||||
//grid.set_row_homogeneous(false);
|
|
||||||
//grid.set_column_homogeneous(false);
|
|
||||||
grid.set_margin(10);
|
|
||||||
//grid.set_vexpand(true);
|
|
||||||
//grid.set_hexpand(true);
|
|
||||||
// grid.set_fill(false);
|
|
||||||
|
|
||||||
boxV.append(grid);
|
|
||||||
|
|
||||||
this->set_titlebar(bar);
|
|
||||||
this->set_child(boxV);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
bool MainWindow::on_timer_tick() {
|
||||||
auto app = Gtk::Application::create("eu.minesweeper");
|
if (!minefield_) return true;
|
||||||
return app->make_window_and_run<MainWindow>(argc, argv);
|
|
||||||
|
auto elapsed = minefield_->get_elapsed_time();
|
||||||
|
auto secs = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();
|
||||||
|
|
||||||
|
int mm = secs / 60;
|
||||||
|
int ss = secs % 60;
|
||||||
|
|
||||||
|
char buf[32];
|
||||||
|
snprintf(buf, sizeof(buf), "<b>⏱️ %02d:%02d</b>", mm, ss);
|
||||||
|
lbl_time_.set_markup(buf);
|
||||||
|
|
||||||
|
return true; // Keep calling
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::show_game_over_dialog(bool won) {
|
||||||
|
auto dialog = Gtk::make_managed<Gtk::MessageDialog>(
|
||||||
|
*this,
|
||||||
|
won ? "Congratulations!" : "Game Over",
|
||||||
|
false,
|
||||||
|
Gtk::MessageType::INFO,
|
||||||
|
Gtk::ButtonsType::OK,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
dialog->set_secondary_text(won ? "You cleared the minefield!" : "Better luck next time.");
|
||||||
|
dialog->signal_response().connect([dialog, this](int response) {
|
||||||
|
dialog->hide();
|
||||||
|
if (response == Gtk::ResponseType::OK) {
|
||||||
|
// Optional: restart?
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog->show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,40 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "minefield.hpp"
|
|
||||||
#include <memory>
|
|
||||||
#include <gtkmm.h>
|
#include <gtkmm.h>
|
||||||
#include <glibmm.h>
|
#include <memory>
|
||||||
#include <gdkmm.h>
|
#include "minefield.hpp"
|
||||||
#include <sigc++/sigc++.h>
|
#include "board_widget.hpp"
|
||||||
#include <iomanip>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#define PROJECT_NAME "minesweeper"
|
class MainWindow : public Gtk::ApplicationWindow {
|
||||||
|
public:
|
||||||
|
MainWindow();
|
||||||
|
~MainWindow() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// UI Setup
|
||||||
|
void setup_header_bar();
|
||||||
|
void setup_board();
|
||||||
|
|
||||||
class MainWindow : public Gtk::Window
|
// Actions
|
||||||
{
|
void start_new_game(const GameDifficulty& difficulty);
|
||||||
Gtk::Box boxV{Gtk::Orientation::VERTICAL};
|
void on_game_state_changed();
|
||||||
Gtk::Box boxH{Gtk::Orientation::HORIZONTAL};
|
bool on_timer_tick();
|
||||||
std::vector<std::shared_ptr<Gtk::ToggleButton>> buttons;
|
void show_game_over_dialog(bool won);
|
||||||
Gtk::Grid grid;
|
|
||||||
Gtk::HeaderBar bar;
|
|
||||||
Gtk::Button newGameButton;
|
|
||||||
Gtk::Button optionButton;
|
|
||||||
Gtk::Label flagLabel;
|
|
||||||
Gtk::Label clockLabel;
|
|
||||||
MineField field {16, 16, 40};
|
|
||||||
int m_elapsedTime;
|
|
||||||
bool newGame;
|
|
||||||
std::shared_ptr<Gdk::Texture> m_textureBomb;
|
|
||||||
std::shared_ptr<Gdk::Texture> m_textureFlag;
|
|
||||||
std::shared_ptr<Gdk::Texture> m_textureFlagBomb;
|
|
||||||
void updateCell(int x, int y);
|
|
||||||
void openBombs();
|
|
||||||
void updateFlagsLabel(int flags);
|
|
||||||
bool updateClockLabel();
|
|
||||||
void gameWon();
|
|
||||||
void gameOver();
|
|
||||||
sigc::connection clockSignalConn;
|
|
||||||
// void OpenNearCells(int index);
|
|
||||||
// void Explode();xo
|
|
||||||
// bool AllCellsOpened();
|
|
||||||
|
|
||||||
public:
|
// Widgets
|
||||||
MainWindow();
|
Gtk::HeaderBar header_bar_;
|
||||||
// void OnNewButtonClick();
|
Gtk::Button btn_new_game_;
|
||||||
void OnCellClick(int x, int y);
|
Gtk::MenuButton btn_difficulty_;
|
||||||
void OnCellRightClick(int n_press, double n_x, double n_y, int index);
|
Gtk::Popover menu_difficulty_;
|
||||||
// void ShowGameWonAnimation();
|
Gtk::Box box_difficulty_; // Content for popover
|
||||||
// void ApplyStyles();
|
|
||||||
// bool UpdateClockLabel();
|
Gtk::Label lbl_time_;
|
||||||
|
Gtk::Label lbl_flags_;
|
||||||
|
|
||||||
|
BoardWidget board_widget_;
|
||||||
|
|
||||||
|
// Game State
|
||||||
|
std::shared_ptr<Minefield> minefield_;
|
||||||
|
sigc::connection timer_conn_;
|
||||||
|
GameDifficulty current_difficulty_ = Minefield::DifficultyEasy;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user