Skip to content

Minesweeper Game#

Example's page

This page describes an example listed in app category.

Minesweeper game implementation driven by ass.

Game Logic#

The entire game logic is located in MinesweeperWindow.cpp.

Initialization#

The game starts with beginGame function. beginGame Initializes a new Minesweeper game with specified dimensions and bomb count. Clears any previous game state, sets up the grid layout, initializes cell views, connects click handlers for opening cells and flagging, and finally packs UI elements to be displayed.

  • Generates a grid with AGridLayout
  • Dynamically creates CellViews based on game data
  • Handles user events (left clicks and right clicks)
void MinesweeperWindow::beginGame(int columns, int rows, int bombs) {
    mOpenedCells = 0;
    mBombs = bombs;
    mFieldColumns = columns;
    mFieldRows = rows;
    mReveal = false;
    mBombsPlanted = false;
    mGrid->setLayout(std::make_unique<AGridLayout>(columns, rows));
    mField.clear();
    mField.resize(columns * rows);

    for (int i = 0; i < columns * rows; ++i) {
        int x = i % columns;
        int y = i / columns;
        auto cell = _new<CellView>(fieldAt(x, y));

        setupEventHandlers(x, y, cell);
        mGrid->addView(cell);
    }

    pack();
}

Event handling#

As was mentioned, the game handles events by setting signal handlers on CellView. This is done inside setupEventHandlers. Here's a little breakdown:

Toggle flag (right click)#

In a minesweeper game, right click toggles the flag. First, we check that game is not finished (mReveal), then we check if cell is open. Finally, we toggle the flag:

connect(cell->clickedRight, this, [&, x, y]() {
    if (mReveal) {
        return;
    }
    auto& c = fieldAt(x, y);
    if (!(c & FieldCell::OPEN)) {
        c ^= FieldCell::HAS_FLAG;
        updateCellViewStyle(x, y);
    }
});

As you can see, game data is represented by FieldCell enum:

1
2
3
4
5
6
7
8
AUI_ENUM_FLAG(FieldCell) {
    EMPTY = 0,
    HAS_BOMB = 1,
    OPEN = 2,
    HAS_FLAG = 4,
    RED_BG = 8,
    DONT_PLANT_BOMB_HERE = 16,
};

FieldCell is just a bitfield implemented thanks to AUI_ENUM_FLAG.

Open cell (left click)#

Left click delegates cell opening logic to openCell which contain game-specific logic.

1
2
3
4
5
6
connect(cell->clicked, this, [&, x, y]() {
    if (mReveal) {
        return;
    }
    openCell(x, y, true);
});

Cell style#

In response to user's actions, the game updates its internal state. To visualize the state, updateCellViewStyle function is called. This method updates the style of a cell at coordinates (x, y) based on its state or any other conditions (e.g., game over, win/lose states). It triggers an event to let the framework know that custom styles need to be re-evaluated and applied accordingly.

In game style sheets, a custom selector tied to CellView is used to display various cell states:

template<FieldCell fieldCell>
struct CellSelector: IAssSubSelector {
public:
    bool isPossiblyApplicable(AView* view) override {
        return dynamic_cast<CellView*>(view) != nullptr;
    }

    bool isStateApplicable(AView* view) override {
        if (auto c = dynamic_cast<CellView*>(view)) {
            return (c->fieldCell() & fieldCell) == fieldCell;
        }
        return false;
    }

    void setupConnections(AView* view, const _<AAssHelper>& helper) override {
        IAssSubSelector::setupConnections(view, helper);
        view->customCssPropertyChanged.clearAllOutgoingConnectionsWith(helper.get());
        AObject::connect(view->customCssPropertyChanged, AUI_SLOT(helper)::onInvalidateStateAss);
    }
};

Such selector is used in style sheets accordingly:

1
2
3
4
5
6
7
8
9
{
  CellSelector<FieldCell::OPEN>(),
  Border { 1_px, 0xffffff_rgb },
  BackgroundSolid { 0xeeeeee_rgb },
},
{
  CellSelector<FieldCell::HAS_FLAG>(),
  BackgroundImage { ":minesweeper/flag.svg" },
},

Also, for reveal game state (on win/lose) there's an additional selector:

struct RevealSelector : IAssSubSelector {
public:
    bool isPossiblyApplicable(AView* view) override {
        return dynamic_cast<MinesweeperWindow*>(view) != nullptr;
    }

    bool isStateApplicable(AView* view) override {
        if (auto c = dynamic_cast<MinesweeperWindow*>(view)) {
            return c->isReveal();
        }
        return false;
    }

