用js制作一个扫雷游戏(vue版)

使用js来制作一个扫雷游戏,可以分为以下几个步骤

1、根据不同难度构建扫雷游戏区域;
2、在游戏区域中放置地雷;
3、处理点击事件;
4、处理游戏结束事件

1、根据不同难度构建扫雷游戏区域

创建一个二维数组进行扫雷区域的描述,这样就可以根据两个索引获取到每一个格子的状态描述,我给每一个格子设置了三个属性:
在这里插入图片描述

isMark:该格子是否已经被标记;
isOpen:该格子是否已经被点开;
value:该格子的内部的数字(‘’表示周围没有雷,1-8表示周围的雷数,9表示雷);

然后使用vue的v-for将雷区绘制出来;
在这里插入图片描述

2、在游戏区域中放置地雷

这部分主要思路是使用while循环,每次循环产生行和列的随机数,然后将对应行列位置的value设置为9(如果已经存在,则该次循环无效),直到将指定的地雷放置完毕;

				let putedMineNum=0;  //已经放置完成雷的个数
				while(putedMineNum<mine){  //mine表示总共需要放置多少个雷
					let putRow=Math.floor(Math.random()*row);
					let putColumn=Math.floor(Math.random()*column);
					let putPosition=numArr[putRow][putColumn];
					//如果随机出来的位置上没有数据,放雷
					if(!putPosition){
						numArr[putRow][putColumn]={
							value:9,
							isMark:false,
							isOpen:false
						};
						putedMineNum++;
					}
				}

放置地雷完成之后,就需要计算雷区的提示数字了
在这里插入图片描述

这部分主要思路就是遍历每一个格子,然后获取其周围格子value==9的数量,思路不难,就是遍历比较多

	function _getEveryMineNum(){
				let {numArr}=this;
				let rowLength=numArr.length;
				let columnLength=numArr[0].length;
				for(let row=0;row<rowLength;row++){
					for(let column=0;column<columnLength;column++){
						let curContent=numArr[row][column]; //格子对象
						// 如果该格子是雷块,跳过
						if(curContent&&curContent.value===9){
							continue;
						}
						//格式周围的八个格子
						let posArr=[[row-1,column],[row-1,column-1],[row-1,column+1],[row,column-1],[row,column+1],[row+1,column+1],[row+1,column-1],[row+1,column]];
						let mineNum=0;
						for(let i=0;i<8;i++){
							let _row=posArr[i][0];
							let _column=posArr[i][1];
							//如果周围的格子超出了整个雷区的处理
							if(_row<0||_column<0||_row>numArr.length-1||_column>numArr[0].length){
								continue
							}
							let curPos=numArr[_row][_column];
							if(curPos&&curPos.value==9){
								mineNum++
							}
						}
						numArr[row][column]={
							value:mineNum||'',
							isMark:false,
							isOpen:false
						}
						this.$set(this.numArr,row,numArr[row])
					}
				}
			},

经过这两步,我们就可以获取到整个雷区的构造了。

3、处理点击事件

通过event.which可以判断鼠标是点击左键(1)还是右键(3),点击事件的规则是:

1、点击右键打上标记或者取消标记,
2、点击左键进行判断:

  • 该格子是雷,游戏结束;
  • 该格子是数字,相安无事;
  • 该格子是空白(周围无雷)

其中当格子是空白时,会打开周围的格子,直到碰到数字格子或者游戏边界为止,如下图
在这里插入图片描述

			function _searchMine(e,row,column){
				let {numArr,isEnd}=this;
				if(isEnd){
					return
				}
				let curPos=numArr[row][column];
				//点击鼠标右键
				let {which}=e;
				if(which==3){
					//点击右键
					this._rightClick(row,column);
					return 
				}
				//已被标记,不可左击
				if(curPos.isMark){
					return
				}
				if(curPos.value==9){
					//点击到雷,游戏结束
					this._failGame();
					return
				}
				//点击左键
				this._leftClick(row,column);
				//判断是否点击完所有格子
				this._isAllClick();
			},

