这里人机的算法还是不太完美,目前也找不到更好的,大家有想法可以一起交流一下。
html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>五子棋</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.game_title {
margin-top: 30px;
font-size: 24px;
text-align: center;
}
.game_canvas {
display: block;
height: 450px;
width: 450px;
margin: 40px auto;
box-shadow: 5px 5px 5px #b9b9b9, -2px -2px 2px #efefef;
cursor: pointer;
}
.interaction {
width: 100px;
height: 50px;
margin: 0 auto;
display: flex;
}
.interaction .restart {
flex: auto;
border-radius: 15px;
background-color: rgb(217, 223, 224);
color: #333;
font-weight: bolder;
transition: box-shadow 0.5s;
font-size: 16px;
border: none;
}
.interaction .restart:hover {
color: black;
box-shadow: 0px 1px 3px #a8a7a7;
cursor: pointer;
}
.result {
width: 200px;
height: 100px;
background-color: rgba(206, 207, 206, 0.95);
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
border-radius: 80px;
font-size: 40px;
line-height: 100px;
text-align: center;
display: none;
}
</style>
</head>
<body>
<h3 class="game_title">五子棋</h3>
<div class="result"></div>
<canvas class="game_canvas"></canvas>
<div class="interaction">
<button class="restart">重新开始</button>
</div>
<script>
/* ----- 基本画布与棋盘绘制 ----- */
const game_canvas = document.querySelector(".game_canvas");
// 必须设置 canvas 内部绘图分辨率(与 CSS 显示尺寸分开)
game_canvas.width = 450;
game_canvas.height = 450;
const context = game_canvas.getContext("2d");
context.strokeStyle = "#b9b9b9";
context.lineWidth = 1;
const drawChessBoard = () => {
context.clearRect(0, 0, 450, 450);
// 背景(木纹色)
context.fillStyle = "#f2d79b";
context.fillRect(0, 0, 450, 450);
for (let i = 0; i < 15; i++) {
// 横线
context.beginPath();
context.moveTo(15, 15 + i * 30);
context.lineTo(435, 15 + i * 30);
context.stroke();
context.closePath();
// 竖线
context.beginPath();
context.moveTo(15 + i * 30, 15);
context.lineTo(15 + i * 30, 435);
context.stroke();
context.closePath();
}
// 星位(可选)
const stars = [3, 7, 11];
context.fillStyle = "#000";
for (let i of stars) {
for (let j of stars) {
context.beginPath();
context.arc(15 + i * 30, 15 + j * 30, 3, 0, Math.PI * 2);
context.fill();
context.closePath();
}
}
}
drawChessBoard();
/* ----- 赢法统计(15x15) ----- */
let wins = [];
for (let i = 0; i < 15; i++) {
wins[i] = [];
for (let j = 0; j < 15; j++) {
wins[i][j] = [];
}
}
let count = 0;
// 横
for (let i = 0; i < 15; i++) {
for (let j = 0; j < 11; j++) {
for (let k = 0; k < 5; k++) {
wins[j + k][i][count] = true;
}
count++;
}
}
// 竖
for (let i = 0; i < 15; i++) {
for (let j = 0; j < 11; j++) {
for (let k = 0; k < 5; k++) {
wins[i][j + k][count] = true;
}
count++;
}
}
// 正斜
for (let i = 0; i < 11; i++) {
for (let j = 0; j < 11; j++) {
for (let k = 0; k < 5; k++) {
wins[i + k][j + k][count] = true;
}
count++;
}
}
// 反斜
for (let i = 0; i < 11; i++) {
for (let j = 14; j > 3; j--) {
for (let k = 0; k < 5; k++) {
wins[i + k][j - k][count] = true;
}
count++;
}
}
/* ----- 棋盘状态 ----- */
let chessboard = [];
for (let i = 0; i < 15; i++) {
chessboard[i] = [];
for (let j = 0; j < 15; j++) {
chessboard[i][j] = 0; // 0 空,1 人(黑),2 机(白/红)
}
}
let me = true; // true 表示人的回合(黑子)
let over = false; // 游戏是否结束
let myWin = []; // 人在每种赢法上的进度
let computerWin = []; // 机在每种赢法上的进度
for (let i = 0; i < count; i++) {
myWin[i] = 0;
computerWin[i] = 0;
}
/* ----- 绘子 ----- */
// me = true 表示黑子(人),否则为机器
const onestep = (i, j, me) => {
context.beginPath();
context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);
context.closePath();
// 黑子为 #000,机器为白色
if (me) {
// 人下的黑子
context.fillStyle = "#000";
context.fill();
} else {
// 机器下的颜色
let grad = context.createRadialGradient(15 + i * 30 - 3, 15 + j * 30 - 3, 2, 15 + i * 30, 15 + j * 30, 13);
grad.addColorStop(0, '#fff');
grad.addColorStop(1, '#ddd'); // 白色
context.fillStyle = grad;
context.fill();
}
}
/* ----- 辅助:计算到最近已有棋子的曼哈顿距离 ----- */
const minDistanceToPieces = (i, j) => {
let minDist = Infinity;
for (let x = 0; x < 15; x++) {
for (let y = 0; y < 15; y++) {
if (chessboard[x][y] !== 0) {
let d = Math.abs(x - i) + Math.abs(y - j);
if (d < minDist) minDist = d;
}
}
}
// 如果棋盘为空(刚开始),返回 0
return minDist === Infinity ? 0 : minDist;
}
/* ----- 修改后的 AI:先检查直接获胜/必须堵点,然后评分(含距离惩罚) ----- */
const computeAI = () => {
if (over) return;
// 1) 先检查是否有直接获胜的落子(机方)
for (let i = 0; i < 15; i++) {
for (let j = 0; j < 15; j++) {
if (chessboard[i][j] === 0) {
for (let k = 0; k < count; k++) {
if (wins[i][j][k] && computerWin[k] + 1 === 5) {
// 下这个点直接获胜
onestep(i, j, false);
chessboard[i][j] = 2;
for (let t = 0; t < count; t++) {
if (wins[i][j][t]) computerWin[t] += 1;
}
document.querySelectorAll(".result")[0].textContent = `你输了`;
document.querySelectorAll(".result")[0].style.display = "block";
over = true;
return;
}
}
}
}
}
// 2) 再检查是否有必须堵住的点(对方下一步能成五)
for (let i = 0; i < 15; i++) {
for (let j = 0; j < 15; j++) {
if (chessboard[i][j] === 0) {
for (let k = 0; k < count; k++) {
if (wins[i][j][k] && myWin[k] + 1 === 5) {
// 必须堵住这个点
onestep(i, j, false);
chessboard[i][j] = 2;
for (let t = 0; t < count; t++) {
if (wins[i][j][t]) computerWin[t] += 1;
}
me = !me;
return;
}
}
}
}
}
// 3) 否则按评分选择最佳点(含距离惩罚和活/眠简单处理)
let myScore = [], computeScore = [];
for (let i = 0; i < 15; i++) {
myScore[i] = [];
computeScore[i] = [];
for (let j = 0; j < 15; j++) {
myScore[i][j] = 0;
computeScore[i][j] = 0;
}
}
// 简单的基础权值(可调整)
const weights = {
1: 200,
2: 400,
3: 2000,
4: 10000
};
const compWeights = {
1: 220,
2: 420,
3: 2200,
4: 20000
};
// 评分遍历
for (let i = 0; i < 15; i++) {
for (let j = 0; j < 15; j++) {
if (chessboard[i][j] === 0) {
for (let k = 0; k < count; k++) {
if (wins[i][j][k]) {
// 简单的活/眠判断:看这条5连的两侧是否被堵(边界或对手)
// 找到该赢法覆盖的5个点后判断两端格子状态
// 为效率起见,尝试根据 wins 的三种类型来简单判断周边被堵情况
// 这里实现一个轻量版本:若 myWin[k] 或 computerWin[k] 的进度被双方同时占,则视为被部分堵,降低权值
// 详细活眠区分可以在更复杂实现中替换
// 对我方(玩家)的影响
if (myWin[k] > 0) {
// 若该赢法中已有对方棋子则根据进度累加
myScore[i][j] += weights[myWin[k]] || 0;
}
// 对机器
if (computerWin[k] > 0) {
computeScore[i][j] += compWeights[computerWin[k]] || 0;
}
}
}
// 距离惩罚:优先靠近已有棋子的点(避免远处落子)
let dist = minDistanceToPieces(i, j); // 曼哈顿距离
let distancePenalty = 0;
// 距离越远惩罚越大(可调)
if (dist > 0) {
distancePenalty = (dist - 1) * 10; // 第一个邻近格不惩罚,越远惩罚越多
}
// 合并策略:进攻与防守兼顾。给防守一个较高权重但不过度盖过进攻。
// 也可以用 max(compute, my*factor) 类策略,这里取线性组合并扣除距离惩罚。
let totalScore = computeScore[i][j] * 1.0 + myScore[i][j] * 0.8 - distancePenalty;
// 把评分暂存在 computeScore[i][j] 中以便后续比较(也可以用独立数组)
computeScore[i][j] = totalScore;
}
}
}
// 选择最大分点(若多个,优先 compute(进攻)分高者,再优先 my(防守)分高者)
let max = -Infinity;
let bestX = 0, bestY = 0;
for (let i = 0; i < 15; i++) {
for (let j = 0; j < 15; j++) {
if (chessboard[i][j] === 0) {
let score = computeScore[i][j];
if (score > max) {
max = score;
bestX = i; bestY = j;
} else if (score === max) {
// 评分相等时按进攻得分优先,再按防守得分
// 重新计算原始分以比较(因为我们把 total 存回 computeScore)
let myS = myScore[i][j], mySbest = myScore[bestX][bestY];
let compS = (computeScore[i][j] - myScore[i][j] * 0.8 + minDistanceToPieces(i, j) * 10) || 0;
let compSbest = (computeScore[bestX][bestY] - myScorebestAdjustment()) || 0;
// 为避免过度复杂,这里简化比较:优先距离更近的点
if (minDistanceToPieces(i, j) < minDistanceToPieces(bestX, bestY)) {
bestX = i; bestY = j;
}
}
}
}
}
// 辅助:用于上面比较时得到 best 的 myScorebest adjustment safe access
function myScorebestAdjustment() {
return myScore[bestX] && myScore[bestX][bestY] ? myScore[bestX][bestY] * 0.8 : 0;
}
// 最后落子
onestep(bestX, bestY, false);
chessboard[bestX][bestY] = 2;
for (let k = 0; k < count; k++) {
if (wins[bestX][bestY][k]) {
computerWin[k] += 1;
// 如果机器达成 5
if (computerWin[k] === 5) {
document.querySelectorAll(".result")[0].textContent = `你输了`;
document.querySelectorAll(".result")[0].style.display = "block";
over = true;
}
}
}
if (!over) me = !me;
}
/* ----- 点击下子(人) ----- */
game_canvas.addEventListener("click", (event) => {
if (over) return;
if (!me) return;
// 取得画布内坐标
const rect = game_canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// 将像素坐标转换为棋盘索引(交叉点)
const i = Math.round((x - 15) / 30);
const j = Math.round((y - 15) / 30);
if (i < 0 || i >= 15 || j < 0 || j >= 15) return;
if (chessboard[i][j] !== 0) return;
// 落子(人)
onestep(i, j, true);
chessboard[i][j] = 1;
// 更新 myWin
for (let k = 0; k < count; k++) {
if (wins[i][j][k]) {
myWin[k] += 1;
if (myWin[k] === 5) {
document.querySelectorAll(".result")[0].textContent = `你赢了`;
document.querySelectorAll(".result")[0].style.display = "block";
over = true;
}
}
}
if (!over) {
me = !me;
// 让机器思考并落子(可以延迟以便更自然)
setTimeout(computeAI, 200);
}
});
/* ----- 重新开始 ----- */
const restartBtn = document.querySelectorAll(".restart")[0];
restartBtn.addEventListener("click", () => {
// 简单重载页面
window.location.reload();
});
</script>
</body>
</html>