|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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], |
|
[1, 0], |
|
[1, 1], |
|
[1, -1] |
|
]; |
|
|
|
const direction2index = (ox, oy) => { |
|
if (ox === 0) return 0; |
|
if (oy === 0) return 1; |
|
if (ox === oy) return 2; |
|
if (ox !== oy) return 3; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = []; |
|
} |
|
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; |
|
this.updatePoint(x, y); |
|
this.history.push([coordinate2Position(x, y, this.size), role]); |
|
} |
|
|
|
undo(x, y) { |
|
this.board[x + 1][y + 1] = 0; |
|
this.updatePoint(x, y); |
|
this.history.pop(); |
|
} |
|
|
|
initPoints() { |
|
|
|
|
|
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)); |
|
} |
|
} |
|
|
|
|
|
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(); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
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 = {}; |
|
|
|
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]) { |
|
for (let step = 1; step <= config.inLineDistance; step++) { |
|
const [nx, ny] = [x + sign * step * ox, y + sign * step * oy]; |
|
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); |
|
|
|
|
|
|
|
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; |
|
|
|
|
|
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; |
|
} |
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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]) { |
|
|
|
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]; |
|
|
|
if (this.board[nx][ny] === 2) { |
|
|
|
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]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
} |
|
if (reachEdge) break; |
|
} |
|
} |
|
} |
|
performance.updateTime += new Date() - start; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateSinglePoint(x, y, role, direction = undefined) { |
|
if (this.board[x + 1][y + 1] !== 0) return; |
|
|
|
|
|
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; |
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'; |
|
} |
|
console.log(result); |
|
} |
|
} |
|
|