右键部分的逻辑判断很简单,只需要判断该格子的isMark状态,进行取反操作:

			//点击右键
			_rightClick(row,column){
				let {numArr,leftMineNum,gameStep}=this;
				let curBlock=numArr[row][column];
				let {isMark}=curBlock;
				curBlock.isMark=!isMark;
				//标记数加或者减
				leftMineNum=leftMineNum+(isMark?1:-1);
				gameStep=gameStep+(isMark?1:-1);
				
				this.numArr=numArr;
				this.leftMineNum=leftMineNum;
				this.gameStep=gameStep;
				//如果已经标记完毕,立刻开始游戏结算
				if(leftMineNum<=0){
					this._endGame();
					return 
				}
			},

左键的逻辑如下:

			//点击左键
			_leftClick(row,column){
				let {numArr,gameStep}=this;
				let rowMax=numArr.length;
				let columnMax=numArr[0].length;
				
				//判断是否超出游戏边界
				if(row<0||column<0||row>rowMax-1||column>columnMax-1){
					return
				}
				let curPos=numArr[row][column];
				
				//isOpen表示格子是否已经点开
				if(curPos.isOpen){
					return
				}
				curPos.isOpen=true;
				
				//gameStep用来判断格子是否全部被点击
				gameStep--;
				this.gameStep=gameStep;
				this.$set(this.numArr,row,numArr[row]);
				
				//如果该格子有数字,return
				if(curPos.value){
					return
				}
				
				//剩下的便是格子是空白的情况,向四周进行延伸
				this._leftClick(row-1,column);
				this._leftClick(row+1,column);
				this._leftClick(row,column-1);
				this._leftClick(row,column+1);
			}

4、处理游戏结束事件

游戏结束有两种情况:

1、点开+标记=== 所有格子;
2、标记数=== 雷的数量

第一种情况,我是使用了一个变量gameStep,每次gameStep的初始值等于行数(row) * 列数(column),每次打上标记或者点开格子,gameStep减1。只要判断gameStep是否小于等于0,就可以判断游戏是否结束;

			//判断是否将格子全部点击完
			_isAllClick(){
				let {gameStep}=this;
				if(gameStep>0){
					return
				}
				this._endGame()
			},

第二种情况,同样是使用了一个变量leftMineNum,每次右键时判断该变量是否小于等于0

				//如果已经标记完毕,立刻开始结算
				if(leftMineNum<=0){
					this._endGame();
					return 
				}

通关游戏只需要判断所有格子中雷是否已经全部被打上标记(也就是value==9&&isMark)

			//进行游戏结算
			_endGame(){
				let {numArr}=this;
				let isAllMark=numArr.every(rowItem=>{
					return rowItem.every(columnItem=>{
						//如果value==9必须isMark==true
						return (columnItem.value==9&&columnItem.isMark)||columnItem.value!=9
					})
				})
				if(isAllMark){
					this._passGame()
				}else{
					this._failGame();
				}
			},
			//游戏通关
			_passGame(){
				this.isEnd=true;
				alert('恭喜你,游戏通关!')
			},
			//游戏失败,将显示出来所有的雷的位置,也就是将value==9的格子isOpen设置为true
			_failGame(){
				let {numArr}=this;
				numArr.forEach(rowItem=>{
					rowItem.forEach(columnItem=>{
						let {value}=columnItem;
						if(value==9){
							columnItem.isOpen=true
						}
					})
				})
				this.numArr=numArr;
				this.isEnd=true;
				setTimeout(function(){
					alert('游戏失败!')
				},300)
			}

注意事项

1、因为整个区域是使用二维数组进行构建的,同时又使用了vue进行数据绑定,因此在触发页面重新渲染的操作可能和一维数组不太一样,可以参考这个链接 vue中如何对多维数组进行渲染

2、vue的数据渲染是个异步操作,最后的游戏失败提示,可以使用定时器,将alert的弹出置于渲染数据完成之后;

源码

