383 lines
8.9 KiB
C++
383 lines
8.9 KiB
C++
#include "minefield.hpp"
|
|
#include <ctime>
|
|
#include <random>
|
|
#include <algorithm>
|
|
#include <iostream>
|
|
|
|
// Define difficulty presets
|
|
const GameDifficulty MineField::DIFFICULTY_EASY = {"Beginner", 9, 9, 10};
|
|
const GameDifficulty MineField::DIFFICULTY_MEDIUM = {"Intermediate", 16, 16, 40};
|
|
const GameDifficulty MineField::DIFFICULTY_HARD = {"Expert", 30, 16, 99};
|
|
const GameDifficulty MineField::DIFFICULTY_EXPERT = {"Master", 30, 20, 145};
|
|
|
|
MineField::MineField(int cols, int rows, int mines) :
|
|
m_rows(rows),
|
|
m_cols(cols),
|
|
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()
|
|
{
|
|
m_cells.clear();
|
|
}
|
|
|
|
void MineField::startTimer()
|
|
{
|
|
if (!m_timerRunning)
|
|
{
|
|
m_startTime = std::chrono::steady_clock::now();
|
|
m_timerRunning = true;
|
|
}
|
|
}
|
|
|
|
void MineField::stopTimer()
|
|
{
|
|
if (m_timerRunning)
|
|
{
|
|
m_endTime = std::chrono::steady_clock::now();
|
|
m_timerRunning = false;
|
|
}
|
|
}
|
|
|
|
int MineField::getElapsedTime() const
|
|
{
|
|
if (m_timerRunning)
|
|
{
|
|
auto now = std::chrono::steady_clock::now();
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(now - m_startTime).count();
|
|
}
|
|
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::mt19937 gen(rd());
|
|
|
|
// Shuffle positions
|
|
std::shuffle(positions.begin(), positions.end(), gen);
|
|
|
|
// Place mines
|
|
int minesToPlace = std::min(m_totalMines, static_cast<int>(positions.size()));
|
|
for (int i = 0; i < minesToPlace; i++)
|
|
{
|
|
m_cells.at(positions[i])->isBomb = true;
|
|
}
|
|
}
|
|
|
|
bool MineField::openCell(int x, int y)
|
|
{
|
|
// Ignore if game is over or cell is already open/flagged
|
|
if (m_gameState != GameState::PLAYING && m_gameState != GameState::READY)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (isOpened(x, y) || isFlagged(x, y))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Start timer on first click if not already started
|
|
if (m_gameState == GameState::READY)
|
|
{
|
|
initBombs(x, y);
|
|
}
|
|
|
|
// Check if bomb
|
|
if (isBomb(x, y))
|
|
{
|
|
// Only emit the game over signal if we haven't already
|
|
if (m_gameState != GameState::LOST) {
|
|
m_gameState = GameState::LOST;
|
|
stopTimer();
|
|
gameOverSignal.emit();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Open cell
|
|
setOpenCell(x, y);
|
|
|
|
// If no bombs nearby, open surrounding cells
|
|
if (bombsNearby(x, y) == 0)
|
|
{
|
|
openNeighboorhood(x, y);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void MineField::computeBombsNearby(int x, int y)
|
|
{
|
|
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;
|
|
}
|