import { Player } from "@player"; import { AugmentationName, GoColor, GoOpponent, GoPlayType } from "@enums"; import { Go } from "../../../src/Go/Go"; import { boardStateFromSimpleBoard, simpleBoardFromBoard, updatedBoardFromSimpleBoard, } from "../../../src/Go/boardAnalysis/boardAnalysis"; import { resetAI } from "../../../src/Go/boardAnalysis/goAI"; import { cheatPlayTwoMoves, cheatRemoveRouter, cheatRepairOfflineNode, cheatSuccessChance, getChains, getControlledEmptyNodes, getGameState, getLiberties, getValidMoves, handlePassTurn, makePlayerMove, resetBoardState, validateMove, } from "../../../src/Go/effects/netscriptGoImplementation"; 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(); beforeEach(() => { setupBasicTestingEnvironment(); }); const mockLogger: (s: string) => void = jest.fn(); const mockCtx = getMockedNetscriptContext((_: string, txt: () => string) => { mockLogger(txt()); }); describe("Netscript Go API unit tests", () => { describe("makeMove() tests", () => { it("should handle invalid moves", () => { const board = ["XOO..", ".....", ".....", ".....", "....."]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); expect(() => makePlayerMove(mockCtx, 0, 0)).toThrow( "Invalid move: 0 0. That node is already occupied by a piece.", ); }); it("should update the board with valid player moves", async () => { const board = ["OXX..", ".....", ".....", ".....", "....."]; const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardState; resetAI(); await makePlayerMove(mockCtx, 1, 0); expect(mockLogger).toHaveBeenCalledWith("Go move played: 1, 0"); expect(boardState.board[1]?.[0]?.color).toEqual(GoColor.black); expect(boardState.board[0]?.[0]?.color).toEqual(GoColor.empty); }); }); describe("passTurn() tests", () => { it("should handle pass attempts", async () => { Go.currentGame = getNewBoardState(7); resetAI(); const result = await handlePassTurn(mockCtx); expect(result.type).toEqual(GoPlayType.move); }); }); describe("getBoardState() tests", () => { it("should correctly return a string version of the board state", () => { const board = ["OXX..", ".....", ".....", ".....", "..###"]; const boardState = boardStateFromSimpleBoard(board); const result = simpleBoardFromBoard(boardState.board); expect(result).toEqual(board); }); }); describe("getGameState() tests", () => { it("should correctly retrieve the current game state", () => { const board = ["OXX..", ".....", "..#..", "...XX", "...X."]; const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.black); boardState.previousBoards = ["OX.........#.....XX...X."]; Go.currentGame = boardState; resetAI(); const result = getGameState(); expect(result).toEqual({ currentPlayer: GoColor.white, whiteScore: 6.5, blackScore: 6, previousMove: [0, 2], bonusCycles: 0, komi: 5.5, }); }); }); describe("resetBoardState() tests", () => { it("should set the player's board to the requested size and opponent", () => { const board = ["OXX..", ".....", ".....", ".....", "..###"]; Go.currentGame = boardStateFromSimpleBoard(board); resetAI(); const newBoard = resetBoardState(mockCtx, GoOpponent.SlumSnakes, 9); expect(newBoard?.[0].length).toEqual(9); expect(Go.currentGame.board.length).toEqual(9); expect(Go.currentGame.ai).toEqual(GoOpponent.SlumSnakes); expect(mockLogger).toHaveBeenCalledWith(`New game started: ${GoOpponent.SlumSnakes}, 9x9`); }); it("should throw an error if an invalid opponent is requested", () => { const board = ["OXX..", ".....", ".....", ".....", "..###"]; Go.currentGame = boardStateFromSimpleBoard(board); resetAI(); expect(() => resetBoardState(mockCtx, GoOpponent.w0r1d_d43m0n, 9)).toThrow( `Invalid opponent requested (${GoOpponent.w0r1d_d43m0n}), this opponent has not yet been discovered`, ); }); it("should throw an error if an invalid size is requested", () => { const board = ["OXX..", ".....", ".....", ".....", "..###"]; Go.currentGame = boardStateFromSimpleBoard(board); resetAI(); expect(() => resetBoardState(mockCtx, GoOpponent.TheBlackHand, 31337)).toThrow( "Invalid subnet size requested (31337), size must be 5, 7, 9, or 13", ); }); }); describe("getValidMoves() unit tests", () => { it("should return all valid and invalid moves on the board", () => { const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); const result = getValidMoves(); expect(result).toEqual([ [false, false, false, false, false], [false, false, false, false, false], [true, false, false, false, false], [false, false, false, false, false], [false, true, false, true, false], ]); }); 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( ["..O.#", ".O.O.", ".OOOO", "XXXXX", "X.X.X"], undefined, GoOpponent.Netburners, GoColor.white, ); const result = getValidMoves(board); expect(result).toEqual([ [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], ]); }); }); describe("getChains() unit tests", () => { it("should assign an ID to all contiguous chains on the board", () => { const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); const result = getChains(); expect(result[4][0]).toEqual(result[3][4]); expect(result[2][1]).toEqual(result[1][3]); expect(result[0][0]).toEqual(result[1][0]); expect(result[0][4]).toEqual(null); }); }); describe("getLiberties() unit tests", () => { it("should display the number of connected empty nodes for each chain on the board", () => { const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); const result = getLiberties(); expect(result).toEqual([ [1, 1, 2, -1, -1], [1, 4, -1, 4, -1], [-1, 4, 4, 4, 4], [3, 3, 3, 3, 3], [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", () => { const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); const result = getControlledEmptyNodes(); expect(result).toEqual(["...O#", "..O.O", "?....", ".....", ".X.X."]); }); it("should show the details for the given board, if provided", () => { const currentBoard = [".....", ".....", ".....", ".....", "....."]; Go.currentGame = boardStateFromSimpleBoard(currentBoard, GoOpponent.Daedalus, GoColor.white); resetAI(); const board = updatedBoardFromSimpleBoard(["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"]); const result = getControlledEmptyNodes(board); 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..", ".....", ".....", ".....", "....."]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); expect(() => validateMove(mockCtx, 0, 0, "playTwoMoves", { repeat: false, suicide: false, }), ).toThrow("The point 0,0 is occupied by a router, so you cannot place a router there"); }); it("should update the board with both player moves if nodes are unoccupied and cheat is successful", async () => { const board = ["OXX..", ".....", ".....", ".....", "....O"]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); await cheatPlayTwoMoves(mockCtx, 4, 3, 3, 4, 0, 0); expect(mockLogger).toHaveBeenCalledWith("Cheat successful. Two go moves played: 4,3 and 3,4"); expect(Go.currentGame.board[4]?.[3]?.color).toEqual(GoColor.black); expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.black); expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.empty); }); it("should pass player turn to AI if the cheat is unsuccessful but player is not ejected", async () => { const board = ["OXX..", ".....", ".....", ".....", "....O"]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); await cheatPlayTwoMoves(mockCtx, 4, 3, 3, 4, 2, 1); expect(mockLogger).toHaveBeenCalledWith("Cheat failed. Your turn has been skipped."); expect(Go.currentGame.board[4]?.[3]?.color).toEqual(GoColor.empty); expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.empty); expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.white); }); it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => { const board = ["OXX..", ".....", ".....", ".....", "....O"]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); Go.currentGame.cheatCount = 1; await cheatPlayTwoMoves(mockCtx, 4, 3, 3, 4, 1, 0); expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet."); expect(Go.currentGame.previousBoards).toEqual([]); }); }); describe("cheatRemoveRouter() tests", () => { it("should handle invalid moves", () => { const board = ["XOO..", ".....", ".....", ".....", "....."]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); expect(() => validateMove(mockCtx, 1, 0, "removeRouter", { emptyNode: false, requireNonEmptyNode: true, repeat: false, suicide: false, }), ).toThrow("The point 1,0 does not have a router on it, so you cannot clear this point with removeRouter()."); }); it("should remove the router if the move is valid", async () => { const board = ["XOO..", ".....", ".....", ".....", "....."]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); await cheatRemoveRouter(mockCtx, 0, 0, 0, 0); expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 0,0 was cleared."); expect(Go.currentGame.board[0][0]?.color).toEqual(GoColor.empty); }); it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => { const board = ["OXX..", ".....", ".....", ".....", "....O"]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); Go.currentGame.cheatCount = 1; await cheatRemoveRouter(mockCtx, 0, 0, 1, 0); expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet."); expect(Go.currentGame.previousBoards).toEqual([]); }); }); describe("cheatRepairOfflineNode() tests", () => { it("should handle invalid moves", () => { const board = ["XOO..", ".....", ".....", ".....", "....#"]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); expect(() => validateMove(mockCtx, 0, 0, "repairOfflineNode", { emptyNode: false, repeat: false, onlineNode: false, requireOfflineNode: true, suicide: false, }), ).toThrow("The node 0,0 is not offline, so you cannot repair the node."); }); it("should update the board with the repaired node if the cheat is successful", async () => { const board = ["OXX..", ".....", ".....", ".....", "....#"]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); resetAI(); await cheatRepairOfflineNode(mockCtx, 4, 4, 0, 0); expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 4,4 was repaired."); expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.empty); }); }); describe("Cheat success chance unit tests", () => { it("should have a base chance", () => { expect(cheatSuccessChance(0)).toEqual(0.6); }); it("should have a scaled chance based on cheat count", () => { expect(cheatSuccessChance(4)).toEqual(0.6 * (0.7 - 0.08) ** 4); }); it("should have a scaled chance based on layer cheat success level", () => { Player.setBitNodeNumber(13); Player.queueAugmentation(AugmentationName.BrachiBlades); Player.queueAugmentation(AugmentationName.GrapheneBrachiBlades); Player.queueAugmentation(AugmentationName.INFRARet); Player.queueAugmentation(AugmentationName.PCMatrix); Player.queueAugmentation(AugmentationName.NeuroFluxGovernor); installAugmentations(); expect(cheatSuccessChance(4)).toEqual(0.6 * (0.7 - 0.08) ** 4 * Player.mults.crime_success); }); }); });