updates and fixes
This commit is contained in:
10
meson.build
10
meson.build
@@ -11,16 +11,20 @@ res = gnome.compile_resources(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
deps = dependency(['gtkmm-4.0', 'sigc++-3.0'])
|
deps = [
|
||||||
|
dependency('gtkmm-4.0'),
|
||||||
|
dependency('sigc++-3.0')
|
||||||
|
]
|
||||||
|
|
||||||
# Source files
|
# Source files
|
||||||
src = [
|
src = [
|
||||||
|
'src/main.cpp',
|
||||||
'src/window.cpp',
|
'src/window.cpp',
|
||||||
'src/window.hpp',
|
'src/window.hpp',
|
||||||
'src/minefield.hpp',
|
'src/minefield.hpp',
|
||||||
'src/minefield.cpp',
|
'src/minefield.cpp',
|
||||||
'src/timer.hpp',
|
'src/board_widget.hpp',
|
||||||
'src/timer.cpp',
|
'src/board_widget.cpp',
|
||||||
res
|
res
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,382 +1,236 @@
|
|||||||
#include "minefield.hpp"
|
#include "minefield.hpp"
|
||||||
#include <ctime>
|
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iostream>
|
#include <queue>
|
||||||
|
|
||||||
// Define difficulty presets
|
const GameDifficulty Minefield::DifficultyEasy = {"Beginner", 9, 9, 10};
|
||||||
const GameDifficulty MineField::DIFFICULTY_EASY = {"Beginner", 9, 9, 10};
|
const GameDifficulty Minefield::DifficultyMedium = {"Intermediate", 16, 16, 40};
|
||||||
const GameDifficulty MineField::DIFFICULTY_MEDIUM = {"Intermediate", 16, 16, 40};
|
const GameDifficulty Minefield::DifficultyHard = {"Expert", 30, 16, 99};
|
||||||
const GameDifficulty MineField::DIFFICULTY_HARD = {"Expert", 30, 16, 99};
|
const GameDifficulty Minefield::DifficultyExpert = {"Master", 30, 20, 145};
|
||||||
const GameDifficulty MineField::DIFFICULTY_EXPERT = {"Master", 30, 20, 145};
|
|
||||||
|
|
||||||
MineField::MineField(int cols, int rows, int mines) :
|
Minefield::Minefield(int cols, int rows, int mines)
|
||||||
m_rows(rows),
|
: cols_(cols), rows_(rows), total_mines_(mines), remaining_flags_(mines) {
|
||||||
m_cols(cols),
|
cells_.resize(cols * rows);
|
||||||
m_totalMines(mines),
|
|
||||||
m_remainingFlags(mines),
|
|
||||||
m_openCells(0),
|
|
||||||
m_gameState(GameState::READY),
|
|
||||||
m_timerRunning(false)
|
|
||||||
{
|
|
||||||
// Create cells
|
|
||||||
m_cells.reserve(m_cols * m_rows);
|
|
||||||
for (int i = 0; i < m_cols * m_rows; i++)
|
|
||||||
{
|
|
||||||
std::shared_ptr<Cell> cell = std::make_shared<Cell>();
|
|
||||||
m_cells.push_back(cell);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MineField::~MineField()
|
const Cell& Minefield::get_cell(int x, int y) const {
|
||||||
{
|
static const Cell invalid_cell;
|
||||||
m_cells.clear();
|
if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return invalid_cell;
|
||||||
|
return cells_[y * cols_ + x];
|
||||||
}
|
}
|
||||||
|
|
||||||
void MineField::startTimer()
|
std::chrono::milliseconds Minefield::get_elapsed_time() const {
|
||||||
{
|
if (state_ == GameState::Ready) return std::chrono::milliseconds(0);
|
||||||
if (!m_timerRunning)
|
auto end = (state_ == GameState::Playing) ? std::chrono::steady_clock::now() : end_time_;
|
||||||
{
|
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start_time_);
|
||||||
m_startTime = std::chrono::steady_clock::now();
|
|
||||||
m_timerRunning = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MineField::stopTimer()
|
void Minefield::generate_mines(int safe_x, int safe_y) {
|
||||||
{
|
std::vector<int> indices(cells_.size());
|
||||||
if (m_timerRunning)
|
std::iota(indices.begin(), indices.end(), 0);
|
||||||
{
|
|
||||||
m_endTime = std::chrono::steady_clock::now();
|
|
||||||
m_timerRunning = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int MineField::getElapsedTime() const
|
// Remove safe zone (3x3 around click)
|
||||||
{
|
indices.erase(std::remove_if(indices.begin(), indices.end(), [&](int idx) {
|
||||||
if (m_timerRunning)
|
int cx = idx % cols_;
|
||||||
{
|
int cy = idx / cols_;
|
||||||
auto now = std::chrono::steady_clock::now();
|
return std::abs(cx - safe_x) <= 1 && std::abs(cy - safe_y) <= 1;
|
||||||
return std::chrono::duration_cast<std::chrono::milliseconds>(now - m_startTime).count();
|
}), indices.end());
|
||||||
}
|
|
||||||
else if (m_gameState == GameState::WON || m_gameState == GameState::LOST)
|
|
||||||
{
|
|
||||||
return std::chrono::duration_cast<std::chrono::milliseconds>(m_endTime - m_startTime).count();
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MineField::timerTick()
|
|
||||||
{
|
|
||||||
if (m_timerRunning)
|
|
||||||
{
|
|
||||||
timerSignal.emit(getElapsedTime());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MineField::reset()
|
|
||||||
{
|
|
||||||
// Reset all cells
|
|
||||||
for (auto& cell : m_cells)
|
|
||||||
{
|
|
||||||
cell->isBomb = false;
|
|
||||||
cell->isFlagged = false;
|
|
||||||
cell->isCleared = false;
|
|
||||||
cell->bombsNearby = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset game state
|
|
||||||
m_openCells = 0;
|
|
||||||
m_remainingFlags = m_totalMines;
|
|
||||||
m_gameState = GameState::READY;
|
|
||||||
m_timerRunning = false;
|
|
||||||
|
|
||||||
// Emit signals
|
|
||||||
resetSignal.emit();
|
|
||||||
remainingFlagsSignal.emit(m_remainingFlags);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MineField::startNewGame(int cols, int rows, int mines)
|
|
||||||
{
|
|
||||||
// Store new dimensions
|
|
||||||
m_cols = cols;
|
|
||||||
m_rows = rows;
|
|
||||||
m_totalMines = mines;
|
|
||||||
m_remainingFlags = mines;
|
|
||||||
|
|
||||||
// Create new cells
|
|
||||||
m_cells.clear();
|
|
||||||
m_cells.reserve(m_cols * m_rows);
|
|
||||||
for (int i = 0; i < m_cols * m_rows; i++)
|
|
||||||
{
|
|
||||||
std::shared_ptr<Cell> cell = std::make_shared<Cell>();
|
|
||||||
m_cells.push_back(cell);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset game state
|
|
||||||
m_openCells = 0;
|
|
||||||
m_gameState = GameState::READY;
|
|
||||||
m_timerRunning = false;
|
|
||||||
|
|
||||||
// Emit signals
|
|
||||||
resetSignal.emit();
|
|
||||||
remainingFlagsSignal.emit(m_remainingFlags);
|
|
||||||
}
|
|
||||||
|
|
||||||
void MineField::initBombs(int firstX, int firstY)
|
|
||||||
{
|
|
||||||
if (m_gameState != GameState::READY)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start timer when first cell is clicked
|
|
||||||
startTimer();
|
|
||||||
m_gameState = GameState::PLAYING;
|
|
||||||
|
|
||||||
// Create a vector of all possible positions
|
|
||||||
std::vector<int> positions;
|
|
||||||
positions.reserve(m_cols * m_rows);
|
|
||||||
|
|
||||||
for (int i = 0; i < m_cols * m_rows; i++)
|
|
||||||
{
|
|
||||||
positions.push_back(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove first clicked position and surrounding cells from available positions
|
|
||||||
positions.erase(
|
|
||||||
std::remove_if(positions.begin(), positions.end(),
|
|
||||||
[this, firstX, firstY](int pos) {
|
|
||||||
int x = pos % m_cols;
|
|
||||||
int y = pos / m_cols;
|
|
||||||
return std::abs(x - firstX) <= 1 && std::abs(y - firstY) <= 1;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
positions.end()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use modern random generator
|
|
||||||
std::random_device rd;
|
std::random_device rd;
|
||||||
std::mt19937 gen(rd());
|
std::mt19937 g(rd());
|
||||||
|
std::shuffle(indices.begin(), indices.end(), g);
|
||||||
|
|
||||||
// Shuffle positions
|
int placed = 0;
|
||||||
std::shuffle(positions.begin(), positions.end(), gen);
|
for (int idx : indices) {
|
||||||
|
if (placed >= total_mines_) break;
|
||||||
|
cells_[idx].is_bomb = true;
|
||||||
|
placed++;
|
||||||
|
}
|
||||||
|
|
||||||
// Place mines
|
// Precompute neighbors
|
||||||
int minesToPlace = std::min(m_totalMines, static_cast<int>(positions.size()));
|
for (int y = 0; y < rows_; ++y) {
|
||||||
for (int i = 0; i < minesToPlace; i++)
|
for (int x = 0; x < cols_; ++x) {
|
||||||
{
|
if (!cells_[y * cols_ + x].is_bomb) {
|
||||||
m_cells.at(positions[i])->isBomb = true;
|
cells_[y * cols_ + x].nearby_bombs = count_nearby_bombs(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MineField::openCell(int x, int y)
|
int Minefield::count_nearby_bombs(int x, int y) const {
|
||||||
{
|
int count = 0;
|
||||||
// Ignore if game is over or cell is already open/flagged
|
for (int dy = -1; dy <= 1; ++dy) {
|
||||||
if (m_gameState != GameState::PLAYING && m_gameState != GameState::READY)
|
for (int dx = -1; dx <= 1; ++dx) {
|
||||||
{
|
if (dx == 0 && dy == 0) continue;
|
||||||
return false;
|
int nx = x + dx;
|
||||||
|
int ny = y + dy;
|
||||||
|
if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) {
|
||||||
|
if (cells_[ny * cols_ + nx].is_bomb) count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOpened(x, y) || isFlagged(x, y))
|
int Minefield::count_nearby_flags(int x, int y) const {
|
||||||
{
|
int count = 0;
|
||||||
return false;
|
for (int dy = -1; dy <= 1; ++dy) {
|
||||||
|
for (int dx = -1; dx <= 1; ++dx) {
|
||||||
|
if (dx == 0 && dy == 0) continue;
|
||||||
|
int nx = x + dx;
|
||||||
|
int ny = y + dy;
|
||||||
|
if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) {
|
||||||
|
if (cells_[ny * cols_ + nx].is_flagged) count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start timer on first click if not already started
|
bool Minefield::open_cell(int x, int y) {
|
||||||
if (m_gameState == GameState::READY)
|
if (state_ == GameState::Won || state_ == GameState::Lost) return false;
|
||||||
{
|
if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return false;
|
||||||
initBombs(x, y);
|
|
||||||
|
Cell& cell = cells_[y * cols_ + x];
|
||||||
|
if (cell.is_flagged || cell.is_revealed) return false;
|
||||||
|
|
||||||
|
if (state_ == GameState::Ready) {
|
||||||
|
start_time_ = std::chrono::steady_clock::now();
|
||||||
|
state_ = GameState::Playing;
|
||||||
|
generate_mines(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if bomb
|
if (cell.is_bomb) {
|
||||||
if (isBomb(x, y))
|
cell.is_exploded = true;
|
||||||
{
|
cell.is_revealed = true;
|
||||||
// Only emit the game over signal if we haven't already
|
state_ = GameState::Lost;
|
||||||
if (m_gameState != GameState::LOST) {
|
end_time_ = std::chrono::steady_clock::now();
|
||||||
m_gameState = GameState::LOST;
|
|
||||||
stopTimer();
|
|
||||||
gameOverSignal.emit();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open cell
|
// Reveal all other bombs
|
||||||
setOpenCell(x, y);
|
for (auto& c : cells_) {
|
||||||
|
if (c.is_bomb && !c.is_flagged) c.is_revealed = true;
|
||||||
// If no bombs nearby, open surrounding cells
|
|
||||||
if (bombsNearby(x, y) == 0)
|
|
||||||
{
|
|
||||||
openNeighboorhood(x, y);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MineField::computeBombsNearby(int x, int y)
|
reveal_recursive(x, y);
|
||||||
{
|
check_win_condition();
|
||||||
int total = 0;
|
|
||||||
// Check all 8 neighboring cells
|
|
||||||
for (int i = -1; i <= 1; i++)
|
|
||||||
{
|
|
||||||
for (int j = -1; j <= 1; j++)
|
|
||||||
{
|
|
||||||
// Skip the cell itself
|
|
||||||
if (i == 0 && j == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int nx = x + i;
|
|
||||||
int ny = y + j;
|
|
||||||
|
|
||||||
// Check if within bounds
|
|
||||||
if (nx >= 0 && nx < m_cols && ny >= 0 && ny < m_rows)
|
|
||||||
{
|
|
||||||
if (isBomb(nx, ny))
|
|
||||||
{
|
|
||||||
++total;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_cells.at(x + y * m_cols)->bombsNearby = total;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MineField::openNeighboorhood(int x, int y)
|
|
||||||
{
|
|
||||||
// Check all 8 neighboring cells
|
|
||||||
for (int i = -1; i <= 1; i++)
|
|
||||||
{
|
|
||||||
for (int j = -1; j <= 1; j++)
|
|
||||||
{
|
|
||||||
// Skip the cell itself
|
|
||||||
if (i == 0 && j == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int nx = x + i;
|
|
||||||
int ny = y + j;
|
|
||||||
|
|
||||||
// Check if within bounds
|
|
||||||
if (nx >= 0 && nx < m_cols && ny >= 0 && ny < m_rows)
|
|
||||||
{
|
|
||||||
// If cell is not opened and not a bomb, open it
|
|
||||||
if (!isOpened(nx, ny) && !isBomb(nx, ny))
|
|
||||||
{
|
|
||||||
setOpenCell(nx, ny);
|
|
||||||
|
|
||||||
// If no bombs nearby, recursively open surrounding cells
|
|
||||||
if (bombsNearby(nx, ny) == 0)
|
|
||||||
{
|
|
||||||
openNeighboorhood(nx, ny);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MineField::isOpened(int x, int y)
|
|
||||||
{
|
|
||||||
return m_cells.at(x + y * m_cols)->isCleared;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MineField::isFlagged(int x, int y)
|
|
||||||
{
|
|
||||||
return m_cells.at(x + y * m_cols)->isFlagged;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MineField::isBomb(int x, int y)
|
|
||||||
{
|
|
||||||
return m_cells.at(x + y * m_cols)->isBomb;
|
|
||||||
}
|
|
||||||
|
|
||||||
int MineField::bombsNearby(int x, int y)
|
|
||||||
{
|
|
||||||
// Calculate bombs nearby if not already calculated
|
|
||||||
if (m_cells.at(x + y * m_cols)->bombsNearby == -1)
|
|
||||||
{
|
|
||||||
computeBombsNearby(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
return m_cells.at(x + y * m_cols)->bombsNearby;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MineField::setOpenCell(int x, int y)
|
|
||||||
{
|
|
||||||
m_cells.at(x + y * m_cols)->isCleared = true;
|
|
||||||
openCellSignal.emit(x, y);
|
|
||||||
++m_openCells;
|
|
||||||
checkGameWon();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MineField::checkGameWon()
|
|
||||||
{
|
|
||||||
// Win condition: All non-bomb cells are opened
|
|
||||||
if (m_openCells == (m_cols * m_rows - m_totalMines) && m_gameState == GameState::PLAYING)
|
|
||||||
{
|
|
||||||
m_gameState = GameState::WON;
|
|
||||||
stopTimer();
|
|
||||||
|
|
||||||
// Auto-flag all remaining bombs
|
|
||||||
for (int i = 0; i < m_cols * m_rows; i++)
|
|
||||||
{
|
|
||||||
int x = i % m_cols;
|
|
||||||
int y = i / m_cols;
|
|
||||||
|
|
||||||
if (isBomb(x, y) && !isFlagged(x, y))
|
|
||||||
{
|
|
||||||
m_cells.at(i)->isFlagged = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m_remainingFlags = 0;
|
|
||||||
remainingFlagsSignal.emit(m_remainingFlags);
|
|
||||||
|
|
||||||
// Emit game won signal with elapsed time
|
|
||||||
gameWonSignal.emit(getElapsedTime());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MineField::toggleFlag(int x, int y)
|
|
||||||
{
|
|
||||||
// Ignore if game is over or cell is already open
|
|
||||||
if (m_gameState != GameState::PLAYING && m_gameState != GameState::READY)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOpened(x, y))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_gameState == GameState::READY)
|
|
||||||
{
|
|
||||||
// Start timer on first action
|
|
||||||
startTimer();
|
|
||||||
m_gameState = GameState::PLAYING;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle flag state
|
|
||||||
if (m_cells.at(x + y * m_cols)->isFlagged)
|
|
||||||
{
|
|
||||||
m_cells.at(x + y * m_cols)->isFlagged = false;
|
|
||||||
++m_remainingFlags;
|
|
||||||
}
|
|
||||||
else if (m_remainingFlags > 0)
|
|
||||||
{
|
|
||||||
m_cells.at(x + y * m_cols)->isFlagged = true;
|
|
||||||
--m_remainingFlags;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// No remaining flags
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingFlagsSignal.emit(m_remainingFlags);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Minefield::reveal_recursive(int x, int y) {
|
||||||
|
// Non-recursive BFS to avoid stack overflow
|
||||||
|
std::vector<std::pair<int, int>> q;
|
||||||
|
q.reserve(cols_ * rows_);
|
||||||
|
q.push_back({x, y});
|
||||||
|
|
||||||
|
// Mark initial as revealed
|
||||||
|
if (!cells_[y * cols_ + x].is_revealed) {
|
||||||
|
cells_[y * cols_ + x].is_revealed = true;
|
||||||
|
revealed_count_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t head = 0;
|
||||||
|
while(head < q.size()){
|
||||||
|
auto [cx, cy] = q[head++];
|
||||||
|
Cell& current = cells_[cy * cols_ + cx];
|
||||||
|
|
||||||
|
if (current.nearby_bombs == 0) {
|
||||||
|
for (int dy = -1; dy <= 1; ++dy) {
|
||||||
|
for (int dx = -1; dx <= 1; ++dx) {
|
||||||
|
if (dx == 0 && dy == 0) continue;
|
||||||
|
int nx = cx + dx;
|
||||||
|
int ny = cy + dy;
|
||||||
|
if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) {
|
||||||
|
Cell& neighbor = cells_[ny * cols_ + nx];
|
||||||
|
if (!neighbor.is_revealed && !neighbor.is_flagged) {
|
||||||
|
neighbor.is_revealed = true;
|
||||||
|
revealed_count_++;
|
||||||
|
q.push_back({nx, ny});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Minefield::toggle_flag(int x, int y) {
|
||||||
|
if (state_ == GameState::Won || state_ == GameState::Lost) return false;
|
||||||
|
if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return false;
|
||||||
|
|
||||||
|
Cell& cell = cells_[y * cols_ + x];
|
||||||
|
if (cell.is_revealed) return false;
|
||||||
|
|
||||||
|
if (cell.is_flagged) {
|
||||||
|
cell.is_flagged = false;
|
||||||
|
remaining_flags_++;
|
||||||
|
} else {
|
||||||
|
if (remaining_flags_ > 0) {
|
||||||
|
cell.is_flagged = true;
|
||||||
|
remaining_flags_--;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Minefield::chord_cell(int x, int y) {
|
||||||
|
if (state_ != GameState::Playing) return false;
|
||||||
|
if (x < 0 || x >= cols_ || y < 0 || y >= rows_) return false;
|
||||||
|
|
||||||
|
Cell& cell = cells_[y * cols_ + x];
|
||||||
|
if (!cell.is_revealed) return false;
|
||||||
|
if (cell.nearby_bombs == 0) return false;
|
||||||
|
|
||||||
|
if (count_nearby_flags(x, y) == cell.nearby_bombs) {
|
||||||
|
bool changed = false;
|
||||||
|
for (int dy = -1; dy <= 1; ++dy) {
|
||||||
|
for (int dx = -1; dx <= 1; ++dx) {
|
||||||
|
if (dx == 0 && dy == 0) continue;
|
||||||
|
int nx = x + dx;
|
||||||
|
int ny = y + dy;
|
||||||
|
if (nx >= 0 && nx < cols_ && ny >= 0 && ny < rows_) {
|
||||||
|
Cell& neighbor = cells_[ny * cols_ + nx];
|
||||||
|
if (!neighbor.is_revealed && !neighbor.is_flagged) {
|
||||||
|
// Standard open logic
|
||||||
|
if (neighbor.is_bomb) {
|
||||||
|
neighbor.is_exploded = true;
|
||||||
|
neighbor.is_revealed = true;
|
||||||
|
state_ = GameState::Lost;
|
||||||
|
end_time_ = std::chrono::steady_clock::now();
|
||||||
|
// Reveal all bombs
|
||||||
|
for (auto& c : cells_) {
|
||||||
|
if (c.is_bomb && !c.is_flagged) c.is_revealed = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
reveal_recursive(nx, ny);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check_win_condition();
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Minefield::check_win_condition() {
|
||||||
|
if (state_ != GameState::Playing) return;
|
||||||
|
|
||||||
|
// Win if all non-bomb cells are revealed
|
||||||
|
int safe_cells = (cols_ * rows_) - total_mines_;
|
||||||
|
if (revealed_count_ == safe_cells) {
|
||||||
|
state_ = GameState::Won;
|
||||||
|
end_time_ = std::chrono::steady_clock::now();
|
||||||
|
remaining_flags_ = 0; // Visually satisfy flags
|
||||||
|
// Flag all unflagged bombs
|
||||||
|
for (auto& c : cells_) {
|
||||||
|
if (c.is_bomb) c.is_flagged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <memory>
|
|
||||||
#include <sigc++/signal.h>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <string>
|
#include <random>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
// Game difficulty presets
|
|
||||||
struct GameDifficulty {
|
struct GameDifficulty {
|
||||||
std::string name;
|
std::string name;
|
||||||
int cols;
|
int cols;
|
||||||
@@ -15,104 +13,64 @@ struct GameDifficulty {
|
|||||||
int mines;
|
int mines;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Game score for leaderboard
|
|
||||||
struct GameScore {
|
|
||||||
std::string playerName;
|
|
||||||
std::string difficulty;
|
|
||||||
int time;
|
|
||||||
std::string date;
|
|
||||||
|
|
||||||
bool operator<(const GameScore& other) const {
|
|
||||||
return time < other.time;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class MineField
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
enum class GameState {
|
enum class GameState {
|
||||||
READY,
|
Ready,
|
||||||
PLAYING,
|
Playing,
|
||||||
WON,
|
Won,
|
||||||
LOST
|
Lost
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct Cell {
|
||||||
|
bool is_bomb : 1 = false;
|
||||||
|
bool is_flagged : 1 = false;
|
||||||
|
bool is_revealed : 1 = false;
|
||||||
|
bool is_exploded : 1 = false; // The specific bomb that killed you
|
||||||
|
uint8_t nearby_bombs : 4 = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Minefield {
|
||||||
|
public:
|
||||||
|
Minefield(int cols, int rows, int mines);
|
||||||
|
|
||||||
|
// Core actions
|
||||||
|
// Returns true if state changed
|
||||||
|
bool open_cell(int x, int y);
|
||||||
|
bool toggle_flag(int x, int y);
|
||||||
|
bool chord_cell(int x, int y);
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
int cols() const { return cols_; }
|
||||||
|
int rows() const { return rows_; }
|
||||||
|
int total_mines() const { return total_mines_; }
|
||||||
|
int remaining_flags() const { return remaining_flags_; }
|
||||||
|
GameState state() const { return state_; }
|
||||||
|
const Cell& get_cell(int x, int y) const;
|
||||||
|
|
||||||
|
// Timing
|
||||||
|
std::chrono::milliseconds get_elapsed_time() const;
|
||||||
|
|
||||||
|
// Difficulty presets
|
||||||
|
static const GameDifficulty DifficultyEasy;
|
||||||
|
static const GameDifficulty DifficultyMedium;
|
||||||
|
static const GameDifficulty DifficultyHard;
|
||||||
|
static const GameDifficulty DifficultyExpert;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Cell
|
void generate_mines(int safe_x, int safe_y);
|
||||||
{
|
void reveal_recursive(int x, int y);
|
||||||
bool isFlagged = false;
|
void check_win_condition();
|
||||||
bool isCleared = false;
|
int count_nearby_flags(int x, int y) const;
|
||||||
bool isBomb = false;
|
int count_nearby_bombs(int x, int y) const;
|
||||||
int bombsNearby = -1;
|
|
||||||
};
|
int cols_;
|
||||||
|
int rows_;
|
||||||
std::vector<std::shared_ptr<Cell>> m_cells;
|
int total_mines_;
|
||||||
int m_rows;
|
int remaining_flags_;
|
||||||
int m_cols;
|
int revealed_count_ = 0;
|
||||||
int m_totalMines;
|
|
||||||
int m_remainingFlags;
|
GameState state_ = GameState::Ready;
|
||||||
int m_openCells;
|
std::vector<Cell> cells_;
|
||||||
GameState m_gameState;
|
|
||||||
std::chrono::time_point<std::chrono::steady_clock> m_startTime;
|
std::chrono::steady_clock::time_point start_time_;
|
||||||
std::chrono::time_point<std::chrono::steady_clock> m_endTime;
|
std::chrono::steady_clock::time_point end_time_;
|
||||||
bool m_timerRunning;
|
|
||||||
|
|
||||||
void computeBombsNearby(int x, int y);
|
|
||||||
void openNeighboorhood(int x, int y);
|
|
||||||
void setOpenCell(int x, int y);
|
|
||||||
void checkGameWon();
|
|
||||||
void startTimer();
|
|
||||||
void stopTimer();
|
|
||||||
|
|
||||||
public:
|
|
||||||
MineField(int cols, int rows, int mines);
|
|
||||||
~MineField();
|
|
||||||
|
|
||||||
void initBombs(int x, int y);
|
|
||||||
bool isBomb(int x, int y);
|
|
||||||
bool isFlagged(int x, int y);
|
|
||||||
bool isOpened(int x, int y);
|
|
||||||
bool openCell(int x, int y);
|
|
||||||
int bombsNearby(int x, int y);
|
|
||||||
|
|
||||||
GameState getGameState() const { return m_gameState; }
|
|
||||||
int getCols() const { return m_cols; }
|
|
||||||
int getRows() const { return m_rows; }
|
|
||||||
bool toggleFlag(int x, int y);
|
|
||||||
int getRemainingFlags() const { return m_remainingFlags; }
|
|
||||||
int getTotalMines() const { return m_totalMines; }
|
|
||||||
int getOpenCells() const { return m_openCells; }
|
|
||||||
|
|
||||||
void reset();
|
|
||||||
void startNewGame(int cols, int rows, int mines);
|
|
||||||
|
|
||||||
// Get elapsed time in milliseconds
|
|
||||||
int getElapsedTime() const;
|
|
||||||
|
|
||||||
// Timer tick (for UI updates)
|
|
||||||
void timerTick();
|
|
||||||
|
|
||||||
// Signal when game is reset
|
|
||||||
sigc::signal<void()> resetSignal;
|
|
||||||
|
|
||||||
// Signal when a cell is opened
|
|
||||||
sigc::signal<void(int, int)> openCellSignal;
|
|
||||||
|
|
||||||
// Signal when flags count changes
|
|
||||||
sigc::signal<void(int)> remainingFlagsSignal;
|
|
||||||
|
|
||||||
// Signal when game is won
|
|
||||||
sigc::signal<void(int)> gameWonSignal; // int parameter is elapsed time in ms
|
|
||||||
|
|
||||||
// Signal when game is lost
|
|
||||||
sigc::signal<void()> gameOverSignal;
|
|
||||||
|
|
||||||
// Signal for timer updates
|
|
||||||
sigc::signal<void(int)> timerSignal; // int parameter is elapsed time in ms
|
|
||||||
|
|
||||||
// Predefined difficulty levels
|
|
||||||
static const GameDifficulty DIFFICULTY_EASY;
|
|
||||||
static const GameDifficulty DIFFICULTY_MEDIUM;
|
|
||||||
static const GameDifficulty DIFFICULTY_HARD;
|
|
||||||
static const GameDifficulty DIFFICULTY_EXPERT;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
#include "timer.hpp"
|
|
||||||
#include <iostream>
|
|
||||||
#include <glibmm/main.h>
|
|
||||||
|
|
||||||
Timer::Timer() : m_running(false) {
|
|
||||||
reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer::~Timer() {
|
|
||||||
stop();
|
|
||||||
|
|
||||||
// Wait for timer thread to finish
|
|
||||||
if (m_timerThread.joinable()) {
|
|
||||||
m_timerThread.join();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Timer::start() {
|
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
|
||||||
|
|
||||||
if (!m_running) {
|
|
||||||
m_running = true;
|
|
||||||
m_startTime = std::chrono::steady_clock::now();
|
|
||||||
|
|
||||||
// Start timer thread if not already running
|
|
||||||
if (!m_timerThread.joinable()) {
|
|
||||||
m_timerThread = std::thread(&Timer::timerThread, this);
|
|
||||||
} else {
|
|
||||||
// Notify thread if already existing
|
|
||||||
m_condition.notify_one();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Timer::stop() {
|
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
|
||||||
m_running = false;
|
|
||||||
m_condition.notify_one();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Timer::reset() {
|
|
||||||
std::lock_guard<std::mutex> lock(m_mutex);
|
|
||||||
m_startTime = std::chrono::steady_clock::now();
|
|
||||||
}
|
|
||||||
|
|
||||||
int Timer::getElapsedTime() const {
|
|
||||||
if (m_running) {
|
|
||||||
auto now = std::chrono::steady_clock::now();
|
|
||||||
return std::chrono::duration_cast<std::chrono::milliseconds>(now - m_startTime).count();
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Timer::timerThread() {
|
|
||||||
while (true) {
|
|
||||||
// Wait for timer to be running
|
|
||||||
{
|
|
||||||
std::unique_lock<std::mutex> lock(m_mutex);
|
|
||||||
m_condition.wait(lock, [this] { return m_running || !m_running; });
|
|
||||||
|
|
||||||
// Exit thread if timer is stopped
|
|
||||||
if (!m_running) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sleep for 100ms (10 updates per second)
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
||||||
|
|
||||||
// Check if timer is still running
|
|
||||||
if (m_running) {
|
|
||||||
// Get elapsed time
|
|
||||||
int time = getElapsedTime();
|
|
||||||
|
|
||||||
// Emit signal through Glib main loop
|
|
||||||
Glib::signal_idle().connect_once([this, time] {
|
|
||||||
timerSignal.emit(time);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <functional>
|
|
||||||
#include <sigc++/signal.h>
|
|
||||||
#include <thread>
|
|
||||||
#include <atomic>
|
|
||||||
#include <mutex>
|
|
||||||
#include <condition_variable>
|
|
||||||
|
|
||||||
class Timer {
|
|
||||||
public:
|
|
||||||
Timer();
|
|
||||||
~Timer();
|
|
||||||
|
|
||||||
// Start timer
|
|
||||||
void start();
|
|
||||||
|
|
||||||
// Stop timer
|
|
||||||
void stop();
|
|
||||||
|
|
||||||
// Reset timer
|
|
||||||
void reset();
|
|
||||||
|
|
||||||
// Get elapsed time in milliseconds
|
|
||||||
int getElapsedTime() const;
|
|
||||||
|
|
||||||
// Signal emitted on timer tick
|
|
||||||
sigc::signal<void(int)> timerSignal;
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::chrono::time_point<std::chrono::steady_clock> m_startTime;
|
|
||||||
std::atomic<bool> m_running;
|
|
||||||
|
|
||||||
// Thread handling
|
|
||||||
std::thread m_timerThread;
|
|
||||||
std::mutex m_mutex;
|
|
||||||
std::condition_variable m_condition;
|
|
||||||
|
|
||||||
// Timer thread function
|
|
||||||
void timerThread();
|
|
||||||
};
|
|
||||||
1167
src/window.cpp
1167
src/window.cpp
File diff suppressed because it is too large
Load Diff
110
src/window.hpp
110
src/window.hpp
@@ -1,98 +1,40 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "minefield.hpp"
|
|
||||||
#include <memory>
|
|
||||||
#include <gtkmm.h>
|
#include <gtkmm.h>
|
||||||
#include <sigc++/sigc++.h>
|
#include <memory>
|
||||||
#include <vector>
|
#include "minefield.hpp"
|
||||||
#include <string>
|
#include "board_widget.hpp"
|
||||||
#include <fstream>
|
|
||||||
#include <filesystem>
|
|
||||||
|
|
||||||
class MainWindow : public Gtk::ApplicationWindow
|
class MainWindow : public Gtk::ApplicationWindow {
|
||||||
{
|
|
||||||
public:
|
public:
|
||||||
MainWindow();
|
MainWindow();
|
||||||
virtual ~MainWindow() = default;
|
~MainWindow() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// UI containers
|
// UI Setup
|
||||||
Gtk::Box m_boxMain{Gtk::Orientation::VERTICAL};
|
void setup_header_bar();
|
||||||
Gtk::HeaderBar m_headerBar;
|
void setup_board();
|
||||||
Gtk::Box m_statusBox{Gtk::Orientation::HORIZONTAL};
|
|
||||||
Gtk::Grid m_grid;
|
|
||||||
Gtk::Overlay m_overlay;
|
|
||||||
|
|
||||||
// Game status widgets
|
// Actions
|
||||||
Gtk::Label m_minesLabel;
|
void start_new_game(const GameDifficulty& difficulty);
|
||||||
Gtk::Label m_flagsLabel;
|
void on_game_state_changed();
|
||||||
Gtk::Label m_timeLabel;
|
bool on_timer_tick();
|
||||||
|
void show_game_over_dialog(bool won);
|
||||||
|
|
||||||
// Header bar controls
|
// Widgets
|
||||||
Gtk::Button m_newGameButton;
|
Gtk::HeaderBar header_bar_;
|
||||||
Gtk::MenuButton m_difficultyButton;
|
Gtk::Button btn_new_game_;
|
||||||
|
Gtk::MenuButton btn_difficulty_;
|
||||||
|
Gtk::Popover menu_difficulty_;
|
||||||
|
Gtk::Box box_difficulty_; // Content for popover
|
||||||
|
|
||||||
// Game field and buttons
|
Gtk::Label lbl_time_;
|
||||||
std::unique_ptr<MineField> m_field;
|
Gtk::Label lbl_flags_;
|
||||||
std::vector<std::shared_ptr<Gtk::ToggleButton>> m_buttons;
|
|
||||||
|
|
||||||
// Resources
|
BoardWidget board_widget_;
|
||||||
std::shared_ptr<Gdk::Texture> m_textureBomb;
|
|
||||||
std::shared_ptr<Gdk::Texture> m_textureFlag;
|
|
||||||
std::shared_ptr<Gdk::Texture> m_textureFlagBomb;
|
|
||||||
std::shared_ptr<Gdk::Texture> m_textureExplosion;
|
|
||||||
|
|
||||||
// Game state tracking
|
// Game State
|
||||||
bool m_firstClick;
|
std::shared_ptr<Minefield> minefield_;
|
||||||
sigc::connection m_timerConnection;
|
sigc::connection timer_conn_;
|
||||||
|
GameDifficulty current_difficulty_ = Minefield::DifficultyEasy;
|
||||||
// Leaderboard
|
|
||||||
std::vector<GameScore> m_leaderboard;
|
|
||||||
Gtk::Dialog* m_leaderboardDialog;
|
|
||||||
Gtk::Dialog* m_difficultyDialog;
|
|
||||||
Gtk::Dialog* m_winDialog;
|
|
||||||
Gtk::Dialog* m_nameDialog;
|
|
||||||
std::string m_currentDifficulty;
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
void setupUI();
|
|
||||||
void setupHeaderBar();
|
|
||||||
void setupStatusBar();
|
|
||||||
void setupGameBoard();
|
|
||||||
void setupLeaderboard();
|
|
||||||
void setupCSSProviders();
|
|
||||||
void loadResources();
|
|
||||||
|
|
||||||
// Event handlers
|
|
||||||
void onCellClick(int x, int y);
|
|
||||||
void onCellRightClick(int n_press, double n_x, double n_y, int index);
|
|
||||||
void onNewGameClick();
|
|
||||||
void onDifficultySelected(const GameDifficulty& difficulty);
|
|
||||||
void showDifficultyDialog();
|
|
||||||
|
|
||||||
// Game callbacks
|
|
||||||
void updateCell(int x, int y);
|
|
||||||
void updateFlagsLabel(int flags);
|
|
||||||
void updateTimeLabel();
|
|
||||||
bool updateTimer();
|
|
||||||
void onGameOver();
|
|
||||||
void onGameWon(int time);
|
|
||||||
void showGameOverAnimation();
|
|
||||||
void showGameWonAnimation();
|
|
||||||
void revealAllBombs();
|
|
||||||
|
|
||||||
// Leaderboard methods
|
|
||||||
void loadLeaderboard();
|
|
||||||
void saveLeaderboard();
|
|
||||||
void showLeaderboard();
|
|
||||||
void addScoreToLeaderboard(const std::string& playerName, const std::string& difficulty, int time);
|
|
||||||
std::string formatTime(int milliseconds) const;
|
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
void resetGame();
|
|
||||||
void startNewGame(int cols, int rows, int mines, const std::string& difficulty);
|
|
||||||
void clearBoard();
|
|
||||||
void setupConfetti();
|
|
||||||
void showNameInputDialog(int time);
|
|
||||||
std::filesystem::path getConfigDir() const;
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user