前言

话说阿望还在大学时,某一天寝室突然停网了,于是和室友两人不约而同地打开了扫雷,比相同难度下谁更快找出全部的雷,玩得不亦乐乎,就这样,扫雷伴我们度过了断网的一周,是整整一周啊,不用上课的那种,可想而知我们是有多无聊了。

这两天临近过年了,该放假的已经放假了,不该放假的已经请假了,公交不打挤了,地铁口不堵了,公司也去了大半部分的人了,就留阿望这种不得不留下来值班的人守着空荡荡的办公室了,于是,多年前那种无所事事的断网心态再次袭来,于是,想玩扫雷的心再次蹦跶出来,于是,点开了电脑的附件,于是,发现我的电脑上并没有扫雷【手动微笑.jpg】。

怎么想起要写扫雷的阿望就不多废话了,直接开干。

扫雷游戏规则

想当年阿望还是在大学才参透扫雷的游戏规则的,初高中的时候都是靠运气瞎点,从没赢过,当然了,境界提升以后,追求的自然就不是赢了,而是速度。

来,看规则:

大白话游戏规则,计算逻辑如下:

  1. 游戏开始,选择难度
  2. 鼠标左键随机点开一个格子A(你想用右键标雷也可以)
  3. 左键点击后一共有三种情况
    1. 是数字,假设是2,表示以该格子A为中心的九宫格除了该格子以外的8个格子内有2个雷

      2)是空白,表示以该格子A为中心的九宫格除了该格子以外的8个格子内没有雷,并且系统计算分别以这8个格子为中心九宫格除了该格子以外的8个格子是否是空白

      a. 是空白,继续计算以这8个格子为中心九宫格除了该格子以外的8个格子是否是空白,循环扩散

      b. 是格子边界,停止边界扩散

      c. 是数字,数字格子翻出来显示,停止扩散

      3)是雷,game over 了。。。
  4. 如果点第一次只翻了一个数字是比较难开始游戏的,如果第一次翻了一大片出来,恭喜你,可以开始秀你的智商了,在确认是雷点的格子点右键,即可标记地雷格,表示你认定这个格子是地雷
  5. 如果数字周围的雷被全部标记出来了,但数字周围还有没被翻开的格子的话,双击该格子,自动将其他格子翻开

好,这下规则我们了解了,接下来,摩拳擦掌,开始写代码咯,本次代码用vue来写,没有原因,习惯了而已。

扫雷代码逻辑

看着游戏规则,我们先来理一理,要如何完成这个功能,我们以最简单的,最能使人理解的步骤,来完成这次功能。

  1. 难度选择
  2. 根据选择的游戏难度绘制游戏主界面,就是格盘
  3. 根据选择的游戏难度随机生成地雷放到盘中
  4. 计算每个格子周围的雷数
  5. 完成用户交互(左键点击翻格子,右键点击放雷)
  6. 空白格扩散翻格子
  7. 计算游戏结束

生成项目

避免vue新手不知如何下手,那就从建项目开始吧,环境安装我就不讲了,脚手架也是要有的

第一步,初始化lovesweeping工程(阿望不喜欢地雷,喜欢小桃心)

项目很小,不需要路由,不需要vuex,即可完成,只带了sass,连eslint都可以不要,看官们可以根据自己的喜好建项目

项目生成之后,helloworld就可以删掉了,它的存在并没有什么意义

文件切分

这一步主要是构建工程结构,简单画一下主要的几个文件

- src
- components
- SelectLevel.vue [新增,难度选择组件]
- LoveSweeping.vue [新增,游戏界面组件]
- App.vue [父组件,负责组件间的切换和某些数据传递]
- main.js
- package.json

好,初步认识了项目结构以后,咱把该新建的建好,可以不加东西,不报错就行

然后就是把组件间的切换代码写好,再来一步步的填充代码

难度选择

