diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7194ea7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.cache +build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..53c722c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.14) + +project(minesweeper LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +find_package(Qt6 REQUIRED COMPONENTS Widgets) + +option(RELEASE_BUILD "Enable Release build" OFF) +if(RELEASE_BUILD) + set(CMAKE_BUILD_TYPE Release) +endif() + +file(GLOB SOURCES "src/*.cpp") +file(GLOB HEADERS "include/*.hpp" "include/*.h") + +qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) + +if (WIN32) + add_executable(minesweeper WIN32 ${SOURCES} ${MOC_SOURCES}) +else() + add_executable(minesweeper ${SOURCES} ${MOC_SOURCES}) +endif() + +target_link_libraries(minesweeper PRIVATE Qt6::Widgets) +target_include_directories(minesweeper PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_compile_definitions(minesweeper PRIVATE + $<$:RELEASE_BUILD> +) diff --git a/assets/bomb.png b/assets/bomb.png new file mode 100644 index 0000000..360dbab Binary files /dev/null and b/assets/bomb.png differ diff --git a/assets/flag.png b/assets/flag.png new file mode 100644 index 0000000..55c8867 Binary files /dev/null and b/assets/flag.png differ diff --git a/assets/misplacedbomb.png b/assets/misplacedbomb.png new file mode 100644 index 0000000..4f6ec0a Binary files /dev/null and b/assets/misplacedbomb.png differ diff --git a/assets/redbomb.png b/assets/redbomb.png new file mode 100644 index 0000000..c7e75a8 Binary files /dev/null and b/assets/redbomb.png differ diff --git a/include/cell.hpp b/include/cell.hpp new file mode 100644 index 0000000..ae45574 --- /dev/null +++ b/include/cell.hpp @@ -0,0 +1,66 @@ +#ifndef CELL_HPP +#define CELL_HPP + +#include "workingdir.h" +#include +#include +#include +#include + +#define STRAIGHTBOMB_IMG_PATH WDIR "assets/bomb.png" +#define MISPLACED_BOMB_IMG_PATH WDIR "assets/misplacedbomb.png" +#define REDBOMB_IMG_PATH WDIR "assets/redbomb.png" +#define FLAG_IMG_PATH WDIR "assets/flag.png" + +#define BLACK_RGB QColor(20, 20, 20) +#define BLUE_RGB QColor(0, 0, 205) +#define GREEN_RGB QColor(0, 128, 0) +#define LIGHTRED_RGB QColor(237, 28, 36) +#define DARKBLUE_RGB QColor(25, 80, 232) +#define DARKRED_RGB QColor(216, 0, 0) +#define AQUA_RGB QColor(7, 147, 131) +#define PURPLE_RGB QColor(149, 3, 168) +#define YELLOWISH_RGB QColor(206, 140, 8) +#define AMETHYST_RGB QColor(191, 50, 242) + +enum FontColor { + BLACK, + BLUE, + GREEN, + LIGHTRED, + DARKBLUE, + DARKRED, + AQUA, + PURPLE, + YELLOWISH, + AMETHYST, +}; + +class CellBtn : public QPushButton { + Q_OBJECT + +public: + explicit CellBtn(QWidget *parent = nullptr); + + void set_font(FontColor font_color = BLACK); + void set_icon(QString icon_path); + void remove_icon(); + void disable(); + void flag(); + void unflag(); + bool is_flagged(); + +private: + QFont cell_font; + QPixmap pixmap; + bool flagged; + +private slots: + void mousePressEvent(QMouseEvent *e); + +signals: + void leftClicked(); + void rightClicked(); +}; + +#endif // CELL_HPP diff --git a/include/game.hpp b/include/game.hpp new file mode 100644 index 0000000..9250112 --- /dev/null +++ b/include/game.hpp @@ -0,0 +1,54 @@ +#ifndef GAME_HPP +#define GAME_HPP + +#define WINDOW_TITLE "Minesweeper" +#define CELL_SIZE 30 + +#include "cell.hpp" +#include "gamebar.hpp" +#include +#include +#include +#include +#include + +class GameWindow : public QDialog { + Q_OBJECT + +public: + explicit GameWindow(int rows, int cols, int mines); + +private: + int rows; + int cols; + int mines; + bool is_first_reveal; + GameBar *gamebar; + + QGridLayout *grid_layout; + QVBoxLayout *main_layout; + QVector> grid_buttons; + QVector> mine_map; + QSet> opening_cells; + QTimer *timer; + int elapsed_time; + + bool get_first_reveal(); + void set_first_reveal(bool boolean); + void create_board(); + void make_opening(int start_row, int start_col); + void set_mines(); + void reveal_cell(int row, int col); + void put_flag(int row, int col); + int count_adjacent_mines(int row, int col); + +private slots: + void update_timer(); + void reset_game(); + +signals: + void gameReset(); + void gameClosed(); +}; + +#endif // GAME_HPP diff --git a/include/gamebar.hpp b/include/gamebar.hpp new file mode 100644 index 0000000..e040f35 --- /dev/null +++ b/include/gamebar.hpp @@ -0,0 +1,30 @@ +#ifndef GAMEBAR_HPP +#define GAMEBAR_HPP + +#include +#include +#include +#include + +class GameBar : public QWidget { + Q_OBJECT + +public: + explicit GameBar(int mines, QWidget *parent = nullptr); + + void update_timer(int seconds); + void update_mine_count(int mines); + +private: + QLCDNumber *timer_display; + QLCDNumber *mine_count_display; + QPushButton *reset_button; + +signals: + void reset_game(); + +private slots: + void handle_reset_button(); +}; + +#endif // GAMEBAR_HPP diff --git a/include/mainmenu.hpp b/include/mainmenu.hpp new file mode 100644 index 0000000..a9d5d92 --- /dev/null +++ b/include/mainmenu.hpp @@ -0,0 +1,42 @@ +#ifndef MAINMENU_HPP +#define MAINMENU_HPP + +#include "game.hpp" +#include +#include +#include +#include +#include + +#define MENU_WINDOW_TITLE "Select Game Variant" +#define MENU_FIXED_WIDTH 900 +#define MENU_FIXED_HEIGHT 600 + +enum GameVariants { + _16x16_40, + _16x16_50, + _16x16_60, + _20x20_75, + _20x20_90, + _20x20_120, + _24x24_140, + _24x24_160, + _24x24_180, +}; + +class MainMenu : public QDialog { + Q_OBJECT + +public: + explicit MainMenu(QWidget *parent = nullptr); + GameVariants get_variant(); + +private: + void setup_layout(); + void select_variant(); + + GameVariants variant; + GameWindow *game_window; +}; + +#endif // MAINMENU_HPP diff --git a/include/utils.hpp b/include/utils.hpp new file mode 100644 index 0000000..e1f6749 --- /dev/null +++ b/include/utils.hpp @@ -0,0 +1,7 @@ +#ifndef UTILS_HPP + +namespace utils { +int rng(int start, int end); +} + +#endif // UTILS_HPP diff --git a/include/workingdir.h b/include/workingdir.h new file mode 100644 index 0000000..d068fa2 --- /dev/null +++ b/include/workingdir.h @@ -0,0 +1,10 @@ +#ifndef WORKINGDIR +#define WORKINGDIR + +#ifdef RELEASE_BUILD +#define WDIR "./" +#else +#define WDIR "../" +#endif + +#endif // WORKINGDIR diff --git a/run b/run new file mode 100755 index 0000000..a049ca6 --- /dev/null +++ b/run @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +if [[ "$1" == "--release-build" ]]; then + cmake_options="-DRELEASE_BUILD=ON" +else + cmake_options="" +fi + +mkdir -p "build" +cd "build" + +if [[ "$OS" == "Windows_NT" ]]; then + cmake -G "MinGW Makefiles" $cmake_options .. +else + cmake $cmake_options .. +fi + +make +./minesweeper diff --git a/src/cell.cpp b/src/cell.cpp new file mode 100644 index 0000000..11b4c41 --- /dev/null +++ b/src/cell.cpp @@ -0,0 +1,74 @@ +#include "cell.hpp" + +CellBtn::CellBtn(QWidget *parent) : QPushButton(parent) { + setFocusPolicy(Qt::NoFocus); +}; + +void CellBtn::set_font(FontColor font_color) { + cell_font.setPointSize(13); + cell_font.setBold(true); + QPalette palette = this->palette(); + + switch (font_color) { + case BLUE: + palette.setColor(QPalette::ButtonText, BLUE_RGB); + break; + case GREEN: + palette.setColor(QPalette::ButtonText, GREEN_RGB); + break; + case LIGHTRED: + palette.setColor(QPalette::ButtonText, LIGHTRED_RGB); + break; + case DARKBLUE: + palette.setColor(QPalette::ButtonText, DARKBLUE_RGB); + break; + case DARKRED: + palette.setColor(QPalette::ButtonText, DARKRED_RGB); + break; + case AQUA: + palette.setColor(QPalette::ButtonText, AQUA_RGB); + break; + case PURPLE: + palette.setColor(QPalette::ButtonText, PURPLE_RGB); + break; + case YELLOWISH: + palette.setColor(QPalette::ButtonText, YELLOWISH_RGB); + break; + case AMETHYST: + palette.setColor(QPalette::ButtonText, AMETHYST_RGB); + break; + default: + palette.setColor(QPalette::ButtonText, Qt::black); + cell_font.setBold(false); + cell_font.setPointSize(11); + break; + } + + setFont(cell_font); + setPalette(palette); +} + +void CellBtn::remove_icon() { setIcon(QIcon()); } +void CellBtn::set_icon(QString icon_path) { + pixmap = QPixmap(icon_path); + setIcon(pixmap); + setIconSize(size()); +} + +void CellBtn::disable() { + disconnect(this, &CellBtn::leftClicked, nullptr, nullptr); + disconnect(this, &CellBtn::rightClicked, nullptr, nullptr); +} + +bool CellBtn::is_flagged() { return flagged; } +void CellBtn::flag() { flagged = true; } +void CellBtn::unflag() { flagged = false; } + +void CellBtn::mousePressEvent(QMouseEvent *event) { + if (event->button() == Qt::RightButton) { + emit rightClicked(); + } else if (event->button() == Qt::LeftButton) { + emit leftClicked(); + } + QPushButton::mouseReleaseEvent(event); +} diff --git a/src/game.cpp b/src/game.cpp new file mode 100644 index 0000000..6f9e290 --- /dev/null +++ b/src/game.cpp @@ -0,0 +1,249 @@ +#include "game.hpp" +#include "cell.hpp" +#include "gamebar.hpp" +#include "utils.hpp" +#include +#include +#include +#include +#include + +GameWindow::GameWindow(int rows, int cols, int mines) + : rows(rows), cols(cols), mines(mines) { + + elapsed_time = 0; + is_first_reveal = true; + main_layout = new QVBoxLayout(this); + gamebar = new GameBar(mines, this); + grid_layout = new QGridLayout(); + timer = new QTimer(this); + + mine_map.resize(rows, QVector(cols, false)); + + create_board(); + resize(cols * CELL_SIZE, rows * CELL_SIZE); + setWindowTitle(WINDOW_TITLE); + + connect(timer, &QTimer::timeout, this, &GameWindow::update_timer); + connect(gamebar, &GameBar::reset_game, this, &GameWindow::reset_game); + + main_layout->addWidget(gamebar); + main_layout->addLayout(grid_layout); + setLayout(main_layout); +} + +bool GameWindow::get_first_reveal() { return is_first_reveal; } +void GameWindow::set_first_reveal(bool boolean) { is_first_reveal = boolean; } + +void GameWindow::update_timer() { + elapsed_time++; + gamebar->update_timer(elapsed_time); +} + +void GameWindow::reset_game() { + emit gameClosed(); + close(); +} + +void GameWindow::create_board() { + grid_buttons.resize(rows, QVector(cols, nullptr)); + + for (int i = 0; i < rows; ++i) { + for (int j = 0; j < cols; ++j) { + grid_buttons[i][j] = new CellBtn(this); + grid_buttons[i][j]->setMinimumSize(30, 30); + grid_buttons[i][j]->set_font(); + grid_layout->setSpacing(1); + grid_layout->setContentsMargins(0, 0, 0, 0); + grid_layout->addWidget(grid_buttons[i][j], i, j); + + connect(grid_buttons[i][j], &CellBtn::leftClicked, + [this, i, j]() { reveal_cell(i, j); }); + + connect(grid_buttons[i][j], &CellBtn::rightClicked, + [this, i, j]() { put_flag(i, j); }); + } + } +} + +void GameWindow::make_opening(int start_row, int start_col) { + opening_cells.clear(); + + int total_cells = rows * cols; + int cells_to_open = total_cells * (utils::rng(12, 24) / 100.0); + + QSet> opened_cells; + QQueue> queue; + + queue.enqueue(qMakePair(start_row, start_col)); + opened_cells.insert(qMakePair(start_row, start_col)); + + while (!queue.isEmpty() && opened_cells.size() < cells_to_open) { + auto cell = queue.dequeue(); + int row = cell.first; + int col = cell.second; + + reveal_cell(row, col); + + for (int i = -1; i <= 1; ++i) { + for (int j = -1; j <= 1; ++j) { + int new_row = row + i; + int new_col = col + j; + if (new_row >= 0 && new_row < rows && new_col >= 0 && new_col < cols) { + QPair new_cell = qMakePair(new_row, new_col); + if (!opened_cells.contains(new_cell)) { + opened_cells.insert(new_cell); + queue.enqueue(new_cell); + } + } + } + } + } + + opening_cells = opened_cells; + + for (const auto &cell : opened_cells) { + mine_map[cell.first][cell.second] = false; + } +} + +void GameWindow::set_mines() { + QVector> possible_positions; + + // collect all possible positions excluding the opening cells + for (int i = 0; i < rows; ++i) { + for (int j = 0; j < cols; ++j) { + QPair cell = qMakePair(i, j); + if (!opening_cells.contains(cell)) { + possible_positions.append(cell); + grid_buttons[cell.first][cell.second]->unflag(); + } + } + } + + // shuffle the possible positions + std::random_device rd; + std::mt19937 g(rd()); + std::shuffle(possible_positions.begin(), possible_positions.end(), g); + + // place mines in the first 'mines' positions + for (int i = 0; i < mines; ++i) { + int row = possible_positions[i].first; + int col = possible_positions[i].second; + mine_map[row][col] = true; + } + + // reveal opening cells + for (const auto &cell : opening_cells) { + reveal_cell(cell.first, cell.second); + } +} + +void GameWindow::reveal_cell(int row, int col) { + if (grid_buttons[row][col]->text() != "") + return; + + if (get_first_reveal()) { + set_first_reveal(false); + timer->start(1000); + + make_opening(row, col); + set_mines(); + + return; + } + + if (mine_map[row][col] == true) { + timer->stop(); + + for (int i = 0; i < rows; ++i) { + for (int j = 0; j < cols; ++j) { + grid_buttons[i][j]->disable(); + if (mine_map[i][j] == true && !grid_buttons[i][j]->is_flagged()) { + grid_buttons[i][j]->set_icon(STRAIGHTBOMB_IMG_PATH); + } else if (mine_map[i][j] == true && grid_buttons[i][j]->is_flagged()) { + grid_buttons[i][j]->set_icon(MISPLACED_BOMB_IMG_PATH); + } + } + } + grid_buttons[row][col]->set_icon(REDBOMB_IMG_PATH); + } else { + int adjacent_mines = count_adjacent_mines(row, col); + + switch (adjacent_mines) { + case 0: + grid_buttons[row][col]->set_font(FontColor::BLACK); + break; + case 1: + grid_buttons[row][col]->set_font(FontColor::BLUE); + break; + case 2: + grid_buttons[row][col]->set_font(FontColor::GREEN); + break; + case 3: + grid_buttons[row][col]->set_font(FontColor::LIGHTRED); + break; + case 4: + grid_buttons[row][col]->set_font(FontColor::DARKBLUE); + break; + case 5: + grid_buttons[row][col]->set_font(FontColor::DARKRED); + break; + case 6: + grid_buttons[row][col]->set_font(FontColor::AQUA); + break; + case 7: + grid_buttons[row][col]->set_font(FontColor::PURPLE); + break; + case 8: + grid_buttons[row][col]->set_font(FontColor::YELLOWISH); + break; + case 9: + grid_buttons[row][col]->set_font(FontColor::AMETHYST); + break; + } + + grid_buttons[row][col]->setText(QString::number(adjacent_mines)); + grid_buttons[row][col]->disable(); + } +} + +void GameWindow::put_flag(int row, int col) { + if (is_first_reveal) + return; + + if (grid_buttons[row][col]->is_flagged()) { + grid_buttons[row][col]->remove_icon(); + grid_buttons[row][col]->unflag(); + connect(grid_buttons[row][col], &CellBtn::leftClicked, this, + [this, row, col]() { reveal_cell(row, col); }); + } else { + grid_buttons[row][col]->set_icon(FLAG_IMG_PATH); + grid_buttons[row][col]->flag(); + disconnect(grid_buttons[row][col], &CellBtn::leftClicked, nullptr, nullptr); + } +} + +int GameWindow::count_adjacent_mines(int row, int col) { + int count = 0; + + for (int i = -1; i <= 1; ++i) { + for (int j = -1; j <= 1; j++) { + int new_row = row + i; + int new_col = col + j; + if (new_row >= 0 && new_row < rows && new_col >= 0 && new_col < cols) { + if (mine_map[new_row][new_col]) { + count++; + } + } + } + } + + return count; +} + +/* + +'mine_map[row][col] == true' is easier to read + +*/ diff --git a/src/gamebar.cpp b/src/gamebar.cpp new file mode 100644 index 0000000..3b93f49 --- /dev/null +++ b/src/gamebar.cpp @@ -0,0 +1,40 @@ +#include "gamebar.hpp" + +GameBar::GameBar(int mines, QWidget *parent) : QWidget(parent) { + timer_display = new QLCDNumber(this); + mine_count_display = new QLCDNumber(this); + reset_button = new QPushButton(this); + + timer_display->setDigitCount(3); + timer_display->setMaximumHeight(55); + mine_count_display->setDigitCount(3); + mine_count_display->setMaximumHeight(55); + + update_timer(0); + update_mine_count(mines); + + reset_button->setText("Quit"); + reset_button->setFixedSize(50, 50); + + connect(reset_button, &QPushButton::clicked, this, + &GameBar::handle_reset_button); + + QHBoxLayout *layout = new QHBoxLayout(this); + layout->addWidget(timer_display); + layout->addWidget(reset_button); + layout->addWidget(mine_count_display); + + setLayout(layout); +} + +void GameBar::update_timer(int seconds) { + timer_display->display(seconds); +} + +void GameBar::update_mine_count(int mines) { + mine_count_display->display(mines); +} + +void GameBar::handle_reset_button() { + emit reset_game(); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..ef67a45 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,80 @@ +#include "game.hpp" +#include "mainmenu.hpp" +#include + +int main(int argc, char **argv) { + QApplication app(argc, argv); + + while (true) { + MainMenu menu; + + if (menu.exec() == QDialog::Accepted) { + GameVariants variant = menu.get_variant(); + int rows, cols, mines; + + switch (variant) { + case _16x16_40: + rows = 16; + cols = 16; + mines = 40; + break; + case _16x16_50: + rows = 16; + cols = 16; + mines = 50; + break; + case _16x16_60: + rows = 16; + cols = 16; + mines = 60; + break; + case _20x20_75: + rows = 20; + cols = 20; + mines = 75; + break; + case _20x20_90: + rows = 20; + cols = 20; + mines = 90; + break; + case _20x20_120: + rows = 20; + cols = 20; + mines = 120; + break; + case _24x24_140: + rows = 24; + cols = 24; + mines = 140; + break; + case _24x24_160: + rows = 24; + cols = 24; + mines = 160; + break; + case _24x24_180: + rows = 24; + cols = 24; + mines = 180; + break; + default: + rows = 16; + cols = 16; + mines = 40; + break; + } + + GameWindow game(rows, cols, mines); + + QObject::connect(&game, &GameWindow::gameClosed, + [&app, &menu]() { menu.show(); }); + + game.exec(); + } else { + break; + } + } + + return app.exec(); +} diff --git a/src/mainmenu.cpp b/src/mainmenu.cpp new file mode 100644 index 0000000..96fe60b --- /dev/null +++ b/src/mainmenu.cpp @@ -0,0 +1,64 @@ +#include "mainmenu.hpp" + +MainMenu::MainMenu(QWidget *parent) : QDialog(parent) { + setup_layout(); +} + +void MainMenu::setup_layout() { + QVBoxLayout *main_layout = new QVBoxLayout(this); + QGridLayout *grid_layout = new QGridLayout(); + + struct button_info { + QString size_label; + QString mine_label; + }; + + QVector button_infos = { + {"16x16", "40 Mines"}, {"16x16", "50 Mines"}, {"16x16", "60 Mines"}, + {"20x20", "75 Mines"}, {"20x20", "90 Mines"}, {"20x20", "120 Mines"}, + {"24x24", "140 Mines"}, {"24x24", "160 Mines"}, {"24x24", "180 Mines"}}; + + QFont font; + font.setPointSize(16); + + for (int i = 0; i < button_infos.size(); ++i) { + QPushButton *button = new QPushButton(this); + + button->setFocusPolicy(Qt::NoFocus); + button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + button->setMinimumSize(200, 100); + button->setCursor(QCursor(Qt::OpenHandCursor)); + + QVBoxLayout *button_layout = new QVBoxLayout(button); + QLabel *size_label = new QLabel(button_infos[i].size_label, button); + QLabel *mine_label = new QLabel(button_infos[i].mine_label, button); + + size_label->setAlignment(Qt::AlignCenter); + mine_label->setAlignment(Qt::AlignCenter); + size_label->setFont(font); + mine_label->setFont(font); + button_layout->addWidget(size_label); + button_layout->addWidget(mine_label); + + connect(button, &QPushButton::clicked, this, [this, i]() { + variant = static_cast(i); + select_variant(); + }); + + grid_layout->addWidget(button, i / 3, i % 3); + } + + main_layout->addLayout(grid_layout); + + setLayout(main_layout); + setWindowTitle(MENU_WINDOW_TITLE); + setFixedSize(MENU_FIXED_WIDTH, MENU_FIXED_HEIGHT); +} + +GameVariants MainMenu::get_variant() { + return variant; +} + +void MainMenu::select_variant() { + accept(); +} diff --git a/src/utils.cpp b/src/utils.cpp new file mode 100644 index 0000000..f733348 --- /dev/null +++ b/src/utils.cpp @@ -0,0 +1,23 @@ +#include "utils.hpp" +#include +#include + +#define INVALID_ARG_MSG \ + "Invalid range: 'start' must be less than or equal to 'end'" + +namespace utils { + +int rng(int start, int end) { + if (start > end) + throw std::invalid_argument(INVALID_ARG_MSG); + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> dis_int(start, end); + + int random_int = dis_int(gen); + + return random_int; +} + +} // namespace utils