File size: 19,332 Bytes
079c32c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 |
/*
帮我用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);
}
}
|