DOM 结构搭建:游戏的基石
这里,我们精心定义了一系列响应式数据,它们如同游戏的 “神经中枢”,掌控着游戏的各个关键环节。
canvasRef和context2D负责与页面的canvas元素交互,获取绘图的能力;isMe决定着回合的归属;chessBoard记录着棋盘上的每一步落子;wins、count、meWin和AIWin则紧密协作,为判断游戏胜负提供依据;gameOver则是游戏结束的标志。在组件挂载后,我们确保能够正确获取canvas的绘图上下文,为后续的精彩绘图操作做好充分准备。
<script setup lang="ts">
// 用于获取 canvas 元素的引用,初始化为 null
const canvasRef = ref<HTMLCanvasElement|null>(null);
// 保存 canvas 的 2D 绘图上下文,初始为 null
const context2D = ref<CanvasRenderingContext2D|null>(null);
// 标记是否轮到玩家(“我”)落子,初始是玩家回合
const isMe = ref(true);
// 二维数组,记录棋盘上每个位置的状态,0 表示无子 1 表示玩家 2 表示AI
const chessBoard = ref<number[][]>([]);
// 三维数组,存储所有可能的赢法集合
const wins = ref<boolean[][][]>([]);
// 赢法的总数,初始为 0
const count = ref(0);
// 记录玩家在每种赢法下的落子数量,用于判断输赢
const meWin = ref<number[]>([]);
// 记录 AI 在每种赢法下的落子数量,用于判断输赢
const AIWin = ref<number[]>([]);
// 标记游戏是否结束,初始未结束
const gameOver = ref(false);
onMounted(() => {
if (canvasRef.value){
context2D.value = canvasRef.value.getContext("2d");
drawChessBoard();
};
})
</script>
<template>
<canvas class="board" ref="canvasRef" width="450px" height="450px"></canvas>
</template>
绘制棋盘:打造对战舞台
在这个函数中,我们巧妙地运用了
canvas的绘图 API。首先,设定好线条的颜色,然后通过两层循环,分别绘制纵线和横线。对于每条线,我们精确地计算起点和终点的坐标,从棋盘的内边距(四边各留15px)开始,以30px的均匀间隔绘制,最终呈现出一个标准的五子棋棋盘,为棋子的落子提供了清晰的 “战场”。
// 绘制棋盘的函数
const drawChessBoard = () => {
// 设置棋盘线条颜色为浅灰色
context2D.value!.strokeStyle = "#BFBFBF";
// 循环绘制纵线
for (let i = 0; i < 15; i++) {
// 移动到纵线起点
context2D.value!.moveTo(15 + i * 30, 15);
// 绘制纵线到终点
context2D.value!.lineTo(15 + i * 30, 435);
// 绘制线条
context2D.value!.stroke();
}
// 循环绘制横线
for (let i = 0; i < 15; i++) {
// 移动到横线起点
context2D.value!.moveTo(15, 15 + i * 30);
// 绘制横线到终点
context2D.value!.lineTo(435, 15 + i * 30);
// 绘制线条
context2D.value!.stroke();
}
}
绘制棋子:落子的艺术呈现
在这个函数里,我们先开启绘图路径,然后使用
arc方法绘制出圆形的棋子轮廓。为了让棋子更加美观逼真,我们运用了createRadialGradient方法创建了径向渐变效果,根据传入的参数判断是玩家棋子还是 AI 棋子,分别设置不同的颜色渐变,从圆心向外扩散,模拟出棋子的立体感。最后,通过设置填充样式并填充路径,将精美的棋子呈现在棋盘上,每一次落子都仿佛是一场艺术的创作。
// 绘制棋子的函数
const drawPieces = (i: number, j: number, me: boolean) => {
// 开始绘制路径
context2D.value!.beginPath();
// 绘制圆形棋子,圆心根据棋盘格位置计算,半径为 13px
context2D.value!.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);
// 闭合路径
context2D.value!.closePath();
// 创建径向渐变,用于棋子的颜色填充,实现立体效果
const gradient = context2D.value!.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13, 15 + i * 30 + 2, 15 + j * 30 - 1, 0);
if (me) {
// 玩家棋子的颜色渐变设置,从黑色到深灰色
gradient.addColorStop(0, "#0A0A0A");
gradient.addColorStop(1, "#636766");
} else {
// AI 棋子的颜色渐变设置,从浅灰色到白色
gradient.addColorStop(0, "#D1D1D1");
gradient.addColorStop(1, "#F9F9F9");
}
// 设置填充样式为渐变
context2D.value!.fillStyle = gradient;
// 填充棋子颜色
context2D.value!.fill();
}
初始化棋盘与赢法:布局胜负规则
initializeBoard函数通过两层嵌套循环,将chessBoard二维数组的每个元素初始化为0,代表棋盘上所有位置初始均无子。而initializeWinningPatterns函数则是整个游戏胜负判断的关键所在。它通过多层嵌套循环,仔细地构建了所有可能的赢法集合,包括横线、纵线、斜线和反斜线这四种情况,每种赢法在wins三维数组中都有对应的位置标记为true,同时记录赢法的总数count。最后,将meWin和AIWin数组的每个元素初始化为0,为后续统计双方在各种赢法下的落子情况做好铺垫,确保游戏的胜负判断能够准确无误地进行。
// 初始化棋盘可落子节点的函数
const initializeBoard = () => {
for (let i = 0; i < 15; i++) {
// 为每一行创建一个空数组
chessBoard.value[i] = [];
for (let j = 0; j < 15; j++) {
// 初始化每个棋盘格为无子状态(0)
chessBoard.value[i][j] = 0;
}
}
}
// 初始化所有赢法集合的函数
const initializeWinningPatterns = () => {
for (let i = 0; i < 15; i++) {
wins.value[i] = [];
for (let j = 0; j < 15; j++) {
wins.value[i][j] = [];
}
}
// 初始化横线赢法
for (let i = 0; i < 15; i++) {
for (let j = 0; j < 11; j++) {
for (let k = 0; k < 5; k++) {
wins.value[i][j + k][count.value] = true;
}
count.value++;
}
}
// 初始化纵线赢法
for (let i = 0; i < 15; i++) {
for (let j = 0; j < 11; j++) {
for (let k = 0; k < 5; k++) {
wins.value[j + k][i][count.value] = true;
}
count.value++;
}
}
// 初始化斜线赢法
for (let i = 0; i < 11; i++) {
for (let j = 0; j < 11; j++) {
for (let k = 0; k < 5; k++) {
wins.value[i + k][j + k][count.value] = true;
}
count.value++;
}
}
// 初始化反斜线赢法
for (let i = 0; i < 11; i++) {
for (let j = 14; j > 3; j--) {
for (let k = 0; k < 5; k++) {
wins.value[i + k][j - k][count.value] = true;
}
count.value++;
}
}
// 初始化玩家和 AI 的每种赢法统计数组
for (let i = 0; i < count.value; i++) {
meWin.value[i] = 0;
AIWin.value[i] = 0;
}
}
检查赢的状态:胜负一念之间
在这个函数中,我们遍历所有的赢法集合,当棋子落在与某个赢法相关的位置时(
wins[i][j][k]为true),根据落子的玩家(player),在对应的meWin或AIWin数组中增加该赢法的统计数量,并将对方在该赢法下的统计置为无效(-1)。然后,检查统计数量是否达到了获胜所需的5颗棋子,如果满足条件,则返回true,表示该玩家获胜;如果遍历完所有赢法后都未满足获胜条件,则返回false,游戏继续进行,胜负就在这一次次的检查中逐渐明晰。
// 检查赢的状态的函数
const checkWin = (i: number, j: number, player: 1 | 2) => {
for (let k = 0; k < count.value; k++) {
if (wins.value[i][j][k]) {
if (player === 1) {
// 玩家落子后,对应赢法的统计数量加 1
meWin.value[k]++;
// 将 AI 在该赢法下的统计置为 -1,表示无效
AIWin.value[k] = -1;
// 判断玩家是否在该赢法下达到 5 颗棋子,即获胜
if (meWin.value[k] >= 5) {
return true;
}
} else {
// AI 落子后,对应赢法的统计数量加 1
AIWin.value[k]++;
// 将玩家在该赢法下的统计置为 -1,表示无效
meWin.value[k] = -1;
// 判断 AI 是否在该赢法下达到 5 颗棋子,即获胜
if (AIWin.value[k] >= 5) {
return true;
}
}
}
}
return false;
}
落子逻辑:游戏的核心交互
在
placePiece函数中,首先进行了落子的合法性检查,确保棋子只能落在空的棋盘格上,并且游戏未结束。然后,调用drawPieces函数绘制出相应的棋子,并更新chessBoard数组记录落子情况。接着,通过checkWin函数检查此次落子是否导致游戏胜负结果的产生,如果有玩家获胜,则设置gameOver为true,并在控制台输出获胜信息。如果游戏仍未结束,通过切换isMe的值来实现回合的交替,并且当玩家落子后,立即调用computerAI函数,让 AI 进行下一步落子决策,整个游戏流程在这一函数中流畅地运转起来。
// 落子的函数
const placePiece = (i: number, j: number, player: 1 | 2) => {
// 检查该位置是否可以落子以及游戏是否已结束
if (chessBoard.value[i][j]!== 0 || gameOver.value) return;
// 绘制棋子
drawPieces(i, j, player === 1);
// 更新棋盘状态,记录落子情况
chessBoard.value[i][j] = player;
// 检查落子后是否有玩家获胜
if (checkWin(i, j, player)) {
gameOver.value = true;
console.log(player === 1? "黑棋赢了🥇" : "白棋赢了✌️");
}
// 如果游戏未结束,切换回合
if (!gameOver.value) {
isMe.value =!isMe.value;
// 如果是玩家落子后,轮到 AI 落子
if (player === 1) {
computerAI();
}
}
}
玩家与 AI 落子交互:人机对决的智慧碰撞
玩家通过鼠标点击棋盘进行落子,而 AI 则通过复杂的算法来选择落子位置,这两者的交互构成了游戏的精彩对战过程。
玩家点击落子
在
handleClick函数中,当玩家点击canvas棋盘时,首先检查是否轮到玩家回合(isMe为true),如果是,则获取点击位置的坐标,并通过简单的数学计算将其转换为棋盘格的索引值,最后调用placePiece函数完成玩家的落子操作,整个过程简洁明了,符合玩家的操作习惯。
// 玩家落子的点击事件处理函数
const handleClick = (event: MouseEvent) => {
// 检查是否轮到玩家落子
if (!isMe.value) return;
// 获取鼠标点击在 canvas 上的坐标
const x = event.offsetX;
const y = event.offsetY;
// 计算棋子在棋盘格中的位置索引
const i = Math.floor(x / 30);
const j = Math.floor(y / 30);
// 执行落子操作
placePiece(i, j, 1);
}
AI 落子决策
// AI 落子的函数
const computerAI = () => {
// 用于记录每个空棋盘格对于玩家的得分
const meScore: number[][] = [];
// 用于记录每个空棋盘格对于 AI 的得分
const AIScore: number[][] = [];
let max = 0;
let u = 0;
let v = 0;
// 初始化得分数组
for (let i = 0; i < 15; i++) {
meScore[i] = [];
AIScore[i] = [];
for (let j = 0; j < 15; j++) {
meScore[i][j] = 0;
AIScore[i][j] = 0;
}
}
// 遍历棋盘上的每个空棋盘格
for (let i = 0; i < 15; i++) {
for (let j = 0; j < 15; j++) {
if (chessBoard.value[i][j] === 0) {
// 对于每个空棋盘格,检查其在所有赢法中的情况
for (let k = 0; k < count.value; k++) {
if (wins.value[i][j][k]) {
// 根据玩家和 AI 在该赢法下的已有落子情况,计算得分
if (meWin.value[k] === 1) {
meScore[i][j] += 200;
} else if (meWin.value[k] === 2) {
meScore[i][j] += 400;
} else if (meWin.value[k] === 3) {
meScore[i][j] += 2000;
} else if (meWin.value[k] === 4) {
meScore[i][j] += 10000;
}
if (AIWin.value[k] === 1) {
AIScore[i][j] += 220;
} else if (AIWin.value[k] === 2) {
AIScore[i][j] += 420;
} else if (AIWin.value[k] === 3) {
AIScore[i][j] += 2200;
} else if (AIWin.value[k] === 4) {
AIScore[i][j] += 20000;
}
}
}
// 比较得分,更新最大值及对应的位置
if (meScore[i][j] > max) {
max = meScore[i][j];
u = i;
v = j;
} else if (meScore[i][j] === max) {
if (AIScore[i][j] > AIScore[u][v]) {
u = i;
v = j;
}
}
if (AIScore[i][j] > max) {
max = AIScore[i][j];
u = i;
v = j;
} else if (AIScore[i][j] === max) {
if (meScore[i][j] > meScore[u][v]) {
u = i;
v = j;
}
}
}
}
}
// AI 在选择的最佳位置落子
placePiece(u, v, 2);
}
- 得分数组初始化
首先,创建了两个二维数组meScore和AIScore,用于分别记录棋盘上每个空位置对于玩家和 AI 的得分情况,初始值都设为0。同时,定义了变量max用于记录当前找到的最大得分,u和v用于记录具有最大得分的棋盘格位置坐标,初始化为0。 - 遍历空棋盘格
通过两层嵌套循环遍历整个棋盘上的每个位置,当发现某个位置chessBoard.value[i][j]为0,即该位置为空时,进入下一步的得分计算逻辑。 - 基于赢法计算得分
针对每个空棋盘格,再遍历所有的赢法(通过count.value控制循环次数)。当该空棋盘格处于某种赢法中(wins.value[i][j][k]为true)时,根据玩家(meWin数组)和 AI(AIWin数组)在该赢法下已有的落子数量来为对应的meScore和AIScore数组元素增加得分。例如,如果玩家在某赢法下已经有1颗棋子(meWin.value[k] === 1),就给该空位置在meScore中的对应元素增加200分,以此类推,不同的落子数量对应不同的分值,体现了该位置对于形成获胜局面的潜在价值,AI 的得分计算同理。 - 确定最佳落子位置
在计算完每个空位置的得分后,通过一系列比较逻辑来确定最佳落子位置。首先,比较meScore数组中的得分,如果某个位置的meScore值大于当前记录的最大得分max,则更新max以及对应的位置坐标u和v;如果得分相等,则进一步比较该位置的AIScore值,选择AIScore更大的位置。同样地,也会比较AIScore数组中的得分情况来确定最佳位置,若AIScore值更大或者相等但对应meScore值更大时,更新最佳位置坐标。 - 执行落子操作
经过上述复杂的评估和比较过程,最终确定了 AI 认为的最佳落子位置(u, v),然后调用placePiece函数,传入 AI 对应的标识2,在该位置完成 AI 的落子操作,使游戏继续推进下去。
通过这样一套目前看来合理且易于实现的简单算法,AI 得以初步具备模拟策略性落子行为的能力。不过,这仅仅是一个基础框架,仍存在很大的优化空间。
感谢阅读,敬请斧正!