js+canvas+less to realize AI Gobang games_ ☆ the past goes with the wind ☆ blog

1, Foreword

Use js+canvas+less to make a simple AI Gobang game. AI players in the game are very powerful and have the function of repentance chess. Here is a brief introduction to the JS part. For the game UI part, please see the source code.

2, Development process

(1) Draw chessboard

Here, use canvas to draw a 15 x 15 chessboard.

// Draw chessboard
    function chessBoard() {
        for (var i = 0; i < chessWidth; i++) {
            // Draw a checkerboard horizontal segment
            ctx.save();
            ctx.beginPath();
            ctx.moveTo(20, 20 + i * 40);
            ctx.lineTo(580, 20 + i * 40);
            ctx.stroke();
            // ctx.restore();
            // Draw checkerboard vertical segments
            ctx.save();
            ctx.beginPath();
            ctx.moveTo(20 + i * 40, 20);
            ctx.lineTo(20 + i * 40, 580);
            ctx.stroke();
            ctx.restore();
        }
    }

(2) Draw chess pieces

Our chess pieces should be drawn on the intersection of horizontal and vertical lines, and the mouse click capture drawing function should be realized, that is, when the player clicks a range centered on the intersection (as shown in the red area below), the chess pieces can also fall on the intersection of lines.


There are two drawing methods. The first one is to round the coordinates relative to the chessboard after clicking the chessboard with the mouse (because our chessboard interval is 40, divide the coordinates by 40 to round), and then transfer the rounded coordinates to the drawing chessboard function to determine the placement position. The second way is to traverse the array, calculate the coordinates of the current position, and then draw the chess pieces. It is not difficult to see that the first method is more efficient than the second.

// Draw chess pieces
    function drawChess(eventX, eventY, flag) {
        ctx.fillStyle = flag ? "#000" : "#fff";
        ctx.beginPath();
        ctx.arc(20 + eventX * 40, 20 + eventY * 40, 10, 0, 360 * Math.PI / 180, true);
        ctx.fill();
        // Focus the chess pieces on the intersection of lines (mode 2, low performance)
        // var wrap = document.querySelector(".wrapper");
        // eventX = event.clientX - wrap.offsetLeft;
        // eventY = event.clientY - wrap.offsetTop;
        // for (var i = 0; i < chessWidth; i++) {
        //     for (var j = 0; j < chessWidth; j++) {
        //         if (eventX >= (20 + j * 40 - 20) && eventX <= (20 + j * 40 + 20) && eventY >= (20 + i * 40 - 20) && eventY <= (20 + i * 40 + 20)) {
        //             eventX =0 * j + 20;
        //             eventY = 40 * i + 20;
        //             break;
        //         }
        //     }
        // }
        // ctx.beginPath();
        // ctx.arc(eventX, eventY, 10, 0, 360 * Math.PI, true);
        // ctx.fill();
    }

(3) Players play chess

With the chessboard and pieces, you are about to start playing chess. There is no difficulty for players to play chess. What you need to pay attention to is the rounding of the coordinates (eventX,eventY) passed to the drawing chessboard function and the association with the UI.

