前端五子棋第二版

试玩链接:五子棋
效果图如下:
在这里插入图片描述
与上个版本相比,主要实现了两个功能:
一个是处理的音效消失的bug,另一个是新增了人机模式。
先说说背景音效的问题,其实解决方案很简单,就是把中文的MP3音乐名改成英文名字即可。
直接上代码:
HTML代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>五子棋</title>
    <link rel="stylesheet" href="css/gobang.css">
</head>

<body>
    <a href="javascript:void(0)" id="computerPlay">人机对战</a>
    <a href="" id="newGame">重开一局</a>
    <a href="javascript:void(0)" id="personPlay">双人对战</a>
    <canvas id="canvas" width="480" height="480">
    </canvas>
    <audio class="audio">
        <source src="cbgm.mp3" type="audio/mp3" />
    </audio>
    <audio class="music">
        <source src="bgm.mp3" type="audio/mp3" />
    </audio>
    <script src="js/game.js"></script>
    <script src="js/ai.js"></script>
    <script src="js/type.js"></script>
</body>

</html>

由于代码量变得更多,所以这个版本我采取的外部文件引用的方式,也就是将css代码以及js代码都分离开来,这样也更符合代码格式标准。
CSS代码:

body {
    margin: 0;
    background-color: #ccc;
}

#canvas {
    display: block;
    position: relative;
    margin: 135px auto;
    background-color: rgb(221, 168, 21);
}
a{
    position: absolute;
    width: 100px;
    height: 25px;
    font-family: Arial;
    color: white;
    border-radius: 10px;
    text-decoration: none;
    text-align: center;
}
#computerPlay {
    background-color: #f14343;
    top: 90px;
    left: 36%;
}
#computerPlay:hover {
    background: #c90707;
}
#personPlay {
    background-color: rgb(57, 190, 243);
    top: 90px;
    left: 57.5%;
}
#personPlay:hover{
    background: rgb(8, 168, 231);
}
#newGame{
    background-color: rgb(221, 224, 26);
    top: 90px;
    left: 47%;
}
#newGame:hover{
    background: rgb(198, 201, 40);
}

游戏开始以及双人对战的js代码:

var canvas = document.querySelector("canvas");
var computerPlay = document.getElementById("computerPlay");
var personPlay = document.getElementById("personPlay");
var chessColor = ['black', 'white'];
var musicStart = false;
var step = 0;
var mapChess = [];
var mode = [
    [1, 0],
    [0, 1],
    [1, 1],
    [1, -1]
]
for (var i = 0; i < 15; i++) {
    mapChess[i] = [];
    for (var j = 0; j < 15; j++) {
        mapChess[i][j] = '';
    }
}
var ctx = canvas.getContext("2d");/* 获取绘制环境 */
for (var i = 1; i < 16; i++) {
    ctx.moveTo(30 * i, 30);
    ctx.lineTo(30 * i, 450);/* 描述绘制路径 */
    ctx.moveTo(30, 30 * i);
    ctx.lineTo(450, 30 * i);
}
ctx.stroke();/* 将之前所有的路径全部绘制一次 */
drawPoint(120, 120);
drawPoint(120, 360);
drawPoint(360, 120);
drawPoint(360, 360);
drawPoint(240, 240);
computerPlay.addEventListener('click', computerStart, false);
personPlay.addEventListener('click', personStart, false);

function personStart() {
    canvas.addEventListener('click', start, false);
    computerPlay.removeEventListener('click', computerStart, false);
}
function computerStart() {
    if (step == 0) {
        document.querySelector('.audio').play();
        document.querySelector('.music').play();
        drawChess(240, 240, 'black');
        mapChess[7][7] = 'black';
        step++;
    }
    canvas.addEventListener('click', aiStart, false);
    personPlay.removeEventListener('click', personStart, false);
}
function drawChess(x, y, color) {
    ctx.fillStyle = color;
    ctx.beginPath();/* 提笔 */
    ctx.arc(x, y, 13, Math.PI * 2, false);
    ctx.fill();
    ctx.stroke();
}
function drawPoint(x, y) {
    ctx.fillStyle = 'black';
    ctx.beginPath();/* 提笔 */
    ctx.arc(x, y, 2, Math.PI * 2, false);
    ctx.fill();
    ctx.stroke();
}
function start(e) {
    var audio = document.querySelector('.audio');
    var music = document.querySelector('.music');
    var color = chessColor[step % 2];
    var dx = Math.floor((e.offsetX + 15) / 30) - 1;
    var dy = Math.floor((e.offsetY + 15) / 30) - 1;
    if (dx < 0 || dx > 14 || dy < 0 || dy > 14) {
        return;
    }
    if (mapChess[dx][dy] == '') {
        var audioPromise = document.querySelector('.audio').play();
        document.querySelector('.music').play();
        music.muted = false;
        drawChess((dx + 1) * 30, (dy + 1) * 30, color);
        mapChess[dx][dy] = color;
        if (judge(dx, dy, color, mode[0], 5) ||
            judge(dx, dy, color, mode[1], 5) ||
            judge(dx, dy, color, mode[2], 5) ||
            judge(dx, dy, color, mode[3], 5)
        ) {
            music.muted = true;
            step % 2 == 0 ? alert("黑棋获胜") : alert("白棋获胜");
            canvas.removeEventListener('click', start, false);
            personPlay.removeEventListener('click', personStart, false);
            return;
        }
        if (audioPromise !== undefined) {
            audioPromise.then(_ => {
                audio.paused = true;
            })
                .catch(error => {

                });
        }
        step++;
    }
}
function judge(x, y, color, mode, number) {
    var count = 1;
    for (var i = 1; i < number; i++) {
        if (mapChess[x + i * mode[0]]) {
            if (mapChess[x + i * mode[0]][y + i * mode[1]] == color) {
                count++;
            } else {
                break;
            }
        }
    }
    for (var i = 1; i < number; i++) {
        if (mapChess[x - i * mode[0]]) {
            if (mapChess[x - i * mode[0]][y - i * mode[1]] == color) {
                count++;
            } else {
                break;
            }
        }
    }
    return count >= number ? true : false;
}

以上这些代码除了新添增三个按钮以及绑定其点击事件外(这部分看代码应该很容易理解),其余部分的代码在上个版本以及讲解过,如果看不明白的伙伴可以参看以下链接:前端五子棋第一版
然后就是实现新增的五子棋的ai功能,不考虑ai,其大体实现思路与双人对战的模式几乎一样,只不过是在玩家下完一步后电脑紧接着再下一步。
电脑下棋的js代码:

var backX = 0;
var backY = 0;

function aiStart(e) {
    var audio = document.querySelector('.audio');
    var music = document.querySelector('.music');
    var color = chessColor[step % 2];
    var dx = Math.floor((e.offsetX + 15) / 30) - 1;
    var dy = Math.floor((e.offsetY + 15) / 30) - 1;
    if (dx < 0 || dx > 14 || dy < 0 || dy > 14) {
        return;
    }
    if (mapChess[dx][dy] == '') {
        var audioPromise = document.querySelector('.audio').play();
        document.querySelector('.music').play();
        music.muted = false;
        //玩家落子
        drawChess((dx + 1) * 30, (dy + 1) * 30, color);
        mapChess[dx][dy] = color;
        if (judge(dx, dy, color, mode[0], 5) ||
            judge(dx, dy, color, mode[1], 5) ||
            judge(dx, dy, color, mode[2], 5) ||
            judge(dx, dy, color, mode[3], 5)
        ) {
            music.muted = true;
            canvas.removeEventListener('click', aiStart, false);
            computerPlay.removeEventListener('click', computerStart, false);
            step % 2 == 0 ? alert("黑棋获胜") : alert("白棋获胜");
            return;
        }
        step++;
        //电脑落子
        AI();
        dx = backX;
        dy = backY;
        drawChess((dx + 1) * 30, (dy + 1) * 30, 'black');
        mapChess[dx][dy] = 'black';
        if (judge(dx, dy, 'black', mode[0], 5) ||
            judge(dx, dy, 'black', mode[1], 5) ||
            judge(dx, dy, 'black', mode[2], 5) ||
            judge(dx, dy, 'black', mode[3], 5)
        ) {
            music.muted = true;
            canvas.removeEventListener('click', aiStart, false);
            computerPlay.removeEventListener('click', computerStart, false);
            step % 2 == 0 ? alert("黑棋获胜") : alert("白棋获胜");
            return;
        }
        if (audioPromise !== undefined) {
            audioPromise.then(_ => {
                audio.paused = true;
            })
                .catch(error => {

                });
        }
        step++;
    }
}

