mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-29 12:27:07 +02:00
GO: Various changes before 2.6.0 (#1120)
This commit is contained in:
@@ -1,13 +1,6 @@
|
||||
import {
|
||||
Board,
|
||||
BoardState,
|
||||
Neighbor,
|
||||
opponents,
|
||||
PlayerColor,
|
||||
playerColors,
|
||||
PointState,
|
||||
validityReason,
|
||||
} from "../boardState/goConstants";
|
||||
import type { Board, BoardState, Neighbor, PointState, SimpleBoard } from "../Types";
|
||||
|
||||
import { GoValidity, GoOpponent, GoColor } from "@enums";
|
||||
import {
|
||||
findAdjacentPointsInChain,
|
||||
findNeighbors,
|
||||
@@ -15,7 +8,6 @@ import {
|
||||
getBoardCopy,
|
||||
getEmptySpaces,
|
||||
getNewBoardState,
|
||||
getStateCopy,
|
||||
isDefined,
|
||||
isNotNull,
|
||||
updateCaptures,
|
||||
@@ -35,120 +27,106 @@ import {
|
||||
*
|
||||
* @returns a validity explanation for if the move is legal or not
|
||||
*/
|
||||
export function evaluateIfMoveIsValid(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: PlayerColor,
|
||||
shortcut = true,
|
||||
) {
|
||||
const point = boardState.board?.[x]?.[y];
|
||||
export function evaluateIfMoveIsValid(boardState: BoardState, x: number, y: number, player: GoColor, shortcut = true) {
|
||||
const point = boardState.board[x]?.[y];
|
||||
|
||||
if (boardState.previousPlayer === null) {
|
||||
return validityReason.gameOver;
|
||||
return GoValidity.gameOver;
|
||||
}
|
||||
if (boardState.previousPlayer === player) {
|
||||
return validityReason.notYourTurn;
|
||||
return GoValidity.notYourTurn;
|
||||
}
|
||||
if (!point) {
|
||||
return validityReason.pointBroken;
|
||||
return GoValidity.pointBroken;
|
||||
}
|
||||
if (point.player !== playerColors.empty) {
|
||||
return validityReason.pointNotEmpty;
|
||||
if (point.color !== GoColor.empty) {
|
||||
return GoValidity.pointNotEmpty;
|
||||
}
|
||||
|
||||
// Detect if the current player has ever previously played this move. Used to detect potential repeated board states
|
||||
const moveHasBeenPlayedBefore = !!boardState.history.find((board) => board[x]?.[y]?.player === player);
|
||||
// Detect if the move might be an immediate repeat (only one board of history is saved to check)
|
||||
const possibleRepeat = boardState.previousBoard && getColorOnSimpleBoard(boardState.previousBoard, x, y) === player;
|
||||
|
||||
if (shortcut) {
|
||||
// If the current point has some adjacent open spaces, it is not suicide. If the move is not repeated, it is legal
|
||||
const liberties = findAdjacentLibertiesForPoint(boardState, x, y);
|
||||
const liberties = findAdjacentLibertiesForPoint(boardState.board, x, y);
|
||||
const hasLiberty = liberties.north || liberties.east || liberties.south || liberties.west;
|
||||
if (!moveHasBeenPlayedBefore && hasLiberty) {
|
||||
return validityReason.valid;
|
||||
if (!possibleRepeat && hasLiberty) {
|
||||
return GoValidity.valid;
|
||||
}
|
||||
|
||||
// If a connected friendly chain has more than one liberty, the move is not suicide. If the move is not repeated, it is legal
|
||||
const neighborChainLibertyCount = findMaxLibertyCountOfAdjacentChains(boardState, x, y, player);
|
||||
if (!moveHasBeenPlayedBefore && neighborChainLibertyCount > 1) {
|
||||
return validityReason.valid;
|
||||
if (!possibleRepeat && neighborChainLibertyCount > 1) {
|
||||
return GoValidity.valid;
|
||||
}
|
||||
|
||||
// If there is any neighboring enemy chain with only one liberty, and the move is not repeated, it is valid,
|
||||
// because it would capture the enemy chain and free up some liberties for itself
|
||||
const potentialCaptureChainLibertyCount = findMinLibertyCountOfAdjacentChains(
|
||||
boardState,
|
||||
boardState.board,
|
||||
x,
|
||||
y,
|
||||
player === playerColors.black ? playerColors.white : playerColors.black,
|
||||
player === GoColor.black ? GoColor.white : GoColor.black,
|
||||
);
|
||||
if (!moveHasBeenPlayedBefore && potentialCaptureChainLibertyCount < 2) {
|
||||
return validityReason.valid;
|
||||
if (!possibleRepeat && potentialCaptureChainLibertyCount < 2) {
|
||||
return GoValidity.valid;
|
||||
}
|
||||
|
||||
// If there is no direct liberties for the move, no captures, and no neighboring friendly chains with multiple liberties,
|
||||
// the move is not valid because it would suicide the piece
|
||||
if (!hasLiberty && potentialCaptureChainLibertyCount >= 2 && neighborChainLibertyCount <= 1) {
|
||||
return validityReason.noSuicide;
|
||||
return GoValidity.noSuicide;
|
||||
}
|
||||
}
|
||||
|
||||
// If the move has been played before and is not obviously illegal, we have to actually play it out to determine
|
||||
// if it is a repeated move, or if it is a valid move
|
||||
const evaluationBoard = evaluateMoveResult(boardState, x, y, player, true);
|
||||
if (evaluationBoard.board[x]?.[y]?.player !== player) {
|
||||
return validityReason.noSuicide;
|
||||
const evaluationBoard = evaluateMoveResult(boardState.board, x, y, player, true);
|
||||
if (evaluationBoard[x]?.[y]?.color !== player) {
|
||||
return GoValidity.noSuicide;
|
||||
}
|
||||
if (moveHasBeenPlayedBefore && checkIfBoardStateIsRepeated(evaluationBoard)) {
|
||||
return validityReason.boardRepeated;
|
||||
if (possibleRepeat && boardState.previousBoard) {
|
||||
const simpleEvalBoard = simpleBoardFromBoard(evaluationBoard);
|
||||
if (areSimpleBoardsIdentical(simpleEvalBoard, boardState.previousBoard)) return GoValidity.boardRepeated;
|
||||
}
|
||||
|
||||
return validityReason.valid;
|
||||
return GoValidity.valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new evaluation board and play out the results of the given move on the new board
|
||||
* @returns the evaluation board
|
||||
*/
|
||||
export function evaluateMoveResult(
|
||||
initialBoardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: playerColors,
|
||||
resetChains = false,
|
||||
) {
|
||||
const boardState = getStateCopy(initialBoardState);
|
||||
boardState.history.push(getBoardCopy(boardState).board);
|
||||
const point = boardState.board[x]?.[y];
|
||||
if (!point) {
|
||||
return initialBoardState;
|
||||
}
|
||||
export function evaluateMoveResult(board: Board, x: number, y: number, player: GoColor, resetChains = false): Board {
|
||||
const evaluationBoard = getBoardCopy(board);
|
||||
const point = evaluationBoard[x]?.[y];
|
||||
if (!point) return board;
|
||||
|
||||
point.player = player;
|
||||
boardState.previousPlayer = player;
|
||||
point.color = player;
|
||||
|
||||
const neighbors = getArrayFromNeighbor(findNeighbors(boardState, x, y));
|
||||
const neighbors = getArrayFromNeighbor(findNeighbors(board, x, y));
|
||||
const chainIdsToUpdate = [point.chain, ...neighbors.map((point) => point.chain)];
|
||||
resetChainsById(boardState, chainIdsToUpdate);
|
||||
|
||||
return updateCaptures(boardState, player, resetChains);
|
||||
resetChainsById(evaluationBoard, chainIdsToUpdate);
|
||||
updateCaptures(evaluationBoard, player, resetChains);
|
||||
return evaluationBoard;
|
||||
}
|
||||
|
||||
export function getControlledSpace(boardState: BoardState) {
|
||||
const chains = getAllChains(boardState);
|
||||
const length = boardState.board[0].length;
|
||||
const whiteControlledEmptyNodes = getAllPotentialEyes(boardState, chains, playerColors.white, length * 2)
|
||||
export function getControlledSpace(board: Board) {
|
||||
const chains = getAllChains(board);
|
||||
const length = board[0].length;
|
||||
const whiteControlledEmptyNodes = getAllPotentialEyes(board, chains, GoColor.white, length * 2)
|
||||
.map((eye) => eye.chain)
|
||||
.flat();
|
||||
const blackControlledEmptyNodes = getAllPotentialEyes(boardState, chains, playerColors.black, length * 2)
|
||||
const blackControlledEmptyNodes = getAllPotentialEyes(board, chains, GoColor.black, length * 2)
|
||||
.map((eye) => eye.chain)
|
||||
.flat();
|
||||
|
||||
const ownedPointGrid = Array.from({ length }, () => Array.from({ length }, () => playerColors.empty));
|
||||
const ownedPointGrid = Array.from({ length }, () => Array.from({ length }, () => GoColor.empty));
|
||||
whiteControlledEmptyNodes.forEach((node) => {
|
||||
ownedPointGrid[node.x][node.y] = playerColors.white;
|
||||
ownedPointGrid[node.x][node.y] = GoColor.white;
|
||||
});
|
||||
blackControlledEmptyNodes.forEach((node) => {
|
||||
ownedPointGrid[node.x][node.y] = playerColors.black;
|
||||
ownedPointGrid[node.x][node.y] = GoColor.black;
|
||||
});
|
||||
|
||||
return ownedPointGrid;
|
||||
@@ -157,30 +135,28 @@ export function getControlledSpace(boardState: BoardState) {
|
||||
/**
|
||||
Clear the chain and liberty data of all points in the given chains
|
||||
*/
|
||||
const resetChainsById = (boardState: BoardState, chainIds: string[]) => {
|
||||
const pointsToUpdate = boardState.board
|
||||
.flat()
|
||||
.filter(isDefined)
|
||||
.filter(isNotNull)
|
||||
.filter((point) => chainIds.includes(point.chain));
|
||||
pointsToUpdate.forEach((point) => {
|
||||
point.chain = "";
|
||||
point.liberties = [];
|
||||
});
|
||||
const resetChainsById = (board: Board, chainIds: string[]) => {
|
||||
for (const column of board) {
|
||||
for (const point of column) {
|
||||
if (!point || !chainIds.includes(point.chain)) continue;
|
||||
point.chain = "";
|
||||
point.liberties = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* For a potential move, determine what the liberty of the point would be if played, by looking at adjacent empty nodes
|
||||
* as well as the remaining liberties of neighboring friendly chains
|
||||
*/
|
||||
export function findEffectiveLibertiesOfNewMove(boardState: BoardState, x: number, y: number, player: PlayerColor) {
|
||||
const friendlyChains = getAllChains(boardState).filter((chain) => chain[0].player === player);
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
|
||||
export function findEffectiveLibertiesOfNewMove(board: Board, x: number, y: number, player: GoColor) {
|
||||
const friendlyChains = getAllChains(board).filter((chain) => chain[0].color === player);
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(board, x, y, player);
|
||||
const neighborPoints = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined);
|
||||
// Get all chains that the new move will connect to
|
||||
const allyNeighbors = neighborPoints.filter((neighbor) => neighbor.player === player);
|
||||
const allyNeighbors = neighborPoints.filter((neighbor) => neighbor.color === player);
|
||||
const allyNeighborChainLiberties = allyNeighbors
|
||||
.map((neighbor) => {
|
||||
const chain = friendlyChains.find((chain) => chain[0].chain === neighbor.chain);
|
||||
@@ -190,7 +166,7 @@ export function findEffectiveLibertiesOfNewMove(boardState: BoardState, x: numbe
|
||||
.filter(isNotNull);
|
||||
|
||||
// Get all empty spaces that the new move connects to that aren't already part of friendly liberties
|
||||
const directLiberties = neighborPoints.filter((neighbor) => neighbor.player === playerColors.empty);
|
||||
const directLiberties = neighborPoints.filter((neighbor) => neighbor.color === GoColor.empty);
|
||||
|
||||
const allLiberties = [...directLiberties, ...allyNeighborChainLiberties];
|
||||
|
||||
@@ -206,17 +182,12 @@ export function findEffectiveLibertiesOfNewMove(boardState: BoardState, x: numbe
|
||||
/**
|
||||
* Find the number of open spaces that are connected to chains adjacent to a given point, and return the maximum
|
||||
*/
|
||||
export function findMaxLibertyCountOfAdjacentChains(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: playerColors,
|
||||
) {
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
|
||||
export function findMaxLibertyCountOfAdjacentChains(boardState: BoardState, x: number, y: number, player: GoColor) {
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState.board, x, y, player);
|
||||
const friendlyNeighbors = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined)
|
||||
.filter((neighbor) => neighbor.player === player);
|
||||
.filter((neighbor) => neighbor.color === player);
|
||||
|
||||
return friendlyNeighbors.reduce((max, neighbor) => Math.max(max, neighbor?.liberties?.length ?? 0), 0);
|
||||
}
|
||||
@@ -224,28 +195,18 @@ export function findMaxLibertyCountOfAdjacentChains(
|
||||
/**
|
||||
* Find the number of open spaces that are connected to chains adjacent to a given point, and return the minimum
|
||||
*/
|
||||
export function findMinLibertyCountOfAdjacentChains(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: playerColors,
|
||||
) {
|
||||
const chain = findEnemyNeighborChainWithFewestLiberties(boardState, x, y, player);
|
||||
export function findMinLibertyCountOfAdjacentChains(board: Board, x: number, y: number, player: GoColor) {
|
||||
const chain = findEnemyNeighborChainWithFewestLiberties(board, x, y, player);
|
||||
return chain?.[0]?.liberties?.length ?? 99;
|
||||
}
|
||||
|
||||
export function findEnemyNeighborChainWithFewestLiberties(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: playerColors,
|
||||
) {
|
||||
const chains = getAllChains(boardState);
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
|
||||
export function findEnemyNeighborChainWithFewestLiberties(board: Board, x: number, y: number, player: GoColor) {
|
||||
const chains = getAllChains(board);
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(board, x, y, player);
|
||||
const friendlyNeighbors = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined)
|
||||
.filter((neighbor) => neighbor.player === player);
|
||||
.filter((neighbor) => neighbor.color === player);
|
||||
|
||||
const minimumLiberties = friendlyNeighbors.reduce(
|
||||
(min, neighbor) => Math.min(min, neighbor?.liberties?.length ?? 0),
|
||||
@@ -259,9 +220,9 @@ export function findEnemyNeighborChainWithFewestLiberties(
|
||||
/**
|
||||
* Returns a list of points that are valid moves for the given player
|
||||
*/
|
||||
export function getAllValidMoves(boardState: BoardState, player: PlayerColor) {
|
||||
return getEmptySpaces(boardState).filter(
|
||||
(point) => evaluateIfMoveIsValid(boardState, point.x, point.y, player) === validityReason.valid,
|
||||
export function getAllValidMoves(boardState: BoardState, player: GoColor) {
|
||||
return getEmptySpaces(boardState.board).filter(
|
||||
(point) => evaluateIfMoveIsValid(boardState, point.x, point.y, player) === GoValidity.valid,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -272,9 +233,9 @@ export function getAllValidMoves(boardState: BoardState, player: PlayerColor) {
|
||||
|
||||
Eyes are important, because a chain of pieces cannot be captured if it fully surrounds two or more eyes.
|
||||
*/
|
||||
export function getAllEyesByChainId(boardState: BoardState, player: playerColors) {
|
||||
const allChains = getAllChains(boardState);
|
||||
const eyeCandidates = getAllPotentialEyes(boardState, allChains, player);
|
||||
export function getAllEyesByChainId(board: Board, player: GoColor) {
|
||||
const allChains = getAllChains(board);
|
||||
const eyeCandidates = getAllPotentialEyes(board, allChains, player);
|
||||
const eyes: { [s: string]: PointState[][] } = {};
|
||||
|
||||
eyeCandidates.forEach((candidate) => {
|
||||
@@ -292,7 +253,7 @@ export function getAllEyesByChainId(boardState: BoardState, player: playerColors
|
||||
|
||||
// If any chain fully encircles the empty space (even if there are other chains encircled as well), the eye is true
|
||||
const neighborsEncirclingEye = findNeighboringChainsThatFullyEncircleEmptySpace(
|
||||
boardState,
|
||||
board,
|
||||
candidate.chain,
|
||||
candidate.neighbors,
|
||||
allChains,
|
||||
@@ -310,8 +271,8 @@ export function getAllEyesByChainId(boardState: BoardState, player: playerColors
|
||||
/**
|
||||
* Get a list of all eyes, grouped by the chain they are adjacent to
|
||||
*/
|
||||
export function getAllEyes(boardState: BoardState, player: playerColors, eyesObject?: { [s: string]: PointState[][] }) {
|
||||
const eyes = eyesObject ?? getAllEyesByChainId(boardState, player);
|
||||
export function getAllEyes(board: Board, player: GoColor, eyesObject?: { [s: string]: PointState[][] }) {
|
||||
const eyes = eyesObject ?? getAllEyesByChainId(board, player);
|
||||
return Object.keys(eyes).map((key) => eyes[key]);
|
||||
}
|
||||
|
||||
@@ -320,33 +281,28 @@ export function getAllEyes(boardState: BoardState, player: playerColors, eyesObj
|
||||
For each player chain number, add any empty space chains that are completely surrounded by a single player's color to
|
||||
an array at that chain number's index.
|
||||
*/
|
||||
export function getAllPotentialEyes(
|
||||
boardState: BoardState,
|
||||
allChains: PointState[][],
|
||||
player: playerColors,
|
||||
_maxSize?: number,
|
||||
) {
|
||||
const nodeCount = boardState.board.map((row) => row.filter((p) => p)).flat().length;
|
||||
export function getAllPotentialEyes(board: Board, allChains: PointState[][], player: GoColor, _maxSize?: number) {
|
||||
const nodeCount = board.map((row) => row.filter((p) => p)).flat().length;
|
||||
const maxSize = _maxSize ?? Math.min(nodeCount * 0.4, 11);
|
||||
const emptyPointChains = allChains.filter((chain) => chain[0].player === playerColors.empty);
|
||||
const emptyPointChains = allChains.filter((chain) => chain[0].color === GoColor.empty);
|
||||
const eyeCandidates: { neighbors: PointState[][]; chain: PointState[]; id: string }[] = [];
|
||||
|
||||
emptyPointChains
|
||||
.filter((chain) => chain.length <= maxSize)
|
||||
.forEach((chain) => {
|
||||
const neighboringChains = getAllNeighboringChains(boardState, chain, allChains);
|
||||
const neighboringChains = getAllNeighboringChains(board, chain, allChains);
|
||||
|
||||
const hasWhitePieceNeighbor = neighboringChains.find(
|
||||
(neighborChain) => neighborChain[0]?.player === playerColors.white,
|
||||
(neighborChain) => neighborChain[0]?.color === GoColor.white,
|
||||
);
|
||||
const hasBlackPieceNeighbor = neighboringChains.find(
|
||||
(neighborChain) => neighborChain[0]?.player === playerColors.black,
|
||||
(neighborChain) => neighborChain[0]?.color === GoColor.black,
|
||||
);
|
||||
|
||||
// Record the neighbor chains of the eye candidate empty chain, if all of its neighbors are the same color piece
|
||||
if (
|
||||
(hasWhitePieceNeighbor && !hasBlackPieceNeighbor && player === playerColors.white) ||
|
||||
(!hasWhitePieceNeighbor && hasBlackPieceNeighbor && player === playerColors.black)
|
||||
(hasWhitePieceNeighbor && !hasBlackPieceNeighbor && player === GoColor.white) ||
|
||||
(!hasWhitePieceNeighbor && hasBlackPieceNeighbor && player === GoColor.black)
|
||||
) {
|
||||
eyeCandidates.push({
|
||||
neighbors: neighboringChains,
|
||||
@@ -366,12 +322,12 @@ export function getAllPotentialEyes(
|
||||
* If so, the original candidate is a true eye.
|
||||
*/
|
||||
function findNeighboringChainsThatFullyEncircleEmptySpace(
|
||||
boardState: BoardState,
|
||||
board: Board,
|
||||
candidateChain: PointState[],
|
||||
neighborChainList: PointState[][],
|
||||
allChains: PointState[][],
|
||||
) {
|
||||
const boardMax = boardState.board[0].length - 1;
|
||||
const boardMax = board[0].length - 1;
|
||||
const candidateSpread = findFurthestPointsOfChain(candidateChain);
|
||||
return neighborChainList.filter((neighborChain, index) => {
|
||||
// If the chain does not go far enough to surround the eye in question, don't bother building an eval board
|
||||
@@ -392,23 +348,23 @@ function findNeighboringChainsThatFullyEncircleEmptySpace(
|
||||
return false;
|
||||
}
|
||||
|
||||
const evaluationBoard = getStateCopy(boardState);
|
||||
const evaluationBoard = getBoardCopy(board);
|
||||
const examplePoint = candidateChain[0];
|
||||
const otherChainNeighborPoints = removePointAtIndex(neighborChainList, index)
|
||||
.flat()
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined);
|
||||
otherChainNeighborPoints.forEach((point) => {
|
||||
const pointToEdit = evaluationBoard.board[point.x]?.[point.y];
|
||||
const pointToEdit = evaluationBoard[point.x]?.[point.y];
|
||||
if (pointToEdit) {
|
||||
pointToEdit.player = playerColors.empty;
|
||||
pointToEdit.color = GoColor.empty;
|
||||
}
|
||||
});
|
||||
const updatedBoard = updateChains(evaluationBoard);
|
||||
const newChains = getAllChains(updatedBoard);
|
||||
const newChainID = updatedBoard.board[examplePoint.x]?.[examplePoint.y]?.chain;
|
||||
updateChains(evaluationBoard);
|
||||
const newChains = getAllChains(evaluationBoard);
|
||||
const newChainID = evaluationBoard[examplePoint.x]?.[examplePoint.y]?.chain;
|
||||
const chain = newChains.find((chain) => chain[0].chain === newChainID) || [];
|
||||
const newNeighborChains = getAllNeighboringChains(boardState, chain, allChains);
|
||||
const newNeighborChains = getAllNeighboringChains(board, chain, allChains);
|
||||
|
||||
return newNeighborChains.length === 1;
|
||||
});
|
||||
@@ -456,8 +412,8 @@ function removePointAtIndex(arr: PointState[][], index: number) {
|
||||
/**
|
||||
* Get all player chains that are adjacent / touching the current chain
|
||||
*/
|
||||
export function getAllNeighboringChains(boardState: BoardState, chain: PointState[], allChains: PointState[][]) {
|
||||
const playerNeighbors = getPlayerNeighbors(boardState, chain);
|
||||
export function getAllNeighboringChains(board: Board, chain: PointState[], allChains: PointState[][]) {
|
||||
const playerNeighbors = getPlayerNeighbors(board, chain);
|
||||
|
||||
const neighboringChains = playerNeighbors.reduce(
|
||||
(neighborChains, neighbor) =>
|
||||
@@ -471,16 +427,16 @@ export function getAllNeighboringChains(boardState: BoardState, chain: PointStat
|
||||
/**
|
||||
* Gets all points that have player pieces adjacent to the given point
|
||||
*/
|
||||
export function getPlayerNeighbors(boardState: BoardState, chain: PointState[]) {
|
||||
return getAllNeighbors(boardState, chain).filter((neighbor) => neighbor && neighbor.player !== playerColors.empty);
|
||||
export function getPlayerNeighbors(board: Board, chain: PointState[]) {
|
||||
return getAllNeighbors(board, chain).filter((neighbor) => neighbor && neighbor.color !== GoColor.empty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all points adjacent to the given point
|
||||
*/
|
||||
export function getAllNeighbors(boardState: BoardState, chain: PointState[]) {
|
||||
export function getAllNeighbors(board: Board, chain: PointState[]) {
|
||||
const allNeighbors = chain.reduce((chainNeighbors: Set<PointState>, point: PointState) => {
|
||||
getArrayFromNeighbor(findNeighbors(boardState, point.x, point.y))
|
||||
getArrayFromNeighbor(findNeighbors(board, point.x, point.y))
|
||||
.filter((neighborPoint) => !isPointInChain(neighborPoint, chain))
|
||||
.forEach((neighborPoint) => chainNeighbors.add(neighborPoint));
|
||||
return chainNeighbors;
|
||||
@@ -495,33 +451,15 @@ export function isPointInChain(point: PointState, chain: PointState[]) {
|
||||
return !!chain.find((chainPoint) => chainPoint.x === point.x && chainPoint.y === point.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks through the board history to see if the current state is identical to any previous state
|
||||
* Capped at 5 for calculation speed, because loops of size 6 are essentially impossible
|
||||
*/
|
||||
function checkIfBoardStateIsRepeated(boardState: BoardState) {
|
||||
const currentBoard = boardState.board;
|
||||
return boardState.history.slice(-5).find((state) => {
|
||||
for (let x = 0; x < state.length; x++) {
|
||||
for (let y = 0; y < state[x].length; y++) {
|
||||
if (currentBoard[x]?.[y]?.player && currentBoard[x]?.[y]?.player !== state[x]?.[y]?.player) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all groups of connected pieces, or empty space groups
|
||||
*/
|
||||
export function getAllChains(boardState: BoardState): PointState[][] {
|
||||
export function getAllChains(board: Board): PointState[][] {
|
||||
const chains: { [s: string]: PointState[] } = {};
|
||||
|
||||
for (let x = 0; x < boardState.board.length; x++) {
|
||||
for (let y = 0; y < boardState.board[x].length; y++) {
|
||||
const point = boardState.board[x]?.[y];
|
||||
for (let x = 0; x < board.length; x++) {
|
||||
for (let y = 0; y < board[x].length; y++) {
|
||||
const point = board[x]?.[y];
|
||||
// If the current chain is already analyzed, skip it
|
||||
if (!point || point.chain === "") {
|
||||
continue;
|
||||
@@ -538,8 +476,8 @@ export function getAllChains(boardState: BoardState): PointState[][] {
|
||||
/**
|
||||
* Find any group of stones with no liberties (who therefore are to be removed from the board)
|
||||
*/
|
||||
export function findAllCapturedChains(chainList: PointState[][], playerWhoMoved: PlayerColor) {
|
||||
const opposingPlayer = playerWhoMoved === playerColors.white ? playerColors.black : playerColors.white;
|
||||
export function findAllCapturedChains(chainList: PointState[][], playerWhoMoved: GoColor) {
|
||||
const opposingPlayer = playerWhoMoved === GoColor.white ? GoColor.black : GoColor.white;
|
||||
const enemyChainsToCapture = findCapturedChainOfColor(chainList, opposingPlayer);
|
||||
|
||||
if (enemyChainsToCapture) {
|
||||
@@ -552,36 +490,36 @@ export function findAllCapturedChains(chainList: PointState[][], playerWhoMoved:
|
||||
}
|
||||
}
|
||||
|
||||
function findCapturedChainOfColor(chainList: PointState[][], playerColor: PlayerColor) {
|
||||
return chainList.filter((chain) => chain?.[0].player === playerColor && chain?.[0].liberties?.length === 0);
|
||||
function findCapturedChainOfColor(chainList: PointState[][], playerColor: GoColor) {
|
||||
return chainList.filter((chain) => chain?.[0].color === playerColor && chain?.[0].liberties?.length === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all empty points adjacent to any piece in a given chain
|
||||
*/
|
||||
export function findLibertiesForChain(boardState: BoardState, chain: PointState[]): PointState[] {
|
||||
return getAllNeighbors(boardState, chain).filter((neighbor) => neighbor && neighbor.player === playerColors.empty);
|
||||
export function findLibertiesForChain(board: Board, chain: PointState[]): PointState[] {
|
||||
return getAllNeighbors(board, chain).filter((neighbor) => neighbor && neighbor.color === GoColor.empty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all empty points adjacent to any piece in the chain that a given point belongs to
|
||||
*/
|
||||
export function findChainLibertiesForPoint(boardState: BoardState, x: number, y: number): PointState[] {
|
||||
const chain = findAdjacentPointsInChain(boardState, x, y);
|
||||
return findLibertiesForChain(boardState, chain);
|
||||
export function findChainLibertiesForPoint(board: Board, x: number, y: number): PointState[] {
|
||||
const chain = findAdjacentPointsInChain(board, x, y);
|
||||
return findLibertiesForChain(board, chain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object that includes which of the cardinal neighbors are empty
|
||||
* (adjacent 'liberties' of the current piece )
|
||||
*/
|
||||
export function findAdjacentLibertiesForPoint(boardState: BoardState, x: number, y: number): Neighbor {
|
||||
const neighbors = findNeighbors(boardState, x, y);
|
||||
export function findAdjacentLibertiesForPoint(board: Board, x: number, y: number): Neighbor {
|
||||
const neighbors = findNeighbors(board, x, y);
|
||||
|
||||
const hasNorthLiberty = neighbors.north && neighbors.north.player === playerColors.empty;
|
||||
const hasEastLiberty = neighbors.east && neighbors.east.player === playerColors.empty;
|
||||
const hasSouthLiberty = neighbors.south && neighbors.south.player === playerColors.empty;
|
||||
const hasWestLiberty = neighbors.west && neighbors.west.player === playerColors.empty;
|
||||
const hasNorthLiberty = neighbors.north && neighbors.north.color === GoColor.empty;
|
||||
const hasEastLiberty = neighbors.east && neighbors.east.color === GoColor.empty;
|
||||
const hasSouthLiberty = neighbors.south && neighbors.south.color === GoColor.empty;
|
||||
const hasWestLiberty = neighbors.west && neighbors.west.color === GoColor.empty;
|
||||
|
||||
return {
|
||||
north: hasNorthLiberty ? neighbors.north : null,
|
||||
@@ -596,22 +534,21 @@ export function findAdjacentLibertiesForPoint(boardState: BoardState, x: number,
|
||||
* current player's pieces. Used for making the connection map on the board
|
||||
*/
|
||||
export function findAdjacentLibertiesAndAlliesForPoint(
|
||||
boardState: BoardState,
|
||||
board: Board,
|
||||
x: number,
|
||||
y: number,
|
||||
_player?: PlayerColor,
|
||||
_player?: GoColor,
|
||||
): Neighbor {
|
||||
const currentPoint = boardState.board[x]?.[y];
|
||||
const player =
|
||||
_player || (!currentPoint || currentPoint.player === playerColors.empty ? undefined : currentPoint.player);
|
||||
const adjacentLiberties = findAdjacentLibertiesForPoint(boardState, x, y);
|
||||
const neighbors = findNeighbors(boardState, x, y);
|
||||
const currentPoint = board[x]?.[y];
|
||||
const player = _player || (!currentPoint || currentPoint.color === GoColor.empty ? undefined : currentPoint.color);
|
||||
const adjacentLiberties = findAdjacentLibertiesForPoint(board, x, y);
|
||||
const neighbors = findNeighbors(board, x, y);
|
||||
|
||||
return {
|
||||
north: adjacentLiberties.north || neighbors.north?.player === player ? neighbors.north : null,
|
||||
east: adjacentLiberties.east || neighbors.east?.player === player ? neighbors.east : null,
|
||||
south: adjacentLiberties.south || neighbors.south?.player === player ? neighbors.south : null,
|
||||
west: adjacentLiberties.west || neighbors.west?.player === player ? neighbors.west : null,
|
||||
north: adjacentLiberties.north || neighbors.north?.color === player ? neighbors.north : null,
|
||||
east: adjacentLiberties.east || neighbors.east?.color === player ? neighbors.east : null,
|
||||
south: adjacentLiberties.south || neighbors.south?.color === player ? neighbors.south : null,
|
||||
west: adjacentLiberties.west || neighbors.west?.color === player ? neighbors.west : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -638,16 +575,16 @@ export function findAdjacentLibertiesAndAlliesForPoint(
|
||||
* be rotated 90 degrees clockwise compared to the board UI as shown in the IPvGO game.
|
||||
*
|
||||
*/
|
||||
export function getSimplifiedBoardState(board: Board): string[] {
|
||||
export function simpleBoardFromBoard(board: Board): string[] {
|
||||
return board.map((column) =>
|
||||
column.reduce((str, point) => {
|
||||
if (!point) {
|
||||
return str + "#";
|
||||
}
|
||||
if (point.player === playerColors.black) {
|
||||
if (point.color === GoColor.black) {
|
||||
return str + "X";
|
||||
}
|
||||
if (point.player === playerColors.white) {
|
||||
if (point.color === GoColor.white) {
|
||||
return str + "O";
|
||||
}
|
||||
return str + ".";
|
||||
@@ -655,29 +592,47 @@ export function getSimplifiedBoardState(board: Board): string[] {
|
||||
);
|
||||
}
|
||||
|
||||
export function getBoardFromSimplifiedBoardState(
|
||||
boardStrings: string[],
|
||||
ai = opponents.Daedalus,
|
||||
lastPlayer = playerColors.black,
|
||||
) {
|
||||
const newBoardState = getNewBoardState(boardStrings[0].length, ai);
|
||||
newBoardState.previousPlayer = lastPlayer;
|
||||
|
||||
for (let x = 0; x < boardStrings[0].length; x++) {
|
||||
for (let y = 0; y < boardStrings[0].length; y++) {
|
||||
const boardStringPoint = boardStrings[x]?.[y];
|
||||
const newBoardPoint = newBoardState.board[x]?.[y];
|
||||
if (boardStringPoint === "#") {
|
||||
newBoardState.board[x][y] = null;
|
||||
}
|
||||
if (boardStringPoint === "X" && newBoardPoint?.player) {
|
||||
newBoardPoint.player = playerColors.black;
|
||||
}
|
||||
if (boardStringPoint === "O" && newBoardPoint?.player) {
|
||||
newBoardPoint.player = playerColors.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updateCaptures(newBoardState, lastPlayer);
|
||||
/** Creates a board object from a simple board. The resulting board has no analytics (liberties/chains) */
|
||||
export function boardFromSimpleBoard(simpleBoard: SimpleBoard): Board {
|
||||
return simpleBoard.map((column, x) =>
|
||||
column.split("").map((char, y) => {
|
||||
if (char === "#") return null;
|
||||
if (char === "X") return blankPointState(GoColor.black, x, y);
|
||||
if (char === "O") return blankPointState(GoColor.white, x, y);
|
||||
return blankPointState(GoColor.empty, x, y);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function boardStateFromSimpleBoard(
|
||||
simpleBoard: SimpleBoard,
|
||||
ai = GoOpponent.Daedalus,
|
||||
lastPlayer = GoColor.black,
|
||||
): BoardState {
|
||||
const newBoardState = getNewBoardState(simpleBoard[0].length, ai, false, boardFromSimpleBoard(simpleBoard));
|
||||
newBoardState.previousPlayer = lastPlayer;
|
||||
updateCaptures(newBoardState.board, lastPlayer);
|
||||
return newBoardState;
|
||||
}
|
||||
|
||||
export function blankPointState(color: GoColor, x: number, y: number): PointState {
|
||||
return {
|
||||
color: color,
|
||||
y,
|
||||
x,
|
||||
chain: "",
|
||||
liberties: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function areSimpleBoardsIdentical(simpleBoard1: SimpleBoard, simpleBoard2: SimpleBoard) {
|
||||
return simpleBoard1.every((column, x) => column === simpleBoard2[x]);
|
||||
}
|
||||
|
||||
export function getColorOnSimpleBoard(simpleBoard: SimpleBoard, x: number, y: number): GoColor | null {
|
||||
const char = simpleBoard[x]?.[y];
|
||||
if (char === "X") return GoColor.black;
|
||||
if (char === "O") return GoColor.white;
|
||||
if (char === ".") return GoColor.empty;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BoardState, playerColors, type PointState } from "../boardState/goConstants";
|
||||
import type { Board, BoardState, PointState } from "../Types";
|
||||
|
||||
import { GoColor } from "@enums";
|
||||
import {
|
||||
getAllChains,
|
||||
getAllEyes,
|
||||
@@ -19,18 +21,18 @@ import { contains, isNotNull } from "../boardState/boardState";
|
||||
* In which case, only the liberties of that one weak chain are worth considering. Other parts of that fully-encircled
|
||||
* enemy space, and other similar spaces, should be ignored, otherwise the game drags on too long
|
||||
*/
|
||||
export function findDisputedTerritory(boardState: BoardState, player: playerColors, excludeFriendlyEyes?: boolean) {
|
||||
export function findDisputedTerritory(boardState: BoardState, player: GoColor, excludeFriendlyEyes?: boolean) {
|
||||
let validMoves = getAllValidMoves(boardState, player);
|
||||
if (excludeFriendlyEyes) {
|
||||
const friendlyEyes = getAllEyes(boardState, player)
|
||||
const friendlyEyes = getAllEyes(boardState.board, player)
|
||||
.filter((eye) => eye.length >= 2)
|
||||
.flat()
|
||||
.flat();
|
||||
validMoves = validMoves.filter((point) => !contains(friendlyEyes, point));
|
||||
}
|
||||
const opponent = player === playerColors.white ? playerColors.black : playerColors.white;
|
||||
const chains = getAllChains(boardState);
|
||||
const emptySpacesToAnalyze = getAllPotentialEyes(boardState, chains, opponent);
|
||||
const opponent = player === GoColor.white ? GoColor.black : GoColor.white;
|
||||
const chains = getAllChains(boardState.board);
|
||||
const emptySpacesToAnalyze = getAllPotentialEyes(boardState.board, chains, opponent);
|
||||
const nodesInsideEyeSpacesToAnalyze = emptySpacesToAnalyze.map((space) => space.chain).flat();
|
||||
|
||||
const playableNodesInsideOfEnemySpace = emptySpacesToAnalyze.reduce((playableNodes: PointState[], space) => {
|
||||
@@ -45,12 +47,12 @@ export function findDisputedTerritory(boardState: BoardState, player: playerColo
|
||||
}
|
||||
|
||||
// Get all opponent chains that make up the border of the opponent-controlled space
|
||||
const neighborChains = getAllNeighboringChains(boardState, neighborChain, chains);
|
||||
const neighborChains = getAllNeighboringChains(boardState.board, neighborChain, chains);
|
||||
|
||||
// Ignore border chains that do not touch the current player's pieces somewhere, as they are likely fully interior
|
||||
// to the empty space in question, or only share a border with the edge of the board and the space, or are not yet
|
||||
// surrounded on the exterior and ready to be attacked within
|
||||
if (!neighborChains.find((chain) => chain?.[0]?.player === player)) {
|
||||
if (!neighborChains.find((chain) => chain?.[0]?.color === player)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -87,12 +89,8 @@ export function findDisputedTerritory(boardState: BoardState, player: playerColo
|
||||
|
||||
Note that this does not detect mutual eyes formed by two chains making an eye together, or eyes via seki, or some other edge cases.
|
||||
*/
|
||||
export function findClaimedTerritory(boardState: BoardState) {
|
||||
const whiteClaimedTerritory = getAllEyes(boardState, playerColors.white).filter(
|
||||
(eyesForChainN) => eyesForChainN.length >= 2,
|
||||
);
|
||||
const blackClaimedTerritory = getAllEyes(boardState, playerColors.black).filter(
|
||||
(eyesForChainN) => eyesForChainN.length >= 2,
|
||||
);
|
||||
export function findClaimedTerritory(board: Board) {
|
||||
const whiteClaimedTerritory = getAllEyes(board, GoColor.white).filter((eyesForChainN) => eyesForChainN.length >= 2);
|
||||
const blackClaimedTerritory = getAllEyes(board, GoColor.black).filter((eyesForChainN) => eyesForChainN.length >= 2);
|
||||
return [...blackClaimedTerritory, ...whiteClaimedTerritory].flat().flat();
|
||||
}
|
||||
|
||||
+113
-165
@@ -1,15 +1,8 @@
|
||||
import {
|
||||
BoardState,
|
||||
EyeMove,
|
||||
Move,
|
||||
MoveOptions,
|
||||
opponentDetails,
|
||||
opponents,
|
||||
PlayerColor,
|
||||
playerColors,
|
||||
playTypes,
|
||||
PointState,
|
||||
} from "../boardState/goConstants";
|
||||
import type { Board, BoardState, EyeMove, Move, MoveOptions, PointState } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { AugmentationName, GoOpponent, GoColor, GoPlayType } from "@enums";
|
||||
import { opponentDetails } from "../Constants";
|
||||
import { findNeighbors, floor, isDefined, isNotNull, passTurn } from "../boardState/boardState";
|
||||
import {
|
||||
evaluateIfMoveIsValid,
|
||||
@@ -26,8 +19,6 @@ import {
|
||||
import { findDisputedTerritory } from "./controlledTerritory";
|
||||
import { findAnyMatchedPatterns } from "./patternMatching";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
import { Player } from "@player";
|
||||
import { AugmentationName } from "@enums";
|
||||
|
||||
/*
|
||||
Basic GO AIs, each with some personality and weaknesses
|
||||
@@ -47,7 +38,7 @@ import { AugmentationName } from "@enums";
|
||||
*
|
||||
* @returns a promise that will resolve with a move (or pass) from the designated AI opponent.
|
||||
*/
|
||||
export async function getMove(boardState: BoardState, player: PlayerColor, opponent: opponents, rngOverride?: number) {
|
||||
export async function getMove(boardState: BoardState, player: GoColor, opponent: GoOpponent, rngOverride?: number) {
|
||||
await sleep(300);
|
||||
const rng = new WHRNG(rngOverride || Player.totalPlaytime);
|
||||
const smart = isSmart(opponent, rng.random());
|
||||
@@ -56,7 +47,7 @@ export async function getMove(boardState: BoardState, player: PlayerColor, oppon
|
||||
const priorityMove = await getFactionMove(moves, opponent, rng.random());
|
||||
if (priorityMove) {
|
||||
return {
|
||||
type: playTypes.move,
|
||||
type: GoPlayType.move,
|
||||
x: priorityMove.x,
|
||||
y: priorityMove.y,
|
||||
};
|
||||
@@ -80,14 +71,14 @@ export async function getMove(boardState: BoardState, player: PlayerColor, oppon
|
||||
|
||||
if (chosenMove) {
|
||||
await sleep(200);
|
||||
console.debug(`Non-priority move chosen: ${chosenMove.x} ${chosenMove.y}`);
|
||||
//console.debug(`Non-priority move chosen: ${chosenMove.x} ${chosenMove.y}`);
|
||||
return {
|
||||
type: playTypes.move,
|
||||
type: GoPlayType.move,
|
||||
x: chosenMove.x,
|
||||
y: chosenMove.y,
|
||||
};
|
||||
} else {
|
||||
console.debug("No valid moves found");
|
||||
//console.debug("No valid moves found");
|
||||
return handleNoMoveFound(boardState, player);
|
||||
}
|
||||
}
|
||||
@@ -98,19 +89,19 @@ export async function getMove(boardState: BoardState, player: PlayerColor, oppon
|
||||
* Ends the game if the player passed on the previous turn before the AI passes,
|
||||
* or if the player will be forced to pass their next turn after the AI passes.
|
||||
*/
|
||||
function handleNoMoveFound(boardState: BoardState, player: playerColors) {
|
||||
function handleNoMoveFound(boardState: BoardState, player: GoColor) {
|
||||
passTurn(boardState, player);
|
||||
const opposingPlayer = player === playerColors.white ? playerColors.black : playerColors.white;
|
||||
const opposingPlayer = player === GoColor.white ? GoColor.black : GoColor.white;
|
||||
const remainingTerritory = getAllValidMoves(boardState, opposingPlayer).length;
|
||||
if (remainingTerritory > 0 && boardState.passCount < 2) {
|
||||
return {
|
||||
type: playTypes.pass,
|
||||
type: GoPlayType.pass,
|
||||
x: -1,
|
||||
y: -1,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: playTypes.gameOver,
|
||||
type: GoPlayType.gameOver,
|
||||
x: -1,
|
||||
y: -1,
|
||||
};
|
||||
@@ -120,20 +111,20 @@ function handleNoMoveFound(boardState: BoardState, player: playerColors) {
|
||||
/**
|
||||
* Given a group of move options, chooses one based on the given opponent's personality (if any fit their priorities)
|
||||
*/
|
||||
async function getFactionMove(moves: MoveOptions, faction: opponents, rng: number): Promise<PointState | null> {
|
||||
if (faction === opponents.Netburners) {
|
||||
async function getFactionMove(moves: MoveOptions, faction: GoOpponent, rng: number): Promise<PointState | null> {
|
||||
if (faction === GoOpponent.Netburners) {
|
||||
return getNetburnersPriorityMove(moves, rng);
|
||||
}
|
||||
if (faction === opponents.SlumSnakes) {
|
||||
if (faction === GoOpponent.SlumSnakes) {
|
||||
return getSlumSnakesPriorityMove(moves, rng);
|
||||
}
|
||||
if (faction === opponents.TheBlackHand) {
|
||||
if (faction === GoOpponent.TheBlackHand) {
|
||||
return getBlackHandPriorityMove(moves, rng);
|
||||
}
|
||||
if (faction === opponents.Tetrads) {
|
||||
if (faction === GoOpponent.Tetrads) {
|
||||
return getTetradPriorityMove(moves, rng);
|
||||
}
|
||||
if (faction === opponents.Daedalus) {
|
||||
if (faction === GoOpponent.Daedalus) {
|
||||
return getDaedalusPriorityMove(moves, rng);
|
||||
}
|
||||
|
||||
@@ -143,14 +134,14 @@ async function getFactionMove(moves: MoveOptions, faction: opponents, rng: numbe
|
||||
/**
|
||||
* Determines if certain failsafes and mistake avoidance are enabled for the given move
|
||||
*/
|
||||
function isSmart(faction: opponents, rng: number) {
|
||||
if (faction === opponents.Netburners) {
|
||||
function isSmart(faction: GoOpponent, rng: number) {
|
||||
if (faction === GoOpponent.Netburners) {
|
||||
return false;
|
||||
}
|
||||
if (faction === opponents.SlumSnakes) {
|
||||
if (faction === GoOpponent.SlumSnakes) {
|
||||
return rng < 0.3;
|
||||
}
|
||||
if (faction === opponents.TheBlackHand) {
|
||||
if (faction === GoOpponent.TheBlackHand) {
|
||||
return rng < 0.8;
|
||||
}
|
||||
|
||||
@@ -198,24 +189,24 @@ async function getSlumSnakesPriorityMove(moves: MoveOptions, rng: number): Promi
|
||||
*/
|
||||
async function getBlackHandPriorityMove(moves: MoveOptions, rng: number): Promise<PointState | null> {
|
||||
if (await moves.capture()) {
|
||||
console.debug("capture: capture move chosen");
|
||||
//console.debug("capture: capture move chosen");
|
||||
return (await moves.capture())?.point ?? null;
|
||||
}
|
||||
|
||||
const surround = await moves.surround();
|
||||
|
||||
if (surround && surround.point && (surround.newLibertyCount ?? 999) <= 1) {
|
||||
console.debug("surround move chosen");
|
||||
//console.debug("surround move chosen");
|
||||
return surround.point;
|
||||
}
|
||||
|
||||
if (await moves.defendCapture()) {
|
||||
console.debug("defend capture: defend move chosen");
|
||||
//console.debug("defend capture: defend move chosen");
|
||||
return (await moves.defendCapture())?.point ?? null;
|
||||
}
|
||||
|
||||
if (surround && surround.point && (surround?.newLibertyCount ?? 999) <= 2) {
|
||||
console.debug("surround move chosen");
|
||||
//console.debug("surround move chosen");
|
||||
return surround.point;
|
||||
}
|
||||
|
||||
@@ -235,23 +226,23 @@ async function getBlackHandPriorityMove(moves: MoveOptions, rng: number): Promis
|
||||
*/
|
||||
async function getTetradPriorityMove(moves: MoveOptions, rng: number): Promise<PointState | null> {
|
||||
if (await moves.capture()) {
|
||||
console.debug("capture: capture move chosen");
|
||||
//console.debug("capture: capture move chosen");
|
||||
return (await moves.capture())?.point ?? null;
|
||||
}
|
||||
|
||||
if (await moves.defendCapture()) {
|
||||
console.debug("defend capture: defend move chosen");
|
||||
//console.debug("defend capture: defend move chosen");
|
||||
return (await moves.defendCapture())?.point ?? null;
|
||||
}
|
||||
|
||||
if (await moves.pattern()) {
|
||||
console.debug("pattern match move chosen");
|
||||
//console.debug("pattern match move chosen");
|
||||
return (await moves.pattern())?.point ?? null;
|
||||
}
|
||||
|
||||
const surround = await moves.surround();
|
||||
if (surround && surround.point && (surround?.newLibertyCount ?? 9) <= 1) {
|
||||
console.debug("surround move chosen");
|
||||
//console.debug("surround move chosen");
|
||||
return surround.point;
|
||||
}
|
||||
|
||||
@@ -283,33 +274,33 @@ async function getDaedalusPriorityMove(moves: MoveOptions, rng: number): Promise
|
||||
*/
|
||||
async function getIlluminatiPriorityMove(moves: MoveOptions, rng: number): Promise<PointState | null> {
|
||||
if (await moves.capture()) {
|
||||
console.debug("capture: capture move chosen");
|
||||
//console.debug("capture: capture move chosen");
|
||||
return (await moves.capture())?.point ?? null;
|
||||
}
|
||||
|
||||
if (await moves.defendCapture()) {
|
||||
console.debug("defend capture: defend move chosen");
|
||||
//console.debug("defend capture: defend move chosen");
|
||||
return (await moves.defendCapture())?.point ?? null;
|
||||
}
|
||||
|
||||
if (await moves.eyeMove()) {
|
||||
console.debug("Create eye move chosen");
|
||||
//console.debug("Create eye move chosen");
|
||||
return (await moves.eyeMove())?.point ?? null;
|
||||
}
|
||||
|
||||
const surround = await moves.surround();
|
||||
if (surround && surround.point && (surround?.newLibertyCount ?? 9) <= 1) {
|
||||
console.debug("surround move chosen");
|
||||
//console.debug("surround move chosen");
|
||||
return surround.point;
|
||||
}
|
||||
|
||||
if (await moves.eyeBlock()) {
|
||||
console.debug("Block eye move chosen");
|
||||
//console.debug("Block eye move chosen");
|
||||
return (await moves.eyeBlock())?.point ?? null;
|
||||
}
|
||||
|
||||
if (await moves.corner()) {
|
||||
console.debug("Corner move chosen");
|
||||
//console.debug("Corner move chosen");
|
||||
return (await moves.corner())?.point ?? null;
|
||||
}
|
||||
|
||||
@@ -319,17 +310,17 @@ async function getIlluminatiPriorityMove(moves: MoveOptions, rng: number): Promi
|
||||
const usePattern = rng > 0.25 || !hasMoves;
|
||||
|
||||
if ((await moves.pattern()) && usePattern) {
|
||||
console.debug("pattern match move chosen");
|
||||
//console.debug("pattern match move chosen");
|
||||
return (await moves.pattern())?.point ?? null;
|
||||
}
|
||||
|
||||
if (rng > 0.4 && (await moves.jump())) {
|
||||
console.debug("Jump move chosen");
|
||||
//console.debug("Jump move chosen");
|
||||
return (await moves.jump())?.point ?? null;
|
||||
}
|
||||
|
||||
if (rng < 0.6 && surround && surround.point && (surround?.newLibertyCount ?? 9) <= 2) {
|
||||
console.debug("surround move chosen");
|
||||
//console.debug("surround move chosen");
|
||||
return surround.point;
|
||||
}
|
||||
|
||||
@@ -339,20 +330,20 @@ async function getIlluminatiPriorityMove(moves: MoveOptions, rng: number): Promi
|
||||
/**
|
||||
* Get a move that places a piece to influence (and later control) a corner
|
||||
*/
|
||||
function getCornerMove(boardState: BoardState) {
|
||||
const boardEdge = boardState.board[0].length - 1;
|
||||
function getCornerMove(board: Board) {
|
||||
const boardEdge = board[0].length - 1;
|
||||
const cornerMax = boardEdge - 2;
|
||||
if (isCornerAvailableForMove(boardState, cornerMax, cornerMax, boardEdge, boardEdge)) {
|
||||
return boardState.board[cornerMax][cornerMax];
|
||||
if (isCornerAvailableForMove(board, cornerMax, cornerMax, boardEdge, boardEdge)) {
|
||||
return board[cornerMax][cornerMax];
|
||||
}
|
||||
if (isCornerAvailableForMove(boardState, 0, cornerMax, cornerMax, boardEdge)) {
|
||||
return boardState.board[2][cornerMax];
|
||||
if (isCornerAvailableForMove(board, 0, cornerMax, cornerMax, boardEdge)) {
|
||||
return board[2][cornerMax];
|
||||
}
|
||||
if (isCornerAvailableForMove(boardState, 0, 0, 2, 2)) {
|
||||
return boardState.board[2][2];
|
||||
if (isCornerAvailableForMove(board, 0, 0, 2, 2)) {
|
||||
return board[2][2];
|
||||
}
|
||||
if (isCornerAvailableForMove(boardState, cornerMax, 0, boardEdge, 2)) {
|
||||
return boardState.board[cornerMax][2];
|
||||
if (isCornerAvailableForMove(board, cornerMax, 0, boardEdge, 2)) {
|
||||
return board[cornerMax][2];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -360,9 +351,9 @@ function getCornerMove(boardState: BoardState) {
|
||||
/**
|
||||
* Find all non-offline nodes in a given area
|
||||
*/
|
||||
function findLiveNodesInArea(boardState: BoardState, x1: number, y1: number, x2: number, y2: number) {
|
||||
function findLiveNodesInArea(board: Board, x1: number, y1: number, x2: number, y2: number) {
|
||||
const foundPoints: PointState[] = [];
|
||||
boardState.board.forEach((column) =>
|
||||
board.forEach((column) =>
|
||||
column.forEach(
|
||||
(point) => point && point.x >= x1 && point.x <= x2 && point.y >= y1 && point.y <= y2 && foundPoints.push(point),
|
||||
),
|
||||
@@ -373,23 +364,17 @@ function findLiveNodesInArea(boardState: BoardState, x1: number, y1: number, x2:
|
||||
/**
|
||||
* Determine if a corner is largely intact and currently empty, and thus a good target for corner takeover moves
|
||||
*/
|
||||
function isCornerAvailableForMove(boardState: BoardState, x1: number, y1: number, x2: number, y2: number) {
|
||||
const foundPoints = findLiveNodesInArea(boardState, x1, y1, x2, y2);
|
||||
const foundPieces = foundPoints.filter((point) => point.player !== playerColors.empty);
|
||||
function isCornerAvailableForMove(board: Board, x1: number, y1: number, x2: number, y2: number) {
|
||||
const foundPoints = findLiveNodesInArea(board, x1, y1, x2, y2);
|
||||
const foundPieces = foundPoints.filter((point) => point.color !== GoColor.empty);
|
||||
return foundPoints.length >= 7 ? foundPieces.length === 0 : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a move from the list of open-area moves
|
||||
*/
|
||||
function getExpansionMove(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
rng: number,
|
||||
moveArray?: Move[],
|
||||
) {
|
||||
const moveOptions = moveArray ?? getExpansionMoveArray(boardState, player, availableSpaces);
|
||||
function getExpansionMove(board: Board, availableSpaces: PointState[], rng: number, moveArray?: Move[]) {
|
||||
const moveOptions = moveArray ?? getExpansionMoveArray(board, availableSpaces);
|
||||
const randomIndex = floor(rng * moveOptions.length);
|
||||
return moveOptions[randomIndex];
|
||||
}
|
||||
@@ -397,21 +382,14 @@ function getExpansionMove(
|
||||
/**
|
||||
* Get a move in open space that is nearby a friendly piece
|
||||
*/
|
||||
function getJumpMove(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
rng: number,
|
||||
moveArray?: Move[],
|
||||
) {
|
||||
const board = boardState.board;
|
||||
const moveOptions = (moveArray ?? getExpansionMoveArray(boardState, player, availableSpaces)).filter(({ point }) =>
|
||||
function getJumpMove(board: Board, player: GoColor, availableSpaces: PointState[], rng: number, moveArray?: Move[]) {
|
||||
const moveOptions = (moveArray ?? getExpansionMoveArray(board, availableSpaces)).filter(({ point }) =>
|
||||
[
|
||||
board[point.x]?.[point.y + 2],
|
||||
board[point.x + 2]?.[point.y],
|
||||
board[point.x]?.[point.y - 2],
|
||||
board[point.x - 2]?.[point.y],
|
||||
].some((point) => point?.player === player),
|
||||
].some((point) => point?.color === player),
|
||||
);
|
||||
|
||||
const randomIndex = floor(rng * moveOptions.length);
|
||||
@@ -421,24 +399,20 @@ function getJumpMove(
|
||||
/**
|
||||
* Finds a move in an open area to expand influence and later build on
|
||||
*/
|
||||
export function getExpansionMoveArray(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
): Move[] {
|
||||
export function getExpansionMoveArray(board: Board, availableSpaces: PointState[]): Move[] {
|
||||
// Look for any empty spaces fully surrounded by empty spaces to expand into
|
||||
const emptySpaces = availableSpaces.filter((space) => {
|
||||
const neighbors = findNeighbors(boardState, space.x, space.y);
|
||||
const neighbors = findNeighbors(board, space.x, space.y);
|
||||
return (
|
||||
[neighbors.north, neighbors.east, neighbors.south, neighbors.west].filter(
|
||||
(point) => point && point.player === playerColors.empty,
|
||||
(point) => point && point.color === GoColor.empty,
|
||||
).length === 4
|
||||
);
|
||||
});
|
||||
|
||||
// Once no such empty areas exist anymore, instead expand into any disputed territory
|
||||
// to gain a few more points in endgame
|
||||
const disputedSpaces = emptySpaces.length ? [] : getDisputedTerritoryMoves(boardState, player, availableSpaces, 1);
|
||||
const disputedSpaces = emptySpaces.length ? [] : getDisputedTerritoryMoves(board, availableSpaces, 1);
|
||||
|
||||
const moveOptions = [...emptySpaces, ...disputedSpaces];
|
||||
|
||||
@@ -451,23 +425,14 @@ export function getExpansionMoveArray(
|
||||
});
|
||||
}
|
||||
|
||||
function getDisputedTerritoryMoves(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
maxChainSize = 99,
|
||||
) {
|
||||
const chains = getAllChains(boardState).filter((chain) => chain.length <= maxChainSize);
|
||||
function getDisputedTerritoryMoves(board: Board, availableSpaces: PointState[], maxChainSize = 99) {
|
||||
const chains = getAllChains(board).filter((chain) => chain.length <= maxChainSize);
|
||||
|
||||
return availableSpaces.filter((space) => {
|
||||
const chain = chains.find((chain) => chain[0].chain === space.chain) ?? [];
|
||||
const playerNeighbors = getAllNeighboringChains(boardState, chain, chains);
|
||||
const hasWhitePieceNeighbor = playerNeighbors.find(
|
||||
(neighborChain) => neighborChain[0]?.player === playerColors.white,
|
||||
);
|
||||
const hasBlackPieceNeighbor = playerNeighbors.find(
|
||||
(neighborChain) => neighborChain[0]?.player === playerColors.black,
|
||||
);
|
||||
const playerNeighbors = getAllNeighboringChains(board, chain, chains);
|
||||
const hasWhitePieceNeighbor = playerNeighbors.find((neighborChain) => neighborChain[0]?.color === GoColor.white);
|
||||
const hasBlackPieceNeighbor = playerNeighbors.find((neighborChain) => neighborChain[0]?.color === GoColor.black);
|
||||
|
||||
return hasWhitePieceNeighbor && hasBlackPieceNeighbor;
|
||||
});
|
||||
@@ -476,8 +441,8 @@ function getDisputedTerritoryMoves(
|
||||
/**
|
||||
* Finds all moves that increases the liberties of the player's pieces, making them harder to capture and occupy more space on the board.
|
||||
*/
|
||||
async function getLibertyGrowthMoves(boardState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
|
||||
const friendlyChains = getAllChains(boardState).filter((chain) => chain[0].player === player);
|
||||
async function getLibertyGrowthMoves(board: Board, player: GoColor, availableSpaces: PointState[]) {
|
||||
const friendlyChains = getAllChains(board).filter((chain) => chain[0].color === player);
|
||||
|
||||
if (!friendlyChains.length) {
|
||||
return [];
|
||||
@@ -503,10 +468,10 @@ async function getLibertyGrowthMoves(boardState: BoardState, player: PlayerColor
|
||||
.map((liberty) => {
|
||||
const move = liberty.libertyPoint;
|
||||
|
||||
const newLibertyCount = findEffectiveLibertiesOfNewMove(boardState, move.x, move.y, player).length;
|
||||
const newLibertyCount = findEffectiveLibertiesOfNewMove(board, move.x, move.y, player).length;
|
||||
|
||||
// Get the smallest liberty count of connected chains to represent the old state
|
||||
const oldLibertyCount = findMinLibertyCountOfAdjacentChains(boardState, move.x, move.y, player);
|
||||
const oldLibertyCount = findMinLibertyCountOfAdjacentChains(board, move.x, move.y, player);
|
||||
|
||||
return {
|
||||
point: move,
|
||||
@@ -520,13 +485,8 @@ async function getLibertyGrowthMoves(boardState: BoardState, player: PlayerColor
|
||||
/**
|
||||
* Find a move that increases the player's liberties by the maximum amount
|
||||
*/
|
||||
async function getGrowthMove(
|
||||
initialState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
rng: number,
|
||||
) {
|
||||
const growthMoves = await getLibertyGrowthMoves(initialState, player, availableSpaces);
|
||||
async function getGrowthMove(board: Board, player: GoColor, availableSpaces: PointState[], rng: number) {
|
||||
const growthMoves = await getLibertyGrowthMoves(board, player, availableSpaces);
|
||||
|
||||
const maxLibertyCount = Math.max(...growthMoves.map((l) => l.newLibertyCount - l.oldLibertyCount));
|
||||
|
||||
@@ -537,8 +497,8 @@ async function getGrowthMove(
|
||||
/**
|
||||
* Find a move that specifically increases a chain's liberties from 1 to more than 1, preventing capture
|
||||
*/
|
||||
async function getDefendMove(initialState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
|
||||
const growthMoves = await getLibertyGrowthMoves(initialState, player, availableSpaces);
|
||||
async function getDefendMove(board: Board, player: GoColor, availableSpaces: PointState[]) {
|
||||
const growthMoves = await getLibertyGrowthMoves(board, player, availableSpaces);
|
||||
const libertyIncreases =
|
||||
growthMoves?.filter((move) => move.oldLibertyCount <= 1 && move.newLibertyCount > move.oldLibertyCount) ?? [];
|
||||
|
||||
@@ -556,14 +516,9 @@ async function getDefendMove(initialState: BoardState, player: PlayerColor, avai
|
||||
* Find a move that reduces the opponent's liberties as much as possible,
|
||||
* capturing (or making it easier to capture) their pieces
|
||||
*/
|
||||
async function getSurroundMove(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
smart = true,
|
||||
) {
|
||||
const opposingPlayer = player === playerColors.black ? playerColors.white : playerColors.black;
|
||||
const enemyChains = getAllChains(boardState).filter((chain) => chain[0].player === opposingPlayer);
|
||||
async function getSurroundMove(board: Board, player: GoColor, availableSpaces: PointState[], smart = true) {
|
||||
const opposingPlayer = player === GoColor.black ? GoColor.white : GoColor.black;
|
||||
const enemyChains = getAllChains(board).filter((chain) => chain[0].color === opposingPlayer);
|
||||
|
||||
if (!enemyChains.length || !availableSpaces.length) {
|
||||
return null;
|
||||
@@ -580,13 +535,13 @@ async function getSurroundMove(
|
||||
const surroundMoves: Move[] = [];
|
||||
|
||||
enemyLiberties.forEach((move) => {
|
||||
const newLibertyCount = findEffectiveLibertiesOfNewMove(boardState, move.x, move.y, player).length;
|
||||
const newLibertyCount = findEffectiveLibertiesOfNewMove(board, move.x, move.y, player).length;
|
||||
|
||||
const weakestEnemyChain = findEnemyNeighborChainWithFewestLiberties(
|
||||
boardState,
|
||||
board,
|
||||
move.x,
|
||||
move.y,
|
||||
player === playerColors.black ? playerColors.white : playerColors.black,
|
||||
player === GoColor.black ? GoColor.white : GoColor.black,
|
||||
);
|
||||
const weakestEnemyChainLength = weakestEnemyChain?.length ?? 99;
|
||||
|
||||
@@ -646,22 +601,17 @@ async function getSurroundMove(
|
||||
* If a chain has multiple eyes, it cannot be captured by the opponent (since they can only fill one eye at a time,
|
||||
* and suiciding your own pieces is not legal unless it captures the opponents' first)
|
||||
*/
|
||||
function getEyeCreationMoves(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
maxLiberties = 99,
|
||||
) {
|
||||
const allEyes = getAllEyesByChainId(boardState, player);
|
||||
const currentEyes = getAllEyes(boardState, player, allEyes);
|
||||
function getEyeCreationMoves(board: Board, player: GoColor, availableSpaces: PointState[], maxLiberties = 99) {
|
||||
const allEyes = getAllEyesByChainId(board, player);
|
||||
const currentEyes = getAllEyes(board, player, allEyes);
|
||||
|
||||
const currentLivingGroupIDs = Object.keys(allEyes).filter((chainId) => allEyes[chainId].length >= 2);
|
||||
const currentLivingGroupsCount = currentLivingGroupIDs.length;
|
||||
const currentEyeCount = currentEyes.filter((eye) => eye.length).length;
|
||||
|
||||
const chains = getAllChains(boardState);
|
||||
const chains = getAllChains(board);
|
||||
const friendlyLiberties = chains
|
||||
.filter((chain) => chain[0].player === player)
|
||||
.filter((chain) => chain[0].color === player)
|
||||
.filter((chain) => chain.length > 1)
|
||||
.filter((chain) => chain[0].liberties && chain[0].liberties?.length <= maxLiberties)
|
||||
.filter((chain) => !currentLivingGroupIDs.includes(chain[0].chain))
|
||||
@@ -672,16 +622,16 @@ function getEyeCreationMoves(
|
||||
availableSpaces.find((availablePoint) => availablePoint.x === point.x && availablePoint.y === point.y),
|
||||
)
|
||||
.filter((point: PointState) => {
|
||||
const neighbors = findNeighbors(boardState, point.x, point.y);
|
||||
const neighbors = findNeighbors(board, point.x, point.y);
|
||||
const neighborhood = [neighbors.north, neighbors.east, neighbors.south, neighbors.west];
|
||||
return (
|
||||
neighborhood.filter((point) => !point || point?.player === player).length >= 2 &&
|
||||
neighborhood.some((point) => point?.player === playerColors.empty)
|
||||
neighborhood.filter((point) => !point || point?.color === player).length >= 2 &&
|
||||
neighborhood.some((point) => point?.color === GoColor.empty)
|
||||
);
|
||||
});
|
||||
|
||||
const eyeCreationMoves = friendlyLiberties.reduce((moveOptions: EyeMove[], point: PointState) => {
|
||||
const evaluationBoard = evaluateMoveResult(boardState, point.x, point.y, player);
|
||||
const evaluationBoard = evaluateMoveResult(board, point.x, point.y, player);
|
||||
const newEyes = getAllEyes(evaluationBoard, player);
|
||||
const newLivingGroupsCount = newEyes.filter((eye) => eye.length >= 2).length;
|
||||
const newEyeCount = newEyes.filter((eye) => eye.length).length;
|
||||
@@ -700,16 +650,16 @@ function getEyeCreationMoves(
|
||||
return eyeCreationMoves.sort((moveA, moveB) => +moveB.createsLife - +moveA.createsLife);
|
||||
}
|
||||
|
||||
function getEyeCreationMove(boardState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
|
||||
return getEyeCreationMoves(boardState, player, availableSpaces)[0];
|
||||
function getEyeCreationMove(board: Board, player: GoColor, availableSpaces: PointState[]) {
|
||||
return getEyeCreationMoves(board, player, availableSpaces)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is only one move that would create two eyes for the opponent, it should be blocked if possible
|
||||
*/
|
||||
function getEyeBlockingMove(boardState: BoardState, player: PlayerColor, availablePoints: PointState[]) {
|
||||
const opposingPlayer = player === playerColors.white ? playerColors.black : playerColors.white;
|
||||
const opponentEyeMoves = getEyeCreationMoves(boardState, opposingPlayer, availablePoints, 5);
|
||||
function getEyeBlockingMove(board: Board, player: GoColor, availablePoints: PointState[]) {
|
||||
const opposingPlayer = player === GoColor.white ? GoColor.black : GoColor.white;
|
||||
const opponentEyeMoves = getEyeCreationMoves(board, opposingPlayer, availablePoints, 5);
|
||||
const twoEyeMoves = opponentEyeMoves.filter((move) => move.createsLife);
|
||||
const oneEyeMoves = opponentEyeMoves.filter((move) => !move.createsLife);
|
||||
|
||||
@@ -727,13 +677,14 @@ function getEyeBlockingMove(boardState: BoardState, player: PlayerColor, availab
|
||||
*/
|
||||
function getMoveOptions(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
player: GoColor,
|
||||
rng: number,
|
||||
smart = true,
|
||||
): { [s in keyof MoveOptions]: () => Promise<Move | null> } {
|
||||
const board = boardState.board;
|
||||
const availableSpaces = findDisputedTerritory(boardState, player, smart);
|
||||
const contestedPoints = getDisputedTerritoryMoves(boardState, player, availableSpaces);
|
||||
const expansionMoves = getExpansionMoveArray(boardState, player, availableSpaces);
|
||||
const contestedPoints = getDisputedTerritoryMoves(board, availableSpaces);
|
||||
const expansionMoves = getExpansionMoveArray(board, availableSpaces);
|
||||
|
||||
// If the player is passing, and all territory is surrounded by a single color: do not suggest moves that
|
||||
// needlessly extend the game, unless they actually can change the score
|
||||
@@ -768,22 +719,19 @@ function getMoveOptions(
|
||||
? defendMove
|
||||
: null;
|
||||
},
|
||||
eyeMove: async () => (endGameAvailable ? null : getEyeCreationMove(boardState, player, availableSpaces) ?? null),
|
||||
eyeBlock: async () => (endGameAvailable ? null : getEyeBlockingMove(boardState, player, availableSpaces) ?? null),
|
||||
eyeMove: async () => (endGameAvailable ? null : getEyeCreationMove(board, player, availableSpaces) ?? null),
|
||||
eyeBlock: async () => (endGameAvailable ? null : getEyeBlockingMove(board, player, availableSpaces) ?? null),
|
||||
pattern: async () => {
|
||||
const point = endGameAvailable
|
||||
? null
|
||||
: await findAnyMatchedPatterns(boardState, player, availableSpaces, smart, rng);
|
||||
const point = endGameAvailable ? null : await findAnyMatchedPatterns(board, player, availableSpaces, smart, rng);
|
||||
return point ? { point } : null;
|
||||
},
|
||||
growth: async () =>
|
||||
endGameAvailable ? null : (await getGrowthMove(boardState, player, availableSpaces, rng)) ?? null,
|
||||
expansion: async () => (await getExpansionMove(boardState, player, availableSpaces, rng, expansionMoves)) ?? null,
|
||||
jump: async () => (await getJumpMove(boardState, player, availableSpaces, rng, expansionMoves)) ?? null,
|
||||
defend: async () => (await getDefendMove(boardState, player, availableSpaces)) ?? null,
|
||||
surround: async () => (await getSurroundMove(boardState, player, availableSpaces, smart)) ?? null,
|
||||
growth: async () => (endGameAvailable ? null : (await getGrowthMove(board, player, availableSpaces, rng)) ?? null),
|
||||
expansion: async () => (await getExpansionMove(board, availableSpaces, rng, expansionMoves)) ?? null,
|
||||
jump: async () => (await getJumpMove(board, player, availableSpaces, rng, expansionMoves)) ?? null,
|
||||
defend: async () => (await getDefendMove(board, player, availableSpaces)) ?? null,
|
||||
surround: async () => (await getSurroundMove(board, player, availableSpaces, smart)) ?? null,
|
||||
corner: async () => {
|
||||
const point = getCornerMove(boardState);
|
||||
const point = getCornerMove(board);
|
||||
return point ? { point } : null;
|
||||
},
|
||||
random: async () => {
|
||||
@@ -811,7 +759,7 @@ function getMoveOptions(
|
||||
/**
|
||||
* Gets the starting score for white.
|
||||
*/
|
||||
export function getKomi(opponent: opponents) {
|
||||
export function getKomi(opponent: GoOpponent) {
|
||||
return opponentDetails[opponent].komi;
|
||||
}
|
||||
|
||||
@@ -823,5 +771,5 @@ export function sleep(ms: number): Promise<void> {
|
||||
}
|
||||
|
||||
export function showWorldDemon() {
|
||||
return Player.augmentations.some((a) => a.name === AugmentationName.TheRedPill) && Player.sourceFileLvl(1);
|
||||
return Player.hasAugmentation(AugmentationName.TheRedPill, true) && Player.sourceFileLvl(1);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Inspired by https://github.com/pasky/michi/blob/master/michi.py
|
||||
import { BoardState, PlayerColor, playerColors, PointState } from "../boardState/goConstants";
|
||||
import type { Board, PointState } from "../Types";
|
||||
|
||||
import { GoColor } from "@enums";
|
||||
import { sleep } from "./goAI";
|
||||
import { findEffectiveLibertiesOfNewMove } from "./boardAnalysis";
|
||||
import { floor } from "../boardState/boardState";
|
||||
@@ -78,25 +80,24 @@ export const threeByThreePatterns = [
|
||||
* Searches the board for any point that matches the expanded pattern set
|
||||
*/
|
||||
export async function findAnyMatchedPatterns(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
board: Board,
|
||||
player: GoColor,
|
||||
availableSpaces: PointState[],
|
||||
smart = true,
|
||||
rng: number,
|
||||
) {
|
||||
const board = boardState.board;
|
||||
const boardSize = board[0].length;
|
||||
const patterns = expandAllThreeByThreePatterns();
|
||||
const moves = [];
|
||||
for (let x = 0; x < boardSize; x++) {
|
||||
for (let y = 0; y < boardSize; y++) {
|
||||
const neighborhood = getNeighborhood(boardState, x, y);
|
||||
const neighborhood = getNeighborhood(board, x, y);
|
||||
const matchedPattern = patterns.find((pattern) => checkMatch(neighborhood, pattern, player));
|
||||
|
||||
if (
|
||||
matchedPattern &&
|
||||
availableSpaces.find((availablePoint) => availablePoint.x === x && availablePoint.y === y) &&
|
||||
(!smart || findEffectiveLibertiesOfNewMove(boardState, x, y, player).length > 1)
|
||||
(!smart || findEffectiveLibertiesOfNewMove(board, x, y, player).length > 1)
|
||||
) {
|
||||
moves.push(board[x][y]);
|
||||
}
|
||||
@@ -109,7 +110,7 @@ export async function findAnyMatchedPatterns(
|
||||
/**
|
||||
Returns false if any point does not match the pattern, and true if it matches fully.
|
||||
*/
|
||||
function checkMatch(neighborhood: (PointState | null)[][], pattern: string[], player: PlayerColor) {
|
||||
function checkMatch(neighborhood: (PointState | null)[][], pattern: string[], player: GoColor) {
|
||||
const patternArr = pattern.join("").split("");
|
||||
const neighborhoodArray = neighborhood.flat();
|
||||
return patternArr.every((str, index) => matches(str, neighborhoodArray[index], player));
|
||||
@@ -118,8 +119,7 @@ function checkMatch(neighborhood: (PointState | null)[][], pattern: string[], pl
|
||||
/**
|
||||
* Gets the 8 points adjacent and diagonally adjacent to the given point
|
||||
*/
|
||||
function getNeighborhood(boardState: BoardState, x: number, y: number) {
|
||||
const board = boardState.board;
|
||||
function getNeighborhood(board: Board, x: number, y: number) {
|
||||
return [
|
||||
[board[x - 1]?.[y - 1], board[x - 1]?.[y], board[x - 1]?.[y + 1]],
|
||||
[board[x]?.[y - 1], board[x]?.[y], board[x]?.[y + 1]],
|
||||
@@ -136,23 +136,23 @@ function getNeighborhood(boardState: BoardState, x: number, y: number) {
|
||||
* A space " " only matches the edge of the board
|
||||
* question mark "?" matches anything
|
||||
*/
|
||||
function matches(stringPoint: string, point: PointState | null, player: PlayerColor) {
|
||||
const opponent = player === playerColors.white ? playerColors.black : playerColors.white;
|
||||
function matches(stringPoint: string, point: PointState | null, player: GoColor) {
|
||||
const opponent = player === GoColor.white ? GoColor.black : GoColor.white;
|
||||
switch (stringPoint) {
|
||||
case "X": {
|
||||
return point?.player === player;
|
||||
return point?.color === player;
|
||||
}
|
||||
case "O": {
|
||||
return point?.player === opponent;
|
||||
return point?.color === opponent;
|
||||
}
|
||||
case "x": {
|
||||
return point?.player !== opponent;
|
||||
return point?.color !== opponent;
|
||||
}
|
||||
case "o": {
|
||||
return point?.player !== player;
|
||||
return point?.color !== player;
|
||||
}
|
||||
case ".": {
|
||||
return point?.player === playerColors.empty;
|
||||
return point?.color === GoColor.empty;
|
||||
}
|
||||
case " ": {
|
||||
return point === null;
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import {
|
||||
BoardState,
|
||||
getGoPlayerStartingState,
|
||||
opponents,
|
||||
PlayerColor,
|
||||
playerColors,
|
||||
PointState,
|
||||
} from "../boardState/goConstants";
|
||||
import type { Board, BoardState, PointState } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { GoOpponent, GoColor } from "@enums";
|
||||
import { newOpponentStats } from "../Constants";
|
||||
import { getAllChains, getPlayerNeighbors } from "./boardAnalysis";
|
||||
import { getKomi } from "./goAI";
|
||||
import { Player } from "@player";
|
||||
import { getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
|
||||
import { floor, isNotNull } from "../boardState/boardState";
|
||||
import { Factions } from "../../Faction/Factions";
|
||||
import { FactionName } from "@enums";
|
||||
import { getEnumHelper } from "../../utils/EnumHelper";
|
||||
import { Go } from "../Go";
|
||||
|
||||
/**
|
||||
* Returns the score of the current board.
|
||||
@@ -21,22 +18,22 @@ import { FactionName } from "@enums";
|
||||
*/
|
||||
export function getScore(boardState: BoardState) {
|
||||
const komi = getKomi(boardState.ai) ?? 6.5;
|
||||
const whitePieces = getColoredPieceCount(boardState, playerColors.white);
|
||||
const blackPieces = getColoredPieceCount(boardState, playerColors.black);
|
||||
const territoryScores = getTerritoryScores(boardState);
|
||||
const whitePieces = getColoredPieceCount(boardState, GoColor.white);
|
||||
const blackPieces = getColoredPieceCount(boardState, GoColor.black);
|
||||
const territoryScores = getTerritoryScores(boardState.board);
|
||||
|
||||
return {
|
||||
[playerColors.white]: {
|
||||
[GoColor.white]: {
|
||||
pieces: whitePieces,
|
||||
territory: territoryScores[playerColors.white],
|
||||
territory: territoryScores[GoColor.white],
|
||||
komi: komi,
|
||||
sum: whitePieces + territoryScores[playerColors.white] + komi,
|
||||
sum: whitePieces + territoryScores[GoColor.white] + komi,
|
||||
},
|
||||
[playerColors.black]: {
|
||||
[GoColor.black]: {
|
||||
pieces: blackPieces,
|
||||
territory: territoryScores[playerColors.black],
|
||||
territory: territoryScores[GoColor.black],
|
||||
komi: 0,
|
||||
sum: blackPieces + territoryScores[playerColors.black],
|
||||
sum: blackPieces + territoryScores[GoColor.black],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -50,13 +47,13 @@ export function endGoGame(boardState: BoardState) {
|
||||
return;
|
||||
}
|
||||
boardState.previousPlayer = null;
|
||||
const statusToUpdate = getPlayerStats(boardState.ai);
|
||||
const statusToUpdate = getOpponentStats(boardState.ai);
|
||||
statusToUpdate.favor = statusToUpdate.favor ?? 0;
|
||||
const score = getScore(boardState);
|
||||
|
||||
if (score[playerColors.black].sum < score[playerColors.white].sum) {
|
||||
if (score[GoColor.black].sum < score[GoColor.white].sum) {
|
||||
resetWinstreak(boardState.ai, true);
|
||||
statusToUpdate.nodePower += floor(score[playerColors.black].sum * 0.25);
|
||||
statusToUpdate.nodePower += floor(score[GoColor.black].sum * 0.25);
|
||||
} else {
|
||||
statusToUpdate.wins++;
|
||||
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
|
||||
@@ -66,12 +63,12 @@ export function endGoGame(boardState: BoardState) {
|
||||
statusToUpdate.highestWinStreak = statusToUpdate.winStreak;
|
||||
}
|
||||
|
||||
const factionName = boardState.ai as unknown as FactionName;
|
||||
const factionName = getEnumHelper("FactionName").getMember(boardState.ai);
|
||||
if (
|
||||
factionName &&
|
||||
statusToUpdate.winStreak % 2 === 0 &&
|
||||
Player.factions.includes(factionName) &&
|
||||
statusToUpdate.favor < getMaxFavor() &&
|
||||
Factions?.[factionName]
|
||||
statusToUpdate.favor < getMaxFavor()
|
||||
) {
|
||||
Factions[factionName].favor++;
|
||||
statusToUpdate.favor++;
|
||||
@@ -79,13 +76,13 @@ export function endGoGame(boardState: BoardState) {
|
||||
}
|
||||
|
||||
statusToUpdate.nodePower +=
|
||||
score[playerColors.black].sum *
|
||||
getDifficultyMultiplier(score[playerColors.white].komi, boardState.board[0].length) *
|
||||
score[GoColor.black].sum *
|
||||
getDifficultyMultiplier(score[GoColor.white].komi, boardState.board[0].length) *
|
||||
getWinstreakMultiplier(statusToUpdate.winStreak, statusToUpdate.oldWinStreak);
|
||||
|
||||
statusToUpdate.nodes += score[playerColors.black].sum;
|
||||
Player.go.boardState = boardState;
|
||||
Player.go.previousGameFinalBoardState = boardState;
|
||||
statusToUpdate.nodes += score[GoColor.black].sum;
|
||||
Go.currentGame = boardState;
|
||||
Go.previousGame = boardState;
|
||||
|
||||
// Update multipliers with new bonuses, once at the end of the game
|
||||
Player.applyEntropy(Player.entropy);
|
||||
@@ -94,8 +91,8 @@ export function endGoGame(boardState: BoardState) {
|
||||
/**
|
||||
* Sets the winstreak to zero for the given opponent, and adds a loss
|
||||
*/
|
||||
export function resetWinstreak(opponent: opponents, gameComplete: boolean) {
|
||||
const statusToUpdate = getPlayerStats(opponent);
|
||||
export function resetWinstreak(opponent: GoOpponent, gameComplete: boolean) {
|
||||
const statusToUpdate = getOpponentStats(opponent);
|
||||
statusToUpdate.losses++;
|
||||
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
|
||||
if (statusToUpdate.winStreak >= 0) {
|
||||
@@ -109,9 +106,9 @@ export function resetWinstreak(opponent: opponents, gameComplete: boolean) {
|
||||
/**
|
||||
* Gets the number pieces of a given color on the board
|
||||
*/
|
||||
function getColoredPieceCount(boardState: BoardState, color: PlayerColor) {
|
||||
function getColoredPieceCount(boardState: BoardState, color: GoColor) {
|
||||
return boardState.board.reduce(
|
||||
(sum, row) => sum + row.filter(isNotNull).filter((point) => point.player === color).length,
|
||||
(sum, row) => sum + row.filter(isNotNull).filter((point) => point.color === color).length,
|
||||
0,
|
||||
);
|
||||
}
|
||||
@@ -119,22 +116,20 @@ function getColoredPieceCount(boardState: BoardState, color: PlayerColor) {
|
||||
/**
|
||||
* Finds all empty spaces fully surrounded by a single player's stones
|
||||
*/
|
||||
function getTerritoryScores(boardState: BoardState) {
|
||||
const emptyTerritoryChains = getAllChains(boardState).filter((chain) => chain?.[0]?.player === playerColors.empty);
|
||||
function getTerritoryScores(board: Board) {
|
||||
const emptyTerritoryChains = getAllChains(board).filter((chain) => chain?.[0]?.color === GoColor.empty);
|
||||
|
||||
return emptyTerritoryChains.reduce(
|
||||
(scores, currentChain) => {
|
||||
const chainColor = checkTerritoryOwnership(boardState, currentChain);
|
||||
const chainColor = checkTerritoryOwnership(board, currentChain);
|
||||
return {
|
||||
[playerColors.white]:
|
||||
scores[playerColors.white] + (chainColor === playerColors.white ? currentChain.length : 0),
|
||||
[playerColors.black]:
|
||||
scores[playerColors.black] + (chainColor === playerColors.black ? currentChain.length : 0),
|
||||
[GoColor.white]: scores[GoColor.white] + (chainColor === GoColor.white ? currentChain.length : 0),
|
||||
[GoColor.black]: scores[GoColor.black] + (chainColor === GoColor.black ? currentChain.length : 0),
|
||||
};
|
||||
},
|
||||
{
|
||||
[playerColors.white]: 0,
|
||||
[playerColors.black]: 0,
|
||||
[GoColor.white]: 0,
|
||||
[GoColor.black]: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -142,17 +137,17 @@ function getTerritoryScores(boardState: BoardState) {
|
||||
/**
|
||||
* Finds all neighbors of the empty points in question. If they are all one color, that player controls that space
|
||||
*/
|
||||
function checkTerritoryOwnership(boardState: BoardState, emptyPointChain: PointState[]) {
|
||||
if (emptyPointChain.length > boardState.board[0].length ** 2 - 3) {
|
||||
function checkTerritoryOwnership(board: Board, emptyPointChain: PointState[]) {
|
||||
if (emptyPointChain.length > board[0].length ** 2 - 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const playerNeighbors = getPlayerNeighbors(boardState, emptyPointChain);
|
||||
const hasWhitePieceNeighbors = playerNeighbors.find((p) => p.player === playerColors.white);
|
||||
const hasBlackPieceNeighbors = playerNeighbors.find((p) => p.player === playerColors.black);
|
||||
const playerNeighbors = getPlayerNeighbors(board, emptyPointChain);
|
||||
const hasWhitePieceNeighbors = playerNeighbors.find((p) => p.color === GoColor.white);
|
||||
const hasBlackPieceNeighbors = playerNeighbors.find((p) => p.color === GoColor.black);
|
||||
const isWhiteTerritory = hasWhitePieceNeighbors && !hasBlackPieceNeighbors;
|
||||
const isBlackTerritory = hasBlackPieceNeighbors && !hasWhitePieceNeighbors;
|
||||
return isWhiteTerritory ? playerColors.white : isBlackTerritory ? playerColors.black : null;
|
||||
return isWhiteTerritory ? GoColor.white : isBlackTerritory ? GoColor.black : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,9 +166,6 @@ export function logBoard(boardState: BoardState): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function getPlayerStats(opponent: opponents) {
|
||||
if (!Player.go.status[opponent]) {
|
||||
Player.go = getGoPlayerStartingState();
|
||||
}
|
||||
return Player.go.status[opponent];
|
||||
export function getOpponentStats(opponent: GoOpponent) {
|
||||
return Go.stats[opponent] ?? (Go.stats[opponent] = newOpponentStats());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user