扫雷小游戏(前端)源码及核心算法讲解

感想:写扫雷游戏的主要原因是因为这段时间刚好迷上了扫雷,便有了写出这个游戏的想法。

   写代码的过程中,我觉得比较重要的算法有两部分:1、初始化游戏时,若该格子不是雷,那么格子中的数字怎么计算得来       

2、游戏时,点到空白的格子,怎么将空白格子所在的空白区域翻开(空白格子周围又有空白格子)

   一、样式   

 <style>
	body{
		background:url(图片地址);
		background-size:cover;
	}
	#canvas{
		display:block;
		box-shadow:0px 0px 10px;
		border-radius:5px;
		border:10px solid #111111;
		margin:0px auto;
		background-color:#ffffff;
	}
	button{display:block;margin:10px auto;border-radius:5px;border:3px solid;background-color:#ffffff}
	p{display:block;margin:10px auto;text-align:center;}
  </style>

二、

<body>
	<canvas id="canvas" height="899" width="450"></canvas>
	
	<div style="position: fixed;top:100px; left: 5%; right: auto;  bottom: auto; " >
		<p id="p"></p>
		<button id="fanpai">翻牌</button>
		<button id="chaqi">插旗</button>
		<button id="newGame">重新开始</button>
		<select id="model">
			<option value="0" selected="selected">简单模式</option>
			<option value="1">一般模式</option>
			<option value="2">困难模式</option>
			<option value="3">地狱模式</option>
		</select>
		<select id="colorCanvas">
			<option value="#11b1ff" selected="selected">蓝色</option>
			<option value="#994d00">棕色</option>
			<option value="#37a483">绿蓝</option>
			<option value="#506aa0">灰蓝</option>
		</select>
	</div>

三、一些变量的定义以及元素的获得

//画布
	var canvas=document.getElementById("canvas");
	var context=canvas.getContext("2d");
	//翻牌按钮
	var fanpaiBtn=document.getElementById("fanpai");
	//插旗按钮
	var chaqiBtn=document.getElementById("chaqi");
	//雷的数目提醒段
	var Pcount=document.getElementById("p");
	//重新开始按钮
	var newGameBtn=document.getElementById("newGame");
	//模式选择
	var model=document.getElementById("model");
	//背景颜色
	var colorCanvas=document.getElementById("colorCanvas");
	//格子的颜色
	var geZiColor="#11b1ff";

	var flag=true;//true-翻牌按钮(默认) false-插旗按钮
	var Lcount=30;//雷的数目
	var Fcount=0;//旗子的数目
	var Tu=[];//Tu[i][j][0]存储数字和炸弹(-1)Tu[i][j][1]存储是否可以翻牌(0->还没翻,1已经翻了)Tu[i][j][2]-是否有插旗,0是没插旗1插旗
	for(var i=0;i<30;i++){
		Tu[i]=[];
		for(var j=0;j<15;j++){
			Tu[i][j]=[];
			for(var k=0;k<3;k++){
				Tu[i][j][k]=0;
			}
		}	
	}

四、图的初始化操作(包含初始化雷的算法)

	//初始化图
	function initTu(){		
		flag=true;//默认翻牌按钮
		if(flag){
			fanpaiBtn.style.borderColor="#cc0000";
			chaqiBtn.style.borderColor="#000000";
		}
		p.innerHTML="剩余雷的数目:"+Lcount;
		for(var i=0;i<30;i++){
			for(var j=0;j<15;j++){
				for(var k=0;k<3;k++){
					Tu[i][j][k]=0;
				}
			}	
		}
		fanpaiBtn.style.borderColor="#cc0000";
		//画布加颜色
		context.fillStyle=geZiColor;	
		for(var i=0;i<30;i++){
			for(var j=0;j<15;j++){
				context.fillRect(j*30+2,i*30+1,27,27);
			}
		}
		//画线
		context.strokeStyle="#000000";
		context.beginPath();
		for(var i=0;i<15;i++){
			//竖线
			context.moveTo(30+i*30,0);
			context.lineTo(30+i*30,900)
			context.stroke();
			//横线	
			context.moveTo(0,30+i*30);
			context.lineTo(450,30+i*30)
			context.stroke();
			context.moveTo(0,30*15+i*30);
			context.lineTo(450,30*15+i*30)
			context.stroke();		
		}
		context.closePath();
		//生成Lcount个雷
		for(var i=0;i<Lcount;i++){
			var x=Math.floor(Math.random()*15);
			var y=Math.floor(Math.random()*30);
			if(Tu[y][x][0]!=-1){//如果位置上已经是雷了,就不放了
				Tu[y][x][0]=-1;
				//计算数字
				if(y-1>=0&&x-1>=0){
					//不是雷 是数字就+1
					if(Tu[y-1][x-1][0]!=-1)Tu[y-1][x-1][0]++;
				}
				if(y-1>=0){
					if(Tu[y-1][x][0]!=-1)Tu[y-1][x][0]++;
				}
				if(y-1>=0&&x+1<15){
					if(Tu[y-1][x+1][0]!=-1)Tu[y-1][x+1][0]++;
				}
				if(x+1<15){
					if(Tu[y][x+1][0]!=-1)Tu[y][x+1][0]++;
				}
				if(y+1<30&&x+1<15){
					if(Tu[y+1][x+1][0]!=-1)Tu[y+1][x+1][0]++;
				}
				if(y+1<30){
					if(Tu[y+1][x][0]!=-1)Tu[y+1][x][0]++;
				}
				if(y+1<30&&x-1>=0){
					if(Tu[y+1][x-1][0]!=-1)Tu[y+1][x-1][0]++;
				}
				if(x-1>=0){
					if(Tu[y][x-1][0]!=-1)Tu[y][x-1][0]++;
				}
			}else{
				i--;//不是雷时,这次循环没有得到雷,无效,i--
			}
		}
	}

初始化时画布加颜色可以一次性画一整个画布,不使用循环,我为了使得格子之间有间隙(看起来比较好看)就使用了循环。

生成雷的算法思想:

    格子上的数字是周围八个位置雷的个数,也就是说雷的存在影响了数字的大小,反过来想,不以数字为中心点,以雷为中心点,则周围的八个位置上会因为中心点的雷的影响,而数字加1。所以该算法主要是在生成的雷时候,也使雷的周围的格子(非雷)的数字加1。

五、点到空白格子时的处理代码

//点击到空白时的处理函数
	function blank(i,j){
		//翻开当前的空白格子
		context.fillStyle="#ffffff";//面白色
		context.fillRect(30*j+1,30*i+1,28,28);
		Tu[i][j][1]=1;//标记被翻了
		Iswin();
		if(i-1>=0){		
			if(Tu[i-1][j][1]==0){//没有被翻过
				if(Tu[i-1][j][0]==0){//空白
					blank(i-1,j);
				}else{//没有被翻过但不是空白->翻开(因为空白的四周没有炸弹所以不需要考虑是否是炸弹而直接翻开)
					context.fillStyle="#ffffff";//面白色
					context.fillRect(30*j+1,30*(i-1)+1,28,28);

					context.fillStyle="#000000";
					context.font="20px Arial";
					context.fillText(Tu[i-1][j][0],8+30*j,23+30*(i-1));
					Tu[i-1][j][1]=1;//标记已经被翻开
					Iswin();
				}				
			}
		}
		if(i+1<30){
			if(Tu[i+1][j][1]==0){//没有被翻过
				if(Tu[i+1][j][0]==0){//空白
					blank(i+1,j);
				}else{//没有被翻过但是不是空白->翻开(因为空白的四周没有炸弹所以不需要考虑是否是炸弹而直接翻开)
					context.fillStyle="#ffffff";//面白色
					context.fillRect(30*j+1,30*(i+1)+1,28,28);

					context.fillStyle="#000000";
					context.font="20px Arial";
					context.fillText(Tu[i+1][j][0],8+30*j,23+30*(i+1));
					Tu[i+1][j][1]=1;//标记已经被翻开
					Iswin();
				}				
			}
		}
		if(j-1>=0){
			if(Tu[i][j-1][1]==0){//没有被翻过
				if(Tu[i][j-1][0]==0){//空白
					blank(i,j-1);
				}else{//没有被翻过但是不是空白(因为空白的四周没有炸弹所以不需要考虑是否是炸弹而直接翻开)
					context.fillStyle="#ffffff";//面白色
					context.fillRect(30*(j-1)+1,30*i+1,28,28);

					context.fillStyle="#000000";
					context.font="20px Arial";
					context.fillText(Tu[i][j-1][0],8+30*(j-1),23+30*i);
					Tu[i][j-1][1]=1;//标记已经被翻开
					Iswin();
				}				
			}
		}
		if(j+1<15){
			if(Tu[i][j+1][1]==0){//没有被翻过
				if(Tu[i][j+1][0]==0){//空白
					blank(i,j+1);
				}else{//没有被翻过但是不是空白(因为空白的四周没有炸弹所以不需要考虑是否是炸弹而直接翻开)
					context.fillStyle="#ffffff";//面白色
					context.fillRect(30*(j+1)+1,30*i+1,28,28);

					context.fillStyle="#000000";
					context.font="20px Arial";
					context.fillText(Tu[i][j+1][0],8+30*(j+1),23+30*i);
					Tu[i][j+1][1]=1;//标记已经被翻开
					Iswin();
				}				
			}	
		}
		if(j+1<15&&i+1<30){
			if(Tu[i+1][j+1][1]==0){//没有被翻过
				if(Tu[i+1][j+1][0]==0){//空白
					blank(i+1,j+1);
				}else{//没有被翻过但是不是空白(因为空白的四周没有炸弹所以不需要考虑是否是炸弹而直接翻开)
					context.fillStyle="#ffffff";//面白色
					context.fillRect(30*(j+1)+1,30*(i+1)+1,28,28);

					context.fillStyle="#000000";
					context.font="20px Arial";
					context.fillText(Tu[i+1][j+1][0],8+30*(j+1),23+30*(i+1));
					Tu[i+1][j+1][1]=1;//标记已经被翻开
					Iswin();
				}				
			}	
		}
		if(j+1<15&&i-1>=0){
			if(Tu[i-1][j+1][1]==0){//没有被翻过
				if(Tu[i-1][j+1][0]==0){//空白
					blank(i-1,j+1);
				}else{//没有被翻过但是不是空白(因为空白的四周没有炸弹所以不需要考虑是否是炸弹而直接翻开)
					context.fillStyle="#ffffff";//面白色
					context.fillRect(30*(j+1)+1,30*(i-1)+1,28,28);

					context.fillStyle="#000000";
					context.font="20px Arial";
					context.fillText(Tu[i-1][j+1][0],8+30*(j+1),23+30*(i-1));
					Tu[i-1][j+1][1]=1;//标记已经被翻开
					Iswin();
				}				
			}	
		}
		if(j-1>=0&&i-1>=0){
			if(Tu[i-1][j-1][1]==0){//没有被翻过
				if(Tu[i-1][j-1][0]==0){//空白
					blank(i-1,j-1);
				}else{//没有被翻过但是不是空白(因为空白的四周没有炸弹所以不需要考虑是否是炸弹而直接翻开)
					context.fillStyle="#ffffff";//面白色
					context.fillRect(30*(j-1)+1,30*(i-1)+1,28,28);

					context.fillStyle="#000000";
					context.font="20px Arial";
					context.fillText(Tu[i-1][j-1][0],8+30*(j-1),23+30*(i-1));
					Tu[i-1][j-1][1]=1;//标记已经被翻开
					Iswin();
				}				
			}	
		}
		if(j-1>=0&&i+1<30){
			if(Tu[i+1][j-1][1]==0){//没有被翻过
				if(Tu[i+1][j-1][0]==0){//空白
					blank(i+1,j-1);
				}else{//没有被翻过但是不是空白(因为空白的四周没有炸弹所以不需要考虑是否是炸弹而直接翻开)
					context.fillStyle="#ffffff";//面白色
					context.fillRect(30*(j-1)+1,30*(i+1)+1,28,28);

					context.fillStyle="#000000";
					context.font="20px Arial";
					context.fillText(Tu[i+1][j-1][0],8+30*(j-1),23+30*(i+1));
					Tu[i+1][j-1][1]=1;//标记已经被翻开
					Iswin();
				}				
			}	
		}
	}

该算法的主要思想:

       点到空白格子时,将其周围的8个格子都翻开(空白格子的周围八个位置都没有雷,所以不用担心翻到雷),若8个格子中又有空白格子,就以新出现的格子为中心,翻开其周围的8个格子(已经翻开了的就不再翻开)【递归思想】。

        以上代码中8个if就是格子周围的八个方向。内层if判断该方向上的格子是不是空白,是就以它为中心继续翻开(递归)。当8个方向都不是空白时结束递归。

六、其他代码

//画方块(翻牌操作)
	function drawS(i,j){//j-x(横) i-y(纵)
		if(Tu[i][j][1]==0&&Tu[i][j][2]==0){//没翻过且没插旗,才可以翻牌
			if(Tu[i][j][0]==0){//空白
				blank(i,j);
			}else if(Tu[i][j][0]==-1){//炸弹
				context.fillStyle="#000000";
				context.fillRect(j*30+2,i*30+1,27,27);
				Tu[i][j][1]=1;//标记已经翻过牌子了
				alert("点到炸弹,输了");
				gameOver();
			}else{//数字
				//context.fillText("0",8,23);//第0个格子
				//context.fillRect(0,0,30,30);第一个格子
				context.fillStyle="#ffffff";//面白色
				context.fillRect(j*30+2,i*30+1,27,27);

				context.fillStyle="#000000";
				context.font="20px Arial";
				context.fillText(Tu[i][j][0],8+30*j,23+30*i);

				Tu[i][j][1]=1;//标记已经翻过牌子了
				Iswin();
			}

		}
	}
	//画旗子
	function drawQizi(i,j){
		
		//插旗,并将旗子放上去
		//旗面
		context.beginPath();
		context.moveTo(13+30*j,i*30+3);
		context.lineTo(26+30*j,12+30*i);
		context.lineTo(13+30*j,12+30*i);
		context.fillStyle="#ff0000";
		context.closePath();
		context.fill();
		//旗杆
		context.beginPath();
		context.lineWidth=3;
		context.moveTo(13+30*j,i*30+3);
		context.lineTo(13+30*j,i*30+18);
		context.closePath();
		context.stroke();
		//旗台
		context.beginPath();
		context.fillStyle="#ff0000";
		context.fillRect(5+30*j,18+30*i,20,8);
		context.closePath();
	}
	//插旗
	function chaQiAction(i,j){
		//翻过的进行插旗动作-无效(没反应)
		if(Tu[i][j][1]==0){//没翻过
			if(Tu[i][j][2]==0){//没插着旗
				drawQizi(i,j);	
				Tu[i][j][2]=1;//表示插旗了
				Fcount++;
			}else{//插着旗
				//取消插旗,并将旗子去掉	
				context.fillStyle=geZiColor;
				context.fillRect(j*30+2,i*30+1,27,27);
				Tu[i][j][2]=0;
				Fcount--;
			}
		p.innerHTML="剩余雷的数目:"+(Lcount-Fcount);
		}
	}
	//图的点击事件
	canvas.onclick=function(e){
		var x=e.offsetX;//横
		var y=e.offsetY;//纵
	
		var i=Math.floor(y/30);//纵
		var j=Math.floor(x/30);//横
		if(flag){//翻牌操作
			drawS(i,j);	
		}else{//插旗动作
			chaQiAction(i,j);
		}

	}
	//按钮点击事件\
	//插旗
	chaqiBtn.onclick=function(){
		flag=false;
		this.style.borderColor="#cc0000";
		fanpaiBtn.style.borderColor="#000000";
	}
	//翻牌
	fanpaiBtn.onclick=function(){
		flag=true;
		this.style.borderColor="#cc0000";
		chaqiBtn.style.borderColor="#000000";
	}
//赢了的判断函数
	function Iswin(){
		//赢的条件是翻了30*15-Lcount次游戏还没输那就赢了
		var fanle=0;
		for(var i=0;i<30;i++){
			for(var j=0;j<15;j++){
				fanle+=Tu[i][j][1];
			}	
		}
		if(fanle==(30*15-Lcount)){
			alert("赢了!!!扫雷达人");
			gameOver();
		}
	}
	//游戏结束
	function gameOver(){
		for(var i=0;i<30;i++){
			for(var j=0;j<15;j++){
				if(Tu[i][j][0]==-1){//所有的雷显示出来
					context.fillStyle="#000000";
					context.fillRect(30*j,30*i,30,30);
					Tu[i][j][1]=1;//标记已经翻过牌子了
				}
			}	
		}
	
	}
	//重新开始按钮
	newGameBtn.onclick=function(){
		canvas.height=canvas.height;
		initTu();
	}
	//模式选择
	model.onchange=function(){
		if(this.value=="0"){Lcount=30;}
		if(this.value=="1"){Lcount=50;}
		if(this.value=="2"){Lcount=90;}
		if(this.value=="3"){Lcount=100;}
		canvas.height=canvas.height;
		initTu();		
	}
	//格子颜色的改变
	colorCanvas.onchange=function(){
		geZiColor=this.value;
		canvas.height=canvas.height;
		initTu();		
	}
	//加载
	window.onload=function(){
	 initTu();
	}

有发现什么bug的话,可以在下面留言,或者上面的算法有错误,或者有更好的算法,想法都可以在下面留言。

猜你喜欢

转载自blog.csdn.net/s_p_y_s/article/details/80016238
今日推荐