用Angular實現一個掃雷的遊戲示例
最近想找些專案練練手,發現去復刻一些小遊戲還挺有意思的,於是就做了一個網頁版的掃雷。
點選這裡 看看最終的效果。
建立應用
該專案使用的是 monorepo 的形式來存放程式碼。在 Angular 中,構建 monorepo 方法如下:
ng new simple-game --createApplication=false ng generate application mine-sweeper
在這裡,因為該專案以後還會包含其他各種其他的應用,所以個人覺得使用 monorepo 構建專案是比較正確的選擇。如果不想使用 monorepo,使用以下命令建立應用:
ng new mine-sweeper
流程圖
首先,我們先來看看掃雷的基本流程。
資料結構抽象
通過觀察流程圖,可以得到掃雷基本上有這麼幾種狀態:
- 開始
- 進行遊戲
- 勝利
- 失敗
方塊的狀態如下:
- 它有雷無雷,取決於它的初始設定;
- 如果沒有雷,那麼它需要展示附近地雷的數量;
- 是否已經被開啟;
我們可以先定義好這些狀態,之後根據不同的狀態,執行不同的邏輯,同時反饋給元件。
// model.ts export enum GameState { BEGINNING = 0x00,PLAYING = 0x01,WIN = 0x02,LOST = 0x03,} export interface IMineBlock { // 當前塊是否是的內部是地雷 readonly isMine: boolean; // 附近地雷塊的數量 readonly nearestMinesCount: number; // 是否已經被點開 readonly isFound: boolean; }
編寫邏輯
為了使得掃雷的邏輯不跟元件耦合,我們需要新增一個 service。
ng generate service mine-sweeper
現在開始邏輯編寫。首先,要儲存遊戲狀態、地雷塊、地雷塊邊長(目前設計的掃雷是正方形)、雷的數量。
export class MineSweeperService { private readonly _mineBlocks = new BehaviorSubject<IMineBlock[]>([]); private readonly _side = new BehaviorSubject(10); private readonly _state = new BehaviorSubject<GameState>(GameState.BEGINNING); private readonly _mineCount = new BehaviorSubject<number>(10); readonly side$ = this._side.asObservable(); readonly mineBlock$ = this._mineBlocks.asObservable(); readonly state$ = this._state.asObservable(); readonly mineCount$ = this._mineCount.asObservable(); get side() { return this._side.value; } set side(value: number) { this._side.next(value); } get mineBlocks() { return this._mineBlocks.value; } get state() { return this._state.value; } get mineCount() { return this._mineCount.value; } //... }
得益於 Rxjs
,通過使用 BehaviorSubject
使得我們可以很方便的將這些狀態變數設計成響應式的。 BehaviorSubject
主要功能是提供了一個響應式的物件,使得邏輯服務可以通過這個物件對資料進行變更,並且,元件也可以通過這些物件來監聽資料變化。
通過上面的準備工作,我們可以開始編寫邏輯函式 start
和 doNext
。 start
的作用是給狀態機重新設定狀態;而 doNext
的作用是根據玩家點選的方塊的索引對遊戲進行狀態轉移。
port class MineSweeperService { // ... start() { this._mineBlocks.next(this.createMineBlocks(this.side)); this._state.next(GameState.BEGINNING); } doNext(index: number): boolean { switch (this.state) { case GameState.LOST: case GameState.WIN: return false; case GameState.BEGINNING: this.prepare(index); this._state.next(GameState.PLAYING); break; case GameState.PLAYING: if (this.testIsMine(index)) { this._state.next(GameState.LOST); } break; default: break; } if (this.vitoryVerify()) { this._state.next(GameState.WIN); } return true; } // ... }
上面的程式碼中包含了 prepare
,testIsMine
,victoryVerify
這三個函式,他們的作用都是進行一些邏輯運算。
我們先看 prepare
,因為他是最先執行的。這個函式的主要邏輯是通過隨機數生成地雷,並且保證使得使用者第一次點選地雷塊的時候,不會出現雷。配合著註釋,我們一行一行的分析它是怎麼執行的。
export class MineSweeperService { private prepare(index: number) { const blocks = [...this._mineBlocks.value]; // 判斷index是否越界了 if (!blocks[index]) { throw Error('Out of index.'); } // 將索引位置的塊設定為已經開啟的狀態。 blocks[index] = { isMine: false,isFound: true,nearestMinesCount: 0 }; // 生成隨機數陣列,其中的隨機數不包含 index。 const numbers = this.generateRandomNumbers(this.mineCount,this.mineBlocks.length,index); // 通過隨機數陣列,設定指定的塊為雷。 for (const num of numbers) { blocks[num] = { isMine: true,isFound: false,nearestMinesCount: 0 }; } // 使用橫縱座標遍歷所有的地雷塊 // 這樣做使得我們可以直接通過對座標的增減來檢測當前塊附近雷的數量。 const side = this.side; for (let i = 0; i < side; i++) { for (let j = 0; j < side; j++) { const index = transform(i,j); const block = blocks[index]; // 如果當前塊是雷,那麼不進行檢測 if (block.isMine) { continue; } // 進行地雷塊的附近的雷的數量檢測,形如這樣 // x 1 o // 1 1 o // o o o // let nearestMinesCount = 0; for (let x = -1; x <= 1; x++) { for (let y = -1; y <= 1; y++) { nearestMinesCount += this.getMineCount(blocks[transform(i + x,j + y)]); } } // 對附近的地雷的數量進行更新 blocks[index] = { ...block,nearestMinesCount }; } } // 如果點選的位置附近的地雷數量是 0,則需要遍歷附近所有的塊,直到所有開啟的塊附近的地雷數量不為零。 if (blocks[index].nearestMinesCount === 0) { this.cleanZeroCountBlock(blocks,index,this.transformToIndex(this.side)); } // 觸發更新 this._mineBlocks.next(blocks); } }
再來看 testIsMine
,其作用是返回一個布林值,這個布林值表示使用者點選的塊是否為地雷。
private testIsMine(index: number): boolean { const blocks = [...this._mineBlocks.value]; if (!blocks[index]) { throw Error('Out of index.'); } // 當前塊為設開啟狀態 const theBlock = { ...blocks[index],isFound: true }; blocks[index] = theBlock; // 如果當前塊是地雷,則找出所有是地雷的地雷塊,將其狀態設定為開啟狀態。 // 或者如果點選的位置附近的地雷數量是 0,則需要遍歷附近所有的塊,直到所有開啟的塊附近的地雷數量不為零。 if (theBlock.isMine) { for (let i = 0; i < blocks.length; i++) { if (blocks[i].isMine) { blocks[i] = { ...blocks[i],isFound: true }; } } } else if (!theBlock.nearestMinesCount) { this.cleanZeroCountBlock(blocks,index); } // 觸發更新 this._mineBlocks.next(blocks); // 返回判定結果 return theBlock.isMine; }
那麼到了 victoryVerify
,它的作用很明顯,就是進行勝利判定:當未開啟的塊的數量等於設定的地雷數量相等的時候,可以被判定為使用者勝利。
private vitoryVerify() { // 對當前地雷塊陣列進行 reduce 查詢。 return this.mineBlocks.reduce((prev,current) => { return !current.isMine && current.isFound ? prev + 1 : prev; },0) === this.mineBlocks.length - this.mineCount; }
現在我們已經介紹完這三個函式,下面將分析 cleanZeroCountBlock
是如何執行的。他的作用就是為了開啟當前塊附近所有為零的塊。
private cleanZeroCountBlock(blocks: IMineBlock[],index: number) { const i = index % this.side; const j = Math.floor(index / this.side); // 對其附近的8個塊進行檢測 for (let x = -1; x <= 1; x++) { for (let y = -1; y <= 1; y++) { const currentIndex = this.transformToIndex(i + x,j + y); const block = blocks[currentIndex]; // 不為原始塊,且塊存在,且未開啟,且不是地雷 if (currentIndex === index || !block || block.isFound || block.isMine) { continue; } // 將其設為開啟狀態 blocks[currentIndex] = { ...block,isFound: true }; // 遞迴查詢 if (blocks[currentIndex].nearestMinesCount === 0) { this.cleanZeroCountBlock(blocks,currentIndex); } } } }
到這裡,我們基本已經編寫完掃雷的具體邏輯。其他相關函式,可以查閱原始碼,不再贅述。
實現頁面
到了這一步,其實就已經完成了大部分的工作,我們根據響應式物件編寫元件,然後給dom物件新增點選事件,並觸發相關的邏輯函式,之後再做各種的錯誤處理等等。頁面程式碼就不貼在這裡,在Github上可以檢視原始碼。
原始碼以及參考
最後,如果有寫得不好或者存在錯誤的地方,歡迎提出批評和修改建議,感謝您的閱讀。
Mine Sweeper 原始碼
Angular 官方文件
Rxjs 官方文件
到此這篇關於用Angular實現一個掃雷的遊戲示例的文章就介紹到這了,更多相關Angular 掃雷內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!