本文详细解析如何使用原生JavaScript结合HTML5 Canvas元素实现一个完整的人机对战五子棋游戏。内容涵盖从实例创建、棋盘绘制、棋子渲染、鼠标交互聚焦,到核心胜负算法初始化及计算机智能落子逻辑的完整实现过程。通过分步代码演示,帮助开发者掌握Canvas绘图API应用及基础游戏AI逻辑。
创建实例与初始化
首先,需要创建五子棋游戏的实例对象。该对象用于管理游戏状态,包括判断游戏是否结束、当前轮到哪一方(玩家或电脑)、记录所有棋子位置、已落下的棋子列表,以及存储赢法统计信息。
function Gobang () {
this.over = false; // 是否结束
this.player = true; // true:我 false:电脑
this.allChesses = []; // 所有棋子
this.existChesses = [] // 已经落下的棋子
this.winsCount = 0; // 赢法总数
this.wins = []; // 所有赢法统计
this.myWins = []; //我的赢法统计
this.computerWins = []; //电脑赢法统计
}
接着,初始化游戏逻辑。这包括生成Canvas棋盘、初始化棋盘网格、实现鼠标移动时的聚焦效果、初始化胜负判断算法,以及实现落子功能。这些步骤共同构成了游戏的基础框架。
//初始化
Gobang.prototype.init = function(opts) {
// 生成canvas棋盘
this.createCanvas(opts);
//棋盘初始化
this.boardInit();
// 鼠标移动聚焦功能实现
this.mouseMove();
//算法初始化
this.algorithmInit();
//落子功能实现
this.dorpChess();
}
在初始化过程中,首要任务是生成Canvas棋盘。代码会检查传入的宽度参数是否为30的倍数,如果不是则抛出范围错误。随后,根据宽度计算棋盘列数,创建Canvas元素并设置其宽高,最后将其添加到指定的容器或页面主体中,并获取绘图上下文。
生成Canvas棋盘
//初始化
//生成canvas
Gobang.prototype.createCanvas = function(opts) {
var opts = opts || {};
if (opts.width && opts.width%30 !== 0) throw new RangeError(opts.width+'不是30的倍数');
this.col = (opts.width && opts.width/30) || 15; // 棋盘列
var oCanvas = document.createElement('canvas');
oCanvas.width = oCanvas.height = opts.width || 450;
this.canvas = oCanvas;
document.querySelector(opts.container || 'body').appendChild(this.canvas);
this.ctx = oCanvas.getContext('2d');
}
棋盘初始化阶段,主要调用绘制棋盘的方法。该方法利用Canvas的绘图上下文,设置线条颜色,并通过循环绘制水平和垂直的线条,形成网格状的棋盘背景。
绘制棋盘网格
//棋盘初始化
Gobang.prototype.boardInit = function(opts){
this.drawBoard();
}
// 画棋盘
Gobang.prototype.drawBoard = function(){
this.ctx.strokeStyle = "#bfbfbf";
for (var i = 0; i < this.col; i++) {
this.ctx.moveTo(15+ 30*i, 15);
this.ctx.lineTo(15+ 30*i, this.col*30-15);
this.ctx.stroke();
this.ctx.moveTo(15, 15+ 30*i);
this.ctx.lineTo(this.col*30-15, 15+ 30*i);
this.ctx.stroke();
}
}

画棋子功能负责在指定的坐标位置绘制棋子。代码根据传入的坐标计算Canvas上的像素位置,使用arc方法绘制圆形。通过createRadialGradient创建径向渐变,根据玩家身份(黑棋或白棋)设置不同的颜色渐变效果,使棋子具有立体感。
绘制棋子与交互效果
// 画棋子
Gobang.prototype.drawChess = function(x, y, player){
var x = 15 + x * 30,
y = 15 + y * 30;
this.ctx.beginPath();
this.ctx.arc(x, y, 13, 0, Math.PI*2);
var grd = this.ctx.createRadialGradient(x + 2, y - 2, 13 , x + 2, y - 2, 0);
if (player) { //我 == 黑棋
grd.addColorStop(0, '#0a0a0a');
grd.addColorStop(1, '#636766');
}else{ //电脑 == 白棋
grd.addColorStop(0, '#d1d1d1');
grd.addColorStop(1, '#f9f9f9');
}
this.ctx.fillStyle = grd;
this.ctx.fill()
}

