From 4218b01dfb7f3c92969e7f5bda61c8f3ac93b4b6 Mon Sep 17 00:00:00 2001 From: Michael Ficocelli Date: Sun, 21 Dec 2025 16:44:24 -0500 Subject: [PATCH] IPVGO: Do not update captures on passed analysis boards (#2415) --- markdown/bitburner.goanalysis.md | 2 +- ...tburner.goanalysis.settestingboardstate.md | 18 +++- src/Go/boardState/boardState.ts | 3 +- src/Go/effects/netscriptGoImplementation.ts | 17 ++-- src/NetscriptFunctions/Go.ts | 9 +- src/ScriptEditor/NetscriptDefinitions.d.ts | 3 +- src/utils/APIBreaks/3.0.0.ts | 10 +++ test/jest/Go/NetscriptGo.test.ts | 85 ++++++++++++++++++- 8 files changed, 127 insertions(+), 20 deletions(-) diff --git a/markdown/bitburner.goanalysis.md b/markdown/bitburner.goanalysis.md index 1808cfa77..161f68958 100644 --- a/markdown/bitburner.goanalysis.md +++ b/markdown/bitburner.goanalysis.md @@ -156,7 +156,7 @@ Reset all win/loss and winstreak records for the No AI opponent. -[setTestingBoardState(boardState, komi)](./bitburner.goanalysis.settestingboardstate.md) +[setTestingBoardState(boardState, komi, nextPlayerIsWhite)](./bitburner.goanalysis.settestingboardstate.md) diff --git a/markdown/bitburner.goanalysis.settestingboardstate.md b/markdown/bitburner.goanalysis.settestingboardstate.md index a44d8fd3f..d3654d36a 100644 --- a/markdown/bitburner.goanalysis.settestingboardstate.md +++ b/markdown/bitburner.goanalysis.settestingboardstate.md @@ -9,7 +9,7 @@ Starts a new game against the "No AI" opponent, and sets the initial board size, **Signature:** ```typescript -setTestingBoardState(boardState: string[], komi?: number): void; +setTestingBoardState(boardState: string[], komi?: number, nextPlayerIsWhite?: boolean): void; ``` ## Parameters @@ -61,6 +61,22 @@ number _(Optional)_ Optional komi value to set for the game. Defaults to 5.5. + + + +nextPlayerIsWhite + + + + +boolean + + + + +_(Optional)_ Optional. Whether or not the next player to play is the white player. Defaults to false. + + diff --git a/src/Go/boardState/boardState.ts b/src/Go/boardState/boardState.ts index 92bc3071b..9ff89c6e5 100644 --- a/src/Go/boardState/boardState.ts +++ b/src/Go/boardState/boardState.ts @@ -90,8 +90,7 @@ export function getNewBoardStateFromSimpleBoard( newState.previousPlayer = GoColor.black; } } - - updateCaptures(newState.board, newState.previousPlayer ?? GoColor.white); + updateChains(newState.board); return newState; } diff --git a/src/Go/effects/netscriptGoImplementation.ts b/src/Go/effects/netscriptGoImplementation.ts index 2a1890c92..fbd544ec6 100644 --- a/src/Go/effects/netscriptGoImplementation.ts +++ b/src/Go/effects/netscriptGoImplementation.ts @@ -242,9 +242,11 @@ export function getChains(_board?: Board) { export function getLiberties(_board?: Board) { const board = _board || Go.currentGame.board; return board.map((column) => - column.reduce((libertyArray: number[], point) => { - libertyArray.push(point?.liberties?.length || -1); - return libertyArray; + column.map((point) => { + if (!point?.liberties || point.color === GoColor.empty) { + return -1; + } + return point.liberties.length; }, []), ); } @@ -278,12 +280,13 @@ export function getControlledEmptyNodes(_board?: Board) { * Resets the active game to be a new board with "No AI" as the opponent. Applies the specified board state and komi to the new game. * Used for testing scenarios. */ -export function setTestingBoardState(ctx: NetscriptContext, board: Board, komi?: number) { - resetBoardState(ctx, GoOpponent.none, board.length); - Go.currentGame.board = board; +export function setTestingBoardState(ctx: NetscriptContext, state: BoardState, komi?: number) { + resetBoardState(ctx, GoOpponent.none, state.board.length); + Go.currentGame = state; if (komi != undefined) { Go.currentGame.komiOverride = komi; } + updateCaptures(Go.currentGame.board, Go.currentGame.previousPlayer ?? GoColor.white, true); GoEvents.emit(); } @@ -444,7 +447,7 @@ export function validateBoardState( return getNewBoardStateFromSimpleBoard( simpleBoard, priorSimpleBoard, - undefined, + GoOpponent.none, playAsWhite ? GoColor.black : GoColor.white, ); } catch (e) { diff --git a/src/NetscriptFunctions/Go.ts b/src/NetscriptFunctions/Go.ts index 6816be217..62366e627 100644 --- a/src/NetscriptFunctions/Go.ts +++ b/src/NetscriptFunctions/Go.ts @@ -114,13 +114,14 @@ export function NetscriptGo(): InternalAPI { const resetAll = helpers.boolean(ctx, "resetAll", _resetAll ?? false); resetStats(resetAll); }, - setTestingBoardState: (ctx) => (_boardState, _komi) => { - const State = validateBoardState(ctx, _boardState); + setTestingBoardState: (ctx) => (_boardState, _komi, _nextPlayerIsWhite) => { + const nextPlayerIsWhite = helpers.boolean(ctx, "nextPlayerIsWhite", _nextPlayerIsWhite ?? false); + const State = validateBoardState(ctx, _boardState, null, nextPlayerIsWhite); if (!State) { throw errorMessage(ctx, "Invalid board state passed to setTestingBoardState()"); } - const komi: number | undefined = _komi !== undefined ? helpers.number(ctx, "komi", _komi) : undefined; - return setTestingBoardState(ctx, State.board, komi); + const komi = helpers.number(ctx, "komi", _komi ?? 5.5); + return setTestingBoardState(ctx, State, komi); }, highlightPoint: (ctx) => (_x, _y, _color, _text) => { const x = helpers.number(ctx, "x", _x); diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts index 60510c9a3..11055303c 100644 --- a/src/ScriptEditor/NetscriptDefinitions.d.ts +++ b/src/ScriptEditor/NetscriptDefinitions.d.ts @@ -4821,8 +4821,9 @@ export interface GoAnalysis { * * @param boardState - The initial board state to use for the new game, in the format used by getBoardState(). * @param komi - Optional komi value to set for the game. Defaults to 5.5. + * @param nextPlayerIsWhite - Optional. Whether or not the next player to play is the white player. Defaults to false. */ - setTestingBoardState(boardState: string[], komi?: number): void; + setTestingBoardState(boardState: string[], komi?: number, nextPlayerIsWhite?: boolean): void; /** * Adds a colored circle indicator to the specified point. These indicators are removed once a move is played. diff --git a/src/utils/APIBreaks/3.0.0.ts b/src/utils/APIBreaks/3.0.0.ts index d00369718..a4609f9d3 100644 --- a/src/utils/APIBreaks/3.0.0.ts +++ b/src/utils/APIBreaks/3.0.0.ts @@ -521,5 +521,15 @@ export const breakingChanges300: VersionBreakingChange = { 'gets the generated contract with a new optional parameter. Your code was migrated to specify "home" as the host.', showWarning: false, }, + { + brokenAPIs: [ + { name: "ns.go.analysis.getValidMoves" }, + { name: "ns.go.analysis.getChains" }, + { name: "ns.go.analysis.getLiberties" }, + { name: "ns.go.analysis.getControlledEmptyNodes" }, + ], + info: "ns.go.analysis methods no longer apply captures to custom board states passed to them, and instead evaluate the given board exactly as-is.", + showWarning: false, + }, ], }; diff --git a/test/jest/Go/NetscriptGo.test.ts b/test/jest/Go/NetscriptGo.test.ts index 53c97911b..43e7af714 100644 --- a/test/jest/Go/NetscriptGo.test.ts +++ b/test/jest/Go/NetscriptGo.test.ts @@ -25,6 +25,7 @@ import { import { getNewBoardState, getNewBoardStateFromSimpleBoard } from "../../../src/Go/boardState/boardState"; import { installAugmentations } from "../../../src/Augmentation/AugmentationHelpers"; import { getMockedNetscriptContext, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities"; +import { NetscriptGo } from "../../../src/NetscriptFunctions/Go"; initGameEnvironment(); @@ -155,20 +156,52 @@ describe("Netscript Go API unit tests", () => { ]); }); + it("should correctly find available moves for a given board when playing as white", () => { + const boardState = ["XOX..", "X.X.X", ".X..X", "...XX", "..XOO"]; + const mockNetscriptContext = getMockedNetscriptContext(); + + const result = NetscriptGo().analysis.getValidMoves(mockNetscriptContext)(boardState, null, true); + + expect(result).toEqual([ + [false, false, false, true, true], + [false, false, false, true, false], + [true, false, true, true, false], + [true, true, true, false, false], + [true, true, false, false, false], + ]); + }); + + it("should correctly find available moves for a given board when playing as white and given a prior board", () => { + const boardState = ["#..##", ".....", "...O.", ".....", "....."]; + const mockNetscriptContext = getMockedNetscriptContext(); + + const result = NetscriptGo().analysis.getValidMoves(mockNetscriptContext)(boardState, boardState, true); + + expect(result).toEqual([ + [false, true, true, false, false], + [true, true, true, true, true], + [true, true, true, false, true], + [true, true, true, true, true], + [true, true, true, true, true], + ]); + }); + it("should return all valid and invalid moves on the board, if a board is provided", () => { const currentBoard = [".....", ".....", ".....", ".....", "....."]; Go.currentGame = boardStateFromSimpleBoard(currentBoard, GoOpponent.Daedalus, GoColor.white); resetAI(); const board = getNewBoardStateFromSimpleBoard( - ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"], - ["XXO.#", "XO.O.", ".OOO.", "XXXXX", "X.X.X"], + ["..O.#", ".O.O.", ".OOOO", "XXXXX", "X.X.X"], + undefined, + GoOpponent.Netburners, + GoColor.white, ); const result = getValidMoves(board); expect(result).toEqual([ - [false, false, false, false, false], - [false, false, false, false, false], + [true, true, false, false, false], + [true, false, false, false, false], [true, false, false, false, false], [false, false, false, false, false], [false, true, false, true, false], @@ -207,6 +240,21 @@ describe("Netscript Go API unit tests", () => { [3, -1, 3, -1, 3], ]); }); + + it("should show zero liberties for groups that would be captured and -1 for empty spaces or offline nodes", () => { + const boardState = [".XXX#", "XOOOX", "XOXOX", "XOOOX", "XXXX."]; + const mockNetscriptContext = getMockedNetscriptContext(); + + const result = NetscriptGo().analysis.getLiberties(mockNetscriptContext)(boardState); + + expect(result).toEqual([ + [-1, 1, 1, 1, -1], + [2, 0, 0, 0, 1], + [2, 0, 0, 0, 1], + [2, 0, 0, 0, 1], + [2, 2, 2, 2, -1], + ]); + }); }); describe("getControlledEmptyNodes() unit tests", () => { it("should show the owner of each empty node, if a single player has fully encircled it", () => { @@ -230,6 +278,35 @@ describe("Netscript Go API unit tests", () => { expect(result).toEqual(["...O#", "..O.O", "?....", ".....", ".X.X."]); }); }); + describe("setTestingBoardState() tests", () => { + it("should set the board to the requested state", () => { + const board = ["OXX..", ".....", ".....", ".....", "....."]; + Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); + resetAI(); + + NetscriptGo().analysis.setTestingBoardState(mockCtx)(["XOX..", "X.X.X", ".X..X", "...XX", "..XOO"], null, true); + + const newBoard = simpleBoardFromBoard(Go.currentGame.board); + expect(newBoard).toEqual(["XOX..", "X.X.X", ".X..X", "...XX", "..X.."]); + expect(Go.currentGame.previousPlayer).toEqual(GoColor.black); + expect(Go.currentGame.komiOverride).toEqual(5.5); + expect(Go.currentGame.ai).toEqual(GoOpponent.none); + }); + + it("should set the board to the requested state, and set the last played color correctly", () => { + const board = ["OXX..", ".....", ".....", ".....", "....."]; + Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); + resetAI(); + + NetscriptGo().analysis.setTestingBoardState(mockCtx)(["XOX..", "X.X.X", ".X..X", "...XX", "..XOO"], 13); + + const newBoard = simpleBoardFromBoard(Go.currentGame.board); + expect(newBoard).toEqual(["XOX..", "X.X.X", ".X..X", "...XX", "..X.."]); + expect(Go.currentGame.previousPlayer).toEqual(GoColor.white); + expect(Go.currentGame.komiOverride).toEqual(13); + expect(Go.currentGame.ai).toEqual(GoOpponent.none); + }); + }); describe("cheatPlayTwoMoves() tests", () => { it("should handle invalid moves", () => { const board = ["XOO..", ".....", ".....", ".....", "....."];