Code covered by the BSD License  

Highlights from
Mine Sweeper

image thumbnail
from Mine Sweeper by David Gregory
Play the game of mine sweeper

mineSweeper(varargin);
function mineSweeper(varargin);
%MINESWEEPER - Play MATLAB Mine Sweeper Game
%
%Inspired by:
%Stephen Clavering's "Mines" found as a Firefox extension
%
%How to play:
%
%The game is probably familiar to everyone, but the game has a slight
%twist.  You may play the game such that there may be up to 7 mines per
%square instead of just one.
%
%Left click:
% - If the square is not a mine then a number will appear counting the
%   number of mines in the uncovered surrounding squares.
%
% - If the square is a mine, the game is over.
%
% - If the square was already uncovered, and all the flags have been set,
%   then it will uncover the surrounding squares - however, if any of
%   those are actually a mine, the game is over.
%
% - If the square was flagged, one flag will be removed.
%
%Right click:
% - Set and cycle through the flags, each right click will add another
%   flag.
%
% - To clear all the flags from the square, continue to right click and
%   cycle back to the reset position. (Or, left click down to 0 flags.)
%
% - If the square was uncovered, nothing will happen.

%TODO: figure out why it gets hung up sometimes placing flags
%TODO: better graphics for the flags

%Created: DAG - 05/14/2007
%Edited:  DAG - 06/01/2007 - Latest version

%% Initial set up

%Change MATLAB's rand state
rand('twister',sum(100*clock));

%Number of board columns
gameBoard.gameCols    = 30;
%Number of board rows
gameBoard.gameRows    = 16;
gameBoard.numberBoxes = gameBoard.gameRows * gameBoard.gameCols;
%Maximum number of mines per square, default value of 1 if not set
gameBoard.maxMines = 1;
if nargin == 1;
	gameBoard.maxMines = varargin{1};
	if ischar(gameBoard.maxMines);%Surprise me
		gameBoard.maxMines = ceil(7 * rand);
	end;
end;

%Initial mine distribution matrix, each column represents a maxMines
%value and each row represents the number of mines to be placed on the
%board
mineDistribution = [...
	100,60,50,40,40,35,30;...
	0,  40,30,30,24,25,24;...
	0,  0, 20,20,18,16,18;...
	0,  0, 0, 10,12,11,12;...
	0,  0, 0, 0, 6, 8, 8;...
	0,  0, 0, 0, 0, 5, 5;...
	0,  0, 0, 0, 0, 0, 3];

%Initial distribution vector for the case we will play
mineDistribution = mineDistribution(:,gameBoard.maxMines);

%Will be used later to determine the number of flags left to mark
flagsLeft = 0 * mineDistribution;

%gameBoard will hold the user clicks and the state of the board:
%The first page:
%0 means covered, 1 means uncovered, -X means X flags set
%The second page:
%The handles to the text representing the flags set
%The third page:
%The handles to the boxes drawn
%The fourth page:
%The mine values
temp = zeros(gameBoard.gameRows,gameBoard.gameCols);
gameBoard.mineValues = temp;
gameBoard.boxHandles = temp;
gameBoard.flagHandle = temp;
gameBoard.userClicks = temp;

%% Create game board display

%Create figure
gameHandle = figure;
figColor = [0.80 0.80 0.80];
set(gameHandle,'Units','Normalized',...
	'Position',[0.15 0.20 0.70 0.50],...
	'Color',figColor,...
	'Name','Mine Sweeper',...
	'NumberTitle','Off',...
	'Resize','Off');

%Add game menu
set(gameHandle,'MenuBar','None');
menuHandle = uimenu('Label','Start New Game');
uimenu(menuHandle,'Label','At most 1 mine,',...
	'Callback','closereq;mineSweeper(1);','Accelerator','1');
uimenu(menuHandle,'Label','At most 2 mines,',...
	'Callback','closereq;mineSweeper(2);','Accelerator','2');