这一步很简单,首先画好难度选择的页面,难度可以自己设置,你觉得合理就行,我这里的数据格式是这样的,用一个对象表示一个难度等级,对象中包含了难度描述,以及难度设置,设置中包含了格子横排数,格子纵排数,雷数

    // 难度
level: [
{
text: '青铜', // 难度描述
value: [9, 9, 10] // 格子横排数,格子纵排数,雷数
}, {
text: '黄金',
value: [12, 9, 20]
]

然后模板中直接渲染列表,这样做的好处是,想要增加难度直接在数组中添加数据即可

    <li v-for="(item, index) in level" :key="index" @click="handleChoseLevel(item.value)">{{ item.text }}</li>

该组件中只有一个方法:选择难度之后,跳转到游戏主界面上去,因为项目没有用路由,直接使用组件间的切换,所以,这个方法只负责告诉父组件,我已经选择好难度了,可以开始游戏了

    // 选择难度
handleChoseLevel(level) {
this.$emit("chose-level", level);
}

代码如下:

界面长这样,当然,你要觉得难看自己换个样式也行

游戏界面

画格盘

通过游戏难度选择来决定游戏格盘的大小,组件间已通过App将游戏难度传至界面组件中,我们用props把数据接收到,消化成自己的数据,画格盘需要的数据有:横排格子数,纵排格子数

画格子:我们将格子的索引暴露出来,后续可以帮助我们试错。整个格局有两种方式来表示格盘,坐标式和索引式,比如横9纵9的格子,[0, 0]代表第1个格子,[2, 3]代表第三行第四列也就是第20个格子。此次使用索引式来标志格盘

    <div
v-for="col in cols" :key="Math.random() + col"
class="game-content-row">
<span
v-for="row in rows" :key="Math.random() + row"
class="game-block">
<span>{{(col - 1) * rows + row - 1}}</span>
</span>
</div>

随机生成地雷

首先data中添加一个minePosition属性,用来记录雷点位置

随机生成地雷比较简单,主要注意,生成的地雷点数在格盘个数范围内,那么就可以写出随机生成的地雷了。界面组件已收集到横排格子数、纵排格子数、雷数,那么就能得到格子总数,假设横9纵9,10个雷,那么就是生成10个81以内的随机数(如果索引从0开始,即80以内)。

    // 随机获取雷点位置
getMinePosition() {
// 定义一个数组装不重复的格点
let mineArr = [];
// 循环雷数生成不重复的雷点
for (let n = 0; n < this.gameInfo[2]; n++) {
const random = Math.floor(Math.random() * this.latticeNum);
if (mineArr.indexOf(random) === -1) {
mineArr.push(random);
} else {
n--;
}
}
this.minePosition = mineArr;
},

把地雷位置暴露出来

格子周围的雷数

确认了雷点位置,接下来要做的就是确认每一个非雷点位置周围的雷的数量,我们用对象来描述一个格子,这个对象主要包含以下几个属性

    // 格子属性
lattice: [{
index: 0, // 格子索引
mineNum: 0, // 周围雷数
isMine: false, // 是否是雷
isOpen: false, // 是否已经被点开
isMark: false, // 是否被标记
}],

这里我们主要用到index, isMine, mineNum属性,这一步,主要是计算每个格子元素的mineNum值,依赖于以下两个方法,个人觉得扫雷游戏最难理解的,最难捋清的逻辑,其中一个就是获取非雷点位置周围8个位置索引的方法getLatticeIndex(另一个是点击空白格扩散)

    // 获取格子周围的雷数,
getMineNumAroundLattice(lattice, index) {
// 先获取格子周围的有效索引
const latticeIndexArr = this.getLatticeIndex(index);
// 循环索引,索引值在雷点数组中的,即为雷,当前格子的雷点数加1
latticeIndexArr.forEach(i => {
if (this.minePosition.indexOf(i) > -1) {
lattice.mineNum ++;
}
});
},
// 获取格子周围的有效索引
getLatticeIndex(index) {
// 存索引值的变量
let latticeIndexArr = [];
// 当前格子位于第几行
const latticeRow = Math.ceil(index / this.rows);
// 当前格子位于第几列(求余为0说明是最右边一列)
const latticeCol = Math.ceil(index % this.rows) || this.rows;
// 第一行没有上一行,不需要计算减1的行值,最后一行没有下一行,不需要计算加1的行值
for (let i = (latticeRow === 1 ? 0 : -1); i < (latticeRow === this.cols ? 1 : 2); i++) {
// 第一列没有左列,不需要计算减1的列值,最后一列没有右列,不需要计算加1的列值
for (let j = (latticeCol === 1 ? 0 : -1); j < (latticeCol === this.rows ? 1 : 2); j++) {
// 索引值 = (当前行值 + (上一行【-1】/当前行【0】/下一行【+1】) - 1【1是索引从0开始,所以需要减去】) * 每行格子数 + 当前列值 + (上一列【-1】/当前列【0】/下一列【+1】)
const latticeIndex = (latticeRow + i - 1) * this.rows + (latticeCol + j);
latticeIndexArr.push(latticeIndex);
}
}
return latticeIndexArr;
},

有了这两个方法,咱成功地获取到了每个非雷点格子周围的雷的数量,来,展示出来,这样展示的好处是,我们一眼就可以看出算法是否正确

没问题了,来,接着往下走,格盘数据基本都设置好了,那我们接下来要做的就是,点开格子操作

点击交互

这一步先做简单点,有个明显的区别就可以了,点雷我们先不管,先看点数字和空白的情况,首先得明确,到时候格子的可见属性是全部要被隐藏的,点击了才会显示出来,这就用到了我们上一步提到的isOpen属性,默认肯定全是不可见的,点击之后,非雷翻开

点数字

点数字很简单,直接翻开,将isOpen属性设置为true

来点不一样的,isOpen === true 的时候字体变红色,走你┏ (゜ω゜)=☞

@click.left="handleClickLattice(lattice[(col - 1) * rows + row - 1])"
    // 点了格子
handleClickLattice(lattice) {
// 是数字
if (lattice.mineNum) {
if (!lattice.isOpen && !lattice.isMark) {
lattice.isOpen = true;
}
}
},

点空白

第二个难点来咯,点空白格需要注意以下几点:1、空白格表示周围8格都没有雷 2、扩散周围8格,判断雷数,循环往复 3、遇边界停止扩散,遇数字停止扩散

假设横9纵9的格盘,第二排第三列格为空白格即第12格,那么点了该空白格之后,首先将其与周围8格(2,3,4,11,12,13,20,21,22)一起,isOpen置为true,然后分别以周围8格为中心,判断该格子是数字,停止扩散,是空白格,继续扩散

    // 代码把下半部分补齐
handleClickLattice(lattice) {
... else {
// 是空白
const latticeIndexArr = this.getLatticeIndex(lattice.index);
this.showWhiteAround(lattice, latticeIndexArr);
}
},
// 展示周围的空白标记,直至边缘(格子边缘或者数字)
showWhiteAround(lattice, latticeIndexArr) {
// 避免有重复的数据停不下来,去个重
latticeIndexArr = [...new Set(latticeIndexArr)];
for (let i = 0; i < latticeIndexArr.length; i++) {
const item = latticeIndexArr[i];
latticeIndexArr.splice(i, 1);
i--;
if (this.lattice[item].isOpen) {
continue;
}
this.lattice[item].isOpen = true;
if (!this.lattice[item].mineNum) {
const arr = this.getLatticeIndex(this.lattice[item].index);
this.showWhiteAround(this.lattice[item], latticeIndexArr.concat(arr));
}
}
},

这一步写完,基本明面上的扫雷步骤就已经完成了,handleClickLattice方法再加一步判断,如果是雷,游戏结束

右键标记雷点

这个就很简单了,写个右击事件,修改一下格子的isMark和isOpen属性,这一步的基本逻辑就是

  1. 右击格子
  2. 格子本身已经被打开

    1)是:格子已经被标记为地雷?

    a. 是:取消标记(isMark和isOpen取反),剩余地雷数+1

    b. 否:说明是右击了已经被点开的数字格,不做任何操作

    2)否:isMark和isOpen取反,记录该格子已经被标记为地雷,格子处于打开状态,剩余地雷数-1,判断是否结束
        // 右键确认是雷点
