写在前面
之前的一段时间手撸了五子棋人机(入门级)博弈,现在写个文章复盘下过程~
效果
实现步骤
- 画出棋盘
- 实现交互落子
- 算胜负
画棋盘
第一步想的就是用一张背景图作为棋盘,然后用一个15*15二维数组记录棋盘的数据,数组里每个数据渲染一个div盒子,也就是225个盒子。
每个盒子样式根据数据来决定不同样式:无样式、黑子、白子。棋子的实现基本上为两种:1.使用css绘制 2. 使用图片。
于是乎第一个问题就来了
当时思考一下...
不用局限于将有线的棋盘作为容器可以外围在扩大一点
落子区域大25px(棋盘每个格子大小为50*50)的区域作为容器,这有正常排列在视觉上呈现的就算在交界处
落子
通过点击事件可以落子,返回二维数组的坐标,根据坐标修改二维数组的数据,实现落子。数据为0为渲染白子样式 为1则渲染黑子样式
实现交互落子
我这的思路是使用一个全局的标志来判断该谁落子
// 标志该谁走棋 1是黑子 0是白子
const [flag, setFlag] = useState<number>(0);
落子后改变falg值实现交互
// 监听flag变化如果falg为1则落子 isLiquidation暂时不用管
useEffect(() => {
if (flag === 1) {
if (isLiquidation === 0) {
robotDownChess();
}
}
return () => { };
}, [flag, isLiquidation]);
实现robotDownChess
方法
// calculatePoint为棋盘分值算法
const robotDownChess = (): void => {
let [x, y] = [0, 0];
[x, y] = calculatePoint(chessList, 0);
if (chessList[x][y] === '') downChess(x, y, 'robot');
};
实现downChess
方法
// 数组深度克隆
const deepCloneArr = (arr: any[]) => {
return JSON.parse(JSON.stringify(arr));
};
// 落子方法
const downChess = (x: number, y: number, type = 'user') => {
if (typeof x !== 'number' || typeof y !== 'number') {
throw new Error('index1 或 index2 不是number类型');
}
const chessListCopy = deepCloneArr(chessList);
const res = chessListCopy[x][y];
if (typeof res === 'number' && [0, 1].includes(res)) {
message.warn('该地方已落子');
return;
}
// 落子到棋盘上的逻辑
if (flag === 0) {
chessListCopy[x][y] = 0;
setChessList(chessListCopy);
setFlag(1);
} else {
chessListCopy[x][y] = 1;
setChessList(chessListCopy);
setFlag(0);
}
// ...other code
}
分值算法现在只是初级的,一个循环遍历的过程,主要算斜着正着的连续同色子的数量,没有算是否为活子
注:活子表示连续同色棋子没有异色棋子拦截
主要算左斜
右斜
竖直
横向
连续多少个子然后在给他们分别算分值就OK
然后通过运算白黑两子来计算自己是该防还是攻,如果分数同等则以攻为守,即对方连续3子我连续3子,电脑会优先成自己的四子,当然这也是最基本要考虑的逻辑
实现方法
/**
* @param arr 棋盘数组
* @param color 当前落子颜色
* @returns 返回最佳落子位置
*/
export const calculatePoint = (arr: item[][], color: item) => {
let point = 0; // 计算对手落子最佳点
let point2 = 0; // 计算自身落子最佳点
const bestLocation = [0, 0]; // 存储对手落子最佳位置
const bestLocation2 = [0, 0]; // 存储自己最佳位置
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr[i].length; j++) {
if (arr[i][j] === '') {
let point3 = getWeighPoint(arr, i, j, color, weighPoint);
if (point3 > point) {
point = point3;
bestLocation[0] = i;
bestLocation[1] = j;
}
let point4 = getWeighPoint(arr, i, j, color === 1 ? 0 : 1, weighPoint);
if (point4 > point2) {
point2 = point4;
bestLocation2[0] = i;
bestLocation2[1] = j;
}
}
}
}
// console.clear()
console.log('point,point2', point, point2);
console.log('bestLocation', bestLocation, bestLocation2);
return point <= point2 ? bestLocation2 : bestLocation;
};
实现getWeightPoint方法
type weighType = {
liveFour: number;
liveThree: number;
liveTwo: number;
liveOne: number;
rushFour: number;
rushThree: number;
rushTwo: number;
rushone: number;
liveFive: number;
};
export type item = 0 | 1 | '';
const weighPoint: weighType = {
liveFive: 100000,
liveFour: 10000, // 4活子 权值最大
rushFour: 5000, // 冲4
liveThree: 1000, // 3活子
rushThree: 500, // 冲3
liveTwo: 50,
rushTwo: 2,
liveOne: 1,
rushone: 0,
};
// 计算棋盘每个点的分值
export function getWeighPoint(
board: item[][],
x: number,
y: number,
color: item,
weigh: weighType,
): number {
let point = 0;
let b1 = board[x][y + 1];
// 判断是否是活子
if (b1 === color && y + 1 <= 14) {
// 判断是否是冲4
if (
b1 === board[x][y + 1] &&
b1 === board[x][y + 2] &&
b1 === board[x][y + 3] &&
b1 === board[x][y + 4]
) {
point += weigh.rushFour;
}
// 判断是否是冲3
if (b1 === board[x][y + 1] && b1 === board[x][y + 2]) {
point += weigh.rushThree;
}
}
// 判断横向
let count = 1;
for (let i = 1; i < 5; i++) {
if (x - i >= 0 && board[x - i][y] === color) {
count++;
} else {
break;
}
}
for (let i = 1; i < 5; i++) {
if (x + i < 15 && board[x + i][y] === color) {
count++;
} else {
break;
}
}
if (count >= 5) {
point += weigh.liveFive;
if (count === 5) {
point += weigh.liveFour; // 冲4
}
} else if (count === 4) {
point += weigh.rushFour;
} else if (count === 3) {
point += weigh.liveThree;
} else if (count === 2) {
point += weigh.rushThree;
} else if (count === 1) {
point += weigh.rushTwo;
}
// 判断竖向
count = 1;
for (let i = 1; i < 5; i++) {
if (y - i >= 0 && board[x][y - i] === color) {
count++;
} else {
break;
}
}
for (let i = 1; i < 5; i++) {
if (y + i < 15 && board[x][y + i] === color) {
count++;
} else {
break; // 如果没有满足条件,则跳出循环
}
}
if (count >= 5) {
point += weigh.liveFive;
if (count === 5) {
point += weigh.liveFour; // 冲4
}
} else if (count === 4) {
point += weigh.rushFour;
} else if (count === 3) {
point += weigh.liveThree;
} else if (count === 2) {
point += weigh.rushThree;
} else if (count === 1) {
point += weigh.rushTwo;
}
// 判断右斜向
count = 1;
for (let i = 1; i < 5; i++) {
if (x - i >= 0 && y - i >= 0 && board[x - i][y - i] === color) {
count++;
} else {
break;
}
}
for (let i = 1; i < 5; i++) {
if (x + i < 15 && y + i < 15 && board[x + i][y + i] === color) {
count++;
} else {
break;
}
}
if (count >= 5) {
point += weigh.liveFive;
if (count === 5) {
point += weigh.liveFour; // 冲4
}
} else if (count === 4) {
point += weigh.rushFour;
} else if (count === 3) {
point += weigh.liveThree;
} else if (count === 2) {
point += weigh.rushThree;
} else if (count === 1) {
point += weigh.rushTwo;
}
// 判断左斜向
count = 1;
for (let i = 1; i < 5; i++) {
if (x - i >= 0 && y + i < 15 && board[x - i][y + i] === color) {
count++;
} else {
break;
}
}
for (let i = 1; i < 5; i++) {
// 如果没有满足条件,则跳出循环
if (x + i < 15 && y - i >= 0 && board[x + i][y - i] === color) {
count++;
} else {
break;
}
}
if (count >= 5) {
point += weigh.liveFive;
if (count === 5) {
point += weigh.liveFour; // 冲4
}
} else if (count === 4) {
point += weigh.rushFour;
} else if (count === 3) {
point += weigh.liveThree;
} else if (count === 2) {
point += weigh.rushThree;
} else if (count === 1) {
point += weigh.rushTwo;
}
return point;
}
得出落子的最高分后相当于机器人赋了一点五子棋的基本逻辑,可以下棋啦~
最后算胜负
算胜负就是比较简单暴力的过程,只要在落子后立即算
const downChess = (x: number, y: number, type = 'user') => {
// other code
for (let i = 0; i < chessListCopy.length; i++) {
const chess = chessListCopy[i];
for (let j = 0; j < chess.length; j++) {
if ([0, 1].includes(chess[j])) {
// x轴连子
if (
chess[j] === chess[j + 1] &&
chess[j] === chess[j + 2] &&
chess[j] === chess[j + 3] &&
chess[j] === chess[j + 4]
) {
showWin(chess[j]);
return;
}
// 斜着连子
else if (
chessListCopy[i][j] === chessListCopy[i + 1][j + 1] &&
chessListCopy[i][j] === chessListCopy[i + 2][j + 2] &&
chessListCopy[i][j] === chessListCopy[i + 3][j + 3] &&
chessListCopy[i][j] === chessListCopy[i + 4][j + 4]
) {
showWin(chessListCopy[i][j]);
return;
}
// y轴连子
else if (
i <= 10 &&
chessListCopy[i][j] === chessListCopy[i + 1][j] &&
chessListCopy[i][j] === chessListCopy[i + 2][j] &&
chessListCopy[i][j] === chessListCopy[i + 3][j] &&
chessListCopy[i][j] === chessListCopy[i + 4][j]
) {
showWin(chessListCopy[i][j]);
return;
}
// 斜着连子
else if (
i <= 10 &&
chessListCopy[i][j] === chessListCopy[i + 1][j - 1] &&
chessListCopy[i][j] === chessListCopy[i + 2][j - 2] &&
chessListCopy[i][j] === chessListCopy[i + 3][j - 3] &&
chessListCopy[i][j] === chessListCopy[i + 4][j - 4]
) {
showWin(chessListCopy[i][j]);
return;
}
}
}
}
}
个人源码git地址:gitee.com/lbxw/script…
dev分支 src/pages/game/gobang
落子的分值算法还有很大优化空间