移动聚焦功能通过监听鼠标移动事件实现。当鼠标在棋盘上移动时,程序会清除当前画布内容,重新绘制棋盘和所有已落下的棋子,并根据鼠标位置计算当前悬停的坐标,绘制一个半透明的预览棋子,以提示玩家落子位置。
// 鼠标移动时触发聚焦效果, 需要前面的聚焦效果消失, 所有需要重绘canvas
Gobang.prototype.mouseMove = function(){
var that = this;
this.canvas.addEventListener('mousemove', function (e) {
that.ctx.clearRect(0, 0, that.col*30, that.col*30);
var x = Math.floor((e.offsetX)/30),
y = Math.floor((e.offsetY)/30);
//重绘棋盘
that.drawBoard();
//移动聚焦效果
that.focusChess(x, y);
//重绘已经下好的棋子
that.redrawedChess()
});
}
//鼠标移动聚焦
Gobang.prototype.focusChess = function(x, y){
this.ctx.beginPath();
this.ctx.fillStyle = '#E74343';
this.ctx.arc(15 + x * 30, 15 + y * 30, 6, 0, Math.PI*2);
this.ctx.fill();
}
//重绘当前下好的棋子
Gobang.prototype.redrawedChess = function(x, y){
for (var i = 0; i < this.existChesses.length; i++) {
this.drawChess(this.existChesses[i].x, this.existChesses[i].y, this.existChesses[i].player);
}
}

