阿望教你用vue寫掃雷(超詳細哦)
前言
話說阿望還在大學時,某一天寢室突然停網了,於是和室友兩人不約而同地打開了掃雷,比相同難度下誰更快找出全部的雷,玩得不亦樂乎,就這樣,掃雷伴我們度過了斷網的一週,是整整一週啊,不用上課的那種,可想而知我們是有多無聊了。
這兩天臨近過年了,該放假的已經放假了,不該放假的已經請假了,公交不打擠了,地鐵口不堵了,公司也去了大半部分的人了,就留阿望這種不得不留下來值班的人守著空蕩蕩的辦公室了,於是,多年前那種無所事事的斷網心態再次襲來,於是,想玩掃雷的心再次蹦躂出來,於是,點開了電腦的附件,於是,發現我的電腦上並沒有掃雷【手動微笑.jpg】。
怎麼想起要寫掃雷的阿望就不多廢話了,直接開幹。
掃雷遊戲規則
想當年阿望還是在大學才參透掃雷的遊戲規則的,初高中的時候都是靠運氣瞎點,從沒贏過,當然了,境界提升以後,追求的自然就不是贏了,而是速度。
來,看規則:
大白話遊戲規則,計算邏輯如下:
- 遊戲開始,選擇難度
- 滑鼠左鍵隨機點開一個格子A(你想用右鍵標雷也可以)
- 左鍵點選後一共有三種情況
1) 是數字,假設是2,表示以該格子A為中心的九宮格除了該格子以外的8個格子內有2個雷
2)是空白,表示以該格子A為中心的九宮格除了該格子以外的8個格子內沒有雷,並且系統計算分別以這8個格子為中心九宮格除了該格子以外的8個格子是否是空白
a. 是空白,繼續計算以這8個格子為中心九宮格除了該格子以外的8個格子是否是空白,迴圈擴散
c. 是數字,數字格子翻出來顯示,停止擴散
3)是雷,game over 了。。。 - 如果點第一次只翻了一個數字是比較難開始遊戲的,如果第一次翻了一大片出來,恭喜你,可以開始秀你的智商了,在確認是雷點的格子點右鍵,即可標記地雷格,表示你認定這個格子是地雷
- 如果數字周圍的雷被全部標記出來了,但數字周圍還有沒被翻開的格子的話,雙擊該格子,自動將其他格子翻開
好,這下規則我們瞭解了,接下來,摩拳擦掌,開始寫程式碼咯,本次程式碼用vue來寫,沒有原因,習慣了而已。
掃雷程式碼邏輯
看著遊戲規則,我們先來理一理,要如何完成這個功能,我們以最簡單的,最能使人理解的步驟,來完成這次功能。
- 難度選擇
- 根據選擇的遊戲難度繪製遊戲主介面,就是格盤
- 根據選擇的遊戲難度隨機生成地雷放到盤中
- 計算每個格子周圍的雷數
- 完成使用者互動(左鍵點選翻格子,右鍵點選放雷)
- 空白格擴散翻格子
- 計算遊戲結束
生成專案
避免vue新手不知如何下手,那就從建專案開始吧,環境安裝我就不講了,腳手架也是要有的
第一步,初始化lovesweeping工程(阿望不喜歡地雷,喜歡小桃心)
專案很小,不需要路由,不需要vuex,即可完成,只帶了sass,連eslint都可以不要,看官們可以根據自己的喜好建專案
專案生成之後,helloworld就可以刪掉了,它的存在並沒有什麼意義
檔案切分
這一步主要是構建工程結構,簡單畫一下主要的幾個檔案
- src
- components
- SelectLevel.vue [新增,難度選擇元件]
- LoveSweeping.vue [新增,遊戲介面元件]
- App.vue [父元件,負責元件間的切換和某些資料傳遞]
- main.js
- package.json
好,初步認識了專案結構以後,咱把該新建的建好,可以不加東西,不報錯就行
然後就是把元件間的切換程式碼寫好,再來一步步的填充程式碼
難度選擇
這一步很簡單,首先畫好難度選擇的頁面,難度可以自己設定,你覺得合理就行,我這裡的資料格式是這樣的,用一個物件表示一個難度等級,物件中包含了難度描述,以及難度設定,設定中包含了格子橫排數,格子縱排數,雷數
// 難度
level: [
{
text: '青銅', // 難度描述
value: [9, 9, 10] // 格子橫排數,格子縱排數,雷數
}, {
text: '黃金',
value: [12, 9, 20]
]
然後模板中直接渲染列表,這樣做的好處是,想要增加難度直接在陣列中新增資料即可
<li v-for="(item, index) in level" :key="index" @click="handleChoseLevel(item.value)">{{ item.text }}</li>
該元件中只有一個方法:選擇難度之後,跳轉到遊戲主介面上去,因為專案沒有用路由,直接使用元件間的切換,所以,這個方法只負責告訴父元件,我已經選擇好難度了,可以開始遊戲了
// 選擇難度
handleChoseLevel(level) {
this.$emit("chose-level", level);
}
程式碼如下:
介面長這樣,當然,你要覺得難看自己換個樣式也行
遊戲介面
畫格盤
通過遊戲難度選擇來決定遊戲格盤的大小,元件間已通過App將遊戲難度傳至介面元件中,我們用props把資料接收到,消化成自己的資料,畫格盤需要的資料有:橫排格子數,縱排格子數
畫格子:我們將格子的索引暴露出來,後續可以幫助我們試錯。整個格局有兩種方式來表示格盤,座標式和索引式,比如橫9縱9的格子,[0, 0]代表第1個格子,[2, 3]代表第三行第四列也就是第20個格子。此次使用索引式來標誌格盤
<div
v-for="col in cols" :key="Math.random() + col"
class="game-content-row">
<span
v-for="row in rows" :key="Math.random() + row"
class="game-block">
<span>{{(col - 1) * rows + row - 1}}</span>
</span>
</div>
隨機生成地雷
首先data中新增一個minePosition屬性,用來記錄雷點位置
隨機生成地雷比較簡單,主要注意,生成的地雷點數在格盤個數範圍內,那麼就可以寫出隨機生成的地雷了。介面元件已收集到橫排格子數、縱排格子數、雷數,那麼就能得到格子總數,假設橫9縱9,10個雷,那麼就是生成10個81以內的隨機數(如果索引從0開始,即80以內)。
// 隨機獲取雷點位置
getMinePosition() {
// 定義一個數組裝不重複的格點
let mineArr = [];
// 迴圈雷數生成不重複的雷點
for (let n = 0; n < this.gameInfo[2]; n++) {
const random = Math.floor(Math.random() * this.latticeNum);
if (mineArr.indexOf(random) === -1) {
mineArr.push(random);
} else {
n--;
}
}
this.minePosition = mineArr;
},
把地雷位置暴露出來
格子周圍的雷數
確認了雷點位置,接下來要做的就是確認每一個非雷點位置周圍的雷的數量,我們用物件來描述一個格子,這個物件主要包含以下幾個屬性
// 格子屬性
lattice: [{
index: 0, // 格子索引
mineNum: 0, // 周圍雷數
isMine: false, // 是否是雷
isOpen: false, // 是否已經被點開
isMark: false, // 是否被標記
}],
這裡我們主要用到index, isMine, mineNum屬性,這一步,主要是計算每個格子元素的mineNum值,依賴於以下兩個方法,個人覺得掃雷遊戲最難理解的,最難捋清的邏輯,其中一個就是獲取非雷點位置周圍8個位置索引的方法getLatticeIndex(另一個是點選空白格擴散)
// 獲取格子周圍的雷數,
getMineNumAroundLattice(lattice, index) {
// 先獲取格子周圍的有效索引
const latticeIndexArr = this.getLatticeIndex(index);
// 迴圈索引,索引值在雷點陣列中的,即為雷,當前格子的雷點數加1
latticeIndexArr.forEach(i => {
if (this.minePosition.indexOf(i) > -1) {
lattice.mineNum ++;
}
});
},
// 獲取格子周圍的有效索引
getLatticeIndex(index) {
// 存索引值的變數
let latticeIndexArr = [];
// 當前格子位於第幾行
const latticeRow = Math.ceil(index / this.rows);
// 當前格子位於第幾列(求餘為0說明是最右邊一列)
const latticeCol = Math.ceil(index % this.rows) || this.rows;
// 第一行沒有上一行,不需要計算減1的行值,最後一行沒有下一行,不需要計算加1的行值
for (let i = (latticeRow === 1 ? 0 : -1); i < (latticeRow === this.cols ? 1 : 2); i++) {
// 第一列沒有左列,不需要計算減1的列值,最後一列沒有右列,不需要計算加1的列值
for (let j = (latticeCol === 1 ? 0 : -1); j < (latticeCol === this.rows ? 1 : 2); j++) {
// 索引值 = (當前行值 + (上一行【-1】/當前行【0】/下一行【+1】) - 1【1是索引從0開始,所以需要減去】) * 每行格子數 + 當前列值 + (上一列【-1】/當前列【0】/下一列【+1】)
const latticeIndex = (latticeRow + i - 1) * this.rows + (latticeCol + j);
latticeIndexArr.push(latticeIndex);
}
}
return latticeIndexArr;
},
有了這兩個方法,咱成功地獲取到了每個非雷點格子周圍的雷的數量,來,展示出來,這樣展示的好處是,我們一眼就可以看出演算法是否正確
沒問題了,來,接著往下走,格盤資料基本都設定好了,那我們接下來要做的就是,點開格子操作
點選互動
這一步先做簡單點,有個明顯的區別就可以了,點雷我們先不管,先看點數字和空白的情況,首先得明確,到時候格子的可見屬性是全部要被隱藏的,點選了才會顯示出來,這就用到了我們上一步提到的isOpen屬性,預設肯定全是不可見的,點選之後,非雷翻開
點數字
點數字很簡單,直接翻開,將isOpen屬性設定為true
來點不一樣的,isOpen === true 的時候字型變紅色,走你┏ (゜ω゜)=☞
@click.left="handleClickLattice(lattice[(col - 1) * rows + row - 1])"
// 點了格子
handleClickLattice(lattice) {
// 是數字
if (lattice.mineNum) {
if (!lattice.isOpen && !lattice.isMark) {
lattice.isOpen = true;
}
}
},
點空白
第二個難點來咯,點空白格需要注意以下幾點:1、空白格表示周圍8格都沒有雷 2、擴散周圍8格,判斷雷數,迴圈往復 3、遇邊界停止擴散,遇數字停止擴散
假設橫9縱9的格盤,第二排第三列格為空白格即第12格,那麼點了該空白格之後,首先將其與周圍8格(2,3,4,11,12,13,20,21,22)一起,isOpen置為true,然後分別以周圍8格為中心,判斷該格子是數字,停止擴散,是空白格,繼續擴散
// 程式碼把下半部分補齊
handleClickLattice(lattice) {
... else {
// 是空白
const latticeIndexArr = this.getLatticeIndex(lattice.index);
this.showWhiteAround(lattice, latticeIndexArr);
}
},
// 展示周圍的空白標記,直至邊緣(格子邊緣或者數字)
showWhiteAround(lattice, latticeIndexArr) {
// 避免有重複的資料停不下來,去個重
latticeIndexArr = [...new Set(latticeIndexArr)];
for (let i = 0; i < latticeIndexArr.length; i++) {
const item = latticeIndexArr[i];
latticeIndexArr.splice(i, 1);
i--;
if (this.lattice[item].isOpen) {
continue;
}
this.lattice[item].isOpen = true;
if (!this.lattice[item].mineNum) {
const arr = this.getLatticeIndex(this.lattice[item].index);
this.showWhiteAround(this.lattice[item], latticeIndexArr.concat(arr));
}
}
},
這一步寫完,基本明面上的掃雷步驟就已經完成了,handleClickLattice方法再加一步判斷,如果是雷,遊戲結束
右鍵標記雷點
這個就很簡單了,寫個右擊事件,修改一下格子的isMark和isOpen屬性,這一步的基本邏輯就是
- 右擊格子
- 格子本身已經被開啟
1)是:格子已經被標記為地雷?
a. 是:取消標記(isMark和isOpen取反),剩餘地雷數+1
b. 否:說明是右擊了已經被點開的數字格,不做任何操作
2)否:isMark和isOpen取反,記錄該格子已經被標記為地雷,格子處於開啟狀態,剩餘地雷數-1,判斷是否結束
// 右鍵確認是雷點
handleSureMinePoint(lattice) {
if (!lattice.isOpen) {
lattice.isMark = true;
lattice.isOpen = true;
this.minePosition.splice(this.minePosition.indexOf(lattice.index), 1);
this.judgeIsOver();
} else {
if (lattice.isMark) {
lattice.isMark = false;
lattice.isOpen = false;
this.minePosition.push(lattice.index);
}
}
},
遊戲是否結束
遊戲結束一共有三種情況:1、點到雷了,直接結束 2、雷被標記完了(有可能失敗了,標錯了) 3、翻開的格子數 + 雷數 = 總格子數
完成功能
接下來要做的就是把格子屬性隱藏起來,假裝不知道,再假裝滑鼠一點,格子就翻過來了。這就用到之前提到的格子屬性isMark和isOpen,本身元素處於隱藏狀態,當被標記或者被開啟的時候設定相應的屬性使其可見就行了,如此,便完成了掃雷的基本功能,有興趣的小朋友可以自己融合多種功能試一試
當然,這只是其中一種實現方式,把所有的計算全部放在玩遊戲之前了,愛動腦筋的朋友們也可以想想,如果放在每一次點選時做計算該如何組織程式碼
阿望的原始碼中還集合了【重開一局】、【改變難度】、【遊戲計時】等功能,樣式相容手機和PC線上玩,在手機上玩的時候我在糾結手機如何模仿PC端的右鍵點選標雷操作,沒有好的想法,不想用雙擊,於是多加了一個狀態,點選頁面【標記】按鈕,即表示標記雷點,再點選一次表示還原,正常點開數字格,坐火車無聊的小朋友可以玩一玩喲
檢視阿望的原始碼:mineSweeping
線上試玩:mine-sweeping-online
希望各位看官不吝右上角賜個小星星哦,阿望這廂有禮啦,新年快樂啦★,°:.☆( ̄▽ ̄)/$:.°★ 。