用Angular實現(xiàn)一個掃雷的游戲示例
最近想找些項目練練手,發(fā)現(xiàn)去復(fù)刻一些小游戲還挺有意思的,于是就做了一個網(wǎng)頁版的掃雷。
點擊這里 看看最終的效果。
創(chuàng)建應(yīng)用
該項目使用的是 monorepo 的形式來存放代碼。在 Angular 中,構(gòu)建 monorepo 方法如下:
ng new simple-game --createApplication=false ng generate application mine-sweeper
在這里,因為該項目以后還會包含其他各種其他的應(yīng)用,所以個人覺得使用 monorepo 構(gòu)建項目是比較正確的選擇。如果不想使用 monorepo,使用以下命令創(chuàng)建應(yīng)用:
ng new mine-sweeper
流程圖
首先,我們先來看看掃雷的基本流程。
數(shù)據(jù)結(jié)構(gòu)抽象
通過觀察流程圖,可以得到掃雷基本上有這么幾種狀態(tài):
- 開始
- 進(jìn)行游戲
- 勝利
- 失敗
方塊的狀態(tài)如下:
- 它有雷無雷,取決于它的初始設(shè)置;
- 如果沒有雷,那么它需要展示附近地雷的數(shù)量;
- 是否已經(jīng)被打開;
我們可以先定義好這些狀態(tài),之后根據(jù)不同的狀態(tài),執(zhí)行不同的邏輯,同時反饋給組件。
// model.ts
export enum GameState {
BEGINNING = 0x00,
PLAYING = 0x01,
WIN = 0x02,
LOST = 0x03,
}
export interface IMineBlock {
// 當(dāng)前塊是否是的內(nèi)部是地雷
readonly isMine: boolean;
// 附近地雷塊的數(shù)量
readonly nearestMinesCount: number;
// 是否已經(jīng)被點開
readonly isFound: boolean;
}
編寫邏輯
為了使得掃雷的邏輯不跟組件耦合,我們需要新增一個 service。
ng generate service mine-sweeper
現(xiàn)在開始邏輯編寫。首先,要存儲游戲狀態(tài)、地雷塊、地雷塊邊長(目前設(shè)計的掃雷是正方形)、雷的數(shù)量。
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 使得我們可以很方便的將這些狀態(tài)變量設(shè)計成響應(yīng)式的。 BehaviorSubject 主要功能是提供了一個響應(yīng)式的對象,使得邏輯服務(wù)可以通過這個對象對數(shù)據(jù)進(jìn)行變更,并且,組件也可以通過這些對象來監(jiān)聽數(shù)據(jù)變化。
通過上面的準(zhǔn)備工作,我們可以開始編寫邏輯函數(shù) start 和 doNext 。 start 的作用是給狀態(tài)機重新設(shè)置狀態(tài);而 doNext 的作用是根據(jù)玩家點擊的方塊的索引對游戲進(jìn)行狀態(tài)轉(zhuǎn)移。
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 這三個函數(shù),他們的作用都是進(jìn)行一些邏輯運算。
我們先看 prepare ,因為他是最先運行的。這個函數(shù)的主要邏輯是通過隨機數(shù)生成地雷,并且保證使得用戶第一次點擊地雷塊的時候,不會出現(xiàn)雷。配合著注釋,我們一行一行的分析它是怎么運行的。
export class MineSweeperService {
private prepare(index: number) {
const blocks = [...this._mineBlocks.value];
// 判斷index是否越界了
if (!blocks[index]) {
throw Error('Out of index.');
}
// 將索引位置的塊設(shè)置為已經(jīng)打開的狀態(tài)。
blocks[index] = { isMine: false, isFound: true, nearestMinesCount: 0 };
// 生成隨機數(shù)數(shù)組,其中的隨機數(shù)不包含 index。
const numbers = this.generateRandomNumbers(this.mineCount, this.mineBlocks.length, index);
// 通過隨機數(shù)數(shù)組,設(shè)置指定的塊為雷。
for (const num of numbers) {
blocks[num] = { isMine: true, isFound: false, nearestMinesCount: 0 };
}
// 使用橫縱坐標(biāo)遍歷所有的地雷塊
// 這樣做使得我們可以直接通過對坐標(biāo)的增減來檢測當(dāng)前塊附近雷的數(shù)量。
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];
// 如果當(dāng)前塊是雷,那么不進(jìn)行檢測
if (block.isMine) {
continue;
}
// 進(jìn)行地雷塊的附近的雷的數(shù)量檢測,形如這樣
// 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)]);
}
}
// 對附近的地雷的數(shù)量進(jìn)行更新
blocks[index] = { ...block, nearestMinesCount };
}
}
// 如果點擊的位置附近的地雷數(shù)量是 0,則需要遍歷附近所有的塊,直到所有打開的塊附近的地雷數(shù)量不為零。
if (blocks[index].nearestMinesCount === 0) {
this.cleanZeroCountBlock(blocks, index, this.transformToIndex(this.side));
}
// 觸發(fā)更新
this._mineBlocks.next(blocks);
}
}
再來看 testIsMine ,其作用是返回一個布爾值,這個布爾值表示用戶點擊的塊是否為地雷。
private testIsMine(index: number): boolean {
const blocks = [...this._mineBlocks.value];
if (!blocks[index]) {
throw Error('Out of index.');
}
// 當(dāng)前塊為設(shè)打開狀態(tài)
const theBlock = { ...blocks[index], isFound: true };
blocks[index] = theBlock;
// 如果當(dāng)前塊是地雷,則找出所有是地雷的地雷塊,將其狀態(tài)設(shè)置為打開狀態(tài)。
// 或者如果點擊的位置附近的地雷數(shù)量是 0,則需要遍歷附近所有的塊,直到所有打開的塊附近的地雷數(shù)量不為零。
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);
}
// 觸發(fā)更新
this._mineBlocks.next(blocks);
// 返回判定結(jié)果
return theBlock.isMine;
}
那么到了 victoryVerify ,它的作用很明顯,就是進(jìn)行勝利判定:當(dāng)未打開的塊的數(shù)量等于設(shè)定的地雷數(shù)量相等的時候,可以被判定為用戶勝利。
private vitoryVerify() {
// 對當(dāng)前地雷塊數(shù)組進(jìn)行 reduce 查找。
return this.mineBlocks.reduce((prev, current) => {
return !current.isMine && current.isFound ? prev + 1 : prev;
}, 0) === this.mineBlocks.length - this.mineCount;
}
現(xiàn)在我們已經(jīng)介紹完這三個函數(shù),下面將分析 cleanZeroCountBlock 是如何運行的。他的作用就是為了打開當(dāng)前塊附近所有為零的塊。
private cleanZeroCountBlock(blocks: IMineBlock[], index: number) {
const i = index % this.side;
const j = Math.floor(index / this.side);
// 對其附近的8個塊進(jìn)行檢測
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;
}
// 將其設(shè)為打開狀態(tài)
blocks[currentIndex] = { ...block, isFound: true };
// 遞歸查詢
if (blocks[currentIndex].nearestMinesCount === 0) {
this.cleanZeroCountBlock(blocks, currentIndex);
}
}
}
}
到這里,我們基本已經(jīng)編寫完掃雷的具體邏輯。其他相關(guān)函數(shù),可以查閱源碼,不再贅述。
實現(xiàn)頁面
到了這一步,其實就已經(jīng)完成了大部分的工作,我們根據(jù)響應(yīng)式對象編寫組件,然后給dom對象添加點擊事件,并觸發(fā)相關(guān)的邏輯函數(shù),之后再做各種的錯誤處理等等。頁面代碼就不貼在這里,在Github上可以查看源碼。
源碼以及參考
最后,如果有寫得不好或者存在錯誤的地方,歡迎提出批評和修改建議,感謝您的閱讀。
到此這篇關(guān)于用Angular實現(xiàn)一個掃雷的游戲示例的文章就介紹到這了,更多相關(guān)Angular 掃雷內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
AngularJS基于ui-route實現(xiàn)深層路由的方法【路由嵌套】
這篇文章主要介紹了AngularJS基于ui-route實現(xiàn)深層路由的方法,涉及AngularJS路由嵌套操作相關(guān)實現(xiàn)步驟與技巧,需要的朋友可以參考下2016-12-12
在 Angular-cli 中使用 simple-mock 實現(xiàn)前端開發(fā) API Mock 接口數(shù)據(jù)模擬功能的方法
這篇文章主要介紹了在 Angular-cli 中使用 simple-mock 實現(xiàn)前端開發(fā) API Mock 接口數(shù)據(jù)模擬功能的方法,需要的朋友可以參考下2018-11-11
基于Angular 8和Bootstrap 4實現(xiàn)動態(tài)主題切換的示例代碼
這篇文章主要介紹了基于Angular 8和Bootstrap 4實現(xiàn)動態(tài)主題切換的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
使用 Github Actions 自動部署 Angular 應(yīng)用到 Github Pages的方法
這篇文章主要介紹了使用 Github Actions 自動部署 Angular 應(yīng)用到 Github Pages,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07
angularJS與bootstrap結(jié)合實現(xiàn)動態(tài)加載彈出提示內(nèi)容
這篇文章主要介紹了angularJS與bootstrap結(jié)合實現(xiàn)動態(tài)加載彈出提示內(nèi)容,通過bootstrp彈出提示。感興趣的朋友可以參考下本篇文章2015-10-10

