兔子零-A酱

欢迎来到我的个人博客,我会在这里记录下关于学习和生活的点点滴滴

五子棋AI(二):绘制界面,添加点击事件,棋局胜负判断

上一篇文章我们简单的介绍了五子棋以及AI相关的基本概念,相信大家也对五子棋的历史以及现状有了一个大概的印象,那么从这篇文章起,就正式开始这个五子棋的制作。
本篇文章我们将完成游戏界面的绘制(包括棋盘,棋子)、棋盘的点击事件以及基本的游戏逻辑。

Tips:本文以及之后的文章均只贴出关键部分实现的代码,想查看完整的源码及效果可以访问该项目的Github地址以及在线演示页面。(地址将在文末贴出)

绘制棋盘与棋子

因为我们的主要目的是AI的设计,所以这个游戏界面设计得比较简单,同时也方便我们的编码实现。

首先大概介绍下这个项目所用到的技术:

  • 界面:考虑到游戏的执行效率和优化问题,这里我们采用HTML5的canvas来绘制游戏界面。
  • 逻辑:不采用复杂的框架或引擎技术,尽量使用简单的JavaScript脚本(少量jQuery语句)来完成基本的逻辑操作以及AI的实现。

本游戏采用类似井字棋的下法规则,棋子落在格子内而不是交叉点。棋子则采用X和O来代替黑棋和白棋(其实是个人不喜欢传统的黄色棋盘和黑白棋子。。审美疲劳),默认情况下X为传统规则下的黑棋,也就是先手。

五子棋棋盘由横纵各15条等距离,垂直交叉的平行线构成。我们首先要做的就是利用canvas的画线函数lineTo来绘制这个棋盘。

//绘制棋盘
function drawChessBoard() {
    canvas = document.getElementById("canvas");
    context = canvas.getContext("2d");
    context.lineWidth = 2;
    context.strokeStyle = "#577A9E";//线条颜色
    var lineOffset = 1;//线条偏移量

    for (var i = 0; i <= 420; i += 28) {//绘制棋盘的线
        context.beginPath();
        context.moveTo(1, i + lineOffset);
        context.lineTo(421, i + lineOffset);
        context.closePath();
        context.stroke();

        context.beginPath();
        context.moveTo(i + lineOffset, 1);
        context.lineTo(i + lineOffset, 421);
        context.closePath();
        context.stroke();
    }
}

这里遇到了一个不是bug的bug,用lineTo画一条线的时候看不出来,但是循环画出棋盘后,就发现线条粗细不一样。在网上查阅了相关资料,才知道是因为canvas绘制线条的方式会使线条变模糊,所以这里必须把原坐标加上一个偏移量才能使绘图恢复正常。(绘制棋子时也会加上这个偏移量)

绘制线条的原理可以参考下面这几篇文章:
HTML5 Canvas中实现绘制一个像素宽的细线
HTML5 Canvas画图教程(3)—canvas出现1像素线条模糊不清的原因
html canvas绘制1px直线时出现的模糊问题。。线的粗细不同是bug么?

接下来我们需要一个15x15二维数组chessData来记录棋盘信息,不同类型的棋子对应二维数组中不同的值。

//初始化棋盘信息
function initChessData() {
    chessData = new Array(15);

    for (var x = 0; x < 15; x++) {
        chessData[x] = new Array(15);
        for (var y = 0; y < 15; y++) {
            chessData[x][y] = 0;//初始化0为没有走过的,1为cross走的,2为circle走的
        }
    }
}

用canvas封装的arc函数可以很轻松的画出圆形:

//绘制circle棋子
function drawChessCircle(x, y) {
    canvas = document.getElementById("canvas");
    context = canvas.getContext("2d");
    context.lineWidth = 3;
    context.strokeStyle = "#e74c3c";

    var offset = 1;

    context.beginPath();
    context.arc(x + offset, y + offset, 9, 0, Math.PI * 2, true);
    context.closePath();
    context.stroke();
}

画一个叉形其实就是画两条相交的直线,画圆的函数传入参数为圆心的坐标,画叉的函数传入的坐标为两条直线相交的坐标,因此需要根据相交点的坐标分别求出直线四个端点的坐标,算法不难,直接上代码:

//绘制cross棋子
function drawChessCross(x, y) {
    canvas = document.getElementById("canvas");
    context = canvas.getContext("2d");
    context.lineWidth = 3;
    context.strokeStyle = "#2ecc71";

    var crossRadius = 8;
    var offset = 1;

    context.beginPath();
    context.moveTo(x + offset - crossRadius, y + offset - crossRadius);
    context.lineTo(x + offset + crossRadius, y + offset + crossRadius);
    context.lineCap = "round";
    context.stroke();

    context.beginPath();
    context.moveTo(x + offset + crossRadius, y + offset - crossRadius);
    context.lineTo(x + offset - crossRadius, y + offset + crossRadius);
    context.lineCap = "round";
    context.stroke();
}

界面的绘制部分到此就基本完成了,接下来我们给canvas画布添加点击事件。

添加点击事件

