<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>实现一个远古扫雷游戏</title>
<link
href="https://fonts.googleapis.com/css?family=Space+Mono:400,700"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css"
/>
<style>
@charset "UTF-8";
*,
*:before,
*:after {
box-sizing: inherit;
}
html {
box-sizing: border-box;
}
body {
text-align: center;
color: #333;
padding: 20px 20px 100px;
background-color: #efefef;
font-family: 'Space Mono', monospace;
}
h1 {
margin: 0 0 32px;
}
p {
font-size: 1em;
line-height: 1.25em;
}
a {
color: inherit;
}
img {
height: auto;
max-width: 100%;
}
.wrap {
text-align: center;
position: relative;
display: inline-block;
}
.legend {
font-size: 14px;
margin: 0 auto 32px;
}
.legend h4,
.legend p {
margin: 0 0 6px;
}
.legend code {
background: #e3e3e3;
}
.legend code .key {
color: #ec433c;
}
.legend code .click {
color: #2a48ec;
}
.top {
border: 6px solid #e3e3e3;
background-color: #e3e3e3;
}
#scoreboard {
display: flex;
padding-bottom: 12px;
justify-content: space-between;
}
#scoreboard .select-wrap {
font-weight: 700;
vertical-align: top;
display: inline-block;
}
#scoreboard .select-wrap select {
margin: 0;
height: 36px;
border-radius: 0;
border-width: 3px;
border-style: solid;
background-color: #d1d1d1;
border-color: white #9e9e9e #9e9e9e white;
}
#scoreboard .select-wrap select:hover,
#scoreboard .select-wrap select:focus {
backgroudn-color: #ebebeb;
}
#scoreboard .reset {
padding: 0 4px;
font-size: 24px;
cursor: pointer;
font-weight: 700;
line-height: 30px;
border-width: 3px;
border-style: solid;
background-color: #d1d1d1;
border-color: white #9e9e9e #9e9e9e white;
}
#scoreboard .reset:hover,
#scoreboard .reset:focus {
outline: none;
background-color: #ebebeb;
}
#scoreboard .counter {
padding: 0 4px;
color: #ec433c;
border: 3px inset;
line-height: 30px;
letter-spacing: 0.08em;
display: inline-block;
background: #333;
text-shadow: 0 0 2px #ec433c;
}
#scoreboard .counter:first-of-type {
margin-right: 20px;
}
#scoreboard .counter:last-of-type {
margin-left: 20px;
}
#grid {
margin: 0 auto;
position: relative;
display: inline-block;
}
#grid ::-moz-selection {
background-color: transparent;
}
#grid ::selection {
background-color: transparent;
}
#grid.disabled .cell {
pointer-events: none;
}
#grid.disabled .status-indicator {
top: 50%;
left: 50%;
z-index: 11;
width: 8vw;
height: 8vw;
font-size: 4vw;
cursor: pointer;
line-height: 8vw;
position: absolute;
border-radius: 50%;
pointer-events: auto;
background-color: #d1d1d1;
transform: translate(-50%, -50%);
border: 1px solid rgba(51, 51, 51, 0.25);
}
#grid.disabled .status-indicator::after {
content: '';
}
#grid.disabled.win .status-indicator::after {
content: '😎';
}
#grid.disabled.lose .status-indicator::after {
content: '☹️';
}
#grid .row {
display: flex;
}
#grid .cell {
cursor: pointer;
width: 24px;
height: 24px;
position: relative;
background-color: #d1d1d1;
border-width: 3px;
border-style: solid;
border-color: white #9e9e9e #9e9e9e white;
}
#grid .cell i {
left: 0;
bottom: 0;
margin: 0;
width: 100%;
font-size: 14px;
font-weight: 700;
font-style: normal;
position: absolute;
line-height: 24px;
}
#grid .cell::before {
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
content: '';
position: absolute;
background-color: #d1d1d1;
}
#grid .cell::after {
top: 50%;
left: 50%;
content: '';
position: absolute;
transform: translate(-50%, -50%);
}
#grid .cell:hover::before {
background-color: #ebebeb;
}
#grid .cell.revealed {
border: 1px solid #b8b8b8;
}
#grid .cell.revealed::before {
display: none;
}
#grid .cell.revealed .flag {
display: none;
}
#grid .cell .flag {
top: 50%;
left: 50%;
z-index: 1;
width: 10px;
height: 4px;
position: absolute;
border-style: solid;
border-width: 4px 0 4px 10px;
transform: translate(-50%, -50%);
border-color: transparent transparent transparent #ec433c;
}
#grid .cell.maybe .flag {
border-color: transparent transparent transparent #d7af42;
}
#grid .cell.mine {
background-color: #ec433c;
}
#grid .cell.mine::after {
border-radius: 50%;
width: 12px;
height: 12px;
background-color: #333;
}
#grid .cell.incorrect .flag::before,
#grid .cell.incorrect .flag::after {
top: 50%;
z-index: 1;
left: -13px;
height: 2px;
width: 16px;
content: '';
position: absolute;
background-color: black;
}
#grid .cell.incorrect .flag::before {
transform: rotate(-45deg);
}
#grid .cell.incorrect .flag::after {
transform: rotate(45deg);
}
#grid .cell.mousedown {
border: none;
}
#leaderboard {
margin-top: 20px;
text-align: center;
}
#leaderboard h4 {
margin: 0 0 10px;
}
#leaderboard ul {
margin: 0;
padding: 10px;
display: inline-block;
background-color: rgba(209, 209, 209, 0.5);
}
#leaderboard ul li {
padding: 2px;
list-style: none;
}
#leaderboard ul li span {
font-weight: 900;
text-transform: capitalize;
}
#leaderboard ul li.highlight {
background-color: #fef178;
}
#leaderboard button {
border: none;
outline: none;
cursor: pointer;
font-size: 12px;
font-weight: 700;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: none;
text-decoration: underline;
}
.cell.white {
color: #fff;
}
.cell.gray-light {
color: #efefef;
}
.cell.gray-mid {
color: #e3e3e3;
}
.cell.gray {
color: #d1d1d1;
}
.cell.gray-dark {
color: #808080;
}
.cell.black {
color: #333;
}
.cell.red {
color: #ec433c;
}
.cell.maroon {
color: #a6070f;
}
.cell.purple {
color: #a42887;
}
.cell.yellow {
color: #d7af42;
}
.cell.yellow-light {
color: #fef178;
}
.cell.blue {
color: #2a48ec;
}
.cell.blue-dark {
color: #233db7;
}
.cell.green {
color: #2bb13d;
}
.cell.turquoise {
color: #28907d;
}
#devbox {
top: 10px;
right: 10px;
position: fixed;
text-align: left;
max-width: 300px;
color: #fff;
background-color: #333;
}
#devbox p {
margin: 0;
font-size: 12px;
padding: 10px 20px;
}
#devbox p + p {
padding-top: 5px;
}
</style>
</head>
<body>
<header class="site-header" role="banner">
<div class="wrap">
<h1>扫雷</h1>
<div class="legend">
<p>
<strong>插旗:</strong>
<code
><span class="key">alt</span>+<span class="click"
>click</span
></code
>
</p>
</div>
</div>
</header>
<main class="site-main" role="main">
<div class="wrap">
<div id="board">
<div class="top">
<div id="scoreboard">
<div id="minecounter" class="counter"></div>
<div>
<div class="select-wrap">
<select name="level" id="level">
<option value="beginner">初级</option>
<option value="intermediate">中级</option>
<option value="expert" selected>高级</option>
</select>
</div>
<button class="reset">🙂</button>
</div>
<div id="timer" class="counter"></div>
</div>
<div id="grid"></div>
</div>
<div class="bottom"></div>
</div>
</div>
</main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
$(document).ready(function () {
var $body, $document, $board, $grid, timer, time, unstarted;
function _init() {
$document = $(document);
$body = $('body');
$body.addClass('loaded');
$board = $('#board');
$grid = $('#grid');
var $timer = $('#timer');
var $mineCounter = $('#minecounter');
var $levelSelect = $('#level');
var levels = {
beginner: '9x9x10',
intermediate: '16x16x44',
expert: '16x30x99',
};
var level = $levelSelect.val();
var levelParams,
rows,
$rows,
columns,
cellCount,
mines,
freeCells,
mineTally,
pauseTime,
beginnerHighScore = 999,
intermediateHighScore = 999,
expertHighScore = 999;
var countColors = {
0: '',
1: 'blue',
2: 'green',
3: 'red',
4: 'blue-dark',
5: 'maroon',
6: 'turquoise',
7: 'purple',
8: 'gray-dark',
};
time = 0;
timer = false;
unstarted = true;
var statusIndicator = '<div class="status-indicator"></div>';
function setLevel(level) {
levelParams = levels[level];
rows = parseInt(levelParams.split('x')[0]);
columns = parseInt(levelParams.split('x')[1]);
cellCount = rows * columns;
mines = levelParams.split('x')[2];
freeCells = cellCount - mines;
}
function setBoard(level) {
$grid
.html(statusIndicator)
.removeClass('disabled lose win')
.addClass('unstarted');
setLevel(level);
unstarted = true;
for (r = 0; r < rows; r++) {
var newCells = '';
for (c = 0; c < columns; c++) {
newCells += '<div class="cell"></div>';
}
$grid.append('<div class="row">' + newCells + '</div>');
}
mineTally = mines;
$mineCounter.html(mineTally);
resetTimer();
}
setBoard(level);
$('html')
.on('mousedown', '.reset', function () {
$(this).text('😮');
})
.on('mouseup', '.reset', function () {
$(this).text('🙂');
stopTimer();
level = $levelSelect.val();
setBoard(level);
});
$('html').on('click', '.status-indicator', function () {
level = $levelSelect.val();
setBoard(level);
});
$levelSelect.on('change', function () {
stopTimer();
resetTimer();
level = $levelSelect.val();
setBoard(level);
});
function layMines(level, clickedCellIndex) {
$rows = $('.row');
var freeCells = $('.cell');
var takenCells = [clickedCellIndex];
for (m = 0; m < mines; m++) {
var mineCell = Math.floor(
Math.random() * Math.floor(freeCells.length)
);
if ($.inArray(mineCell, takenCells) > -1) {
m--;
continue;
}
takenCells.push(mineCell);
$(freeCells[mineCell]).addClass('mine');
}
var $cells = $('.cell');
for (c = 0; c < $cells.length; c++) {
var $cell = $($cells[c]);
$cell.attr('data-cell', c);
if ($cell.is('.mine')) {
continue;
}
var mineCount = 0;
var rowPos = Math.floor(c / columns);
var $currentRow = $cell.closest('.row');
$currentRow.attr('data-row', rowPos);
var rowCells = $currentRow.find('.cell');
var cellPos = c % columns;
if ($(rowCells[cellPos - 1]).is('.mine')) {
mineCount++;
}
if ($(rowCells[cellPos + 1]).is('.mine')) {
mineCount++;
}
if (rowPos > 0) {
var prevRowCells = $($rows[rowPos - 1]).find('.cell');
if ($(prevRowCells[cellPos - 1]).is('.mine')) {
mineCount++;
}
if ($(prevRowCells[cellPos]).is('.mine')) {
mineCount++;
}
if ($(prevRowCells[cellPos + 1]).is('.mine')) {
mineCount++;
}
}
if (rowPos < rows - 1) {
var nextRowCells = $($rows[rowPos + 1]).find('.cell');
if ($(nextRowCells[cellPos - 1]).is('.mine')) {
mineCount++;
}
if ($(nextRowCells[cellPos]).is('.mine')) {
mineCount++;
}
if ($(nextRowCells[cellPos + 1]).is('.mine')) {
mineCount++;
}
}
if (mineCount > 0) {
$cell.html('<i>' + mineCount + '</i>');
var colorClass = countColors[mineCount];
$cell.addClass(colorClass);
} else {
$cell.addClass('zero');
}
}
}
$('html')
.off('click', '#grid.unstarted .cell')
.on('click', '#grid.unstarted .cell', function (e) {
$grid.removeClass('unstarted');
if (unstarted && !$(e.target).is('.mine')) {
layMines(level, $('.cell').index(this));
timer = window.setInterval(startTimer, 1000);
unstarted = false;
}
});
function resetTimer() {
$timer.html('000');
time = 0;
}
function startTimer() {
time++;
if (time < 10) {
$timer.html('00' + time);
} else if (time > 9 && time < 100) {
$timer.html('0' + time);
} else {
$timer.html(time);
}
}
function stopTimer() {
window.clearInterval(timer);
}
function pauseTimer() {
stopTimer();
pauseTime = parseInt($('#timer').html());
}
function unpauseTimer() {
time = pauseTime;
timer = window.setInterval(startTimer, 1000);
pauseTime = false;
}
$(window)
.on('blur', function () {
pauseTimer();
})
.on('focus', function () {
if (pauseTime) {
unpauseTimer();
}
});
function checkCell($cell) {
if (!$cell.is('.mine') && !$cell.is('.revealed')) {
cellClick($cell, 'reveal');
if ($cell.is('.zero')) {
$cell.trigger('click');
}
}
}
function cellClick($cell, action) {
if (action === 'flag' && !$cell.is('.revealed')) {
if ($cell.is('.flagged')) {
$cell.removeClass('flagged');
$cell.addClass('maybe');
mineTally++;
updateMinecounter(mineTally);
} else if ($cell.is('.maybe')) {
$cell.removeClass('maybe');
var flag = $cell.find('.flag');
flag.remove();
} else {
$cell.addClass('flagged');
$cell.append('<span class="flag"></span>');
mineTally--;
updateMinecounter(mineTally);
}
} else if (action === 'reveal') {
$cell.addClass('revealed');
if ($cell.is('.mine')) {
lose();
}
statusCheck();
} else if (action === 'clear') {
if (!$cell.is('.revealed') || $cell.is('.zero')) {
return;
}
clearClick($cell);
}
}
function updateMinecounter(mineTally) {
if (mineTally < 10) {
$mineCounter.html('0' + mineTally);
} else {
$mineCounter.html(mineTally);
}
}
function zeroClick($cell) {
var cellPos = $cell.prevAll().length;
var $currentRow = $cell.closest('.row');
var rowPos = parseInt($currentRow.attr('data-row'));
var rowCells = $currentRow.find('.cell');
checkCell($(rowCells[cellPos - 1]));
checkCell($(rowCells[cellPos + 1]));
if (rowPos > 0) {
var prevRowCells = $($rows[rowPos - 1]).find('.cell');
checkCell($(prevRowCells[cellPos - 1]));
checkCell($(prevRowCells[cellPos]));
checkCell($(prevRowCells[cellPos + 1]));
}
if (rowPos < rows) {
var nextRowCells = $($rows[rowPos + 1]).find('.cell');
checkCell($(nextRowCells[cellPos - 1]));
checkCell($(nextRowCells[cellPos]));
checkCell($(nextRowCells[cellPos + 1]));
}
}
function clearClick($cell) {
var cellPos = $cell.prevAll().length;
var $currentRow = $cell.closest('.row');
var rowPos = parseInt($currentRow.attr('data-row'));
var rowCells = $currentRow.find('.cell');
var adjacentCells = [];
var correctClear = true;
var adjacentMines = 0;
var adjacentFlags = 0;
var i;
adjacentCells.push($(rowCells[cellPos - 1]));
adjacentCells.push($(rowCells[cellPos + 1]));
if (rowPos > 0) {
var prevRowCells = $($rows[rowPos - 1]).find('.cell');
adjacentCells.push($(prevRowCells[cellPos - 1]));
adjacentCells.push($(prevRowCells[cellPos]));
adjacentCells.push($(prevRowCells[cellPos + 1]));
}
if (rowPos < rows) {
var nextRowCells = $($rows[rowPos + 1]).find('.cell');
adjacentCells.push($(nextRowCells[cellPos - 1]));
adjacentCells.push($(nextRowCells[cellPos]));
adjacentCells.push($(nextRowCells[cellPos + 1]));
}
for (i = 0; i < adjacentCells.length; i++) {
if ($(adjacentCells[i]).is('.mine')) {
adjacentMines++;
}
if ($(adjacentCells[i]).is('.flagged')) {
adjacentFlags++;
}
}
if (adjacentFlags === adjacentMines) {
for (i = 0; i < adjacentCells.length; i++) {
if ($(adjacentCells[i]).is('.mine')) {
if ($(adjacentCells[i]).is('.flagged')) {
continue;
} else {
$(adjacentCells[i]).addClass('revealed');
correctClear = false;
}
} else if ($(adjacentCells[i]).is('.flagged')) {
correctClear = false;
$(adjacentCells[i]).addClass('incorrect');
lose();
}
}
if (correctClear) {
for (i = 0; i < adjacentCells.length; i++) {
if (!$(adjacentCells[i]).is('.mine')) {
if ($(adjacentCells[i]).is('.zero')) {
zeroClick($(adjacentCells[i]));
}
cellClick($(adjacentCells[i]), 'reveal');
}
}
}
} else {
return;
}
}
function statusCheck() {
if ($('.cell.revealed').length == freeCells) {
stopTimer();
var winTime = $('#timer').html();
$grid.addClass('disabled win');
resetHighScore(level, winTime);
}
}
function lose() {
$grid.addClass('disabled lose');
stopTimer();
}
$('html').on('click', '.cell', function (e) {
e.preventDefault();
var action = 'reveal';
var $cell = $(this);
if (e.altKey || e.which === 3) {
action = 'flag';
} else if (
$cell.is('.revealed') ||
(e.which === 1) & (e.which === 3)
) {
action = 'clear';
}
if ($cell.is('.flagged') && !e.altKey) {
return;
}
if ($cell.is('.zero')) {
zeroClick($cell);
}
cellClick($cell, action);
});
$('html')
.on('mousedown', '.cell:not(.revealed,.flagged)', function (e) {
if (!e.altKey && e.which !== 3) {
$(this).addClass('mousedown');
}
})
.on('mouseup mouseleave', '.cell.mousedown', function () {
$(this).removeClass('mousedown');
});
function resetHighScore(level, winTime) {
if (localStorage.getItem(level)) {
if (winTime < localStorage.getItem(level)) {
localStorage.setItem(level, winTime);
populateHighScore(level, winTime, true);
}
} else {
localStorage.setItem(level, winTime);
populateHighScore(level, winTime, true);
}
}
function populateHighScore(level, highScore, highlight) {
if (!$('#leaderboard').length) {
$board
.find('.bottom')
.append(
'<div id="leaderboard"><h4>High Scores</h4><ul><li class="beginner"></li><li class="intermediate"></li><li class="expert"></li></ul><div><button id="score-reset" class="score-reset">Clear Scores</button></div></div>'
);
}
if (highlight === true) {
$('#leaderboard .highlight:not(.' + level + ')').removeClass(
'highlight'
);
$('#leaderboard .' + level).addClass('highlight');
}
var highScoreDisplay = parseInt(highScore, 10);
$('#leaderboard .' + level).html(
'<span>' + level + '</span>: ' + highScoreDisplay + ' seconds'
);
}
function clearScores() {
localStorage.clear();
$('#leaderboard').remove();
}
$('html').on('click', '#score-reset', clearScores);
}
_init();
});
</script>
</body>
</html>