//Players play chess
chess.addEventListener("click", function (event) {
        event = event || window.event;
        // If it's not the player's turn
        if (isMan == false) {
            alert("Don't worry, it's not your turn yet.");
            return
        }
        // If the game is over
        if (gameOver == true) {
            alert("The game is over, please click restart.");
            return
        }
        // Make the piece fall on the focus of the chessboard line
        eventX = Math.floor(event.offsetX / 40);
        eventY = Math.floor(event.offsetY / 40);
        console.log(eventX);
        // If there is no drop in the current position, you can drop
        if (chessPlace[eventX][eventY] == 0) {
            // Luozi
            drawChess(eventX, eventY, true);
            // Record data
            playerData();
            // Drop sound
            downMp3.play();
            // Can repent
            isRetract = true;
            // Repentance cannot be revoked
            isUnretract = false;
            // You can start over
            isRestart = true;
            // Save the location of the drop
            chessPlace[eventX][eventY] = 1;
            // Judge whether to win or lose
            if (win(eventX, eventY, num = 1)) {
                // game over
                gameOver = true;
                // Record data
                playerData(true);
                // Play winning music
                winMp3.play();
                // The computer can no longer be left behind
                isMan = true;
                // Prompt for another set
                var choice = confirm("You won. Another game?");
                if (choice) {
                    // Empty chessboard
                    clear();
                    // Empty drop steps and scores
                    playerData("", "clearPath");
                    computerData("", "clearPath");
                    gameOver = false;
                }
            }
            // Judge whether there is a draw
            else if (tie()) {
                // game over
                gameOver = true;
                // It's the player's turn to play chess in the next game
                isMan = true;
                // Do you want another round
                var choice = confirm("Draw, another game?");
                if (choice) {
                    // Empty chessboard
                    clear();
                    // Empty drop steps and scores
                    playerData("", "clearPath");
                    computerData("", "clearPath");
                    gameOver = false;
                }
            } else {
                // It's the computer's turn
                isMan = false;
            }
        } else {
            alert("The current position has been occupied, please choose another position");
        }
        if (gameOver == false && isMan == false) {
            // Computer chess
            computerDown();
        }
    });

(4) Computer chess

Computer chess should be the most complex step in the whole game making process. Here I use the five tuple and scorecard algorithm to realize computer AI chess.
1. Quintuple:
The Gobang board has a size of 15 x 15. There are 572 quintuples in all four directions. Each quintuple is given a score (or weight). The score contributed by this quintuple to each position is the score of the quintuple itself. For the whole board, the score of each position is the sum of the scores of all quintuples in the four directions of the position,
Then select the position with the highest score from all empty positions, which is the optimal position of the computer.
2. Score sheet:
Here is a set of the best scorecard given by a foreign leader, which can make AI very smart.

function chessScore(playerNum, computerNum) {
        // Machine attack

        // 1. If there are both human and machine falls, the score is 0
        if (playerNum > 0 && computerNum > 0) {
            return 0;
        }
        // 2. If all pieces are empty and there are no pieces, it will be divided into 7 points
        if (playerNum == 0 && computerNum == 0) {
            return 7;
        }
        // 3. If the machine drops a child, the score is 35
        if (computerNum == 1) {
            return 35;
        }
        // 4. If the machine falls into two pieces, the score is 800
        if (computerNum == 2) {
            return 800;
        }
        // 5. If the machine falls three times, the score is 15000
        if (computerNum == 3) {
            return 15000;
        }
        // 6. If the machine falls into four pieces, the score is 800000
        if (computerNum == 4) {
            return 800000;
        }

        // Machine defense

        // 7. If a player loses a child, it will be divided into 15 points
        if (playerNum == 1) {
            return 15;
        }
        // 8. If the player loses two children, the score is 400
        if (playerNum == 2) {
            return 400;
        }
        // 9. If the player loses three children, the score is 1800
        if (playerNum == 3) {
            return 1800;
        }
        // 10. If the player loses four children, the score is 100000
        if (playerNum == 4) {
            return 100000;
        }

        return -1; //In other cases, an error occurs and the code is not executed
    }

The empty position with the largest weight is obtained by traversing the quintuple, which is the optimal position of AI.

