之前雖然寫過走迷宮,不少人反映沒找到代碼不會部署,沒看明白原理,此次把更詳細寫出優化並將代碼放到github,趁着520能夠本身放一些圖片獻給女神!javascript
原由
先看效果圖(文末有動態圖)(在線電腦嘗試地址http://biggsai.com/maze.html):html
項目github地址:https://github.com/javasmall/mazegame:前端
做爲程序猿,經常由於身務繁忙,常常忙於coding不多閒暇來顧及女友,也常被吐槽爲渣男。java
可是,渣着渣着,卻發現520到了。臥槽,520?!git
咱們經常後悔本身衝動時候說過的話,我準備個毛線,啥都沒準備,但好面子的我也不可能隨隨便便欺騙人家女孩子(雖然已經騙了😭),程序員
習慣性打開淘寶,逛着看看有沒有啥合適的,一想今天已經19號了,臥槽要涼了難道?github
一想還有美團補救一下能夠送的到,打開美團發現都是吃喝玩,完蛋,真的要涼了。算法
深夜,我在懺悔本身說過的大話,沒本事不能瞎說啊,一夜啥也幹不了啥哇。編程
等等,還有一夜耶!canvas
此時我忽然眼前一亮,對於程序猿,一夜也能製造屬於他的浪漫!
翻開各類搜索引擎、博客搜素一番:程序員520表白神器 代碼 果真不出我所料!有東西,哈哈哈ctrl+c
,ctrl+v
我可太會了,浪漫要來了!
找了幾個代碼,發現哇塞好牛哇,還能跑起來,可是發現好多雷同我也找不到原做者是誰,但我心中很是崇拜做者的才華(救命之恩)。
這時個人頭腦又浮現她時長對我說的一句話:"一點都不上心"!這些可能確實都已經很老套和小兒科了,不行,我要以我本身的方式來!我要有點我本身的特點。
我琢磨着怎麼用熟悉的數據結構與算法搞個有趣的東西。
追憶着我知道的數據結構與算法:鏈表、隊列、棧、圖、dfs、bfs、並查集、Dijkstra、Prime……
之前隱隱約約好像記得有人用Java Swing配合並查集算法畫迷宮,我是否能夠寫一個走迷宮的小遊戲呢?但我搜了一下並未發現有HTML、JavaScript版本的,但Swing那玩意除了特定場景基本沒人用我要整一個不同的,雖然JavaScript和HTML我不太熟,但我應該能夠,加油💪!
分析
我須要從0到1的設計實現一個走迷宮遊戲,但這裏面必定是困難阻阻,不過但凡問題均可以先拆開分步,而後逐個擊破合併。
對於一個走迷宮的小遊戲,我想了一下可能須要掌握如下的知識:
- 搞懂一個初始位置至結束位置有到達路徑的迷宮生成算法
- 用JavaScript在canvas上畫棋盤(迷宮初始狀態)
- 利用迷宮生成算法生成一個迷宮(擦除須要擦掉的線)
- 利用JavaScript、事件監聽和canvas畫圖實現方塊移動(要考慮不出界、不穿牆)
琢磨的過程大概是:
- 畫單條線—>畫多個線—>擦線(造成迷宮)—>方塊移動、移動約束(不出界不穿牆)—>完成遊戲。
畫線(棋盤)
對於html+js(canvas)畫的東西之前沒接觸過,但以前學過Java Swing寫過五子棋遊戲、翻卡牌遊戲和這個有點相似,在html中有個canvas 的畫布,能夠在上面畫一些東西和聲明一些監聽(鍵盤監聽)。
對於這種canvas、Java Swing、QT等畫圖庫,若是你使用它進行畫圖,要清楚你畫上去的東西其實就是一條條線,沒有實際的屬性,你只不過須要在編程語言中根據數據結構與算法去操做畫布,讓canvas畫布的內容是對你寫的數據結構或算法是一個正確合理的展示。
因此對於迷宮來講,一個個線條線條是沒有屬性的,只有位置x,y
,你操做這個畫布時候,可能和咱們習慣的面相對象思惟不同,設計的線或者點的時候,要可以經過計算推理這些點、線在什麼位置,在後續畫線、擦線、移動方格的時候根據這個位置進行操做讓整個迷宮在視覺效果上是完整統一的。
對於這個畫棋盤的步驟也很簡單,首先嚐試畫一條線,事先想好迷宮每一個方格格的大小和方格總個數,以後按照起始(左上)座標順序畫水平方向、豎直方向的線(平行線之間起末點位置上有規律的),最終就能夠實現一個視覺上的迷宮。
<!DOCTYPE html> <html> <head> <title>MyHtml.html</title> </head> <body> <canvas id="mycanvas" width="600px" height="600px"></canvas> </body> <script type="text/javascript"> var chessboradsize=14;//棋盤大小 var chess = document.getElementById("mycanvas"); var context = chess.getContext('2d'); function drawChessBoard(){//繪畫 for(var i=0;i<chessboradsize+1;i++){ context.strokeStyle='gray';//可選區域 context.moveTo(15+i*30,15);//垂直方向畫15根線,相距30px; context.lineTo(15+i*30,15+30*chessboradsize); context.stroke(); context.moveTo(15,15+i*30);//水平方向畫15根線,相距30px;棋盤爲14*14; context.lineTo(15+30*chessboradsize,15+i*30); context.stroke(); } } drawChessBoard();//繪製棋盤 </script> </html>
實現效果
畫迷宮
隨機迷宮怎麼生成?怎麼搞?又陷入一臉懵逼。
由於咱們想要迷宮,那麼就須要這個迷宮出口和入口有連通路徑,不研究的話很難知道用什麼算法生成這個迷宮。這時耳角傳來熟悉的聲音:用並查集(不相交集合)。
迷宮和不相交集合有什麼聯繫呢?(規則)
以前筆者在前面數據結構與算法系列中曾經介紹過並查集(不相交集合),它的主要功能是森林的合併、查找:不聯通的經過並查集可以快速將兩個森林合併,而且可以快速查詢兩個節點是否在同一個森林(集合)中!
而隨機迷宮生成正也利用了這個思想:在每一個方格都不聯通的狀況下,是一個棋盤方格,每一個迷宮格子相對獨立,這是它的初始狀態;後面生成可能須要若干相鄰節點進行聯通(合併爲一個集合),且這個節點能夠跟鄰居可能相連,也可能不相連。咱們能夠經過並查集實現其底層數據結構的支撐。
在這裏面,咱們把每一個格子當成一個個集合元素,而每一個集合與周圍的牆則是證實其是否直接聯通,咱們就是要經過聯通一部分方格(擦掉部分牆)實現整個隨機迷宮。
具體思路爲:(主要理解並查集)
1:定義好不想交集合的基本類和方法(search,union
等) 2:數組初始化,每個數組元素都是一個集合,值爲-1 3:隨機查找一個格子(一維數據要轉換成二維,有點麻煩),在隨機找一面牆(也就是找這個格子的上下左右),還要判斷找的格子出沒出界是否爲一個合法的格子。
- 具體生成一個隨機數m(小於迷宮總格子數)
- 將一維隨機數m轉成在迷宮橫縱二維位置位置p,具體爲:
[m/長,m%長]
這裏的長表示迷宮的行數或者列數。 - 在位置p的上下左右隨機找一個位置q
[m/長+1,m%長]
或[m/長-1,m%長]
或[m/長,m%長+1]
或[m/長,m%長-1]
- 判斷是否越界,若是越界從新查找,不然進行下一步。
4:判斷兩個格子p和q(這時候要將二維座標轉成其一維數組編號)是否在一個集合(並查集查找)。若是在,則返回第三步從新找,若是不在,那麼把牆挖去。 5:把牆挖去(合併)有點繁瑣,就算兩個方格不連通,須要再經過位置判斷它那種牆(上下隔離仍是左右隔離),而後再經過計算精肯定位到這個牆起點末點位置而後擦掉(須要考慮至關多的細節)。 6:重複上面工做,直到第一個(1,1)和(n,n)聯通中止獲得一個完整的迷宮。雖然採用隨機數找方格找牆,可是這個數據量效率和結果都仍是挺不錯的。
在其中要搞清一維二維數組的關係。一維是真實數據,進行並查集查找、合併操做。轉化爲二維更可能是爲了查找位置。要搞懂轉化!
注意:避免混淆,搞清數組的地址和邏輯矩陣位置。數組從0開始的,邏輯上你本身判斷,別搞混淆!
你可能會問,這個算法爲何最終能生成一個起始末尾聯通迷宮,由於咱們的終止就是以它爲條件,不連通的話就會讓迷宮內隨機聯通一個本不聯通的迷宮,而這種可能性是頗有限的,因此到不了最壞狀況就能知足迷宮聯通,而後又是隨機找點,可讓迷宮看起來更勻稱一些。
主要邏輯爲:
while(search(0)!=search(aa*aa-1))//主要思路 { var num = parseInt(Math.random() * aa*aa );//產生一個小於196的隨機數 var neihbour=getnei(num); if(search(num)==search(neihbour)){continue;} else//不在一個上 { isling[num][neihbour]=1;isling[neihbour][num]=1; drawline(num,neihbour);//劃線 union(num,neihbour); } }
那麼在前面的代碼爲
<!DOCTYPE html> <html> <head> <title>MyHtml.html</title> </head> <body> <canvas id="mycanvas" width="600px" height="600px"></canvas> </body> <script type="text/javascript"> var chessboradSize=14; var chess = document.getElementById("mycanvas"); var context = chess.getContext('2d'); var tree = [];//存放是否聯通 var isling=[];//判斷是否相連 for(var i=0;i<chessboradSize;i++){ tree[i]=[]; for(var j=0;j<chessboradSize;j++){ tree[i][j]=-1;//初始值爲0 } } for(var i=0;i<chessboradSize*chessboradSize;i++){ isling[i]=[]; for(var j=0;j<chessboradSize*chessboradSize;j++){ isling[i][j]=-1;//初始值爲0 } } function drawChessBoard(){//繪畫 for(var i=0;i<chessboradSize+1;i++){ context.strokeStyle='gray';//可選區域 context.moveTo(15+i*30,15);//垂直方向畫15根線,相距30px; context.lineTo(15+i*30,15+30*chessboradSize); context.stroke(); context.moveTo(15,15+i*30);//水平方向畫15根線,相距30px;棋盤爲14*14; context.lineTo(15+30*chessboradSize,15+i*30); context.stroke(); } } drawChessBoard();//繪製棋盤 function getnei(a)//得到鄰居號 random { var x=parseInt(a/chessboradSize);//要精確成整數 var y=a%chessboradSize; var mynei=new Array();//儲存鄰居 if(x-1>=0){mynei.push((x-1)*chessboradSize+y);}//上節點 if(x+1<14){mynei.push((x+1)*chessboradSize+y);}//下節點 if(y+1<14){mynei.push(x*chessboradSize+y+1);}//有節點 if(y-1>=0){mynei.push(x*chessboradSize+y-1);}//下節點 var ran=parseInt(Math.random() * mynei.length ); return mynei[ran]; } function search(a)//找到根節點 { if(tree[parseInt(a/chessboradSize)][a%chessboradSize]>0)//說明是子節點 { return search(tree[parseInt(a/chessboradSize)][a%chessboradSize]);//不能壓縮路徑路徑壓縮 } else return a; } function value(a)//找到樹的大小 { if(tree[parseInt(a/chessboradSize)][a%chessboradSize]>0)//說明是子節點 { return tree[parseInt(a/chessboradSize)][a%chessboradSize]=value(tree[parseInt(a/chessboradSize)][a%chessboradSize]);//不能路徑壓縮 } else return -tree[parseInt(a/chessboradSize)][a%chessboradSize]; } function union(a,b)//合併 { var a1=search(a);//a根 var b1=search(b);//b根 if(a1==b1){} else { if(tree[parseInt(a1/chessboradSize)][a1%chessboradSize]<tree[parseInt(b1/chessboradSize)][b1%chessboradSize])//這個是負數(),爲了簡單減小計算,不在調用value函數 { tree[parseInt(a1/chessboradSize)][a1%chessboradSize]+=tree[parseInt(b1/chessboradSize)][b1%chessboradSize];//個數相加 注意是負數相加 tree[parseInt(b1/chessboradSize)][b1%chessboradSize]=a1; //b樹成爲a樹的子樹,b的根b1直接指向a; } else { tree[parseInt(b1/chessboradSize)][b1%chessboradSize]+=tree[parseInt(a1/chessboradSize)][a1%chessboradSize]; tree[parseInt(a1/chessboradSize)][a1%chessboradSize]=b1;//a所在樹成爲b所在樹的子樹 } } } function drawline(a,b)//劃線,要判斷是上下仍是左右 { var x1=parseInt(a/chessboradSize); var y1=a%chessboradSize; var x2=parseInt(b/chessboradSize); var y2=b%chessboradSize; var x3=(x1+x2)/2; var y3=(y1+y2)/2; if(x1-x2==1||x1-x2==-1)//左右方向的點 須要上下劃線 { context.strokeStyle = 'white'; context.clearRect(29+x3*30, y3*30+16,2,28); } else { context.strokeStyle = 'white'; context.clearRect(x3*30+16, 29+y3*30,28,2); } } while(search(0)!=search(chessboradSize*chessboradSize-1))//主要思路 { var num = parseInt(Math.random() * chessboradSize*chessboradSize );//產生一個小於196的隨機數 var neihbour=getnei(num); if(search(num)==search(neihbour)){continue;} else//不在一個上 { isling[num][neihbour]=1;isling[neihbour][num]=1; drawline(num,neihbour);//劃線 union(num,neihbour); } } </script> </html>
這樣,離勝利又進了一步,實現效果:
方塊移動
這部分我採用的方法不是動態真的移動(關鍵我不會哈哈),而是一格一格的跳躍,也就是當走到下一個格子將當前格子的方塊擦掉,在移動的那個格子中再畫一個方塊,選擇方塊是由於方塊更方便擦除繪畫計算,能夠根據像素大小精準擦除。固然熟悉JavaScript的能夠弄個小人進去玩玩。
另外,在移動中要注意不能隔空穿牆、越界。那麼怎麼判斷呢?很好辦,移動前目標方格,咱們判斷其是否直接聯通,注意是直接聯通而不是聯通(極可能繞一圈聯通但不能直接越過去,因此這裏並查集不能壓縮路徑哦),若是直接不連通,那麼不進行操做,不然進行方塊移動。
另外,事件的監聽上下左右本身百度查一查就能夠獲得,添加按鈕對一些事件監聽,完成整個遊戲這些不是最主要的。
爲了豐富遊戲可玩性,將方法封裝,能夠設置關卡(只需改變迷宮大小),這樣就能夠實現通關了。
結語
在線嘗試地址 http://biggsai.com/maze.html,代碼能夠到githubhttps://github.com/javasmall/mazegame:上下載或到bigsai
公衆號回覆: ** 迷宮** 便可得到! 下載項目以後修改圖片和本身想說的話,放到本身服務器上,就能夠給女神看了。
避免吃狗糧,動態圖圖片換了一下(排行榜功能被閹割了):
筆者前端能力和算法能力有限,寫的可能不是特別好,還請見諒!固然,筆者歡迎和一塊兒熱愛學習的人共同進步、學習!歡迎關注筆者公衆號:bigsai,歡迎關注、點贊!蟹蟹!