算法初始化阶段,程序会计算所有可能的获胜组合(赢法)。这包括横向、纵向和对角线方向的所有连续五子连线情况,为后续的胜负判断提供数据基础。
算法初始化与赢法统计
//算法初始化
Gobang.prototype.algorithmInit = function(){
//初始化棋盘的每个位置和赢法
for (var x = 0; x < this.col; x++) {
this.allChesses[x] = [];
this.wins[x] = [];
for (var y = 0; y < this.col; y++) {
this.allChesses[x][y] = false;
this.wins[x][y] = [];
}
}
//获取所有赢法
this.computedWins();
// 初始化电脑和我每个赢法当前拥有的棋子数
for (var i = 0; i < this.winsCount; i++) {
this.myWins[i] = 0;
this.computerWins[i] = 0;
}
}
获取所有赢法的具体实现中,程序遍历棋盘上的每一个交叉点,并检查以该点为起点的各种方向上的五子连线可能性。这些赢法被存储在数组中,用于后续判断玩家或电脑是否达成胜利条件。
Gobang.prototype.computedWins = function(){
/*
直线赢法
以15列为准
*/
for (var x = 0; x < this.col; x++) { //纵向所有赢法
for (var y = 0; y < this.col-4; y ++) {
this.winsCount ++;
/*
如:
1.组成的第一种赢法
[0,0]
[0,1]
[0,2]
[0,3]
[0,4]
2.组成的第二种赢法
[0,1]
[0,2]
[0,3]
[0,4]
[0,5]
以此类推一列最多也就11种赢法, 所有纵向x有15列 每列最多11种, 所有纵向总共15 * 11种
*/
//以下for循环给每种赢法的位置信息储存起来
for (var k = 0; k < 5; k ++) {
this.wins[x][y+k][this.winsCount] = true;
/*
位置信息
第一种赢法的时候:
this.wins = [
[
[1:true],
[1:true],
[1:true],
[1:true],
[1:true]
],
[
......
]
]
虽然这是一个三维数组, 我们把它拆分下就好理解了
相当于 this.wins[0][0][1], this.wins[0][4][1], this.wins[0][5][1], this.wins[0][6][1], this.wins[0][7][1]
因为对象可以这样取值:
var obj = {
a: 10,
b: 'demo'
}
obj['a'] === obj.a
所有也就相当于 this.wins[0][0].1, this.wins[0][8].1, this.wins[0][9].1, this.wins[0][10].1, this.wins[0][11].1
虽然数组不能这么取值,可以这么理解
所以 this.wins[0][0].1 就可以理解为 在 x=0, y=0, 上有第一种赢法
this.wins[0][12].1 就可以理解为 在 x=0, y=1, 上有第一种赢法
......
以上this.wins[0][0],this.wins[0][13]...可以看作是 this.wins[x][y]
所以第一种赢法的坐标就是: [0,0] [0,1] [0,2] [0,3] [0,4]
*/
}
}
}
for (var y = 0; y < this.col; y++) { //横向所有赢法, 同纵向赢法一样,也是15 * 11种
for (var x = 0; x < this.col-4; x ++) {
this.winsCount ++;
for (var k = 0; k < 5; k ++) {
this.wins[x+k][y][this.winsCount] = true;
}
}
}
交叉赢法的统计是胜负判断的核心。程序会根据当前棋盘状态,更新玩家和电脑的赢法统计数组。如果某一方在某个赢法上的得分达到五子,则判定该方获胜。

/*
交叉赢法
*/
for (var x = 0; x < this.col-4; x++) { // 左 -> 右 开始的所有交叉赢法 总共11 * 11种
for (var y = 0; y < this.col-4; y ++) {
this.winsCount ++;
/*
如:
1. [0,0]
[1,1]
[2,2]
[3,3]
[4,4]
2. [0,1]
[1,2]
[2,3]
[3,4]
[4,5]
3. [0,2]
[1,3]
[2,4]
[3,5]
[4,6]
...
[1,0]
[2,1]
[3,2]
[4,3]
[5,5]
相当于从左至右 一列列计算过去
*/
for (var k = 0; k < 5; k ++) {
this.wins[x+k][y+k][this.winsCount] = true;
}
}
}
for (var x = this.col-1; x >= 4; x --) { //右 -> 左 开始的所有交叉赢法 总共11 * 11种
for (var y = 0; y < this.col-4; y ++) {
this.winsCount ++;
for (var k = 0; k < 5; k ++) {
this.wins[x-k][y+k][this.winsCount] = true;
}
}
}
}
落子实现功能处理玩家的点击事件。当玩家点击棋盘时,程序会检查该位置是否已有棋子,如果没有,则在该位置绘制棋子,更新已落棋子列表,并检查是否产生胜利条件。如果游戏未结束,则切换轮到电脑落子。
落子逻辑与计算机智能
//落子实现
Gobang.prototype.dorpChess = function(){
var that = this;
this.canvas.addEventListener('click', function(e) {
// 判断是否结束
if (that.over) return;
var x = Math.floor((e.offsetX)/30),
y = Math.floor((e.offsetY)/30);
//判断该棋子是否已存在
if (that.allChesses[x][y]) return;
// 检查落子情况
that.checkChess(x, y)
if (!that.over) {
that.player = false;
that.computerDropChess();//计算机落子
}
})
}
//检查落子情况
Gobang.prototype.checkChess = function(x, y){
//画棋
this.drawChess(x, y, this.player);
//记录落下的棋子
this.existChesses.push({
x: x,
y: y,
player: this.player
});
//该位置棋子置为true,证明已经存在
this.allChesses[x][y] = true;
this.currWinChesses(x, y, this.player);
}
//判断当前坐标赢的方法各自拥有几粒棋子
Gobang.prototype.currWinChesses = function(x, y, player){
var currObj = player ? this.myWins : this.computerWins;
var enemyObj = player ? this.computerWins : this.myWins;
var currText = player ? '我' : '电脑';
for (var i = 1; i <= this.winsCount; i++) {
if (this.wins[x][y][i]) { //因为赢法统计是从1开始的 所以对应我的赢法需要减1
currObj[i-1] ++; // 每个经过这个点的赢法都增加一个棋子;
enemyObj[i-1] = 6; //这里我下好棋了,证明电脑不可能在这种赢法上取得胜利了, 置为6就永远不会到5
if (currObj[i-1] === 5) { //当达到 5 的时候,证明我胜利了
alert(currText+'赢了')
this.over = true;
}
}
}
}
计算机落子实现是游戏智能化的关键。程序会根据当前的棋盘状态,利用预设的赢法统计数组,计算每个空位的得分。通过评估各个方向的威胁和机会,选择得分最高的位置进行落子,从而实现基本的智能对战逻辑。
// 计算机落子
Gobang.prototype.computerDropChess = function(){
var myScore = [], //玩家比分
computerScore = [], // 电脑比分
maxScore = 0; //最大比分
//比分初始化
var scoreInit = function(){
for( var x = 0; x < this.col; x ++) {
myScore[x] = [];
computerScore[x] = [];
for (var y = 0; y < this.col; y ++) {
myScore[x][y] = 0;
computerScore[x][y] = 0;
}
}
}
scoreInit.call(this);
//电脑待会落子的坐标
var x = 0, y = 0;
// 基于我和电脑的每种赢法拥有的棋子来返回对应的分数
function formatScore(o, n) {
if (o < 6 && o > 0) {
var n = 10;
for (var i = 0; i < o; i++) {
n *= 3;
}
return n
}
return 0
}
// 获取没有落子的棋盘区域
function existChess(arr) {
var existArr = [];
for (var i = 0; i < arr.length; i++) {
for (var j = 0; j < arr[i].length; j++) {
if (!arr[i][j]) {
existArr.push({x:i, y:j})
}
}
}
return existArr;
}
var exceptArr = existChess(this.allChesses);
// 循环未落子区域,找出分数最大的位置
for (var i = 0; i < exceptArr.length; i++) {
var o = exceptArr[i];
// 循环所有赢的方法
for (var k = 0; k < this.winsCount; k++) {
//判断每个坐标对应的赢法是否存在
if (this.wins[o.x][o.y][k]) {
// 计算每种赢法,拥有多少棋子,获取对应分数
// 电脑起始分数需要高一些,因为现在是电脑落子, 优先权大
myScore[o.x][o.y] += formatScore(this.myWins[k-1], 10);
computerScore[o.x][o.y] += formatScore(this.computerWins[k-1], 11);
}
}
//我的分数判断
if (myScore[o.x][o.y] > maxScore) { //当我的分数大于最大分数时, 证明这个位置的是对我最有利的
maxScore = myScore[o.x][o.y];
x = o.x;
y = o.y;
}else if (myScore[o.x][o.y] === maxScore) { //当我的分数与最大分数一样时, 证明我在这两个位置下的效果一样, 所以我们应该去判断在这两个位置时,电脑方对应的分数
if (computerScore[o.x][o.y] > computerScore[x][y]) {
x = o.x;
y = o.y;
}
}
// 电脑分数判断, 因为是电脑落子, 所以优先权大
if (computerScore[o.x][o.y] > maxScore) {
maxScore = computerScore[o.x][o.y];
x = o.x;
y = o.y;
}else if (computerScore[o.x][o.y] === maxScore) {
if (myScore[o.x][o.y] > myScore[x][y]) {
x = o.x;
y = o.y;
}
}
}
this.checkChess(x, y)
if (!this.over) {
this.player = true;
}
}
var gobang = new Gobang();
gobang.init()
完整的源代码可以在GitHub上获取,方便开发者下载和学习。
此外,还提供了在线演示地址,用户可以直接在浏览器中体验五子棋人机对战的效果。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。