javascript - 练习题:事件练习 - 扫雷

时间:2022-11-21 18:59:33

HTML

<!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>扫雷游戏</title>
<link rel="stylesheet" href="index.css">
</head>
<body>

<div class="container">
<h1>扫雷游戏</h1>
<div class="level">
<button class="active">初级</button>
<button>中级</button>
<button>高级</button>
</div>
<div class="info">总雷数: <span class="mineNum"></span></div>
<div class="info">插旗数: <span class="flagNum"></span></div>
<div class="mineArea"></div>
</div>
<script src="index.js"></script>
</body>
</html>

css部分

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

.container {
margin: 0;
}

h1 {
text-align: center;
font-size: 46px;
font-weight: 100;
margin: 30px atuo 20px auto;
}

.level {
text-align: center;
margin-bottom: 10px;
}

.level button {
padding: 5px 15px;
background-color: #02a4ad;
outline: none;
border: none;
border-radius: 3px;
margin: 0 5px;
cursor: pointer;
color: #f2f2f2;
}

.level button.active {
background-color: #00abff;
}

.info {
text-align: center;
font-weight: 200;
margin: 10px auto;
}

/* 表格样式 */
table {
background-color: #929196;
margin: 0 auto;
border-spacing: 1px;
}

table td {
width: 24px;
height: 24px;
background-color: #ccc;
border: 2px solid;
border-color: #fff #a1a1a1 #a1a1a1 #fff;
text-align: center;
line-height: 24px;
font-weight: bold;
cursor: pointer;
}

td>div {
width: 100%;
height: 100%;
}

/* 显示雷 */
td>div.mine {
background: #ccc url('./imgs/mine.png') center / cover no-repeat;
/* opacity: 0.2; */
opacity: 0;
}

/* 显示旗 */
td>div.flag {
background: #ccc url('./imgs/flag.png') center / cover no-repeat;
opacity: 1;
}

/* 踩雷样式 */
td>div.error {
background-color: rgb(216, 0, 0);
}

td>div.right {
background-color: green;
}

/* 不同数字对应的颜色 */
td>div.zero {
border-color: #d9d9d9;
background: #d9d9d9;
}

td>div.one {
border-color: #d9d9d9;
background: #d9d9d9;
color: #0332fe;
}

td>div.two {
border-color: #d9d9d9;
background: #d9d9d9;
color: #019f02
}

td>div.three {
border-color: #d9d9d9;
background: #d9d9d9;
color: #ff2600
}

td>div.four {
border-color: #d9d9d9;
background: #d9d9d9;
color: #93208f;
}

td>div.five {
border-color: #d9d9d9;
background: #d9d9d9;
color: #ff7f29;
}

td>div.six {
border-color: #d9d9d9;
background: #d9d9d9;
color: #ff3fff;
}

td>div.seven {
border-color: #d9d9d9;
background: #d9d9d9;
color: #3fffbf;
}

td>div.eight {
border-color: #d9d9d9;
background: #d9d9d9;
color: #22ee0f;
}

JS部分


/**
* 工具函数 ==========
* @param {*} selector
* @returns
*/
// 获取 DOM元素 选第一个符合要求的
function $(selector){
return document.querySelector(selector);
}

// 获取 DOM元素 选所有符合要求的
function $$(selector){
return document.querySelectorAll(selector);
}

/**
* 游戏配置 ==========
*/
var config = {
easy: {
row: 10,
col: 10,
mineNum: 10,
},
normal: {
row: 15,
col: 15,
mineNum: 30,
},
hard: {
row: 20,
col: 20,
mineNum: 60,
}
}
// 当前游戏难度,开始是easy;
var curLevel = config.easy;


/**
* 游戏主要逻辑 ===========
*/

// 用于存储生成的地雷的数组
var mineArray = null;
// 雷区容器
var mineArea = $('.mineArea');
// 用于存储整张地图每个格子额外的信息
var tableData = [];
// 存储用户插旗的 DOM 元素
var flagArray = [];
// 获取游戏难度选择的所有按钮
var btns = $$('.level>button');
// 插旗数量 DOM 元素
var flagNum = $('.flagNum');

// 当前级别雷数的 DOM 元素
var mineNumber = $('.mineNum');

/**
* 生成地雷的方法
* @returns 返回地雷数组
*/

function initMine() {
// 生成对应长度的数组
var arr = new Array(curLevel.row * curLevel.col);
// 往数组里填充值
for (var i = 0; i < arr.length; i++) {
arr[i] = i;
}

// 打乱数组
arr.sort(() => 0.5 - Math.random());
// 只保留对应雷数量的数组长度

return arr.slice(0, curLevel.mineNum);
}

