IPVGO: Record full history to avoid infinite ko capture loops on larger boards (#1299)

This commit is contained in:
Michael Ficocelli
2024-06-02 23:19:26 -04:00
committed by GitHub
parent 2f7950b49c
commit d9f04203cf
7 changed files with 61 additions and 28 deletions
+46 -10
View File
@@ -44,7 +44,7 @@ export function evaluateIfMoveIsValid(boardState: BoardState, x: number, y: numb
}
// Detect if the move might be an immediate repeat (only one board of history is saved to check)
const possibleRepeat = boardState.previousBoards.find((board) => getColorOnSimpleBoard(board, x, y) === player);
const possibleRepeat = boardState.previousBoards.find((board) => getColorOnBoardString(board, 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
@@ -86,8 +86,8 @@ export function evaluateIfMoveIsValid(boardState: BoardState, x: number, y: numb
return GoValidity.noSuicide;
}
if (possibleRepeat && boardState.previousBoards.length) {
const simpleEvalBoard = simpleBoardFromBoard(evaluationBoard);
if (boardState.previousBoards.find((board) => areSimpleBoardsIdentical(simpleEvalBoard, board))) {
const simpleEvalBoard = boardStringFromBoard(evaluationBoard);
if (boardState.previousBoards.includes(simpleEvalBoard)) {
return GoValidity.boardRepeated;
}
}
@@ -548,7 +548,8 @@ export function findAdjacentLibertiesAndAlliesForPoint(
}
/**
* Retrieves a simplified version of the board state. "X" represents black pieces, "O" white, and "." empty points.
* Retrieves a simplified version of the board state.
* "X" represents black pieces, "O" white, "." empty points, and "#" offline nodes.
*
* For example, a 5x5 board might look like this:
* ```
@@ -563,14 +564,15 @@ export function findAdjacentLibertiesAndAlliesForPoint(
*
* Each string represents a vertical column on the board, and each character in the string represents a point.
*
* Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index [1][0].
* Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of
* index (1 * N) + 0 , where N is the size of the board.
*
* Note that the [0][0] point is shown on the bottom-left on the visual board (as is traditional), and each
* Note that index 0 (the [0][0] point) is shown on the bottom-left on the visual board (as is traditional), and each
* string represents a vertical column on the board. In other words, the printed example above can be understood to
* be rotated 90 degrees clockwise compared to the board UI as shown in the IPvGO game.
*
*/
export function simpleBoardFromBoard(board: Board): string[] {
export function simpleBoardFromBoard(board: Board): SimpleBoard {
return board.map((column) =>
column.reduce((str, point) => {
if (!point) {
@@ -587,6 +589,39 @@ export function simpleBoardFromBoard(board: Board): string[] {
);
}
/**
* Returns a string representation of the given board.
* The string representation is the same as simpleBoardFromBoard() but concatenated into a single string
*
* For example, a 5x5 board might look like this:
* ```
* "XX.O.X..OO.XO..XXO...XOO."
* ```
*/
export function boardStringFromBoard(board: Board): string {
return simpleBoardFromBoard(board).join("");
}
/**
* Returns a full board object from a string representation of the board.
* The string representation is the same as simpleBoardFromBoard() but concatenated into a single string
*
* For example, a 5x5 board might look like this:
* ```
* "XX.O.X..OO.XO..XXO...XOO."
* ```
*/
export function boardFromBoardString(boardString: string): Board {
// Turn the SimpleBoard string into a string array, allowing access of each point via indexes e.g. [0][1]
const boardSize = Math.round(Math.sqrt(boardString.length));
const boardTiles = boardString.split("");
const simpleBoardArray = Array(boardSize).map((_, index) =>
boardTiles.slice(index * boardSize, (index + 1) * boardSize).join(""),
);
return boardFromSimpleBoard(simpleBoardArray);
}
/** 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) =>
@@ -624,8 +659,9 @@ export function areSimpleBoardsIdentical(simpleBoard1: SimpleBoard, simpleBoard2
return simpleBoard1.every((column, x) => column === simpleBoard2[x]);
}
export function getColorOnSimpleBoard(simpleBoard: SimpleBoard, x: number, y: number): GoColor | null {
const char = simpleBoard[x]?.[y];
export function getColorOnBoardString(boardString: string, x: number, y: number): GoColor | null {
const boardSize = Math.round(Math.sqrt(boardString.length));
const char = boardString[x * boardSize + y];
if (char === "X") return GoColor.black;
if (char === "O") return GoColor.white;
if (char === ".") return GoColor.empty;
@@ -643,7 +679,7 @@ export function getPreviousMove(): [number, number] | null {
const row = Go.currentGame.board[+rowIndexString] ?? [];
for (const pointIndexString in row) {
const point = row[+pointIndexString];
const priorColor = point && priorBoard && getColorOnSimpleBoard(priorBoard, point.x, point.y);
const priorColor = point && priorBoard && getColorOnBoardString(priorBoard, point.x, point.y);
const currentColor = point?.color;
const isPreviousPlayer = currentColor === Go.currentGame.previousPlayer;
const isChanged = priorColor !== currentColor;