    void setupConnections(AView* view, const _<AAssHelper>& helper) override {
        IAssSubSelector::setupConnections(view, helper);
        view->customCssPropertyChanged.clearAllOutgoingConnectionsWith(helper.get());
        AObject::connect(view->customCssPropertyChanged, AUI_SLOT(helper)::onInvalidateStateAss);
    }
};

RevealSelector and CellSelector are used together to show game results:

{
  RevealSelector {} >> CellSelector<FieldCell::HAS_BOMB>(),
  BackgroundImage { ":minesweeper/bomb.svg" },
},
{
  RevealSelector {} >> CellSelector<FieldCell::HAS_FLAG>(),
  BackgroundImage { ":minesweeper/no_bomb_flag.svg" },
},
{
  RevealSelector {} >> CellSelector<FieldCell::HAS_FLAG | FieldCell::HAS_BOMB>(),
  BackgroundImage { ":minesweeper/bomb_flag.svg" },
},

Source Code#

Repository

CMakeLists.txt#

1
2
3
4
5
6
7
8
if (NOT (AUI_PLATFORM_WIN OR AUI_PLATFORM_LINUX OR AUI_PLATFORM_MACOS))
    return()
endif ()

aui_executable(aui.example.minesweeper)
aui_compile_assets(aui.example.minesweeper)

aui_link(aui.example.minesweeper PRIVATE aui::core aui::views)

src/Style.cpp#

//
// Created by alex2772 on 1/4/21.
//

#include <AUI/View/AButton.h>
#include "CellView.h"
#include "MinesweeperWindow.h"
#include "NewGameWindow.h"
#include <AUI/ASS/ASS.h>

using namespace ass;

/// [CellSelector]
template<FieldCell fieldCell>
struct CellSelector: IAssSubSelector {
public:
    bool isPossiblyApplicable(AView* view) override {
        return dynamic_cast<CellView*>(view) != nullptr;
    }

    bool isStateApplicable(AView* view) override {
        if (auto c = dynamic_cast<CellView*>(view)) {
            return (c->fieldCell() & fieldCell) == fieldCell;
        }
        return false;
    }

    void setupConnections(AView* view, const _<AAssHelper>& helper) override {
        IAssSubSelector::setupConnections(view, helper);
        view->customCssPropertyChanged.clearAllOutgoingConnectionsWith(helper.get());
        AObject::connect(view->customCssPropertyChanged, AUI_SLOT(helper)::onInvalidateStateAss);
    }
};
/// [CellSelector]


/// [RevealSelector]
struct RevealSelector : IAssSubSelector {
public:
    bool isPossiblyApplicable(AView* view) override {
        return dynamic_cast<MinesweeperWindow*>(view) != nullptr;
    }

    bool isStateApplicable(AView* view) override {
        if (auto c = dynamic_cast<MinesweeperWindow*>(view)) {
            return c->isReveal();
        }
        return false;
    }

    void setupConnections(AView* view, const _<AAssHelper>& helper) override {
        IAssSubSelector::setupConnections(view, helper);
        view->customCssPropertyChanged.clearAllOutgoingConnectionsWith(helper.get());
        AObject::connect(view->customCssPropertyChanged, AUI_SLOT(helper)::onInvalidateStateAss);
    }
};
/// [RevealSelector]


struct GlobalStyle {
    GlobalStyle() {
        AStylesheet::global().addRules({
          {
            t<CellView>(),
            FixedSize { 26_dp },
            BackgroundSolid { 0xdedede_rgb },
            Border { 1_px, 0xeaeaea_rgb },
          },
          {
            !RevealSelector{} >> t<CellView>::hover(),
            BackgroundSolid { 0xfdfdfd_rgb },
          },
          /// [open]
          {
            CellSelector<FieldCell::OPEN>(),
            Border { 1_px, 0xffffff_rgb },
            BackgroundSolid { 0xeeeeee_rgb },
          },
          {
            CellSelector<FieldCell::HAS_FLAG>(),
            BackgroundImage { ":minesweeper/flag.svg" },
          },
          /// [open]

          // display mines for dead

          /// [reveal]
          {
            RevealSelector {} >> CellSelector<FieldCell::HAS_BOMB>(),
            BackgroundImage { ":minesweeper/bomb.svg" },
          },
          {
            RevealSelector {} >> CellSelector<FieldCell::HAS_FLAG>(),
            BackgroundImage { ":minesweeper/no_bomb_flag.svg" },
          },
          {
            RevealSelector {} >> CellSelector<FieldCell::HAS_FLAG | FieldCell::HAS_BOMB>(),
            BackgroundImage { ":minesweeper/bomb_flag.svg" },
          },
          /// [reveal]
          {
            CellSelector<FieldCell::RED_BG>(),
            BackgroundSolid { 0xff0000_rgb },
            Border { nullptr },
          },

          // misc
          {
            class_of(".frame"),
            Border { 1_dp, 0x444444_rgb },
          },
          { class_of(".frame") > t<AButton>(), Margin { 4_dp } },
          { t<NewGameWindow>(), Padding { 4_dp } },
        });
    }
} s;

