diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..536ae03 --- /dev/null +++ b/meson.build @@ -0,0 +1,16 @@ +project('minesweeper', 'cpp', + version : '0.1', + default_options : ['warning_level=3']) + +gnome = import('gnome') + +res = gnome.compile_resources( + 'resources', 'resources/gresource.xml', + source_dir: 'resources', + c_name: 'gresources' +) + +deps = dependency(['gtkmm-4.0', 'sigc++-3.0']) +src = ['Window.cpp', 'window.hpp', 'MineField.hpp', 'MineField.cpp', res] +exe = executable('minesweeper', src, dependencies : deps, install : true) + diff --git a/resources/bomb-solid.svg b/resources/bomb-solid.svg new file mode 100644 index 0000000..7f7d9db --- /dev/null +++ b/resources/bomb-solid.svg @@ -0,0 +1,38 @@ + + + + + + + diff --git a/resources/confetti.png b/resources/confetti.png new file mode 100644 index 0000000..4fae300 Binary files /dev/null and b/resources/confetti.png differ diff --git a/resources/explosion-flag.svg b/resources/explosion-flag.svg new file mode 100644 index 0000000..9403eaa --- /dev/null +++ b/resources/explosion-flag.svg @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/resources/explosion-solid.svg b/resources/explosion-solid.svg new file mode 100644 index 0000000..a535b76 --- /dev/null +++ b/resources/explosion-solid.svg @@ -0,0 +1,38 @@ + + + + + + + diff --git a/resources/flag-bomb.svg b/resources/flag-bomb.svg new file mode 100644 index 0000000..6c3505a --- /dev/null +++ b/resources/flag-bomb.svg @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/resources/flag-regular.svg b/resources/flag-regular.svg new file mode 100644 index 0000000..b42054c --- /dev/null +++ b/resources/flag-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/flag-solid.svg b/resources/flag-solid.svg new file mode 100644 index 0000000..a59b3b4 --- /dev/null +++ b/resources/flag-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/gresource.xml b/resources/gresource.xml new file mode 100644 index 0000000..c221533 --- /dev/null +++ b/resources/gresource.xml @@ -0,0 +1,11 @@ + + + + bomb-solid.svg + explosion-solid.svg + flag-regular.svg + flag-solid.svg + explosion-flag.svg + flag-bomb.svg + + diff --git a/src/MineField.cpp b/src/MineField.cpp new file mode 100644 index 0000000..9d062ec --- /dev/null +++ b/src/MineField.cpp @@ -0,0 +1,115 @@ +#include "MineField.hpp" + +MineField::MineField(int cols, int rows, int mines): m_rows(rows), + m_cols(cols), + m_totalMines(mines), + m_remainingFlags(mines), + m_exploded(false) { + for(int i=0; i< m_cols*m_rows; i++) { + std::shared_ptr cell = std::make_shared(); + m_cells.push_back(cell); + } +} + + +void MineField::initBombs(int x, int y) { + + 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_rows) || position == startPos) { + continue; + } + m_cells.at(position)->isBomb = true; + --remainingMines; + } +} + +bool MineField::clearCell(int x, int y) { + setClearCell(x, y); + + if(isBomb(x, y)) { + m_exploded = true; + return false; + } + + if (bombsNearby(x, y) == 0) { + openNeighboorhood(x, y); + } + return true; +} + +void MineField::computeBombsNearby(int x, int y) { + int total = 0; + //compute bombs in neighboorhood + for(int i=-1; i<2; i++) { + for(int j=-1; j<2; j++) { + if(x+i >= 0 && x+i < m_cols && y+j >= 0 && y+j < m_rows) { + if(isBomb(x+i, y+j)){ + ++total; + } + } + } + } + m_cells.at(x + y * m_rows)->bombsNearby = total; +} + +void MineField::openNeighboorhood(int x, int y) { + //compute bombs in neighboorhood + for(int i=-1; i<2; i++) { + for(int j=-1; j<2; j++) { + if(x+i >= 0 && x+i < m_cols && y+j >= 0 && y+j < m_rows) { + if((isCleared(x+i, y+j) == false) && (isBomb(x+i, y+j) == false)){ + setClearCell((x+i), (y+j)); + if(bombsNearby(x+i, y+j) == 0) { + openNeighboorhood(x+i, y+j); + } + } + } + } + } +} + +bool MineField::isCleared(int x, int y) { + return m_cells.at(x + y * m_rows)->isCleared; +} + +bool MineField::isFlagged(int x, int y) { + return m_cells.at(x + y * m_rows)->isFlagged; +} + +bool MineField::isBomb(int x, int y) { + return m_cells.at(x + y * m_rows)->isBomb; +} + +int MineField::bombsNearby(int x, int y) { + if(m_cells.at(x + y * m_rows)->bombsNearby == -1) { + computeBombsNearby(x, y); + } + return m_cells.at(x + y * m_rows)->bombsNearby; +} + +void MineField::setClearCell(int x, int y) { + m_cells.at(x + y * m_rows)->isCleared = true; + clearCellSignal.emit(x, y); +} + +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; + remainingFlagsChangedSignal.emit(m_remainingFlags); + return true; + } + else if(m_remainingFlags > 0) { + m_cells.at(x + y * m_rows)->isFlagged = true; + --m_remainingFlags; + remainingFlagsChangedSignal.emit(m_remainingFlags); + return true; + } + return false; +} diff --git a/src/MineField.hpp b/src/MineField.hpp new file mode 100644 index 0000000..4f9f276 --- /dev/null +++ b/src/MineField.hpp @@ -0,0 +1,46 @@ +#pragma once + +//#include +#include +#include +#include +#include +#include + +struct Cell { + bool isFlagged; + bool isCleared; + bool isBomb; + int bombsNearby; + Cell(): isFlagged(false), isCleared(false), isBomb(false), bombsNearby(-1) {}; +}; + +class MineField { + + std::vector> m_cells; + int m_rows; + int m_cols; + int m_totalMines; + int m_remainingFlags; + bool m_exploded; + void computeBombsNearby(int x, int y); + void openNeighboorhood(int x, int y); + void setClearCell(int x, int y); + +public: + 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 isCleared(int x, int y); + bool clearCell(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; }; + sigc::signal clearCellSignal; + sigc::signal remainingFlagsChangedSignal; +}; diff --git a/src/Window.cpp b/src/Window.cpp new file mode 100644 index 0000000..e243895 --- /dev/null +++ b/src/Window.cpp @@ -0,0 +1,339 @@ +#include "window.hpp" +#include "gdkmm/pixbuf.h" +#include "gtkmm/cssprovider.h" +#include "gtkmm/image.h" +#include "sigc++/adaptors/bind.h" +#include "sigc++/functors/mem_fun.h" +#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) { + (void)n_press, (void)n_x, (void)n_y; + int x = index % field.getCols(); + int y = index / field.getRows(); + + if(field.isCleared(x, y) == false) { + field.toggleFlag(x, y); + if(field.isFlagged(x, y)) { + auto imgflag = Gtk::make_managed(); + imgflag->set(m_pixbufFlag); + buttons.at(x + y * field.getRows())->set_child(*imgflag); + buttons.at(x + y * field.getRows())->set_active(true); + } + else { + buttons.at(x + y * field.getRows())->unset_child(); + buttons.at(x+ y * field.getRows())->queue_draw(); + buttons.at(x + y * field.getRows())->set_active(false); + } + } + + // Glib::ustring msg = Glib::ustring::compose("Remaining flags: %1", field.getRemainingFlags()); + // flagLabel.set_label(msg); +} + +void MainWindow::updateFlagsLabel(int flags) { + 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::OpenNearCells(int index, std::set &visited) { +// int cols = field->Cols(); +// int x = index % cols; +// int y = index / cols; + +// if (visited.count(index)) return; + +// Cell* cell = field->GetCell(x, y); +// if (!cell || cell->bombsNearby > 0 || cell->type == CellType::Bomb) return; + +// visited.insert(index); +// buttons[index]->set_active(true); + +// for (int i = -1; i <= 1; i++) { +// for (int j = -1; j <= 1; j++) { +// if (i == 0 && j == 0) continue; // Skip the current cell + +// int nx = x + i; +// int ny = y + j; +// int newIndex = ny * cols + nx; +// Cell* neighborCell = field->GetCell(nx, ny); + +// // Bounds check before recursive call +// if (nx >= 0 && nx < cols && ny >= 0 && ny < cols) { +// if (visited.count(newIndex) == 0) { +// OpenNearCells(newIndex, visited); +// } +// if (neighborCell && !buttons[newIndex]->get_active() && !neighborCell->isFlag) { +// OpenNearCells(newIndex, visited); +// } +// } +// } +// } +// } + + + +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 if(field.isBomb(x, y)) { + openBombs(); + } + else { + field.clearCell(x, y); + } +} + + +void MainWindow::openBombs() { + for(int i=0; i < field.getCols() * field.getRows(); i++) { + int x = i % field.getCols(); + int y = i / field.getRows(); + + buttons.at(i)->set_sensitive(false); + + if(field.isBomb(x, y)) { + if(field.isFlagged(x, y)) { + auto imgFlagBomb = std::make_shared(); + imgFlagBomb->set(m_pixbufFlagBomb); + buttons.at(i)->set_child(*imgFlagBomb); + } + else { + auto imgBomb = std::make_shared(); + imgBomb->set(m_pixbufBomb); + buttons.at(i)->set_child(*imgBomb); + } + buttons.at(i)->set_active(true); + } + } +} + +void MainWindow::updateCell(int x, int y) { + if(field.isCleared(x, y)) { + if (field.bombsNearby(x, y) > 0) { + switch(field.bombsNearby(x, y)) { + case 1: + buttons.at(x + y * field.getRows())->get_style_context()->add_class("label-1"); + break; + case 2: + buttons.at(x + y * field.getRows())->get_style_context()->add_class("label-2"); + break; + case 3: + buttons.at(x + y * field.getRows())->get_style_context()->add_class("label-3"); + break; + case 4: + buttons.at(x + y * field.getRows())->get_style_context()->add_class("label-4"); + break; + case 5: + buttons.at(x + y * field.getRows())->get_style_context()->add_class("label-5"); + break; + case 6: + buttons.at(x + y * field.getRows())->get_style_context()->add_class("label-6"); + break; + case 7: + buttons.at(x + y * field.getRows())->get_style_context()->add_class("label-7"); + break; + case 8: + buttons.at(x + y * field.getRows())->get_style_context()->add_class("label-8"); + break; + } + buttons.at(x + y * field.getRows())->set_label(Glib::ustring::format(field.bombsNearby(x, y))); + } + buttons.at(x + y * field.getRows())->set_active(true); + buttons.at(x + y * field.getRows())->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; +// } + + +// bool MainWindow::UpdateClockLabel() +// { +// if(gameOver) return false; + +// elapsedTime++; + +// int deciseconds = elapsedTime % 10; +// int seconds = (elapsedTime / 10) % 60; +// int minutes = (elapsedTime /600) % 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); + +// return true; +// } + +MainWindow::MainWindow() +{ + // ApplyStyles(); // Load the CSS file + elapsedTime = 0; + newGame = true; + set_title("MineSweeper"); + set_default_size(400, 400); + set_resizable(false); + + boxV = Gtk::Box(Gtk::Orientation::VERTICAL); + boxH = Gtk::Box(Gtk::Orientation::HORIZONTAL); + + boxH.set_hexpand(true); + + boxV.append(boxH); + boxH.set_expand(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); + + 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_pixbufBomb = Gdk::Pixbuf::create_from_resource("/minesweeper/bomb-solid"); + m_pixbufFlag = Gdk::Pixbuf::create_from_resource("/minesweeper/flag-solid"); + m_pixbufFlagBomb = Gdk::Pixbuf::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(); + 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.clearCellSignal.connect(sigc::bind(sigc::mem_fun(*this, &MainWindow::updateCell))); + field.remainingFlagsChangedSignal.connect(sigc::bind(sigc::mem_fun(*this, \ + &MainWindow::updateFlagsLabel))); + //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"); + + //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) { + auto app = Gtk::Application::create("eu.minesweeper"); + return app->make_window_and_run(argc, argv); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..c13ef29 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,30 @@ +#include "MineField.hpp" +#include + +int main() { + + MineField field(160,160, 100); + + srand(time(NULL)); + + int x = rand() % 160; + int y = rand() % 160; + + field.initBombs(x, y); + + printf("Opened cell: %i %i\n", x, y); + printf("Neighboor bombs: %i\n", field.bombsNearby(x,y)); + + while(!field.isGameOver()) { + x = rand() % 160; + y = rand() % 160; + + if(field.clearCell(x, y)) { + printf("Opened cell: %i %i\n", x, y); + printf("Neighboor bombs: %i\n", field.bombsNearby(x,y)); + } + else { + printf("Bomb found in cell: %i %i\n", x, y); + } + } +} diff --git a/src/window.hpp b/src/window.hpp new file mode 100644 index 0000000..60026c5 --- /dev/null +++ b/src/window.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "MineField.hpp" +#include "gdkmm/pixbuf.h" +#include +//#include +#include +#include +#include +#include +//#include +//#include +#include +//#include +// #include +// #include +//#include +//#include +// #include +// #include +// #include +// #include +// #include + +#define PROJECT_NAME "minesweeper" + + +class MainWindow : public Gtk::Window +{ + 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, 40}; + int elapsedTime; + bool newGame; + std::shared_ptr m_pixbufBomb; + std::shared_ptr m_pixbufFlag; + std::shared_ptr m_pixbufFlagBomb; + void updateCell(int x, int y); + void openBombs(); + void updateFlagsLabel(int flags); + +// sigc::connection clockConn; +// void OpenNearCells(int index); +// void Explode(); +// 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(); +};