function computerDown() {
        // Initialize score group
        for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth; j++) {
                score[i][j] = 0;
            }
        }
        // Number of black chess (players) in quintuple
        var playerNum = 0;
        // Number of white chess (computer) in quintuple
        var computerNum = 0;
        // Five tuple temporary score
        var tempScore = 0;
        // Maximum score
        var maxScore = -1;

        // Horizontal search
        for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth - 4; j++) {
                for (var k = j; k < j + 5; k++) {
                    // If it's a player's son
                    if (chessPlace[k][i] == 1) {
                        playerNum++;
                    } else if (chessPlace[k][i] == 2) { //If it's a computer
                        computerNum++;
                    }
                }
                // The number of black and white chess in each quintuple is transferred into the scoring table
                tempScore = chessScore(playerNum, computerNum);
                // Add a score for each position of the quintuple
                for (var k = j; k < j + 5; k++) {
                    score[k][i] += tempScore;
                }
                // Clear the number of pieces in the quintuple and the temporary score of the quintuple
                playerNum = 0;
                computerNum = 0;
                tempScore = 0;
            }
        }

        // Vertical search
        for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth - 4; j++) {
                for (var k = 0; k < j + 5; k++) {
                    // If it's a player's son
                    if (chessPlace[i][k] == 1) {
                        playerNum++;
                    } else if (chessPlace[i][k] == 2) { //If it's a computer
                        computerNum++;
                    }
                }
                // The number of black and white chess in each quintuple is transferred into the scoring table
                tempScore = chessScore(playerNum, computerNum);
                // Add a score for each position of the quintuple
                for (var k = j; k < j + 5; k++) {
                    score[i][k] += tempScore;
                }
                // Clear the number of pieces and instantaneous score value in the quintuple
                playerNum = 0;
                computerNum = 0;
                tempScore = 0;
            }
        }


        // Backslash search

        // Upper part of backslash
        for (var i = chessWidth - 1; i >= 4; i--) {
            for (var k = i, j = 0; j < chessWidth && k >= 0; j++, k--) {
                var m = k; //x 14 13
                var n = j; //y 0  1
                for (; m > k - 5 && k - 5 >= -1; m--, n++) {
                    // If it's a player's son
                    if (chessPlace[m][n] == 1) {
                        playerNum++;
                    } else if (chessPlace[m][n] == 2) { //If it's a computer
                        computerNum++;
                    }
                }
                // Note that in oblique judgment, it may not form a quintuple (close to the four top corners of the chessboard), so this situation should be ignored
                if (m == k - 5) {
                    // The number of black and white chess in each quintuple is transferred into the scoring table
                    tempScore = chessScore(playerNum, computerNum);
                    // Add a score for each position of the quintuple
                    for (m = k, n = j; m > k - 5; m--, n++) {
                        score[m][n] += tempScore;
                    }
                }
                // Clear the number of pieces in the quintuple and the temporary score of the quintuple
                playerNum = 0;
                computerNum = 0;
                tempScore = 0;
            }
        }
        // Lower part of backslash
        for (var i = 1; i < 15; i++) {
            for (var k = i, j = chessWidth - 1; j >= 0 && k < 15; j--, k++) {
                var m = k; //y 1 
                var n = j; //x 14
                for (; m < k + 5 && k + 5 <= 15; m++, n--) {
                    // If it's a player's son
                    if (chessPlace[n][m] == 1) {
                        playerNum++;
                    } else if (chessPlace[n][m] == 2) { //If it's a computer
                        computerNum++;
                    }
                }
                // Note that in oblique judgment, it may not form a quintuple (close to the four top corners of the chessboard), so this situation should be ignored
                if (m == k + 5) {
                    // The number of black and white chess in each quintuple is transferred into the scoring table
                    tempScore = chessScore(playerNum, computerNum);
                    // Add a score for each position of the quintuple
                    for (m = k, n = j; m < k + 5; m++, n--) {
                        score[n][m] += tempScore;
                    }
                }
                // Clear the number of pieces in the quintuple and the temporary score of the quintuple
                playerNum = 0;
                computerNum = 0;
                tempScore = 0;
            }
        }

        // Forward slash search

        // Upper part of forward slash
        for (var i = 0; i < chessWidth - 1; i++) {
            for (var k = i, j = 0; j < chessWidth && k < chessWidth; j++, k++) {
                var m = k;
                var n = j;
                for (; m < k + 5 && k + 5 <= chessWidth; m++, n++) {
                    // If it's a player's son
                    if (chessPlace[m][n] == 1) {
                        playerNum++;
                    } else if (chessPlace[m][n] == 2) { //If it's a computer
                        computerNum++;
                    }
                }
                // Note that in oblique judgment, it may not form a quintuple (close to the four top corners of the chessboard), so this situation should be ignored
                if (m == k + 5) {
                    // The number of black and white chess in each quintuple is transferred into the scoring table
                    tempScore = chessScore(playerNum, computerNum);
                    // Add a score for each position of the quintuple
                    for (m = k, n = j; m < k + 5; m++, n++) {
                        score[m][n] += tempScore;
                    }
                }
                // Clear the number of pieces in the quintuple and the temporary score of the quintuple
                playerNum = 0;
                computerNum = 0;
                tempScore = 0;
            }
        }

        // Lower part of forward slash
        for (var i = 1; i < chessWidth - 4; i++) {
            for (var k = i, j = 0; j < chessWidth && k < chessWidth; j++, k++) {
                var m = k;
                var n = j;
                for (; m < k + 5 && k + 5 <= chessWidth; m++, n++) {
                    // If it's a player's son
                    if (chessPlace[n][m] == 1) {
                        playerNum++;
                    } else if (chessPlace[n][m] == 2) { //If it's a computer
                        computerNum++;
                    }
                }
                // Note that in oblique judgment, it may not form a quintuple (close to the four top corners of the chessboard), so this situation should be ignored
                if (m == k + 5) {
                    // The number of black and white chess in each quintuple is transferred into the scoring table
                    tempScore = chessScore(playerNum, computerNum);
                    // Add a score for each position of the quintuple
                    for (m = k, n = j; m < k + 5; m++, n++) {
                        score[n][m] += tempScore;
                    }
                }
                // Clear the number of pieces in the quintuple and the temporary score of the quintuple
                playerNum = 0;
                computerNum = 0;
                tempScore = 0;
            }
        }

        // Find the position with the highest score from the empty position
        for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth; j++) {
                if (chessPlace[i][j] == 0 && score[i][j] > maxScore) {
                    goalX = i;
                    goalY = j;
                    maxScore = score[i][j];
                }
            }
        }
        if (goalX != -1 && goalY != -1 && chessPlace[goalX][goalY] == 0) {
            // Luozi
            drawChess(goalX, goalY, false);
            // Save game data
            computerData();
            // Save the drop at this location
            chessPlace[goalX][goalY] = 2;
            // Judge whether to win or lose
            if (win(goalX, goalY, num = 2)) {
                // game over
                gameOver = true;
                // Save game data
                computerData(true);
                // Next round player drop
                isMan = true;
                var choice = confirm("You lost, another game?");
                // Playback failed sound
                failMp3.play();
                if (choice) {
                    // Empty chessboard
                    clear();
                    // Empty drop steps and scores
                    playerData("", "clearPath");
                    computerData("", "clearPath");
                    gameOver = false;
                }
            } else if (tie()) {
                // game over
                gameOver = true;
                // It's the player's turn to play chess in the next game
                isMan = true;
                // Do you want another round
                var choice = confirm("Draw, another game?");
                if (choice) {
                    // Empty chessboard
                    clear();
                    // Empty drop steps and scores
                    playerData("", "clearPath");
                    computerData("", "clearPath");
                    gameOver = false;
                }
            } else {
                // It's the player's turn to play chess
                isMan = true;
            }
        }

    }

