Hi, I’m The Bold Tomato. This is the summary and sharing of minesweeper, the second game of “Challenge one game a month”.

Demo address: wanghaida.com/demo/2202-m…

Github Repository: github.com/wanghaida/g…

In order for a game to be fun, in addition to gameplay, design is also necessary. But what if I can’t design? So download the Microsoft Minesweeper from the Microsoft Store and go through the directory, and you find this:

Ho, isn’t this the Sprite we’re familiar with? Not all of them, but the basics. Love love ~

The map

Each square in Sprite is exactly 84×84 pixels, divided by 2 for hd, 42×42 for each square. The game is available in 9×9, 16×16, and 30×16 difficulty levels. Based on 30×16, the size of the game area reaches 1260×672, including some small spacing, the small screen is slightly difficult, so in addition to 9×9 using 84/2 = 42 pixels, Others use 84/2.4 = 35 pixels.

Simply set up the shelves with a grid:

:root {
    --base: 2;

    --row: 9;
    --col: 9;
}

/ / play area
#game {
    display: grid;
    grid-template-rows: repeat(var(--row), calc(84px / var(--base)));
    grid-template-columns: repeat(var(--col), calc(84px / var(--base)));
    gap: 2px;

    div {
        background: url('./images/sprite_state.png') calc(-84px / var(--base)) 0 / calc(1344px / var(--base)) calc(420px/ var(--base)) no-repeat; }}Copy the code
// The game area
const oGame = document.getElementById('game');

