RosettaCodeData/Task/Minesweeper-game/MATLAB/minesweeper-game-2.m
2023-07-01 13:44:08 -04:00

243 lines
8.3 KiB
Matlab

function Minesweeper
% Game parameters (should be modified by user)
nRows = 6;
nCols = 4;
percentMines = 0.15;
% Create minefield
nMines = ceil(percentMines*nRows*nCols);
field = makeGrid(nRows, nCols, nMines);
% Create timer for updating in axes title
stopwatch = timer('TimerFcn', {@updateTime, field}, ...
'ExecutionMode', 'fixedRate', 'Period', 1, 'StartDelay', 1, ...
'TasksToExecute', 999);
% Specify callbacks
set(gcf, 'CloseRequestFcn', {@cleanUp, stopwatch})
set(gca, 'ButtonDownFcn', {@onClick, field, stopwatch})
end
function field = makeGrid(nRows, nCols, nMines)
% Create minefield with unit squares
% Use quadrant IV to make indexing semi-consistent
% i.e. square in ith row, jth column has lower-right corner at (j, -i)
figure
set(gcf, 'Color', [1 1 1])
axis([0 nCols -nRows 0])
axis square
axis manual
hold on
set(gca, 'GridLineStyle', '-')
grid on
set(gca, 'XTick', 0:nCols)
set(gca, 'YTick', -nRows:0)
set(gca, 'XTickLabel', [])
set(gca, 'YTickLabel', [])
set(gca, 'Color', [0.6 0.6 0.6])
setTitle(nMines, ': )', 0)
xlabel('left-click to dig, right-click to mark')
% Set up field structure
% text will contain the handles to the labels in each square
% One character per square (or other functions will break)
% . Nothing noted
% M Marked as mine
% ? Marked as unknown
% 1-8 Digits indicate how many mines this square touches
% * Mine (game over)
% (blank) Square has been (dug) and is not mined nor touching any mines
% squares will contain handles to the colored "fill" objects
% fill objects will be deleted once square is "dug"
% Use ishandle() to determine if square is not yet dug
% mines will contain a logical array indicating positions of mines
% true Mine
% false No mine
% Later versions of MATLAB use gobjects() to preallocate text and squares
field = struct('text', zeros(nRows, nCols), ...
'squares', zeros(nRows, nCols), ...
'mines', false(nRows, nCols));
% Create individual square color and label objects
for r = 1:nRows
for c = 1:nCols
field.squares(r, c) = ...
fill([c-1 c-1 c c c-1], [-r+1 -r -r -r+1 -r+1], [0.9 0.9 0.9]);
set(field.squares(r, c), 'HitTest', 'off')
field.text(r, c) = text(c-0.5, -r+0.5, '.');
set(field.text(r, c), 'FontSize', 12, 'FontWeight', 'bold', ...
'HorizontalAlignment', 'center', 'HitTest', 'off');
end
end
% Place mines randomly without repeats
k = 0;
while k < nMines
idx = randi(nRows*nCols);
if ~field.mines(idx)
field.mines(idx) = true;
k = k+1;
end
end
end
function onClick(obj, event, field, stopwatch)
if strcmp(stopwatch.Running, 'off')
start(stopwatch)
end
pt = get(obj, 'CurrentPoint');
r = ceil(-pt(1, 2));
c = ceil(pt(1, 1));
if r > 0 && c > 0 && r <= size(field.squares, 1) && ... % Not yet been "dug"
c <= size(field.squares, 2) && ishandle(field.squares(r, c))
buttons = {'normal' 'alt' 'extend'};
btn = find(strcmp(get(get(obj, 'Parent'), 'SelectionType'), buttons));
labels = '.M?'; % Unmarked, mine flag, unknown flag
currLabel = get(field.text(r, c), 'String');
if btn == 1 % Left click
if currLabel ~= labels(2); % Don't dig if flagged as mine
if field.mines(r, c) % Mine there -> you lose
gameLost(field, stopwatch, r, c)
else % No mine -> free to dig
minesLeft = countMineFlags(field);
digSquare(field, r, c)
if all(all(ishandle(field.squares) == field.mines))
gameWon(field, stopwatch)
else
faceTimer = timer('StartDelay', 0.5, ...
'StartFcn', {@setTitleOnTimer, minesLeft, ...
': o', stopwatch.TasksExecuted}, ...
'TimerFcn', {@setTitleOnTimer, minesLeft, ...
': )', stopwatch.TasksExecuted}, ...
'StopFcn', @deleteTimer);
start(faceTimer)
end
end
end
elseif btn == 2 % Right-click
% Rotate through labels list to the next one
switch find(currLabel == labels)
case 1
newLabel = labels(2);
case 2
newLabel = labels(3);
case 3
newLabel = labels(1);
end
set(field.text(r, c), 'String', newLabel);
setTitle(countMineFlags(field), ': )', stopwatch.TasksExecuted)
elseif btn == 3 % Middle-click
% Mark/unmark unknown flag
if currLabel == labels(1)
set(field.text(r, c), 'String', labels(3));
elseif currLabel == labels(3)
set(field.text(r, c), 'String', labels(1));
end
end
end
end
function updateTime(obj, event, field)
setTitle(countMineFlags(field), ': )', obj.TasksExecuted)
end
function deleteTimer(obj, event)
delete(obj)
end
function setTitleOnTimer(obj, event, mines, face, time)
setTitle(mines, face, time)
end
function setTitle(mines, face, time)
title(sprintf('%03d Mines %s Timer %03d', mines, face, time))
end
function minesLeft = countMineFlags(field)
% Determine how many mines are unmarked (negative means too many mines marked)
minesLeft = sum(field.mines(:));
for k = 1:numel(field.text)
if get(field.text(k), 'String') % Not an empty string
minesLeft = minesLeft-(get(field.text(k), 'String') == 'M');
end
end
end
function digSquare(field, r, c)
% If square is touching one or more mines then indicate the number
% Otherwise indicate no mines and dig surrounding squares recursively
% Assumes current square is clear of mines
delete(field.squares(r, c))
[nRows, nCols] = size(field.mines);
surrR = [r-1 r r+1 r-1 r+1 r-1 r r+1];
surrC = [c-1 c-1 c-1 c c c+1 c+1 c+1];
toDelete = surrR < 1 | surrR > nRows | surrC < 1 | surrC > nCols;
surrR(toDelete) = [];
surrC(toDelete) = [];
nearMines = sum(field.mines(sub2ind([nRows nCols], surrR, surrC)));
label = sprintf('%d', nearMines);
textColor = [0 0 0 ; 0 0 1 ; 0 1 0 ; 1 0 0 ; 0.5 0.1 0.9 ; ...
0.6 0 0 ; 0.2 0.5 0.3 ; 0.2 0.2 0.1 ; 0 0 0];
if ~nearMines
label = '';
for k = 1:length(surrR)
if ~field.mines(surrR(k), surrC(k)) && ...
ishandle(field.squares(surrR(k), surrC(k))) && ...
~strcmp(get(field.text(k), 'String'), 'M')
digSquare(field, surrR(k), surrC(k))
end
end
end
set(field.text(r, c), 'String', label, 'Color', textColor(nearMines+1, :))
end
function gameLost(field, stopwatch, r, c)
stop(stopwatch)
setTitle(countMineFlags(field), 'X (', stopwatch.TasksExecuted)
set(field.squares(r, c), 'FaceColor', [1 0 0])
for k = 1:numel(field.text)
if field.mines(k) && any(get(field.text(k), 'String') == '.?')
set(field.text(k), 'String', '*', 'FontSize', 20)
elseif ~field.mines(k) && ishandle(field.squares(k)) && ...
get(field.text(k), 'String') == 'M'
set(field.text(k), 'String', 'X', 'Color', [1 0 0])
end
end
set(gca, 'HitTest', 'off')
queryPlayAgain('Game over')
end
function gameWon(field, stopwatch)
% Flag any leftover mines and indicate win
stop(stopwatch)
set(field.text(ishandle(field.squares)), 'String', 'M')
setTitle(0, 'B )', stopwatch.TasksExecuted)
set(gca, 'HitTest', 'off')
queryPlayAgain('Minefield cleared!')
end
function queryPlayAgain(msg)
% Ask player if they want to play again
% Reset game by closing and reopening figure
choice = questdlg(sprintf('%s\nWould you like to play again?', msg), ...
'', 'Yes', 'No', 'No');
if strcmp(choice, 'Yes')
close
Minesweeper
end
end
function cleanUp(obj, event, stopwatch)
% Stop and close down all necessary processes
stop(stopwatch)
delete(stopwatch)
delete(obj)
end