用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 相关文章推荐
dotopAlert 提示用户需安装播放器的代码
Sep 17 Javascript
js浏览器本地存储store.js介绍及应用
May 13 Javascript
JavaScript截取、切割字符串的技巧
Jan 07 Javascript
JS获取IMG图片高宽的简单实例
May 17 Javascript
从零学习node.js之详解异步控制工具async(八)
Feb 27 Javascript
通过命令行生成vue项目框架的方法
Jul 12 Javascript
Vue.js实现可配置的登录表单代码详解
Mar 29 Javascript
Angular2之二级路由详解
Aug 31 Javascript
在Vuex使用dispatch和commit来调用mutations的区别详解
Sep 18 Javascript
js实现AI五子棋人机大战
May 28 Javascript
JavaScript代码简化技巧实例解析
Sep 09 Javascript
JS前端使用canvas实现扩展物体类和事件派发
Aug 05 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
PHP面向对象编程快速入门
2006/12/14 PHP
php传值方式和ajax的验证功能
2017/03/27 PHP
浅谈PHP SHA1withRSA加密生成签名及验签
2019/03/18 PHP
给moz-firefox下添加IE方法和属性
2007/04/10 Javascript
理解Javascript_02_理解undefined和null
2010/10/11 Javascript
cookie.js 加载顺序问题怎么才有效
2013/07/31 Javascript
js日期、星座的级联显示代码
2014/01/23 Javascript
javascript实现的一个随机点名功能
2014/08/26 Javascript
jQuery实现的类似淘宝网站搜索框样式代码分享
2015/08/24 Javascript
jQuery实现类似标签风格的导航菜单效果代码
2015/08/25 Javascript
JS+CSS实现精美的二级导航效果代码
2015/09/17 Javascript
JavaScript学习小结(7)之JS RegExp
2015/11/29 Javascript
jQuery选择器用法实例详解
2015/12/17 Javascript
基于jQuery实现仿搜狐辩论投票动画代码(附源码下载)
2016/02/18 Javascript
javascript仿京东导航左侧分类导航下拉菜单效果
2020/11/25 Javascript
字符串反转_JavaScript
2016/04/28 Javascript
基于jquery实现弹幕效果
2016/09/29 Javascript
利用Node.js编写跨平台的spawn语句详解
2017/02/12 Javascript
Swiper自定义分页器使用详解
2017/12/28 Javascript
基于three.js编写的一个项目类示例代码
2018/01/05 Javascript
ReactRouter的实现方法
2021/01/25 Javascript
Python基于回溯法解决01背包问题实例
2017/12/06 Python
Python使用ctypes调用C/C++的方法
2019/01/29 Python
使用Python+wxpy 找出微信里把你删除的好友实例
2019/02/21 Python
python实现可逆简单的加密算法
2019/03/22 Python
对Python 中矩阵或者数组相减的法则详解
2019/08/26 Python
python实现二分类的卡方分箱示例
2019/11/22 Python
深入了解Python 方法之类方法 &amp; 静态方法
2020/08/17 Python
python 图像增强算法实现详解
2021/01/24 Python
中学生差生评语
2014/01/30 职场文书
中学生期末评语
2014/02/03 职场文书
物理力学求职信
2014/02/18 职场文书
员工安全生产承诺书
2014/05/22 职场文书
作风转变心得体会
2014/09/02 职场文书
美丽人生观后感
2015/06/03 职场文书
PyTorch中permute的使用方法
2022/04/26 Python