From d81358c80f1ef37321b7ef13dc1ba3855d57905c Mon Sep 17 00:00:00 2001 From: Michael Ficocelli Date: Tue, 19 Mar 2024 14:07:15 -0400 Subject: [PATCH] IPVGO: Add support to netscript API for game state, current player, and alternate ways to check/wait on AI turn (#1142) --- electron/package-lock.json | 2 +- markdown/bitburner.go.cheat.md | 28 +- markdown/bitburner.go.getboardstate.md | 11 +- markdown/bitburner.go.getcurrentplayer.md | 19 ++ markdown/bitburner.go.getgamestate.md | 22 ++ markdown/bitburner.go.makemove.md | 9 +- markdown/bitburner.go.md | 17 +- markdown/bitburner.go.opponentnextturn.md | 34 ++ markdown/bitburner.go.passturn.md | 9 +- .../doc/programming/go_algorithms.md | 10 +- src/Go/Enums.ts | 1 - src/Go/Go.ts | 5 +- src/Go/Types.ts | 5 +- src/Go/boardAnalysis/goAI.ts | 8 +- src/Go/boardAnalysis/scoring.ts | 9 +- src/Go/effects/netscriptGoImplementation.ts | 293 ++++++++++++------ src/Go/ui/GoGameboardWrapper.tsx | 12 +- src/Go/ui/GoScoreSummaryTable.tsx | 4 +- src/Netscript/RamCostGenerator.ts | 3 + src/NetscriptFunctions/Go.ts | 88 +++--- src/ScriptEditor/NetscriptDefinitions.d.ts | 166 +++++----- test/jest/Go/NetscriptGo.test.ts | 94 +++--- 22 files changed, 541 insertions(+), 308 deletions(-) create mode 100644 markdown/bitburner.go.getcurrentplayer.md create mode 100644 markdown/bitburner.go.getgamestate.md create mode 100644 markdown/bitburner.go.opponentnextturn.md diff --git a/electron/package-lock.json b/electron/package-lock.json index 91bea6b61..36e63c273 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "bitburner", - "version": "2.5.2", + "version": "2.6.1", "dependencies": { "electron-log": "^4.4.8", "electron-store": "^8.1.0", diff --git a/markdown/bitburner.go.cheat.md b/markdown/bitburner.go.cheat.md index 530cc1e5e..973a2ff3d 100644 --- a/markdown/bitburner.go.cheat.md +++ b/markdown/bitburner.go.cheat.md @@ -15,10 +15,9 @@ cheat: { x: number, y: number, ): Promise<{ - type: "invalid" | "move" | "pass" | "gameOver"; - x: number; - y: number; - success: boolean; + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; }>; playTwoMoves( x1: number, @@ -26,30 +25,27 @@ cheat: { x2: number, y2: number, ): Promise<{ - type: "invalid" | "move" | "pass" | "gameOver"; - x: number; - y: number; - success: boolean; + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; }>; repairOfflineNode( x: number, y: number, ): Promise<{ - type: "invalid" | "move" | "pass" | "gameOver"; - x: number; - y: number; - success: boolean; + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; }>; destroyNode( x: number, y: number, ): Promise<{ - type: "invalid" | "move" | "pass" | "gameOver"; - x: number; - y: number; - success: boolean; + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; }>; }; ``` diff --git a/markdown/bitburner.go.getboardstate.md b/markdown/bitburner.go.getboardstate.md index dd99f3005..ed84ad0d5 100644 --- a/markdown/bitburner.go.getboardstate.md +++ b/markdown/bitburner.go.getboardstate.md @@ -8,15 +8,8 @@ Retrieves a simplified version of the board state. "X" represents black pieces, For example, a 5x5 board might look like this: -``` - [ - "XX.O.", - "X..OO", - ".XO..", - "XXO.#", - ".XO.#", - ] -``` +
 \[ "XX.O.", "X..OO", ".XO..", "XXO.\#", ".XO.\#", \] 
+ 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\]. diff --git a/markdown/bitburner.go.getcurrentplayer.md b/markdown/bitburner.go.getcurrentplayer.md new file mode 100644 index 000000000..03bb8d61d --- /dev/null +++ b/markdown/bitburner.go.getcurrentplayer.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [bitburner](./bitburner.md) > [Go](./bitburner.go.md) > [getCurrentPlayer](./bitburner.go.getcurrentplayer.md) + +## Go.getCurrentPlayer() method + +Returns the color of the current player, or 'None' if the game is over. + +**Signature:** + +```typescript +getCurrentPlayer(): "White" | "Black" | "None"; +``` +**Returns:** + +"White" \| "Black" \| "None" + +"White" \| "Black" \| "None" + diff --git a/markdown/bitburner.go.getgamestate.md b/markdown/bitburner.go.getgamestate.md new file mode 100644 index 000000000..56bb4016e --- /dev/null +++ b/markdown/bitburner.go.getgamestate.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [bitburner](./bitburner.md) > [Go](./bitburner.go.md) > [getGameState](./bitburner.go.getgamestate.md) + +## Go.getGameState() method + +Gets the status of the current game. Shows the current player, current score, and the previous move coordinates. Previous move coordinates will be \[-1, -1\] for a pass, or if there are no prior moves. + +**Signature:** + +```typescript +getGameState(): { + currentPlayer: "White" | "Black" | "None"; + whiteScore: number; + blackScore: number; + previousMove: [number, number] | null; + }; +``` +**Returns:** + +{ currentPlayer: "White" \| "Black" \| "None"; whiteScore: number; blackScore: number; previousMove: \[number, number\] \| null; } + diff --git a/markdown/bitburner.go.makemove.md b/markdown/bitburner.go.makemove.md index eb336d7ad..cd229be4c 100644 --- a/markdown/bitburner.go.makemove.md +++ b/markdown/bitburner.go.makemove.md @@ -13,10 +13,9 @@ makeMove( x: number, y: number, ): Promise<{ - type: "invalid" | "move" | "pass" | "gameOver"; - x: number; - y: number; - success: boolean; + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; }>; ``` @@ -29,7 +28,7 @@ makeMove( **Returns:** -Promise<{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }> +Promise<{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }> a promise that contains if your move was valid and successful, the opponent move's x and y coordinates (or pass) in response, or an indication if the game has ended diff --git a/markdown/bitburner.go.md b/markdown/bitburner.go.md index af3341053..fc5dea287 100644 --- a/markdown/bitburner.go.md +++ b/markdown/bitburner.go.md @@ -17,25 +17,18 @@ export interface Go | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [analysis](./bitburner.go.analysis.md) | | { getValidMoves(): boolean\[\]\[\]; getChains(): (number \| null)\[\]\[\]; getLiberties(): number\[\]\[\]; getControlledEmptyNodes(): string\[\]; } | Tools to analyze the IPvGO subnet. | -| [cheat](./bitburner.go.cheat.md) | | { getCheatSuccessChance(): number; removeRouter( x: number, y: number, ): Promise<{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }>; playTwoMoves( x1: number, y1: number, x2: number, y2: number, ): Promise<{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }>; repairOfflineNode( x: number, y: number, ): Promise<{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }>; destroyNode( x: number, y: number, ): Promise<{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }>; } | Illicit and dangerous IPvGO tools. Not for the faint of heart. Requires Bitnode 14.2 to use. | +| [cheat](./bitburner.go.cheat.md) | | { getCheatSuccessChance(): number; removeRouter( x: number, y: number, ): Promise<{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }>; playTwoMoves( x1: number, y1: number, x2: number, y2: number, ): Promise<{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }>; repairOfflineNode( x: number, y: number, ): Promise<{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }>; destroyNode( x: number, y: number, ): Promise<{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }>; } | Illicit and dangerous IPvGO tools. Not for the faint of heart. Requires Bitnode 14.2 to use. | ## Methods | Method | Description | | --- | --- | -| [getBoardState()](./bitburner.go.getboardstate.md) |

