溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Angular如何實現(xiàn)一個掃雷游戲

發(fā)布時間:2020-07-27 11:29:10 來源:億速云 閱讀:139 作者:小豬 欄目:web開發(fā)

小編這次要給大家分享的是Angular如何實現(xiàn)一個掃雷游戲,文章內(nèi)容豐富,感興趣的小伙伴可以來了解一下,希望大家閱讀完這篇文章之后能夠有所收獲。

創(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

流程圖

首先,我們先來看看掃雷的基本流程。

Angular如何實現(xiàn)一個掃雷游戲 

數(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ù) startdoNext 。 start 的作用是給狀態(tài)機(jī)重新設(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ù)的主要邏輯是通過隨機(jī)數(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 };

  // 生成隨機(jī)數(shù)數(shù)組,其中的隨機(jī)數(shù)不包含 index。
  const numbers = this.generateRandomNumbers(this.mineCount, this.mineBlocks.length, index);
  // 通過隨機(jī)數(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 &#63; 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);
   }
  }
 }
}

看完這篇關(guān)于Angular如何實現(xiàn)一個掃雷游戲的文章,如果覺得文章內(nèi)容寫得不錯的話,可以把它分享出去給更多人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI