Simple Console based Minesweeper game

Started by
4 comments, last by Paradigm Shifter 10 years, 10 months ago

Recently I've gotten to thinking about what it would take to implement a simple console based minesweeper game, what data structures and algorithms would be needed for a bare-minimum playable game. No variations on game size, difficult, etc. No scoring or timing. No visual aids for the player beyond revealing appropriate portions of the map.

This weekend, I had a few spare hours, and took a crack at such an implementation. It actually ended up being easier than I had expected (took about an hour and a half total). Wound up being a bit over a hundred lines of code. The sources below are actually over 200 lines, but I spent half an hour liberally commenting the code for posting. I also set myself an additional task of utilizing the STL as much as possible in this mini-project as well, which I think went a long way in keeping the line count down. I was not intentionally trying to keep line count down though.

Hope this is an appropriate place to post this. I don't have any questions to ask, any problems I need assistance with. Just wanted to toss this out there for others to check out, if they wish. I'm certainly open to any comments, criticisms, or questions though.

So, without further ado:


#include <iostream>
#include <vector>
#include <queue>
#include <utility>
#include <random>
#include <ctime>

using namespace std;


//matrix is a 2d vector of chars, used for both our map and our mask
typedef vector<vector<char> > matrix;

//below are our minesweeper map params
const int x_dim = 10, y_dim = 10, numMines = 10;

/********************************************
Add a mine to our map
returns true if mine added
false if mine wasn't added (attempted to add a mine to a location that already had one)
also updates proximity values in the map as well
*********************************************/
bool addMine(matrix& map)
{
	//Generate a random location for this mine
	int x = rand() % x_dim, y = rand() % y_dim;

	//Add the mine, if one isn't already there
	if(map[x][y] != '*')
	{
		map[x][y] = '*';
		//walk through all neighboring locations and increment their proximity counts
		for(int dx = x-1; dx <= x+1; dx++)
			for(int dy = y-1; dy <= y+1; dy++)
				if(dx >= 0 && dx < x_dim && dy >= 0 && dy < y_dim)
					if(map[dx][dy] != '*') //Don't update proximity count for mine locations
						if(map[dx][dy] == '.')
							map[dx][dy] = '1'; //initial proximity, set to 1
						else
							map[dx][dy]++; //otherwise, increment current count (works even with chars so long as count never exceeds 9)
		return true;
	}

	//random location already contains a mine
	return false;
}


/*****************************************************************
reveals the specified location of the map/mask
expands areas that are not in proximity to a mine as well
returns true if a mine was revealed (ie game over)
******************************************************************/
bool revealLocation(matrix& map, matrix& mask, int x, int y)
{
	//reveal the location
	mask[x][y] = '.';

	//if location is a mine, game over, just return true
	if(map[x][y] == '*')
		return true;

	//If our location isn't in proximity to a mine
	//we reveal all neighboring locations
	//dots indicate no neighboring mine
	if(map[x][y] == '.')
	{
		//openLocations holds neighboring locations that also
		//are not in proximity to a mine
		queue<pair<int, int> > openLocations;
		openLocations.push(make_pair(x, y));

		//Walk through all dot locations and reveal their neighboars
		while(!openLocations.empty())
		{
			//Get the next location from our queue
			pair<int, int> next = openLocations.front();
			
			//The two for loops iterate over a 3x3 block within our map
			//surrounding the point next.  It will check the point itself
			//as well, which is redundant, but we hardly need highly
			//optimized code here
			for(int dx = next.first-1; dx <= next.first+1; dx++)
			{
				for(int dy = next.second-1; dy <= next.second+1; dy++)
				{
					//Let's make sure the current location is within the
					//bounds of our map.  If next is an edge location, then
					//we'll be iterating over some points outside the map
					//So just ignore those points
					if(dx >= 0 && dx < x_dim && dy >= 0 && dy < y_dim)
					{
						//if this neighbor is a dot location and hasn't
						//previously been revealed, add it to our list
						if(map[dx][dy] == '.' && mask[dx][dy] == '#')
							openLocations.push(make_pair(dx, dy));

						//reveal this neighboring location
						mask[dx][dy] = '.';

					}
				}
			}
			//We're done with the current location in our queue, so we can remove it
			openLocations.pop();
		}
	}

	return false;
}

/******************************************************
returns a count of the remaining masked locations
used to determine if only mines are masked (ie our win condition)
*******************************************************/
int countMask(matrix& mask)
{
	int count = 0;
	for(int x = 0; x < x_dim; x++)
		for(int y = 0; y < y_dim; y++)
			if(mask[x][y] == '#') count++;

	return count;
}

