HTML扫雷实战

来源:互联网 发布:电脑打开软件特效 编辑:程序博客网 时间:2024/06/05 17:55

新买的电脑,配置贼高了,玩扫雷一点都不卡……

(玩梗)


请注意,本文使用了与默认原创版权协议不一致的协议,本文适用的协议以末尾注明的为准。

扫雷,(Minesweeper,港台称为“踩地雷”)想必大多数人都玩过。它的实现也不困难。下面是利用Javascript扫雷的例子。

    • 主文件
      • 准备工作
      • 创建场景
      • 随机生成场景
      • 右键标雷
      • 左键开方块
      • 自动打开周围的地雷
      • 游戏实用工具
        • Check
        • GameWin
        • GameOver
    • 主文件之后的事情
    • 后记

主文件

主要的程序只在一个文件中,这里把它叫做 game.js

作为一个合格的扫雷游戏,它需要满足以下功能:
* 左键点击方块将其打开;
* 右键点击方块将其标记;
* 若点开的方块是地雷则游戏结束;
* 若点开的方块不是地雷则显示其周围8个方块中格子的数量;
* 若打开的方块周围无地雷则自动打开周围的地雷;
* 若一个打开的方块周围标记的方块数量等于它本身显示的数字相同,则可以自动打开剩余的方块;
* 第一下不是雷,且雷阵随机;
* 当所有的非雷方块都打开时,游戏胜利。

一项一项来解决它们。

准备工作

首先定义一些常量

const fieldWidth = 40;      //定义区域的宽度const fieldHeight = 18;     //定义区域的高度const mineDensity = 0.12;   //定义雷的密度

之后是一些内部的数据

var fieldBlocks = new Array();         //记录着每个方块的idvar fieldData = new Array();           //记录着每个方块是不是雷var fieldOpened = new Array();         //记录着每个方块是不是已经被打开var fieldFlagged = new Array();        //记录着每个方块是不是被标记为雷var fieldSurroundingMines = Array();   //记录着每个方块周围有多少个雷var fieldSurroundingFlags = Array();   //记录着每个方块周围有多少个被标记的方块

以及一些不好分类但是又必须需要的变量

var firstClick = true;         //是不是第一次点击?var alreadyCleared = false;    //是不是已经胜利了……我也有点搞不懂为什么要这个变量

以及通过id来查找所在位置的坐标的两个函数

//获取X坐标,遍历比较,找到就返回X坐标,找不到就返回-1function getX(ID) {  for (var y = 0; y <= fieldHeight + 1; y++)  {    for (var x = 0; x <= fieldWidth + 1; x++)    {      if ( (x == 0) || (x == fieldWidth + 1) || (y == 0) || (y == fieldHeight + 1) ) continue;      if (fieldBlocks[y][x] == ID) return x;    }  }  return -1;}//获取Y坐标,遍历比较,找到就返回Y坐标,找不到就返回-1function getY(ID) {  for (var y = 0; y <= fieldHeight + 1; y++)  {    for (var x = 0; x <= fieldWidth + 1; x++)    {      if ( (x == 0) || (x == fieldWidth + 1) || (y == 0) || (y == fieldHeight + 1) ) continue;      if (fieldBlocks[y][x] == ID) return y;    }  }  return -1;}

创建场景

内部数据的变量事实上都是二维数组……

为了在之后的操作中不需要考虑边界方块的四周方块数量不足八个的问题,特意在场地的上下左右边界外添加了一行/一列。

首先在UI层面下手

