前端仔教你一步步实现人人对战五子棋小游戏【canvas详细版】

栏目: 后端 · 发布时间: 5年前

内容简介:线上地址--gobang online pc上使用谷歌浏览器比较友好@~@代码仓库--前置知识点:阮生的es6教程和
前端仔教你一步步实现人人对战五子棋小游戏【canvas详细版】

线上地址--gobang online pc上使用谷歌浏览器比较友好@~@

代码仓库-- gobang tutorial 欢迎对此仓库进行扩展或star啦 @~@

前置知识点:阮生的es6教程和 MDN的canvas教程

以上, 兵马未动,粮草先行 。看官可以先体验下小游戏并且粗略了解下相关的知识点后(熟悉者可跳过,欢迎留言改进哈),再往下读。

前言

本来是没打算在掘金上再写关于canvas版本的五子棋小游戏文章的,因为之前已经在掘金上发表过类似的文章-- 谈谈前端实现五子棋游戏 。最近团队轮到自己分享,然而,在短短的一个星期的时间内没有想到比较实际可行的知识点或者项目拿来分享,毕竟工作日还得搬砖。于是乎,自己利用周末的时间将五子棋小游戏重新梳理了一波,整理成一个教程,使它成为自己在一个小时的分享会上面分享的干货。

秉承着 会就分享,不会就折腾 的宗旨。竟然已经整理了一波的教程,那就放出来给大伙指点指点。下面进入正题:

五子棋规则

五子棋的规则有点点复杂,我这里就简化并改写成下面这几条:

  1. 对局双方各执一色棋子。
  2. 空棋盘开局。
  3. 黑先、白后或者白先、黑后,交替下子,每次只能下一子。
  4. 横线、竖线或者斜线上有连续五个同一色的棋子,则游戏结束。

正式比赛的规则,看官可以到五子棋_百度百科这里了解。本博文的案例是以上面列出来的四条规则为基础,来实现五子棋小游戏的。

项目骨架

为了方便管理、扩展功能和编写代码,我这里使用了 es6的class语法 ,面向对象的思想来实现。首先,自己定义一个类 Gobang ,如下:

class Gobang { // 这里设置一个五子棋的类,统一管理代码
    // Gobang这个类的构造函数,options是在实例化的时候要传过来的值
    constructor(options={}){ // 设置参数的默认值,es6之前不允许这样设置
        this.options = options;
        // 初始化
        this.init();
    }
    // 初始化
    init() {
        const { options } = this;// 结构赋值
        console.log(options); // 打印出传入的实例的配置选项
    }
}

// 实例化对象
let gobangInstance1 = new Gobang(); // 没有传配置项的时候
let gobangInstance2 = new Gobang({
    canvas: 'chess'
}); // 传配置项的时候
复制代码

上面的 Gobang 类中,包含了一个 constructorinit 方法。其中 constructor 方法是类默认的方法,通过 new 命令生成对象实例时候,自动调用该方法。一个类必须有一个 constructor 方法,如果没有显式定义,一个空的 constructor 方法会默认添加。然后就是 init 方法了,这里我是整个类的初始化的入口方法。

项目骨架 代码在仓库中对应的位置是 skeleton

绘制棋盘

棋盘,我们可以分为两种,一种是视觉上的棋盘,另外一个是逻辑上的棋盘,你是看不见的。如下截图:

前端仔教你一步步实现人人对战五子棋小游戏【canvas详细版】

首先,我们实现 20*20 的物理上的棋盘,并且配上一些样式。当然,为了高可配置,我们使用上面代码骨架上的 options 进行传值:

// 实例化对象
let gobang = new Gobang({
    canvas: 'chess', // html中设定的画布的id
    gobangStyle: { // 五子棋的一些样式
        padding: 30, // 边和边之间的距离
        count: 20, // 棋盘的边数,整数
        borderColor: '#bfbfbf', // 描边的颜色
    }
});
复制代码

然后就进行物理棋盘的绘制了,这里是使用 canvas 的相关知识点,控制 画笔 更改着笔点并画线条:

// 绘制出物理棋盘
drawChessBoard() {
    const context = this.chessboard.getContext('2d');// 获取绘制上下文
    const {padding, count, borderColor} = this.options.gobangStyle;
    // 设置棋盘的宽高
    this.chessboard.width = this.chessboard.height = padding * count;
    // 设置画笔的颜色
    context.strokeStyle = borderColor;

    let half_padding = padding/2;// 考虑绘制的棋子展示的位置,所以要预留一些边距,可以审查元素看下
    // 画棋盘
    for(var i = 0; i < count; i++){
        context.moveTo(half_padding+i*padding, half_padding);
        context.lineTo(half_padding+i*padding, padding*count-half_padding);
        context.stroke(); // 这里绘制出的是竖轴
        context.moveTo(half_padding, half_padding+i*padding);
        context.lineTo(count*padding-half_padding, half_padding+i*padding);
        context.stroke(); // 这里绘制出的是横轴
    }
}
复制代码

