用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 相关文章推荐
FormValid0.5版本发布,带ajax自定义验证例子
Aug 17 Javascript
javascript css float属性的特殊写法
Nov 13 Javascript
jQuery对象初始化的传参方式
Feb 26 Javascript
jquery利用命名空间移除绑定事件的方法
Mar 11 Javascript
分享有关jQuery中animate、slide、fade等动画的连续触发、滞后反复执行的bug
Jan 10 Javascript
javascript日期比较方法实例分析
Jun 17 Javascript
JQuery 设置checkbox值二次无效的解决方法
Jul 22 Javascript
js 颜色选择插件
Jan 23 Javascript
JavaScript的for循环中嵌套一个点击事件的问题解决
Mar 03 Javascript
vue.js的手脚架vue-cli项目搭建的步骤
Aug 30 Javascript
创建Vue项目以及引入Iview的方法示例
Dec 03 Javascript
JS实现盒子拖拽效果
Feb 06 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
destoon实现资讯信息前面调用它所属分类的方法
2014/07/15 PHP
php获取四位字母和数字的随机数的实现方法
2015/01/09 PHP
Thinkphp模板标签if和eq的区别和比较实例分析
2015/07/01 PHP
thinkPHP框架自动填充原理与用法分析
2018/04/03 PHP
通过JS 获取Mouse Position(鼠标坐标)的代码
2009/09/21 Javascript
javascript中类的定义及其方式(《javascript高级程序设计》学习笔记)
2011/07/04 Javascript
JQuery实现超链接鼠标提示效果的方法
2015/06/10 Javascript
jquery实现带渐变淡入淡出并向右依次展开的多级菜单效果实例
2015/08/22 Javascript
超精准的javascript验证身份证号的具体实现方法
2015/11/18 Javascript
全面解析JavaScript中apply和call以及bind(推荐)
2016/06/15 Javascript
在JavaScript中调用Java类和接口的方法
2016/09/07 Javascript
js变量提升深入理解
2016/09/16 Javascript
angularJS Provider、factory、service详解及实例代码
2016/09/21 Javascript
js实现点击每个li节点,都弹出其文本值及修改
2016/12/15 Javascript
详解js中常规日期格式处理、月历渲染和倒计时函数
2016/12/28 Javascript
JS非空验证及邮箱验证的实例
2017/08/11 Javascript
js实现简易聊天对话框
2017/08/17 Javascript
微信小程序页面滑动屏幕加载数据效果
2020/11/16 Javascript
微信小程序promsie.all和promise顺序执行
2017/10/27 Javascript
JS加密插件CryptoJS实现AES加密操作示例
2018/08/16 Javascript
详解Python中的元组与逻辑运算符
2015/10/13 Python
python Django批量导入不重复数据
2016/03/25 Python
利用Python读取txt文档的方法讲解
2018/06/23 Python
Python3 关于pycharm自动导入包快捷设置的方法
2019/01/16 Python
解决项目pycharm能运行,在终端却无法运行的问题
2019/01/19 Python
python做反被爬保护的方法
2019/07/01 Python
关于python pycharm中输出的内容不全的解决办法
2020/01/10 Python
One.com挪威:北欧成长最快的网络托管公司
2016/11/19 全球购物
介绍一下JMS编程步骤
2015/09/22 面试题
党校培训思想汇报
2013/12/30 职场文书
服务之星获奖感言
2014/01/21 职场文书
个人优缺点自我评价
2014/01/27 职场文书
幼儿园春季开学寄语
2014/04/03 职场文书
党在我心中的演讲稿
2014/09/13 职场文书
教师节老师寄语
2015/05/28 职场文书
Docker部署Mysql8的实现步骤
2022/07/07 Servers