.com-mine{&_header{背景颜色:#eceff1;底部边距:.5em;边界半径:4px;填充:.8em;框阴影:01px3pxrgba(0,0,0,0.2),01px1pxrgba(0,0,0,0.14),02px1px-1pxrgba(0,0,0,0.12);显示:弹性;对齐项目:flex-end;ul{弹性:1;宽度:0;li{显示:内联块;保证金:.3em1em.3em0;}}}&_block{位置:相对;&:not(.is-open){光标:指针;}.back{显示:块;宽度:100%;}}&_num,&_flag,&_mine{位置:绝对;顶部:0;右:0;底部:0;左:0;}&_num{显示:弹性;对齐项目:居中;证明内容:居中;字体大小:16px;}&_flag{background:url("./img/flag-color.png")无重复中心center;背景大小:60%;}&_mine{background:url("./img/bomb-color.png")无重复中心center;背景尺寸:30%;}table{border-radius:4px;填充:1px;背景色:#b0bec5;}}接上一篇教程:H5小游戏开发教程页面基础布局开发不好意思让大家久等了。从上周开始,工作很忙,没时间写。这期间也在思考有没有更好或者更简单的实现方式,能够在不同的设备上有很好的体验;通过本教程,我将为大家带来一个非常简单的扫雷游戏实现方案;本来打算用两篇文章,因为太简单了,就用一篇文章搞定了。先来欣赏一下本文实现的游戏界面:扫雷游戏界面扫雷游戏界面我想大家应该都玩过Windows版的扫雷游戏吧?事实上,这个游戏有成功的秘诀。考查你思考问题的能力;如果一个格子的值为1,那么它周围8个方向只有一个地雷;同理,如果一个格子的值为2,那么它周围8个方向只有2个地雷;由于一个格子最多有8个相邻的格子,所以一个格子周围最多有8个地雷。现在,我们正式开始。首先,我们在src根目录下创建一个文件:shared.js文件,用于定义所有游戏通用的变量和函数;我们在这个文件中定义了一个genArr函数;这个函数非常简单,用于创建一个指定的长度并用指定的值填充它;在我们的游戏教程中,这个函数将被广泛用于生成遍历数组。exportconstgenArr=(len,v)=>Array(len).fill(v)然后,在src/components/mine文件夹下创建一个文件game.js。我们先用JS文档注释声明了两个类型,并介绍了我们将要用到的一些功能;通过语义,大家应该能理解这两个类型中字段的含义吧?GameOptions类型中rows为行数,cols为列数,mineCount为地雷数;Block类型的num是格子的值,open是打开的flag,flag是flag标志,flag用来标记已经确定的地雷,防止误点击。/***@typedef{{rows:number,cols:number,mineCount:number}}GameOptions游戏选项**@typedef{{num:number,open:boolean,flag:boolean}}Blockgrid*/import{computed,reactive}from'vue'import{genArr}from'../../shared'然后,我们导出一个匿名函数。大家记住:我们后面所有的JS代码都是写在这个函数里面的。exportdefault()=>{}然后,我们创建一个反应对象来保存游戏状态。conststate=reactive({rows:9,//行数cols:9,//列数mineCount:10,//地雷数/**@type{Block[][]}二维数组forstoringgrids*/blocks:[],isOver:false,//游戏结束isFirstClick:true//是否是第一次点击})然后,我们定义一个函数,根据出现的次数生成一个二维数组网格行和列,网格初始值num全部设置为0,open和flag属性均为false。/**@returns{Block[][]}*/constgenBlocks=(rows,cols)=>{returngenArr(rows).map(()=>genArr(cols).map(()=>({num:0,open:false,flag:false})))}然后,我们定义一个函数来获取网格对象。由于我们需要在很多地方获取网格对象,所以最好定义一个函数。constgetBlock=(row,col)=>(state.blocks[row]||[])[col]扫雷游戏有个原则,第一次点击的格子不能是地雷,所以我们必须在玩家第一次点击时打开它。一格后,生成地雷分布图;我们生成地雷分布图的函数需要行坐标和列坐标,保证坐标一定不是地雷。下面是生成矿山分布图的函数:constgenMap=(row,col)=>{const{blocks}=stategenArr(state.rows).reduce((t,_,i)=>[...t,...genArr(state.cols).map((_,j)=>[i,j])],[])//由行和列坐标组成的一维数组。filter(_=>!(_[0]===row&&_[1]===col))//过滤掉玩家第一次点击的坐标.sort(()=>Math.random()-.5)//对坐标进行随机排序。slice(0,state.mineCount)//根据地雷的数量对数组进行切片。forEach(_=>{blocks[_[0]][_[1]].num=9//遍历坐标数组,将坐标对应的格子对象的值设置为9,9代表打雷})//下面的遍历用于更新每个非地雷网格周围的地雷数量,num的值为地雷数量blocks.forEach((a,i)=>{a.forEach((b,j)=>{if(b.num<9){b.num=[getBlock(i-1,j-1),getBlock(i-1,j),getBlock(i-1,j+1),getBlock(i,j+1),getBlock(i+1,j+1),getBlock(i+1,j),getBlock(i+1,j-1),getBlock(i,j-1)]。过滤器(_=>_&&_.num>8).length}})})}当玩家点击一个格子时,如果该格子的值为0,那么我们需要深度递归遍历自动打开所有相邻的值为0和1的格子;下面是自动打开网格的函数定义:constopenBlocks=(row,col)=>{;[[row-1,col],[row,col+1],[row+1,col],[row,col-1]].forEach(coords=>{constblock=getBlock(...coords)//es6参数扩展if(block&&!block.open&&!block.flag){//如果网格存在andisnotopenedandisnotflagdif(block.num<2){//如果网格值为0和1,则打开网格block.open=true}if(block.num<1){//如果value为0,进行一次深度递归遍历下面是自动打开所有地雷的函数:constopenMineBlocks=()=>{state.blocks.forEach(a=>{a.forEach(b=>{if(b.num>8){b.open=true}})})}当玩家打开所有不是雷的格子时,我们需要结束game,下面是判断挑战是否完成的函数:constisFinish=()=>{returnstate.blocks.every(a=>a.filter(b=>b.num<9).every(b=>b.open))}我们需要一个开始新游戏的函数,这个函数用于重置或更新游戏状态,并开始游戏;以下是开始游戏函数的定义:/**@param{GameOptions}options*/conststart=(options={})=>{Object.keys(options).forEach(key=>{if(options[key]){state[key]=options[key]}})state.isOver=falsestate.isFirstClick=truestate.blocks=genBlocks(state.rows,state.cols)}我们需要一个处理网格点击的函数事件constonBlockClick=(row,col)=>{constblock=getBlock(row,col)if(state.isOver||block.flag||block.open)return//如果游戏结束或网格被标记或网格关闭打开,直接返回block.open=trueif(state.isFirstClick){//如果是第一次点击网格,则生成矿图state.isFirstClick=falsegenMap(row,col)}elseif(block.num>8){//如果格子是地雷,结束游戏并自动引爆所有地雷state.isOver=trueopenMineBlocks()returnsetTimeout(()=>confirm('挑战失败!你要不要重新开始?')&&start(),100)}elseif(block.num<1){openBlocks(row,col)//使用深度递归遍历,打开值为0和1的相邻单元格}//如果挑战成功,则给玩家2个选择:挑战更高难度或重新挑战本难度isFinish()&&setTimeout(()=>confirm('挑战成功!是否要挑战更高难度?')?开始({行:state.rows+1,mineCount:状态。mineCount+state.cols}):start(),100)}我们需要一个函数来处理网格的上下文菜单,它用于在插入标志和移除标志之间切换。/**@param{PointerEvent}evt*/constonBlockContextmenu=(row,col,evt)=>{evt.preventDefault()if(state.isOver)returnconstblock=getBlock(row,col)if(!block.open){block.flag=!block.flag}}下面是3个计算属性的定义,分别用来统计flag的个数、已打开的格子数、未打开的格子数。constflagCount=computed(()=>{returnstate.blocks.reduce((t,a)=>t+a.filter(_=>_.flag).length,0)})constopenCount=computed(()=>{returnstate.blocks.reduce((t,a)=>t+a.filter(_=>_.open).length,0)})constunopenCount=computed(()=>state.rows*state.cols-openCount.value)最后,我们在导出的匿名函数的底部返回组件中使用的变量和函数。return{state,flagCount,openCount,unopenCount,start,onBlockClick,onBlockContextmenu}下面是src/components/mine/Index.vue文件的源码。我们使用表格来承载游戏界面;我觉得table很适合二维数组数据的可视化。- 网格布局:{{state.rows}}×{{state.cols}}
- 地雷数量:{{state.mineCount}}
- 标志数量:{{flagCount}}
- 打开数量:{{openCount}}
- 未打开计数:{{unopenCount}}
新游戏8":class="`${cls}_mine`">0":class="[`${cls}_num`,getNumCls(b.num)]">{{b.num}}