用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 相关文章推荐
[原创]后缀就扩展名为js的文件是什么文件
Dec 06 Javascript
jquery实现非叠加式的搜索框提示效果
Jan 07 Javascript
node.js学习总结之调式代码的方法
Jun 25 Javascript
Javascript中拼接大量字符串的方法
Feb 05 Javascript
jQuery实现点击关注和取消功能
Jul 03 jQuery
AngularJS中下拉框的基本用法示例
Oct 11 Javascript
vue完成项目后,打包成静态文件的方法
Sep 03 Javascript
vue 实现小程序或商品秒杀倒计时
Apr 14 Javascript
微信小程序基于canvas渐变实现的彩虹效果示例
May 03 Javascript
vue实现员工信息录入功能
Jun 11 Javascript
JavaScript动态生成表格的示例
Nov 02 Javascript
vue编写简单的购物车功能
Jan 08 Vue.js
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
在项目中寻找代码的坏命名
2012/07/14 PHP
浅谈php7的重大新特性
2015/10/23 PHP
yii,CI,yaf框架+smarty模板使用方法
2015/12/29 PHP
php blowfish加密解密算法
2016/07/02 PHP
PHP实现的自定义数组排序函数与排序类示例
2016/11/18 PHP
PHP命令Command模式用法实例分析
2018/08/08 PHP
php判断某个方法是否存在函数function_exists (),method_exists()与is_callable()区别与用法解析
2020/04/20 PHP
jQuery实战之品牌展示列表效果
2011/04/10 Javascript
关于jQuery判断元素是否存在的问题示例探讨
2014/07/21 Javascript
JQuery EasyUI 加载两次url的原因分析及解决方案
2014/08/18 Javascript
Zero Clipboard实现浏览器复制到剪贴板的方法(多个复制按钮)
2016/03/24 Javascript
js实现图片缓慢放大缩小效果
2016/08/02 Javascript
jQuery Validate插件自定义验证规则的方法
2016/12/27 Javascript
基于vue 实现token验证的实例代码
2017/12/14 Javascript
nodejs基于WS模块实现WebSocket聊天功能的方法
2018/01/12 NodeJs
VueJs组件之父子通讯的方式
2018/05/06 Javascript
vue-router之nuxt动态路由设置的两种方法小结
2018/09/26 Javascript
Vue 解决多级动态面包屑导航的问题
2019/11/04 Javascript
python实现数独算法实例
2015/06/09 Python
python 通过xml获取测试节点和属性的实例
2018/03/31 Python
详解DeBug Python神级工具PySnooper
2019/07/03 Python
python word转pdf代码实例
2019/08/16 Python
python爬虫请求头设置代码
2020/07/28 Python
css实例教程 一款纯css3实现的超炫动画背画特效
2014/11/05 HTML / CSS
荷兰领先的百货商店:De Bijenkorf
2018/10/17 全球购物
摄影助理岗位职责
2014/02/07 职场文书
《美丽的公鸡》教学反思
2014/02/25 职场文书
文秘求职信范文
2014/04/10 职场文书
求职信内容怎么写
2014/05/26 职场文书
公务员培的训心得体会
2014/09/01 职场文书
2014年社区妇联工作总结
2014/12/02 职场文书
2014年教研员工作总结
2014/12/23 职场文书
银行培训心得体会范文
2016/01/09 职场文书
iPhone13 Pro外观确定,升级4800万镜头,4月20日发新品
2021/04/15 数码科技
Nginx的gzip相关介绍
2022/05/11 Servers
Nginx配置使用详解
2022/07/07 Servers