handleSureMinePoint(lattice) {
if (!lattice.isOpen) {
lattice.isMark = true;
lattice.isOpen = true;
this.minePosition.splice(this.minePosition.indexOf(lattice.index), 1);
this.judgeIsOver();
} else {
if (lattice.isMark) {
lattice.isMark = false;
lattice.isOpen = false;
this.minePosition.push(lattice.index);
}
}
},
游戏是否结束

游戏结束一共有三种情况:1、点到雷了,直接结束 2、雷被标记完了(有可能失败了,标错了) 3、翻开的格子数 + 雷数 = 总格子数

完成功能

接下来要做的就是把格子属性隐藏起来,假装不知道,再假装鼠标一点,格子就翻过来了。这就用到之前提到的格子属性isMark和isOpen,本身元素处于隐藏状态,当被标记或者被打开的时候设置相应的属性使其可见就行了,如此,便完成了扫雷的基本功能,有兴趣的小朋友可以自己融合多种功能试一试

当然,这只是其中一种实现方式,把所有的计算全部放在玩游戏之前了,爱动脑筋的朋友们也可以想想,如果放在每一次点击时做计算该如何组织代码

阿望的源代码中还集合了【重开一局】、【改变难度】、【游戏计时】等功能,样式兼容手机和PC在线玩,在手机上玩的时候我在纠结手机如何模仿PC端的右键点击标雷操作,没有好的想法,不想用双击,于是多加了一个状态,点击页面【标记】按钮,即表示标记雷点,再点击一次表示还原,正常点开数字格,坐火车无聊的小朋友可以玩一玩哟