(5) Repentance chess function

The function of repentance chess is mainly to empty the pieces at the position of repentance chess. Here I use the method of fixed-point clearing and redrawing. First, we draw a circle of the same size as the chess pieces at the coordinates of the chess pieces to cover the chess pieces on the chessboard. At this time, we will find that not only the chess pieces are covered, but also the chessboard lines in the covered area of the chess pieces on the chessboard are covered, so we have to fill in the covered lines.

a. Before repentance:

b. After repentance:


c. Through the observation in the figure, we can find that the missing part is the previous mouse click capture area, so we only need to fill in the lines of this area.

// Empty pieces
    function clearChess(x, y) {
        // Clear the chess pieces in this position
        ctx.clearRect(x * 40, y * 40, 40, 40);
        // Clear the position of the chess piece and mark it as zero
        chessPlace[x][y] = 0;
        // Draw the cleared chessboard line
        x = x * 40 + 20;
        y = y * 40 + 20;
        // Draw a horizontal line
        ctx.beginPath();
        ctx.moveTo(x - 20, y);
        ctx.lineTo(x + 20, y);
        ctx.stroke();
        // Draw vertical lines
        ctx.beginPath();
        ctx.moveTo(x, y - 20);
        ctx.lineTo(x, y + 20);
        ctx.stroke();
    }