uimenu(menuHandle,'Label','At most 3 mines,',...
	'Callback','closereq;mineSweeper(3);','Accelerator','3');
uimenu(menuHandle,'Label','At most 4 mines,',...
	'Callback','closereq;mineSweeper(4);','Accelerator','4');
uimenu(menuHandle,'Label','At most 5 mines,',...
	'Callback','closereq;mineSweeper(5);','Accelerator','5');
uimenu(menuHandle,'Label','At most 6 mines,',...
	'Callback','closereq;mineSweeper(6);','Accelerator','6');
uimenu(menuHandle,'Label','At most 7 mines,',...
	'Callback','closereq;mineSweeper(7);','Accelerator','7');
uimenu(menuHandle,'Label','Surprise me,',...
	'Callback','closereq;mineSweeper(''s'');','Accelerator','S');
uimenu(menuHandle,'Label','End game,',...
	'Callback','closereq;','Accelerator','Q');

%Add box to count number of flags remaining
flagHandle = uicontrol(gameHandle,'Style','text');
set(flagHandle,'Units','Normalized',...
	'Position',[0.02 0.55 0.14 0.40],...
	'BackgroundColor',figColor);

%Add message dialog box
talkHandle = uicontrol(gameHandle,'Style','text');
set(talkHandle,'Units','Normalized',...
	'Position',[0.02 0.35 0.14 0.10],...
	'BackgroundColor',figColor,...
	'String','Good luck!','FontSize',16);

%Add game board axes
axisHandle = axes;
set(axisHandle,'Units','Normalized',...
	'Position',[0.18 0.05 0.80 0.90],...
	'XLim',[0 gameBoard.gameCols],'YLim',[0 gameBoard.gameRows],...
	'YDir','Reverse',...
	'XTick',[],'YTick',[],'Box','On');

%% Create game board values

%The indices into the board which contain a mine will be randomly
%selected using randperm
gameBoardIndex = randperm(gameBoard.numberBoxes);
mineCumulative = cumsum(mineDistribution);

%Set the first set as containing one mine
gameBoard.mineValues(gameBoardIndex(1:mineCumulative)) = -1;

%Loop through remaining random spots for next mine increment
for dMine = 2:gameBoard.maxMines;
	gameBoard.mineValues(gameBoardIndex...
		(mineCumulative(dMine - 1) + 1:mineCumulative(dMine))) = -dMine;
end;

%For each non-mine position, need to count the number of mines around,
%start by computing all the rows and columns
[dIndexRow,dIndexCol] = ind2sub...
	([gameBoard.gameRows gameBoard.gameCols],...
	1:gameBoard.numberBoxes);