接着就是逻辑的棋盘的记录了。这里我使用了二维数组去记录棋盘点的位置,比如 (0,0) 点对应的数组下标是 [0][0] ;然后 (1,2) 点对应的下标是 [1][2] ...以此类推。这里在记录好点之后,也为他们进行赋值为0,表示此处没有落子,如果有落子,记录为1(黑子)或2(白子)。具体逻辑棋盘代码如下:

// 绘制逻辑矩阵棋盘
initChessboardMatrix(){
    const {count} = this.options.gobangStyle;
    const checkerboard = [];
    // 存在(x,y)矩阵点
    for(let x = 0; x < count; x++){
        checkerboard[x] = [];
        for(let y = 0; y < count; y++){
            checkerboard[x][y] = 0; // 全部赋值为0,表示此坐标是没有棋子的
        }
    }
}
复制代码

绘制棋盘 代码在仓库中对应的位置是 chess_board

绘制棋子

绘制棋子这个简单。在标题中表明了是使用canvas的相关知识点,棋子是使用canvas来绘制的。具体用的canvas的知识点有 arc和createRadialGradient 方法。前者是绘制一个圆,后者是为这个圆添加颜色渐变效果,使得棋子看起来更加有质感。当然,这里需要绘制黑白两种颜色的棋子,需要有个flag来进行标识是否是黑色/白色,代码中有介绍。

drawChessman(x , y, isBlack){// 绘制的(x,y)坐标,isBlack判断是黑棋子还是白色棋子
    const context = this.chessboard.getContext('2d');
    context.beginPath();
    context.arc(x, y, 10, 0, 2 * Math.PI);// 画圆,半径这里设定为10px
    context.closePath();
    // 为棋子添加渐变颜色
    let gradient = context.createRadialGradient(x, y, 10, x-5, y-5, 0);// createRadialGradient(x1,y1,r1,x2,y2,r2)创建放射状/圆形渐变对象。
    if(isBlack){ // 黑子
        gradient.addColorStop(0,'#0a0a0a'); // 开始的颜色
        gradient.addColorStop(1,'#636766'); // 结束的颜色
    }else{ // 白子
        gradient.addColorStop(0,'#d1d1d1');
        gradient.addColorStop(1,'#f9f9f9');
    }
    context.fillStyle = gradient;
    context.fill();
}
复制代码

对应的效果图如下:

前端仔教你一步步实现人人对战五子棋小游戏【canvas详细版】

绘制棋子 代码在仓库中对应的位置是 chessman

落子实现人人对战

在上一节中,只是讲解了怎么去绘制棋子。接下来我们要将绘制好的棋子放到要下在棋盘的相关点击位置,并且实现黑白两棋的交替下棋,也就是实现人人对战啦。

首先,我们在初始化入口那里先初始化下棋子的角色(是黑棋还是白棋),获取单元格的宽度。

init() {
    // 角色,1是黑色棋子,2是白色棋子
    this.role = options.role || 1;

    // 单个格子的宽高
    this.lattice = {
        width: options.gobangStyle.padding,
        height: options.gobangStyle.padding
    };
}
复制代码

接下来就可以实行点击棋盘位置的计算了,获取相关的逻辑棋盘的坐标点,之后在这个坐标点进行棋子的绘制:

// 监听落子
listenDownChessman() {
    // 监听点击棋盘对象事件
    this.chessboard.onclick = event => {
        let {padding} = this.options.gobangStyle;
        // 获取棋子的位置(x,y)坐标,如(0,0),(0,2)
        let {
            offsetX: x,
            offsetY: y,
        } = event; // 解构赋值
        // console.log(x,y);
        x = Math.abs(Math.round((x-padding/2)/this.lattice.width));// 防止边界的为负数,故取绝对值
        y = Math.abs(Math.round((y-padding/2)/this.lattice.height));
        // console.log(x,y);
        // 点击的是棋盘,并且是空位置才可以落子
        if(this.checkerboard[x][y] !== undefined && Object.is(this.checkerboard[x][y],0)){
            // 更新矩阵值
            this.checkerboard[x][y] = this.role;
            // 刻画棋子
            this.drawChessman(x,y,Object.is(this.role , 1));
            // 切换棋子的角色
            this.role = Object.is(this.role , 1) ? 2 : 1;
        }
    }
}