d. Backgammon function:

// Repentance chess
    retract.addEventListener("click", function () {
        // Trigger button click sound
        clickSound.play();
        if (gameOver) {
            alert("The game is over. I can't repent!");
        } else if (isRetract == false) {
            alert("Can't repent!");
        } else {
            isRetract = false; //It means that you have repented your chess pieces and can't repent any more
            isUnretract = true; //Repentance chess can be revoked only after repentance chess pieces
            clearChess(eventX, eventY); //Clear the player's chess pieces at the target position
            clearChess(goalX, goalY); //Clear computer chess pieces at the target location
            playerData("", "retract"); //Reset player game data
            computerData("", "retract"); //Reset PC game data
        }
    });

(6) Undo repentance function

The undo repentance function is relatively simple. You only need to redraw the chess pieces of the previous step.

// Undo repentance
    unretract.addEventListener("click", function () {
        // Trigger button click sound
        clickSound.play();
        if (gameOver) {
            alert("The game is over, you can't undo it!");
        } else if (isUnretract == false) {
            alert("Cannot undo!");
        } else {
            isUnretract = false; //You cannot undo it again after you undo it
            isRetract = true; //After canceling repentance chess, you can repent chess again (current position)
            drawChess(eventX, eventY, true); //Draw the player's chess pieces at the target position
            drawChess(goalX, goalY, false); //Draw computer chess pieces at the target position
            chessPlace[eventX][eventY] = 1;
            chessPlace[goalX][goalY] = 2;
            playerData(); //Reset player game data
            computerData(); //Reset PC game data
        }
    });

(7) Decide whether to win or lose

There are many ways to decide whether to win or lose here, but the only constant principle is Wuzi Lianzhu.
Method 1: traverse the whole chessboard to find five connected beads.

 for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth; j++) {
                // Horizontal win
                if (chessPlace[i][j] != 0 && i < chessWidth - 4 &&
                    chessPlace[i][j] == chessPlace[i + 1][j] &&
                    chessPlace[i][j] == chessPlace[i + 2][j] &&
                    chessPlace[i][j] == chessPlace[i + 3][j] &&
                    chessPlace[i][j] == chessPlace[i + 4][j]) {
                    return flag = "win";
                }
                // Vertical win
                if (chessPlace[i][j] != 0 && j < chessWidth - 4 &&
                    chessPlace[i][j] == chessPlace[i][j + 1] &&
                    chessPlace[i][j] == chessPlace[i][j + 2] &&
                    chessPlace[i][j] == chessPlace[i][j + 3] &&
                    chessPlace[i][j] == chessPlace[i][j + 4]) {
                    return flag = "win";
                }
                // Positive slash wins
                if (chessPlace[i][j] != 0 &&
                    i < chessWidth - 4 && j < chessWidth - 4 &&
                    chessPlace[i][j] == chessPlace[i + 1][j + 1] &&
                    chessPlace[i][j] == chessPlace[i + 2][j + 2] &&
                    chessPlace[i][j] == chessPlace[i + 3][j + 3] &&
                    chessPlace[i][j] == chessPlace[i + 4][j + 4]) {
                    return flag = "win";
                }

            }
        }
         //Backslash wins
        for (var i = 0; i < chessWidth; i++) {
            for (var j = chessWidth - 1; j > 3; j--) {
                if (chessPlace[i][j] != 0 &&
                    chessPlace[i][j] == chessPlace[i + 1][j - 1] &&
                    chessPlace[i][j] == chessPlace[i + 2][j - 2] &&
                    chessPlace[i][j] == chessPlace[i + 3][j - 3] &&
                    chessPlace[i][j] == chessPlace[i + 4][j - 4]) {
                    return flag = "win";
                }
            }
        }