<!doctype html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport"
	content="width=750, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>Document</title>
	<link rel="stylesheet" type="text/css" href="http://at.alicdn.com/t/font_963134_2tgi4v5uqgb.css">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.21/vue.min.js"></script>

	<style type="text/css">
		*{
			margin:0;
			padding:0;

		}
		.input-container{
			margin:10px 0;
		}
		.btn{
			width:60px;
			line-height: 30px;
			background: #3388ff;
			color:#fff;
			border:0 none;
			margin:0 20px;
		}
		.type-item{
			margin-bottom:10px;
			display: flex;
			align-items:center;
			justify-content: center;
		}
		.type-item .item{
			width:180px;
		}
		.left-mine{
			text-align: center;
		}
		.wrapper{
			display: flex;
			justify-content: center;
		}
		.content{
			margin-top:10px;
			padding:4px;
			border-radius: 4px;
			background: lightblue;
		}
		.content .line{
			margin:4px 0;
			display: flex;
			justify-content: center;
		}
		.content .line .item{
			margin:0 2px;
			width:20px;
			height:20px;
			border-radius:2px;
			background: gray;
			box-shadow: 0 0 4px 1px rgba(255,255,255,0.3);
			cursor:pointer;
			text-align: center;
			font-size:12px;
			color:#fff;
			line-height: 20px;
			transition: all 0.5s;
			position:relative;
			overflow: hidden;
		}
		.content .line .item .block{
			width:100%;
			height:100%;
			display: block;
			position:absolute;
			top:0;
			left:0;
			text-align: center;
		}
		.content .line .item.active{
			background: black;
		}
		.content .line .item.danger{
			background: #fff;
			color:red;
		}
		.content .line .item .mark{
			font-size:30px;
		}
	</style>
</head>
<body>
	<div id="container" oncontextmenu="return false">
		<div class="type" v-cloak>
			<div class="type-item" v-for="(item,index) in typeArr" :key="index">
				<div class="item">难度{{index+1}}{{item.row}}&times;{{item.column}},{{item.mine}}个雷</div>
				<button class="btn" @click="_initArr(index)">选择</button>
			</div>
		</div>
		<div class="left-mine">
			<span>剩余雷数:</span>
			<span>{{leftMineNum}}</span>
		</div>
		<div class="wrapper">
			<div class="content" v-show="numArr.length">
			<div class="line" v-for="(rowItem,rowIndex) in numArr" :key="rowIndex">
				<div 
				class="item"
				v-for="(columnItem,columnIndex) in rowItem" 
				:key="columnIndex"
				@mouseDown="_searchMine($event,rowIndex,columnIndex)"
				:class="{active:columnItem.isOpen&&columnItem.value!='9',danger:columnItem.isOpen&&columnItem.value=='9'}"
				>	
					<div v-if="columnItem.isOpen" class="block">
						<span class="iconfont icon-dilei" v-if='columnItem.value==9'></span>
						<span v-if='columnItem.value!=9'>{{columnItem.value}}</span>
					</div>
					<div v-if="columnItem.isMark" class="block">
						<span class="iconfont icon-qizhi"></span>
						<span>&times;</span>
					</div>
				</div>
			</div>
		</div>
		</div>
		
	</div>
</body>

