1. 程式人生 > >掃雷小遊戲(前端)原始碼及核心演算法講解

掃雷小遊戲(前端)原始碼及核心演算法講解

感想:寫掃雷遊戲的主要原因是因為這段時間剛好迷上了掃雷,便有了寫出這個遊戲的想法。

   寫程式碼的過程中,我覺得比較重要的演算法有兩部分: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的話,可以在下面留言,或者上面的演算法有錯誤,或者有更好的演算法,想法都可以在下面留言。