/**
* 场景重置,清空场景
*/
function clearScene() {
mineArea.innerHTML = "";
flagArray = []; // 清空插旗数组
flagNum.innerHTML = 0; // 重置托旗的数量
mineNumber.innerHTML = curLevel.mineNum; // 重置当前级别的雷数
}

/**
* 游戏初始化
*/
function init() {
// 清空场景,或叫重置信息
clearScene();

// 1. 随机生成所选配置对应数量的雷
mineArray = initMine();
// console.log(mineArray);
// 2. 生成所选配置的表格
var table = document.createElement('table');
// 初始化格子下标
var index = 0;
for (var i = 0; i < curLevel.row; i++) {
// 创建新一行
var tr = document.createElement('tr');
tableData[i] = [];
for (var j = 0; j < curLevel.col; j++) {
var td = document.createElement('td');
var div = document.createElement('div');
// 每个小格子都会对应一个JS对象
// 该对象存储了额外的信息
tableData[i][j] = {
row: i, // 该格子的行
col: j, // 该格的列
type: 'number', // 格子的属性,数字:number 雷:mine
value: 0, // 周围雷的数量
index, // 格子的下标
checked: false // 是否被检验过
};
// td.innerHTML = 0;
// 为每个 div 添加下标,方便用户点击时获取
div.dataset.id = index;
// 标记当前的div是可以插旗的
div.classList.add('canFlag');

// 查看当前格子的下标是否在雷的数组里面
if (mineArray.includes(tableData[i][j].index)) {
tableData[i][j].type = 'mine';
div.classList.add('mine');
}
td.appendChild(div);
tr.appendChild(td);

// 下标自增
index++;
}
table.appendChild(tr);
}
// 添加到雷容器中
mineArea.appendChild(table);

// 每次初始化的时候,重新绑定事件,每次游戏结束时,是移除了事件的
// 鼠标点击事件,用 mousedown 不要用 click
mineArea.onmousedown = function (e) {
// console.log(e.button);
if (e.button === 0) {
// 0 是左键,进行区域搜索
searchArea(e.target);
}
if (e.button === 2) {
// 2 是右键,插旗
flag(e.target);
}
}
}

/**
* 显示答案
*/
function showAnswer() {
// 要把所有的雷显示出来
// 有些雷可能是插了旗的,需要判断插旗是否正确
// 正确添加绿色背景,错误添加红色背景
var isAllRight = true;
// 获取所有雷的 DOM 元素
var mineArr = $$('td>div.mine');
for (var i = 0; i < mineArr.length; i++) {
mineArr[i].style.opacity = 1;
}
// 遍历用户的插旗
for (var i = 0; i < flagArray.length; i++) {
// console.log(flagArray[i]);
if (flagArray[i].classList.contains('mine')) {
// 说明插旗全对
flagArray[i].classList.add('right');
} else {
flagArray[i].classList.add('error');
isAllRight = false;
}
}
if (!isAllRight || flagArray.length !== curLevel.mineNum) {
gameOver(false);
}
// 取消事件
mineArea.onmousedown = null;
}

/**
*找到对应 DOM 在 tableData 里面的JS对象
* @param {*} cell
*/
function getTableItem(cell) {
var index = cell.dataset.id;
// console.log(index);
var flatTableData = tableData.flat();
return flatTableData.filter(item => item.index == index)[0];
}

/**
* 会返回该对象明对应的四周的边界
* @param {*} obj
*/
function getBound(obj) {
// 确定上下边界
var rowTop = obj.row - 1 < 0 ? 0 : obj.row - 1;
var rowBottom = obj.row + 1 === curLevel.row ? curLevel.row - 1 : obj.row + 1;
// 确定左右边界
var colLeft = obj.col - 1 < 0 ? 0 : obj.col - 1;
var colRight = obj.col + 1 === curLevel.col ? curLevel.col - 1 : obj.col + 1;

return {
rowTop, rowBottom, colLeft, colRight,
};
}

/**
* 返回周围一圈雷的数量
* @param {*} obj 格子对应的JS对象
*/
function findMineNum(obj) {
var count = 0; // 地雷计数器
var { rowTop, rowBottom, colLeft, colRight } = getBound(obj);
for (var i = rowTop; i <= rowBottom; i++) {
for (var j = colLeft; j <= colRight; j++) {
if (tableData[i][j].type === 'mine') {
count++;
}
}
}
return count;
}