//鼠标点击事件
function play(e) {
    //判断该局是否结束
    if (isWell) {
        alert("本局已结束,重玩请刷新");
        return;
    }

    var bbox = canvas.getBoundingClientRect();

    //计算鼠标点击的区域
    var x = parseInt((e.clientX - bbox.left) / 28);
    var y = parseInt((e.clientY - bbox.top) / 28);

    curUser = 1;//设置当前玩家为cross

    if (curUser == 1) {
        chessType = 1;//设置当前棋子类型为cross
    } else {
        chessType = 2;
    }

    //判断该位置是否有棋子
    if (chessData[x][y] != 0) {
        return;
    }

    generateManChess(x, y);

    if (isWell) {
        alert("你赢了");
        return;
    }

    generateAIChess();
}

Tips:isWellcurUserchessType均为全局变量,游戏开始后便读取其初始值。(见源码)

因为我们要模拟人类与电脑下棋的过程,所以需要两个函数来生成人类和电脑的棋子,他们分别是generateManChessgenerateAIChess函数。
目前还没实现AI,所以generateAIChess暂时采用取随机数的方式来体现电脑下棋的效果。

//生成人类棋子
function generateManChess(x, y) {
    drawChess(x, y, chessType);//绘制cross棋子
    checkWin(x, y, chessType);
}

//生成AI棋子
function generateAIChess() {
    var x;
    var y;

    curUser = 2;//设置当前玩家为circle

    if (curUser == 1) {
        chessType = 1;
    } else {
        chessType = 2;//设置当前棋子类型为circle
    }

    while (1) {
        x = Math.floor(Math.random()*10) + Math.floor(Math.random()*6);
        y = Math.floor(Math.random()*10) + Math.floor(Math.random()*6);

        if (chessData[x][y] == 0) {
            break;
        }
    }

    drawChess(x, y, chessType);
    checkWin(x, y, chessType);
    if (isWell) {
        alert("你输了");
        return;
    }
}

棋局判负逻辑

最后要实现的便是游戏的判负逻辑,但是由于五子棋的特殊性,判负并不像象棋那么简单。玩过五子棋的都知道,取得胜利的条件就是让五个子连成一线,所以我们就需要遍历整个棋盘的二维数组,来获取是否有连续的五个棋子连成了一条线。那么这里的思路就是,设置4个变量来计数,当棋盘上每出现一个棋子,就获取这个棋子周围每个方向的情况,如果某个方向上有同类的棋子就让这个方向的变量值+1,最后统计每个变量值是否大于等于5。

//判断胜负
function checkWin(x, y, chessType) {
    //设置每个方向的连子计数
    var countLeftRight = 0;
    var countUpDown = 0;
    var countUpperLeftToLowerRight = 0;//左上到右下
    var countUpperRightToLowerLeft = 0;//右上到左下

    //左右判断
    for (var i = x; i >= 0; i--) {
        if (chessData[i][y] != chessType) {
            break;
        }
        countLeftRight++;
    }
    for (var i = x + 1; i < 15; i++) {
        if (chessData[i][y] != chessType) {
            break;
        }
        countLeftRight++;
    }
    //上下判断
    for (var i = y; i >= 0; i--) {
        if (chessData[x][i] != chessType) {
            break;
        }
        countUpDown++;
    }
    for (var i = y + 1; i < 15; i++) {
        if (chessData[x][i] != chessType) {
            break;
        }
        countUpDown++;
    }
    //左上右下判断
    var t = y;
    var w = y + 1;
    for (var i = x; i >= 0; i--) {

        if (chessData[i][t] != chessType) {
            break;
        }
        if (t >= 0) {
            t = t - 1;
        }
        countUpperLeftToLowerRight++;
    }
    for (var i = x + 1; i < 15; i++) {
        if (chessData[i][w] != chessType) {
            break;
        }
        if (w < 15) {
            w = w + 1;
        }
        countUpperLeftToLowerRight++;
    }
    //右上左下判断
    var h = y;
    var z = y + 1;
    for (var i = x; i < 15; i++) {
        if (chessData[i][h] != chessType) {
            break;
        }
        if (h >= 0) {
            h--;
        }
        countUpperRightToLowerLeft++;
    }
    for (var i = x - 1; i >= 0; i--) {
        if (chessData[i][z] != chessType) {
            break;
        }
        if (z <= 15) {
            z++;
        }
        countUpperRightToLowerLeft++;
    }

    if (countLeftRight >= 5 || countUpDown >= 5 || countUpperLeftToLowerRight >= 5 || countUpperRightToLowerLeft >= 5) {
        isWell = true;//设置该局已经获胜,不可以再走了
    }
}

至此,一个简单的对弈效果就实现出来了,只是我们现在还没有给电脑添加AI,所以电脑看起来还很傻,没有“智商”。从下一篇文章开始我们就正式进入AI的部分,第三篇文章将会介绍一个棋类AI的常用算法并实现它。

Tips:本系列的文章并不是一个完整作品的实现教程,博主也是边做边写文章,所以文章里贴出来的代码也许并不是最终版本的代码,但是或多或少代表了一个版本的基本实现思路,仅供参考。

本项目的Github地址:https://github.com/csd758371536/GomokuWithAI
在线演示页面:http://gomoku.csdoker.com/

上一篇文章 下一篇文章