Spaces:
Sleeping
Sleeping
/* | |
帮我用JS写一个 Evaluate 类,作用是进行五子棋评分,原理是通过遍历棋盘上的每个点,计算出每个点的得分,最后将所有点的得分相加,得到当前棋盘的总得分。计算分数的规则如下: | |
- 每一个点的得分都是通过计算这个点在横、竖、左斜、右斜四个方向上的得分,这些得分按照连五、活四、冲四、活三、眠三、活二、眠二,存储在不同的数组中,最后将这些数组的得分相加,得到当前点的总得分。 | |
- 每一个方向上,都通过匹配模式串的方式计算分数。 | |
- 只计算空位的得分,不计算已经有棋子的得分。 | |
这个类要记住当前棋盘状态,提供如下方法: | |
- 提供 move 和 undo 方法,用于模拟下棋和悔棋,并且同步更新当前棋盘状态和得分。 | |
- 提供 evaluate 方法,用于计算当前棋盘的得分。 | |
- 提供 evaluatePoint 方法,用于计算某个点的得分。 | |
*/ | |
import { uniq } from 'lodash'; | |
import { shapes, getShapeFast, isFive, isFour, getAllShapesOfPoint } from './shape'; | |
import { coordinate2Position, isLine, isAllInLine, hasInLine, position2Coordinate } from './position'; | |
import { config } from './config'; | |
export const FIVE = 10000000; | |
export const BLOCK_FIVE = FIVE; | |
export const FOUR = 100000; | |
export const FOUR_FOUR = FOUR; // 双冲四 | |
export const FOUR_THREE = FOUR; // 冲四活三 | |
export const THREE_THREE = FOUR / 2; // 双三 | |
export const BLOCK_FOUR = 1500; | |
export const THREE = 1000; | |
export const BLOCK_THREE = 150; | |
export const TWO_TWO = 200; // 双活二 | |
export const TWO = 100; | |
export const BLOCK_TWO = 15; | |
export const ONE = 10; | |
export const BLOCK_ONE = 1; | |
// 形状转换分数,注意这里的分数是当前位置还没有落子的分数 | |
export const getRealShapeScore = (shape) => { | |
switch (shape) { | |
case shapes.FIVE: | |
return FOUR; | |
case shapes.BLOCK_FIVE: | |
return BLOCK_FOUR; | |
case shapes.FOUR: | |
return THREE; | |
case shapes.FOUR_FOUR: | |
return THREE; | |
case shapes.FOUR_THREE: | |
return THREE; | |
case shapes.BLOCK_FOUR: | |
return BLOCK_THREE; | |
case shapes.THREE: | |
return TWO; | |
case shapes.THREE_THREE: | |
return THREE_THREE / 10; | |
case shapes.BLOCK_THREE: | |
return BLOCK_TWO; | |
case shapes.TWO: | |
return ONE; | |
case shapes.TWO_TWO: | |
return TWO_TWO / 10; | |
default: | |
return 0; | |
} | |
} | |
const allDirections = [ | |
[0, 1], // Horizontal | |
[1, 0], // Vertical | |
[1, 1], // Diagonal \ | |
[1, -1] // Diagonal / | |
]; | |
const direction2index = (ox, oy) => { | |
if (ox === 0) return 0; // | | |
if (oy === 0) return 1; // - | |
if (ox === oy) return 2; // \ | |
if (ox !== oy) return 3; // / | |
}; | |
// const shape2score = { | |
// [shapes.FIVE]: FIVE, | |
// [shapes.BLOCK_FIVE]: BLOCK_FIVE, | |
// [shapes.FOUR]: FOUR, | |
// [shapes.FOUR_FOUR]: FOUR_FOUR, // 双冲四 | |
// [shapes.FOUR_THREE]: FOUR_THREE, // 冲四活三 | |
// [shapes.THREE_THREE]: THREE_THREE, // 双三 | |
// [shapes.BLOCK_FOUR]: BLOCK_FOUR, | |
// [shapes.THREE]: THREE, | |
// [shapes.BLOCK_THREE]: BLOCK_THREE, | |
// [shapes.TWO_TWO]: TWO_TWO, // 双活二 | |
// [shapes.TWO]: TWO, | |
// [shapes.NONE]: 0, | |
// }; | |
export const performance = { | |
updateTime: 0, | |
getPointsTime: 0, | |
} | |
export default class Evaluate { | |
constructor(size = 15) { | |
this.size = size; | |
this.board = Array.from({ length: size + 2 }).map((_, i) => | |
Array.from({ length: size + 2 }).map((_, j) => | |
(i === 0 || j === 0 || i === size + 1 || j === size + 1) ? 2 : 0 | |
) | |
); | |
this.blackScores = Array.from({ length: size }).map(() => Array.from({ length: size }).fill(0)); | |
this.whiteScores = Array.from({ length: size }).map(() => Array.from({ length: size }).fill(0)); | |
this.initPoints(); | |
this.history = []; // 记录历史 [position, role] | |
} | |
move(x, y, role) { | |
// 清空记录 | |
for (const d of [0, 1, 2, 3]) { | |
this.shapeCache[role][d][x][y] = 0; | |
this.shapeCache[-role][d][x][y] = 0; | |
} | |
this.blackScores[x][y] = 0; | |
this.whiteScores[x][y] = 0; | |
// 更新分数 | |
this.board[x + 1][y + 1] = role; // Adjust for the added wall | |
this.updatePoint(x, y); | |
this.history.push([coordinate2Position(x, y, this.size), role]); | |
} | |
undo(x, y) { | |
this.board[x + 1][y + 1] = 0; // Adjust for the added wall | |
this.updatePoint(x, y); | |
this.history.pop(); | |
} | |
initPoints() { | |
// 缓存每个点位的分数,避免重复计算 | |
// 结构: [role][direction][x][y] = shape | |
this.shapeCache = {}; | |
for (let role of [1, -1]) { | |
this.shapeCache[role] = {}; | |
for (let direction of [0, 1, 2, 3]) { | |
this.shapeCache[role][direction] = Array.from({ length: this.size }).map(() => Array.from({ length: this.size }).fill(shapes.NONE)); | |
} | |
} | |
// 缓存每个形状对应的点位 | |
// 结构: pointsCache[role][shape] = Set(direction1, direction2); | |
this.pointsCache = {} | |
for (let role of [1, -1]) { | |
this.pointsCache[role] = {}; | |
for (let key of Object.keys(shapes)) { | |
const shape = shapes[key]; | |
this.pointsCache[role][shape] = new Set(); | |
} | |
} | |
} | |
// 只返回和最后几步在一条直线上的点位。 | |
// 这么做有一点问题: | |
// 1. 因为己方可能会由于防守暂时离开原来的线,这样就会导致己方被中断,只能增加最后几步的长度,比如不是取最后一步,而不是最后3步 | |
// 2. 如果不是取最后1步,取的步数太多了,反而还不如直接返回所有点位。 | |
getPointsInLine(role) { | |
let pointsInLine = {}; // 在一条线上的点位 | |
let hasPointsInLine = false; | |
Object.keys(shapes).forEach((key) => { | |
pointsInLine[shapes[key]] = new Set(); | |
}); | |
let last2Points = this.history.slice(-config.inlineCount).map(([position, role]) => position); | |
const processed = {}; // 已经处理过的点位 | |
// 在last2Points中查找是否有点位在一条线上 | |
for (let r of [role, -role]) { | |
for (let point of last2Points) { | |
const [x, y] = position2Coordinate(point, this.size); | |
for (let [ox, oy] of allDirections) { | |
for (let sign of [1, -1]) { // -1 for negative direction, 1 for positive direction | |
for (let step = 1; step <= config.inLineDistance; step++) { | |
const [nx, ny] = [x + sign * step * ox, y + sign * step * oy]; // +1 to adjust for wall | |
const position = coordinate2Position(nx, ny, this.size); | |
// 检测是否到达边界 | |
if (nx < 0 || nx >= this.size || ny < 0 || ny >= this.size) { | |
break; | |
} | |
if (this.board[nx + 1][ny + 1] !== 0) { | |
continue; | |
} | |
if (processed[position] === r) continue; | |
processed[position] = r; | |
for (let direction of [0, 1, 2, 3]) { | |
const shape = this.shapeCache[r][direction][nx][ny]; | |
// 到达边界停止,但是注意到达对方棋子不能停止 | |
if (shape) { | |
pointsInLine[shape].add(coordinate2Position(nx, ny, this.size)); | |
hasPointsInLine = true; | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
if (hasPointsInLine) { | |
return pointsInLine; | |
} | |
return false; | |
} | |
getPoints(role, depth, vct, vcf) { | |
const first = depth % 2 === 0 ? role : -role; // 先手 | |
const start = new Date(); | |
if (config.onlyInLine && this.history.length >= config.inlineCount) { | |
const pointsInLine = this.getPointsInLine(role); | |
if (pointsInLine) { | |
performance.getPointsTime += new Date - start; | |
return pointsInLine; | |
} | |
} | |
let points = {}; // 全部点位 | |
Object.keys(shapes).forEach((key) => { | |
points[shapes[key]] = new Set(); | |
}); | |
const lastPoints = this.history.slice(-4).map(([position, role]) => position); | |
// const last2Points = this.history.slice(-2).map(([position, role]) => position); | |
// 在 shapeCache中查找对应的 shape | |
for (let r of [role, -role]) { | |
for (let i = 0; i < this.size; i++) { | |
for (let j = 0; j < this.size; j++) { | |
let fourCount = 0, blockFourCount = 0, threeCount = 0; | |
for (let direction of [0, 1, 2, 3]) { | |
if (this.board[i + 1][j + 1] !== 0) continue; | |
const shape = this.shapeCache[r][direction][i][j]; | |
if (!shape) continue; | |
// const scores = r === 1 ? this.blackScores : this.whiteScores; | |
// 冲四,考虑自己的冲四,连五和对方的连五 | |
if (vcf) { | |
if (r === first && !isFour(shape) && !isFive(shape)) continue; | |
if (r === -first && isFive(shape)) continue | |
} | |
const point = i * this.size + j; | |
if (vct) { | |
// 自己只进攻, 只考虑自己的活三,自己和对面的冲四、活四 | |
if (depth % 2 === 0) { | |
if (depth === 0 && r !== first) continue; // 并且第一步一定是从自己进攻开始,而不是一上来就防守 | |
if (shape !== shapes.THREE && !isFour(shape) && !isFive(shape)) continue; | |
// 对面的活三不考虑 | |
if (shape === shapes.THREE && r !== first) continue; | |
// 第一步只考虑自己的棋 | |
if (depth === 0 && r !== first) continue; | |
if (depth > 0) { | |
// 为了优化速度,这里增加了一个有损剪枝逻辑: 从第二步开始,只有 能形成活二以上的活三和冲四才考虑,这样可以过滤掉大部分无效的活三和冲四,但是也存在极少情况的错误剪枝 | |
if (shape === shapes.THREE && getAllShapesOfPoint(this.shapeCache, i, j, r).length === 1) continue; | |
if (shape === shapes.BLOCK_FOUR && getAllShapesOfPoint(this.shapeCache, i, j, r).length === 1) continue; | |
} | |
} | |
// 对面只防守,只考虑自己的冲四,活四,和对方的活三 | |
else { | |
if (shape !== shapes.THREE && !isFour(shape) && !isFive(shape)) continue; | |
if (shape === shapes.THREE && r === -first) continue; // 不考虑防守方的活三 | |
if (depth > 1) { | |
// 有损剪枝,如果单纯冲四无法和任何棋子联系在一起,则直接剪掉 | |
if (shape === shapes.BLOCK_FOUR && getAllShapesOfPoint(this.shapeCache, i, j).length === 1) continue; | |
// 从防守方的第二步开始,只有和最近两步连成一条线才行 | |
if (shape === shapes.BLOCK_FOUR && !hasInLine(point, lastPoints, this.size)) continue; | |
} | |
} | |
} | |
if (vcf) { | |
if (!isFour(shape) && !isFive(shape)) continue; | |
} | |
// 优化方式,从第3步开始,不考虑 在当前路径之外的活三以下的点位 | |
if (depth > 2 && (shape === shapes.TWO || shape === shapes.TWO_TWO || shape === shapes.BLOCK_THREE) && !hasInLine(point, lastPoints, this.size)) continue; | |
points[shape].add(point); | |
if (shape === shapes.FOUR) fourCount++; | |
else if (shape === shapes.BLOCK_FOUR) blockFourCount++; | |
else if (shape === shapes.THREE) threeCount++; | |
let unionShape = undefined; | |
if (fourCount >= 2) { | |
unionShape = shapes.FOUR_FOUR; | |
} else if (blockFourCount && threeCount) { | |
unionShape = shapes.FOUR_THREE; | |
} else if (threeCount >= 2) { | |
unionShape = shapes.THREE_THREE; | |
} | |
if (unionShape) { | |
points[unionShape].add(point); | |
} | |
} | |
} | |
} | |
} | |
// 否则继续返回所有的点位 | |
performance.getPointsTime += new Date - start; | |
return points; | |
} | |
// 当一个位置发生变时候,要更新这个位置的四个方向上得分,更新规则是: | |
// 1. 如果这个位置是空的,那么就重新计算这个位置的得分 | |
// 2. 如果碰到了边界或者对方的棋子,那么就停止计算 | |
// 3. 如果超过2个空位,那么就停止计算 | |
// 4. 要更新自己的和对方的得分 | |
updatePoint(x, y) { | |
const start = new Date(); | |
this.updateSinglePoint(x, y, 1); | |
this.updateSinglePoint(x, y, -1); | |
for (let [ox, oy] of allDirections) { | |
for (let sign of [1, -1]) { // -1 for negative direction, 1 for positive direction | |
// let emptyCount = 0; | |
for (let step = 1; step <= 5; step++) { | |
let reachEdge = false; | |
for (let role of [1, -1]) { | |
const [nx, ny] = [x + sign * step * ox + 1, y + sign * step * oy + 1]; // +1 to adjust for wall | |
// 到达边界停止 | |
if (this.board[nx][ny] === 2) { | |
// Stop if wall or opponent's piece is found | |
reachEdge = true; | |
break; | |
} else if (this.board[nx][ny] === -role) { // 达到对方棋子,则转换角色 | |
continue; | |
} else if (this.board[nx][ny] === 0) { | |
this.updateSinglePoint(nx - 1, ny - 1, role, [sign * ox, sign * oy]); // -1 to adjust back from wall | |
// 这里不能跳过,可能会在悔棋时漏掉一些待更新的点位 | |
// emptyCount++; | |
// if (emptyCount >= 3) { | |
// // Stop if more than 2 empty spaces | |
// break; | |
// } | |
} | |
} | |
if (reachEdge) break; | |
} | |
} | |
} | |
performance.updateTime += new Date() - start; | |
} | |
/* | |
计算单个点的得分 | |
计算原理是: | |
在当前位置放一个当前角色的棋子,遍历四个方向,生成四个方向上的字符串,用patters来匹配字符串, 匹配到的话,就将对应的得分加到scores上 | |
四个方向的字符串生成规则是:向两边都延伸5个位置,如果遇到边界或者对方的棋子,就停止延伸 | |
在更新周围棋子时,只有一个方向需要更新,因此可以传入direction参数,只更新一个方向 | |
*/ | |
updateSinglePoint(x, y, role, direction = undefined) { | |
if (this.board[x + 1][y + 1] !== 0) return; // Not an empty spot | |
// Temporarily place the piece | |
this.board[x + 1][y + 1] = role; | |
let directions = [] | |
if (direction) { | |
directions.push(direction); | |
} else { | |
directions = allDirections; | |
} | |
const shapeCache = this.shapeCache[role]; | |
// 先清除缓存 | |
for (let [ox, oy] of directions) { | |
shapeCache[direction2index(ox, oy)][x][y] = shapes.NONE; | |
} | |
let score = 0; | |
let blockfourCount = 0; | |
let threeCount = 0; | |
let twoCount = 0; | |
// 再计算已有得分 | |
for (let intDirection of [0, 1, 2, 3]) { | |
const shape = shapeCache[intDirection][x][y]; | |
if (shape > shapes.NONE) { | |
score += getRealShapeScore(shape); | |
if (shape === shapes.BLOCK_FOUR) blockfourCount += 1; | |
if (shape === shapes.THREE) threeCount += 1; | |
if (shape === shapes.TWO) twoCount += 1; | |
} | |
} | |
for (let [ox, oy] of directions) { | |
const intDirection = direction2index(ox, oy); | |
let [shape, selfCount] = getShapeFast(this.board, x, y, ox, oy, role); | |
if (!shape) continue; | |
if (shape) { | |
// 注意只缓存单个的形状,双三等复合形状不要缓存,因为这种缓存起来其实依赖两个形状,太复杂,所以在这里直接根据缓存的单个形状来计算双三等复合形状 | |
shapeCache[intDirection][x][y] = shape; | |
if (shape === shapes.BLOCK_FOUR) blockfourCount += 1; | |
if (shape === shapes.THREE) threeCount += 1; | |
if (shape === shapes.TWO) twoCount += 1; | |
if (blockfourCount >= 2) { | |
shape = shapes.FOUR_FOUR; | |
} else if (blockfourCount && threeCount) { | |
shape = shapes.FOUR_THREE; | |
} else if (threeCount >= 2) { | |
shape = shapes.THREE_THREE; | |
} else if (twoCount >= 2) { | |
shape = shapes.TWO_TWO; | |
} | |
score += getRealShapeScore(shape); | |
} | |
} | |
this.board[x + 1][y + 1] = 0; // Remove the temporary piece | |
if (role === 1) { | |
this.blackScores[x][y] = score; | |
} else { | |
this.whiteScores[x][y] = score; | |
} | |
return score; | |
} | |
// 计算整个棋盘的得分 | |
evaluate(role) { | |
let blackScore = 0; | |
let whiteScore = 0; | |
for (let i = 0; i < this.blackScores.length; i++) { | |
for (let j = 0; j < this.blackScores[i].length; j++) { | |
blackScore += this.blackScores[i][j]; | |
} | |
} | |
for (let i = 0; i < this.whiteScores.length; i++) { | |
for (let j = 0; j < this.whiteScores[i].length; j++) { | |
whiteScore += this.whiteScores[i][j]; | |
} | |
} | |
const score = role == 1 ? blackScore - whiteScore : whiteScore - blackScore; | |
return score; | |
} | |
/** | |
* 获取有价值的点位 | |
* @param {*} role 当前角色 | |
* @param {*} onlyThrees 只返回 活三、冲四、活四 | |
* @param {*} maxCount 最多返回多少个点位,这个参数只会裁剪活三以下的点位 | |
* @returns | |
*/ | |
getMoves(role, depth, onThree = false, onlyFour = false) { | |
const moves = Array.from(this._getMoves(role, depth, onThree, onlyFour)).map((move) => [Math.floor(move / this.size), move % this.size]); | |
return moves; | |
} | |
_getMoves(role, depth, onlyThree = false, onlyFour = false) { | |
const points = this.getPoints(role, depth, onlyThree, onlyFour); | |
const fives = points[shapes.FIVE]; | |
const blockFives = points[shapes.BLOCK_FIVE]; | |
if (fives?.size || blockFives?.size) return new Set([...fives, ...blockFives]); | |
const fours = points[shapes.FOUR]; | |
const blockfours = points[shapes.BLOCK_FOUR]; // 冲四比较特殊,在活四的时候要考虑,在活三的时候也要考虑 | |
if (onlyFour || fours?.size) { | |
return new Set([...fours, ...blockfours]); | |
} | |
const four_fours = points[shapes.FOUR_FOUR]; | |
if (four_fours.size) return new Set([...four_fours, ...blockfours]); | |
// 双三和活三 | |
const threes = points[shapes.THREE]; | |
const four_threes = points[shapes.FOUR_THREE]; | |
if (four_threes?.size) return new Set([...four_threes, ...blockfours, ...threes]); | |
const three_threes = points[shapes.THREE_THREE]; | |
if (three_threes?.size) return new Set([...three_threes, ...blockfours, ...threes]); | |
if (onlyThree) return new Set([...blockfours, ...threes]); | |
const blockthrees = points[shapes.BLOCK_THREE]; | |
const two_twos = points[shapes.TWO_TWO]; | |
const twos = points[shapes.TWO]; | |
const res = new Set([...blockfours, ...threes, ...blockthrees, ...two_twos, ...twos].slice(0, 20)); | |
return res; | |
} | |
display() { | |
let result = ''; | |
for (let i = 1; i < this.size + 1; i++) { | |
for (let j = 1; j < this.size + 1; j++) { | |
switch (this.board[i][j]) { | |
case 1: | |
result += 'O '; | |
break; | |
case -1: | |
result += 'X '; | |
break; | |
default: | |
result += '- '; | |
break; | |
} | |
} | |
result += '\n'; // New line at the end of each row | |
} | |
console.log(result); | |
} | |
} | |