/**
* 根据 tableData 中的 JS对象,返回对应的 DIV
* @param {*} obj
*/
function getDOM(obj) {
// 获取到所有的 DIV
var divArray = $$('td>div');
// 返回对应下标的 div
return divArray[obj.index];
}

/**
* 搜索该单元格周围的九宫格区域
* @param {*} cell 用户点击的单元格
*/
function getAround(cell) {
if (!cell.classList.contains('flag')) {
// 当前单元格没有被插旗,我们才进行此操作
cell.parentNode.style.border = 'none';
cell.classList.remove('canFlag');

// 1. 获取到该DOM元素在tableData里所对应的对象;
var tableItem = getTableItem(cell);
// console.log(tableItem);
if (!tableItem) {
return;
}
// 代表当前单元格已经被核对过了
tableItem.checked = true;
// 得到了 DOM 对象所对应的JS对象
// 那我们可以查看周围一圈是否有雷
var mineNum = findMineNum(tableItem);
if (!mineNum) {
// 进入此 if 说明周围没有雷,需要继续搜索
var { rowTop, rowBottom, colLeft, colRight } = getBound(tableItem);
for (var i = rowTop; i <= rowBottom; i++) {
for (var j = colLeft; j <= colRight; j++) {
if (!tableData[i][j].checked) {
getAround(getDOM(tableData[i][j])); // 递归搜索
}
}
}
} else {
// 说明周围有雷,当前格子要显示对应雷的数量
var cl = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight'];
cell.classList.add(cl[mineNum]); // 这招聪明,不用 switch
cell.innerHTML = mineNum;
}
}


}

/**
* 区域搜索
* @param {*} cell 用户点击的DOM元素
*/
function searchArea(cell) {
// 1. 当前单元格雷,游戏结束
if (cell.classList.contains('mine')) {
// 进入此if,说明踩雷了
// 把当前雷变红色,其他雷也显示出来
cell.classList.add('error');
showAnswer();
return;
}
// 2. 当前单元格不雷,判断周围有没有雷
// 2.1 如果有雷,显示雷的数量
// 2.2 如果没有雷,继续递归搜索
getAround(cell);
}

/**
* 判断用户的插旗是否全部正确
*/
function isWin() {
for (var i = 0; i < flagArray.length; i++) {
if (!flagArray[i].classList.contains('mine')) {
return false;
}
}
return true;
}

/**
* 游戏结束
* 分为两种情况:
* @param {*} isWin 这是个布尔值, true 代表胜利;false 代表失败
*/
function gameOver(isWin) {
var mess = '';
if (isWin) {
mess = '游戏胜利,你找出了所有的雷~';
} else {
mess = '游戏失败~';
}

setTimeout(function () {
window.alert(mess);
}, 0);
}

/**
*
* @param {*} cell 用户点击的 DOM 元素
*/
function flag(cell) {
// 只有点击的 DOM 元素包含 canFlag
if (cell.classList.contains('canFlag')) {
if (!flagArray.includes(cell)) {
// 进行插旗操作
flagArray.push(cell);
cell.classList.add('flag');
// 还要判断插旗数量
if (flagArray.length === curLevel.mineNum) {
// 判断玩家是否胜利
if (isWin()) {
gameOver(true);
}
// 无论游戏胜利还是失败,都应该进入 showAnswer, 显示最终答案
showAnswer();
}
} else {
// 说明这个单元格已经在数组里面了
// 也就是说,用户现在是要取消插旗
var index = flagArray.indexOf(cell);
flagArray.splice(index, 1);
cell.classList.remove('flag');
}
// 标识插旗数
flagNum.innerHTML = flagArray.length;
}
}

/**
* 绑定事件
*/
function bindEvent() {
// 阻止默认的鼠标右键行为
mineArea.oncontextmenu = function (e) {
e.preventDefault();
}

// 游戏难度选择
$('.level').onclick = function (e) {
for (var i = 0; i < btns.length; i++) {
btns[i].classList.remove('active');
}
e.target.classList.add('active');
switch (e.target.innerHTML) {
case '初级': {
curLevel = config.easy;
break;
}
case '中级': {
curLevel = config.normal;
break;
}
case '高级': {
curLevel = config.hard;
break;
}
}
init();
}
}

/**
* 程序入口
*/
function main() {
// 1. 游戏初始化
init();
// 2. 绑定事件
bindEvent();
}

main();