src/NewGameWindow.h#

#pragma once
#include "MinesweeperWindow.h"
#include "AUI/Platform/AWindow.h"
#include "AUI/View/ANumberPicker.h"
#include "AUI/View/ALabel.h"

class NewGameWindow : public AWindow {
public:
    NewGameWindow(MinesweeperWindow* minesweeper);

private:
    MinesweeperWindow* mMinesweeper;
    _<ANumberPicker> mWidth;
    _<ANumberPicker> mHeight;
    _<ANumberPicker> mMines;
    _<ALabel> mDifficultyLabel;

    void updateMinesMax();
    void updateDifficultyLabel();
    void begin();

};

src/CellView.h#

#pragma once
#include "FieldCell.h"
#include "AUI/View/AView.h"

class CellView : public AView {
public:
    CellView(FieldCell& cell);

    void render(ARenderContext context) override;

    [[nodiscard]]
    FieldCell fieldCell() const { return mCell; }

private:
    FieldCell& mCell;
    FieldCell mCellValueCopy;

};

src/NewGameWindow.cpp#

#include "NewGameWindow.h"

#include "AUI/Layout/AGridLayout.h"
#include "AUI/Layout/AHorizontalLayout.h"
#include "AUI/Layout/AVerticalLayout.h"
#include "AUI/View/AButton.h"
#include "AUI/View/ALabel.h"
#include "AUI/View/ANumberPicker.h"
#include "AUI/Util/UIBuildingHelpers.h"

int gWidth = 10;
int gHeight = 10;
int gMines = 10;

void NewGameWindow::updateMinesMax() { mMines->setMax(mWidth->getValue() * mHeight->getValue() - 25); }

void NewGameWindow::updateDifficultyLabel() {
    mMines->setMax(mWidth->getValue() * mHeight->getValue() * 3 / 4);
    int difficulty = mWidth->getValue() * mHeight->getValue() / glm::max(mMines->getValue(), int64_t(1));

    AString text = "Difficulty: ";
    switch (difficulty) {
        default:
        case 0:
        case 1:
            text += "very low";
            break;
        case 2:
        case 3:
            text += "high";
            break;
        case 4:
        case 5:
            text += "medium";
            break;
        case 6:
        case 7:
        case 8:
            text += "low";
            break;
    }
    mDifficultyLabel->setText(text);
}

NewGameWindow::NewGameWindow(MinesweeperWindow* minesweeper)
  : AWindow("New game", 100, 100, minesweeper), mMinesweeper(minesweeper) {
    setWindowStyle(WindowStyle::MODAL);

    setLayout(std::make_unique<AVerticalLayout>());
    setContents(Vertical {
      _form({
        {
          "Cells by width:"_as,
          mWidth = _new<ANumberPicker>() AUI_LET {
                       it->setMin(8);
                       it->setMax(25);
                   },
        },
        {
          "Cells by height:"_as,
          mHeight =
              _new<ANumberPicker>() AUI_LET {
                  it->setMin(8);
                  it->setMax(25);
              },
        },
        {
          "Mines count:"_as,
          mMines = _new<ANumberPicker>() AUI_LET { it->setMin(8); },
        },
      }),
      mDifficultyLabel = _new<ALabel>(),
      Horizontal {
        _new<ASpacerExpanding>(),
        _new<AButton>("Start game") AUI_LET {
                it->setDefault();
                connect(it->clicked, me::begin);
            },
        _new<AButton>("Cancel").connect(&AButton::clicked, me::close),
      },
    });

    mWidth->setValue(gWidth);
    mHeight->setValue(gHeight);

    updateMinesMax();

    mMines->setValue(gMines);

    updateDifficultyLabel();

    connect(mWidth->valueChanging, me::updateMinesMax);
    connect(mWidth->valueChanging, me::updateDifficultyLabel);
    connect(mHeight->valueChanging, me::updateMinesMax);
    connect(mHeight->valueChanging, me::updateDifficultyLabel);
    connect(mMines->valueChanging, me::updateDifficultyLabel);

    pack();
}

