前言

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

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

文件切分

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

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

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

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

难度选择

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

  1. // 难度
  2. level: [
  3. {
  4. text: '青铜', // 难度描述
  5. value: [9, 9, 10] // 格子横排数,格子纵排数,雷数
  6. }, {
  7. text: '黄金',
  8. value: [12, 9, 20]
  9. ]

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

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

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

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

代码如下:

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

游戏界面

画格盘

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

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

  1. <div
  2. v-for="col in cols" :key="Math.random() + col"
  3. class="game-content-row">
  4. <span
  5. v-for="row in rows" :key="Math.random() + row"
  6. class="game-block">
  7. <span>{{(col - 1) * rows + row - 1}}</span>
  8. </span>
  9. </div>

随机生成地雷

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

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

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

把地雷位置暴露出来

格子周围的雷数

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

  1. // 格子属性
  2. lattice: [{
  3. index: 0, // 格子索引
  4. mineNum: 0, // 周围雷数
  5. isMine: false, // 是否是雷
  6. isOpen: false, // 是否已经被点开
  7. isMark: false, // 是否被标记
  8. }],

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

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

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

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

点击交互

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

点数字

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

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

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

点空白

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

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

  1. // 代码把下半部分补齐
  2. handleClickLattice(lattice) {
  3. ... else {
  4. // 是空白
  5. const latticeIndexArr = this.getLatticeIndex(lattice.index);
  6. this.showWhiteAround(lattice, latticeIndexArr);
  7. }
  8. },
  9. // 展示周围的空白标记,直至边缘(格子边缘或者数字)
  10. showWhiteAround(lattice, latticeIndexArr) {
  11. // 避免有重复的数据停不下来,去个重
  12. latticeIndexArr = [...new Set(latticeIndexArr)];
  13. for (let i = 0; i < latticeIndexArr.length; i++) {
  14. const item = latticeIndexArr[i];
  15. latticeIndexArr.splice(i, 1);
  16. i--;
  17. if (this.lattice[item].isOpen) {
  18. continue;
  19. }
  20. this.lattice[item].isOpen = true;
  21. if (!this.lattice[item].mineNum) {
  22. const arr = this.getLatticeIndex(this.lattice[item].index);
  23. this.showWhiteAround(this.lattice[item], latticeIndexArr.concat(arr));
  24. }
  25. }
  26. },

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

右键标记雷点

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

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

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

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

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

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

游戏结束一共有三种情况: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. php检测函数

    验证字符串是否为电子邮箱  var_dump(filter_var('bob@example.com', FILTER_VALIDATE_EMAIL));       //bob@example.co ...

  2. H3C RIPv1的缺点

  3. jquery核心基础

    jquery对对象的操作:   检查对象类型: 老式的javascript使用typeOf()操作符,但他是不符合逻辑的,在某些情况下,typeOf()返回的不是一个正确的值,或者返回一个出乎意料的值 ...

  4. Linux 内核 kobject 初始化

    本书已经展示了许多数据类型, 带有简单的在编译或者运行时初始化机制. 一个 kobject 的初始化有些复杂, 特别当使用它的所有函数时. 不管一个 kobject 如何使用, 但是, 必须进行几个步 ...

  5. 学习Java第七周

    重要知识点 1.“super”的用法 构造器和方法,都用关键字super指向超类,但是用的方法不一样.方法用这个关键字去执行被重载的超类中的方法 2.接口和抽象类的异同 相同: 1.接口和抽象类都有抽 ...

  6. HDU4528 小明捉迷藏 [搜索-BFS]

    一.题意 小明S在迷宫n*m中找大明D和二明E,障碍物X不能走,问你计算是否能在时间t内找到大明和二明 二.分析 2.1与普通的BFS不同,这里可以走回头路,这里应该建立四维的标记数组标记数组,例如v ...

  7. 025.MFC_窗口操作

    窗口操作 一.建立名为dialogOp 的mfc 工程 ,添加9个button 和1个check box组件,并按如图修改caption属性. 最大化窗口 双击最大化button,进入dialogOp ...

  8. docker 挂载目录挂载不上**

    最近做项目做一个shared Dynamodb, 使用docker挂载一个image,发现怎么都挂载不上, 使用 $ pwd E:\Work\Aws\git\schema\dynamodb\local ...

  9. 如何删除Word自动编号后文字和编号之间的空白距离

    一.出现的现象:使用word进行自动编号之后,编号和其后的文字出现如下图所示的空白 二.如何解决问题 选中列表内容右键->调整列表缩进->选择“编号之后(W)"为不特别标注-&g ...

  10. Effective TestStand Operator Interfaces

    目录 为什么要使用操作员界面? 是什么决定一个好的界面? 用户的类型 和 界面的必要元素 TestStand 架构 TestStand 自带的例子 自定义用户界面 TestStand 提供的三个管理控 ...