// 刻画棋子
drawChessman(x,y,isBlack) {
    const context = this.chessboard.getContext('2d');
    const {padding} = this.options.gobangStyle;
    let half_padding = padding/2;
    context.beginPath();
    context.arc(half_padding+x*padding,half_padding+y*padding,half_padding-2,0,2*Math.PI);
    let gradient = context.createRadialGradient(half_padding+x*padding+2,half_padding+y*padding-2,half_padding-2,half_padding+x*padding+2,half_padding+y*padding-2,0);
    if(isBlack){
        gradient.addColorStop(0,'#0a0a0a');
        gradient.addColorStop(1,'#636766');
    }else{
        gradient.addColorStop(0,'#d1d1d1');
        gradient.addColorStop(1,'#f9f9f9');
    }
    context.fillStyle = gradient;
    context.fill();
}
复制代码

落子实现人人对战 代码在仓库中对应的位置是 listen_chessman

实现悔棋

在双方下棋中,允许对方或者自己对已经下的棋子进行调整,也就是悔棋,恢复上一步的操作,然后再重新下棋。实现悔棋功能的时候,需要知道下棋的历史记录和当前的落子步数和角色。

对于历史的记录,这里对每一步的落子都使用一个对象进行存储,并放到一个 history 的数组里面进行保存:

init() {
    // 走棋的历史记录
    this.history = [];
    // 当前步
    this.currentStep = 0;
}

listenDownChessman() {
    ...
    // 落子之后有可能悔棋之后落子,这种情况下应该重置历史记录
    this.history.length = this.currentStep;
    this.history.push({// 保存坐标和角色快照
        x,
        y,
        role: this.role
    });
    this.currentStep++;  // 当前步骤自加
    ...
}
复制代码

然后在执行悔棋的时候,将前一个记录的棋子的在棋盘上对应的ui给抹除掉就行了,不能将 history 中对应的位置移除哦,因为是要用到撤销悔棋的啊。销毁完棋子后,要对物理棋盘上的ui进行修补,修补的情况一共有九种:

  • 左上角棋盘
  • 左边缘棋盘
  • 左下角棋盘
  • 下边缘棋盘
  • 右下角棋盘
  • 右边缘棋盘
  • 右上角棋盘
  • 上边缘棋盘
  • 中间(非边界)棋盘
// 悔棋
regretChess() {
    // 找到最后一次记录,回滚到上一次的ui状态
    if(this.history.length){
        const prev = this.history[this.currentStep - 1];
        if(prev){
            const {
                x,
                y,
                role
            } = prev;
            // 销毁棋子
            this.minusStep(x,y);
            this.checkerboard[prev.x][prev.y] = 0; // 置空操作
            this.currentStep--; // 步数自减
            // 角色发生改变,下一步的下棋是该撤销棋子的角色
            this.role = Object.is(role,1) ? 1 : 2;
        }
    }
}
// 销毁棋子
minusStep(x, y) {
    const context = this.chessboard.getContext('2d');
    const {padding, count} = this.options.gobangStyle;
    context.clearRect(x*padding, y*padding, padding,padding);
    // 修补删除的棋盘位置
    // 重画该圆周围的格子,对边角的格式进行特殊的处理
    let half_padding = padding/2; // 棋盘单元格的一半
    if(x<=0 && y <=0){ // 情况比较多,一共九种情况
        this.fixchessboard(half_padding,half_padding,half_padding,padding,half_padding,half_padding,padding,half_padding);
    }else if(x>=count-1 && y<=0){
        this.fixchessboard(count*padding-half_padding,half_padding,count*padding-padding,half_padding,count*padding-half_padding,half_padding,count*padding-half_padding,padding);
    }else if(y>=count-1 && x <=0){
        this.fixchessboard(15,count*padding-half_padding,half_padding,count*padding-padding,half_padding,count*padding-half_padding,padding,count*padding-half_padding);
    }else if(x>=count-1 && y >= count-1){
        this.fixchessboard(count*padding-half_padding,count*padding-half_padding,count*padding-padding,count*padding-half_padding,count*padding-half_padding,count*padding-half_padding,count*padding-half_padding,count*padding-padding);
    }else if(x <=0 && y >0 && y <count-1){
        this.fixchessboard(half_padding,padding*y+half_padding,padding,padding*y+half_padding,half_padding,padding*y,half_padding,padding*y+padding);
    }else if(y <= 0 && x > 0 && x < count-1){
        this.fixchessboard(x*padding+half_padding,half_padding,x*padding+half_padding,padding,x*padding,half_padding,x*padding+padding,half_padding);
    }else if(x>=count-1 && y >0 && y < count-1){
        this.fixchessboard(count*padding-half_padding,y*padding+half_padding,count*padding-padding,y*padding+half_padding,count*padding-half_padding,y*padding,count*padding-half_padding,y*padding+padding);
    }else if(y>=count-1 && x > 0 && x < count-1){
        this.fixchessboard(x*padding+half_padding,count*padding-half_padding,x*padding+half_padding,count*padding-padding,x*padding,count*padding-half_padding,x*padding+padding,count*padding-half_padding);
    }else{
        this.fixchessboard(half_padding+x*padding,y*padding,half_padding+x*padding,y*padding + padding,x*padding,y*padding+half_padding,(x+1)*padding,y*padding+half_padding)
    }
}
// 修补删除后的棋盘
fixchessboard (a , b, c , d , e , f , g , h){
    const context = this.chessboard.getContext('2d');
    const {borderColor, lineWidth} = this.options.gobangStyle;
    context.strokeStyle = borderColor;
    context.lineWidth = lineWidth;
    context.beginPath();
    context.moveTo(a , b);
    context.lineTo(c , d);
    context.moveTo(e, f);
    context.lineTo(g , h);
    context.stroke();
}
复制代码

