diff --git a/src/Go/SaveLoad.ts b/src/Go/SaveLoad.ts index 5833a0aff..4ef2c6858 100644 --- a/src/Go/SaveLoad.ts +++ b/src/Go/SaveLoad.ts @@ -12,7 +12,6 @@ import { isInteger, isNumber } from "../types"; type PreviousGameSaveData = { ai: GoOpponent; board: SimpleBoard; previousPlayer: GoColor | null } | null; type CurrentGameSaveData = PreviousGameSaveData & { - previousBoard: SimpleBoard | null; cheatCount: number; passCount: number; }; @@ -35,7 +34,6 @@ export function getGoSave(): SaveFormat { currentGame: { ai: Go.currentGame.ai, board: simpleBoardFromBoard(Go.currentGame.board), - previousBoard: Go.currentGame.previousBoard, previousPlayer: Go.currentGame.previousPlayer, cheatCount: Go.currentGame.cheatCount, passCount: Go.currentGame.passCount, @@ -94,8 +92,6 @@ function loadCurrentGame(currentGame: unknown): BoardState | string { const requiredSize = currentGame.board.length; const board = loadSimpleBoard(currentGame.board, requiredSize); if (typeof board === "string") return board; - const previousBoard = currentGame.previousBoard ? loadSimpleBoard(currentGame.previousBoard, requiredSize) : null; - if (typeof previousBoard === "string") return previousBoard; const previousPlayer = getEnumHelper("GoColor").getMember(currentGame.previousPlayer) ?? null; if (!isInteger(currentGame.cheatCount) || currentGame.cheatCount < 0) return "invalid number for currentGame.cheatCount"; @@ -105,7 +101,7 @@ function loadCurrentGame(currentGame: unknown): BoardState | string { boardState.previousPlayer = previousPlayer; boardState.cheatCount = currentGame.cheatCount; boardState.passCount = currentGame.passCount; - boardState.previousBoard = previousBoard; + boardState.previousBoards = []; return boardState; } diff --git a/src/Go/Types.ts b/src/Go/Types.ts index 8201746d5..0ab0e7217 100644 --- a/src/Go/Types.ts +++ b/src/Go/Types.ts @@ -36,8 +36,8 @@ export type EyeMove = { export type BoardState = { board: Board; previousPlayer: GoColor | null; - /** The previous board position as a SimpleBoard */ - previousBoard: SimpleBoard | null; + /** The previous board positions as a SimpleBoard */ + previousBoards: SimpleBoard[]; ai: GoOpponent; passCount: number; cheatCount: number; diff --git a/src/Go/boardAnalysis/boardAnalysis.ts b/src/Go/boardAnalysis/boardAnalysis.ts index e34a0e3c9..c98f8fa1c 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.previousBoard && getColorOnSimpleBoard(boardState.previousBoard, x, y) === player; + const possibleRepeat = boardState.previousBoards.find((board) => getColorOnSimpleBoard(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 @@ -85,9 +85,11 @@ export function evaluateIfMoveIsValid(boardState: BoardState, x: number, y: numb if (evaluationBoard[x]?.[y]?.color !== player) { return GoValidity.noSuicide; } - if (possibleRepeat && boardState.previousBoard) { + if (possibleRepeat && boardState.previousBoards.length) { const simpleEvalBoard = simpleBoardFromBoard(evaluationBoard); - if (areSimpleBoardsIdentical(simpleEvalBoard, boardState.previousBoard)) return GoValidity.boardRepeated; + if (boardState.previousBoards.find((board) => areSimpleBoardsIdentical(simpleEvalBoard, board))) { + return GoValidity.boardRepeated; + } } return GoValidity.valid; diff --git a/src/Go/boardState/boardState.ts b/src/Go/boardState/boardState.ts index 644506624..b00c48f70 100644 --- a/src/Go/boardState/boardState.ts +++ b/src/Go/boardState/boardState.ts @@ -27,7 +27,7 @@ export function getNewBoardState( } const newBoardState: BoardState = { - previousBoard: null, + previousBoards: [], previousPlayer: GoColor.white, ai: ai, passCount: 0, @@ -82,7 +82,12 @@ export function makeMove(boardState: BoardState, x: number, y: number, player: G return false; } - boardState.previousBoard = simpleBoardFromBoard(boardState.board); + // 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; @@ -266,7 +271,7 @@ export function getEmptySpaces(board: Board): PointState[] { export function getStateCopy(initialState: BoardState) { const boardState = structuredClone(initialState); - boardState.previousBoard = initialState.previousBoard ? [...initialState.previousBoard] : null; + boardState.previousBoards = initialState.previousBoards ?? []; boardState.previousPlayer = initialState.previousPlayer; boardState.ai = initialState.ai; boardState.passCount = initialState.passCount; diff --git a/src/Go/effects/effect.ts b/src/Go/effects/effect.ts index 93845e8b4..0b325e37b 100644 --- a/src/Go/effects/effect.ts +++ b/src/Go/effects/effect.ts @@ -98,7 +98,7 @@ function calculateMults(): Multipliers { } export function playerHasDiscoveredGo() { - const playedGame = Go.currentGame.previousBoard; + const playedGame = Go.currentGame.previousBoards.length; const hasRecords = getRecordValues(Go.stats).some((stats) => stats.wins + stats.losses); const isInBn14 = Player.bitNodeN === 14; diff --git a/src/Go/effects/netscriptGoImplementation.ts b/src/Go/effects/netscriptGoImplementation.ts index c8dc1157c..40d1012c1 100644 --- a/src/Go/effects/netscriptGoImplementation.ts +++ b/src/Go/effects/netscriptGoImplementation.ts @@ -300,11 +300,11 @@ export function getCurrentPlayer(): "None" | "White" | "Black" { * Find a move made by the previous player, if present. */ export function getPreviousMove(): [number, number] | null { - if (Go.currentGame.passCount) { + const priorBoard = Go.currentGame?.previousBoards[0]; + if (Go.currentGame.passCount || !priorBoard) { return null; } - const priorBoard = Go.currentGame?.previousBoard; for (const rowIndexString in Go.currentGame.board) { const row = Go.currentGame.board[+rowIndexString] ?? []; for (const pointIndexString in row) { @@ -348,7 +348,7 @@ export function resetBoardState(error: (s: string) => void, opponent: GoOpponent } const oldBoardState = Go.currentGame; - if (oldBoardState.previousPlayer !== null && oldBoardState.previousBoard) { + if (oldBoardState.previousPlayer !== null && oldBoardState.previousBoards.length) { resetWinstreak(oldBoardState.ai, false); } diff --git a/src/Go/ui/GoGameboardWrapper.tsx b/src/Go/ui/GoGameboardWrapper.tsx index 453964b1a..a644bdb69 100644 --- a/src/Go/ui/GoGameboardWrapper.tsx +++ b/src/Go/ui/GoGameboardWrapper.tsx @@ -143,7 +143,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps setScoreOpen(false); setSearchOpen(false); setOpponent(newOpponent); - if (boardState.previousPlayer !== null && boardState.previousBoard) { + if (boardState.previousPlayer !== null && boardState.previousBoards.length) { resetWinstreak(boardState.ai, false); } @@ -152,16 +152,16 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps } function getPriorMove() { - if (!boardState.previousBoard) return boardState; + if (!boardState.previousBoards.length) return boardState; const priorState = getStateCopy(boardState); priorState.previousPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black; - priorState.board = boardFromSimpleBoard(boardState.previousBoard); + priorState.board = boardFromSimpleBoard(boardState.previousBoards[0]); updateCaptures(priorState.board, priorState.previousPlayer); return priorState; } function showPreviousMove(newValue: boolean) { - if (boardState.previousBoard) { + if (boardState.previousBoards.length) { setShowPriorMove(newValue); } } @@ -175,7 +175,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps boardState.previousPlayer === GoColor.white && !getAllValidMoves(boardState, GoColor.black).length; const disablePassButton = opponent !== GoOpponent.none && boardState.previousPlayer === GoColor.black && waitingOnAI; - const scoreBoxText = boardState.previousBoard + const scoreBoxText = boardState.previousBoards.length ? `Score: Black: ${score[GoColor.black].sum} White: ${score[GoColor.white].sum}` : "Place a router to begin!"; @@ -256,7 +256,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps /> showPreviousMove(newValue)} text="Show previous move" tooltip={<>Show the board as it was before the last move} diff --git a/src/Go/ui/GoPoint.tsx b/src/Go/ui/GoPoint.tsx index e17b9c9b7..5c1c6217b 100644 --- a/src/Go/ui/GoPoint.tsx +++ b/src/Go/ui/GoPoint.tsx @@ -41,7 +41,8 @@ export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwne const sizeClass = getSizeClass(state.board[0].length, classes); - const isNewStone = state.previousBoard && getColorOnSimpleBoard(state.previousBoard, x, y) === GoColor.empty; + const isNewStone = + state.previousBoards.length && getColorOnSimpleBoard(state.previousBoards[0], x, y) === GoColor.empty; const isPriorMove = player === state.previousPlayer && isNewStone; const emptyPointColorClass = diff --git a/src/Go/ui/GoTutorialChallenge.tsx b/src/Go/ui/GoTutorialChallenge.tsx index 83fd0ddb6..e416642c2 100644 --- a/src/Go/ui/GoTutorialChallenge.tsx +++ b/src/Go/ui/GoTutorialChallenge.tsx @@ -38,7 +38,7 @@ export function GoTutorialChallenge({ const [showReset, setShowReset] = useState(false); const handleClick = (x: number, y: number) => { - if (stateRef.current.previousBoard) { + if (stateRef.current.previousBoards.length) { SnackbarEvents.emit(`Hit 'Reset' to try again`, ToastVariant.WARNING, 2000); return; } @@ -67,6 +67,7 @@ export function GoTutorialChallenge({ const reset = () => { stateRef.current = getStateCopy(state); + stateRef.current.previousBoards = []; setDisplayText(description); setShowReset(false); }; diff --git a/test/jest/Go/NetscriptGo.test.ts b/test/jest/Go/NetscriptGo.test.ts index 8599093cc..b477aea46 100644 --- a/test/jest/Go/NetscriptGo.test.ts +++ b/test/jest/Go/NetscriptGo.test.ts @@ -81,7 +81,7 @@ describe("Netscript Go API unit tests", () => { it("should correctly retrieve the current game state", async () => { const board = ["OXX..", ".....", ".....", "...XX", "...X."]; const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.black); - boardState.previousBoard = ["OX..", ".....", ".....", "...XX", "...X."]; + boardState.previousBoards = [["OX..", ".....", ".....", "...XX", "...X."]]; Go.currentGame = boardState; const result = getGameState(); @@ -232,7 +232,7 @@ describe("Netscript Go API unit tests", () => { await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 1, 0); expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet."); - expect(Go.currentGame.previousBoard).toEqual(null); + expect(Go.currentGame.previousBoards).toEqual([]); }); }); describe("cheatRemoveRouter() tests", () => { @@ -270,7 +270,7 @@ describe("Netscript Go API unit tests", () => { await cheatRemoveRouter(mockLogger, 0, 0, 1, 0); expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet."); - expect(Go.currentGame.previousBoard).toEqual(null); + expect(Go.currentGame.previousBoards).toEqual([]); }); }); describe("cheatRepairOfflineNode() tests", () => { diff --git a/test/jest/Go/boardAnalysis.test.ts b/test/jest/Go/boardAnalysis.test.ts index 03cc352ea..805ebff94 100644 --- a/test/jest/Go/boardAnalysis.test.ts +++ b/test/jest/Go/boardAnalysis.test.ts @@ -53,4 +53,16 @@ describe("Go board analysis tests", () => { expect(validity).toEqual(GoValidity.noSuicide); }); + + 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...", ".....", ".....", ".....", "....."]); + const validity = evaluateIfMoveIsValid(boardState, 0, 0, GoColor.white, false); + + expect(validity).toEqual(GoValidity.boardRepeated); + }); }); diff --git a/test/jest/__snapshots__/FullSave.test.ts.snap b/test/jest/__snapshots__/FullSave.test.ts.snap index bbc438f50..d710741d4 100644 --- a/test/jest/__snapshots__/FullSave.test.ts.snap +++ b/test/jest/__snapshots__/FullSave.test.ts.snap @@ -41,7 +41,6 @@ exports[`Check Save File Continuity GoSave continuity 1`] = ` ], "cheatCount": 0, "passCount": 0, - "previousBoard": null, "previousPlayer": "White", }, "previousGame": null,