void NewGameWindow::begin() {
    close();
    mMinesweeper->beginGame(gWidth = mWidth->getValue(), gHeight = mHeight->getValue(), gMines = mMines->getValue());
}

src/MinesweeperWindow.cpp#

#include "MinesweeperWindow.h"

#include <AUI/Util/UIBuildingHelpers.h>
#include "CellView.h"
#include "NewGameWindow.h"
#include "AUI/Platform/AMessageBox.h"
#include "AUI/Util/ARandom.h"

MinesweeperWindow::MinesweeperWindow() : AWindow("Minesweeper", 100_dp, 100_dp) {
    setContents(Vertical {
      Horizontal {
        Centered::Expanding {
          _new<AButton>("New game...").connect(&AButton::clicked, me::newGame),
        },
      },
      _container<AStackedLayout>(
          { // also assign ".frame" ASS class in place
            mGrid = _new<AViewContainer>() << ".frame" }) });

    beginGame(10, 10, 20);
}

void MinesweeperWindow::openCell(int x, int y, bool doGameLoseIfBomb) {
    if (mReveal) {
        // beginGame(16, 10);
        return;
    }
    FieldCell& c = fieldAt(x, y);

    if (!mBombsPlanted) {
        for (int i = -1; i <= 1; ++i) {
            for (int j = -1; j <= 1; ++j) {
                if (isValidCell(x + i, y + j))
                    fieldAt(x + i, y + j) |= FieldCell::DONT_PLANT_BOMB_HERE;
            }
        }

        mBombsPlanted = true;

        ARandom r;
        for (int i = 0; i < mBombs;) {
            int x = r.nextInt() % mFieldColumns;
            int y = r.nextInt() % mFieldRows;

            if (fieldAt(x, y) == FieldCell::EMPTY) {
                fieldAt(x, y) |= FieldCell::HAS_BOMB;
                ++i;
            }
        }
    } else if (bool(c & (FieldCell::OPEN | FieldCell::HAS_FLAG))) {
        return;
    }

    if (bool(c & FieldCell::HAS_BOMB)) {
        if (doGameLoseIfBomb) {
            c |= FieldCell::RED_BG;
            mReveal = true;
            emit customCssPropertyChanged();
            redraw();
            AMessageBox::show(this, "You lost!", "You lost! Ahahahhaa!");
        }
        return;
    }
    c |= FieldCell::OPEN;
    mOpenedCells += 1;

    int bombCount = countBombsAround(x, y);
    c |= FieldCell(bombCount << 16);
    updateCellViewStyle(x, y);

    if (mOpenedCells + mBombs == mFieldRows * mFieldColumns) {
        mReveal = true;
        emit customCssPropertyChanged();
        redraw();
        AMessageBox::show(this, "You won!", "Respect +");
    }

    if (bombCount == 0) {
        for (int i = -1; i <= 1; ++i) {
            for (int j = -1; j <= 1; ++j) {
                if (!(i == 0 && j == 0)) {
                    int cellX = x + i;
                    int cellY = y + j;
                    if (isValidCell(cellX, cellY)) {
                        openCell(cellX, cellY, false);
                    }
                }
            }
        }
    }
}
void MinesweeperWindow::updateCellViewStyle(int x, int y) const {
    AUI_EMIT_FOREIGN(mGrid->getViews()[y * mFieldColumns + x], customCssPropertyChanged);
}

int MinesweeperWindow::countBombsAround(int x, int y) {
    int count = 0;
    for (int i = -1; i <= 1; ++i) {
        for (int j = -1; j <= 1; ++j) {
            if (!(i == 0 && j == 0)) {
                int cellX = x + i;
                int cellY = y + j;

                if (isValidCell(cellX, cellY)) {
                    if (bool(fieldAt(cellX, cellY) & FieldCell::HAS_BOMB)) {
                        count += 1;
                    }
                }
            }
        }
    }
    return count;
}

void MinesweeperWindow::newGame() { _new<NewGameWindow>(this)->show(); }


/// [beginGame]
void MinesweeperWindow::beginGame(int columns, int rows, int bombs) {
    mOpenedCells = 0;
    mBombs = bombs;
    mFieldColumns = columns;
    mFieldRows = rows;
    mReveal = false;
    mBombsPlanted = false;
    mGrid->setLayout(std::make_unique<AGridLayout>(columns, rows));
    mField.clear();
    mField.resize(columns * rows);

    for (int i = 0; i < columns * rows; ++i) {
        int x = i % columns;
        int y = i / columns;
        auto cell = _new<CellView>(fieldAt(x, y));

        setupEventHandlers(x, y, cell);
        mGrid->addView(cell);
    }

    pack();
}
/// [beginGame]

