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


Posted in Javascript onMay 15, 2020

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

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

创建应用

该项目使用的是 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 ? 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 扫雷内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
理解JavaScript的prototype属性
Feb 11 Javascript
文本有关的样式和jQuery求对象的高宽问题分别说明
Aug 30 Javascript
Jquery的基本对象转换和文档加载用法实例
Feb 25 Javascript
使用Curl命令查看请求响应时间方法
Nov 04 Javascript
js如何判断是否在iframe中及防止网页被别站用iframe嵌套
Jan 11 Javascript
js实现3d悬浮效果
Feb 16 Javascript
JS实现页面内跳转的简单代码
Sep 03 Javascript
微信小程序 input表单与redio及下拉列表的使用实例
Sep 20 Javascript
AngularJS中下拉框的高级用法示例
Oct 11 Javascript
vue+element UI实现树形表格带复选框的示例代码
Apr 16 Javascript
Vue scrollBehavior 滚动行为实现后退页面显示在上次浏览的位置
May 27 Javascript
vue quill editor 使用富文本添加上传音频功能
Jan 14 Javascript
Node.js API详解之 dns模块用法实例分析
May 15 #Javascript
关于vue3默认把所有onSomething当作v-on事件绑定的思考
May 15 #Javascript
js实现简单贪吃蛇游戏
May 15 #Javascript
Javascript执行流程细节原理解析
May 14 #Javascript
使用npm命令提示: 'npm' 不是内部或外部命令,也不是可运行的程序的处理方法
May 14 #Javascript
javascript中的offsetWidth、clientWidth、innerWidth及相关属性方法
May 14 #Javascript
vue组件系列之TagsInput详解
May 14 #Javascript
You might like
一个多文件上传的例子(原创)
2006/10/09 PHP
php防止sql注入代码实例
2013/12/18 PHP
PHP上传图片进行等比缩放可增加水印功能
2014/01/13 PHP
php使用类继承解决代码重复的问题
2015/02/11 PHP
php使用curl并发减少后端访问时间的方法分析
2016/05/12 PHP
php实现小程序支付完整版
2018/10/09 PHP
PHP切割汉字的常用方法实例总结
2019/04/27 PHP
JQuery自定义事件的应用 JQuery最佳实践
2010/08/01 Javascript
Javascript 类型转换方法
2010/10/24 Javascript
js+数组实现网页上显示时间/星期几的实用方法
2013/01/18 Javascript
详解JS正则replace的使用方法
2016/03/06 Javascript
Ionic如何创建APP项目
2016/06/03 Javascript
flag和jq on 的绑定多个对象和方法(必看)
2017/02/27 Javascript
js for循环倒序输出数组元素的实例
2017/03/01 Javascript
AngularJS $http模块POST请求实现
2017/04/08 Javascript
一个简易的js图片轮播效果
2017/07/22 Javascript
浅谈vue中数据双向绑定的实现原理
2017/09/14 Javascript
JS实现手写parseInt的方法示例
2017/09/24 Javascript
vue 登录滑动验证实现代码
2018/08/24 Javascript
vue 实现边输入边搜索功能的实例讲解
2018/09/16 Javascript
[00:35]DOTA2上海特级锦标赛 EG战队宣传片
2016/03/04 DOTA
利用python程序生成word和PDF文档的方法
2017/02/14 Python
对pytorch网络层结构的数组化详解
2018/12/08 Python
Python利用heapq实现一个优先级队列的方法
2019/02/03 Python
Python----数据预处理代码实例
2019/03/20 Python
值得收藏的10道python 面试题
2019/04/15 Python
python利用JMeter测试Tornado的多线程
2020/01/12 Python
美国汽车交易网站:Edmunds
2016/08/17 全球购物
中层干部岗位职责
2013/12/18 职场文书
寒假家长评语大全
2014/04/16 职场文书
公务员试用期满考核材料
2014/05/22 职场文书
2014年班组建设工作总结
2014/12/01 职场文书
扬州个园导游词
2015/02/06 职场文书
2015圣诞节贺卡寄语
2015/03/24 职场文书
幼儿园园长工作总结2015
2015/05/25 职场文书
用Python提取PDF表格的方法
2021/04/11 Python