前端Canvas入门——用canvas写五子棋?

前言

五子棋的实现其实不难,因为本身就是一个很小的游戏。

至于画线什么的,其实很简单,都是lineTo(),moveTo()就行了。

难的在于——怎么让棋子落入到指定的格子上,怎么判断连子胜利。

当然啦,这部分是参考了别人的做法去实现的,也终于理解什么叫做CV工程师了。

But,修复bug还是用了我不少时间。

OK,坐稳扶好,下面开始~

实现

创建画布

至于创建canvas这部分就不写了,直接使用JS创建会有代码提示,非常友好~(如果不会,可以查看上条:用JS创建canvas

我这里创建的画布大小是800*800的,你可以根据实际需要,创建出对应的画布大小。

画线布局

这部分很简单,都是重复的内容,但如果你创建的棋盘很大,那你画线就很多了,虽然可以不断CV,但是改来改去,看得眼睛都疼。

而且这也不符合低代码的特性,所以咱们还是使用循环来写吧:

for (let i = 1; i < 16; i++) {
    ctx.beginPath()
    ctx.moveTo(50, 50 * i)
    ctx.lineTo(750, 50 * i)
    ctx.stroke()
    ctx.closePath()

    ctx.beginPath()
    ctx.moveTo(50 * i, 50)
    ctx.lineTo(50 * i, 750)
    ctx.stroke()
    ctx.closePath()
}

写完运行,你就可以得到: 

记住,不要画满,因为上下左右都要有边距,棋盘不就是这个样子的吗?

下棋落子

画棋子

这部分很简单,就是画一个圆而已,直接:

ctx.arc(x, y, 20, 0, Math.PI * 2)
ctx.fill()

就可以了,但是别忘了我们是下棋的,所以需要监听点击事件,给Canvas添加点击事件即可。

但还有个问题,棋子不在线的相交点上,so,我们得解决这个问题。

怎么解决?

因为我们是在canvas中点击的,Canvas是有坐标系的,所以我们只需要获取到点击的坐标即可知道鼠标点击在哪里了。

这是个好主意,但是怎么获取到点击坐标?

用点击事件的e即可,从中解构出offsetX、offsetY即可。

canvas.addEventListener('click', function handlClick(e)  {
    let { offsetX, offsetY } = e
}

落子

既然获取到点击坐标了,我们就要划定范围了。

首先就是限制棋子的范围必须要在画好的线内,这个好办,只需要限制范围即可:

if(offsetX < 25 || offsetX > 775 || offsetY < 25 || offsetY > 775){
    return
}

但是点击的时候,不一定是精确点击到相交点的,所以我们还需要对点击坐标进行一定的处理——这也是为什么上面是25,而不是50的原因。

let x = Math.floor((offsetX + 25) / 50) * 50
let y = Math.floor((offsetY + 25) / 50) * 50

我们只需要让点击的坐标,先在X、Y轴加上25,然后除以50向下取整即可。

为什么要先除以50,然后又乘以50?可以思考一下~

然后补上上面说的画棋子,即可让棋子完美落入到格子上,完整代码:

canvas.addEventListener('click', function handlClick(e)  {
    let { offsetX, offsetY } = e

    if(offsetX < 25 || offsetX > 775 || offsetY < 25 || offsetY > 775){
        return
    }

    let x = Math.floor((offsetX + 25) / 50) * 50
    let y = Math.floor((offsetY + 25) / 50) * 50

    ctx.beginPath()
    ctx.arc(x, y, 20, 0, Math.PI * 2)
    ctx.fill()
    ctx.closePath()
}

黑白双煞

完成上面的步骤可以发现,为什么只有一种颜色的棋子?

我白色的呢?去哪里了?怎么解决这个问题?

这里我们先在监听函数外面定义一个变量,取啥名都可以,这里咱:

let isBlack = true

因为默认是黑子先下,所以定义isBlack,默认是true的,然后咱们再使用三元变量定义一下fillStyle即可~:

ctx.fillStyle = isBlack ? "black": "white"

这个时候,还不行,我们需要在监听函数的默认改变isBlack的值:

isBlack = !isBlack

到目前为止,监听函数的代码:

let isBlack = true
canvas.addEventListener('click', function handlClick(e)  {
    let { offsetX, offsetY } = e

    if(offsetX < 25 || offsetX > 775 || offsetY < 25 || offsetY > 775){
        return
    }

    let x = Math.floor((offsetX + 25) / 50) * 50
    let y = Math.floor((offsetY + 25) / 50) * 50

    ctx.beginPath()
    ctx.arc(x, y, 20, 0, Math.PI * 2)
    ctx.fillStyle = isBlack ? "black" : "white"
    ctx.fill()
    ctx.closePath()

    isBlack = !isBlack
}

重复落子

这个时候,运行代码,打开浏览器,可以发现,点击同一个地方,居然可以变色,这太不符合常理了,这不应该啊~

怎么解决?

记录下子位置。

用神马记录?数组?好像可以?

创建数组

我们先在外面定义一个数组,用来接受棋子位置。

在创建棋盘的时候,我们在对应地生成一个二维数组。

这个时候,每次点击都可以记录棋子的位置了:

let circles = []
for (let i = 0; i <= 16; i++) {
    circles[i] = []
}

对了,我这个是包括了边框的,如果你不需要,可以自己修改一下范围值。

记录位置

记录位置还是在监听的时候,在画棋子之前进行即可。

记录棋子位置很简单,我们只需要棋子在二维数组中所在的位置即可。

也就是,我们只需要知道它在数组中的位置,而不是坐标。

i = X/50,j = Y/50即可

禁止重复落子

在点击的时候判断这个地方是否在二维数组中存在数值,如果有,就结束函数往下运行,并给出提示。

if(circles[i][j]){
    tip.innerHTML = '不能重复落子'
    return
}

这里的tip应该要先定义,都会做吧(狗头)。

这部分代码:

const tip = document.querySelector('.tip')
let isBlack = true
let circles = []
for (let i = 0; i <= 16; i++) {
    circles[i] = []
}
canvas.addEventListener('click', function handlClick(e)  {
    let { offsetX, offsetY } = e

    if(offsetX < 25 || offsetX > 775 || offsetY < 25 || offsetY > 775){
        return
    }

    let i = Math.floor((offsetX + 25) / 50)
    let j = Math.floor((offsetY + 25) / 50)

    if(circles[i][j]){
        tip.innerHTML = '不能重复落子'
        return
    }

    let x = i * 50
    let y = j * 50
    circles[i][j] = isBlack ? 'black' : 'white'

    ctx.beginPath()
    ctx.arc(x, y, 20, 0, Math.PI * 2)
    ctx.fillStyle = isBlack ? "black" : "white"
    ctx.fill()
    ctx.closePath()

    tip.innerHTML = isBlack ? '请白棋落子' : '请黑棋落子'
    isBlack = !isBlack
}

都能看懂吧?

游戏胜利

终于来到我们最喜欢的环节了,怎么判断游戏胜利?

判断游戏胜利,不就是横竖斜四个方向上判断是否连续5个子吗?

为什么是四个方向,横竖斜不就三个方向吗?

OK,开始写函数。

竖向判定

直接给代码:

function checkULine(row, col){
    let up = 1
    let down = 1
    let count = 1
    let target = isBlack ? 'black' : 'white'
    let times = 0
    while(times < 100){
        times++
        
        if(circles[row][col - up <= 0 ? 0 : col - up] && circles[row][col - up <= 0 ? 0 : col - up] == target){
            count++
            up++
        }
        
        if(circles[row][col + down >= 16 ? 16 : col + down] && circles[row][col + down >= 16 ? 16 : col + down] == target){
            count++
            down++
        }
        if(count >= 5 || (circles[row][col - up <= 0 ? 0 : col - up] !== target && circles[row][col + down >= 16 ? 16 : col + down] !== target)){
            break
        }
    }
    return count >= 5
}

首先就是函数需要两个参数,当前行,当前列。

因为是竖向判定,所以我们需要从当前棋子往上寻找,或者是往下寻找。

因为本身就是有一个子了,所以直接定义count为1。

你也可以定义为0,然后判断count大于等于4即可。

道路千万条,CV第一条。

接着就是开始往上往下寻找,这个函数是一直寻找,直到碰到边界,所以需要边界值判断,否则会报错。

至于别的bug,我没遇到,要是有的话,可以评论区发给我看看~

横向方向上的判定,我就不展开细说了,直接给代码:

function checkDLine(row, col){
    let left = 1
    let right = 1
    let count = 1
    let target = isBlack ? 'black' : 'white'
    let times = 0
    while(times < 100){
        times++
        
        if(circles[row - left <= 0 ? 0 : row - left][col] && circles[row - left <= 0 ? 0 : row - left][col] == target){
            count++
            left++
        }
        
        if(circles[row + right >= 16 ? 16: row + right][col] && circles[row + right >= 16 ? 16: row + right][col] == target){
            count++
            right++
        }
        if(count >= 5 || (circles[row - left <= 0 ? 0 : row - left][col] !== target && circles[row + right >= 16 ? 16: row + right][col] !== target)){
            break
        }
    }
    return count >= 5
}

斜向判定

这里举例从左上到右下:

function checkUX(row, col){
    let lt = 1
    let rb = 1
    let count = 1
    let target = isBlack ? 'black' : 'white'
    let times = 0

    while(times < 100){
        times++
        
        if(circles[row - lt <= 0 ? 0 : row - lt][col - lt <=  0 ? 0 : col - lt] && circles[row - lt <= 0 ? 0 : row - lt][col - lt <=  0 ? 0 : col - lt] == target){
            count++
            lt++
        }
        
        if(circles[row + rb >= 16 ? 16 : row + rb][col + rb >= 16 ? 16 : col + rb] && circles[row + rb >= 16 ? 16 : row + rb][col + rb >= 16 ? 16 : col + rb] == target){
            count++
            rb++
        }
        if(count >= 5 || (circles[row - lt <= 0 ? 0 : row - lt][col - lt <=  0 ? 0 : col - lt] !== target && circles[row + rb >= 16 ? 16 : row + rb][col + rb >= 16 ? 16 : col + rb] !== target)){
            break
        }
    }
    return count >= 5
}

其实也很简单理解,只是这个时候,不只是一个方向上寻找了而已,所以显得比较困难。

有兴趣的,可以自己画个2维数组去理解一下,这里不给出。

右上到左下:

function checkDX(row, col){
    let rt = 1
    let lb = 1

    let count = 1
    let target = isBlack ? 'black' : 'white'
    let times = 0
    while(times < 100){
        times++
        
        if (circles[row + rt >= 16 ? 16 : row + rt][col - rt <= 0 ? Math.abs(col-rt) : col - rt] && circles[row + rt >= 16 ? 16 : row + rt][col - rt <= 0 ? Math.abs(col-rt) : col - rt] == target) {
            count++
            rt++
        }
        
        if (circles[row - lb <= 0 ? 0 : row - lb][col + lb >= 16 ? 16 : col + lb] && circles[row - lb <= 0 ? 0 : row - lb][col + lb >= 16 ? 16 : col + lb] == target) {
            count++
            lb++
        }
        if(count >= 5 || (circles[row + rt >= 16 ? 16 : row + rt][col - rt <= 0 ? Math.abs(col-rt) : col - rt] !== target && circles[row - lb <= 0 ? 0 : row - lb][col + lb >= 16 ? 16 : col + lb] !== target)){
            break
        }
    }
    return count >= 5
}

有能力的,可以尝试着把这几个函数集成为一个。

我是不行了,Level太低了,解决不了兼容性的bug。

函数调用

函数是写了,但是你需要调用啊。

在调用之前,咱们先定义一个变量用来判断是否胜利了。

let endGame = false

接着,在监听函数中,调用这些函数:

canvas.addEventListener('click', function handlClick(e)  {
    let { offsetX, offsetY } = e

    if(offsetX < 25 || offsetX > 775 || offsetY < 25 || offsetY > 775){
        return
    }

    let i = Math.floor((offsetX + 25) / 50)
    let j = Math.floor((offsetY + 25) / 50)

    if(circles[i][j]){
        tip.innerHTML = '不能重复落子'
        return
    }

    let x = i * 50
    let y = j * 50
    circles[i][j] = isBlack ? 'black' : 'white'

    ctx.beginPath()
    let tx = isBlack ? x - 14 : x + 14
    let ty = isBlack ? y - 14 : y + 14
    const g = ctx.createRadialGradient(tx, ty, 0, tx, ty, 60)
    g.addColorStop(0, isBlack ? "#ccc" : "#666")
    g.addColorStop(0.5, isBlack ? "black" : "white")
    ctx.arc(x, y, 20, 0, Math.PI * 2)
    ctx.fillStyle = g
    ctx.fill()
    ctx.closePath()

    tip.innerHTML = isBlack ? '请白棋落子' : '请黑棋落子'
    endGame = checkULine(i, j) || checkDLine(i, j) || checkUX(i, j) || checkDX(i, j)
    if(endGame){
        setTimeout(() => {
            alert(`游戏结束,${ isBlack ? '黑方' : '白方' }获胜!`)
            location.reload() // 刷新本页
        }, 10)
        return
    }
    isBlack = !isBlack
})

这里我是添加了渐变色,让棋子更具立体感。

tx、ty是用来计算径向渐变圆心所在位置的。

致谢

非常感谢B站大佬,叩丁狼,Respect!

写在最后

可以改善的地方:

25、775这个数字能不能去掉呢?

结束游戏用的是弹窗,能不能更高级一点呢?

多屏适配。

如果棋子全下满了,怎么办?

OK, Bye~ 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值