diff --git a/src/Go/Types.ts b/src/Go/Types.ts index 1697e20ae..c765c6aec 100644 --- a/src/Go/Types.ts +++ b/src/Go/Types.ts @@ -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; diff --git a/src/Go/boardAnalysis/boardAnalysis.ts b/src/Go/boardAnalysis/boardAnalysis.ts index 5fd6c2c5d..c3d8b097c 100644 --- a/src/Go/boardAnalysis/boardAnalysis.ts +++ b/src/Go/boardAnalysis/boardAnalysis.ts @@ -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; diff --git a/src/Go/boardState/boardState.ts b/src/Go/boardState/boardState.ts index 3eb80e841..808cdd634 100644 --- a/src/Go/boardState/boardState.ts +++ b/src/Go/boardState/boardState.ts @@ -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; diff --git a/src/Go/ui/GoGameboardWrapper.tsx b/src/Go/ui/GoGameboardWrapper.tsx index 466f1e5c5..6c2e76072 100644 --- a/src/Go/ui/GoGameboardWrapper.tsx +++ b/src/Go/ui/GoGameboardWrapper.tsx @@ -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; } diff --git a/src/Go/ui/GoPoint.tsx b/src/Go/ui/GoPoint.tsx index 5c1c6217b..5e67346e9 100644 --- a/src/Go/ui/GoPoint.tsx +++ b/src/Go/ui/GoPoint.tsx @@ -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 = diff --git a/test/jest/Go/NetscriptGo.test.ts b/test/jest/Go/NetscriptGo.test.ts index 85a6174ee..28d05d896 100644 --- a/test/jest/Go/NetscriptGo.test.ts +++ b/test/jest/Go/NetscriptGo.test.ts @@ -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(); diff --git a/test/jest/Go/boardAnalysis.test.ts b/test/jest/Go/boardAnalysis.test.ts index 805ebff94..97df6b678 100644 --- a/test/jest/Go/boardAnalysis.test.ts +++ b/test/jest/Go/boardAnalysis.test.ts @@ -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);