int main()
{
	srand((unsigned int)time(NULL));

	//map contains our mines and proximity values
	//Only unmasked locations of map will be visible though
	//as tracked by our following mask matrix
	matrix map;

	//mask tracks revealed locations
	//# char indicates unrevealed locations
	//. char indicated revealed locations
	matrix mask;


	//Generate our map and mask
	for(int i = 0; i < x_dim; i++)
	{
		map.push_back(vector<char>(y_dim, '.'));
		mask.push_back(vector<char>(y_dim, '#'));
	}
	int mineCount = 0;


	//loop to add mines until we have a full minefield
	do
	{
		if(addMine(map))
			mineCount++;
	}while(mineCount != numMines);


	int x_in, y_in;
	do
	{
		//Display the masked minefield
		//output column indices first
		cout << "  0123456789" << endl;
		cout << "  ----------" << endl;

		//loop through our rows
		for(int x = 0; x < x_dim; x++)
		{
			//output each row index before the row itself
			cout << x << "|";

			//loop through all the columns for this row
			for(int y = 0; y < y_dim; y++)
			{
				//if location is still masked, display mask char
				//otherwise display the underlying map value
				if(mask[x][y] == '#')
					cout << '#';
				else
					cout << map[x][y];
			}
			cout << endl;
		}

		//Wait for user input to reveal a location
		cin >> x_in >> y_in;

		//pass the specified location to our revealLocation function
		//revealLocation returns true if a mine was unmasked
		if(revealLocation(map, mask, x_in, y_in))
		{
			cout << "You set off a mine.  Game over!" << endl;
			break;
		}

		//Count our masked location, if == numMines, then only mines are left
		//and player has won the game
		if(countMask(mask) == numMines)
		{
			cout << "You have found all the mines.  Congratulations!" << endl;
			break;
		}

	}while(1);

	map.clear();
	mask.clear();

	cout << endl;

	system("pause");
	return 0;
}


Advertisement

I wrote a unix based minesweeper when I was learning C.

I see you are calculating the score of neighbouring mines every time you add one. Don't do that, just do it when a tile without a mine is revealed by the player. Another reason to do it that way is that the game cheats if you pick a mine on the first turn and moves it somewhere else ;)

I used recursion rather than a queue for opening multiple zero squares. Your way is fine though.

EDIT: Everyone on my course moaned at me until I put in a "mark square" ability as well, I used *a4 syntax for that. It was popular in the class everyone used to play it ;) I had variable size and number of bombs too, either as arguments to main (so you could do minesweeper 10 10 20 for a 10 x 10 board with 20 mines), or asked you for the values if you didn't use the command line arguments. If I ran out of letters I went aa, bb, cc, dd, etc. EDIT2: I see you use numbers for both rows and columns though. I allowed a4 or 4a to be valid input, the letters were the rows and the numerals specified columns.

I notice you don't do any error checking on the bounds of the input either. You'll want to do that!

"Most people think, great God will come from the sky, take away everything, and make everybody feel high" - Bob Marley

I was more interested in just getting the core gameplay implemented as a short exercise. If I progress with it any further at all, it will be a shift to a full graphical UI. I've never really been a fan of cin based input error checking. I find it rather cumbersome, but then I guess that can probably be said about many/most forms of error checking.

EDIT: Now that I think about it more though, I like your idea of using an alphanumeric combination as opposed to a double numeric for rows columns, and I'm thinking I'll get input in a string and play around with regex's for parsing. Regex's may be a bit overkill, but I don't get a lot of real opportunities to use them, and I'm not all that proficient with them as a result. Plus they're certainly more interesting than checking the state of cin, clearing, and ignoring.

Yeah, I would have gone on and done a GUI version if we weren't using a command line based unix system. It's pretty easy to do if you use fixed size button controls for the cells.

"Most people think, great God will come from the sky, take away everything, and make everybody feel high" - Bob Marley

I just copy and pasted your code into MSVC++ to check it out and noticed that you don't clear the screen before updating the screen, an easy way to do this on windows is to call system("cls"); this provides a much more user friendly experience biggrin.png

This is where I added it in to your code:


...
//Wait for user input to reveal a location
cin >> x_in >> y_in;
system("cls"); 
...

Cool little project though!

EDIT: Now that I think about it more though, I like your idea of using an alphanumeric combination as opposed to a double numeric for rows columns, and I'm thinking I'll get input in a string and play around with regex's for parsing. Regex's may be a bit overkill, but I don't get a lot of real opportunities to use them, and I'm not all that proficient with them as a result. Plus they're certainly more interesting than checking the state of cin, clearing, and ignoring.

I used the power of sscanf I think ;) It was C rather than C++ though.

"Most people think, great God will come from the sky, take away everything, and make everybody feel high" - Bob Marley

This topic is closed to new replies.

Advertisement