const minesweeper = {
    /** * Initialize the map */
    initMap(row = 9, col = 9, mines = 10) {
        // Clear the original map
        oGame.innerHTML = ' ';

        // Adjust the map layout
        document.documentElement.style.setProperty('--base', row > 9 ? '2.4' : '2');

        document.documentElement.style.setProperty('--row', row);
        document.documentElement.style.setProperty('--col', col);

        // Virtual nodes are used to host DOM nodes, which can be added at once
        const oFragment = document.createDocumentFragment();

        for (let i = 0; i < row; i++) {
            for (let j = 0; j < col; j++) {
                // Create a coordinate node
                const oDiv = document.createElement('div');

                // Put coordinate nodes into virtual nodesoFragment.appendChild(oDiv); }}// Put the virtual node into the game areaoGame.appendChild(oFragment); }};// Start 9x9 games by default
minesweeper.initMap();
Copy the code

The following graph is obtained:

CSS uses three variables –base to scale each square, –row to represent rows, and –col to represent columns. The familiar grid system uses the repeat() function to draw trips and columns.

I’m going to add a keyframe animation and timer:

#game {
    .state-loading {
        background-position: calc(-84px / var(--base)) 0;
        animation: state-loading 0.2 s steps(1);
    }
    .state-closed {
        background-position: calc(-84px / var(--base)) 0; }}@keyframes state-loading {
    0% {
        background-position: calc(-504px / var(--base)) calc(-84px / var(--base));
    }
    25% {
        background-position: calc(-672px / var(--base)) calc(-84px / var(--base));
    }
    50% {
        background-position: calc(-840px / var(--base)) calc(-84px / var(--base));
    }
    75% {
        background-position: calc(-924px / var(--base)) calc(-84px / var(--base));
    }
    100% {
        background-position: calc(-1260px / var(--base)) calc(-84px/ var(--base)); }}Copy the code
const minesweeper = {
    mapCount: 0.mapTimer: null.initMap(row = 9, col = 9, mines = 10){...document.documentElement.style.setProperty('--col', col);
        
        // Virtual nodes are used to host DOM nodes, which can be added at once
        const oFragment = document.createDocumentFragment();

        for (let i = 0; i < row; i++) {
            for (let j = 0; j < col; j++) {
                // Create a coordinate node
                const oDiv = document.createElement('div');

                // Put coordinate nodes into virtual nodesoFragment.appendChild(oDiv); }}// Put the virtual node into the game area
        oGame.appendChild(oFragment);

        // Load the animation
        clearInterval(this.mapTimer);
        this.mapCount = 0;
        this.mapTimer = setInterval(() = > {
            // All lines have been traversed
            if (this.mapCount >= row) {
                // Clear the timer
                return clearInterval(this.mapTimer);
            }

            for (let i = 0; i < col; i++) {
                oDiv = oGame.children[this.mapCount * col + i];

                // Load the animation style
                oDiv.className = 'state-loading';
                oDiv.addEventListener('animationend'.function fn() {
                    oDiv.className = 'state-closed'; 
                    oDiv.removeEventListener('animationend', fn);
                });
            }

            // Increase the number of rows traversed
            this.mapCount++;
        }, 100); }};Copy the code

Theoretically, there is no problem, but maybe because of DOM, when the number of blocks is too many, there will be an obvious delay in loading from left to right, so I changed from adding divs into the game area first and then adding styles to each line to adding styles at the same time:

const minesweeper = {
    mapCount: 0.mapTimer: null.initMap(row = 9, col = 9, mines = 10){...document.documentElement.style.setProperty('--col', col);

        // Load the animation
        clearInterval(this.mapTimer);
        this.mapCount = 0;
        this.mapTimer = setInterval(() = > {
            // All lines have been traversed
            if (this.mapCount >= row) {
                // Clear the timer
                return clearInterval(this.mapTimer);
            }

            // Virtual nodes are used to host DOM nodes, which can be added at once
            const oFragment = document.createDocumentFragment();

            for (let i = 0; i < col; i++) {
                // Create a coordinate node
                const oDiv = document.createElement('div');

                // Load the animation style
                oDiv.className = 'state-loading';
                oDiv.addEventListener('animationend'.function fn() {
                    oDiv.className = 'state-closed';
                    oDiv.removeEventListener('animationend', fn);
                });

                // Put coordinate nodes into virtual nodes
                oFragment.appendChild(oDiv);
            }

            // Put the virtual node into the game area
            oGame.appendChild(oFragment);

            // Increase the number of rows traversed
            this.mapCount++;
        }, 100); }};Copy the code

For the convenience of manipulating squares, and for a slightly faster performance, let’s use a map to store the corresponding states of squares. Dom only stores the corresponding coordinates of data:

const minesweeper = {
    /** * Game data **@desc Represent the properties * of each square by a two-dimensional array@example* [* [item, item, item], * [item, item, item], * [item, item, item], *] * * item = {* // Whether to open * isOpen: Boolean, * // explode: Boolean, * // explode: Boolean * // explode: Boolean, * // explode: Boolean, */ / flag normal question * sign: string, */ / Type 0: blank, 1-8: number, 9: mine * type: number, *} */
    map: []./** ** game status ** loaded: finished, loading: ongoing, over: ongoing */
    state: 'loading'./** * Initialize the map */
    row: 9.col: 9.mines: 10.initMap(row = 9, col = 9, mines = 10) {
        // Game map size
        this.map = [];
        this.row = row;
        this.col = col;
        this.mines = row * col === 256 ? 40 : row * col === 480 ? 99 : mines; // 16x16 ? 40 : 30x16 ? 99 : mines;

        // Change the game state
        this.state = 'loading';
        // Number of landmines
        document.getElementById('mines').innerHTML = this.mines;
        // Game time (simple timer, not shown here).this.mapTimer = setInterval(() = > {
            // All lines have been traversed
            if (this.mapCount >= row) {
                // Status changes
                this.state = 'loaded';
                // Clear the timer
                return clearInterval(this.mapTimer);
            }

            // Virtual nodes are used to host DOM nodes, which can be added at once
            const oFragment = document.createDocumentFragment();

            const mapTemp = [];
            for (let i = 0; i < col; i++) {
                // Create a coordinate node
                const oDiv = document.createElement('div'); ./ / coordinates
                oDiv.pos = [this.mapCount, i];
                / / square
                mapTemp.push({
                    // Check whether it has been opened
                    isOpen: false.// Do I recurse
                    isCheck: false.// Whether there was an explosion
                    isExplode: false.// flag normal question
                    sign: 'normal'.// Type 0: blank, 1-8: numbers, 9: mines
                    type: 0});// Put coordinate nodes into virtual nodes
                oFragment.appendChild(oDiv);
            }
            this.map.push(mapTemp);

            // Put the virtual node into the game area
            oGame.appendChild(oFragment);

            // Increase the number of rows traversed
            this.mapCount++;
        }, 100); }};Copy the code

Generally ok:

Based on dynamic effect

Add the basic Spaces, numbers, flags and question marks:

.state-flag-down {
    background-position: calc(-1260px / var(--base)) calc(-168px / var(--base));
    animation: state-flag-down 0.1 s steps(1);
}
.state-flag-up {
    background-position: calc(-252px / var(--base)) calc(-252px / var(--base));
    animation: state-flag-up 0.1 s steps(1);
}
.state-normal-down {
    background-position: calc(-588px / var(--base)) calc(-168px / var(--base));
    animation: state-normal-down 0.1 s steps(1);
}
.state-normal-up {
    background-position: calc(-924px / var(--base)) calc(-168px / var(--base));
    animation: state-normal-up 0.1 s steps(1);
}
.state-question-down {
    background-position: calc(-588px / var(--base)) calc(-252px / var(--base));
    animation: state-question-down 0.1 s steps(1);
}
.state-question-up {
    background-position: calc(-924px / var(--base)) calc(-252px / var(--base));
    animation: state-question-up 0.1 s steps(1);
}

.state-0 {
    background-position: 0 0;
}
.state-1 {
    background-position: calc(-504px / var(--base)) 0;
}
.state-2 {
    background-position: calc(-588px / var(--base)) 0;
}
.state-3 {
    background-position: calc(-672px / var(--base)) 0;
}
.state-4 {
    background-position: calc(-756px / var(--base)) 0;
}
.state-5 {
    background-position: calc(-840px / var(--base)) 0;
}
.state-6 {
    background-position: calc(-924px / var(--base)) 0;
}
.state-7 {
    background-position: calc(-1008px / var(--base)) 0;
}
.state-8 {
    background-position: calc(-1092px / var(--base)) 0;
}
.state-9 {
    background-position: calc(-336px / var(--base)) 0;
}

@keyframes state-flag-down {
    0% {
        background-position: calc(-1008px / var(--base)) calc(-168px / var(--base));
    }
    3333.% {
        background-position: calc(-1092px / var(--base)) calc(-168px / var(--base));
    }
    6666.% {
        background-position: calc(-1176px / var(--base)) calc(-168px / var(--base));
    }
    100% {
        background-position: calc(-1260px / var(--base)) calc(-168px/ var(--base)); }}@keyframes state-flag-up {
    0% {
        background-position: 0 calc(-252px / var(--base));
    }
    3333.% {
        background-position: calc(-84px / var(--base)) calc(-252px / var(--base));
    }
    6666.% {
        background-position: calc(-168px / var(--base)) calc(-252px / var(--base));
    }
    100% {
        background-position: calc(-252px / var(--base)) calc(-252px/ var(--base)); }}@keyframes state-normal-down {
    0% {
        background-position: calc(-336px / var(--base)) calc(-168px / var(--base));
    }
    3333.% {
        background-position: calc(-420px / var(--base)) calc(-168px / var(--base));
    }
    6666.% {
        background-position: calc(-504px / var(--base)) calc(-168px / var(--base));
    }
    100% {
        background-position: calc(-588px / var(--base)) calc(-168px/ var(--base)); }}@keyframes state-normal-up {
    0% {
        background-position: calc(-672px / var(--base)) calc(-168px / var(--base));
    }
    3333.% {
        background-position: calc(-756px / var(--base)) calc(-168px / var(--base));
    }
    6666.% {
        background-position: calc(-840px / var(--base)) calc(-168px / var(--base));
    }
    100% {
        background-position: calc(-924px / var(--base)) calc(-168px/ var(--base)); }}@keyframes state-question-down {
    0% {
        background-position: calc(-336px / var(--base)) calc(-252px / var(--base));
    }
    3333.% {
        background-position: calc(-420px / var(--base)) calc(-252px / var(--base));
    }
    6666.% {
        background-position: calc(-504px / var(--base)) calc(-252px / var(--base));
    }
    100% {
        background-position: calc(-588px / var(--base)) calc(-252px/ var(--base)); }}@keyframes state-question-up {
    0% {
        background-position: calc(-672px / var(--base)) calc(-252px / var(--base));
    }
    3333.% {
        background-position: calc(-756px / var(--base)) calc(-252px / var(--base));
    }
    6666.% {
        background-position: calc(-840px / var(--base)) calc(-252px / var(--base));
    }
    100% {
        background-position: calc(-924px / var(--base)) calc(-252px/ var(--base)); }}Copy the code

The effect is as follows:

Randomly generated mine

Random generation of mines is relatively simple, generate N non-repeating numbers in 0-row * col, and then convert them into two-dimensional coordinates:

const minesweeper = {
    initMap(row = 9, col = 9, mines = 10){...this.mapTimer = setInterval(() = > {
            // All lines have been traversed
            if (this.mapCount >= row) {
                // Status changes
                this.state = 'loaded';
                // Generate mines
                this.generateMines();
                // Clear the timer
                return clearInterval(this.mapTimer); }},100);
    },
    /** * Generates a mine */
    generateMines() {
        // Enter N non-repeating numbers
        let pos = [];
        while(pos.length ! = =this.mines) {
            pos = [...new Set([...pos, Math.floor(Math.random() * this.row * this.col)])];
        }

        // Convert a number to a coordinate array
        for (let i = 0; i < pos.length; i++) {
            const x = Math.floor(pos[i] / this.col);
            const y = pos[i] % this.col;

            pos[i] = [x, y];
            // Change the corresponding data type to 9 (mine)
            this.map[x][y].type = 9;
        }

        // Calculate the coordinates around the mine
        for (let i = 0; i < pos.length; i++) {
            // Find the surrounding coordinates
            const around = this.findPos(pos[i]);
            for (let j = 0; j < around.length; j++) {
                const grid = this.map[around[j][0]][around[j][1]].// If it is not a mine, add 1
                if(grid.type ! = =9) { grid.type++; }}}},};Copy the code

Find the surrounding coordinates

The findPos method is used to return the surrounding coordinates:

const minesweeper = {
    /** * find the surrounding coordinates and remove the boundary value **@example* Assuming the coordinates are [x, y], then the surrounding coordinates are:  * [ * [x - 1, y - 1], [x - 1, y], [x - 1, y + 1], * [x, y - 1], ..., [x, y + 1], * [x + 1, y - 1], [x + 1, y], [x + 1, y + 1], * ] */
    findPos([x, y]) {
        // Ambient coordinates
        const pos = [
            [x - 1, y - 1], [x - 1, y], [x - 1, y + 1],
            [x,     y - 1],             [x,     y + 1],
            [x + 1, y - 1], [x + 1, y], [x + 1, y + 1]];// Simple collision detection removes boundary values
        return pos.filter(([x, y]) = >! (x <0 || y < 0 || x >= this.row || y >= this.col));
    },
    findPosUDLR([x, y]) {
        // Ambient coordinates
        const pos = [
            [x - 1, y], / /
            [x + 1, y], / /
            [x, y - 1]./ / left
            [x, y + 1]./ / right
        ];
        // Simple collision detection removes boundary values
        return pos.filter(([x, y]) = >! (x <0 || y < 0 || x >= this.row || y >= this.col)); }};Copy the code

FindPosUDLR, on the other hand, is used to return about 4 Spaces, mainly for the explosion effect after the game is over, so as not to look a little dull.

Game Logic analysis

Mouse events are down, up, move, and double click.

Why not combine press and lift as click events here? Because the mouse is pressed in a square left button does not let go, move out of the current square needs to be restored, mainly to prevent the point of error. And right click and then lift, need to mark transformation (normal -> flag -> question mark -> normal), so can not be simply processed into a click event, but also have to cache the pressed box, after the move, lift to determine or not the original box.

As for the double – click event, mainly exists in the double – click number to quickly open the unmarked box operation.

The mouse click

// Block cache
let oTemp = null;

// Mouse down from the block
oGame.addEventListener('mousedown'.(ev) = > {
    // The game is loaded/the game is over
    if (oGame === ev.target || ['loading'.'over'].includes(minesweeper.state)) return;

    const [x, y] = ev.target.pos;
    if (false === minesweeper.map[x][y].isOpen) {
        // Cache presses the element
        oTemp = ev.target;
        // Add a press style to the cached element
        oTemp.className = 'state-' + minesweeper.map[x][y].sign + '-down'; }});Copy the code

If there are no squares, or the game is loading/the game is over, the logic is not processed. Loading is completed by clicking on the initMap timer, and then loading the div.state-closed with mapCount.

First determine that the current square is not open, and then cache the currently clicked square, add a press effect to the square.

The mouse moves

// Mouse movement
oGame.addEventListener('mousemove'.(ev) = > {
    // The cached oTemp is inconsistent with the current element
    if(oTemp && oTemp ! == ev.target) {// If the cached element is a press style
        if (oTemp.className.match(/state\-.+\-down/)) {
            // Add lift styles to cached elements
            oTemp.className = oTemp.className.replace('-down'.'-up');
        }
        // Delete cached elements
        oTemp = null; }});Copy the code

To move is to determine that the cached DOM exists and is not equal to the current element, and that it has a pushdown style, so the pushdown style changes to lift.

The mouse is raised

// Mouse up
oGame.addEventListener('mouseup'.(ev) = > {
    // The cached oTemp is inconsistent with the current element
    if(oTemp ! == ev.target) {// Delete cached elements
        oTemp = null;
        return;
    }

    const [x, y] = ev.target.pos;

    / / click
    if (ev.button === 0) {
        // No markup
        if (minesweeper.map[x][y].sign === 'normal') {
            // Handle the click event
            minesweeper.handleClick(oTemp);
        } else {
            // Add lift styles to cached elements
            oTemp.className = 'state-' + minesweeper.map[x][y].sign + '-up'; }}// Right click and do not open
    if (ev.button === 2) {
        // Change the state of the tag
        minesweeper.map[x][y].sign = {
            flag: 'question'.normal: 'flag'.question: 'normal',
        }[minesweeper.map[x][y].sign];

        // Number of landmines
        if (minesweeper.map[x][y].sign === 'flag') {
            minesweeper.mines -= 1;
        }
        if (minesweeper.map[x][y].sign === 'question') {
            minesweeper.mines += 1;
        }
        document.getElementById('mines').innerHTML = minesweeper.mines;

        // Add lift styles to cached elements
        oTemp.className = 'state-' + minesweeper.map[x][y].sign + '-up';
    }

    // Delete cached elements
    oTemp = null;
});
Copy the code

If the click event is marked as Normal, the event is logically processed. If the event is flag or question, the event is added and no processing is performed.

Right-click on the event to mark the change of state of the box and add a lift style. If it turns into a flag, subtract one from the number of mines, and if it turns into something else, add one.

Mouse click

/ / double
oGame.addEventListener('dblclick'.(ev) = > {
    // Do not click the center box
    if (oGame === ev.target) return;

    const [x, y] = ev.target.pos;
    const grid = minesweeper.map[x][y];

    // Open and a number
    if (grid.isOpen && grid.type > 0 && grid.type < 9) {
        minesweeper.handleNumber([x, y], grid.type);

        // Judge game winsminesweeper.judgeVictory(); }});Copy the code

Game logic processing

Here we look at the handleClick and handleNumber methods in detail.

const minesweeper = {
    /** * handle the click event */
    handleClick(dom) {
        // Change the status
        if (this.state ! = ='ongoing') {
            this.state = 'ongoing';
            this.startTime = +new Date(a);this.startInterval();
        }

        const grid = this.map[dom.pos[0]][dom.pos[1]].// Change the open state
        grid.isOpen = true;
        // Modify the recursive state
        grid.isCheck = true;
        // Modify the block style
        dom.className = 'state-' + grid.type;

        // Handle blank squares
        if (grid.type === 0) {
            this.handleSpace(dom.pos);
        }
        // Handle mine blocks
        else if (grid.type === 9) {
            this.handleMines([dom.pos]);
        }
        // // Handle number blocks (here changed to double click trigger)
        // else if (grid.type > 0 && grid.type < 9) {
        // this.handleNumber(dom.pos, grid.type);
        // }

        // Judge game wins
        this.judgeVictory(); }};Copy the code

The first step is to determine the state. If it is not currently in the game state, modify the state and turn on the timer.

Once you have the grid data, change its open state, recursive state, and block style.

Working with blank squares

If it is a blank square, then in addition to its own changes, it expands outward to show all blank squares and adjacent numeric squares.

After clicking the red dot, judge all the squares in the red box, skip the number (1/2/3/9), and recurse the blank (4/6/7).

const minesweeper = {
    /** * handles blank squares */
    handleSpace(pos) {
        // Find the surrounding coordinates
        const around = this.findPos(pos);
        for (let i = 0; i < around.length; i++) {
            / / coordinates
            const [x, y] = around[i];
            // correspond to square
            const grid = this.map[x][y];

            // Not recursive and marked normal
            if (false === grid.isCheck && 'normal' === grid.sign) {
                // Change the open state
                grid.isOpen = true;
                // Modify the recursive state
                grid.isCheck = true;

                // Load the animation style
                const oDiv = oGame.children[x * this.col + y];
                oDiv.className = 'state-' + grid.sign + '-down';
                oDiv.addEventListener('animationend'.function fn() {
                    oDiv.className = 'state-' + grid.type;
                    oDiv.removeEventListener('animationend', fn);
                });

                // If it is a number, skip it
                if (grid.type > 0 && grid.type < 9) {
                    continue;
                }
                // If it is blank, recurse
                if (grid.type === 0) {
                    this.handleSpace(around[i]); }}}},};Copy the code

Dealing with mine blocks

const minesweeper = {
    /** * Handle mine blocks */
    handleMines(pos) {
        // Change the status
        this.state = 'over';
        oGame.className = 'fail';
        // Clear time
        clearInterval(this.startTimer);

        // Flag all mines and false flags
        for (let i = 0; i < this.map.length; i++) {
            for (let j = 0; j < this.map[i].length; j++) {
                // It is a mine and not a flag
                if (this.map[i][j].type === 9 && this.map[i][j].sign ! = ='flag') {
                    oGame.children[i * this.col + j].className = 'state-9';
                }
                // It is not a mine but a flag
                if (this.map[i][j].type ! = =9 && this.map[i][j].sign === 'flag') {
                    oGame.children[i * this.col + j].className = 'state-flag-error'; }}}for (let i = 0; i < pos.length; i++) {
            / / coordinates
            const [x, y] = pos[i];
            // Current box
            const grid = this.map[x][y];

            // Change the open state
            grid.isOpen = true;
            // Change the explosion state
            grid.isExplode = true;

            // Load the animation style
            const oDiv = oGame.children[x * this.col + y];
            oDiv.className = 'state-over';
            oDiv.addEventListener('animationend'.function fn() {
                // End of game animation
                minesweeper.explodeMines(pos[i]);
                oDiv.removeEventListener('animationend', fn); }); }}};Copy the code

If you touch a mine, it’s over. Mark all unflagged mines and all wrong flags first. This method passes an array of coordinates, and since double clicking on the number triggers the possibility of guessing multiple mines wrong, it cycles through the positions, adding open, exploding states to the mines, and adding an explosion animation.

There’s no explosion animation in Sprite, so where did you get the material? MSN has a canvas version of the game 😉

I saved the material myself and photoshopped a Sprite:

Execute the end of game animation explodeMines method after the explosion animation ends:

const minesweeper = {
    /** * Handle mine explosions */
    explodeMines(pos) {
        setTimeout(() = > {
            // Find the surrounding coordinates
            const around = this.findPosUDLR(pos);
            for (let i = 0; i < around.length; i++) {
                / / coordinates
                const [x, y] = around[i];
                // correspond to square
                const grid = this.map[x][y];

                // There is no explosion
                if (grid.isExplode === false&& grid.sign ! = ='flag') {
                    // Change the explosion state
                    grid.isExplode = true;

                    const oDiv = oGame.children[x * this.col + y];

                    // If it is a mine
                    if (grid.type === 9) {
                        // Change the open state
                        grid.isOpen = true;
                        // Load the animation style
                        oDiv.className = 'state-over';
                    }
                    // If it is not opened
                    else if(! grid.isOpen) {// Load the animation style
                        oDiv.className = 'state-explode';
                        oDiv.addEventListener('animationend'.function fn() {
                            oDiv.className = 'state-closed';
                            oDiv.removeEventListener('animationend', fn);
                        });
                    }

                    // Explosion recursion
                    this.explodeMines(around[i]); }}},100); }};Copy the code

FindPosUDLR is used to carry out diamond diffusion, turn red when meeting the normal mark, execute the explosion effect when meeting the mine, and recursively complete the traversal of all squares.

The stupid findPos method:

Working with number blocks

Remember the number squares in double click? Originally I also put it in handleClick, but it is obviously a bug when it is opened.

What does the digital module deal with? Let’s start with the code:

const minesweeper = {
    /** ** handles number blocks */
    handleNumber(pos, type) {
        // Find the surrounding coordinates
        const around = this.findPos(pos);

        If flag >= number (type), the remaining normal flags can be clicked
        let flag = 0;
        // flag box
        const flags = [];
        // Mine cube
        const mines = [];

        for (let i = 0; i < around.length; i++) {
            / / coordinates
            const [x, y] = around[i];
            // correspond to square
            const grid = this.map[x][y];

            / / the flag
            if (grid.sign === 'flag') { flag++; flags.push({ ... grid,pos: around[i] });
            }
            / / mines
            if (grid.type === 9) { mines.push({ ... grid,pos: around[i] }); }}if (flag >= type) {
            // Check whether the tags are correct, because it is a sequential push, so it is easy to rotate the string and compare
            if (JSON.stringify(flags) === JSON.stringify(mines)) {
                this.handleSpace(pos);
            }
            // Mark error
            else {
                // Handle error flags
                for (let i = 0; i < flags.length; i++) {
                    if (flags[i].type === 9) continue;

                    oGame.children[flags[i].pos[0] * this.col + flags[i].pos[1]].className = 'state-flag-error';
                }
                // Handle the wrong mine
                this.handleMines(mines.filter((item) = >item.sign ! = ='flag').map((item) = >item.pos)); }}}};Copy the code

First, get the coordinates of the number pos and the content type. In fact, it is ok to get the content from the map through POS, but I found it outside, so I easily passed it in.

Cycle to find the number of tags. Only when flag >= number type can the remaining normal tags be clicked.

First, check whether the marks are correct. Because they are all in a sequence of push, we can simply turn the string and compare. If the comparison is successful, the current number square will be treated as a blank square.

The wrong flag will be displayed, and the wrong mine coordinates will be thrown into the logic that deals with mine blocks.

Judge game wins

The condition for victory is the number of unopened blocks === the number of flags + the number of mines, so both clicking on a block and double-clicking on a number block should determine victory in the game:

const minesweeper = {
    /**
     * 判断游戏胜利
     *
     * 未打开的方块 === 旗子的数量 + 地雷的数量
     */
    judgeVictory() {
        let count = 0;
        let flags = 0;

        for (let i = 0; i < this.map.length; i++) {
            for (let j = 0; j < this.map[i].length; j++) {
                // Open the box
                if (this.map[i][j].isOpen === false) {
                    count++;
                }
                // The number of flags
                if (this.map[i][j].sign === 'flag') { flags++; }}}if (count === flags + this.mines) {
            // Change the status
            this.state = 'over';
            oGame.className = 'success';
            // Clear time
            clearInterval(this.startTimer); }}};Copy the code

At the end

In fact, there are hundreds of millions of details I feel simple not shown in the article, interested can go to Github to look at the source code, compared to see a better understanding.

Including the garden theme and all the audio is actually in the game folder, you can add it yourself. Here’s another garden-themed Sprite:

The method is the same, write more keyframes for CSS.

The above.