Method 2: look from the current position to the surrounding position.

function win(eventX, eventY, num) {
        // Save the number of the same pieces connected together
        var count = 0;
        // Save the current chess coordinates
        var x = eventX;
        var y = eventY;
        // Horizontal win
        for (var i = x - 1; i >= 0; i--) {
            if (chessPlace[i][y] == num) {
                count++;
            } else {
                break;
            }
        }
        for (var i = x + 1; i < chessWidth; i++) {
            if (chessPlace[i][y] == num) {
                count++;
            } else {
                break;
            }
        }
        if (count >= 4) {
            return true;
        }
        count = 0;
        // Vertical win
        for (var i = y - 1; i >= 0; i--) {
            if (chessPlace[x][i] == num) {
                count++;
            } else {
                break;
            }
        }
        for (var i = y + 1; i < chessWidth; i++) {
            if (chessPlace[x][i] == num) {
                count++;
            } else {
                break;
            }
        }
        if (count >= 4) {
            return true;
        }
        count = 0;
        // Positive slash wins
        for (var i = x - 1, j = y - 1; i >= 0 && j >= 0; i--, j--) {
            if (chessPlace[i][j] == num) {
                count++;
            } else {
                break;
            }
        }
        for (var i = x + 1, j = y + 1; i < chessWidth && j < chessWidth; i++, j++) {
            if (chessPlace[i][j] == num) {
                count++;
            } else {
                break;
            }
        }
        if (count >= 4) {
            return true;
        }
        count = 0;
        // Backslash wins
        for (var i = x - 1, j = y + 1; i >= 0 && j < chessWidth; i--, j++) {
            if (chessPlace[i][j] == num) {
                count++;
            } else {
                break;
            }
        }
        for (var i = x + 1, j = y - 1; i < chessWidth && j >= 0; i++, j--) {
            if (chessPlace[i][j] == num) {
                count++;
            } else {
                break;
            }
        }
        if (count >= 4) {
            return true;
        }
        count = 0;
    }

Here I use the second method, and I also recommend you to use the second method, because the first method is not efficient, and there is a bug that the backslash sometimes cannot determine the outcome.

(8) Draw

By traversing the whole chessboard to see if there is an empty position, if there is, it is not a draw, on the contrary, it is a draw.

function tie() {
        var count = 0;
        for (var i = 0; i < chessWidth; i++) {
            for (var j = 0; j < chessWidth; j++) {
                if (chessPlace[i][j] != 0) {
                    count++;
                } else {
                    break;
                }
            }
        }
        if (count == 225) {
            return true;
        }
    }

Here, our Gobang has almost been developed. The next step is the processing on the UI. Of course, I only designed man-machine combat here, not player to player. If you are interested, you can try to add a player to player module.

3, Complete game rendering

1. Menu:

2. Game part:

4, Display of operation results

AI Gobang online preview

5, Project source code

Gobang project source code

Tags: Javascript Front-end html5 less

Posted on Fri, 10 Sep 2021 04:36:53 -0400 by Alt_F4