function create(){  var count = 1;  document.write("</br>");   //我在Chrome上试过,不先写一个换行,第一行的格子就显示不出来,天知道为什么  for (var y = 0; y <= fieldHeight + 1; y++)   //按列循环  {    fieldBlocks[y] = new Array;  //每一列都创建一个子数列,代表一行    for (var x = 0; x <= fieldWidth + 1; x++) //从第0行的第0列开始创建    {      //在UI上,第0行和第0列以及最后一行与最后一列都是额外添加的,不应该显示出来      if ( (x == 0) || (x == fieldWidth + 1) || (y == 0) || (y == fieldHeight + 1) ) continue;      b = document.createElement("img"); //方块其实是图片……      b.setAttribute("id", "block_" + count.toString()); //将方块的id设置为“block_编号”的模式      b.src = "./img/Block.svg";  //加载未点开的方块的图片资源      b.width = "30";  //设置大小      b.onclick = function(){blockDetonate(event.target.id)};   //添加鼠标左键单击的事件      b.oncontextmenu = function(){setUnsetFlag(event.target.id); return false};   //添加鼠标右键单击的事件      document.body.appendChild(b);   //将元素加到页面中      fieldBlocks[y][x] = b.id;    //将这个方块的id记录到fieldBlocks的相应位置以方便日后索引      count++;   //递增计数器      if (x == fieldWidth) document.write("</br>");   //每一行的末尾插入换行    }  }}

但是更重要的是初始化好内部的数据

function initDataPlace(){  for (var y = 0; y <= fieldHeight + 1; y++)  {    fieldData[y] = new Array;    fieldOpened[y] = new Array;    fieldFlagged[y] = new Array;    fieldSurroundingFlags[y] = new Array;    for (var x = 0; x <= fieldWidth + 1; x++)    {      fieldData[y][x] = false;         //将所有方块都初始设置为无雷      fieldOpened[y][x] = false;       //也都是没打开      fieldFlagged[y][x] = false;      //也没标记雷      fieldSurroundingFlags[y][x] = 0; //没有标记雷,周围标记计数器清零    }  }}

为了在页面加载的时候可以创建场景,我们需要重写 window.onload 方法

window.onload = function(){  initDataPlace();  create();}

随机生成场景

随机生成场景在第一次点开方块的时候调用。与其它的扫雷软件不同的是,这个例子中地雷的数量是不确定的。不信可以看下列代码

function initMines(noX, noY){  for (var y = 1; y <= fieldHeight; y++)  {    for (var x = 1; x <= fieldWidth; x++)    {      fieldData[y][x] = (Math.random() < mineDensity);    //是不是雷是概率事件,所以雷数量不定。      if ((y == noY) && (x == noX)) fieldData[y][x] = false;  //一定要保证第一次点开的地方不是雷    }  }//更新每个方块周围地雷数目  for (var y = 0; y <= fieldHeight + 1; y++)  {    fieldSurroundingMines[y] = new Array;    for (var x = 0; x <= fieldWidth + 1; x++)    {      fieldSurroundingMines[y][x] = 0;      if ( (x == 0) || (x == fieldWidth + 1) || (y == 0) || (y == fieldHeight + 1) ) continue;      if (fieldData[y - 1][x - 1] == true) fieldSurroundingMines[y][x]++;      if (fieldData[y - 1][x] == true) fieldSurroundingMines[y][x]++;      if (fieldData[y - 1][x + 1] == true) fieldSurroundingMines[y][x]++;      if (fieldData[y][x - 1] == true) fieldSurroundingMines[y][x]++;      if (fieldData[y][x + 1] == true) fieldSurroundingMines[y][x]++;      if (fieldData[y + 1][x - 1] == true) fieldSurroundingMines[y][x]++;      if (fieldData[y + 1][x] == true) fieldSurroundingMines[y][x]++;      if (fieldData[y + 1][x + 1] == true) fieldSurroundingMines[y][x]++;    }  }}

右键标雷

在之前的代码中,方块的右键事件被关联到了一个 setUnsetFlag 函数中。现在我们实现它

function setUnsetFlag(ID){  if (fieldOpened[getY(ID)][getX(ID)] == false)  //已经打开的方块不能再进行标记  {    b = document.getElementById(ID);    if (fieldFlagged[getY(ID)][getX(ID)])  //已经标记的地雷再点一次就会撤销标记,并且对于周围八个方块来说,它们的周围少了一个被标记的方块    {      b.src = "./img/Block.svg"      fieldSurroundingFlags[getY(ID) - 1][getX(ID) - 1]--;      fieldSurroundingFlags[getY(ID) - 1][getX(ID)]--;      fieldSurroundingFlags[getY(ID) - 1][getX(ID) + 1]--;      fieldSurroundingFlags[getY(ID)][getX(ID) - 1]--;      fieldSurroundingFlags[getY(ID)][getX(ID) + 1]--;      fieldSurroundingFlags[getY(ID) + 1][getX(ID) - 1]--;      fieldSurroundingFlags[getY(ID) + 1][getX(ID)]--;      fieldSurroundingFlags[getY(ID) + 1][getX(ID) + 1]--;    }    else  //没有被标记的方块则会被加上标记,并且对于周围八个方块来说,它们的周围多了一个被标记的方块    {      b.src = "./img/Flag.svg";      fieldSurroundingFlags[getY(ID) - 1][getX(ID) - 1]++;      fieldSurroundingFlags[getY(ID) - 1][getX(ID)]++;      fieldSurroundingFlags[getY(ID) - 1][getX(ID) + 1]++;      fieldSurroundingFlags[getY(ID)][getX(ID) - 1]++;      fieldSurroundingFlags[getY(ID)][getX(ID) + 1]++;      fieldSurroundingFlags[getY(ID) + 1][getX(ID) - 1]++;      fieldSurroundingFlags[getY(ID) + 1][getX(ID)]++;      fieldSurroundingFlags[getY(ID) + 1][getX(ID) + 1]++;    }    fieldFlagged[getY(ID)][getX(ID)] = !fieldFlagged[getY(ID)][getX(ID)]; //切换本方块的标记状态  }  return false;  //防止出现右键菜单}

左键开方块

在上面的代码中可以看到,左键点击事件是 blockDetonate。detonate在英文中是“引爆”的意思,这个词显然不够准确

function blockDetonate(ID){  // 防止意外事故、防止打开已打开的方块、防止打开被标上雷的方块  if ((ID != undefined) && (fieldOpened[getY(ID)][getX(ID)] == false) && (!fieldFlagged[getY(ID)][getX(ID)]))  {    if (firstClick) // 如果是第一次点击就生成雷区    {      initMines(getX(ID), getY(ID));      firstClick = false;    }    b = document.getElementById(ID);    fieldOpened[getY(ID)][getX(ID)] = true; //没打开的标记为打开    if (fieldData[getY(ID)][getX(ID)] == true) //如果是雷就游戏失败    {      b.src = "./img/Mine.svg";      GameOver();      return;    }    else // 反之就显示数字,Mine_0, Mine_1 ... Mine_9.svg    {      b.src = "./img/Mine_" + fieldSurroundingMines[getY(ID)][getX(ID)].toString() + ".svg";    }    // 将点开的方块的点击事件转为“若一个打开的方块周围标记的方块数量等于它本身显示的数字相同,则可以自动打开剩余的方块”    b.onclick = function(){surroundingDetonate(event.target.id)};    // 处理“若打开的方块周围无地雷则自动打开周围的地雷”    if (fieldSurroundingMines[getY(ID)][getX(ID)] == 0) surroundingDetonate(ID);    if (Check()) GameWin();  }}

“自动打开周围的地雷”

这个方法叫做 surroundingDetonate,涉及了递归。在翻阅了维基百科之后我才知道,这个递归是有名字的,叫做 Flood fill……

function surroundingDetonate(ID){  // 如果满足了可以自动打开的条件……  if (fieldSurroundingMines[getY(ID)][getX(ID)] == fieldSurroundingFlags[getY(ID)][getX(ID)])  {    //调用了blockDetonate方法,而那个方法又会回来调用surroundingDetonate方法,构成递归    if ((!fieldOpened[getY(ID) - 1][getX(ID) - 1]) && (!fieldFlagged[getY(ID) - 1][getX(ID) - 1])) blockDetonate(fieldBlocks[getY(ID) - 1][getX(ID) - 1]);    if ((!fieldOpened[getY(ID) - 1][getX(ID)]) && (!fieldFlagged[getY(ID) - 1][getX(ID)])) blockDetonate(fieldBlocks[getY(ID) - 1][getX(ID)]);    if ((!fieldOpened[getY(ID) - 1][getX(ID) + 1]) && (!fieldFlagged[getY(ID) - 1][getX(ID) + 1])) blockDetonate(fieldBlocks[getY(ID) - 1][getX(ID) + 1]);    if ((!fieldOpened[getY(ID)][getX(ID) - 1]) && (!fieldFlagged[getY(ID)][getX(ID) - 1])) blockDetonate(fieldBlocks[getY(ID)][getX(ID) - 1]);    if ((!fieldOpened[getY(ID)][getX(ID) + 1]) && (!fieldFlagged[getY(ID)][getX(ID) + 1])) blockDetonate(fieldBlocks[getY(ID)][getX(ID) + 1]);    if ((!fieldOpened[getY(ID) + 1][getX(ID) - 1]) && (!fieldFlagged[getY(ID) + 1][getX(ID) - 1])) blockDetonate(fieldBlocks[getY(ID) + 1][getX(ID) - 1]);    if ((!fieldOpened[getY(ID) + 1][getX(ID)]) && (!fieldFlagged[getY(ID) + 1][getX(ID)])) blockDetonate(fieldBlocks[getY(ID) + 1][getX(ID)]);    if ((!fieldOpened[getY(ID) + 1][getX(ID) + 1]) && (!fieldFlagged[getY(ID) + 1][getX(ID) + 1])) blockDetonate(fieldBlocks[getY(ID) + 1][getX(ID) + 1]);  }}

游戏实用工具

以上的代码构成了游戏的主体。在以上的代码中涉及到了三个还没有提及的函数,在此写明

Check()

这个函数在blockDetonate方法中出现,用途是判断是否达成了游戏胜利的条件

function Check(){  for (var y = 1; y <= fieldHeight; y++)  {    for (var x = 1; x <= fieldWidth; x++)    {      //只要有未打开的非雷方块就不算胜利;如果有打开的雷块,早就调用GameOver函数了      if ((fieldData[y][x] == false) && (fieldOpened[y][x] == false)) return false;    }  }  return true;}

GameWin()

这个函数同样也在blockDetonate方法中出现,用来进行游戏胜出后的工作

function GameWin(){  for (var y = 1; y <= fieldHeight; y++)  {    for (var x = 1; x <= fieldWidth; x++)    {      if ( (x == 0) || (x == fieldWidth + 1) || (y == 0) || (y == fieldHeight + 1) ) continue;      // 撤销对左键点击事件的侦听      document.getElementById(fieldBlocks[y][x]).onclick = function(){};      // 取消对右键事件的侦听      document.getElementById(fieldBlocks[y][x]).oncontextmenu = function(){return false};      // 没有标记的雷显示出标记      if (fieldData[y][x] == true) document.getElementById(fieldBlocks[y][x]).src = "./img/flag.svg";    }  }  // 不这样写,有可能会出现多个提示框  if (!alreadyCleared) alert("You Win!");  alreadyCleared = true;}

GameOver()

也是blockDetonate中出现的,游戏失败后的工作

function GameOver(){  for (var y = 1; y <= fieldHeight; y++)  {    for (var x = 1; x <= fieldWidth; x++)    {      document.getElementById(fieldBlocks[y][x]).onclick = function(){};      document.getElementById(fieldBlocks[y][x]).oncontextmenu = function(){return false};      if (fieldData[y][x] == true) document.getElementById(fieldBlocks[y][x]).src = "./img/Mine.svg";    }  }  alert("Game Over");}

主文件之后的事情

之后的事情就比较简单了,在这个js文件的同一目录下有一个/img目录,里面应有以下文件:
* Block.svg
* Flag.svg
* Mine_0.svg
* Mine_1.svg
* Mine_2.svg
* Mine_3.svg
* Mine_4.svg
* Mine_5.svg
* Mine_6.svg
* Mine_7.svg
* Mine_8.svg
* Mine.svg

还需要有一个HTML文件来加载这个Javascript,如下:

<!DOCTYPE html><html>    <head>        <title>Minesweeper Test</title>    </head>    <body>    </body>    <script src=".\game.js"></script></html>

这样就大功告成了,打开网页就可以玩扫雷了
(话说要玩扫雷肯定还是去打开程序,谁会去在线扫雷呢…………)

后记

事实上我提前知道了作业后熬夜写的这个扫雷,自以为提前完成了作业,谁知老师要求使用一个叫做Construct 2的软件来开发。我试了一下,发现我写不出扫雷……绝望

另外,这根本不是HTML5!

你这么浆糊般的代码谁愿意看啊!

……看样子我还要去学习一个啊

(完)

本文以Creative Commons 3.0 BY-SA协议发布,可无需经过同意转载,唯需遵守该协议

原创粉丝点击