@algorithm.ts/gomoku
Version:
A algorithm based on minimax search and alpha-beta prune to solve gomoku game.
1,181 lines (1,166 loc) • 49.3 kB
JavaScript
'use strict';
var queue = require('@algorithm.ts/queue');
exports.GomokuDirectionType = void 0;
(function (GomokuDirectionType) {
GomokuDirectionType[GomokuDirectionType["LEFT"] = 0] = "LEFT";
GomokuDirectionType[GomokuDirectionType["TOP_LEFT"] = 2] = "TOP_LEFT";
GomokuDirectionType[GomokuDirectionType["TOP"] = 4] = "TOP";
GomokuDirectionType[GomokuDirectionType["TOP_RIGHT"] = 6] = "TOP_RIGHT";
GomokuDirectionType[GomokuDirectionType["RIGHT"] = 1] = "RIGHT";
GomokuDirectionType[GomokuDirectionType["BOTTOM_RIGHT"] = 3] = "BOTTOM_RIGHT";
GomokuDirectionType[GomokuDirectionType["BOTTOM"] = 5] = "BOTTOM";
GomokuDirectionType[GomokuDirectionType["BOTTOM_LEFT"] = 7] = "BOTTOM_LEFT";
})(exports.GomokuDirectionType || (exports.GomokuDirectionType = {}));
const GomokuDirections = Array.from(Object.entries({
[exports.GomokuDirectionType.TOP]: [-1, 0],
[exports.GomokuDirectionType.TOP_RIGHT]: [-1, 1],
[exports.GomokuDirectionType.RIGHT]: [0, 1],
[exports.GomokuDirectionType.BOTTOM_RIGHT]: [1, 1],
[exports.GomokuDirectionType.BOTTOM]: [1, 0],
[exports.GomokuDirectionType.BOTTOM_LEFT]: [1, -1],
[exports.GomokuDirectionType.LEFT]: [0, -1],
[exports.GomokuDirectionType.TOP_LEFT]: [-1, -1],
}).reduce((acc, [key, value]) => {
const index = Number(key);
acc[index] = value;
return acc;
}, []));
const GomokuDirectionTypes = {
full: [
exports.GomokuDirectionType.LEFT,
exports.GomokuDirectionType.TOP_LEFT,
exports.GomokuDirectionType.TOP,
exports.GomokuDirectionType.TOP_RIGHT,
exports.GomokuDirectionType.RIGHT,
exports.GomokuDirectionType.BOTTOM_RIGHT,
exports.GomokuDirectionType.BOTTOM,
exports.GomokuDirectionType.BOTTOM_LEFT,
],
leftHalf: [
exports.GomokuDirectionType.LEFT,
exports.GomokuDirectionType.TOP_LEFT,
exports.GomokuDirectionType.TOP,
exports.GomokuDirectionType.TOP_RIGHT,
],
rightHalf: [
exports.GomokuDirectionType.RIGHT,
exports.GomokuDirectionType.BOTTOM_RIGHT,
exports.GomokuDirectionType.BOTTOM,
exports.GomokuDirectionType.BOTTOM_LEFT,
],
};
const GomokuDirectionTypeBitset = {
full: GomokuDirectionTypes.full.reduce((acc, dirType) => acc | (1 << dirType), 0),
leftHalf: GomokuDirectionTypes.leftHalf.reduce((acc, dirType) => acc | (1 << dirType), 0),
rightHalf: GomokuDirectionTypes.rightHalf.reduce((acc, dirType) => acc | (1 << dirType), 0),
};
const createHighDimensionArray = (elementProvider, firstDimension, ...dimensions) => {
const result = new Array(firstDimension);
if (dimensions.length <= 0) {
for (let i = 0; i < firstDimension; ++i) {
result[i] = elementProvider(i);
}
return result;
}
for (let i = 0; i < firstDimension; ++i) {
result[i] = createHighDimensionArray(elementProvider, ...dimensions);
}
return result;
};
const { full: fullDirectionTypes$1, rightHalf: halfDirectionTypes$2 } = GomokuDirectionTypes;
class GomokuMoverContext {
MAX_ROW;
MAX_COL;
MAX_ADJACENT;
MAX_DISTANCE_OF_NEIGHBOR;
TOTAL_PLAYER;
TOTAL_POS;
MIDDLE_POS;
board;
_idxMap;
_gomokuDirections;
_maxMovableMap;
_dirStartPosMap;
_dirStartPosSet;
_dirNeighborSet;
_neighborPlacedCount;
_rightHalfDirCountMap;
_placedCount;
constructor(props) {
const { MAX_ROW, MAX_COL, MAX_ADJACENT, MAX_DISTANCE_OF_NEIGHBOR } = props;
const _MAX_ROW = Math.max(1, MAX_ROW);
const _MAX_COL = Math.max(1, MAX_COL);
const _MAX_ADJACENT = Math.max(1, MAX_ADJACENT);
const _MAX_DISTANCE_OF_NEIGHBOR = Math.max(1, MAX_DISTANCE_OF_NEIGHBOR);
const _TOTAL_PLAYER = 2;
const _TOTAL_POS = _MAX_ROW * _MAX_COL;
this.MAX_ROW = _MAX_ROW;
this.MAX_COL = _MAX_COL;
this.MAX_ADJACENT = _MAX_ADJACENT;
this.MAX_DISTANCE_OF_NEIGHBOR = _MAX_DISTANCE_OF_NEIGHBOR;
this.TOTAL_PLAYER = _TOTAL_PLAYER;
this.TOTAL_POS = _TOTAL_POS;
this.MIDDLE_POS = _TOTAL_POS >> 1;
this.board = new Array(_TOTAL_POS).fill(-1);
const _idxMap = new Array(_TOTAL_POS);
for (let r = 0; r < _MAX_ROW; ++r) {
for (let c = 0; c < _MAX_COL; ++c) {
const posId = this.idx(r, c);
_idxMap[posId] = [r, c];
}
}
const _gomokuDirections = GomokuDirections.map(([dr, dc]) => dr * MAX_ROW + dc);
const _maxMovableMap = createHighDimensionArray(() => 0, fullDirectionTypes$1.length, _TOTAL_POS);
const _dirStartPosSet = createHighDimensionArray(() => [], fullDirectionTypes$1.length);
const _dirStartPosMap = createHighDimensionArray(() => 0, fullDirectionTypes$1.length, _TOTAL_POS);
this.traverseAllDirections(dirType => {
const revDirType = dirType ^ 1;
const [dr, dc] = GomokuDirections[dirType];
return posId => {
const [r, c] = _idxMap[posId];
const r2 = r + dr;
const c2 = c + dc;
if (r2 < 0 || r2 >= _MAX_ROW || c2 < 0 || c2 >= _MAX_ROW) {
_maxMovableMap[dirType][posId] = 0;
_dirStartPosMap[revDirType][posId] = posId;
_dirStartPosSet[revDirType].push(posId);
}
else {
const posId2 = this.idx(r2, c2);
_maxMovableMap[dirType][posId] = _maxMovableMap[dirType][posId2] + 1;
_dirStartPosMap[revDirType][posId] = _dirStartPosMap[revDirType][posId2];
}
};
});
const _dirNeighborSet = new Array(_TOTAL_POS);
for (let posId = 0; posId < _TOTAL_POS; ++posId) {
const neighbors = [];
_dirNeighborSet[posId] = neighbors;
for (const dirType of fullDirectionTypes$1) {
let posId2 = posId;
for (let step = 0; step < _MAX_DISTANCE_OF_NEIGHBOR; ++step) {
if (1 > _maxMovableMap[dirType][posId2])
break;
posId2 += _gomokuDirections[dirType];
neighbors.push(posId2);
}
}
}
const _rightHalfDirCountMap = createHighDimensionArray(() => [], fullDirectionTypes$1.length, _TOTAL_POS);
this._gomokuDirections = _gomokuDirections;
this._idxMap = _idxMap;
this._maxMovableMap = _maxMovableMap;
this._dirStartPosMap = _dirStartPosMap;
this._dirStartPosSet = _dirStartPosSet;
this._dirNeighborSet = _dirNeighborSet;
this._neighborPlacedCount = new Array(_TOTAL_POS).fill(0);
this._rightHalfDirCountMap = _rightHalfDirCountMap;
this._placedCount = 0;
}
get placedCount() {
return this._placedCount;
}
init(pieces) {
const board = this.board;
board.fill(-1);
this._placedCount = 0;
const { _neighborPlacedCount } = this;
_neighborPlacedCount.fill(0);
for (const piece of pieces) {
const posId = this.idx(piece.r, piece.c);
if (board[posId] >= 0)
continue;
board[posId] = piece.p;
this._placedCount += 1;
for (const neighborId of this.accessibleNeighbors(posId)) {
_neighborPlacedCount[neighborId] += 1;
}
}
const { _rightHalfDirCountMap } = this;
for (const dirType of halfDirectionTypes$2) {
for (const startPosId of this.getStartPosSet(dirType)) {
const counters = _rightHalfDirCountMap[dirType][startPosId];
let index = 0;
const maxSteps = this.maxMovableSteps(startPosId, dirType) + 1;
for (let i = 0, posId = startPosId, i2, posId2; i < maxSteps; i = i2, posId = posId2) {
const playerId = board[posId];
for (i2 = i + 1, posId2 = posId; i2 < maxSteps; ++i2) {
posId2 = this.fastMoveOneStep(posId2, dirType);
if (board[posId2] !== playerId)
break;
}
counters[index++] = { playerId, count: i2 - i };
}
counters.length = index;
}
}
}
forward(posId, playerId) {
this.board[posId] = playerId;
this._placedCount += 1;
for (const neighborId of this.accessibleNeighbors(posId)) {
this._neighborPlacedCount[neighborId] += 1;
}
for (const dirType of halfDirectionTypes$2) {
this._updateHalfDirCounter(playerId, posId, dirType);
}
}
revert(posId) {
this.board[posId] = -1;
this._placedCount -= 1;
for (const neighborId of this.accessibleNeighbors(posId)) {
this._neighborPlacedCount[neighborId] -= 1;
}
for (const dirType of halfDirectionTypes$2) {
this._updateHalfDirCounter(-1, posId, dirType);
}
}
idx(r, c) {
return r * this.MAX_ROW + c;
}
revIdx(posId) {
return this._idxMap[posId];
}
isValidPos(r, c) {
return r >= 0 && r < this.MAX_ROW && c >= 0 && c < this.MAX_COL;
}
isValidIdx(posId) {
return posId >= 0 && posId < this.TOTAL_POS;
}
safeMove(posId, dirType, step) {
return step <= this._maxMovableMap[dirType][posId]
? posId + this._gomokuDirections[dirType] * step
: -1;
}
safeMoveOneStep(posId, dirType) {
return 1 <= this._maxMovableMap[dirType][posId] ? posId + this._gomokuDirections[dirType] : -1;
}
fastMove(posId, dirType, step) {
return posId + this._gomokuDirections[dirType] * step;
}
fastMoveOneStep(posId, dirType) {
return posId + this._gomokuDirections[dirType];
}
maxMovableSteps(posId, dirType) {
return this._maxMovableMap[dirType][posId];
}
accessibleNeighbors(posId) {
return this._dirNeighborSet[posId];
}
hasPlacedNeighbors(posId) {
return this._neighborPlacedCount[posId] > 0;
}
couldReachFinalInDirection(playerId, posId, dirType) {
const { MAX_ADJACENT, board } = this;
const revDirType = dirType ^ 1;
const maxMovableSteps0 = this.maxMovableSteps(posId, revDirType);
const maxMovableSteps2 = this.maxMovableSteps(posId, dirType);
if (maxMovableSteps0 + maxMovableSteps2 + 1 < MAX_ADJACENT)
return false;
let count = 1;
for (let id = posId, step = 0; step < maxMovableSteps0; ++step) {
id = this.fastMoveOneStep(id, revDirType);
if (board[id] !== playerId)
break;
count += 1;
}
for (let id = posId, step = 0; step < maxMovableSteps2; ++step) {
id = this.fastMoveOneStep(id, dirType);
if (board[id] !== playerId)
break;
count += 1;
}
return count >= MAX_ADJACENT;
}
getStartPosId(posId, dirType) {
return this._dirStartPosMap[dirType][posId];
}
getStartPosSet(dirType) {
return this._dirStartPosSet[dirType];
}
getDirCounters(startPosId, dirType) {
return this._rightHalfDirCountMap[dirType][startPosId];
}
traverseAllDirections(handle) {
const { TOTAL_POS } = this;
const { leftHalf, rightHalf } = GomokuDirectionTypes;
for (const dirType of leftHalf) {
const h = handle(dirType);
for (let posId = 0; posId < TOTAL_POS; ++posId)
h(posId);
}
for (const dirType of rightHalf) {
const h = handle(dirType);
for (let posId = TOTAL_POS - 1; posId >= 0; --posId)
h(posId);
}
}
_updateHalfDirCounter(playerId, posId, dirType) {
const revDirType = dirType ^ 1;
const startPosId = this.getStartPosId(posId, dirType);
const counters = this._rightHalfDirCountMap[dirType][startPosId];
let index = 0;
let remain = this.maxMovableSteps(posId, revDirType) + 1;
for (; index < counters.length; ++index) {
const cnt = counters[index].count;
if (cnt >= remain)
break;
remain -= cnt;
}
if (remain === 1) {
if (counters[index].count === 1) {
const isSameWithLeft = index > 0 && counters[index - 1].playerId === playerId;
const isSameWithRight = index + 1 < counters.length && counters[index + 1].playerId === playerId;
if (isSameWithLeft) {
if (isSameWithRight) {
counters[index - 1].count += 1 + counters[index + 1].count;
counters.splice(index, 2);
}
else {
counters[index - 1].count += 1;
counters.splice(index, 1);
}
}
else {
if (isSameWithRight) {
counters[index + 1].count += 1;
counters.splice(index, 1);
}
else {
counters[index].playerId = playerId;
}
}
}
else {
counters[index].count -= 1;
if (index > 0 && counters[index - 1].playerId === playerId) {
counters[index - 1].count += 1;
}
else {
counters.splice(index, 0, { playerId, count: 1 });
}
}
}
else if (remain < counters[index].count) {
const { playerId: hitPlayerId, count: hitCount } = counters[index];
counters.splice(index, 1, { playerId: hitPlayerId, count: remain - 1 }, { playerId, count: 1 }, { playerId: hitPlayerId, count: hitCount - remain });
}
else {
counters[index].count -= 1;
if (index + 1 < counters.length && counters[index + 1].playerId === playerId) {
counters[index + 1].count += 1;
}
else {
counters.splice(index + 1, 0, { playerId, count: 1 });
}
}
}
}
const { rightHalf: halfDirectionTypes$1 } = GomokuDirectionTypes;
const { rightHalf: allDirectionTypeBitset$1 } = GomokuDirectionTypeBitset;
class GomokuMoverCounter {
context;
_mustWinPosSet;
_candidateCouldReachFinal;
constructor(context) {
this.context = context;
this._mustWinPosSet = createHighDimensionArray(() => new Set(), context.TOTAL_PLAYER);
this._candidateCouldReachFinal = createHighDimensionArray(() => 0, context.TOTAL_PLAYER, context.TOTAL_POS);
}
init() {
this._candidateCouldReachFinal.forEach(item => item.fill(0));
this._mustWinPosSet.forEach(set => set.clear());
const { context } = this;
const { TOTAL_POS, board } = context;
const [ccrf0, ccrf1] = this._candidateCouldReachFinal;
const [mwps0, mwps1] = this._mustWinPosSet;
for (let posId = 0; posId < TOTAL_POS; ++posId) {
if (board[posId] >= 0)
continue;
let flag0 = 0;
let flag1 = 0;
for (const dirType of halfDirectionTypes$1) {
const bitFlag = 1 << dirType;
if (context.couldReachFinalInDirection(0, posId, dirType))
flag0 |= bitFlag;
if (context.couldReachFinalInDirection(1, posId, dirType))
flag1 |= bitFlag;
}
ccrf0[posId] = flag0;
ccrf1[posId] = flag1;
if (flag0 > 0)
mwps0.add(posId);
if (flag1 > 0)
mwps1.add(posId);
}
}
forward(posId) {
this._updateRelatedCouldReachFinal(posId);
}
revert(posId) {
this._updateRelatedCouldReachFinal(posId);
}
mustWinPosSet(playerId) {
return this._mustWinPosSet[playerId];
}
candidateCouldReachFinal(playerId, posId) {
return this._candidateCouldReachFinal[playerId][posId] > 0;
}
_updateRelatedCouldReachFinal(centerPosId) {
const { context } = this;
const { board } = context;
this._updateCouldReachFinal(centerPosId, allDirectionTypeBitset$1);
for (const dirType of halfDirectionTypes$1) {
const expiredBitset = 1 << dirType;
const revDirType = dirType ^ 1;
const maxMovableSteps0 = context.maxMovableSteps(centerPosId, revDirType);
for (let posId = centerPosId, step = 0; step < maxMovableSteps0; ++step) {
posId = context.fastMoveOneStep(posId, revDirType);
if (board[posId] < 0) {
this._updateCouldReachFinal(posId, expiredBitset);
break;
}
}
const maxMovableSteps2 = context.maxMovableSteps(centerPosId, dirType);
for (let posId = centerPosId, step = 0; step < maxMovableSteps2; ++step) {
posId = context.fastMoveOneStep(posId, dirType);
if (board[posId] < 0) {
this._updateCouldReachFinal(posId, expiredBitset);
break;
}
}
}
}
_updateCouldReachFinal(posId, expiredBitset) {
if (expiredBitset > 0) {
const { context, _mustWinPosSet, _candidateCouldReachFinal } = this;
const prevFlag0 = _candidateCouldReachFinal[0][posId];
const prevFlag1 = _candidateCouldReachFinal[1][posId];
_mustWinPosSet[0].delete(posId);
_mustWinPosSet[1].delete(posId);
if (context.board[posId] >= 0) {
_candidateCouldReachFinal[0][posId] = 0;
_candidateCouldReachFinal[1][posId] = 0;
return;
}
let nextFlag0 = 0;
let nextFlag1 = 0;
for (const dirType of halfDirectionTypes$1) {
const bitFlag = 1 << dirType;
if (bitFlag & expiredBitset) {
if (context.couldReachFinalInDirection(0, posId, dirType))
nextFlag0 |= bitFlag;
if (context.couldReachFinalInDirection(1, posId, dirType))
nextFlag1 |= bitFlag;
}
else {
nextFlag0 |= prevFlag0 & bitFlag;
nextFlag1 |= prevFlag1 & bitFlag;
}
}
_candidateCouldReachFinal[0][posId] = nextFlag0;
_candidateCouldReachFinal[1][posId] = nextFlag1;
if (nextFlag0 > 0)
_mustWinPosSet[0].add(posId);
if (nextFlag1 > 0)
_mustWinPosSet[1].add(posId);
}
}
}
class GomokuMover {
rootPlayerId;
context;
counter;
state;
constructor(props) {
this.context = props.context;
this.counter = props.counter;
this.state = props.state;
this.rootPlayerId = 0;
}
resetRootPlayerId(rootPlayerId) {
this.rootPlayerId = rootPlayerId & 1;
}
init(pieces) {
this.context.init(pieces);
this.counter.init(pieces);
this.state.init(pieces);
}
forward(posId, playerId) {
const { context, counter, state } = this;
if (context.board[posId] < 0) {
context.forward(posId, playerId);
counter.forward(posId, playerId);
state.forward(posId, playerId);
}
}
revert(posId) {
const { context, counter, state } = this;
if (context.board[posId] >= 0) {
context.revert(posId);
counter.revert(posId);
state.revert(posId);
}
}
expand(nextPlayerId, candidates, candidateGrowthFactor, MAX_SIZE) {
return this.state.expand(nextPlayerId, candidates, candidateGrowthFactor, MAX_SIZE);
}
topCandidate(nextPlayerId) {
return this.state.topCandidate(nextPlayerId);
}
score(nextPlayerId) {
return this.state.score(nextPlayerId ^ 1, this.rootPlayerId);
}
isFinal() {
return this.state.isFinal();
}
couldReachFinal(nextPlayerId) {
return this.counter.mustWinPosSet(nextPlayerId).size > 0;
}
}
const { full: fullDirectionTypes, rightHalf: halfDirectionTypes } = GomokuDirectionTypes;
const { rightHalf: allDirectionTypeBitset } = GomokuDirectionTypeBitset;
class GomokuMoverState {
NEXT_MOVER_BUFFER = 4;
context;
counter;
scoreMap;
_candidateQueues;
_candidateInqSets;
_candidateSet;
_candidateScores;
_candidateScoreDirMap;
_candidateScoreExpired;
_stateScores;
_stateScoreDirMap;
_countOfReachFinal;
_countOfReachFinalDirMap;
constructor(props) {
const { context, counter, scoreMap } = props;
const _candidateQueues = createHighDimensionArray(() => new queue.PriorityQueue({ compare: (x, y) => y.score - x.score }), context.TOTAL_PLAYER);
const _candidateInqSets = createHighDimensionArray(() => new Set(), context.TOTAL_PLAYER, context.TOTAL_POS);
const _candidateSet = new Set();
const _candidateScores = createHighDimensionArray(() => 0, context.TOTAL_PLAYER, context.TOTAL_POS);
const _candidateScoreDirMap = createHighDimensionArray(() => 0, context.TOTAL_PLAYER, context.TOTAL_POS, fullDirectionTypes.length);
const _candidateScoreExpired = createHighDimensionArray(() => allDirectionTypeBitset, context.TOTAL_POS);
const _stateScores = createHighDimensionArray(() => 0, context.TOTAL_PLAYER);
const _stateScoreDirMap = createHighDimensionArray(() => 0, context.TOTAL_PLAYER, context.TOTAL_POS, fullDirectionTypes.length);
const _countOfReachFinal = createHighDimensionArray(() => 0, context.TOTAL_PLAYER);
const _countOfReachFinalDirMap = createHighDimensionArray(() => 0, context.TOTAL_PLAYER, context.TOTAL_POS, fullDirectionTypes.length);
this.context = context;
this.counter = counter;
this.scoreMap = scoreMap;
this._candidateQueues = _candidateQueues;
this._candidateInqSets = _candidateInqSets;
this._candidateSet = _candidateSet;
this._candidateScores = _candidateScores;
this._candidateScoreDirMap = _candidateScoreDirMap;
this._candidateScoreExpired = _candidateScoreExpired;
this._stateScores = _stateScores;
this._stateScoreDirMap = _stateScoreDirMap;
this._countOfReachFinal = _countOfReachFinal;
this._countOfReachFinalDirMap = _countOfReachFinalDirMap;
}
init(pieces) {
this._candidateQueues.forEach(Q => Q.init());
this._candidateInqSets.forEach(inqSets => inqSets.forEach(inqSet => inqSet.clear()));
this._candidateSet.clear();
this._candidateScoreExpired.fill(allDirectionTypeBitset);
this._stateScores.fill(0);
this._countOfReachFinal.fill(0);
const { context, _stateScores, _stateScoreDirMap, _countOfReachFinal, _countOfReachFinalDirMap, } = this;
for (const dirType of halfDirectionTypes) {
for (const startPosId of context.getStartPosSet(dirType)) {
const { score: score0, countOfReachFinal: countOfReachFinal0 } = this._evaluateScoreInDirection(0, startPosId, dirType);
const { score: score1, countOfReachFinal: countOfReachFinal1 } = this._evaluateScoreInDirection(1, startPosId, dirType);
_stateScores[0] += score0;
_stateScores[1] += score1;
_stateScoreDirMap[0][startPosId][dirType] = score0;
_stateScoreDirMap[1][startPosId][dirType] = score1;
_countOfReachFinal[0] += countOfReachFinal0;
_countOfReachFinal[1] += countOfReachFinal1;
_countOfReachFinalDirMap[0][startPosId][dirType] = countOfReachFinal0;
_countOfReachFinalDirMap[1][startPosId][dirType] = countOfReachFinal1;
}
}
const { _candidateSet } = this;
for (const piece of pieces) {
const centerPosId = context.idx(piece.r, piece.c);
for (const posId of context.accessibleNeighbors(centerPosId)) {
if (context.board[posId] < 0)
_candidateSet.add(posId);
}
}
if (context.board[context.MIDDLE_POS] < 0)
_candidateSet.add(context.MIDDLE_POS);
for (const posId of _candidateSet)
this._reEvaluateAndEnqueueCandidate(posId);
}
forward(posId) {
const { context, _candidateSet } = this;
_candidateSet.delete(posId);
for (const posId2 of context.accessibleNeighbors(posId)) {
if (context.board[posId2] < 0)
_candidateSet.add(posId2);
}
this._updateStateScore(posId);
this._expireCandidates(posId);
}
revert(posId) {
const { context, _candidateSet } = this;
if (context.hasPlacedNeighbors(posId))
_candidateSet.add(posId);
for (const posId2 of context.accessibleNeighbors(posId)) {
if (!context.hasPlacedNeighbors(posId2))
_candidateSet.delete(posId2);
}
this._updateStateScore(posId);
this._expireCandidates(posId);
if (context.board[context.MIDDLE_POS] < 0 && !_candidateSet.has(context.MIDDLE_POS)) {
_candidateSet.add(context.MIDDLE_POS);
this._reEvaluateAndEnqueueCandidate(context.MIDDLE_POS);
}
}
expand(nextPlayerId, candidates, candidateGrowthFactor, MAX_SIZE = this.context.TOTAL_POS) {
const topCandidate = this.topCandidate(nextPlayerId);
if (topCandidate === undefined)
return 0;
if (topCandidate.score === Number.MAX_VALUE) {
candidates[0] = topCandidate;
return 1;
}
const topScore = topCandidate.score;
const { _candidateScoreExpired } = this;
const Q = this._candidateQueues[nextPlayerId];
const inqSets = this._candidateInqSets[nextPlayerId];
const candidateScores = this._candidateScores[nextPlayerId];
let _size = 0;
let item;
for (; _size < MAX_SIZE;) {
item = Q.dequeue();
if (item === undefined)
break;
if (_candidateScoreExpired[item.posId] === 0 && item.score === candidateScores[item.posId]) {
if (item.score * candidateGrowthFactor < topScore) {
Q.enqueue(item);
break;
}
candidates[_size++] = item;
}
else {
inqSets[item.posId].delete(item.score);
}
}
if (Q.size > this.context.TOTAL_POS) {
Q.exclude(item => {
if (_candidateScoreExpired[item.posId] > 0 || item.score !== candidateScores[item.posId]) {
inqSets[item.posId].delete(item.score);
return true;
}
return false;
});
}
candidates.length = _size;
Q.enqueues(candidates);
return _size;
}
topCandidate(nextPlayerId) {
const mustDropPos0 = this.counter.mustWinPosSet(nextPlayerId);
if (mustDropPos0.size > 0) {
for (const posId of mustDropPos0)
return { posId, score: Number.MAX_VALUE };
}
const mustDropPos1 = this.counter.mustWinPosSet(nextPlayerId ^ 1);
if (mustDropPos1.size > 0) {
for (const posId of mustDropPos1)
return { posId, score: Number.MAX_VALUE };
}
const { _candidateScoreExpired } = this;
const Q = this._candidateQueues[nextPlayerId];
const inqSets = this._candidateInqSets[nextPlayerId];
const candidateScores = this._candidateScores[nextPlayerId];
let item;
for (;;) {
item = Q.dequeue();
if (item === undefined)
break;
inqSets[item.posId].delete(item.score);
if (_candidateScoreExpired[item.posId] === 0 && item.score === candidateScores[item.posId]) {
break;
}
}
if (item !== undefined) {
Q.enqueue(item);
inqSets[item.posId].add(item.score);
}
return item;
}
score(currentPlayer, scoreForPlayer) {
const score0 = this._stateScores[scoreForPlayer];
const score1 = this._stateScores[scoreForPlayer ^ 1];
const nextMoverFac = this.NEXT_MOVER_BUFFER;
return currentPlayer === scoreForPlayer
? score0 - score1 * nextMoverFac
: score0 * nextMoverFac - score1;
}
isWin(currentPlayer) {
return this._countOfReachFinal[currentPlayer] > 0;
}
isDraw() {
return this.context.placedCount === this.context.TOTAL_POS;
}
isFinal() {
const { context, _countOfReachFinal } = this;
if (context.placedCount === context.TOTAL_POS)
return true;
if (_countOfReachFinal[0] > 0 || _countOfReachFinal[1] > 0)
return true;
return false;
}
_updateStateScore(posId) {
const { context, _countOfReachFinal, _countOfReachFinalDirMap, _stateScores, _stateScoreDirMap, } = this;
for (const dirType of halfDirectionTypes) {
const startPosId = context.getStartPosId(posId, dirType);
const { score: score0, countOfReachFinal: countOfReachFinal0 } = this._evaluateScoreInDirection(0, startPosId, dirType);
const { score: score1, countOfReachFinal: countOfReachFinal1 } = this._evaluateScoreInDirection(1, startPosId, dirType);
_stateScores[0] += score0 - _stateScoreDirMap[0][startPosId][dirType];
_stateScores[1] += score1 - _stateScoreDirMap[1][startPosId][dirType];
_stateScoreDirMap[0][startPosId][dirType] = score0;
_stateScoreDirMap[1][startPosId][dirType] = score1;
_countOfReachFinal[0] += countOfReachFinal0 - _countOfReachFinalDirMap[0][startPosId][dirType];
_countOfReachFinal[1] += countOfReachFinal1 - _countOfReachFinalDirMap[1][startPosId][dirType];
_countOfReachFinalDirMap[0][startPosId][dirType] = countOfReachFinal0;
_countOfReachFinalDirMap[1][startPosId][dirType] = countOfReachFinal1;
}
}
_expireCandidates(centerPosId) {
const { context, _candidateSet, _candidateScoreExpired } = this;
for (const dirType of halfDirectionTypes) {
const startPosId = context.getStartPosId(centerPosId, dirType);
const maxSteps = context.maxMovableSteps(startPosId, dirType);
const bitMark = 1 << dirType;
for (let posId = startPosId, step = 0;; ++step) {
_candidateScoreExpired[posId] |= bitMark;
if (posId !== centerPosId && _candidateSet.has(posId)) {
this._reEvaluateAndEnqueueCandidate(posId);
}
if (step === maxSteps)
break;
posId = context.fastMoveOneStep(posId, dirType);
}
}
if (_candidateSet.has(centerPosId))
this._reEvaluateAndEnqueueCandidate(centerPosId);
}
_reEvaluateAndEnqueueCandidate(posId) {
this._reEvaluateCandidate(posId);
const candidateScore0 = this._candidateScores[0][posId];
const inq0 = this._candidateInqSets[0][posId];
if (!inq0.has(candidateScore0)) {
inq0.add(candidateScore0);
this._candidateQueues[0].enqueue({ posId, score: candidateScore0 });
}
const candidateScore1 = this._candidateScores[1][posId];
const inq1 = this._candidateInqSets[1][posId];
if (!inq1.has(candidateScore1)) {
inq1.add(candidateScore1);
this._candidateQueues[1].enqueue({ posId, score: candidateScore1 });
}
}
_reEvaluateCandidate(posId) {
const expiredBitset = this._candidateScoreExpired[posId];
if (expiredBitset > 0) {
const { NEXT_MOVER_BUFFER, context, _candidateScoreDirMap, _stateScoreDirMap } = this;
let prevScore0 = 0;
let prevScore1 = 0;
for (const dirType of halfDirectionTypes) {
const startPosId = context.getStartPosId(posId, dirType);
prevScore0 += _stateScoreDirMap[0][startPosId][dirType];
prevScore1 += _stateScoreDirMap[1][startPosId][dirType];
}
let score0 = 0;
this._temporaryForward(posId, 0);
for (const dirType of halfDirectionTypes) {
if ((1 << dirType) & expiredBitset) {
const startPosId = context.getStartPosId(posId, dirType);
const { score } = this._evaluateScoreInDirection(0, startPosId, dirType);
_candidateScoreDirMap[0][posId][dirType] = score;
score0 += score;
}
else
score0 += _candidateScoreDirMap[0][posId][dirType];
}
this._temporaryRevert(posId);
let score1 = 0;
this._temporaryForward(posId, 1);
for (const dirType of halfDirectionTypes) {
if ((1 << dirType) & expiredBitset) {
const startPosId = context.getStartPosId(posId, dirType);
const { score } = this._evaluateScoreInDirection(1, startPosId, dirType);
_candidateScoreDirMap[1][posId][dirType] = score;
score1 += score;
}
else
score1 += _candidateScoreDirMap[1][posId][dirType];
}
this._temporaryRevert(posId);
const deltaScore0 = score0 - prevScore0;
const deltaScore1 = score1 - prevScore1;
this._candidateScores[0][posId] = deltaScore0 * NEXT_MOVER_BUFFER + deltaScore1;
this._candidateScores[1][posId] = deltaScore0 + deltaScore1 * NEXT_MOVER_BUFFER;
this._candidateScoreExpired[posId] = 0;
}
}
_evaluateScoreInDirection(playerId, startPosId, dirType) {
const { context } = this;
const { con, gap } = this.scoreMap;
const { MAX_ADJACENT } = context;
const THRESHOLD = MAX_ADJACENT - 1;
const counters = context.getDirCounters(startPosId, dirType);
const _size = counters.length;
let score = 0;
let countOfReachFinal = 0;
for (let i = 0, j; i < _size; i = j) {
for (; i < _size; ++i) {
if (counters[i].playerId === playerId)
break;
}
if (i === _size)
break;
const hasPrefixSpaces = i > 0 && counters[i - 1].playerId < 0;
let maxPossibleCnt = hasPrefixSpaces ? counters[i - 1].count : 0;
for (j = i; j < _size; ++j) {
const pid = counters[j].playerId;
if (pid >= 0 && pid !== playerId)
break;
maxPossibleCnt += counters[j].count;
}
if (maxPossibleCnt >= MAX_ADJACENT) {
const i0 = hasPrefixSpaces ? i - 1 : i;
for (let k = i, usedK = -1, leftPossibleCnt = 0; k < j; k += 2) {
if (k > i0)
leftPossibleCnt += counters[k - 1].count;
const isFreeSide0 = k > i0 ? 1 : 0;
const cnt1 = counters[k].count;
if (cnt1 < THRESHOLD && k + 2 < j && counters[k + 1].count === 1) {
if (k + 1 < j)
leftPossibleCnt += counters[k + 1].count;
const cnt2 = counters[k + 2].count;
if (cnt2 < THRESHOLD) {
const middleCnt = cnt1 + 1 + cnt2;
const rightPossibleCnt = maxPossibleCnt - leftPossibleCnt - middleCnt;
const isFreeSide2 = k + 3 < j ? 1 : 0;
const normalizedCnt = Math.min(cnt1 + cnt2, THRESHOLD);
const baseScore = gap[normalizedCnt][isFreeSide0 + isFreeSide2];
if (baseScore > 0)
score += baseScore + Math.min(leftPossibleCnt, rightPossibleCnt);
usedK = k + 2;
}
}
if (usedK < k) {
let normalizedCnt = cnt1;
if (cnt1 >= MAX_ADJACENT) {
normalizedCnt = MAX_ADJACENT;
countOfReachFinal += 1;
}
const isFreeSide2 = k + 1 < j ? 1 : 0;
const rightPossibleCnt = maxPossibleCnt - leftPossibleCnt - cnt1;
const baseScore = con[normalizedCnt][isFreeSide0 + isFreeSide2];
if (baseScore > 0)
score += baseScore + Math.min(leftPossibleCnt, rightPossibleCnt);
}
leftPossibleCnt += cnt1;
}
}
}
return { score, countOfReachFinal };
}
_temporaryForward(posId, playerId) {
this.context.forward(posId, playerId);
}
_temporaryRevert(posId) {
this.context.revert(posId);
}
}
class AlphaBetaSearcher {
MAX_CANDIDATE_COUNT;
MIN_PROMOTION_SCORE;
CANDIDATE_GROWTH_FACTOR;
searchContext;
deeperSearcher;
_candidateCache;
constructor(props) {
this.MAX_CANDIDATE_COUNT = props.MAX_CANDIDATE_COUNT;
this.MIN_PROMOTION_SCORE = props.MIN_PROMOTION_SCORE;
this.CANDIDATE_GROWTH_FACTOR = props.CANDIDATE_GROWTH_FACTOR;
this.searchContext = props.mover;
this.deeperSearcher = props.deeperSearcher;
this._candidateCache = [];
}
search(rootPlayerId, alpha, beta) {
const { searchContext, _candidateCache: candidates } = this;
const _size = searchContext.expand(rootPlayerId, candidates, this.CANDIDATE_GROWTH_FACTOR, this.MAX_CANDIDATE_COUNT);
let bestMoveId = candidates[0].posId ?? -1;
if (_size < 2)
return bestMoveId;
const nextPlayerId = rootPlayerId ^ 1;
const { MIN_PROMOTION_SCORE, deeperSearcher } = this;
for (let i = 0; i < _size; ++i) {
const candidate = candidates[i];
const posId = candidate.posId;
searchContext.forward(posId, rootPlayerId);
const gamma = candidate.score < MIN_PROMOTION_SCORE
? searchContext.score(nextPlayerId)
: deeperSearcher.search(nextPlayerId, alpha, beta, 1);
searchContext.revert(posId);
if (alpha < gamma) {
alpha = gamma;
bestMoveId = posId;
}
if (beta <= alpha)
break;
}
return bestMoveId;
}
}
class DeepSearcher {
MAX_SEARCH_DEPTH;
MIN_PROMOTION_SCORE;
mover;
constructor(props) {
this.MAX_SEARCH_DEPTH = props.MAX_SEARCH_DEPTH;
this.MIN_PROMOTION_SCORE = props.MIN_PROMOTION_SCORE;
this.mover = props.mover;
}
search(curPlayerId, alpha, beta, cur) {
const { mover, MAX_SEARCH_DEPTH } = this;
const { rootPlayerId } = mover;
if (mover.couldReachFinal(curPlayerId)) {
return curPlayerId === rootPlayerId ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
}
if (mover.couldReachFinal(curPlayerId ^ 1)) {
return curPlayerId === rootPlayerId ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
}
const candidate = mover.topCandidate(curPlayerId);
if (candidate === undefined)
return Number.MAX_VALUE;
mover.forward(candidate.posId, curPlayerId);
const gamma = cur >= MAX_SEARCH_DEPTH && candidate.score < this.MIN_PROMOTION_SCORE
? mover.score(curPlayerId ^ 1)
: this.search(curPlayerId ^ 1, alpha, beta, cur + 1);
mover.revert(candidate.posId);
return gamma;
}
}
class NarrowSearcher {
MAX_SEARCH_DEPTH;
MAX_CANDIDATE_COUNT;
MIN_PROMOTION_SCORE;
CANDIDATE_GROWTH_FACTOR;
mover;
deeperSearcher;
_candidatesListCache;
constructor(props) {
this.MAX_SEARCH_DEPTH = props.MAX_SEARCH_DEPTH;
this.MAX_CANDIDATE_COUNT = props.MAX_CANDIDATE_COUNT;
this.MIN_PROMOTION_SCORE = props.MIN_PROMOTION_SCORE;
this.CANDIDATE_GROWTH_FACTOR = props.CANDIDATE_GROWTH_FACTOR;
this.mover = props.mover;
this.deeperSearcher = props.deeperSearcher;
this._candidatesListCache = {};
}
search(curPlayerId, alpha, beta, cur) {
const { mover, MAX_SEARCH_DEPTH } = this;
const { rootPlayerId } = mover;
if (mover.couldReachFinal(curPlayerId)) {
return curPlayerId === rootPlayerId ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
}
if (mover.couldReachFinal(curPlayerId ^ 1)) {
return curPlayerId === rootPlayerId ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
}
if (cur > MAX_SEARCH_DEPTH)
return mover.score(curPlayerId);
const candidates = this._getCandidates(cur);
const _size = mover.expand(curPlayerId, candidates, this.CANDIDATE_GROWTH_FACTOR, this.MAX_CANDIDATE_COUNT);
if (_size <= 0)
return Number.MAX_VALUE;
const { MIN_PROMOTION_SCORE, deeperSearcher } = this;
for (let i = 0; i < _size; ++i) {
const candidate = candidates[i];
const posId = candidate.posId;
mover.forward(posId, curPlayerId);
const gamma = cur >= MAX_SEARCH_DEPTH && candidate.score >= MIN_PROMOTION_SCORE
? deeperSearcher.search(curPlayerId ^ 1, alpha, beta, 1)
: this.search(curPlayerId ^ 1, alpha, beta, cur + 1);
mover.revert(posId);
if (curPlayerId === rootPlayerId) {
if (alpha < gamma)
alpha = gamma;
}
else {
if (beta > gamma)
beta = gamma;
}
if (beta <= alpha)
break;
}
return curPlayerId === rootPlayerId ? alpha : beta;
}
_getCandidates(cur) {
let candidates = this._candidatesListCache[cur];
if (candidates === undefined) {
candidates = [];
this._candidatesListCache[cur] = candidates;
}
return candidates;
}
}
const createGomokuSearcher = (props) => {
const { narrowSearcherOptions, deepSearcherOption, searchContext } = props;
const deepSearcher = new DeepSearcher({ ...deepSearcherOption, mover: searchContext });
let searcher = deepSearcher;
for (let i = narrowSearcherOptions.length - 1; i >= 0; --i) {
const option = narrowSearcherOptions[i];
const narrowSearcher = new NarrowSearcher({
...option,
mover: searchContext,
deeperSearcher: searcher,
});
searcher = narrowSearcher;
}
return searcher;
};
const createDefaultGomokuSearcher = (scoreMap, searchContext, options) => {
const { MAX_ADJACENT, CANDIDATE_GROWTH_FACTOR } = options;
const narrowSearcherOptions = [
{
MAX_SEARCH_DEPTH: 2,
MAX_CANDIDATE_COUNT: 8,
MIN_PROMOTION_SCORE: scoreMap.con[MAX_ADJACENT - 3][2] * 4,
CANDIDATE_GROWTH_FACTOR,
},
{
MAX_SEARCH_DEPTH: 4,
MAX_CANDIDATE_COUNT: 4,
MIN_PROMOTION_SCORE: scoreMap.con[MAX_ADJACENT - 2][1] * 2,
CANDIDATE_GROWTH_FACTOR,
},
{
MAX_SEARCH_DEPTH: 8,
MAX_CANDIDATE_COUNT: 2,
MIN_PROMOTION_SCORE: scoreMap.con[MAX_ADJACENT - 2][2] * 4,
CANDIDATE_GROWTH_FACTOR,
},
];
const deepSearcherOption = {
MAX_SEARCH_DEPTH: 16,
MIN_PROMOTION_SCORE: scoreMap.con[MAX_ADJACENT - 1][1],
};
return createGomokuSearcher({ narrowSearcherOptions, deepSearcherOption, searchContext });
};
const createScoreMap = (MAX_ADJACENT) => {
const con = new Array(MAX_ADJACENT + 1).fill([]).map(() => [0, 0, 0]);
const gap = new Array(MAX_ADJACENT + 1).fill([]).map(() => [0, 0, 0]);
let uintValue = 16;
const COST_OF_GAP = uintValue * 2;
for (let cnt = 1; cnt < MAX_ADJACENT; ++cnt, uintValue *= 16) {
con[cnt] = [0, uintValue, uintValue * 2];
gap[cnt] = [0, uintValue / 2 - COST_OF_GAP, uintValue - COST_OF_GAP];
}
const _v = con[MAX_ADJACENT - 1][1];
gap[MAX_ADJACENT - 1] = [_v - COST_OF_GAP, _v - COST_OF_GAP / 2, _v - COST_OF_GAP / 4];
gap[MAX_ADJACENT] = [_v - COST_OF_GAP, _v - COST_OF_GAP / 2, _v - COST_OF_GAP / 4];
con[MAX_ADJACENT] = [uintValue, uintValue, uintValue];
return { con, gap };
};
class GomokuSolution {
CANDIDATE_GROWTH_FACTOR;
scoreMap;
mover;
_moverContext;
_searcher;
constructor(props) {
const { MAX_ROW, MAX_COL, MAX_ADJACENT = 5, MAX_DISTANCE_OF_NEIGHBOR = 2, CANDIDATE_GROWTH_FACTOR = 8, } = props;
const _moverContext = new GomokuMoverContext({
MAX_ROW,
MAX_COL,
MAX_ADJACENT,
MAX_DISTANCE_OF_NEIGHBOR,
});
const scoreMap = props.scoreMap ?? createScoreMap(_moverContext.MAX_ADJACENT);
const counter = new GomokuMoverCounter(_moverContext);
const state = new GomokuMoverState({ context: _moverContext, counter, scoreMap });
const mover = new GomokuMover({ context: _moverContext, counter, state });
const _searcher = new AlphaBetaSearcher({
MAX_CANDIDATE_COUNT: 16,
MIN_PROMOTION_SCORE: scoreMap.con[MAX_ADJACENT - 3][1],
CANDIDATE_GROWTH_FACTOR,
mover,
deeperSearcher: props.deeperSearcher?.(mover) ??
createDefaultGomokuSearcher(scoreMap, mover, {
MAX_ADJACENT: _moverContext.MAX_ADJACENT,
CANDIDATE_GROWTH_FACTOR,
}),
});
this.CANDIDATE_GROWTH_FACTOR = CANDIDATE_GROWTH_FACTOR;
this.mover = mover;
this.scoreMap = scoreMap;
this._moverContext = _moverContext;
this._searcher = _searcher;
}
init(pieces, searcher) {
this.mover.init(pieces);
if (searcher != null) {
this._searcher = searcher;
}
}
forward(r, c, playerId) {
const { mover, _moverContext } = this;
if (_moverContext.isValidPos(r, c)) {
const posId = _moverContext.idx(r, c);
mover.forward(posId, playerId);
}
}
revert(r, c) {
const { mover, _moverContext: moverContext } = this;
if (moverContext.isValidPos(r, c)) {
const posId = moverContext.idx(r, c);
mover.revert(posId);
}
}
minimaxSearch(nextPlayerId) {
if (this.mover.isFinal())
return [-1, -1];
const { mover, _moverContext } = this;
if (_moverContext.placedCount * 2 < _moverContext.MAX_ADJACENT) {
const _candidates = [];
const _size = Math.min(8, mover.expand(nextPlayerId, _candidates, 1.5));
const index = Math.min(_size - 1, Math.round(Math.random() * _size));
const bestMoveId = _candidates[index].posId;
const [r, c] = _moverContext.revIdx(bestMoveId);
return [r, c];
}
mover.resetRootPlayerId(nextPlayerId);
const bestMoveId = this._searcher.search(nextPlayerId, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, 1);
if (bestMoveId < 0) {
throw new Error('Oops! Something must be wrong, cannot find a valid moving strategy');
}
const [r, c] = _moverContext.revIdx(bestMoveId);
return [r, c];
}
}
exports.AlphaBetaSearcher = AlphaBetaSearcher;
exports.DeepSearcher = DeepSearcher;
exports.GomokuDirectionTypeBitset = GomokuDirectionTypeBitset;
exports.GomokuDirectionTypes = GomokuDirectionTypes;
exports.GomokuDirections = GomokuDirections;
exports.GomokuMover = GomokuMover;
exports.GomokuMoverContext = GomokuMoverContext;
exports.GomokuMoverCounter = GomokuMoverCounter;
exports.GomokuMoverState = GomokuMoverState;
exports.GomokuSolution = GomokuSolution;
exports.NarrowSearcher = NarrowSearcher;
exports.createDefaultGomokuSearcher = createDefaultGomokuSearcher;
exports.createGomokuSearcher = createGomokuSearcher;
exports.createHighDimensionArray = createHighDimensionArray;
exports.createScoreMap = createScoreMap;