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;
|
||||
previousPlayer: GoColor | null;
|
||||
/** The previous board positions as a SimpleBoard */
|
||||
previousBoards: SimpleBoard[];
|
||||
previousBoards: string[];
|
||||
ai: GoOpponent;
|
||||
passCount: 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)
|
||||
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;
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
findLibertiesForChain,
|
||||
getAllChains,
|
||||
boardFromSimpleBoard,
|
||||
simpleBoardFromBoard,
|
||||
boardStringFromBoard,
|
||||
} from "../boardAnalysis/boardAnalysis";
|
||||
import { endGoGame } from "../boardAnalysis/scoring";
|
||||
import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes";
|
||||
@ -89,15 +89,12 @@ export function makeMove(boardState: BoardState, x: number, y: number, player: G
|
||||
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];
|
||||
if (!point) return false;
|
||||
|
||||
// Add move to board history
|
||||
boardState.previousBoards.unshift(boardStringFromBoard(boardState.board));
|
||||
|
||||
point.color = player;
|
||||
boardState.previousPlayer = player;
|
||||
boardState.passCount = 0;
|
||||
|
@ -9,7 +9,7 @@ import { SnackbarEvents } from "../../ui/React/Snackbar";
|
||||
import { getNewBoardState, getStateCopy, makeMove, passTurn, updateCaptures } from "../boardState/boardState";
|
||||
import { bitverseArt, weiArt } from "../boardState/asciiArt";
|
||||
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 { OptionSwitch } from "../../ui/React/OptionSwitch";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
@ -150,7 +150,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
if (!boardState.previousBoards.length) return boardState;
|
||||
const priorState = getStateCopy(boardState);
|
||||
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);
|
||||
return priorState;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { GoColor } from "@enums";
|
||||
import { columnIndexes } from "../Constants";
|
||||
import { findNeighbors } from "../boardState/boardState";
|
||||
import { pointStyle } from "../boardState/goStyles";
|
||||
import { findAdjacentLibertiesAndAlliesForPoint, getColorOnSimpleBoard } from "../boardAnalysis/boardAnalysis";
|
||||
import { findAdjacentLibertiesAndAlliesForPoint, getColorOnBoardString } from "../boardAnalysis/boardAnalysis";
|
||||
|
||||
interface GoPointProps {
|
||||
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 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 emptyPointColorClass =
|
||||
|
@ -92,9 +92,9 @@ describe("Netscript Go API unit tests", () => {
|
||||
|
||||
describe("getGameState() tests", () => {
|
||||
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);
|
||||
boardState.previousBoards = [["OX..", ".....", ".....", "...XX", "...X."]];
|
||||
boardState.previousBoards = ["OX.........#.....XX...X."];
|
||||
Go.currentGame = boardState;
|
||||
|
||||
const result = getGameState();
|
||||
|
@ -57,10 +57,10 @@ describe("Go board analysis tests", () => {
|
||||
it("identifies invalid moves from repeat", async () => {
|
||||
const board = [".X...", ".....", ".....", ".....", "....."];
|
||||
const boardState = boardStateFromSimpleBoard(board);
|
||||
boardState.previousBoards.push([".X...", ".....", ".....", ".....", "....."]);
|
||||
boardState.previousBoards.push([".X...", ".....", ".....", ".....", "....."]);
|
||||
boardState.previousBoards.push([".X...", ".....", ".....", ".....", "....."]);
|
||||
boardState.previousBoards.push(["OX...", ".....", ".....", ".....", "....."]);
|
||||
boardState.previousBoards.push(".X.......................");
|
||||
boardState.previousBoards.push(".X.......................");
|
||||
boardState.previousBoards.push(".X.......................");
|
||||
boardState.previousBoards.push("OX.......................");
|
||||
const validity = evaluateIfMoveIsValid(boardState, 0, 0, GoColor.white, false);
|
||||
|
||||
expect(validity).toEqual(GoValidity.boardRepeated);
|
||||
|
Loading…
Reference in New Issue
Block a user