|
|
@@ -1,329 +1,1126 @@
|
|
|
+
|
|
|
#include "window.hpp"
|
|
|
-#include "gdkmm/texture.h"
|
|
|
-#include "sigc++/adaptors/bind.h"
|
|
|
-#include "sigc++/functors/mem_fun.h"
|
|
|
-
|
|
|
-//}
|
|
|
-// 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.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);
|
|
|
+#include <iostream>
|
|
|
+#include <sstream>
|
|
|
+#include <algorithm>
|
|
|
+#include <iomanip>
|
|
|
+#include <ctime>
|
|
|
+#include <chrono>
|
|
|
+#include <random>
|
|
|
+#include <filesystem>
|
|
|
+
|
|
|
+MainWindow::MainWindow()
|
|
|
+{
|
|
|
+ // Initialize the game field with default settings
|
|
|
+ m_field = std::make_unique<MineField>(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::setupUI()
|
|
|
+{
|
|
|
+ // 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::setupHeaderBar()
|
|
|
+{
|
|
|
+ // Create header bar
|
|
|
+ m_headerBar.set_show_title_buttons(true);
|
|
|
+
|
|
|
+ // Add title widget
|
|
|
+ auto titleLabel = Gtk::make_managed<Gtk::Label>("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);
|
|
|
+}
|
|
|
+
|
|
|
+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);
|
|
|
+}
|
|
|
+
|
|
|
+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);
|
|
|
}
|
|
|
- else
|
|
|
- {
|
|
|
- buttons.at(pos)->unset_child();
|
|
|
- buttons.at(pos)->queue_draw();
|
|
|
- buttons.at(pos)->set_active(false);
|
|
|
+
|
|
|
+ // 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<Gtk::ToggleButton>();
|
|
|
+ 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::updateFlagsLabel(int flags)
|
|
|
+void MainWindow::loadResources()
|
|
|
{
|
|
|
- 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();
|
|
|
+ // 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::openBombs()
|
|
|
-{
|
|
|
- for (int i = 0; i < field.getCols() * field.getRows(); i++)
|
|
|
- {
|
|
|
- int x = i % field.getCols();
|
|
|
- int y = i / field.getCols();
|
|
|
-
|
|
|
- buttons.at(i)->set_sensitive(false);
|
|
|
-
|
|
|
- if (field.isBomb(x, y))
|
|
|
- {
|
|
|
- if (field.isFlagged(x, y))
|
|
|
- {
|
|
|
- auto imgFlagBomb = std::make_shared<Gtk::Image>();
|
|
|
- imgFlagBomb->set(m_textureFlagBomb);
|
|
|
- buttons.at(i)->set_child(*imgFlagBomb);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- auto imgBomb = std::make_shared<Gtk::Image>();
|
|
|
- imgBomb->set(m_textureBomb);
|
|
|
- buttons.at(i)->set_child(*imgBomb);
|
|
|
- }
|
|
|
- buttons.at(i)->set_active(true);
|
|
|
+}
|
|
|
+
|
|
|
+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<Gtk::Image>();
|
|
|
+ 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<int>(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<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()
|
|
|
+void MainWindow::updateFlagsLabel(int flags)
|
|
|
{
|
|
|
- // clockSignalConn.disconnect();
|
|
|
- // std::cout << "Signal gameOver emmited\n";
|
|
|
+ m_flagsLabel.set_label(Glib::ustring::compose("Flags: %1/%2",
|
|
|
+ flags,
|
|
|
+ m_field->getTotalMines()));
|
|
|
}
|
|
|
|
|
|
-void MainWindow::updateClockLabel()
|
|
|
+bool MainWindow::updateTimer()
|
|
|
{
|
|
|
+ // Update timer every 100ms
|
|
|
+ if (m_field->getGameState() == MineField::GameState::PLAYING) {
|
|
|
+ m_field->timerTick();
|
|
|
+ updateTimeLabel();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
|
|
|
- size_t time = 100; // field.getCurrentTime();
|
|
|
+void MainWindow::updateTimeLabel()
|
|
|
+{
|
|
|
+ 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)
|
|
|
+ ));
|
|
|
+}
|
|
|
|
|
|
- int deciseconds = (time / 100) % 10;
|
|
|
- int seconds = (time / 1000) % 60;
|
|
|
- int minutes = (time / 60000) % 60;
|
|
|
+void MainWindow::onGameOver()
|
|
|
+{
|
|
|
+ // Disconnect timer
|
|
|
+ m_timerConnection.disconnect();
|
|
|
+
|
|
|
+ // Show all bombs
|
|
|
+ revealAllBombs();
|
|
|
+
|
|
|
+ // Show game over animation
|
|
|
+ showGameOverAnimation();
|
|
|
+}
|
|
|
|
|
|
- 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);
|
|
|
+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);
|
|
|
}
|
|
|
|
|
|
-void MainWindow::handleClockSig(size_t time)
|
|
|
+void MainWindow::revealAllBombs()
|
|
|
{
|
|
|
- (void)time;
|
|
|
- m_clockDispatch.emit();
|
|
|
+ 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<Gtk::Image>();
|
|
|
+ imgFlagBomb->set(m_textureFlagBomb);
|
|
|
+ m_buttons[index]->set_child(*imgFlagBomb);
|
|
|
+ } else {
|
|
|
+ auto imgBomb = Gtk::make_managed<Gtk::Image>();
|
|
|
+ imgBomb->set(m_textureBomb);
|
|
|
+ m_buttons[index]->set_child(*imgBomb);
|
|
|
+ }
|
|
|
+ m_buttons[index]->set_active(true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-MainWindow::MainWindow()
|
|
|
+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<Gtk::Image>();
|
|
|
+ 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<Gtk::MessageDialog>(*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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+void MainWindow::setupConfetti()
|
|
|
+{
|
|
|
+ // Create confetti pieces
|
|
|
+ std::random_device rd;
|
|
|
+ std::mt19937 gen(rd());
|
|
|
+ std::uniform_int_distribution<int> colorDist(0, 5);
|
|
|
+ std::uniform_int_distribution<int> sizeDist(10, 20);
|
|
|
+ std::uniform_int_distribution<int> xDist(0, m_field->getCols() - 1);
|
|
|
+ std::uniform_int_distribution<int> yDist(0, m_field->getRows() - 1);
|
|
|
+
|
|
|
+ const std::vector<std::string> 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<Gtk::DrawingArea>();
|
|
|
+ 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<Cairo::Context>& 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<int>(800, 2000)(gen);
|
|
|
+ Glib::signal_timeout().connect_once([confetti]() {
|
|
|
+ confetti->unparent();
|
|
|
+ }, duration);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+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<Gtk::Image>();
|
|
|
+ imgFlag->set(m_textureFlag);
|
|
|
+ m_buttons[index]->set_child(*imgFlag);
|
|
|
+ m_buttons[index]->set_active(true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+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");
|
|
|
+}
|
|
|
+
|
|
|
+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<int>(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));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void MainWindow::onDifficultySelected(const GameDifficulty& difficulty)
|
|
|
+{
|
|
|
+ // Start new game with selected difficulty
|
|
|
+ startNewGame(difficulty.cols, difficulty.rows, difficulty.mines, difficulty.name);
|
|
|
+}
|
|
|
+
|
|
|
+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<MineField>(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");
|
|
|
+
|
|
|
+ // 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);
|
|
|
+
|
|
|
+ // If the window is already visible, we need to queue a resize
|
|
|
+ queue_resize();
|
|
|
+
|
|
|
+ // Setup new game board
|
|
|
+ setupGameBoard();
|
|
|
+}
|
|
|
+
|
|
|
+void MainWindow::showDifficultyDialog()
|
|
|
+{
|
|
|
+ // Create custom difficulty dialog
|
|
|
+ auto dialog = Gtk::make_managed<Gtk::Dialog>("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::Box>(Gtk::Orientation::HORIZONTAL, 5);
|
|
|
+ auto widthLabel = Gtk::make_managed<Gtk::Label>("Width:");
|
|
|
+ widthLabel->set_halign(Gtk::Align::START);
|
|
|
+ auto widthSpinner = Gtk::make_managed<Gtk::SpinButton>();
|
|
|
+ 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::Box>(Gtk::Orientation::HORIZONTAL, 5);
|
|
|
+ auto heightLabel = Gtk::make_managed<Gtk::Label>("Height:");
|
|
|
+ heightLabel->set_halign(Gtk::Align::START);
|
|
|
+ auto heightSpinner = Gtk::make_managed<Gtk::SpinButton>();
|
|
|
+ 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::Box>(Gtk::Orientation::HORIZONTAL, 5);
|
|
|
+ auto minesLabel = Gtk::make_managed<Gtk::Label>("Mines:");
|
|
|
+ minesLabel->set_halign(Gtk::Align::START);
|
|
|
+ auto minesSpinner = Gtk::make_managed<Gtk::SpinButton>();
|
|
|
+ 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<int>(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();
|
|
|
+}
|
|
|
+
|
|
|
+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");
|
|
|
+}
|
|
|
+
|
|
|
+std::string MainWindow::formatTime(int milliseconds) const
|
|
|
{
|
|
|
- // 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);
|
|
|
- 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_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)));
|
|
|
- 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));
|
|
|
-
|
|
|
- // optionButton.set_icon_name("open-menu");
|
|
|
-
|
|
|
- // 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);
|
|
|
-
|
|
|
- // 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 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();
|
|
|
+}
|
|
|
+
|
|
|
+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;
|
|
|
+}
|
|
|
+
|
|
|
+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());
|
|
|
+}
|
|
|
+
|
|
|
+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;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void MainWindow::showLeaderboard()
|
|
|
+{
|
|
|
+ // Create leaderboard dialog
|
|
|
+ auto dialog = Gtk::make_managed<Gtk::Dialog>("Leaderboard", *this, true);
|
|
|
+ dialog->set_default_size(500, 400);
|
|
|
+
|
|
|
+ // Create scrolled window
|
|
|
+ auto scrolledWindow = Gtk::make_managed<Gtk::ScrolledWindow>();
|
|
|
+ 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<Gtk::ListBox>();
|
|
|
+ listBox->set_selection_mode(Gtk::SelectionMode::NONE);
|
|
|
+ listBox->set_show_separators(true);
|
|
|
+
|
|
|
+ // Add header row
|
|
|
+ auto headerRow = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 10);
|
|
|
+ headerRow->add_css_class("header-row");
|
|
|
+
|
|
|
+ auto rankHeader = Gtk::make_managed<Gtk::Label>("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<Gtk::Label>("Player");
|
|
|
+ nameHeader->set_hexpand(true);
|
|
|
+ nameHeader->set_halign(Gtk::Align::START);
|
|
|
+ nameHeader->add_css_class("header");
|
|
|
+
|
|
|
+ auto difficultyHeader = Gtk::make_managed<Gtk::Label>("Difficulty");
|
|
|
+ difficultyHeader->set_hexpand(true);
|
|
|
+ difficultyHeader->set_halign(Gtk::Align::START);
|
|
|
+ difficultyHeader->add_css_class("header");
|
|
|
+
|
|
|
+ auto timeHeader = Gtk::make_managed<Gtk::Label>("Time");
|
|
|
+ timeHeader->set_hexpand(true);
|
|
|
+ timeHeader->set_halign(Gtk::Align::START);
|
|
|
+ timeHeader->add_css_class("header");
|
|
|
+
|
|
|
+ auto dateHeader = Gtk::make_managed<Gtk::Label>("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<Gtk::ListBoxRow>();
|
|
|
+ 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::Box>(Gtk::Orientation::HORIZONTAL, 10);
|
|
|
+ row->add_css_class("score-row");
|
|
|
+
|
|
|
+ auto rank = Gtk::make_managed<Gtk::Label>(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<Gtk::Label>(score.playerName);
|
|
|
+ name->set_hexpand(true);
|
|
|
+ name->set_halign(Gtk::Align::START);
|
|
|
+
|
|
|
+ auto difficulty = Gtk::make_managed<Gtk::Label>(score.difficulty);
|
|
|
+ difficulty->set_hexpand(true);
|
|
|
+ difficulty->set_halign(Gtk::Align::START);
|
|
|
+
|
|
|
+ auto time = Gtk::make_managed<Gtk::Label>(formatTime(score.time));
|
|
|
+ time->set_hexpand(true);
|
|
|
+ time->set_halign(Gtk::Align::START);
|
|
|
+
|
|
|
+ auto date = Gtk::make_managed<Gtk::Label>(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<Gtk::ListBoxRow>();
|
|
|
+ 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();
|
|
|
+}
|
|
|
+
|
|
|
+void MainWindow::showNameInputDialog(int time)
|
|
|
+{
|
|
|
+ // Create name input dialog
|
|
|
+ auto dialog = Gtk::make_managed<Gtk::Dialog>("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<Gtk::Label>();
|
|
|
+ congratsLabel->set_markup("<span size='large'>Congratulations!</span>");
|
|
|
+ congratsLabel->set_margin(10);
|
|
|
+
|
|
|
+ // Add time message
|
|
|
+ auto timeLabel = Gtk::make_managed<Gtk::Label>();
|
|
|
+ timeLabel->set_markup(Glib::ustring::compose(
|
|
|
+ "You completed the game in <b>%1</b>", formatTime(time)
|
|
|
+ ));
|
|
|
+
|
|
|
+ // Add difficulty message
|
|
|
+ auto difficultyLabel = Gtk::make_managed<Gtk::Label>();
|
|
|
+ difficultyLabel->set_markup(Glib::ustring::compose(
|
|
|
+ "Difficulty: <b>%1</b>", m_currentDifficulty
|
|
|
+ ));
|
|
|
+
|
|
|
+ // Add name input field
|
|
|
+ auto nameBox = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 5);
|
|
|
+ auto nameLabel = Gtk::make_managed<Gtk::Label>("Your name:");
|
|
|
+ nameLabel->set_halign(Gtk::Align::START);
|
|
|
+ auto nameEntry = Gtk::make_managed<Gtk::Entry>();
|
|
|
+ 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<Gtk::MessageDialog>(
|
|
|
+ *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();
|
|
|
+}
|
|
|
+
|
|
|
+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<MainWindow>(argc, argv);
|
|
|
+ auto app = Gtk::Application::create("org.gtkmm.minesweeper");
|
|
|
+ return app->make_window_and_run<MainWindow>(argc, argv);
|
|
|
}
|