小编这次要给大家分享的是Angular如何实现一个扫雷游戏,文章内容丰富,感兴趣的小伙伴可以来了解一下,希望大家阅读完这篇文章之后能够有所收获。
创建应用
该项目使用的是 monorepo 的形式来存放代码。在 Angular 中,构建 monorepo 方法如下:
ng new simple-game --createApplication=false
ng generate application mine-sweeper
在这里,因为该项目以后还会包含其他各种其他的应用,所以个人觉得使用 monorepo 构建项目是比较正确的选择。如果不想使用 monorepo,使用以下命令创建应用:
ng new mine-sweeper
流程图
首先,我们先来看看扫雷的基本流程。
数据结构抽象
通过观察流程图,可以得到扫雷基本上有这么几种状态:
方块的状态如下:
我们可以先定义好这些状态,之后根据不同的状态,执行不同的逻辑,同时反馈给组件。
// 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
主要功能是提供了一个响应式的对象,使得逻辑服务可以通过这个对象对数据进行变更,并且,组件也可以通过这些对象来监听数据变化。
通过上面的准备工作,我们可以开始编写逻辑函数 start
和 doNext
。 start
的作用是给状态机重新设置状态;而 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);
}
}
}
}
看完这篇关于Angular如何实现一个扫雷游戏的文章,如果觉得文章内容写得不错的话,可以把它分享出去给更多人看到。
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。