1. 项目整体设计:先拆清“HTML、AI、象棋”三件事的关系
写一个HTML版AI象棋程序,起源其实很朴素。有一天我想在浏览器里跟电脑下盘象棋,但不想登录棋类平台、不想被广告挡住,更不想为了一个小功能去启动 Python 后端。我需要的只是一个用 HTML、CSS、JavaScript 写的单文件页面,双击 index.html 就能跑起来,能正常下棋,AI会响应,顺手还能悔棋、换先手、调难度。
这个标题拆开看是三件事:HTML负责搭出页面壳子,AI负责模拟“对手”,象棋程序负责把车马炮兵卒的走法、将军、胜负这套规则跑起来。很多新手一看到“AI”两个字就发怵,以为要自己从头写神经网络。其实象棋的AI完全可以走传统博弈树路线:程序准备好所有合法走法,利用估值函数给棋盘打分,再用搜索算法挑出最有利的着法。这套实现不依赖显卡、不需要训练数据,一台普通电脑都能跑,代码量也远没有想象中大。
如果你想用这个项目练手,或者要做课程设计、比赛演示,这篇文章会把它拆成几个可以独立验收的模块:棋盘界面、数据模型、规则引擎、AI搜索、交互细节。完整的工程里,模块顺序应该是先做棋盘渲染和基础数据结构,再做走法生成与合法性过滤,最后接入AI评估函数和搜索剪枝。我的建议是不要一上来就写搜索,先把“人能走子、能吃子、能判断将军”做出来,AI部分整个就是一道加法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 棋盘与规则引擎:让双方先把子“走明白”
2.1 用二维数组保存棋局,不用DOM节点管数据
象棋棋盘是9列10行,落子位置是离散的格子,最适合的数据结构就是二维数组。开局时初始化一个数组,每个位置要么是null,要么是包含side和type的对象,例如红色“车”就是 { side: 'red', type: '车' }。这个方案比把棋子全部铺在DOM节点上更好调试,因为在AI搜索过程中,我们要反复模拟走子、撤销、再走子,频繁操作DOM会非常慢,但操作二维数组只是内存读写。
坐标这里的“行”是从上往下数的,0到9;列是从左到右,0到8。棋盘上方是黑方,下方是红方。可以这样初始化棋盘:
javascript复制const ROWS = 10;
const COLS = 9;
function createInitBoard() {
const board = Array.from({ length: ROWS }, () => Array(COLS).fill(null));
const redBack = ['车', '马', '相', '仕', '帅', '仕', '相', '马', '车'];
const blackBack = ['车', '马', '象', '士', '将', '士', '象', '马', '车'];
for (let x = 0; x < COLS; x++) {
board[9][x] = { side: 'red', type: redBack[x] };
board[0][x] = { side: 'black', type: blackBack[x] };
}
board[7][1] = { side: 'red', type: '炮' };
board[7][7] = { side: 'red', type: '炮' };
board[2][1] = { side: 'black', type: '炮' };
board[2][7] = { side: 'black', type: '炮' };
for (let x = 0; x < COLS; x += 2) {
board[6][x] = { side: 'red', type: '兵' };
board[3][x] = { side: 'black', type: '卒' };
}
return board;
}
注意红方在下方、黑方在上方。如果有同学习惯把红方放在上面,后面所有关于“兵过河”“仕不出九宫”的判断都要同步反过来,非常容易出错。用这种上下结构,红方“帅”的活动范围是第7到第9行、第3到第5列,恰好落在底部的九宫格里,语义很直观。
2.2 用Canvas画棋盘,把像素坐标换算成行列
画棋盘我用Canvas 2D,不直接写格子的div,理由和前面说的一样:界面要能快速重绘。玩家走一步,AI思考后再走一步,中间还要高亮选中棋子、显示可走位置,Canvas重绘比反复增删DOM节点轻松得多。
画盘的基础参数一般是:四周留30像素左右的边距,内部网格区域宽度固定成一个方便计算的值,比如每个格子边长50像素,那么棋盘总宽度大约是950+两倍边距,高度是1050+边距。下面是裁剪过的绘制逻辑:
javascript复制const margin = 30;
const cellSize = 50;
function boardX(col) {
return margin + col * cellSize;
}
function boardY(row) {
return margin + row * cellSize;
}
画竖线时不需要每条都从第0行画到第9行,因为最左侧和最右侧会形成棋盘外框;画横线时中间两条河界处的横线被楚河汉界文字截断,所以常用循环分两段画。九宫斜线也要单独判断:红方九宫在下方,黑方九宫在上方。
绘制棋子时比较省事的做法是每个棋子画一个圆,再调用 ctx.fillText 画中文棋子名。红方用红色描边、红色文字,黑方用黑色。选中的棋子额外画一圈亮色外环,可走位置用半透明小圆点表示,这样人机交互会清晰很多。
这里有一个比较隐蔽的坑:浏览器高分屏缩放。如果在普通笔记本上看起来正常,换到Retina屏幕,Canvas棋盘和棋子会明显发虚。原因是设备的物理像素密度比CSS像素高,Canvas默认按设备像素渲染。解决方式是在窗口尺寸变化时重新设置canvas.width大小:
javascript复制function resizeCanvas() {
const dpr = window.devicePixelRatio || 1;
const displayWidth = document.getElementById('board').clientWidth;
const displayHeight = displayWidth * (10 / 9);
canvas.style.width = displayWidth + 'px';
canvas.style.height = displayHeight + 'px';
canvas.width = displayWidth * dpr;
