After taking a course in C++, I have written my first program and I'm looking for some feedback both on coding style and maybe algorithmic improvements. The code can also be found at GitHub. It compiles and runs fine but is not very robust for user input.
I opted to try to emulate the game Minesweeper. My implementation is as follows:
#include "minesweeper/minesweeper.h"
int main()
{
Minesweeper game{9};
game.play();
}
First I have a class which I use as a 'cell':
#ifndef MINESWEEPERCELL_H_
#define MINESWEEPERCELL_H_
#include <cstddef>
class MinesweeperCell
{
bool d_isBomb = false;
char d_state = '*'; // * or F
std::size_t d_numBombsNear = 0;
bool d_visited = false;
public:
MinesweeperCell() = default;
void setBomb();
bool isBomb() const;
void setState(char const ch);
char state() const;
void setNumBombsNear(std::size_t const num);
std::size_t numBombsNear() const;
bool visited() const;
void setVisited();
};
#endif
The implementations of these getters and setters are trivial and hence are left out for brevity. They can be found at the GitHub project.
Next is the most important class:
#ifndef MINESWEEPER_H_
#define MINESWEEPER_H_
#include <cstddef>
#include <vector>
#include <iosfwd>
#include <utility>
#include "../minesweepercell/minesweepercell.h"
class Minesweeper
{
std::size_t d_size; // square only so far
std::vector<std::vector<MinesweeperCell>> d_gameBoard;
std::vector<std::pair<int, int>> d_bombLocations;
public:
Minesweeper(std::size_t const size = 9);
void play();
private:
void initializeBoard(); // Places randomly d_size + 1 bombs on board
void setNumBombsNear();
void checkSurrounding(std::size_t const xCoord, std::size_t const yCoord);
void showBombs();
bool processInput(char const cmd, std::size_t const xCoord, std::size_t const yCoord);
friend std::ostream &operator<<(std::ostream &out,
Minesweeper const &minesweeper);
};
#endif
I have implemented the member functions as follows (following the 1 function per file idiom, using internal headers):
// minesweeper.ih
#include <random>
#include <iostream>
#include "minesweeper.h"
using namespace std;
I am aware of the pitfalls of including the whole standard namespace. In my opinion, its ability to improve the readability of my code is more important than rare namespace clashes.
#include "minesweeper.ih"
Minesweeper::Minesweeper(size_t const size)
:
d_size{size},
d_gameBoard{vector<vector<MinesweeperCell>>(size, vector<MinesweeperCell>(size))}
{
initializeBoard();
setNumBombsNear();
}
.
#include "minesweeper.ih"
void Minesweeper::play()
{
bool notDone = true;
while (notDone)
{
char cmd;
cin >> cmd;
if (cmd == 'q')
break;
size_t xCoord, yCoord;
cin >> xCoord >> yCoord;
if (xCoord < 0 or xCoord >= d_size or yCoord < 0 or yCoord >= d_size)
{
cout << "Please input valid coordinates\n";
continue;
}
notDone = processInput(cmd, xCoord, yCoord);
cout << *this;
}
}
.
#include "minesweeper.ih"
void Minesweeper::initializeBoard()
{
size_t numOfBombs = 0;
uniform_int_distribution<int> distribution(0, d_size - 1);
default_random_engine generator(random_device{}());
while (numOfBombs != d_size + 1)
{
// generate a pair of indices
pair<int, int> randomNums{distribution(generator), distribution(generator)};
if (d_gameBoard[randomNums.first][randomNums.second].isBomb())
continue;
++numOfBombs;
d_gameBoard[randomNums.first][randomNums.second].setBomb();
d_bombLocations.push_back(randomNums);
}
}
.
#include "minesweeper.ih"
void Minesweeper::setNumBombsNear()
{
int moves[8][2] = { {-1,0}, {-1, -1}, {0, -1}, {1, -1},
{1, 0}, {1, 1}, {0, 1}, {-1, 1} };
for (size_t row = 0; row < d_size; ++row)
{
for (size_t col = 0; col < d_size; ++col)
{
size_t num = 0;
for (size_t idx = 0; idx != 8 /*sizeof(move) / sizeof(move[0]) */; ++idx)
{
size_t possibleRow = row + moves[idx][0];
size_t possibleCol = col + moves[idx][1];
if (possibleRow < 0 or possibleRow >= d_size or possibleCol < 0 or possibleCol >= d_size)
continue;
num += d_gameBoard[possibleRow][possibleCol].isBomb() ? 1 : 0;
}
d_gameBoard[row][col].setNumBombsNear(num);
}
}
}
.
#include "minesweeper.ih"
void Minesweeper::checkSurrounding(size_t const xCoord, size_t const yCoord)
{
static int moves[8][2] = { {-1,0}, {-1, -1}, {0, -1}, {1, -1},
{1, 0}, {1, 1}, {0, 1}, {-1, 1} };
for (size_t idx = 0; idx != 8 /*sizeof(move) / sizeof(move[0]) */; ++idx)
{
size_t possibleX = xCoord + moves[idx][0];
size_t possibleY = yCoord + moves[idx][1];
if (possibleX < 0 or possibleX >= d_size or possibleY < 0 or possibleY >= d_size)
continue;
if (d_gameBoard[possibleX][possibleY].visited())
continue;
d_gameBoard[possibleX][possibleY].setVisited();
if (d_gameBoard[possibleX][possibleY].numBombsNear() == 0)
{
d_gameBoard[possibleX][possibleY].setState('0');
checkSurrounding(possibleX, possibleY);
}
else if (d_gameBoard[possibleX][possibleY].numBombsNear() != 0)
{
d_gameBoard[possibleX][possibleY].setState('0' + d_gameBoard[possibleX][possibleY].numBombsNear());
}
}
}
.
#include "minesweeper.h"
void Minesweeper::showBombs()
{
for (auto &location : d_bombLocations)
{
d_gameBoard[location.first][location.second].setState('B');
}
}
.
#include "minesweeper.ih"
bool Minesweeper::processInput(char const cmd, size_t const xCoord, size_t const yCoord)
{
switch (cmd)
{
case 'f':
d_gameBoard[xCoord][yCoord].setState('F');
break;
case 'u':
d_gameBoard[xCoord][yCoord].setState('*');
break;
case 'p':
if (d_gameBoard[xCoord][yCoord].isBomb() and d_gameBoard[xCoord][yCoord].state() != 'F')
{
cout << "Boom! You have hit a bomb and lost the game!\n";
showBombs();
return false;
}
if (d_gameBoard[xCoord][yCoord].state() == 'F')
{
cout << "This coordinate is flagged, please unflag first\n";
break;
}
d_gameBoard[xCoord][yCoord].setState('0' + d_gameBoard[xCoord][yCoord].numBombsNear());
if (d_gameBoard[xCoord][yCoord].numBombsNear() != 0)
break;
else
checkSurrounding(xCoord, yCoord);
break;
default:
cout << "Invalid command, please try again\n";
break;
}
return true;
}
.
#include "minesweeper.ih"
ostream &operator<<(ostream &out, Minesweeper const &minesweeper)
{
for (auto &row: minesweeper.d_gameBoard)
{
for (auto &element: row)
{
out << element.state();
}
out << '\n';
}
}
std::size_t
for a replacement of nonnegative integers anyway, since they do not overflow. An alternative is a conditional value typenonnegative<int>
. \$\endgroup\$ – Maikel Mar 28 '17 at 8:04