查看阿望的源码:mineSweeping

在线试玩:mine-sweeping-online

希望各位看官不吝右上角赐个小星星哦,阿望这厢有礼啦,新年快乐啦★,°:.☆( ̄▽ ̄)/$:.°★

阿望教你用vue写扫雷(超详细哦)的更多相关文章

  1. 使用vue写扫雷游戏

    上班闲来没事做,,心血来潮.想用刚学的vue,写一个扫雷游戏..好了,直入正题. 第一步,先制作一个10x10的格子图..这个divcss就不说了..大家都会. 第二步,制造一个数组,用来生成随机雷区 ...

  2. 教你用Vue写一个开心消消乐

    之前做过一个算法题,算法要求就是写一个开心消消乐的逻辑算法,当时也是考虑了一段时间才做出来.后来想了想,既然核心算法都有了,能不能实现一个开心消消乐的小游戏呢,于是花了两天时间做了一个小游戏出来. 效 ...

  3. Vue2全家桶之一:vue-cli(vue脚手架)超详细教程

    本文转载于:https://www.jianshu.com/p/32beaca25c0d   都说Vue2简单上手容易,的确,看了官方文档确实觉得上手很快,除了ES6语法和webpack的配置让你感到 ...

  4. vue-cli(vue脚手架)超详细教程

          都说Vue2简单上手容易,的确,看了官方文档确实觉得上手很快,除了ES6语法和webpack的配置让你感到陌生,重要的是思路的变换,以前用jq随便拿全局变量和修改dom的锤子不能用了,vu ...

  5. 手把手教你从零写一个简单的 VUE

    本系列是一个教程,下面贴下目录~1.手把手教你从零写一个简单的 VUE2.手把手教你从零写一个简单的 VUE--模板篇 今天给大家带来的是实现一个简单的类似 VUE 一样的前端框架,VUE 框架现在应 ...

  6. 手把手教你从零写一个简单的 VUE--模板篇

    教程目录1.手把手教你从零写一个简单的 VUE2.手把手教你从零写一个简单的 VUE--模板篇 Hello,我又回来了,上一次的文章教会了大家如何书写一个简单 VUE,里面实现了VUE 的数据驱动视图 ...

  7. 教你用shell写CGI程序

    以前用shell写过一些cgi的例子.今天向大家介绍一下. CGI是一种接口的标准,并不区分编程语言,也就是说,CGI可以用任何一种语言编写,只要这种语言具有标准输入.输出和环境变量.CGI会将标准输 ...

  8. 一步一步教你用 Vue.js + Vuex 制作专门收藏微信公众号的 app

    一步一步教你用 Vue.js + Vuex 制作专门收藏微信公众号的 app 转载 作者:jrainlau 链接:https://segmentfault.com/a/1190000005844155 ...

  9. vue写出放大镜的效果

    用vue写出放大镜查看图片的效果. 安装 npm install vue2.0-zoom 引入 import imgZoom from 'vue2.0-zoom' 组件 components: { i ...

随机推荐

  1. java 声明多个泛型类型和通配符

    若一个类中多个字段需要不同的泛型声明,则在声明类的时候指定多个泛型类型即可: 格式: public interface IDAO<PK, T> { PK add(T t); void re ...

  2. 2018-8-10-如何在-UWP-使用-wpf-的-Trigger-

    title author date CreateTime categories 如何在 UWP 使用 wpf 的 Trigger lindexi 2018-08-10 19:16:51 +0800 2 ...

  3. Moq基础 判断方法被执行

    如果想知道注入的类的某个方法被使用了几次,就可以通过 mock 提供的方法进行判断方法有没被执行或被使用多少次 本文是一个系列,具体请看 Moq基础(一) 为什么需要单元测试框架 Moq基础(二) 快 ...

  4. asp dotnet core 支持客户端上传文件

    本文告诉大家如何在 asp dotnet core 支持客户端上传文件 新建一个 asp dotnet core 程序,创建一个新的类,用于给客户端上传文件的信息 public class Kanaj ...

  5. Linux 内核硬件抽象

    我们结束 PCI 的讨论, 通过快速看一下系统如何处理在市场上的多种 PCI 控制器. 这只 是一个信息性的小节, 打算来展示给好奇的读者, 内核的面向对象分布如何向下扩展到最 低层. 用来实现硬件抽 ...

  6. CodeForces Goodbye 2017

    传送门 A - New Year and Counting Cards •题意 有n张牌,正面有字母,反面有数字 其中元音字母$a,e,o,i,u$的另一面必须对应$0,2,4,6,8$的偶数 其他字 ...

  7. XSS攻击及防范

    1.什么是XSS攻击 跨站脚本攻击(Cross Site Scripting),攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到 ...

  8. C++模板特化与偏特化

    C++模板 说到C++模板特化与偏特化,就不得不简要的先说说C++中的模板.我们都知道,强类型的程序设计迫使我们为逻辑结构相同而具体数据类型不同的对象编写模式一致的代码,而无法抽取其中的共性,这样显然 ...

  9. MySQL基础篇(04):存储过程和视图,用法和特性详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.存储过程 1.概念简介 存储程序是被存储在服务器中的组合SQL语句,经编译创建并保存在数据库中,用户可通过存储过程的名字调用执行.存储过程 ...

  10. 「Main」

    这里就是我的小主页辣. My Introduction I am Louch. 姓名:楼翰诚 性别:汉纸 生日:2004/03/09(和加加林同一天呢QAQ) 星座:双鱼座 学校:义乌中学 QQ:10 ...