<script type="text/javascript">
	let app=new Vue({
		el:'#container',
		data:{
			typeArr:[{
				row:9,
				column:9,
				mine:10
			},{
				row:16,
				column:16,
				mine:40
			},{
				row:16,
				column:40,
				mine:99
			}
			],
			curType:{
				row:0,
				column:0,
				mine:0
			},
			numArr:[],
			leftMineNum:0,
			gameStep:0,
			isEnd:true
		},
		methods:{
			_initArr(index){
				this.curType=this.typeArr[index];
				let {row,column,mine}=this.curType;
				this.leftMineNum=mine;
				this.gameStep=row*column;
				this.isEnd=false;
				this._createMineArea();
			},
			_createMineArea(){
				this._putMineInArea();
				this._getEveryMineNum();
			},
			//将雷放入区域
			_putMineInArea(){
				let {curType}=this;
				let {row,column,mine}=curType;
				let numArr=new Array(row);
				for(let i=0;i<row;i++){
					numArr[i]=new Array(column)
				}
				let putedMineNum=0;
				while(putedMineNum<mine){
					let putRow=Math.floor(Math.random()*row);
					let putColumn=Math.floor(Math.random()*column);
					let putPosition=numArr[putRow][putColumn];
					if(!putPosition){
						numArr[putRow][putColumn]={
							value:9,
							isMark:false,
							isOpen:false
						};
						putedMineNum++;
					}
				}
				this.numArr=numArr;
			},
			//获取每一个格子附近的雷数
			_getEveryMineNum(){
				let {numArr}=this;
				let rowLength=numArr.length;
				let columnLength=numArr[0].length;
				for(let row=0;row<rowLength;row++){
					for(let column=0;column<columnLength;column++){
						let curContent=numArr[row][column];
						if(curContent&&curContent.value===9){
							continue;
						}
						let posArr=[[row-1,column],[row-1,column-1],[row-1,column+1],[row,column-1],[row,column+1],[row+1,column+1],[row+1,column-1],[row+1,column]];
						let mineNum=0;
						for(let i=0;i<8;i++){
							let _row=posArr[i][0];
							let _column=posArr[i][1];
							if(_row<0||_column<0||_row>numArr.length-1||_column>numArr[0].length){
								continue
							}
							let curPos=numArr[_row][_column];
							if(curPos&&curPos.value==9){
								mineNum++
							}
						}
						numArr[row][column]={
							value:mineNum||'',
							isMark:false,
							isOpen:false
						}

						this.$set(this.numArr,row,numArr[row])
					}
				}
				console.log(numArr);
			},
			_searchMine(e,row,column){
				let {numArr,isEnd}=this;
				if(isEnd){
					return
				}
				let curPos=numArr[row][column];
				//点击鼠标右键
				let {which}=e;
				if(which==3){
					//点击右键
					this._rightClick(row,column);
					return 
				}
				//已被标记,不可左击
				if(curPos.isMark){
					return
				}
				if(curPos.value==9){
					//点击到雷,游戏结束
					this._failGame();
					return
				}
				//点击左键
				this._leftClick(row,column);
				this._isAllClick();
			},
			//点击左键
			_leftClick(row,column){
				let {numArr,gameStep}=this;
				let rowMax=numArr.length;
				let columnMax=numArr[0].length;
				//判断是否超出游戏边界
				if(row<0||column<0||row>rowMax-1||column>columnMax-1){
					return
				}
				let curPos=numArr[row][column];
				//isOpen表示格子是否已经点开
				if(curPos.isOpen){
					return
				}
				curPos.isOpen=true;
				//gameStep用来判断格子是否全部被点击
				gameStep--;
				this.gameStep=gameStep;
				this.$set(this.numArr,row,numArr[row]);
				//如果该格子有数字,return
				if(curPos.value){
					return
				}
				//剩下的便是格子是空白的情况,向四周进行延伸
				this._leftClick(row-1,column);
				this._leftClick(row+1,column);
				this._leftClick(row,column-1);
				this._leftClick(row,column+1);
			},
			//点击右键
			_rightClick(row,column){
				let {numArr,leftMineNum,gameStep}=this;
				let curBlock=numArr[row][column];
				let {isMark}=curBlock;
				curBlock.isMark=!isMark;
				leftMineNum=leftMineNum+(isMark?1:-1);
				gameStep=gameStep+(isMark?1:-1);
				
				this.numArr=numArr;
				this.leftMineNum=leftMineNum;
				this.gameStep=gameStep;
				//如果已经标记完毕,立刻开始结算
				if(leftMineNum<=0){
					this._endGame();
					return 
				}
			},
			//判断是否将格子全部点击完
			_isAllClick(){
				let {gameStep}=this;
				if(gameStep>0){
					return
				}
				this._endGame()
			},
			//进行游戏结算
			_endGame(){
				let {numArr}=this;
				let isAllMark=numArr.every(rowItem=>{
					return rowItem.every(columnItem=>{
						return (columnItem.value==9&&columnItem.isMark)||columnItem.value!=9
					})
				})
				if(isAllMark){
					this._passGame()
				}else{
					this._failGame();
				}
			},
			//游戏通关
			_passGame(){
				this.isEnd=true;
				alert('恭喜你,游戏通关!')
			},
			//结束游戏
			_failGame(){
				let {numArr}=this;
				numArr.forEach(rowItem=>{
					rowItem.forEach(columnItem=>{
						let {value,isOpen,isMark}=columnItem;
						if(value==9){
							columnItem.isOpen=true
						}
					})
				})
				this.numArr=numArr;
				this.isEnd=true;
				setTimeout(function(){
					alert('游戏失败!')
				},300)
			}
		}
	})
</script>
</html>

标签