最近对扫雷比较感兴趣 就打算模仿者写一下算法
全程都是自己想到的 不一定是标准的 但是可以用
效果图如下:
实现过程以及原理:
前置全局数据
// 画布元素
const canvasDom = document.querySelector("#canvas");
// 基础信息
const config = {
dom: canvasDom,
height: canvasDom.offsetHeight,
width: canvasDom.offsetWidth,
top: canvasDom.offsetTop,
left: canvasDom.offsetLeft,
right: 0,
bottom: 0,
};
config.right = config.left + config.width;
config.bottom = config.top + config.height;
console.log("config -->>", config);
// 网格大小
const gridSize = {
xSize: 20,
YSize: 20,
boomSize: 90,
boomSizeBack: 0,
nowBoomSize: 0
};
// 每个格子大小
const gridInfo = {
gridWidth: config.width / gridSize.xSize,
gridHeight: config.height / gridSize.YSize,
};
let isEnd = false; // 是否已经结束
let firstClick = true; // 是否第一次点击
console.log("gridInfo -->>", gridInfo);
1 生成网格信息
实现思路:
1.1:生成网格
其中 xSize*YSize
则是最大格子数量,boomSize
则是最大生成雷的数量,
通过生成出来的数据可以包含以下信息:open
是否被扫开isBoom
是否属于雷offsetX,offsetY
偏移量x
,y当前雷的坐标width,height
宽度。
然后进行数据保存 我是通过对象的形式进行保存的 可以优化一些读取速度 具体形式如下:
当中的key 是通过当前 x,y 坐标轴方式记录 `${x},${y}`如下
'0,0': {
x: '',
y: '',
offsetX: '',
offsetY: '',
width: '',
height: '',
open: false,
isBoom: false,
},
'0,1':{
...
},
'19,19' : {
...
}
实现代码如下:
// 初始化表格数据
function initData() {
/**
* gridSize: 网格大小
* gridInfo: 单个网格信息
*/
// 获取雷的生成数量
gridSize.boomSizeBack = gridSize.boomSize;
gridSize.nowBoomSize = gridSize.boomSize;
// 简单判断是否超出容器
if (gridSize.boomSizeBack >= gridSize.xSize * gridSize.YSize + 9) {
throw "雷的数量超过格子总数";
}
for (let yIndex = 0; yIndex < gridSize.YSize; yIndex++) {
for (let xIndex = 0; xIndex < gridSize.xSize; xIndex++) {
const item = {
x: xIndex,
y: yIndex,
offsetX: xIndex * gridInfo.gridWidth,
offsetY: yIndex * gridInfo.gridHeight,
width: gridInfo.gridWidth,
height: gridInfo.gridHeight,
open: false,
isBoom: false,
};
gridStore.setId(`${xIndex},${yIndex}`, item);
}
}
}
1.2 第一次开图
我目前是 第一次点击无论如何都不能触发雷
的思路所以实现思路如下:
// 获取x、y轴
function getEventPosition(ev) {
let x, y;
if (ev.layerX || ev.layerX == 0) {
x = ev.layerX;
y = ev.layerY;
} else if (ev.offsetX || ev.offsetX == 0) {
// Opera
x = ev.offsetX;
y = ev.offsetY;
}
return { x, y };
}
canvasDom.addEventListener(
"mouseup",
function (e) {
// 获取点击坐标
const p = getEventPosition(e);
// 通过点击道德坐标获取相对应数据Id
let x = p.x;
let y = p.y;
x = target.x - e.left;
y = target.y - e.top;
let xIndex = Math.ceil(x / gridInfo.gridWidth) - 1;
let yIndex = Math.ceil(y / gridInfo.gridHeight) - 1;
const target = gridStore.getId(`${xIndex},${yIndex}`);
if (target) {
if (!firstClick) {
firstClick = false;
// 如果是第一次点击 则进行打开地图 防止第一次点击就触发雷
openMap(xIndex, yIndex);
initBoom();
}
}
// 刷新显示
reloadGrid();
},
false
);
function openMap(x, y) {
for (let xIndex = x - 1; xIndex < x + 2; xIndex++) {
for (let yIndex = y - 1; yIndex < y + 2; yIndex++) {
const id = `${xIndex},${yIndex}`;
const target = gridStore.getId(id);
if (target) {
target.open = true;
}
}
}
}
其中openMap
获取到的坐标点 通过 x-1 x+2
的方式 获取九宫格需要循环的x轴 y-1 y+2
的方式获取九宫格需要循环的y轴 得到的则是 3x3
的空间 将周围的数据标记为 open
1.3 生成雷
这里进行的方式是通过定义的雷的数量进行循环 随机获取数据中的下标
其中 为了防止随机到的可能会出现相同的 则会进行递归调用
// 初始化雷
function initBoom() {
const gridData = gridStore.getStore();
const gridList = Object.keys(gridData);
const boomList = [];
let sameIndex = 0;
for (let index = 0; index < gridSize.boomSizeBack; index++) {
// 随机取到下标 进行随机生成雷
const targetIndex = Math.floor(Math.random() * gridList.length);
const id = gridList[targetIndex];
const item = gridStore.getId(id);
if (!item.isBoom && !item.open) {
item.isBoom = true;
boomList.push(item);
boomStore.setId(id, item);
} else {
// 如果出现随机到相同的下标 进行标记 后续重新随机生成
sameIndex += 1;
}
}
gridSize.boomSizeBack = sameIndex;
// 如果包含相同的下标 则进行递归调用 保证雷生成出来的数量是和指定的相同
if (sameIndex) {
initBoom();
} else {
// 调用自动开图
autoOpenMap();
}
}
1.4 第一次自动打开安全的区域
其中 这一部分我想到的就是 暴力循环 还没有想到其他的快速计算的方式
思路是 循环所有的数据 进行9宫格
判断是否包含有雷 如果都没有雷 则进行打开
// 自动打开地图
function autoOpenMap() {
const gridData = gridStore.getStore();
for (const key in gridData) {
const safeItem = gridData[key];
isSafeMap(safeItem.x, safeItem.y);
const safeNumber = getSafeNumber(safeItem.x, safeItem.y);
safeItem.number = safeNumber;
}
}
// 进行九宫格判断
function isSafeMap(x, y) {
const tempMap = {};
let isSafe = true;
safeFor: for (let xIndex = x - 1; xIndex < x + 2; xIndex++) {
for (let yIndex = y - 1; yIndex < y + 2; yIndex++) {
const id = `${xIndex},${yIndex}`;
const target = gridStore.getId(id);
if (target) {
tempMap[id] = target;
if (target.isBoom) {
isSafe = false;
break safeFor; // 整段跳出循环
}
}
}
}
if (isSafe) {
// 如果没有发现雷 则进行打开
for (const key in tempMap) {
const item = tempMap[key];
item.open = true;
safeStore.setId(key, item);
}
}
}
1.4 标记周围雷数量
这个就是简单的循环进行数数字了 也算是暴力循环 待优化
// 添加周围数字提醒
function getSafeNumber(x, y) {
let safeIndex = 0;
for (let xIndex = x - 1; xIndex < x + 2; xIndex++) {
for (let yIndex = y - 1; yIndex < y + 2; yIndex++) {
const id = `${xIndex},${yIndex}`;
const target = gridStore.getId(id);
if (target && target.isBoom) {
safeIndex += 1;
}
}
}
return safeIndex;
}
1.5 标记后一键打开周围的地图
当点击数字后进行判断周围的标记数量 如果标记数量和当前的数字一样 则进行打开其余为打开的区域 当然如果是标记错误的进行点击到雷处理
// 打开标记周围的图
function openMarkMap(target) {
const x = target.x;
const y = target.y;
let index = 0;
let targetNumber = target.number;
const mapList = [];
for (let xIndex = x - 1; xIndex < x + 2; xIndex++) {
for (let yIndex = y - 1; yIndex < y + 2; yIndex++) {
const id = `${xIndex},${yIndex}`;
const target = gridStore.getId(id);
if (target) {
mapList.push(target);
if (target.mark) {
index += 1;
}
}
}
}
if (index === targetNumber) {
for (const item of mapList) {
if (!item.mark) {
if (item.isBoom) {
touchBoom(item);
} else {
item.open = true;
gridSize.nowBoomSize -= 1;
safeStore.setId(`${item.x},${item.y}`, item);
}
}
}
}
}
基本上主要思路就这些 其实写下来感觉没有想象中的那么难 当然我这个是比较简单的方式 后续可能还会稍微改改 优化优化 完整项目如下:
https://github.com/SDSGK/minesweeper