实现悔棋 代码在仓库中对应的位置是 regret_chess

实现撤销悔棋

有允许悔棋,那么就有允许撤销悔棋这样子才合理。同悔棋功能,撤销悔棋是需要知道下棋的历史记录和当前的步骤和棋子角色的。如下:

// 撤销悔棋
revokedRegretChess(){
    const next = this.history[this.currentStep]; // 撤销的点的下一个
    if(next) {
        this.drawChessman(next.x, next.y, next.role === 1); // 在上次撤销的点上画棋
        this.checkerboard[next.x][next.y] = next.role;
        this.currentStep++; // 当前步骤自加
        this.role = Object.is(this.role, 1) ? 2 : 1; // 角色的切换
    }
}
复制代码

实现撤销悔棋 代码在仓库中对应的位置是 revoked_regret_chess

胜利提示/游戏结束

五子棋的的结束也就是必须要决出胜利者,或者是棋盘没有位置可以下棋了。这里考虑决出胜利为游戏结束的切入点,上面也说到了如何才算是一方获胜-- 横线、竖线或者斜线上有连续五个同一色的棋子 。那么我们就对这四种情况进行处理了,我们在矩阵中记录当前点击的数组点中是否有连续的五个1(黑子)或者连续的五个2(白子)即可。如下截图的x轴获胜,注意gif图右侧打印出来的数组内容:

前端仔教你一步步实现人人对战五子棋小游戏【canvas详细版】

四种获胜的情况和或者的提示相关的代码如下:

// 裁判观察棋子,判断获胜一方
checkReferee(x , y , role) {
    if((x == undefined)||(y == undefined)||(role==undefined)) return;
    // 连杀的分数,五个同一色的棋子连成一条直线就是胜利
    let countContinuous = 0;
    const XContinuous = this.checkerboard.map(x => x[y]); // x轴上连杀
    const YContinuous = this.checkerboard[x]; // y轴上连杀
    const S1Continuous = []; // 存储左斜线连杀
    const S2Continuous = []; // 存储右斜线连杀
    this.checkerboard.forEach((_y,i) => {
        // 左斜线
        const S1Item = _y[y - (x - i)];
        if(S1Item !== undefined){
            S1Continuous.push(S1Item);
        }
        // 右斜线
        const S2Item = _y[y + (x - i)];
        if(S2Item !== undefined) {
            S2Continuous.push(S2Item);
        }
    });
    // 当前落棋点所在的X轴/Y轴/交叉斜轴,只要有能连起来的5个子的角色即有胜者
    [XContinuous, YContinuous, S1Continuous, S2Continuous].forEach(axis => {
        if(axis.some((x, i) => axis[i] !== 0 &&
                axis[i - 2] === axis[i - 1] &&
                axis[i - 1] === axis[i] &&
                axis[i] === axis[i + 1] &&
                axis[i + 1] === axis[i + 2])) {
            countContinuous++
        }
    });
    // 如果赢了就给出提示
    if(countContinuous){
        this.win = true;
        let msg = (role == 1 ? '黑' : '白') + '子胜利:v:';
        // 提示信息
        this.result.innerText = msg;
        // 不允许再操作
        this.chessboard.onclick = null;
    }
}
复制代码

胜利提示/游戏结束 代码在仓库中对应的位置是 winner_hint

嗯~至此,已经一步步讲解完如何开发一个能够在pc上愉快玩耍的休闲小游戏-五子棋了。当然,很多的参数我都是设置在 options 这里,其实为了更好的用户体验,你可以将这些设置在ui层面供用户自行调节的;再者你可以在项目基础上实现其他功能,比如人机对战等。如果有什么想法的话,欢迎下方留言或者前往此代码仓库 gobang-tutorial 进行相关动能补充或者完善@~@


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web Design in a Nutshell

Web Design in a Nutshell

Jennifer Niederst / O'Reilly Media, Inc. / 2006-02-21 / USD 34.99

Are you still designing web sites like it's 1999? If so, you're in for a surprise. Since the last edition of this book appeared five years ago, there has been a major climate change with regard to web......一起来看看 《Web Design in a Nutshell》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具