void MinesweeperWindow::setupEventHandlers(int x, int y, const _<CellView>& cell) {
    /// [clicked]
    connect(cell->clicked, this, [&, x, y]() {
        if (mReveal) {
            return;
        }
        openCell(x, y, true);
    });
    /// [clicked]

    /// [clickedRight]
    connect(cell->clickedRight, this, [&, x, y]() {
        if (mReveal) {
            return;
        }
        auto& c = fieldAt(x, y);
        if (!(c & FieldCell::OPEN)) {
            c ^= FieldCell::HAS_FLAG;
            updateCellViewStyle(x, y);
        }
    });
    /// [clickedRight]

}

src/MinesweeperWindow.h#

#pragma once

#include "FieldCell.h"
#include "AUI/Platform/ACustomCaptionWindow.h"
#include "CellView.h"

class MinesweeperWindow : public AWindow {
public:
    void beginGame(int columns, int rows, int bombs);
    MinesweeperWindow();

    [[nodiscard]]
    bool isReveal() const { return mReveal; }

private:
    int mFieldColumns;
    int mFieldRows;
    bool mBombsPlanted = false;
    int mBombs;
    int mOpenedCells;

    _<AViewContainer> mGrid;
    AVector<FieldCell> mField;

    void openCell(int x, int y, bool doGameLoseIfBomb);
    int countBombsAround(int x, int y);
    bool isValidCell(int x, int y) { return x >= 0 && x < mFieldColumns && y >= 0 && y < mFieldRows; }
    FieldCell& fieldAt(int x, int y) { return mField[mFieldColumns * y + x]; }

    bool mReveal = false;

    void newGame();

    void updateCellViewStyle(int x, int y) const;
    void setupEventHandlers(int x, int y, const _<CellView>& cell);
};

src/CellView.cpp#

#include "CellView.h"

#include "AUI/Render/IRenderer.h"

CellView::CellView(FieldCell& cell) : mCell(cell), mCellValueCopy(cell) {
    connect(clickedButton, this, [&]() {
        emit customCssPropertyChanged();
    });
}

void CellView::render(ARenderContext context) {
    if (mCell != mCellValueCopy) {
        mCellValueCopy = mCell;
    }
    AView::render(context);

    if (bool(mCell & FieldCell::OPEN)) {
        int count = field_cell::getBombCountAround(mCell);
        if (count) {
            AFontStyle fs;
            fs.size = getHeight() * 6 / 7;
            fs.align = ATextAlign::CENTER;
            auto color = AColor::BLACK;

            switch (count) {
                case 1:
                    color = 0x0000ffffu;
                    break;
                case 2:
                    color = 0x008000ffu;
                    break;
                case 3:
                    color = 0xff0000ffu;
                    break;
                case 4:
                    color = 0x000080ffu;
                    break;
                case 5:
                    color = 0x800000ffu;
                    break;
                case 6:
                    color = 0x008080ffu;
                    break;
                case 7:
                    color = 0x000000ffu;
                    break;
                case 8:
                    color = 0x808080ffu;
                    break;
            }

            context.render.setColor(color);
            context.render.string({getWidth() / 3, (getHeight() - fs.size) / 2}, AString::number(count), fs);
        }
    }
}

src/main.cpp#

1
2
3
4
5
6
7
8
#include <AUI/Platform/Entry.h>
#include "MinesweeperWindow.h"

AUI_ENTRY {
    _new<MinesweeperWindow>()->show();

    return 0;
}

src/FieldCell.h#

#pragma once

#include "AUI/Reflect/AEnumerate.h"

/// [FieldCell]
AUI_ENUM_FLAG(FieldCell) {
    EMPTY = 0,
    HAS_BOMB = 1,
    OPEN = 2,
    HAS_FLAG = 4,
    RED_BG = 8,
    DONT_PLANT_BOMB_HERE = 16,
};
/// [FieldCell]

namespace field_cell {
    inline uint16_t getBombCountAround(FieldCell fc) {
        return int(fc) >> 16;
    }

    inline void setBombCountAround(FieldCell& fc, uint16_t count) {
        reinterpret_cast<std::underlying_type_t<FieldCell>&>(fc) &= 0xffff;
        reinterpret_cast<std::underlying_type_t<FieldCell>&>(fc) |= int(count) << 16;
    }
}