dIndexRow = repmat(dIndexRow',[1 3]);
dIndexCol = repmat(dIndexCol',[1 3]);
%The rows and columns that surround are +/- 1 from the middle
surroundingPoints = repmat([-1 0 1],[size(dIndexRow,1),1]);
dIndexRow = dIndexRow + surroundingPoints;
dIndexCol = dIndexCol + surroundingPoints;

for dIndex = 1:gameBoard.numberBoxes;
	if gameBoard.mineValues(dIndex) < 0;
		%Nothing to count, index is flagged
		continue;
	end;
	%Find the surrounding squares
	surroundingIndices = surroundingSquares(...
		dIndexRow(dIndex,:),gameBoard.gameRows,...
		dIndexCol(dIndex,:),gameBoard.gameCols);

	%Find these gameBoard values
	temp = gameBoard.mineValues(surroundingIndices);
	%Only count the flags (negative values)
	k    = temp < 0;
	%Sum the number of flags surrounding this index
	gameBoard.mineValues(dIndex) = abs(sum(sum(temp .* k)));
end;

%Display the game board in the background of the figure
gameBoard.xOffset = -0.75;%Offset from edge of row/col
gameBoard.yOffset = -0.50;%Offset from edge of row/col
[x,y] = meshgrid(...
	1 + gameBoard.xOffset:1:gameBoard.gameCols + gameBoard.xOffset,...
	1 + gameBoard.yOffset:1:gameBoard.gameRows + gameBoard.yOffset);

%Display text
textHandles = text(x(:),y(:),int2str(gameBoard.mineValues(:)),...
	'FontSize',8);
set(textHandles,'Color',[0.00 0.00 1.00]);
%The 0 values should not display, and mines are to be colored red
k = gameBoard.mineValues(:);
set(textHandles(k == 0),'Visible','Off');
set(textHandles(k <  0),'Color',[1.00 0.00 0.00]);

%Now, cover the number of mines (otherwise the game would be pointless)
gameBoard.uncoveredColor = [0.40 0.40 0.40];%dark gray covering square
gameBoard.markedColor    = [1.00 1.00 0.50];%yellow indicating flagged
%For each grid point, create a patch of a box covering the area
for dRow = 0:gameBoard.gameRows - 1;
	for dCol = 0:gameBoard.gameCols - 1;
		gameBoard.boxHandles(dRow + 1,dCol + 1) = patch(...
			[dCol, dCol,     dCol + 1, dCol + 1, dCol],...
			[dRow, dRow + 1, dRow + 1, dRow,     dRow],...
			gameBoard.uncoveredColor);
		set(gameBoard.boxHandles(dRow + 1,dCol + 1),...
			'EdgeColor',[0.00 0.00 0.00]);
	end;
end;

%Setup the text containing the flag information
flagsLeftString = char(zeros(gameBoard.maxMines,6));
for dMine = 1:gameBoard.maxMines;
	switch dMine;
		case 1;
			flagsLeftString(dMine,:) = ' I  - ';
		case 2;
			flagsLeftString(dMine,:) = 'II  - ';
		case 3;
			flagsLeftString(dMine,:) = 'III - ';
		case 4;
			flagsLeftString(dMine,:) = 'IV  - ';
		case 5;
			flagsLeftString(dMine,:) = ' V  - ';
		case 6;
			flagsLeftString(dMine,:) = 'VI  - ';
		case 7;
			flagsLeftString(dMine,:) = 'VII - ';
	end;
end;

%% Play game
gameContinues = true;
gameWon       = false;

while gameContinues;

	%Update mine count
	for dMine = 1:gameBoard.maxMines;
		flagsLeft(dMine,1) = mineDistribution(dMine) -...
			sum(sum(gameBoard.userClicks == -dMine));
	end;
	set(flagHandle,...
		'String',[flagsLeftString,...
		int2str(flagsLeft(1:gameBoard.maxMines))],'FontSize',12);

	%Determine if game has been won
	k = gameBoard.mineValues < 0;
	if all(all(gameBoard.mineValues(k) == gameBoard.userClicks(k))) &&...
			all(all(gameBoard.userClicks(~k) == 1));
		%All flags set and match the mine value, and all boxes uncovered
		gameContinues = false;
		gameWon       = true;
		continue;
	end;

	%Get user click
	try;
		m = waitforbuttonpress;
		if m == 1;
			continue;
		end;
	catch;
		%User closed game
		return;
	end;

	%Return mouse position
	thisPoint = get(axisHandle,'CurrentPoint');
	thisRow   = floor(thisPoint(1,2)) + 1;
	thisCol   = floor(thisPoint(1,1)) + 1;
	%Return mouse button used
	thisButton = get(gameHandle,'SelectionType');

	if thisRow <= 0 || thisRow > gameBoard.gameRows ||...
			thisCol <= 0 || thisCol > gameBoard.gameCols;
		%Clicked outside the board, try again
		continue;
	end;

	switch thisButton;
		case 'normal';%Left button

			%Square has a flag on it, left click void
			if gameBoard.userClicks(thisRow,thisCol) < 0;
				gameBoard.userClicks(thisRow,thisCol) =...
					gameBoard.userClicks(thisRow,thisCol) + 1;
				%Place/update flag on board
				gameBoard = setFlag(gameBoard,thisRow,thisCol);
				continue;
			end;

			%Square was not clear, but is a mine
			if gameBoard.mineValues(thisRow,thisCol) < 0 &&...
					gameBoard.userClicks(thisRow,thisCol) == 0;
				%Oops, stepped on a mine
				set(gameBoard.boxHandles(thisRow,thisCol),...
					'FaceColor','None');
				gameContinues = false;
				continue;
			end;

			%Square was not clear, not a mine
			if gameBoard.mineValues(thisRow,thisCol) >= 0 &&...
					gameBoard.userClicks(thisRow,thisCol) == 0;
				set(gameBoard.boxHandles(thisRow,thisCol),...
					'FaceColor','None');
				gameBoard.userClicks(thisRow,thisCol) = 1;
				newlyUncovered = sub2ind...
					([gameBoard.gameRows gameBoard.gameCols],...
					thisRow,thisCol);
			end;

			%Square clear, number of flags set, clear surrounding area
			surroundingIndices = surroundingSquares(...
				thisRow,gameBoard.gameRows,...
				thisCol,gameBoard.gameCols);

			temp = gameBoard.userClicks(surroundingIndices);
			k    = temp < 0;
			flagsSet = abs(sum(sum(temp .* k)));

			if flagsSet == gameBoard.mineValues(thisRow,thisCol) &&...
					flagsSet > 0 &&...
					gameBoard.userClicks(thisRow,thisCol) == 1;
				%Find all surrounding points that aren't flagged
				k = gameBoard.userClicks(surroundingIndices) < 0;
				set(gameBoard.boxHandles(surroundingIndices(~k)),...
					'FaceColor','None');
				gameBoard.userClicks(surroundingIndices(~k)) = 1;
				newlyUncovered = surroundingIndices(~k);
				if any(gameBoard.mineValues(surroundingIndices(~k)) < 0);
					%Oops, set a flag incorrectly and uncovered a mine
					gameContinues = false;
					continue;
				end;
			end;

			%Call a recursive function to uncover all the connected 0
			%values, this will happen if the square was a 0 or if the
			%flags were set and a 0 was uncovered
			[newlyUncovered,gameBoard] = uncoverZeros...
				(newlyUncovered,gameBoard,[]);

		case 'alt';%Right button

			%Box already cleared, right click void
			if gameBoard.userClicks(thisRow,thisCol) == 1;
				continue;
			end;

			%Mark flag, subtract 1 to mean another flag added
			gameBoard.userClicks(thisRow,thisCol) =...
				gameBoard.userClicks(thisRow,thisCol) - 1;

			%Place/update flag on board
			gameBoard = setFlag(gameBoard,thisRow,thisCol);
	end;
end;

%Game over, display end game message
if gameWon;
	newColor = [0.30 0.30 1.00];
	set(talkHandle,'String','Well Done!');
else;
	newColor = [1.00 0.50 0.00];
	set(talkHandle,'String',' O O P S! ');
	%Display flags set incorrectly
	k = gameBoard.userClicks < 0 &...
		gameBoard.mineValues ~= gameBoard.userClicks;
	set(gameBoard.boxHandles(k),'FaceColor',[1.00 0.00 0.00]);
	%Display mines not flagged
	k = gameBoard.mineValues < 0 & gameBoard.userClicks >= 0;
	set(gameBoard.boxHandles(k),'FaceColor','None');
end;
set(gameHandle,'Color',newColor);
set(flagHandle,'BackgroundColor',newColor);
set(talkHandle,'BackgroundColor',newColor);

end

%% Set Flags

%Function to place/update the correct flag on the board
function gameBoard = setFlag(gameBoard,thisRow,thisCol);

%Use mod arithmetic only
temp = mod(-gameBoard.userClicks(thisRow,thisCol),...
	gameBoard.maxMines + 1);
gameBoard.userClicks(thisRow,thisCol) = -temp;
%If text has previously been set...
if gameBoard.flagHandle(thisRow,thisCol) ~= 0;
	%...Clear flag string
	set(gameBoard.flagHandle(thisRow,thisCol),'String','');
end;
%Set square color to indicate it is marked
thisColor = gameBoard.markedColor;
switch temp;%Number of flags set
	case 0;%Reset square to uncovered state
		flagString = '';
		thisColor = gameBoard.uncoveredColor;
	case 1;
		flagString = '  I  ';
	case 2;
		flagString = ' I I ';
	case 3;
		flagString = 'I I I';
	case 4;
		flagString = 'I V ';
	case 5;
		flagString = '  V ';
	case 6;
		flagString = 'V I ';
	case 7;
		flagString = 'VI I';
end;
%Display flags, set square color
gameBoard.flagHandle(thisRow,thisCol) =...
	text(thisCol + gameBoard.xOffset,thisRow + gameBoard.yOffset,...
	flagString,'FontName','Arial','FontSize',6);
set(gameBoard.boxHandles(thisRow,thisCol),...
	'FaceColor',thisColor);

end

%% Surrounding Squares Indices

%Return the indices surrounding the current square(s)
function surroundingIndices = surroundingSquares(rowIndices,gameRows,...
	colIndices,gameCols);

if length(rowIndices) == 1;
	rowIndices = rowIndices + [-1 0 1];
end;
if length(colIndices) == 1;
	colIndices = colIndices + [-1 0 1];
end;

%For one index at a time, determine the rows and columns that
%surround the index, removing those off the game board.
m = ~(rowIndices <= 0 | rowIndices > gameRows);
n = ~(colIndices <= 0 | colIndices > gameCols);
%Compute the "all-possible" pairings of rows and columns
[surroundingRows,surroundingCols] = meshgrid...
	(rowIndices(m),colIndices(n));
%Easier to use indices
surroundingIndices = sub2ind...
	([gameRows gameCols],surroundingRows,surroundingCols);

end

%% Uncover 0 Squares

%Recursive function to uncover squares with a 0 value
function [newlyUncovered,gameBoard] =...
	uncoverZeros(newlyUncovered,gameBoard,alreadyCleared);

%Game size
[gameRows,gameCols] = size(gameBoard.mineValues);

%Keep only the new squares that are a 0
newlyUncovered = newlyUncovered...
	(gameBoard.mineValues(newlyUncovered) == 0);

while ~isempty(newlyUncovered);
	%Take the first index
	[rowIndex,colIndex] = ind2sub...
		([gameRows gameCols],newlyUncovered(1));
	%Return all points the surround this square
	temp = surroundingSquares(rowIndex,gameRows,colIndex,gameCols);

	%Add to a list of squares already dealt with to avoid an infinite
	%recursion
	alreadyCleared = unique([alreadyCleared;newlyUncovered(1)]);

	%Remove squares marked as already clear
	temp = setdiff(temp(:),alreadyCleared);
	%Remove point currently being cleared
	newlyUncovered(1) = [];
	%Add the surrounding squares to this list
	newlyUncovered = unique([newlyUncovered;temp(:)]);

	%If the user set a 0 as a flag don't uncover
	temp = temp(gameBoard.userClicks(temp) >= 0);

	%Mark these squares as cleared
	gameBoard.userClicks(temp) = 1;
	set(gameBoard.boxHandles(temp),'FaceColor','None');

	%Recursive call, will continue until the trail of 0s has been
	%uncovered
	[newlyUncovered,gameBoard] = uncoverZeros...
		(newlyUncovered,gameBoard,alreadyCleared);
end;

end

Contact us at files@mathworks.com