Retrieves a simplified version of the board state. "X" represents black pieces, "O" white, and "." empty points. "\#" are dead nodes that are not part of the subnet. (They are not territory nor open nodes.)

For example, a 5x5 board might look like this:

-``` - [ - "XX.O.", - "X..OO", - ".XO..", - "XXO.#", - ".XO.#", - ] -``` -

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\].

Note that 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 subnet tab.

| +| [getBoardState()](./bitburner.go.getboardstate.md) |

Retrieves a simplified version of the board state. "X" represents black pieces, "O" white, and "." empty points. "\#" are dead nodes that are not part of the subnet. (They are not territory nor open nodes.)

For example, a 5x5 board might look like this:

 \[ "XX.O.", "X..OO", ".XO..", "XXO.\#", ".XO.\#", \] 

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\].

Note that 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 subnet tab.

| +| [getCurrentPlayer()](./bitburner.go.getcurrentplayer.md) | Returns the color of the current player, or 'None' if the game is over. | +| [getGameState()](./bitburner.go.getgamestate.md) | Gets the status of the current game. Shows the current player, current score, and the previous move coordinates. Previous move coordinates will be \[-1, -1\] for a pass, or if there are no prior moves. | | [getOpponent()](./bitburner.go.getopponent.md) | Returns the name of the opponent faction in the current subnet. | | [makeMove(x, y)](./bitburner.go.makemove.md) | Make a move on the IPvGO subnet gameboard, and await the opponent's response. x:0 y:0 represents the bottom-left corner of the board in the UI. | +| [opponentNextTurn(logOpponentMove)](./bitburner.go.opponentnextturn.md) | Returns a promise that resolves with the success or failure state of your last move, and the AI's response, if applicable. x:0 y:0 represents the bottom-left corner of the board in the UI. | | [passTurn()](./bitburner.go.passturn.md) |

Pass the player's turn rather than making a move, and await the opponent's response. This ends the game if the opponent passed on the previous turn, or if the opponent passes on their following turn.

This can also be used if you pick up the game in a state where the opponent needs to play next. For example: if BitBurner was closed while waiting for the opponent to make a move, you may need to call passTurn() to get them to play their move on game start.

| | [resetBoardState(opponent, boardSize)](./bitburner.go.resetboardstate.md) |

Gets new IPvGO subnet with the specified size owned by the listed faction, ready for the player to make a move. This will reset your win streak if the current game is not complete and you have already made moves.

Note that some factions will have a few routers on the subnet at this state.

opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Daedalus" or "Illuminati",