function AI() {
    var computerScore = [];
    var personScore = [];
    var maxScore = 0;
    for (var i = 0; i < 15; i++) {
        personScore[i] = [];
        computerScore[i] = [];
        for (var j = 0; j < 15; j++) {
            personScore[i][j] = 0;
            computerScore[i][j] = 0;
        }
    }
    for (var i = 0; i < mapChess.length; i++) {
        for (var j = 0; j < mapChess.length; j++) {
            if (mapChess[i][j] == '') {
                var sum = 0;
                //一步得分
                for (var k = 0; k <= 3; k++) {
                    var perArr = getType(i, j, 'white', mode[k]);
                    personScore[i][j] += 4 * Math.pow(10, perArr[1] - perArr[0]);
                    var comArr = getType(i, j, 'black', mode[k]);
                    computerScore[i][j] += 6 * Math.pow(10, comArr[1] - comArr[0]);
                }
                sum = computerScore[i][j] + personScore[i][j];
                if (sum > maxScore) {
                    maxScore = sum;
                    xMax = i;
                    yMax = j;
                }
            }
        }
    }
    backX = xMax;
    backY = yMax;
}

在aiStart这个函数里说明两点:

  1. 由于电脑下棋是无需触发点击事件,所以我直接将其绑定在玩家下棋的后面。
  2. 所谓ai下棋,无非就是通过一系列复杂的逻辑计算,得到最终位置的坐标,所以我使用全解变量backX ,backY 来接收最终的电脑下棋的位置。

在AI这个函数里说明两点:

  1. 首先需要用computerScore 和personScore 来分别保存棋盘中每个位置的计算得分,所以它们也是和棋盘一样大的数组。又由于每次下棋后部分位置的得分会改变,所以它们也是局部变量。(正是由于只有部分位置的得分会改变,所以这里也是算棋功能的优化点)
  2. 通过getType函数来获取某个点的一个方向的得分(只有四个方向),这里我把电脑的得分系数设置为6而玩家的得分系数设置为4,因为下棋的目的是获胜,所以己方的优势更重要一些,但是同时敌之要低,我方也需要争取,这也是为什么最后我设置的得分是两者之间的求和而不是作差。

最后,该亮出最神奇的getType函数:

function getType(x, y, color, mode) {
    var countBlock = 0;
    var count = 1;
    var i = 1;
    var j = 1;
    while ((x + i * mode[0]) < 15 && (y + i * mode[1]) < 15 &&
        (x + i * mode[0]) >= 0 && (y + i * mode[1]) >= 0 &&
        mapChess[x + i * mode[0]][y + i * mode[1]] != '') {
        if (mapChess[x + i * mode[0]][y + i * mode[1]] === color) {
            count++;
        } else {
            countBlock++;
            break;
        }
        i++;
    }
    while ((x - j * mode[0]) >= 0 && (y - j * mode[1]) >= 0 &&
        (x - j * mode[0]) < 15 && (y - j * mode[1]) < 15 &&
        mapChess[x - j * mode[0]][y - j * mode[1]] != '') {
        if (mapChess[x - j * mode[0]][y - j * mode[1]] === color) {
            count++;
        } else {
            countBlock++;
            break;
        }
        j++;
    }
    if (count >= 5) {
        countBlock = 0;
    } else {
        //两头都被堵住或者遇到边界的位置时这个方向下棋等于没下
        if (countBlock == 2 ||
            (x + i * mode[0]) < 0 || (y + i * mode[1]) < 0 ||
            (x + i * mode[0]) >= 15 || (y + i * mode[1]) >= 15 ||
            (x - j * mode[0]) < 0 || (y - j * mode[1]) < 0 ||
            (x - j * mode[0]) >= 15 || (y - j * mode[1]) >= 15
        ) {
            countBlock = count;
        }
    }
    return [countBlock, count];
}

在getType这个函数里说明三点:

  1. 首先为什么说它神奇呢,因为这个函数包括了五子棋里面的眠一,活一,死一,眠二,活二,死二眠三,活三,死三,眠四,死四,活四,成五这么多种单个位置的局面(这里的死是表示两端都已被对手的棋子封住),注意这里并不包含跳眠二,跳活二等跳棋局面的考虑。(但是后面版本我会加上)
  2. 这里面还做出了countBlock == 2以及边界考虑,就是为了避免电脑下无用棋。
  3. 最终的结果是由count数值减去countBlock数值作为指数,其实这并不是一个最好的算法,但是是一个很有效率的算法。

总之,此电脑ai依然存在不少bug,但是就单步下棋水平而言,我个人感觉已经马马虎虎了。后续的重点将是给与电脑算棋甚至是算杀的功能。这部分算法的难度很大,我目前暂时还没有这个能力,所以还得更加持续学习,我目前有个思路就是回溯剪枝算法。这里我很欢迎大佬们评论区留言赐教!

猜你喜欢

转载自blog.csdn.net/asd0356/article/details/104750446