js 游戏编程:扫雷
全程无参考,自行设计编码,2小时速通:
<meta charset="UTF-8">
<style>
* {
-webkit-touch-callout:none;
-webkit-user-select:none;
-khtml-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none;
}
input , textarea{
-webkit-user-select:auto;
user-select: auto;
}
section {
white-space: nowrap;
min-width: 1024px;
}
.unit, .active, .bomb, .white, .flag {
text-align: center;
width: 2.8rem;
height: 2.8rem;
background-color: rgb(129, 169, 245);
color: white;
font-weight: bolder;
line-height: 25px;
}
.active {
background-color: rgb(61, 105, 187) !important;
}
.bomb {
background-color: rgb(227, 40, 40) !important;
}
.white {
background-color: rgb(73, 94, 133) !important;
}
.flag {
background-color: rgb(247, 169, 86) !important;
}
body {
overflow: auto;
}
span {
animation: rot 2s linear infinite;
font-size: 1.5rem;
line-height: 27px;
display: block;
width: 2.1rem;
height: 2.1rem;
}
@keyframes rot {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<div id="main"></div>
<script>
document.addEventListener('touchstart', e =>
e.preventDefault());
// 数学工具
const random = () => Math.random();
const floor = (v) => Math.floor(v);
// main 区域
let main = document.getElementById('main');
///// 地板 mtx - 地板单位元素 box
// 1 - 创建 地板 mtx 的容器
let mtx;
let rn = 10;
let cn = 10;
let crtMtx = (rnn, cnn) => {
// 创建行容器
if(rnn !== undefined) rn = rnn;
if(cnn !== undefined) cn = cnn;
mtx = new Array(rn);
for(var i = 0; i < rn; i++){
// 创建当前行 每一列 box 的容器
mtx[i] = new Array(cn + 1);
// 每一行的第一列 为 当前行每一列 box 的父容器(行) sec
mtx[i][0] = document.createElement('section');
// 将每一行对应的 sec 容器 渲染到 main 容器上
main.appendChild(mtx[i][0]);
}
}
// 2 - 创建 地板元素 box(包括 这个地砖 的相关数据)
let geneBoxAndData = (x, y) => {
var box = document.createElement('button');
box.className = 'unit';
box.innerHTML = ' ';
// 记录 box 的坐标
box.setAttribute("x", x);
box.setAttribute("y", y);
// var val = 0; // 是雷:-1,不是雷-周围雷的个数:0-8,被打开 +8
return {"e":box, "v":0};
}
// 3 - 整个地板,生成每个地砖,并渲染到页面
let geneMtxData = () => {
for(var r = 0; r < mtx.length; r++){
var colSec = mtx[r][0];
for(var c = 1; c < mtx[0].length; c++){
// 生成数据
var data = geneBoxAndData(r, c);
// 加入dom和数据
colSec.appendChild(data.e);
mtx[r][c] = data;
}
}
}
//// 1 - 整个地板,地雷生成(在mtx上生成num个雷)
//// 2 - 必须管理每个地雷 --- 地雷列表
//// 2.1 - 列表的元素 是地雷的坐标(r, c)
let bombs = [];
// 2.2 - 第一次点击之后的位置 不能是地雷
let first = [5, 5];
let geneBombs = (num) => {
if(num > (rn * cn) >> 2) {
alert('雷的数量不能超过当前地砖的25%');
initGlobal();
return;
}
var rdmRow = floor(random() * (mtx.length - 1));
var rdmCol = floor(random() * (mtx[0].length - 1)) + 1;
for(var i = 0; i < num; i++){
while(mtx[rdmRow][rdmCol].v === -1 ||
(rdmRow < first[0] + 2 && rdmRow > first[0] - 2 &&
rdmCol < first[1] + 2 && rdmCol > first[1] - 2)){
rdmRow = floor(random() * (mtx.length - 1));
rdmCol = floor(random() * (mtx[0].length - 1)) + 1;
} mtx[rdmRow][rdmCol].v = -1;
// 管理这个雷的位置
bombs.push([rdmRow, rdmCol]);
}
}
//// 确定 整个地板-每个地砖 的地雷数据
// 1 - 确定 一个雷对周围地砖的影响
let oneBombData = (r, c) => {
for(var i = -1; i < 2; i++){
var row = r + i;
// 行存在
if(row < 0 || row >= mtx.length) continue;
for(var j = -1; j < 2; j++){
var col = c + j;
// 列存在
if(col < 1 || col >= mtx[0].length) continue;
// 当前行列位置 不是雷
if(mtx[row][col].v != -1) mtx[row][col].v++;
}
}
}
// 2 - 确定 所有地雷对整个地砖的影响
let allBombsData = () => {
for(var i = 0; i < bombs.length; i++)
oneBombData(bombs[i][0], bombs[i][1]);
}
let renderOne = (r, c) => {
var data = mtx[r][c];
if(data.v < 1) data.e.innerHTML = data.v == -1 ? '<span>原</span>' : ' ';
else data.e.innerHTML = data.v;
if(data.v == -1) {
if(!flags.has(`${r},${c}`)) data.e.className = "bomb";
}
else if(data.v == 0) data.e.className = "white";
else data.e.className = "active";
}
//// 渲染一个地砖 并访问
let renderVisitOne = (r, c)=>{
// 渲染
renderOne(r, c);
// 访问
vst.set(`${r},${c}`, "");
vstnum++;
}
let renderAll = () => {
for(var r = 0; r < mtx.length; r++){
for(var c = 1; c < mtx[0].length; c++)
renderOne(r, c);
}
}
// 求单次点击最大扩大范围
// - 防止重复遍历,记录已访问位置
let vst = new Map();
let dfs = (r, c) => {
renderVisitOne(r, c);
if(mtx[r][c].v == -1) {
isFail = true;
return;
}
for(var i = -1; i < 2; i++){
var row = r + i;
// 行存在
if(row < 0 || row >= mtx.length) continue;
for(var j = -1; j < 2; j++){
var col = c + j;
// 列存在
if(col < 1 || col >= mtx[0].length) continue;
// 当前行列位置 是0
if(!vst.has(`${row},${col}`)){
if(mtx[row][col].v == 0) dfs(row, col);
else if(mtx[r][c].v == 0 && mtx[row][col].v > 0 ) renderVisitOne(row, col);
}
}
}
}
// main
// 长按逻辑 - 是否插旗了
let flags = new Map();
// 长按逻辑 - 时间点之差
let downTime;
let ltOrC = (ev, flt = nextLongTouch, fc = nextClick, t = 130) => {
if(ev.timeStamp - downTime > t) flt(ev);
else fc(ev);
}
let getDownNow = (ev) => downTime = ev.timeStamp;
// LongTouchEv
let addLTEv = (e, flt, fc, t) => {
e.addEventListener('mousedown', getDownNow);
e.addEventListener('touchstart', getDownNow);
downTime = new Date().getTime(); // 初始化时间戳
e.addEventListener('mouseup', ltOrC);
e.addEventListener('touchend', ltOrC);
}
let removeLTEv = (e) => {
e.removeEventListener('mousedown', getDownNow);
e.removeEventListener('touchstart', getDownNow);
e.removeEventListener('mouseup', ltOrC);
e.removeEventListener('touchend', ltOrC);
}
// 第二次及以后的点击事件
let nextClick = (ev) => {
var btn = ev.target;
if(!btn.hasAttribute("x")) return;
var x = floor(btn.getAttribute("x"));
var y = floor(btn.getAttribute("y"));
// 点击逻辑 - dfs
if(flags.has(`${x},${y}`)) return;
// 没有插旗,就去dfs回溯访问所有能访问/无雷的地砖
dfs(x, y); // vst = new Map();
if(isFail) {
var remd = cntRemindNum();
main.removeEventListener('mousedown', firstClick);
main.removeEventListener('touchstart', firstClick);
removeLTEv(main);
((remd) => {
alert('你已经失败了!\n' +
' 剩余雷数:' + (remindBombs - remd) + " 个没有扫出来!\n" +
' 5 秒后重启游戏!');
renderAll();
return new Promise(resolve => {
setTimeout(resolve, 5000);
})
})(remd).then(() => initGlobal());
}
if(vstnum == sum){
if(cntRemindNum() == targetBombs){
alert('恭喜您已通关!');
initGlobal();
}
}
}
let nextLongTouch = (ev) => {
var btn = ev.target;
if(!btn.hasAttribute("x")) return;
var x = floor(btn.getAttribute("x"));
var y = floor(btn.getAttribute("y"));
// 长按逻辑 - 插旗 / 拔旗
var key = `${x},${y}`;
if(flags.has(key)) {
flags.delete(key);
mtx[x][y].e.className = 'unit';
vstnum--;
} else {
if(vst.has(key)) return;
flags.set(key, "");
mtx[x][y].e.className = 'flag';
vstnum++;
}
if(vstnum == sum){
alert('恭喜您已通关!');
initGlobal();
}
}
// 第一次的点击事件
let firstClick = (ev) => {
// 事件委托
var btn = ev.target;
first[0] = floor(btn.getAttribute("x"));
first[1] = floor(btn.getAttribute("y"));
// 生成地雷
geneBombs(targetBombs);
allBombsData();
dfs(first[0], first[1]);
// renderBombsData();
main.removeEventListener('mousedown', firstClick);
main.removeEventListener('touchstart', firstClick);
// 添加之后的每次点击事件
addLTEv(main, nextLongTouch, nextClick, 1000);
}
let initBombs = () => {
// 添加 第一次点击的事件监听
main.addEventListener('mousedown', firstClick);
main.addEventListener('touchstart', firstClick);
}
// 游戏相关数据
let isFail = false;
let targetBombs = 60;
let remindBombs = targetBombs;
// 当前访问了多少个
// - 虽然记录了访问数组,但是还要维护访问个数
let vstnum = -1;
// 总数
let sum = 0;
let initGlobal = () => {
main.removeEventListener('mousedown', firstClick);
main.removeEventListener('touchstart', firstClick);
removeLTEv(main);
main.innerHTML = '';
mtx = null;
first = [];
bombs = [];
vst = new Map();
flags = new Map();
isFail = false;
targetBombs = 60;
remindBombs = targetBombs;
vstnum = -1;
sum = 0;
// 开始执行主函数
_main_func();
}
// 计算剩余雷数
let cntRemindNum = () => {
var remd = 0;
flags.forEach((v, k, m) => {
var strs = k.split(",");
if(mtx[floor(strs[0])][floor(strs[1])].v == -1) remd++;
});
return remd;
}
let _main_func = (rn, cn) => {
crtMtx(rn, cn);
sum = rn * cn;
geneMtxData();
initBombs();
}
_main_func(15, 20);
</script>