掃雷小遊戲(前端)原始碼及核心演算法講解
阿新 • • 發佈:2019-02-17
感想:寫掃雷遊戲的主要原因是因為這段時間剛好迷上了掃雷,便有了寫出這個遊戲的想法。
寫程式碼的過程中,我覺得比較重要的演算法有兩部分: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的話,可以在下面留言,或者上面的演算法有錯誤,或者有更好的演算法,想法都可以在下面留言。