mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-25 00:53:52 +01:00
IPVGO: Record full history to avoid infinite ko capture loops on larger boards (#1299)
This commit is contained in:
parent
2f7950b49c
commit
d9f04203cf
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user