function mineSweeper(varargin);
%
%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.
%
%% 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