看代码注释

完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mineSweeper</title>
<style>
* {
padding: 0;
margin: 0;
}
body {
display: flex;
}
#main {
margin: 20px auto;
}
</style>
</head>
<body>
<div id="main"></div>
<script>
class Cell {
constructor(id, position, x, y, size, background, textColor, text, type) {
this.id = id;
this.position = position;
this.x = x;
this.y = y;
this.size = size; // 尺寸
this.background = background; // 背景色
this.textColor = textColor;// 文本颜色
this.text = text; // 文本: 0附近没有雷,不显示
this.type = type; // 0: 雷 1: 非雷
this.status = 0; // 0: 未打开 1: 打开 2: 标记 3: 疑问
}
}
class MineSweeper {
el; // 容器
canvas = document.createElement('canvas'); // 画布
ctx = this.canvas.getContext('2d'); // 画笔
canvasSize = [200, 280]; // 画布尺寸
header = 80; // 头部尺寸
size = [10, 10]; // 游戏格子个数
cellSize = 20; // 格子尺寸 -- 这里会在运行时重新计算
mineNumber = 10; // 雷数量
// 不同数字使用的不同颜色
textColors = [
'#FF7F00',
'#00aa00',
'#aa0000',
'#00dddd',
'#0000FF',
'#8B00FF',
'#307b80',
'#0b0733'
];
gamePadding = 2; // 游戏内边距
cellMargin = 2; // 每个格子的外边距
background = 'rgb(99,99,99)'; // 背景色
cellBackground = 'rgb(170,180,210)'; // 未打开的格子背景
openBackground = 'rgb(215,225,255)'; // 打开的格子的背景
mineBackground = 'rgb(100,110,140)'; // 雷的背景
mineTextColor = 'rgb(255,255,255)'; // 标识雷的文本的颜色
headerBackground = 'rgb(230, 230, 230)'; // 头部控制区背景
headerPadding = 10; // 头部控制区内边距
titleColor = 'rgb(0,0,0)'; // 头部文本颜色
timerColor = 'rgb(200,0,0)'; // 计时器颜色
playTimeText = '00:00:00'; // 计时器显示文本
playTime = 0; // 本局游戏游玩时间
startTime = 0; // 本局游戏开始时间
cells = []; // 格子数组
mines = []; // 雷数组
openSum = 0; // 打开的格子个数
gameStatus = 0; // 0: 未开始 1: 开始 2: 结束 3: 成功
markNum = 0; // 标记为雷的个数
startButtonSize = { // 开始按钮定位
id: 'start',
x: 94,
y: 23,
width: 45,
height: 20
}
resetButtonSize = { // 重置按钮定位
id: 'reset',
x: 143,
y: 23,
width: 45,
height: 20
}
rowAddButtonSize = { // 行增加按钮定位
id: 'rowAdd',
x: 43,
y: 8,
width: 12,
height: 12
}
rowSubtractButtonSize = { // 行减少按钮定位
id: 'rowSubtract',
x: 73,
y: 8,
width: 12,
height: 12
}
colAddButtonSize = { // 列增加按钮定位
id: 'colAdd',
x: 43,
y: 28,
width: 12,
height: 12
}
colSubtractButtonSize = { // 列减少按钮定位
id: 'colSubtract',
x: 73,
y: 28,
width: 12,
height: 12
}
mineAddButtonSize = { // 雷增加按钮定位
id: 'mineAdd',
x: 133,
y: 8,
width: 12,
height: 12
}
mineSubtractButtonSize = { // 雷减少按钮定位
id: 'mineSubtract',
x: 185,
y: 8,
width: 12,
height: 12
}
startButtonColor = 'rgb(0,200,0)'; // 开始按钮文本颜色
resetButtonColor = 'rgb(200,0,0)'; // 重置按钮文本颜色
buttons = [ // 全部按钮
this.startButtonSize,
this.resetButtonSize,
this.rowAddButtonSize,
this.rowSubtractButtonSize,
this.colAddButtonSize,
this.colSubtractButtonSize,
this.mineAddButtonSize,
this.mineSubtractButtonSize
];
hover; // 高亮的按钮
hoverColor = 'rgba(0,0,0, 0.2)'; // 按钮hover背景
constructor(el) {
this.el = el;
this.init();
}
init() {
this.initCanvas();
this.el.appendChild(this.canvas);
this.initData();
this.initEvent();
this.ani();
}
// 初始化canvas
initCanvas() {
this.canvas.width = this.canvasSize[0];
this.canvas.height = this.canvasSize[1];
this.canvas.style.background = this.background;
this.cellSize = (this.canvas.width - this.gamePadding * 2 - (this.size[0] - 1) * this.cellMargin) / this.size[0];
}
// 初始化格子
initData() {
for (let i = 0; i < this.size[0]; i++) {
this.cells[i] = [];
for (let j = 0; j < this.size[1]; j++) {
this.cells[i][j] = new Cell(
i + j,
[i, j],
this.gamePadding + i * (this.cellSize + this.cellMargin),
this.gamePadding + j * (this.cellSize + this.cellMargin) + this.header,
this.cellSize,
this.cellBackground,
'#000',
0, // ✳
1
);
}
}
this.randomMine(); // 布置雷位置
}
randomMine() {
let num = 0;
this.mines = [];
while (num < this.mineNumber) { // 根据雷个数布置雷
const i = this.random(this.size[0] - 1);
const j = this.random(this.size[1] - 1);
const cell = this.cells[i][j];
if (cell.type) {
cell.type = 0;
cell.text = '✳';
cell.textColor = this.mineTextColor;
this.mines.push(cell);
num++;
}
}
this.mines.forEach(item => {
this.calcRound(item); // 设置雷旁边格子的文本
});
}
calcRound(mine) {
const [i, j] = mine.position; // 雷坐标
this.cross(i, j, (i, j) => { // 雷一圈的格子坐标
if (this.cells[i][j].type !== 0) {
let num = 0;
this.cross(i, j, (i, j) => { // 判断周围雷个数
this.cells[i][j].type === 0 && num++;
});
this.cells[i][j].text = num; // 设置文本
this.cells[i][j].textColor = this.textColors[num - 1]; // 设置文本颜色
}
});
}
random(max, min = 0) {
return ~~(Math.random() * (max - min + 1) + min);
}
initEvent() {
this.canvas.addEventListener('click', (e) => {
if (this.gameStatus === 1) {
const cell = this.getClickCell(e); // 获取点中的格子
if (cell && cell.status === 0) {
this.openCell(cell); // 点中了格子并且格子状态为未打开
}
}
});
this.canvas.addEventListener('contextmenu', (e) => {
e.preventDefault();
if (this.gameStatus === 1) {
const cell = this.getClickCell(e);// 获取点中的格子
if (cell) {
if (cell.status === 0) { // 未打开设置成标记
cell.status = 2;
this.markNum++;
} else if (cell.status === 2) { // 标记状态设置成疑问
cell.status = 3;
this.markNum--;
} else if (cell.status === 3) { // 疑问状态设置为未打开
cell.status = 0;
}
}
}
});
this.canvas.addEventListener('click', (e) => {
if (this.hover) { // 根据点击的按钮进行相关交互
switch (this.hover.id) {
case 'start':
this.start();
break;
case 'reset':
this.reset();
break;
case 'colAdd':
if (this.size[0] < 50) {
this.size[0] = this.size[0] + 10;
}
this.changeSize(...this.size);
break;
case 'colSubtract':
if (this.size[0] > 10) {
this.size[0] = this.size[0] - 10;
}
this.changeSize(...this.size);
break;
case 'rowAdd':
if (this.size[1] < 50) {
this.size[1] = this.size[1] + 10;
}
this.changeSize(...this.size);
break;
case 'rowSubtract':
if (this.size[1] > 10) {
this.size[1] = this.size[1] - 10;
}
this.changeSize(...this.size);
break;
case 'mineAdd':
if (this.mineNumber < this.size[0] * this.size[1] * 0.6) {
this.mineNumber += 10;
}
this.changeSize(...this.size);
break;
case 'mineSubtract':
if (this.mineNumber > 10) {
this.mineNumber -= 10;
}
this.changeSize(...this.size);
break;
default:
break;
}
}
});
// 判断鼠标是否移动到按钮上
this.canvas.addEventListener('mousemove', (e) => {
const { offsetX, offsetY } = e;
let i = this.buttons.length;
this.hover = null;
while (i--) {
const button = this.buttons[i];
if (
offsetX > button.x
&& offsetX < button.x + button.width
&& offsetY > button.y
&& offsetY < button.y + button.height
) {
this.hover = button;
break;
}
}
});
}
// 获取鼠标当前位置是否在格子上
getClickCell(e) {
const { offsetX, offsetY } = e;
const i = ~~((offsetX - this.gamePadding + this.cellMargin) / (this.cellSize + this.cellMargin));
const lastI = this.gamePadding + i * (this.cellSize + this.cellMargin);
const j = ~~((offsetY - this.gamePadding + this.cellMargin - this.header) / (this.cellSize + this.cellMargin));
const lastJ = this.gamePadding + j * (this.cellSize + this.cellMargin) + this.header;
if (offsetX > lastI && offsetY > lastJ) {
return this.cells[i][j];
}
}
// 打开格子
openCell(cell) {
if (cell.status !== 0) { // 不是未打开状态无法打开
return;
}
cell.status = 1;
cell.background = this.openBackground;
this.openSum++;
if (cell.text) {
if (cell.type === 0) { // 雷结束 -- 点中了
this.gameOver(cell);
}
} else {
// 开8个方向格子 -- 待优化
const [i, j] = cell.position;
this.cross(i, j, (i, j) => this.openCell(this.cells[i][j]));
}
}
gameOver(cell) {
this.gameStatus = 2;
console.log('Game Over');
this.mines.forEach(item => { // 游戏结束显示全部的雷
item.status = 1;
item.background = this.mineBackground;
});
if (cell) { // 被点中的雷背景变红
cell.background = '#a00';
cell.textColor = '#fff';
} else {
console.log('Timeout');
}
}
// 边界判断--格子周围的八个格子--有格子则执行回调
cross(i, j, callback) {
j - 1 >= 0 && callback(i, j - 1); // 上
j + 1 < this.size[1] && callback(i, j + 1); // 下
if (i - 1 >= 0) {
callback(i - 1, j); // 左
j - 1 >= 0 && callback(i - 1, j - 1); // 左上
j + 1 < this.size[1] && callback(i - 1, j + 1); // 左下
}
if (i + 1 < this.size[0]) {
callback(i + 1, j); // 右
j - 1 >= 0 && callback(i + 1, j - 1); // 右上
j + 1 < this.size[1] && callback(i + 1, j + 1); // 右下
}
}
draw() {
this.drawHeader();
for (let i = 0; i < this.size[0]; i++) {
for (let j = 0; j < this.size[1]; j++) {
const cell = this.cells[i][j];
this.drawCell(cell);
switch (cell.status) { // 根据格子状态绘制格子
case 1:
if (cell.type) {
if (cell.text) {
this.drawText(cell);
}
} else {
this.drawCell(cell);
this.drawText(cell);
}
break;
case 2:
if (cell.type === 1 && this.gameStatus === 2) {
this.drawError(cell); // 标记错误的格子
} else {
this.drawMark(cell); // 标记的格子
}
break;
case 3:
this.drawQuestionMark(cell); // 疑问状态的格子
break;
default:
break;
}
}
}
if (this.gameStatus === 3) { // 扫雷成功
this.drawSuccess();
}
}
drawHeader() {
// 背景色
this.ctx.beginPath();
this.ctx.fillStyle = this.headerBackground;
this.ctx.fillRect(0, 0, this.canvasSize[0], this.header);
this.ctx.closePath();
if (this.hover) {
const { x, y, width, height } = this.hover;
this.ctx.fillStyle = this.hoverColor;
this.ctx.fillRect(x, y, width, height);
this.canvas.style.cursor = 'pointer';
} else {
this.canvas.style.cursor = 'default';
}
// row title
this.ctx.beginPath();
this.ctx.font = `16px cursive`;
this.ctx.fillStyle = this.titleColor;
this.ctx.textAlign = 'right';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('row:', this.headerPadding + 30, 15);
this.ctx.closePath();
// row ++
this.ctx.beginPath();
this.ctx.font = `20px cursive`;
this.ctx.fillStyle = this.titleColor;
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('+', this.headerPadding + 35, 15);
this.ctx.closePath();
// row
this.ctx.beginPath();
this.ctx.font = `16px cursive`;
this.ctx.fillStyle = this.titleColor;
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText(this.size[1], this.headerPadding + 45, 15);
this.ctx.closePath();
// row --
this.ctx.beginPath();
this.ctx.font = `20px cursive`;
this.ctx.fillStyle = this.titleColor;
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('-', this.headerPadding + 65, 15);
this.ctx.closePath();
// col title
this.ctx.beginPath();
this.ctx.font = `16px cursive`;
this.ctx.fillStyle = this.titleColor;
this.ctx.textAlign = 'right';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('col:', this.headerPadding + 30, 35);
this.ctx.closePath();
// col ++
this.ctx.beginPath();
this.ctx.font = `20px cursive`;
this.ctx.fillStyle = this.titleColor;
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('+', this.headerPadding + 35, 35);
this.ctx.closePath();
// col
this.ctx.beginPath();
this.ctx.font = `16px cursive`;
this.ctx.fillStyle = this.titleColor;
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText(this.size[0], this.headerPadding + 45, 35);
this.ctx.closePath();
// col --
this.ctx.beginPath();
this.ctx.font = `20px cursive`;
this.ctx.fillStyle = this.titleColor;
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('-', this.headerPadding + 65, 35);
this.ctx.closePath();
// mine title
this.ctx.beginPath();
this.ctx.font = `16px cursive`;
this.ctx.fillStyle = this.titleColor;
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('mine:', this.headerPadding + 80, 15);
this.ctx.closePath();
// mine ++
this.ctx.beginPath();
this.ctx.font = `20px cursive`;
this.ctx.fillStyle = this.titleColor;
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('+', this.headerPadding + 125, 15);
this.ctx.closePath();
// mine
this.ctx.beginPath();
this.ctx.font = `16px cursive`;
this.ctx.fillStyle = this.titleColor;
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillText(this.mineNumber, this.headerPadding + 155, 15);
this.ctx.closePath();
// mine --
this.ctx.beginPath();
this.ctx.font = `20px cursive`;
this.ctx.fillStyle = this.titleColor;
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('-', this.headerPadding + 177, 15);
this.ctx.closePath();
// start
this.ctx.beginPath();
this.ctx.font = `16px cursive`;
this.ctx.fillStyle = this.startButtonColor;
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('Start', this.headerPadding + 85, 35);
this.ctx.closePath();
// reset
this.ctx.beginPath();
this.ctx.font = `16px cursive`;
this.ctx.fillStyle = this.resetButtonColor;
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('Reset', this.headerPadding + 135, 35);
this.ctx.closePath();
// timer
this.ctx.beginPath();
this.ctx.font = `20px cursive`;
this.ctx.fillStyle = this.timerColor;
this.ctx.textAlign = 'left';
this.ctx.textBaseline = 'middle';
this.ctx.fillText(this.playTimeText, this.headerPadding + 45, 60);
this.ctx.closePath();
}
drawSuccess() {
// 画成功图层
const x = this.canvasSize[0] / 2;
const y = this.canvasSize[1] / 2 + 0.5 * this.header;
this.ctx.beginPath();
this.ctx.fillStyle = 'rgba(0,0,0,0.1)';
this.ctx.fillRect(0, this.header, this.canvasSize[0], this.canvasSize[1] - this.header);
this.ctx.closePath();
this.ctx.beginPath();
this.ctx.arc(x, y, 80, 0, Math.PI * 2);
this.ctx.fillStyle = 'rgba(0,0,0,0.2)';
this.ctx.fill();
this.ctx.closePath();
this.ctx.beginPath();
this.ctx.fillStyle = 'rgba(255,255,255,1)';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.font = '30px Verdana';
this.ctx.fillText('Success', x, y);
this.ctx.closePath();
}
// 画格子
drawCell(cell) {
const { x, y, size, background } = cell;
this.ctx.beginPath();
this.ctx.fillStyle = background;
this.ctx.fillRect(x, y, size, size);
this.ctx.closePath();
}
// 画文字
drawText(cell) {
const { text, x, y, size, textColor } = cell;
this.ctx.beginPath();
this.ctx.font = `${size * 0.7}px Verdana`;
this.ctx.fillStyle = textColor;
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillText(text, x + size / 2, y + size / 2);
this.ctx.closePath();
}
// 画问号
drawQuestionMark(cell) {
const { x, y, size, textColor } = cell;
this.ctx.beginPath();
this.ctx.font = `${size * 0.7}px Verdana`;
this.ctx.fillStyle = textColor;
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('?', x + size / 2, y + size / 2);
this.ctx.closePath();
}
// 画标记
drawMark(cell) {
const { x, y, size } = cell;
this.ctx.beginPath();
this.ctx.fillStyle = '#dd0000';
this.ctx.moveTo(x + size * 0.3, y + size * 0.2);
this.ctx.lineTo(x + size * 0.7, y + size * 0.35);
this.ctx.lineTo(x + size * 0.35, y + size * 0.5);
this.ctx.lineTo(x + size * 0.35, y + size * 0.8);
this.ctx.lineTo(x + size * 0.3, y + size * 0.8);
this.ctx.fill();
this.ctx.closePath();
}
// 画错误 -- 点中雷后标记错误的格子
drawError(cell) {
const { x, y, size } = cell;
this.ctx.beginPath();
this.ctx.fillStyle = '#aa0000';
this.ctx.fillRect(x, y, size, size);
this.ctx.closePath();
this.ctx.beginPath();
this.ctx.strokeStyle = '#ffffff';
this.ctx.lineWidth = this.cellSize / 7;
this.ctx.moveTo(x + 2, y + 2);
this.ctx.lineTo(x + size - 2, y + size - 2);
this.ctx.stroke();
this.ctx.closePath();
this.ctx.beginPath();
this.ctx.strokeStyle = '#ffffff';
this.ctx.lineWidth = this.cellSize / 7;
this.ctx.moveTo(x + size - 2, y + 2);
this.ctx.lineTo(x + 2, y + size - 2);
this.ctx.stroke();
this.ctx.closePath();
}
// 格式化计时器
formatTime(time) {
let mm = Math.floor(time / (1000 * 60));
const ss = Math.floor((time - mm * 60 * 1000) / 1000).toString().padStart(2, '0');
const ms = Math.floor((time - mm * 60 * 1000 - ss * 1000) / 10).toString().padStart(2, '0');
mm < 10 && (mm = mm.toString().padStart(2, '0'));
return `${mm}:${ss}:${ms}`;
}
// 计算时间
calcTimer() {
this.playTimeText = this.formatTime(this.playTime);
}
clear() {
this.ctx.clearRect(0, 0, ...this.canvasSize);
}
reset() {
this.mineNumber = this.mineNumber > this.size[0] * this.size[1] * 0.6 ? ~~(this.size[0] * this.size[1] * 0.6 / 10) * 10 : this.mineNumber;
this.initData();
this.gameStatus = 0;
this.markNum = 0;
this.openSum = 0;
this.playTimeText = '00:00:00';
}
start() {
if (this.gameStatus === 0) {
this.gameStatus = 1;
this.startTime = Date.now();
}
}
changeSize(x, y) {
this.size = [x, y];
this.canvasSize = [x * 20, y * 20 + this.header];
this.initCanvas();
this.reset();
}
changeMineNumber(num) {
this.mineNumber = num;
}
ani = () => {
this.clear();
this.draw();
if (this.gameStatus === 1) {
// 计时器工作
this.playTime = Date.now() - this.startTime;
if (this.playTime > 5940000) { // 超时结束
this.gameOver();
}
this.calcTimer();
if (this.openSum === this.size[0] * this.size[1] - this.mineNumber && this.markNum === this.mineNumber) {
this.gameStatus = 3;
console.log('Success');
}
}
requestAnimationFrame(this.ani);
}
}
const mineSweeper = new MineSweeper(document.getElementById('main'));
</script>
</body>
</html>