Recently, I was looking for something to practice and found it interesting to recreate some small games, so I made a web version of minesweeper.

Click here to see the final look.

Create an

The project uses the form monorePO to store the code. In Angular, the monorepo method is built as follows:

ng new simple-game --createApplication=false 
ng generate application mine-sweeper

Copy the code

In this case, since the project will include a variety of other applications in the future, I feel that using Monorepo to build the project is the right choice. If you don’t want to use monorepo, use the following command to create the application:

ng new mine-sweeper
Copy the code

The flow chart

First, let’s look at the basic process of mine clearance.

Data structure abstraction

By observing the flow chart, it can be seen that mine clearance basically has the following states:

  • start
  • In the game
  • victory
  • failure

The state of the block is as follows:

  • It has thunder and no thunder, depending on its initial setup;
  • If there are no mines, then it needs to show the number of mines nearby;
  • Whether it has been opened;

We can define these states and then, depending on the state, execute different logic and feed back to the component.

// model.ts

export enum GameState {
    BEGINNING = 0x00,
    PLAYING = 0x01,
    WIN = 0x02,
    LOST = 0x03,}export interface IMineBlock {
    // Whether the current block is inside a mine
    readonly isMine: boolean;
    // The number of mines in the vicinity
    readonly nearestMinesCount: number;
    // Whether it has been opened
    readonly isFound: boolean;
}
Copy the code

Write logic

In order for the minesweeper logic not to be coupled to the component, we need to add a new service.

ng generate service mine-sweeper
Copy the code

Now start writing the logic. First, store the game state, the mine block, the side length of the mine block (currently minesweeper is designed to be a square), and the number of mines.

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; }

    / /...
}

Copy the code

Thanks to Rxjs, using BehaviorSubject makes it easy to design these state variables to be responsive. The BehaviorSubject provides a responsive object for logical services to change data, and for components to listen for data changes.

With the preparation above, we can start writing the logical functions start and doNext. Start is used to reset the state of the state machine. DoNext, on the other hand, does state transitions based on the index of the squares the player clicks on.

export 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;
    }
    
    // ...
}
Copy the code

The above code contains the prepare, testIsMine, and victoryVerify functions, all of which perform logical operations.

Let’s start with Prepare because he’s the first one to run. The main logic of this function is to generate a mine from a random number and ensure that the first time the user clicks on the mine block, the mine will not appear. Along with annotations, we analyze how it works line by line.

export class MineSweeperService {
    private prepare(index: number) {
        const blocks = [...this._mineBlocks.value];
        // Determine if index is out of bounds
        if(! blocks[index]) {throw Error('Out of index.');
        }
        // Set the block at the index position to open.
        blocks[index] = { isMine: false, isFound: true, nearestMinesCount: 0 };

        // Generate an array of random numbers that do not contain index.
        const numbers = this.generateRandomNumbers(this.mineCount, this.mineBlocks.length, index);
        // Set the specified block to thunder through the random number array.
        for (const num of numbers) {
            blocks[num] = { isMine: true, isFound: false, nearestMinesCount: 0 };
        }

        // Walk through all mine blocks using horizontal and vertical coordinates
        // This allows us to detect the number of mines near the current block directly by increasing or decreasing the sitting target.
        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 the current block is thunder, no detection is performed
                if (block.isMine) {
                    continue;
                }

                // Check the number of mines near the mine block, like this
                // 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)]); }}// Update the number of mines in the vicinity
                blocks[index] = { ...block, nearestMinesCount };
            }
        }

        // If the number of mines near the clicking position is 0, we need to traverse all nearby blocks until the number of mines near all open blocks is not zero.
        if (blocks[index].nearestMinesCount === 0) {
            this.cleanZeroCountBlock(blocks, index, this.transformToIndex(this.side));
        }

        // Trigger the update
        this._mineBlocks.next(blocks); }}Copy the code

To see the testIsMine, it returns a Boolean value indicating whether the block you are clicking on is a mine.

private testIsMine(index: number) :boolean {
    const blocks = [...this._mineBlocks.value];
    if(! blocks[index]) {throw Error('Out of index.');
    }

    // The current block is set to open
    consttheBlock = { ... blocks[index], isFound:true };
    blocks[index] = theBlock;

    // If the current block is a mine, find all the mine blocks that are mines and set their state to open.
    // Or if the number of mines near the click position is 0, we need to traverse all nearby blocks until the number of mines near all open blocks is not zero.
    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);
    }

    // Trigger the update
    this._mineBlocks.next(blocks);

    // return the result
    return theBlock.isMine;
}
Copy the code

VictoryVerify, of course, is a victory check: a user wins when the number of unopened blocks equals the number of mines specified.

    private vitoryVerify() {
        // Perform a Reduce lookup on the current mine block array.
        return this.mineBlocks.reduce((prev, current) = > {
            return! current.isMine && current.isFound ? prev +1 : prev;
        }, 0) = = =this.mineBlocks.length - this.mineCount;
    }
Copy the code

Now that we’ve covered these three functions, let’s look at how cleanZeroCountBlock works. Its purpose is to open all zeros in the vicinity of the current block.

private cleanZeroCountBlock(blocks: IMineBlock[], index: number) {
    const i = index % this.side;
    const j = Math.floor(index / this.side);
    // Check 8 blocks near it
    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];
            // It is not the original block, and the block exists, is not opened, and is not a mine
            if(currentIndex === index || ! block || block.isFound || block.isMine) {continue;
            }
            // Set it to openblocks[currentIndex] = { ... block, isFound:true };

            // recursive query
            if (blocks[currentIndex].nearestMinesCount === 0) {
                this.cleanZeroCountBlock(blocks, currentIndex); }}}}Copy the code

At this point, we have almost written the concrete logic of mine clearance. Other related functions, you can refer to the source code, do not repeat.

To realize the page

At this point, most of the work is done. We write components based on responsive objects, add click events to dom objects, trigger the associated logic functions, and then do all the error handling and so on. The page code is not posted here, you can view the source code on Github.

Source code and references

Finally, if there is any bad writing or mistakes, you are welcome to make criticism and suggestions for revision. Thank you for reading.

Mime Sweeper source

Angular Official documentation

Rxjs official documentation