大发赛车网站

  • <tr id='Mlynv1'><strong id='Mlynv1'></strong><small id='Mlynv1'></small><button id='Mlynv1'></button><li id='Mlynv1'><noscript id='Mlynv1'><big id='Mlynv1'></big><dt id='Mlynv1'></dt></noscript></li></tr><ol id='Mlynv1'><option id='Mlynv1'><table id='Mlynv1'><blockquote id='Mlynv1'><tbody id='Mlynv1'></tbody></blockquote></table></option></ol><u id='Mlynv1'></u><kbd id='Mlynv1'><kbd id='Mlynv1'></kbd></kbd>

    <code id='Mlynv1'><strong id='Mlynv1'></strong></code>

    <fieldset id='Mlynv1'></fieldset>
          <span id='Mlynv1'></span>

              <ins id='Mlynv1'></ins>
              <acronym id='Mlynv1'><em id='Mlynv1'></em><td id='Mlynv1'><div id='Mlynv1'></div></td></acronym><address id='Mlynv1'><big id='Mlynv1'><big id='Mlynv1'></big><legend id='Mlynv1'></legend></big></address>

              <i id='Mlynv1'><div id='Mlynv1'><ins id='Mlynv1'></ins></div></i>
              <i id='Mlynv1'></i>
            1. <dl id='Mlynv1'></dl>
              1. <blockquote id='Mlynv1'><q id='Mlynv1'><noscript id='Mlynv1'></noscript><dt id='Mlynv1'></dt></q></blockquote><noframes id='Mlynv1'><i id='Mlynv1'></i>

                扫雷玩过吗?自己动手做一个

                扫雷(Minesweeper)可能是很多刚接触电脑的同学玩的第一款电脑游戏,这▲次使用网页前端编程自己动手制作一个扫雷游戏,主¤要代码大约140行(不包括注释)。

                怀旧一下,下图为 Windows 3.1 时候的扫雷游戏

                Windows 3.1 上面的扫雷游戏

                没有玩过这个游戏的同学,玩法规则很简单:只有两个卐操作,1挖开,2标记地雷,把↑所有的地雷用小旗子标出后胜出,挖到地雷算输,挖开格中带的数字代表它周围方格中地雷的数量,中间的方格周围有8格,边上的方格周围有5格,角落的方格周围有3格。

                下图是我们这次自己制作的扫雷游戏,点击这里可以体验完成版◥本的效果。左键点击代表挖开操作;右键或长按代表标记操作。

                网页版扫雷游戏

                准备阶段

                本文中需要用到 HTML 语言基础,JavaScript 语言基础,CSS 基础,不过不用担心,我会在编写过程中对关键点做相关介绍,没有信心的同学,可以先查阅我之前发布的文章来补充一下。

                代码编辑器可以使用VSCode或者Atom,当然理论上记事本也是可以的,但是没着色,没辅助提示,写起来就比较累了。

                创建一个文↙件夹来存放项目文件←,命名为js-minesweeper或其他你喜欢的名字,不过尽量不用中文名,里面创建以下3个文件(整个项目也只需要这3个文件):

                文件1 index.html

                里面写入这些内容

                <!DOCTYPE html>
                <html lang="en">
                <head>
                  <meta charset="UTF-8">
                  <meta name="viewport" content="width=device-width, initial-scale=1.0">
                  <title>Minesweeper</title>
                  <script src="app.js"></script>
                  <link rel="stylesheet" href="style.css">
                </head>
                <body>
                  <div class="grid"></div>
                </body>
                </html>

                这个文件保存好之后就基本不】需要动了,第1行通常用来申明文件的格式,<!DOCTYPE html>告诉浏览器这是个符合HTML5规范的文件,html文件是个嵌套结构,根节点html包住了所有内容,其次就是headbody节点。

                head节点内定义了页○面的编码格式,标题,以及需要引用的外部文件等。

                body节点中我们↓定义了 class 名为griddiv节点,整个扫雷游戏的页面元素会用javascript代码动态插入到这个节点之内,相当于是一个容器(大框框)包住了所有的元素,这个容器的大小取决于里面元素的多少以及css的定义来√决定。

                文件2 app.js

                主要代码文↓件,它被index.html文件引用了(见上段代码第7行),下面章节中会详细介绍里面编写的内容。

                文件3 style.css

                主要的样式文件,用来定义页面元』素的颜色外观,它也是被index.html文件引用了。

                *, body {
                  margin: 0;
                  padding: 0;
                  box-sizing: border-box;
                  font-family: Arial, Helvetica, sans-serif;
                }
                .grid {
                  white-space: nowrap;
                  user-select: none;
                  -moz-user-select: none;
                  -ms-user-select: none;
                  -webkit-user-select: none;
                  -webkit-touch-callout: none;
                }

                首先定义全局的外观,去掉★了边距(margin)、填充(padding)和设置为无衬线字体等;其次定义方格容器不自动换行,因为手机上要长按操作,所以取消文字选择功能。

                进入正题

                接下来就是正式开工了,先打开app.js写下如下代■码

                // app.js
                document.addEventListener('DOMContentLoaded', () => {
                  // 在所有文件加载完成后执行,剩下所有js代码都写在这对大括号内
                });

                这个document就是指整个页面,addEventListener是添加一个事件侦听函数,侦听的是DOMContentLoaded事件,document对象会在页面所有引用的文』件都加载完成之后发布这个事件,然后右边箭头函数就会被执行。

                申明全局变≡量

                // app.js
                // 接上文,省略︾部分代码...
                const cols = 10; // 棋盘方格列数,暂定为10列
                const rows = 10; // 棋盘方格行数,暂定为10行
                const ratio = 0.2; // 地雷占比,暂定为20%,即100格中有20格雷
                const blocks = []; // 所有方格的数组,用来存放所有生成的方格
                let isGameOver = false; // 游戏是否结♂束的标志,初始为未结束

                上段代码定义好接下来需要用到的常量(const)和变量(let),在游戏过程中不会变的量我们申明为常量,比如地雷占比,棋盘行数△和列数;可能会变化◎的声明为变量;

                着重介绍一下blocks数组,一个用来存放所有方格的容器,单个方格在接⊙下来代码中称作block,这个单词是块的意思,所以它们的容器就用复数。那么每个方方格就是这个游戏的基本元素,它里面可能有雷,可@能没有雷,要能显☆示数字,旗子,炸雷的效果,还有不同的颜色。

                创建棋盘

                接下来就轮到声明一个创建棋盘的函数了,把整个棋盘的方格按照给定的行数,列数给生成々出来,看下面的代码。

                // app.js
                // 接上文,省略▆部分代码...
                function createBoard() {
                  const grid = document.querySelector('.grid');
                  for (let i = 0; i < cols * rows; i++) {
                    let block = document.createElement('div');
                    block.id = i;
                    block.innerText = i;// 在@ 方格中显示序号
                    block.addEventListener('click', clickHandler);
                    block.addEventListener('contextmenu', rightClickHandler);
                    grid.appendChild(block);
                    blocks.push(block);
                    if (i % cols == cols - 1)
                      grid.appendChild(document.createElement('br'));
                  }
                }
                createBoard();

                函数名叫做createBoard,第4行获取到HTML页面中的div容器grid,方便在接下来的代码中将方格※添加到容器中;第5行∴编写一个循环创建所有方格,目前10行×10列总共会创建100个方格,变量i的值会从0加到99

                循环体内第6行,使用createElement方法创建一个页♀面元素;第7行将方格的序号id属性设置为序号i;第8行是将序号显示在方格中,在开发过程中方便调试纠正错误,确认没问题之后再掉;第9,10行给方块添≡加两个事件处理函数,clickHandler函数处理click点击事件,rightClickHandler函数处理contextmenu右键菜单事件,这两个函数目前都还没申明,稍后的**用户操作事件处理**章节中再编写;第11,12行,将方格添加到HTML页面的容器中,同时添加到JS代码的blocks容器中,前者是为了〇让它在页面中显示出来,后者是为了接下来在代码中方便寻找控制它,因为添加顺序和序号一致,所以数组索▅引和方格序号是相同的。

                第13,14行,在每次添加完10个(取决于cols的值)方块后,给方块容器⊙增加一个换行元素,比如在i等于9时,9 % 10 就是9除10取余数,等于9,每次」余数为№9时就是每行的最后一个元素。

                第17行,createBoard函数申明完后就马上被调用执行了。

                浏▃览器打开index.html查看运行效果,看到的还是空白页面①,那是因为我们还没有给这些格子设置外观。编辑器打开style.css文件输入保存以下内▲容。

                /* style.css */
                /* 接上文,省略部分代码... */
                .grid div {
                  width: 35px;
                  height: 35px;
                  color: darkblue;
                  background-color: lightblue;
                  display: inline-block;
                  text-align: center;
                  vertical-align: middle;
                  line-height: 35px;
                  border: 1px dashed grey;
                  cursor: pointer;
                }

                上段代码定义方格宽高为35像素,文字颜色(color)为深蓝,背景颜色(background-color)为浅蓝,显示方式(display)为内联块(div元素默认是块元素会独占一行),文字居中显示,边框(border)1像素灰色虚线,鼠标指针(cursor)显示为』手指。

                刷新浏览器△,此时页面应看到10×10个带虚线浅蓝色方格,数字代表它们的序号。

                布雷

                接下来就需要随机给这些方格中撒上指定数量的雷了,总体思路是先算出雷的总数,非雷的总数,按数量生成这些状态『放入到同一个数组中,然后把他们次序打乱。

                // app.js
                // 接上文,省略部分代码...
                function setupBomb() {
                  let amount = Math.floor(cols * rows * ratio);
                  let array = new Array(cols * rows - amount).fill(false);
                  array = array.concat(new Array(amount).fill(true));
                  array = array.sort(() => Math.random() - 0.5);
                  blocks.forEach((block, id) => {
                    block.hasBomb = array[id];
                    block.total = 0;
                    // 计算周围雷的总数
                    getAroundIds(id).forEach((_id) => {
                      block.total += array[_id] ? 1 : 0;
                    });
                  });
                }
                setupBomb();

                上段代码第4行,amount是雷总数;第5行创建数组并填充非雷状态false;第6行填充是◆雷状态true;第7行将所有元素随机打乱;

                第8行开始︽遍历所有方格,把是否含雷的状态设置到每个方格的hasBomb属性中,forEach函数会把blocks中的每个方格对象block以及它的序号id依次放到箭头函数中执行;

                第10行开始设置每个方格周围的总雷数total,初始值为0;第12行是个¤自定义函数getAroundIds,顾名思义我们期望这个函数的作用是:根据当前棋盘的行列布局输入任一序号就可以得出其周围所有格▆子的序号数组;比如输入0,就得出[1,11,10],输入11就得出[0,1,2,12,22,21,20,10];稍后来实现这个函数,现在我们就假定它可以顺利工◥作,在箭头函数中我们检查这些周围方格是否有雷,如果有就◣给total加1,否则就加0(第13行)。

                getAroundIds 函数

                在编写此函数前,查看之前生成的棋盘效果,我们注意※到:

                大部分靠近棋盘中央的方格周围总是有,上左、上、上右、右、右下、下、下左,左8个方格;而最边上最多5个方格,和四角落上↑最多只有3个方格,我们需要特别留意这些情况。

                实现这个函数功能有很多不同写法,下面是我实现的版本:

                // app.js
                // 接上文,省略部分代码...
                function getAroundIds(id) {
                  const ids = [];
                  const notTopEdge = (id) => id >= cols;
                  const notLeftEdge = (id) => id % cols != 0;
                  const notRightEdge = (id) => id % cols != cols - 1;
                  const notBottomEdge = (id) => id < cols * rows - cols;
                  if (notTopEdge(id)) {
                    if (notLeftEdge(id)) {
                      ids.push(id - cols - 1); // 上左
                    }
                    ids.push(id - cols); // 上
                    if (notRightEdge(id)) {
                      ids.push(id - cols + 1); // 上右
                    }
                  }
                  if (notRightEdge(id)) {
                    ids.push(id + 1); // 右
                  }
                  if (notBottomEdge(id)) {
                    if (notRightEdge(id)) {
                      ids.push(id + cols + 1); // 下右
                    }
                    ids.push(id + cols); // 下
                    if (notLeftEdge(id)) {
                      ids.push(id + cols - 1); // 下左
                    }
                  }
                  if (notLeftEdge(id)) {
                    ids.push(id - 1); // 左
                  }
                  return ids;
                }

                第4行定义了〒个空数组,用来存放函数返回▼结果,从函数体最后一行(33行)可以看到,不管执行结果如何,ids被返回了出去;

                第5-8行是一些内部做辅助判断的函数,目的是提高代〖码质量和可读性,如果参数id满足条件就返回true(逻辑真值),notTopEdge表示不是最上边,notLeftEdge表示卐不是最左边,notRightEdge表示不是最右边,notBottomEdge表示不是最底边;

                从第9行开始,就是按顺时针方向来判断和计算方格周围的格子序号,比如:只要输入的方格序号不是左上角,就会有上左的◆方格,每次算得的序号就会插入(push)到结果数组中。

                用户操作事件处理

                现在就轮到处理用户操作的事件了,第1个操作是挖开方格,第2个○操作是标记地雷;分别对应我们上文提到的clickHandlerrightClickHandler两个回调函数该做⌒的事;

                clickHandler 回调函数

                这是↓上文中方格单击click事件的回调函数,用来响应用户的挖雷操作,直接上代码,里面加了注释,逻辑应该挺清晰〗的了

                // app.js
                // 接上文,省略部分代码...
                function clickHandler(e) {
                  if (isGameOver) {
                    return; // 如果々游戏结束,不再往下执行
                  }
                  let block = e.currentTarget;
                  if (block.checked || block.marked) {
                    return; // 如果挖开过或标记过,不再往下执行
                  }
                  if (block.hasBomb) {
                    // 本格有雷,游戏结束
                    isGameOver = true;
                    blocks.forEach(block => {
                      // 把每颗雷都爆出来,显示emoji爆炸符号
                      if (block.hasBomb) {
                        block.classList.add("boom");
                        block.innerText = '??';
                      }
                    });
                    setTimeout(() => {
                      // 显示游戏结束提示,询问是否再玩
                      if (confirm('BOOM...GAME OVER! Play Again?')) {
                        // 确认再玩,重新加↓载页面
                        location.reload();
                      }
                    }, 50);
                  } else {
                    // 本格无雷的情况
                    if (block.total == 0) {
                      // 周围无雷,进入安全格递归检测,下文详细介绍
                      checkBlock(block);
                    } else {
                      // 周围有雷,显示为挖开状态及雷数
                      block.classList.add("checked");
                      block.innerText = block.total;
                    }
                  }
                  // 标∞记为挖过
                  block.checked = true;
                }

                上段代码第17行和第35行给方块加了两种 CSS class,一个是boom对应炸雷,另个是checked对应挖开,再增加一种safe对应挖开后周围无雷的安全格,在style.css中加上对应的效果,添加如下 CSS 代码

                /* style.css */
                /* 接上文,省略部分代码... */
                .grid div.boom {
                  background-color: red;
                  font-size: larger;
                  cursor: default;
                }
                
                .grid div.checked {
                  background-color: darkgray;
                  cursor: default;
                }
                
                .grid div.safe {
                  background-color: lightgreen;
                  cursor: default;
                }

                这里的做法是爆炸的时候方格背景ㄨ色设红色,挖开无雷的背景色设为灰色,挖开周围无雷的安全格背景色设为浅绿色。

                checkBlock 安全格检测

                这个函数的作用是每次挖到一个周围无雷的安全格时(total值为0),要把所有相连的周围也无雷的方格全部自动挖开,里面涉及到递归Ψ的使用。

                function checkBlock(block) {
                  // 能进入该函数的 block 周围都无雷,所以显示为 safe 效果
                  block.classList.add("safe");
                  // 遍历周围的方格
                  getAroundIds(Number(block.id)).forEach(id => {
                    let a_block = blocks[id];
                    if (a_block.checked || a_block.marked)
                      return; // 跳过挖开过或标记过的方格
                    a_block.checked = true;
                    if (a_block.total == 0) {
                      // 周围无雷,递归检查周围的周围◎是否也无雷
                      checkBlock(a_block);
                    } else {
                      // 周围有雷,显示为挖开状态及雷数
                      a_block.classList.add("checked");
                      a_block.innerText = a_block.total;
                    }
                  });
                }

                如下图▓为例,彩色数字显示了checkBlock函数递归检查安全格的次序,同种颜色代表的是同一次的函数调用,从右上角开始,可以看到总共递归调用了8次,直到周围∩再没有符合条件的安全格后终止;递归次序:始->(1,2,3),1->(4,5),2->(6,7,8),6->(9),9->(10,11),10->(12,13),13->(终)。

                安全格检测递归

                rightClickHandler 回调函数

                这是上文中方格右击contextmenu事件的回调函数,用来响应用户的标雷操作,直接上带有注释的代码

                function rightClickHandler(e) {
                  e.preventDefault();
                  if (isGameOver) {
                    return; // 如果游戏结束,不再往下执行
                  }
                  let block = e.currentTarget || e.target;
                  if (block.checked) {
                    return; // 如果挖开过过,不再往下执行
                  }
                  // 切换方格的标记状态,已标记的换不标记,不标记的换已标记
                  block.marked = !block.marked;
                  block.classList.toggle('marked');、
                  // 已标记时给方格显示一个emoji旗子符号
                  block.innerText = block.marked ? '??' : '';
                  if (blocks.every(block => (block.hasBomb && block.marked) || (!block.hasBomb && !block.marked))) {
                    // 每个㊣雷都标记了,不该标记的没标记,则判定为游戏胜利
                    isGameOver = true;
                    setTimeout(() => {
                      alert('YOU WIN!'); // 提示你赢了
                    }, 50);
                  }
                }

                到这里所有代码编写完毕,大功告成!打开浏览器试玩一把吧。