DDR爱好者之家 Design By 杰米

最近想找些项目练练手,发现去复刻一些小游戏还挺有意思的,于是就做了一个网页版的扫雷。

点击这里 看看最终的效果。

创建应用

该项目使用的是 monorepo 的形式来存放代码。在 Angular 中,构建 monorepo 方法如下:

ng new simple-game --createApplication=false 
ng generate application mine-sweeper

在这里,因为该项目以后还会包含其他各种其他的应用,所以个人觉得使用 monorepo 构建项目是比较正确的选择。如果不想使用 monorepo,使用以下命令创建应用:

ng new mine-sweeper

流程图

首先,我们先来看看扫雷的基本流程。

用Angular实现一个扫雷的游戏示例 

数据结构抽象

通过观察流程图,可以得到扫雷基本上有这么几种状态:

  • 开始
  • 进行游戏
  • 胜利
  • 失败

方块的状态如下:

  • 它有雷无雷,取决于它的初始设置;
  • 如果没有雷,那么它需要展示附近地雷的数量;
  • 是否已经被打开;

我们可以先定义好这些状态,之后根据不同的状态,执行不同的逻辑,同时反馈给组件。

// 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 主要功能是提供了一个响应式的对象,使得逻辑服务可以通过这个对象对数据进行变更,并且,组件也可以通过这些对象来监听数据变化。

通过上面的准备工作,我们可以开始编写逻辑函数 startdoNextstart 的作用是给状态机重新设置状态;而 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 "htmlcode">
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 官方文档

DDR爱好者之家 Design By 杰米
广告合作:本站广告合作请联系QQ:858582 申请时备注:广告合作(否则不回)
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
DDR爱好者之家 Design By 杰米

《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线

暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。

艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。

《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。