| diff --git a/markdown/bitburner.go.opponentnextturn.md b/markdown/bitburner.go.opponentnextturn.md new file mode 100644 index 000000000..d3b1b2630 --- /dev/null +++ b/markdown/bitburner.go.opponentnextturn.md @@ -0,0 +1,34 @@ + + +[Home](./index.md) > [bitburner](./bitburner.md) > [Go](./bitburner.go.md) > [opponentNextTurn](./bitburner.go.opponentnextturn.md) + +## Go.opponentNextTurn() method + +Returns a promise that resolves with the success or failure state of your last move, and the AI's response, if applicable. x:0 y:0 represents the bottom-left corner of the board in the UI. + +**Signature:** + +```typescript +opponentNextTurn(logOpponentMove: boolean = true): Promise<{ + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; + }>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| logOpponentMove | boolean | _(Optional)_ optional, if false prevents logging opponent move | + +**Returns:** + +Promise<{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }> + +a promise that contains if your last move was valid and successful, the opponent move's x and y coordinates (or pass) in response, or an indication if the game has ended + +## Remarks + +RAM cost: 0 GB + diff --git a/markdown/bitburner.go.passturn.md b/markdown/bitburner.go.passturn.md index ea742ff42..5bedddaf1 100644 --- a/markdown/bitburner.go.passturn.md +++ b/markdown/bitburner.go.passturn.md @@ -12,15 +12,14 @@ This can also be used if you pick up the game in a state where the opponent need ```typescript passTurn(): Promise<{ - type: "invalid" | "move" | "pass" | "gameOver"; - x: number; - y: number; - success: boolean; + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; }>; ``` **Returns:** -Promise<{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }> +Promise<{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }> a promise that contains if your move was valid and successful, the opponent move's x and y coordinates (or pass) in response, or an indication if the game has ended diff --git a/src/Documentation/doc/programming/go_algorithms.md b/src/Documentation/doc/programming/go_algorithms.md index c9a660536..8d7e75d4a 100644 --- a/src/Documentation/doc/programming/go_algorithms.md +++ b/src/Documentation/doc/programming/go_algorithms.md @@ -97,8 +97,8 @@ Both `makeMove()` and `passTurn()` , when awaited, return an object that tells y success: boolean; // If the opponent moved or passed, or if the game is now over, or if your move was invalid type: "invalid" | "move" | "pass" | "gameOver"; - x: number; // Opponent move's x coord (if applicable) - y: number; // Opponent move's y coord (if applicable) + x: number | null; // Opponent move's x coord (if applicable) + y: number | null; // Opponent move's y coord (if applicable) } ``` @@ -130,14 +130,14 @@ export async function main(ns) { result = await ns.go.makeMove(x, y); } + // Log opponent's next move, once it happens + await ns.go.opponentNextTurn(); + await ns.sleep(200); // Keep looping as long as the opponent is playing moves } while (result?.type !== "gameOver"); - // After the opponent passes, end the game by passing as well - await ns.go.passTurn(); - // TODO: add a loop to keep playing // TODO: reset board, e.g. `ns.go.resetBoardState("Netburners", 7)` } diff --git a/src/Go/Enums.ts b/src/Go/Enums.ts index 7a2d4b63d..250966cc3 100644 --- a/src/Go/Enums.ts +++ b/src/Go/Enums.ts @@ -27,7 +27,6 @@ export enum GoValidity { } export enum GoPlayType { - invalid = "invalid", move = "move", pass = "pass", gameOver = "gameOver", diff --git a/src/Go/Go.ts b/src/Go/Go.ts index ece1fb52d..53360beb9 100644 --- a/src/Go/Go.ts +++ b/src/Go/Go.ts @@ -1,5 +1,5 @@ -import type { GoOpponent } from "./Enums"; -import type { BoardState, OpponentStats } from "./Types"; +import type { GoOpponent } from "@enums"; +import type { BoardState, OpponentStats, Play } from "./Types"; import { getRecordValues, PartialRecord } from "../Types/Record"; import { getNewBoardState } from "./boardState/boardState"; @@ -10,6 +10,7 @@ export class GoObject { previousGame: BoardState | null = null; currentGame: BoardState = getNewBoardState(7); stats: PartialRecord = {}; + nextTurn: Promise | null = null; prestigeAugmentation() { for (const stats of getRecordValues(this.stats)) { diff --git a/src/Go/Types.ts b/src/Go/Types.ts index 59712ffc7..8201746d5 100644 --- a/src/Go/Types.ts +++ b/src/Go/Types.ts @@ -52,10 +52,9 @@ export type PointState = { }; export type Play = { - success: boolean; type: GoPlayType; - x: number; - y: number; + x: number | null; + y: number | null; }; export type Neighbor = { diff --git a/src/Go/boardAnalysis/goAI.ts b/src/Go/boardAnalysis/goAI.ts index b5df25751..e771f593a 100644 --- a/src/Go/boardAnalysis/goAI.ts +++ b/src/Go/boardAnalysis/goAI.ts @@ -96,14 +96,14 @@ function handleNoMoveFound(boardState: BoardState, player: GoColor) { if (remainingTerritory > 0 && boardState.passCount < 2) { return { type: GoPlayType.pass, - x: -1, - y: -1, + x: null, + y: null, }; } else { return { type: GoPlayType.gameOver, - x: -1, - y: -1, + x: null, + y: null, }; } } diff --git a/src/Go/boardAnalysis/scoring.ts b/src/Go/boardAnalysis/scoring.ts index 6468868e8..436f29945 100644 --- a/src/Go/boardAnalysis/scoring.ts +++ b/src/Go/boardAnalysis/scoring.ts @@ -1,7 +1,7 @@ import type { Board, BoardState, PointState } from "../Types"; import { Player } from "@player"; -import { GoOpponent, GoColor } from "@enums"; +import { GoOpponent, GoColor, GoPlayType } from "@enums"; import { newOpponentStats } from "../Constants"; import { getAllChains, getPlayerNeighbors } from "./boardAnalysis"; import { getKomi } from "./goAI"; @@ -46,6 +46,13 @@ export function endGoGame(boardState: BoardState) { if (boardState.previousPlayer === null) { return; } + Go.nextTurn = Promise.resolve({ + type: GoPlayType.gameOver, + x: null, + y: null, + success: true, + }); + boardState.previousPlayer = null; const statusToUpdate = getOpponentStats(boardState.ai); statusToUpdate.favor = statusToUpdate.favor ?? 0; diff --git a/src/Go/effects/netscriptGoImplementation.ts b/src/Go/effects/netscriptGoImplementation.ts index 56a913871..f474041f8 100644 --- a/src/Go/effects/netscriptGoImplementation.ts +++ b/src/Go/effects/netscriptGoImplementation.ts @@ -5,87 +5,194 @@ import { AugmentationName, GoColor, GoOpponent, GoPlayType, GoValidity } from "@ import { Go, GoEvents } from "../Go"; import { getMove, sleep } from "../boardAnalysis/goAI"; import { getNewBoardState, makeMove, passTurn, updateCaptures, updateChains } from "../boardState/boardState"; -import { evaluateIfMoveIsValid, getControlledSpace, simpleBoardFromBoard } from "../boardAnalysis/boardAnalysis"; +import { + evaluateIfMoveIsValid, + getColorOnSimpleBoard, + getControlledSpace, + simpleBoardFromBoard, +} from "../boardAnalysis/boardAnalysis"; import { getScore, resetWinstreak } from "../boardAnalysis/scoring"; import { WorkerScript } from "../../Netscript/WorkerScript"; import { WHRNG } from "../../Casino/RNG"; +/** + * Check the move based on the current settings + */ +export function validateMove(error: (s: string) => void, x: number, y: number, methodName = "", settings = {}) { + const check = { + emptyNode: true, + requireNonEmptyNode: false, + repeat: true, + onlineNode: true, + requireOfflineNode: false, + suicide: true, + ...settings, + }; + + const boardSize = Go.currentGame.board.length; + if (x < 0 || x >= boardSize) { + error(`Invalid column number (x = ${x}), column must be a number 0 through ${boardSize - 1}`); + } + if (y < 0 || y >= boardSize) { + error(`Invalid row number (y = ${y}), row must be a number 0 through ${boardSize - 1}`); + } + + const moveString = `${methodName} ${x},${y}: `; + validateTurn(error, moveString); + + const validity = evaluateIfMoveIsValid(Go.currentGame, x, y, GoColor.black); + const point = Go.currentGame.board[x][y]; + if (!point && check.onlineNode) { + error( + `The node ${x},${y} is offline, so you cannot ${ + methodName === "removeRouter" + ? "clear this point with removeRouter()" + : methodName === "destroyNode" + ? "destroy the node. (Attempted to destroyNode)" + : "place a router there" + }.`, + ); + } + if (validity === GoValidity.noSuicide && check.suicide) { + error( + `${moveString} ${validity}. That point has no neighboring empty nodes, and is not connected to a network with access to empty nodes, meaning it would be instantly captured if played there.`, + ); + } + if (validity === GoValidity.boardRepeated && check.repeat) { + error( + `${moveString} ${validity}. That move would repeat the previous board state, which is illegal as it leads to infinite loops.`, + ); + } + if (point?.color !== GoColor.empty && check.emptyNode) { + error( + `The point ${x},${y} is occupied by a router, so you cannot ${ + methodName === "destroyNode" ? "destroy this node. (Attempted to destroyNode)" : "place a router there" + }`, + ); + } + + if (point?.color === GoColor.empty && check.requireNonEmptyNode) { + error(`The point ${x},${y} does not have a router on it, so you cannot clear this point with removeRouter().`); + } + if (point && check.requireOfflineNode) { + error(`The node ${x},${y} is not offline, so you cannot repair the node.`); + } +} + +export function validateTurn(error: (s: string) => void, moveString = "") { + if (Go.currentGame.previousPlayer === GoColor.black) { + error( + `${moveString} ${GoValidity.notYourTurn}. Do you have multiple scripts running, or did you forget to await makeMove() or opponentNextTurn()`, + ); + } + if (Go.currentGame.previousPlayer === null) { + error( + `${moveString} ${GoValidity.gameOver}. You cannot make more moves. Start a new game using resetBoardState().`, + ); + } +} + /** * Pass player's turn and await the opponent's response (or logs the end of the game if both players pass) */ export async function handlePassTurn(logger: (s: string) => void) { passTurn(Go.currentGame, GoColor.black); + logger("Go turn passed."); + if (Go.currentGame.previousPlayer === null) { logEndGame(logger); - return Promise.resolve({ - type: GoPlayType.gameOver, - x: -1, - y: -1, - success: true, - }); + return getOpponentNextMove(false, logger); + } else { + return getAIMove(Go.currentGame); } - return getAIMove(logger, Go.currentGame); } /** * Validates and applies the player's router placement */ -export async function makePlayerMove(logger: (s: string) => void, x: number, y: number) { +export async function makePlayerMove(logger: (s: string) => void, error: (s: string) => void, x: number, y: number) { const boardState = Go.currentGame; const validity = evaluateIfMoveIsValid(boardState, x, y, GoColor.black); const moveWasMade = makeMove(boardState, x, y, GoColor.black); if (validity !== GoValidity.valid || !moveWasMade) { - await sleep(500); - logger(`ERROR: Invalid move: ${validity}`); - - if (validity === GoValidity.notYourTurn) { - logger("Do you have multiple scripts running, or did you forget to await makeMove() ?"); - } - - return Promise.resolve(invalidMoveResponse); + error(`Invalid move: ${x} ${y}. ${validity}.`); } GoEvents.emit(); logger(`Go move played: ${x}, ${y}`); - const response = getAIMove(logger, boardState); - await sleep(300); - return response; + return getAIMove(boardState); +} + +/** + Returns the promise that provides the opponent's move, once it finishes thinking. + */ +export async function getOpponentNextMove(logOpponentMove = true, logger: (s: string) => void) { + // Handle the case where Go.nextTurn isn't populated yet + if (!Go.nextTurn) { + const previousMove = getPreviousMove(); + const type = + Go.currentGame.previousPlayer === null ? GoPlayType.gameOver : previousMove ? GoPlayType.move : GoPlayType.pass; + + Go.nextTurn = Promise.resolve({ + type, + x: previousMove?.[0] ?? null, + y: previousMove?.[1] ?? null, + }); + } + + // Only asynchronously log the opponent move if not disabled by the player + if (logOpponentMove) { + return Go.nextTurn.then((move) => { + if (move.type === GoPlayType.gameOver) { + logEndGame(logger); + } else if (move.type === GoPlayType.pass) { + logger(`Opponent passed their turn. You can end the game by passing as well.`); + } else if (move.type === GoPlayType.move) { + logger(`Opponent played move: ${move.x}, ${move.y}`); + } + return move; + }); + } + + return Go.nextTurn; } /** * Retrieves a move from the current faction in response to the player's move */ -async function getAIMove(logger: (s: string) => void, boardState: BoardState, success = true): Promise { +export async function getAIMove(boardState: BoardState): Promise { let resolve: (value: Play) => void; - const aiMoveResult = new Promise((res) => { + Go.nextTurn = new Promise((res) => { resolve = res; }); getMove(boardState, GoColor.white, Go.currentGame.ai).then(async (result) => { - // If a new game has started while this async code ran, drop it - if (boardState !== Go.currentGame) { - return resolve({ ...result, success: false }); + if (result.type === GoPlayType.pass) { + passTurn(Go.currentGame, GoColor.white); } - if (result.type === "gameOver") { - logEndGame(logger); - } - if (result.type !== GoPlayType.move) { - return resolve({ ...result, success }); + + // If there is no move to apply, simply return the result + if (boardState !== Go.currentGame || result.type !== GoPlayType.move || result.x === null || result.y === null) { + return resolve(result); } await sleep(400); const aiUpdatedBoard = makeMove(boardState, result.x, result.y, GoColor.white); + + // Handle the AI breaking. This shouldn't ever happen. if (!aiUpdatedBoard) { boardState.previousPlayer = GoColor.white; - logger(`Invalid AI move attempted: ${result.x}, ${result.y}. This should not happen.`); - } else { - logger(`Opponent played move: ${result.x}, ${result.y}`); + console.error(`Invalid AI move attempted: ${result.x}, ${result.y}. This should not happen.`); + GoEvents.emit(); + return resolve(result); } + + await sleep(300); GoEvents.emit(); - resolve({ ...result, success }); + resolve(result); }); - return aiMoveResult; + return Go.nextTurn; } /** @@ -161,6 +268,60 @@ export function getControlledEmptyNodes() { ); } +/** + * Gets the status of the current game. + * Shows the current player, current score, and the previous move coordinates. + * Previous move coordinates will be [-1, -1] for a pass, or if there are no prior moves. + */ +export function getGameState() { + const currentPlayer = getCurrentPlayer(); + const score = getScore(Go.currentGame); + const previousMove = getPreviousMove(); + + return { + currentPlayer, + whiteScore: score[GoColor.white].sum, + blackScore: score[GoColor.black].sum, + previousMove, + }; +} + +/** + * Returns 'None' if the game is over, otherwise returns the color of the current player's turn + */ +export function getCurrentPlayer(): "None" | "White" | "Black" { + if (Go.currentGame.previousPlayer === null) { + return "None"; + } + return Go.currentGame.previousPlayer === GoColor.black ? GoColor.white : GoColor.black; +} + +/** + * Find a move made by the previous player, if present. + */ +export function getPreviousMove(): [number, number] | null { + if (Go.currentGame.passCount) { + 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) { + const point = row[+pointIndexString]; + const priorColor = point && priorBoard && getColorOnSimpleBoard(priorBoard, point.x, point.y); + const currentColor = point?.color; + const isPreviousPlayer = currentColor === Go.currentGame.previousPlayer; + const isChanged = priorColor !== currentColor; + if (priorColor && currentColor && isPreviousPlayer && isChanged) { + return [+rowIndexString, +pointIndexString]; + } + } + } + + return null; +} + /** * Handle post-game logging */ @@ -208,13 +369,6 @@ export function checkCheatApiAccess(error: (s: string) => void): void { } } -export const invalidMoveResponse: Play = { - success: false, - type: GoPlayType.invalid, - x: -1, - y: -1, -}; - /** * Determines if the attempted cheat move is successful. If so, applies the cheat via the callback, and gets the opponent's response. * @@ -233,7 +387,7 @@ export async function determineCheatSuccess( callback(); state.cheatCount++; GoEvents.emit(); - return getAIMove(logger, state, true); + return getAIMove(state); } // If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly losing else if (state.cheatCount && (ejectRngOverride ?? rng.random()) < 0.1) { @@ -241,9 +395,8 @@ export async function determineCheatSuccess( resetBoardState(logger, state.ai, state.board[0].length); return { type: GoPlayType.gameOver, - x: -1, - y: -1, - success: false, + x: null, + y: null, }; } // If the cheat fails, your turn is skipped @@ -251,7 +404,7 @@ export async function determineCheatSuccess( logger(`Cheat failed. Your turn has been skipped.`); passTurn(state, GoColor.black, false); state.cheatCount++; - return getAIMove(logger, state, false); + return getAIMove(state); } } @@ -279,15 +432,7 @@ export function cheatRemoveRouter( successRngOverride?: number, ejectRngOverride?: number, ) { - const point = Go.currentGame.board[x][y]; - if (!point) { - logger(`The node ${x},${y} is offline, so you cannot clear this point with removeRouter().`); - return invalidMoveResponse; - } - if (point.color === GoColor.empty) { - logger(`The point ${x},${y} does not have a router on it, so you cannot clear this point with removeRouter().`); - return invalidMoveResponse; - } + const point = Go.currentGame.board[x][y]!; return determineCheatSuccess( logger, () => { @@ -313,24 +458,8 @@ export function cheatPlayTwoMoves( successRngOverride?: number, ejectRngOverride?: number, ) { - const point1 = Go.currentGame.board[x1][y1]; - if (!point1) { - logger(`The node ${x1},${y1} is offline, so you cannot place a router there.`); - return invalidMoveResponse; - } - if (point1.color !== GoColor.empty) { - logger(`The point ${x1},${y1} is not empty, so you cannot place a router there.`); - return invalidMoveResponse; - } - const point2 = Go.currentGame.board[x2][y2]; - if (!point2) { - logger(`The node ${x2},${y2} is offline, so you cannot place a router there.`); - return invalidMoveResponse; - } - if (point2.color !== GoColor.empty) { - logger(`The point ${x2},${y2} is not empty, so you cannot place a router there.`); - return invalidMoveResponse; - } + const point1 = Go.currentGame.board[x1][y1]!; + const point2 = Go.currentGame.board[x2][y2]!; return determineCheatSuccess( logger, @@ -354,12 +483,6 @@ export function cheatRepairOfflineNode( successRngOverride?: number, ejectRngOverride?: number, ) { - const point = Go.currentGame.board[x][y]; - if (point) { - logger(`The node ${x},${y} is not offline, so you cannot repair the node.`); - return invalidMoveResponse; - } - return determineCheatSuccess( logger, () => { @@ -386,16 +509,6 @@ export function cheatDestroyNode( successRngOverride?: number, ejectRngOverride?: number, ) { - const point = Go.currentGame.board[x][y]; - if (!point) { - logger(`The node ${x},${y} is already offline, so you cannot destroy the node.`); - return invalidMoveResponse; - } - if (point.color !== GoColor.empty) { - logger(`The point ${x},${y} is not empty, so you cannot destroy this node.`); - return invalidMoveResponse; - } - return determineCheatSuccess( logger, () => { diff --git a/src/Go/ui/GoGameboardWrapper.tsx b/src/Go/ui/GoGameboardWrapper.tsx index ae736ba65..453964b1a 100644 --- a/src/Go/ui/GoGameboardWrapper.tsx +++ b/src/Go/ui/GoGameboardWrapper.tsx @@ -7,7 +7,6 @@ import { GoColor, GoOpponent, GoPlayType, GoValidity, ToastVariant } from "@enum import { Go, GoEvents } from "../Go"; import { SnackbarEvents } from "../../ui/React/Snackbar"; import { getNewBoardState, getStateCopy, makeMove, passTurn, updateCaptures } from "../boardState/boardState"; -import { getMove } from "../boardAnalysis/goAI"; import { bitverseArt, weiArt } from "../boardState/asciiArt"; import { getScore, resetWinstreak } from "../boardAnalysis/scoring"; import { boardFromSimpleBoard, evaluateIfMoveIsValid, getAllValidMoves } from "../boardAnalysis/boardAnalysis"; @@ -19,6 +18,7 @@ import { GoScoreModal } from "./GoScoreModal"; import { GoGameboard } from "./GoGameboard"; import { GoSubnetSearch } from "./GoSubnetSearch"; import { CorruptableText } from "../../ui/React/CorruptableText"; +import { getAIMove } from "../effects/netscriptGoImplementation"; interface GoGameboardWrapperProps { showInstructions: () => void; @@ -119,10 +119,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps async function takeAiTurn(boardState: BoardState) { setWaitingOnAI(true); - const move = await getMove(boardState, GoColor.white, opponent); - - // If a new game has started while this async code ran, just drop it - if (boardState !== Go.currentGame) return; + const move = await getAIMove(boardState); if (move.type === GoPlayType.pass) { SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000); @@ -134,10 +131,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps setScoreOpen(true); return; } - - const didUpdateBoard = makeMove(boardState, move.x, move.y, GoColor.white); - - if (didUpdateBoard) setWaitingOnAI(false); + setWaitingOnAI(false); } function newSubnet() { diff --git a/src/Go/ui/GoScoreSummaryTable.tsx b/src/Go/ui/GoScoreSummaryTable.tsx index d9bb803cb..8118780aa 100644 --- a/src/Go/ui/GoScoreSummaryTable.tsx +++ b/src/Go/ui/GoScoreSummaryTable.tsx @@ -15,8 +15,8 @@ export const GoScoreSummaryTable = ({ score, opponent }: GoScoreSummaryTableProp const classes = boardStyles(); const blackScore = score[GoColor.black]; const whiteScore = score[GoColor.white]; - const blackPlayerName = opponent === GoOpponent.none ? "Black" : "You"; - const whitePlayerName = opponent === GoOpponent.none ? "White" : opponent; + const blackPlayerName = opponent === GoOpponent.none ? GoColor.black : "You"; + const whitePlayerName = opponent === GoOpponent.none ? GoColor.white : opponent; return ( <> diff --git a/src/Netscript/RamCostGenerator.ts b/src/Netscript/RamCostGenerator.ts index 7e115cc89..aaa913993 100644 --- a/src/Netscript/RamCostGenerator.ts +++ b/src/Netscript/RamCostGenerator.ts @@ -249,7 +249,10 @@ const go = { makeMove: 4, passTurn: 0, getBoardState: 4, + getCurrentPlayer: 0, + getGameState: 0, getOpponent: 0, + opponentNextTurn: 0, resetBoardState: 0, analysis: { getValidMoves: 8, diff --git a/src/NetscriptFunctions/Go.ts b/src/NetscriptFunctions/Go.ts index 46e28e309..46bfd82a6 100644 --- a/src/NetscriptFunctions/Go.ts +++ b/src/NetscriptFunctions/Go.ts @@ -2,7 +2,6 @@ import type { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper"; import type { Go as NSGo } from "@nsdefs"; import type { Play } from "../Go/Types"; -import { GoColor } from "@enums"; import { Go } from "../Go/Go"; import { helpers } from "../Netscript/NetscriptHelpers"; import { simpleBoardFromBoard } from "../Go/boardAnalysis/boardAnalysis"; @@ -15,36 +14,23 @@ import { checkCheatApiAccess, getChains, getControlledEmptyNodes, + getCurrentPlayer, + getGameState, getLiberties, + getOpponentNextMove, getValidMoves, handlePassTurn, - invalidMoveResponse, makePlayerMove, resetBoardState, throwError, + validateMove, + validateTurn, } from "../Go/effects/netscriptGoImplementation"; import { getEnumHelper } from "../utils/EnumHelper"; const logger = (ctx: NetscriptContext) => (message: string) => helpers.log(ctx, () => message); const error = (ctx: NetscriptContext) => (message: string) => throwError(ctx.workerScript, message); -/** - * Ensures the given coordinates are valid for the current board size - */ -function validateRowAndColumn(ctx: NetscriptContext, x: number, y: number) { - const boardSize = Go.currentGame.board.length; - - if (x < 0 || x >= boardSize) { - throwError( - ctx.workerScript, - `Invalid column number (x = ${x}), column must be a number 0 through ${boardSize - 1}`, - ); - } - if (y < 0 || y >= boardSize) { - throwError(ctx.workerScript, `Invalid row number (y = ${y}), row must be a number 0 through ${boardSize - 1}`); - } -} - /** * Go API implementation */ @@ -52,24 +38,29 @@ export function NetscriptGo(): InternalAPI { return { makeMove: (ctx: NetscriptContext) => - async (_x, _y): Promise => { + (_x, _y): Promise => { const x = helpers.number(ctx, "x", _x); const y = helpers.number(ctx, "y", _y); - validateRowAndColumn(ctx, x, y); - - return makePlayerMove(logger(ctx), x, y); + validateMove(error(ctx), x, y, "makeMove"); + return makePlayerMove(logger(ctx), error(ctx), x, y); }, - passTurn: (ctx: NetscriptContext) => async (): Promise => { - if (Go.currentGame.previousPlayer === GoColor.black) { - helpers.log(ctx, () => `It is not your turn; you cannot pass.`); - helpers.log(ctx, () => `Do you have multiple scripts running, or did you forget to await makeMove() ?`); - return Promise.resolve(invalidMoveResponse); - } + passTurn: (ctx: NetscriptContext) => (): Promise => { + validateTurn(error(ctx), "passTurn()"); return handlePassTurn(logger(ctx)); }, + opponentNextTurn: (ctx: NetscriptContext) => (_logOpponentMove) => { + const logOpponentMove = typeof _logOpponentMove === "boolean" ? _logOpponentMove : true; + return getOpponentNextMove(logOpponentMove, logger(ctx)); + }, getBoardState: () => () => { return simpleBoardFromBoard(Go.currentGame.board); }, + getCurrentPlayer: () => () => { + return getCurrentPlayer(); + }, + getGameState: () => () => { + return getGameState(); + }, getOpponent: () => () => { return Go.currentGame.ai; }, @@ -100,44 +91,65 @@ export function NetscriptGo(): InternalAPI { }, removeRouter: (ctx: NetscriptContext) => - async (_x, _y): Promise => { + (_x, _y): Promise => { checkCheatApiAccess(error(ctx)); const x = helpers.number(ctx, "x", _x); const y = helpers.number(ctx, "y", _y); - validateRowAndColumn(ctx, x, y); + validateMove(error(ctx), x, y, "removeRouter", { + emptyNode: false, + requireNonEmptyNode: true, + repeat: false, + suicide: false, + }); return cheatRemoveRouter(logger(ctx), x, y); }, playTwoMoves: (ctx: NetscriptContext) => - async (_x1, _y1, _x2, _y2): Promise => { + (_x1, _y1, _x2, _y2): Promise => { checkCheatApiAccess(error(ctx)); const x1 = helpers.number(ctx, "x", _x1); const y1 = helpers.number(ctx, "y", _y1); - validateRowAndColumn(ctx, x1, y1); + validateMove(error(ctx), x1, y1, "playTwoMoves", { + repeat: false, + suicide: false, + }); const x2 = helpers.number(ctx, "x", _x2); const y2 = helpers.number(ctx, "y", _y2); - validateRowAndColumn(ctx, x2, y2); + validateMove(error(ctx), x2, y2, "playTwoMoves", { + repeat: false, + suicide: false, + }); return cheatPlayTwoMoves(logger(ctx), x1, y1, x2, y2); }, repairOfflineNode: (ctx: NetscriptContext) => - async (_x, _y): Promise => { + (_x, _y): Promise => { checkCheatApiAccess(error(ctx)); const x = helpers.number(ctx, "x", _x); const y = helpers.number(ctx, "y", _y); - validateRowAndColumn(ctx, x, y); + validateMove(error(ctx), x, y, "repairOfflineNode", { + emptyNode: false, + repeat: false, + onlineNode: false, + requireOfflineNode: true, + suicide: false, + }); return cheatRepairOfflineNode(logger(ctx), x, y); }, destroyNode: (ctx: NetscriptContext) => - async (_x, _y): Promise => { + (_x, _y): Promise => { checkCheatApiAccess(error(ctx)); const x = helpers.number(ctx, "x", _x); const y = helpers.number(ctx, "y", _y); - validateRowAndColumn(ctx, x, y); + validateMove(error(ctx), x, y, "destroyNode", { + repeat: false, + onlineNode: true, + suicide: false, + }); return cheatDestroyNode(logger(ctx), x, y); }, diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts index 9d0ed9edb..a554c9169 100644 --- a/src/ScriptEditor/NetscriptDefinitions.d.ts +++ b/src/ScriptEditor/NetscriptDefinitions.d.ts @@ -1058,9 +1058,6 @@ export type SleeveTask = | SleeveSupportTask | SleeveSynchroTask; -/** @public */ -type GoOpponent = "Netburners" | "Slum Snakes" | "The Black Hand" | "Tetrads" | "Daedalus" | "Illuminati"; - /** Object representing a port. A port is a serialized queue. * @public */ export interface NetscriptPort { @@ -3923,6 +3920,9 @@ export interface Gang { nextUpdate(): Promise; } +/** @public */ +type GoOpponent = "Netburners" | "Slum Snakes" | "The Black Hand" | "Tetrads" | "Daedalus" | "Illuminati"; + /** * IPvGO api * @public @@ -3941,10 +3941,9 @@ export interface Go { x: number, y: number, ): Promise<{ - type: "invalid" | "move" | "pass" | "gameOver"; - x: number; - y: number; - success: boolean; + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; }>; /** @@ -3961,10 +3960,26 @@ export interface Go { * */ passTurn(): Promise<{ - type: "invalid" | "move" | "pass" | "gameOver"; - x: number; - y: number; - success: boolean; + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; + }>; + + /** + * Returns a promise that resolves with the success or failure state of your last move, and the AI's response, if applicable. + * x:0 y:0 represents the bottom-left corner of the board in the UI. + * + * @param logOpponentMove optional, if false prevents logging opponent move + * + * @remarks + * RAM cost: 0 GB + * + * @returns a promise that contains if your last move was valid and successful, the opponent move's x and y coordinates (or pass) in response, or an indication if the game has ended + */ + opponentNextTurn(logOpponentMove: boolean = true): Promise<{ + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; }>; /** @@ -3972,15 +3987,16 @@ export interface Go { * "#" are dead nodes that are not part of the subnet. (They are not territory nor open nodes.) * * For example, a 5x5 board might look like this: -``` - [ - "XX.O.", - "X..OO", - ".XO..", - "XXO.#", - ".XO.#", - ] -``` + * + *
+   *    [
+   *       "XX.O.",
+   *       "X..OO",
+   *       ".XO..",
+   *       "XXO.#",
+   *       ".XO.#",
+   *    ]
+   * 
* * Each string represents a vertical column on the board, and each character in the string represents a point. * @@ -3995,6 +4011,24 @@ export interface Go { */ getBoardState(): string[]; + /** + * Returns the color of the current player, or 'None' if the game is over. + * @returns "White" | "Black" | "None" + */ + getCurrentPlayer(): "White" | "Black" | "None"; + + /** + * Gets the status of the current game. + * Shows the current player, current score, and the previous move coordinates. + * Previous move coordinates will be [-1, -1] for a pass, or if there are no prior moves. + */ + getGameState(): { + currentPlayer: "White" | "Black" | "None"; + whiteScore: number; + blackScore: number; + previousMove: [number, number] | null; + }; + /** * Returns the name of the opponent faction in the current subnet. */ @@ -4024,11 +4058,9 @@ export interface Go { * Shows if each point on the board is a valid move for the player. * * The true/false validity of each move can be retrieved via the X and Y coordinates of the move. -``` - const validMoves = ns.go.analysis.getValidMoves(); - - const moveIsValid = validMoves[x][y]; -``` + * `const validMoves = ns.go.analysis.getValidMoves();` + * + * `const moveIsValid = validMoves[x][y];` * * Note that 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 @@ -4048,16 +4080,15 @@ export interface Go { * * For example, a 5x5 board might look like this. There is a large chain #1 on the left side, smaller chains * 2 and 3 on the right, and a large chain 0 taking up the center of the board. - * -``` - [ - [ 0,0,0,3,4], - [ 1,0,0,3,3], - [ 1,1,0,0,0], - [null,1,0,2,2], - [null,1,0,2,5], - ] -``` + *
+     *       [
+     *         [   0,0,0,3,4],
+     *         [   1,0,0,3,3],
+     *         [   1,1,0,0,0],
+     *         [null,1,0,2,2],
+     *         [null,1,0,2,5],
+     *       ]
+     * 
* @remarks * RAM cost: 16 GB * (This is intentionally expensive; you can derive this info from just getBoardState() ) @@ -4072,15 +4103,15 @@ export interface Go { * For example, a 5x5 board might look like this. The chain in the top-left touches 5 total empty nodes, and the one * in the center touches four. The group in the bottom-right only has one liberty; it is in danger of being captured! * -``` - [ - [-1, 5,-1,-1, 2], - [ 5, 5,-1,-1,-1], - [-1,-1, 4,-1,-1], - [ 3,-1,-1, 3, 1], - [ 3,-1,-1, 3, 1], - ] -``` + *
+     *      [
+     *         [-1, 5,-1,-1, 2],
+     *         [ 5, 5,-1,-1,-1],
+     *         [-1,-1, 4,-1,-1],
+     *         [ 3,-1,-1, 3, 1],
+     *         [ 3,-1,-1, 3, 1],
+     *      ]
+     * 
* * @remarks * RAM cost: 16 GB @@ -4096,15 +4127,16 @@ export interface Go { * Filled points of any color are indicated with '.' * * In this example, white encircles some space in the top-left, black encircles some in the top-right, and between their routers is contested space in the center: -``` - [ - "OO..?", - "OO.?.", - "O.?.X", - ".?.XX", - "?..X#", - ] -``` + * + *
+     *   [
+     *      "OO..?",
+     *      "OO.?.",
+     *      "O.?.X",
+     *      ".?.XX",
+     *      "?..X#",
+     *   ]
+     * 
* * @remarks * RAM cost: 16 GB @@ -4147,10 +4179,9 @@ export interface Go { x: number, y: number, ): Promise<{ - type: "invalid" | "move" | "pass" | "gameOver"; - x: number; - y: number; - success: boolean; + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; }>; /** * Attempts to place two routers at once on empty nodes. Note that this ignores other move restrictions, so you can @@ -4173,10 +4204,9 @@ export interface Go { x2: number, y2: number, ): Promise<{ - type: "invalid" | "move" | "pass" | "gameOver"; - x: number; - y: number; - success: boolean; + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; }>; /** @@ -4197,10 +4227,9 @@ export interface Go { x: number, y: number, ): Promise<{ - type: "invalid" | "move" | "pass" | "gameOver"; - x: number; - y: number; - success: boolean; + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; }>; /** @@ -4222,10 +4251,9 @@ export interface Go { x: number, y: number, ): Promise<{ - type: "invalid" | "move" | "pass" | "gameOver"; - x: number; - y: number; - success: boolean; + type: "move" | "pass" | "gameOver"; + x: number | null; + y: number | null; }>; }; } diff --git a/test/jest/Go/NetscriptGo.test.ts b/test/jest/Go/NetscriptGo.test.ts index 6039c96b7..8599093cc 100644 --- a/test/jest/Go/NetscriptGo.test.ts +++ b/test/jest/Go/NetscriptGo.test.ts @@ -8,12 +8,13 @@ import { cheatRepairOfflineNode, getChains, getControlledEmptyNodes, + getGameState, getLiberties, getValidMoves, handlePassTurn, - invalidMoveResponse, makePlayerMove, resetBoardState, + validateMove, } from "../../../src/Go/effects/netscriptGoImplementation"; import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject"; import "../../../src/Faction/Factions"; @@ -31,11 +32,13 @@ describe("Netscript Go API unit tests", () => { const board = ["XOO..", ".....", ".....", ".....", "....."]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); const mockLogger = jest.fn(); + const mockError = jest.fn(() => { + throw new Error("Invalid"); + }); - const result = await makePlayerMove(mockLogger, 0, 0); + await makePlayerMove(mockLogger, mockError, 0, 0).catch((_) => _); - expect(result).toEqual(invalidMoveResponse); - expect(mockLogger).toHaveBeenCalledWith("ERROR: Invalid move: That node is already occupied by a piece"); + expect(mockError).toHaveBeenCalledWith("Invalid move: 0 0. That node is already occupied by a piece."); }); it("should update the board with valid player moves", async () => { @@ -43,10 +46,10 @@ describe("Netscript Go API unit tests", () => { const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardState; const mockLogger = jest.fn(); + const mockError = jest.fn(); - const result = await makePlayerMove(mockLogger, 1, 0); + await makePlayerMove(mockLogger, mockError, 1, 0); - expect(result.success).toEqual(true); 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); @@ -59,7 +62,6 @@ describe("Netscript Go API unit tests", () => { const result = await handlePassTurn(mockLogger); - expect(result.success).toEqual(true); expect(result.type).toEqual(GoPlayType.move); }); }); @@ -75,6 +77,24 @@ describe("Netscript Go API unit tests", () => { }); }); + describe("getGameState() 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."]; + Go.currentGame = boardState; + + const result = getGameState(); + + expect(result).toEqual({ + currentPlayer: GoColor.white, + whiteScore: 6.5, + blackScore: 6, + previousMove: [0, 2], + }); + }); + }); + describe("resetBoardState() tests", () => { it("should set the player's board to the requested size and opponent", () => { const board = ["OXX..", ".....", ".....", ".....", "..###"]; @@ -170,12 +190,14 @@ describe("Netscript Go API unit tests", () => { it("should handle invalid moves", async () => { const board = ["XOO..", ".....", ".....", ".....", "....."]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); - const mockLogger = jest.fn(); - - const result = await cheatPlayTwoMoves(mockLogger, 0, 0, 1, 0, 0, 0); - - expect(result).toEqual(invalidMoveResponse); - expect(mockLogger).toHaveBeenCalledWith("The point 0,0 is not empty, so you cannot place a router there."); + const mockError = jest.fn(); + validateMove(mockError, 0, 0, "playTwoMoves", { + repeat: false, + suicide: false, + }); + expect(mockError).toHaveBeenCalledWith( + "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 () => { @@ -183,9 +205,8 @@ describe("Netscript Go API unit tests", () => { Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); const mockLogger = jest.fn(); - const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 0, 0); + await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 0, 0); expect(mockLogger).toHaveBeenCalledWith("Cheat successful. Two go moves played: 4,3 and 3,4"); - expect(result.success).toEqual(true); 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); @@ -196,9 +217,8 @@ describe("Netscript Go API unit tests", () => { Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); const mockLogger = jest.fn(); - const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 2, 1); + await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 2, 1); expect(mockLogger).toHaveBeenCalledWith("Cheat failed. Your turn has been skipped."); - expect(result.success).toEqual(false); 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); @@ -210,9 +230,8 @@ describe("Netscript Go API unit tests", () => { Go.currentGame.cheatCount = 1; const mockLogger = jest.fn(); - const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 1, 0); + await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 1, 0); expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet."); - expect(result.success).toEqual(false); expect(Go.currentGame.previousBoard).toEqual(null); }); }); @@ -220,12 +239,14 @@ describe("Netscript Go API unit tests", () => { it("should handle invalid moves", async () => { const board = ["XOO..", ".....", ".....", ".....", "....."]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); - const mockLogger = jest.fn(); - - const result = await cheatRemoveRouter(mockLogger, 1, 0, 0, 0); - - expect(result).toEqual(invalidMoveResponse); - expect(mockLogger).toHaveBeenCalledWith( + const mockError = jest.fn(); + validateMove(mockError, 1, 0, "removeRouter", { + emptyNode: false, + requireNonEmptyNode: true, + repeat: false, + suicide: false, + }); + expect(mockError).toHaveBeenCalledWith( "The point 1,0 does not have a router on it, so you cannot clear this point with removeRouter().", ); }); @@ -235,9 +256,8 @@ describe("Netscript Go API unit tests", () => { Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); const mockLogger = jest.fn(); - const result = await cheatRemoveRouter(mockLogger, 0, 0, 0, 0); + await cheatRemoveRouter(mockLogger, 0, 0, 0, 0); - expect(result.success).toEqual(true); expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 0,0 was cleared."); expect(Go.currentGame.board[0][0]?.color).toEqual(GoColor.empty); }); @@ -248,9 +268,8 @@ describe("Netscript Go API unit tests", () => { Go.currentGame.cheatCount = 1; const mockLogger = jest.fn(); - const result = await cheatRemoveRouter(mockLogger, 0, 0, 1, 0); + await cheatRemoveRouter(mockLogger, 0, 0, 1, 0); expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet."); - expect(result.success).toEqual(false); expect(Go.currentGame.previousBoard).toEqual(null); }); }); @@ -258,12 +277,16 @@ describe("Netscript Go API unit tests", () => { it("should handle invalid moves", async () => { const board = ["XOO..", ".....", ".....", ".....", "....#"]; Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); - const mockLogger = jest.fn(); + const mockError = jest.fn(); + validateMove(mockError, 0, 0, "repairOfflineNode", { + emptyNode: false, + repeat: false, + onlineNode: false, + requireOfflineNode: true, + suicide: false, + }); - const result = await cheatRepairOfflineNode(mockLogger, 0, 0); - - expect(result).toEqual(invalidMoveResponse); - expect(mockLogger).toHaveBeenCalledWith("The node 0,0 is not offline, so you cannot repair the node."); + expect(mockError).toHaveBeenCalledWith("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 () => { @@ -271,9 +294,8 @@ describe("Netscript Go API unit tests", () => { Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); const mockLogger = jest.fn(); - const result = await cheatRepairOfflineNode(mockLogger, 4, 4, 0, 0); + await cheatRepairOfflineNode(mockLogger, 4, 4, 0, 0); expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 4,4 was repaired."); - expect(result.success).toEqual(true); expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.empty); }); });