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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 61 additions and 28 deletions

@ -37,7 +37,7 @@ export type BoardState = {
board: Board; board: Board;
previousPlayer: GoColor | null; previousPlayer: GoColor | null;
/** The previous board positions as a SimpleBoard */ /** The previous board positions as a SimpleBoard */
previousBoards: SimpleBoard[]; previousBoards: string[];
ai: GoOpponent; ai: GoOpponent;
passCount: number; passCount: number;
cheatCount: number; cheatCount: number;

@ -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) // 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 (shortcut) {
// If the current point has some adjacent open spaces, it is not suicide. If the move is not repeated, it is legal // 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; return GoValidity.noSuicide;
} }
if (possibleRepeat && boardState.previousBoards.length) { if (possibleRepeat && boardState.previousBoards.length) {
const simpleEvalBoard = simpleBoardFromBoard(evaluationBoard); const simpleEvalBoard = boardStringFromBoard(evaluationBoard);
if (boardState.previousBoards.find((board) => areSimpleBoardsIdentical(simpleEvalBoard, board))) { if (boardState.previousBoards.includes(simpleEvalBoard)) {
return GoValidity.boardRepeated; 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: * 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. * 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 * 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. * 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) => return board.map((column) =>
column.reduce((str, point) => { column.reduce((str, point) => {
if (!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) */ /** Creates a board object from a simple board. The resulting board has no analytics (liberties/chains) */
export function boardFromSimpleBoard(simpleBoard: SimpleBoard): Board { export function boardFromSimpleBoard(simpleBoard: SimpleBoard): Board {
return simpleBoard.map((column, x) => return simpleBoard.map((column, x) =>
@ -624,8 +659,9 @@ export function areSimpleBoardsIdentical(simpleBoard1: SimpleBoard, simpleBoard2
return simpleBoard1.every((column, x) => column === simpleBoard2[x]); return simpleBoard1.every((column, x) => column === simpleBoard2[x]);
} }
export function getColorOnSimpleBoard(simpleBoard: SimpleBoard, x: number, y: number): GoColor | null { export function getColorOnBoardString(boardString: string, x: number, y: number): GoColor | null {
const char = simpleBoard[x]?.[y]; const boardSize = Math.round(Math.sqrt(boardString.length));
const char = boardString[x * boardSize + y];
if (char === "X") return GoColor.black; if (char === "X") return GoColor.black;
if (char === "O") return GoColor.white; if (char === "O") return GoColor.white;
if (char === ".") return GoColor.empty; if (char === ".") return GoColor.empty;
@ -643,7 +679,7 @@ export function getPreviousMove(): [number, number] | null {
const row = Go.currentGame.board[+rowIndexString] ?? []; const row = Go.currentGame.board[+rowIndexString] ?? [];
for (const pointIndexString in row) { for (const pointIndexString in row) {
const point = row[+pointIndexString]; 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 currentColor = point?.color;
const isPreviousPlayer = currentColor === Go.currentGame.previousPlayer; const isPreviousPlayer = currentColor === Go.currentGame.previousPlayer;
const isChanged = priorColor !== currentColor; const isChanged = priorColor !== currentColor;

@ -9,7 +9,7 @@ import {
findLibertiesForChain, findLibertiesForChain,
getAllChains, getAllChains,
boardFromSimpleBoard, boardFromSimpleBoard,
simpleBoardFromBoard, boardStringFromBoard,
} from "../boardAnalysis/boardAnalysis"; } from "../boardAnalysis/boardAnalysis";
import { endGoGame } from "../boardAnalysis/scoring"; import { endGoGame } from "../boardAnalysis/scoring";
import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes"; import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes";
@ -89,15 +89,12 @@ export function makeMove(boardState: BoardState, x: number, y: number, player: G
return false; return false;
} }
// Only maintain last 7 moves
boardState.previousBoards.unshift(simpleBoardFromBoard(boardState.board));
if (boardState.previousBoards.length > 7) {
boardState.previousBoards.pop();
}
const point = boardState.board[x][y]; const point = boardState.board[x][y];
if (!point) return false; if (!point) return false;
// Add move to board history
boardState.previousBoards.unshift(boardStringFromBoard(boardState.board));
point.color = player; point.color = player;
boardState.previousPlayer = player; boardState.previousPlayer = player;
boardState.passCount = 0; boardState.passCount = 0;

@ -9,7 +9,7 @@ import { SnackbarEvents } from "../../ui/React/Snackbar";
import { getNewBoardState, getStateCopy, makeMove, passTurn, updateCaptures } from "../boardState/boardState"; import { getNewBoardState, getStateCopy, makeMove, passTurn, updateCaptures } from "../boardState/boardState";
import { bitverseArt, weiArt } from "../boardState/asciiArt"; import { bitverseArt, weiArt } from "../boardState/asciiArt";
import { getScore, resetWinstreak } from "../boardAnalysis/scoring"; import { getScore, resetWinstreak } from "../boardAnalysis/scoring";
import { boardFromSimpleBoard, evaluateIfMoveIsValid, getAllValidMoves } from "../boardAnalysis/boardAnalysis"; import { boardFromBoardString, evaluateIfMoveIsValid, getAllValidMoves } from "../boardAnalysis/boardAnalysis";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { OptionSwitch } from "../../ui/React/OptionSwitch"; import { OptionSwitch } from "../../ui/React/OptionSwitch";
import { boardStyles } from "../boardState/goStyles"; import { boardStyles } from "../boardState/goStyles";
@ -150,7 +150,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
if (!boardState.previousBoards.length) return boardState; if (!boardState.previousBoards.length) return boardState;
const priorState = getStateCopy(boardState); const priorState = getStateCopy(boardState);
priorState.previousPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black; priorState.previousPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
priorState.board = boardFromSimpleBoard(boardState.previousBoards[0]); priorState.board = boardFromBoardString(boardState.previousBoards[0]);
updateCaptures(priorState.board, priorState.previousPlayer); updateCaptures(priorState.board, priorState.previousPlayer);
return priorState; return priorState;
} }

@ -7,7 +7,7 @@ import { GoColor } from "@enums";
import { columnIndexes } from "../Constants"; import { columnIndexes } from "../Constants";
import { findNeighbors } from "../boardState/boardState"; import { findNeighbors } from "../boardState/boardState";
import { pointStyle } from "../boardState/goStyles"; import { pointStyle } from "../boardState/goStyles";
import { findAdjacentLibertiesAndAlliesForPoint, getColorOnSimpleBoard } from "../boardAnalysis/boardAnalysis"; import { findAdjacentLibertiesAndAlliesForPoint, getColorOnBoardString } from "../boardAnalysis/boardAnalysis";
interface GoPointProps { interface GoPointProps {
state: BoardState; state: BoardState;
@ -42,7 +42,7 @@ export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwne
const sizeClass = getSizeClass(state.board[0].length, classes); const sizeClass = getSizeClass(state.board[0].length, classes);
const isNewStone = const isNewStone =
state.previousBoards.length && getColorOnSimpleBoard(state.previousBoards[0], x, y) === GoColor.empty; state.previousBoards.length && getColorOnBoardString(state.previousBoards[0], x, y) === GoColor.empty;
const isPriorMove = player === state.previousPlayer && isNewStone; const isPriorMove = player === state.previousPlayer && isNewStone;
const emptyPointColorClass = const emptyPointColorClass =

@ -92,9 +92,9 @@ describe("Netscript Go API unit tests", () => {
describe("getGameState() tests", () => { describe("getGameState() tests", () => {
it("should correctly retrieve the current game state", async () => { it("should correctly retrieve the current game state", async () => {
const board = ["OXX..", ".....", ".....", "...XX", "...X."]; const board = ["OXX..", ".....", "..#..", "...XX", "...X."];
const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.black); const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.black);
boardState.previousBoards = [["OX..", ".....", ".....", "...XX", "...X."]]; boardState.previousBoards = ["OX.........#.....XX...X."];
Go.currentGame = boardState; Go.currentGame = boardState;
const result = getGameState(); const result = getGameState();

@ -57,10 +57,10 @@ describe("Go board analysis tests", () => {
it("identifies invalid moves from repeat", async () => { it("identifies invalid moves from repeat", async () => {
const board = [".X...", ".....", ".....", ".....", "....."]; const board = [".X...", ".....", ".....", ".....", "....."];
const boardState = boardStateFromSimpleBoard(board); const boardState = boardStateFromSimpleBoard(board);
boardState.previousBoards.push([".X...", ".....", ".....", ".....", "....."]); boardState.previousBoards.push(".X.......................");
boardState.previousBoards.push([".X...", ".....", ".....", ".....", "....."]); boardState.previousBoards.push(".X.......................");
boardState.previousBoards.push([".X...", ".....", ".....", ".....", "....."]); boardState.previousBoards.push(".X.......................");
boardState.previousBoards.push(["OX...", ".....", ".....", ".....", "....."]); boardState.previousBoards.push("OX.......................");
const validity = evaluateIfMoveIsValid(boardState, 0, 0, GoColor.white, false); const validity = evaluateIfMoveIsValid(boardState, 0, 0, GoColor.white, false);
expect(validity).toEqual(GoValidity.boardRepeated); expect(validity).toEqual(GoValidity.boardRepeated);