IPVGO: Add support to netscript API for game state, current player, and alternate ways to check/wait on AI turn (#1142)

This commit is contained in:
Michael Ficocelli 2024-03-19 14:07:15 -04:00 committed by GitHub
parent 6aaeb6b59e
commit d81358c80f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 541 additions and 308 deletions

@ -6,7 +6,7 @@
"packages": { "packages": {
"": { "": {
"name": "bitburner", "name": "bitburner",
"version": "2.5.2", "version": "2.6.1",
"dependencies": { "dependencies": {
"electron-log": "^4.4.8", "electron-log": "^4.4.8",
"electron-store": "^8.1.0", "electron-store": "^8.1.0",

@ -15,10 +15,9 @@ cheat: {
x: number, x: number,
y: number, y: number,
): Promise<{ ): Promise<{
type: "invalid" | "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number; x: number | null;
y: number; y: number | null;
success: boolean;
}>; }>;
playTwoMoves( playTwoMoves(
x1: number, x1: number,
@ -26,30 +25,27 @@ cheat: {
x2: number, x2: number,
y2: number, y2: number,
): Promise<{ ): Promise<{
type: "invalid" | "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number; x: number | null;
y: number; y: number | null;
success: boolean;
}>; }>;
repairOfflineNode( repairOfflineNode(
x: number, x: number,
y: number, y: number,
): Promise<{ ): Promise<{
type: "invalid" | "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number; x: number | null;
y: number; y: number | null;
success: boolean;
}>; }>;
destroyNode( destroyNode(
x: number, x: number,
y: number, y: number,
): Promise<{ ): Promise<{
type: "invalid" | "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number; x: number | null;
y: number; y: number | null;
success: boolean;
}>; }>;
}; };
``` ```

@ -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: For example, a 5x5 board might look like this:
``` <pre lang="javascript"> \[ "XX.O.", "X..OO", ".XO..", "XXO.\#", ".XO.\#", \] </pre>
[
"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. Each string represents a vertical column on the board, and each character in the string represents a point.
Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index \[1\]\[0\]. Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index \[1\]\[0\].

@ -0,0 +1,19 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Go](./bitburner.go.md) &gt; [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"

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Go](./bitburner.go.md) &gt; [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; }

@ -13,10 +13,9 @@ makeMove(
x: number, x: number,
y: number, y: number,
): Promise<{ ): Promise<{
type: "invalid" | "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number; x: number | null;
y: number; y: number | null;
success: boolean;
}>; }>;
``` ```
@ -29,7 +28,7 @@ makeMove(
**Returns:** **Returns:**
Promise&lt;{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }&gt; Promise&lt;{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }&gt;
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 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

@ -17,25 +17,18 @@ export interface Go
| Property | Modifiers | Type | Description | | Property | Modifiers | Type | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| [analysis](./bitburner.go.analysis.md) | | { getValidMoves(): boolean\[\]\[\]; getChains(): (number \| null)\[\]\[\]; getLiberties(): number\[\]\[\]; getControlledEmptyNodes(): string\[\]; } | Tools to analyze the IPvGO subnet. | | [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&lt;{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }&gt;; playTwoMoves( x1: number, y1: number, x2: number, y2: number, ): Promise&lt;{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }&gt;; repairOfflineNode( x: number, y: number, ): Promise&lt;{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }&gt;; destroyNode( x: number, y: number, ): Promise&lt;{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }&gt;; } | 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&lt;{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }&gt;; playTwoMoves( x1: number, y1: number, x2: number, y2: number, ): Promise&lt;{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }&gt;; repairOfflineNode( x: number, y: number, ): Promise&lt;{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }&gt;; destroyNode( x: number, y: number, ): Promise&lt;{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }&gt;; } | Illicit and dangerous IPvGO tools. Not for the faint of heart. Requires Bitnode 14.2 to use. |
## Methods ## Methods
| Method | Description | | Method | Description |
| --- | --- | | --- | --- |
| [getBoardState()](./bitburner.go.getboardstate.md) | <p>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.)</p><p>For example, a 5x5 board might look like this:</p> | [getBoardState()](./bitburner.go.getboardstate.md) | <p>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.)</p><p>For example, a 5x5 board might look like this:</p><p><pre lang="javascript"> \[ "XX.O.", "X..OO", ".XO..", "XXO.\#", ".XO.\#", \] </pre></p><p>Each string represents a vertical column on the board, and each character in the string represents a point.</p><p>Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index \[1\]\[0\].</p><p>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.</p> |
``` | [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. |
"XX.O.",
"X..OO",
".XO..",
"XXO.#",
".XO.#",
]
```
<p>Each string represents a vertical column on the board, and each character in the string represents a point.</p><p>Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index \[1\]\[0\].</p><p>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.</p> |
| [getOpponent()](./bitburner.go.getopponent.md) | Returns the name of the opponent faction in the current subnet. | | [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. | | [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) | <p>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.</p><p>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.</p> | | [passTurn()](./bitburner.go.passturn.md) | <p>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.</p><p>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.</p> |
| [resetBoardState(opponent, boardSize)](./bitburner.go.resetboardstate.md) | <p>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.</p><p>Note that some factions will have a few routers on the subnet at this state.</p><p>opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Daedalus" or "Illuminati",</p> | | [resetBoardState(opponent, boardSize)](./bitburner.go.resetboardstate.md) | <p>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.</p><p>Note that some factions will have a few routers on the subnet at this state.</p><p>opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Daedalus" or "Illuminati",</p> |

@ -0,0 +1,34 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Go](./bitburner.go.md) &gt; [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&lt;{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }&gt;
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

@ -12,15 +12,14 @@ This can also be used if you pick up the game in a state where the opponent need
```typescript ```typescript
passTurn(): Promise<{ passTurn(): Promise<{
type: "invalid" | "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number; x: number | null;
y: number; y: number | null;
success: boolean;
}>; }>;
``` ```
**Returns:** **Returns:**
Promise&lt;{ type: "invalid" \| "move" \| "pass" \| "gameOver"; x: number; y: number; success: boolean; }&gt; Promise&lt;{ type: "move" \| "pass" \| "gameOver"; x: number \| null; y: number \| null; }&gt;
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 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

@ -97,8 +97,8 @@ Both `makeMove()` and `passTurn()` , when awaited, return an object that tells y
success: boolean; success: boolean;
// If the opponent moved or passed, or if the game is now over, or if your move was invalid // If the opponent moved or passed, or if the game is now over, or if your move was invalid
type: "invalid" | "move" | "pass" | "gameOver"; type: "invalid" | "move" | "pass" | "gameOver";
x: number; // Opponent move's x coord (if applicable) x: number | null; // Opponent move's x coord (if applicable)
y: number; // Opponent move's y 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); result = await ns.go.makeMove(x, y);
} }
// Log opponent's next move, once it happens
await ns.go.opponentNextTurn();
await ns.sleep(200); await ns.sleep(200);
// Keep looping as long as the opponent is playing moves // Keep looping as long as the opponent is playing moves
} while (result?.type !== "gameOver"); } 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: add a loop to keep playing
// TODO: reset board, e.g. `ns.go.resetBoardState("Netburners", 7)` // TODO: reset board, e.g. `ns.go.resetBoardState("Netburners", 7)`
} }

@ -27,7 +27,6 @@ export enum GoValidity {
} }
export enum GoPlayType { export enum GoPlayType {
invalid = "invalid",
move = "move", move = "move",
pass = "pass", pass = "pass",
gameOver = "gameOver", gameOver = "gameOver",

@ -1,5 +1,5 @@
import type { GoOpponent } from "./Enums"; import type { GoOpponent } from "@enums";
import type { BoardState, OpponentStats } from "./Types"; import type { BoardState, OpponentStats, Play } from "./Types";
import { getRecordValues, PartialRecord } from "../Types/Record"; import { getRecordValues, PartialRecord } from "../Types/Record";
import { getNewBoardState } from "./boardState/boardState"; import { getNewBoardState } from "./boardState/boardState";
@ -10,6 +10,7 @@ export class GoObject {
previousGame: BoardState | null = null; previousGame: BoardState | null = null;
currentGame: BoardState = getNewBoardState(7); currentGame: BoardState = getNewBoardState(7);
stats: PartialRecord<GoOpponent, OpponentStats> = {}; stats: PartialRecord<GoOpponent, OpponentStats> = {};
nextTurn: Promise<Play> | null = null;
prestigeAugmentation() { prestigeAugmentation() {
for (const stats of getRecordValues(this.stats)) { for (const stats of getRecordValues(this.stats)) {

@ -52,10 +52,9 @@ export type PointState = {
}; };
export type Play = { export type Play = {
success: boolean;
type: GoPlayType; type: GoPlayType;
x: number; x: number | null;
y: number; y: number | null;
}; };
export type Neighbor = { export type Neighbor = {

@ -96,14 +96,14 @@ function handleNoMoveFound(boardState: BoardState, player: GoColor) {
if (remainingTerritory > 0 && boardState.passCount < 2) { if (remainingTerritory > 0 && boardState.passCount < 2) {
return { return {
type: GoPlayType.pass, type: GoPlayType.pass,
x: -1, x: null,
y: -1, y: null,
}; };
} else { } else {
return { return {
type: GoPlayType.gameOver, type: GoPlayType.gameOver,
x: -1, x: null,
y: -1, y: null,
}; };
} }
} }

@ -1,7 +1,7 @@
import type { Board, BoardState, PointState } from "../Types"; import type { Board, BoardState, PointState } from "../Types";
import { Player } from "@player"; import { Player } from "@player";
import { GoOpponent, GoColor } from "@enums"; import { GoOpponent, GoColor, GoPlayType } from "@enums";
import { newOpponentStats } from "../Constants"; import { newOpponentStats } from "../Constants";
import { getAllChains, getPlayerNeighbors } from "./boardAnalysis"; import { getAllChains, getPlayerNeighbors } from "./boardAnalysis";
import { getKomi } from "./goAI"; import { getKomi } from "./goAI";
@ -46,6 +46,13 @@ export function endGoGame(boardState: BoardState) {
if (boardState.previousPlayer === null) { if (boardState.previousPlayer === null) {
return; return;
} }
Go.nextTurn = Promise.resolve({
type: GoPlayType.gameOver,
x: null,
y: null,
success: true,
});
boardState.previousPlayer = null; boardState.previousPlayer = null;
const statusToUpdate = getOpponentStats(boardState.ai); const statusToUpdate = getOpponentStats(boardState.ai);
statusToUpdate.favor = statusToUpdate.favor ?? 0; statusToUpdate.favor = statusToUpdate.favor ?? 0;

@ -5,87 +5,194 @@ import { AugmentationName, GoColor, GoOpponent, GoPlayType, GoValidity } from "@
import { Go, GoEvents } from "../Go"; import { Go, GoEvents } from "../Go";
import { getMove, sleep } from "../boardAnalysis/goAI"; import { getMove, sleep } from "../boardAnalysis/goAI";
import { getNewBoardState, makeMove, passTurn, updateCaptures, updateChains } from "../boardState/boardState"; 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 { getScore, resetWinstreak } from "../boardAnalysis/scoring";
import { WorkerScript } from "../../Netscript/WorkerScript"; import { WorkerScript } from "../../Netscript/WorkerScript";
import { WHRNG } from "../../Casino/RNG"; 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) * 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) { export async function handlePassTurn(logger: (s: string) => void) {
passTurn(Go.currentGame, GoColor.black); passTurn(Go.currentGame, GoColor.black);
logger("Go turn passed.");
if (Go.currentGame.previousPlayer === null) { if (Go.currentGame.previousPlayer === null) {
logEndGame(logger); logEndGame(logger);
return Promise.resolve({ return getOpponentNextMove(false, logger);
type: GoPlayType.gameOver, } else {
x: -1, return getAIMove(Go.currentGame);
y: -1,
success: true,
});
} }
return getAIMove(logger, Go.currentGame);
} }
/** /**
* Validates and applies the player's router placement * 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 boardState = Go.currentGame;
const validity = evaluateIfMoveIsValid(boardState, x, y, GoColor.black); const validity = evaluateIfMoveIsValid(boardState, x, y, GoColor.black);
const moveWasMade = makeMove(boardState, x, y, GoColor.black); const moveWasMade = makeMove(boardState, x, y, GoColor.black);
if (validity !== GoValidity.valid || !moveWasMade) { if (validity !== GoValidity.valid || !moveWasMade) {
await sleep(500); error(`Invalid move: ${x} ${y}. ${validity}.`);
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);
} }
GoEvents.emit(); GoEvents.emit();
logger(`Go move played: ${x}, ${y}`); logger(`Go move played: ${x}, ${y}`);
const response = getAIMove(logger, boardState); return getAIMove(boardState);
await sleep(300); }
return response;
/**
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 * 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<Play> { export async function getAIMove(boardState: BoardState): Promise<Play> {
let resolve: (value: Play) => void; let resolve: (value: Play) => void;
const aiMoveResult = new Promise<Play>((res) => { Go.nextTurn = new Promise<Play>((res) => {
resolve = res; resolve = res;
}); });
getMove(boardState, GoColor.white, Go.currentGame.ai).then(async (result) => { getMove(boardState, GoColor.white, Go.currentGame.ai).then(async (result) => {
// If a new game has started while this async code ran, drop it if (result.type === GoPlayType.pass) {
if (boardState !== Go.currentGame) { passTurn(Go.currentGame, GoColor.white);
return resolve({ ...result, success: false });
} }
if (result.type === "gameOver") {
logEndGame(logger); // 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) {
if (result.type !== GoPlayType.move) { return resolve(result);
return resolve({ ...result, success });
} }
await sleep(400); await sleep(400);
const aiUpdatedBoard = makeMove(boardState, result.x, result.y, GoColor.white); const aiUpdatedBoard = makeMove(boardState, result.x, result.y, GoColor.white);
// Handle the AI breaking. This shouldn't ever happen.
if (!aiUpdatedBoard) { if (!aiUpdatedBoard) {
boardState.previousPlayer = GoColor.white; boardState.previousPlayer = GoColor.white;
logger(`Invalid AI move attempted: ${result.x}, ${result.y}. This should not happen.`); console.error(`Invalid AI move attempted: ${result.x}, ${result.y}. This should not happen.`);
} else {
logger(`Opponent played move: ${result.x}, ${result.y}`);
}
GoEvents.emit(); GoEvents.emit();
resolve({ ...result, success }); return resolve(result);
}
await sleep(300);
GoEvents.emit();
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 * 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. * 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(); callback();
state.cheatCount++; state.cheatCount++;
GoEvents.emit(); 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 // 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) { 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); resetBoardState(logger, state.ai, state.board[0].length);
return { return {
type: GoPlayType.gameOver, type: GoPlayType.gameOver,
x: -1, x: null,
y: -1, y: null,
success: false,
}; };
} }
// If the cheat fails, your turn is skipped // If the cheat fails, your turn is skipped
@ -251,7 +404,7 @@ export async function determineCheatSuccess(
logger(`Cheat failed. Your turn has been skipped.`); logger(`Cheat failed. Your turn has been skipped.`);
passTurn(state, GoColor.black, false); passTurn(state, GoColor.black, false);
state.cheatCount++; state.cheatCount++;
return getAIMove(logger, state, false); return getAIMove(state);
} }
} }
@ -279,15 +432,7 @@ export function cheatRemoveRouter(
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: number, ejectRngOverride?: number,
) { ) {
const point = Go.currentGame.board[x][y]; 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;
}
return determineCheatSuccess( return determineCheatSuccess(
logger, logger,
() => { () => {
@ -313,24 +458,8 @@ export function cheatPlayTwoMoves(
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: number, ejectRngOverride?: number,
) { ) {
const point1 = Go.currentGame.board[x1][y1]; const point1 = Go.currentGame.board[x1][y1]!;
if (!point1) { const point2 = Go.currentGame.board[x2][y2]!;
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;
}
return determineCheatSuccess( return determineCheatSuccess(
logger, logger,
@ -354,12 +483,6 @@ export function cheatRepairOfflineNode(
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: 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( return determineCheatSuccess(
logger, logger,
() => { () => {
@ -386,16 +509,6 @@ export function cheatDestroyNode(
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: 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( return determineCheatSuccess(
logger, logger,
() => { () => {

@ -7,7 +7,6 @@ import { GoColor, GoOpponent, GoPlayType, GoValidity, ToastVariant } from "@enum
import { Go, GoEvents } from "../Go"; import { Go, GoEvents } from "../Go";
import { SnackbarEvents } from "../../ui/React/Snackbar"; import { SnackbarEvents } from "../../ui/React/Snackbar";
import { getNewBoardState, getStateCopy, makeMove, passTurn, updateCaptures } from "../boardState/boardState"; import { getNewBoardState, getStateCopy, makeMove, passTurn, updateCaptures } from "../boardState/boardState";
import { getMove } from "../boardAnalysis/goAI";
import { bitverseArt, weiArt } from "../boardState/asciiArt"; import { bitverseArt, weiArt } from "../boardState/asciiArt";
import { getScore, resetWinstreak } from "../boardAnalysis/scoring"; import { getScore, resetWinstreak } from "../boardAnalysis/scoring";
import { boardFromSimpleBoard, evaluateIfMoveIsValid, getAllValidMoves } from "../boardAnalysis/boardAnalysis"; import { boardFromSimpleBoard, evaluateIfMoveIsValid, getAllValidMoves } from "../boardAnalysis/boardAnalysis";
@ -19,6 +18,7 @@ import { GoScoreModal } from "./GoScoreModal";
import { GoGameboard } from "./GoGameboard"; import { GoGameboard } from "./GoGameboard";
import { GoSubnetSearch } from "./GoSubnetSearch"; import { GoSubnetSearch } from "./GoSubnetSearch";
import { CorruptableText } from "../../ui/React/CorruptableText"; import { CorruptableText } from "../../ui/React/CorruptableText";
import { getAIMove } from "../effects/netscriptGoImplementation";
interface GoGameboardWrapperProps { interface GoGameboardWrapperProps {
showInstructions: () => void; showInstructions: () => void;
@ -119,10 +119,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
async function takeAiTurn(boardState: BoardState) { async function takeAiTurn(boardState: BoardState) {
setWaitingOnAI(true); setWaitingOnAI(true);
const move = await getMove(boardState, GoColor.white, opponent); const move = await getAIMove(boardState);
// If a new game has started while this async code ran, just drop it
if (boardState !== Go.currentGame) return;
if (move.type === GoPlayType.pass) { if (move.type === GoPlayType.pass) {
SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000); 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); setScoreOpen(true);
return; return;
} }
setWaitingOnAI(false);
const didUpdateBoard = makeMove(boardState, move.x, move.y, GoColor.white);
if (didUpdateBoard) setWaitingOnAI(false);
} }
function newSubnet() { function newSubnet() {

@ -15,8 +15,8 @@ export const GoScoreSummaryTable = ({ score, opponent }: GoScoreSummaryTableProp
const classes = boardStyles(); const classes = boardStyles();
const blackScore = score[GoColor.black]; const blackScore = score[GoColor.black];
const whiteScore = score[GoColor.white]; const whiteScore = score[GoColor.white];
const blackPlayerName = opponent === GoOpponent.none ? "Black" : "You"; const blackPlayerName = opponent === GoOpponent.none ? GoColor.black : "You";
const whitePlayerName = opponent === GoOpponent.none ? "White" : opponent; const whitePlayerName = opponent === GoOpponent.none ? GoColor.white : opponent;
return ( return (
<> <>

@ -249,7 +249,10 @@ const go = {
makeMove: 4, makeMove: 4,
passTurn: 0, passTurn: 0,
getBoardState: 4, getBoardState: 4,
getCurrentPlayer: 0,
getGameState: 0,
getOpponent: 0, getOpponent: 0,
opponentNextTurn: 0,
resetBoardState: 0, resetBoardState: 0,
analysis: { analysis: {
getValidMoves: 8, getValidMoves: 8,

@ -2,7 +2,6 @@ import type { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
import type { Go as NSGo } from "@nsdefs"; import type { Go as NSGo } from "@nsdefs";
import type { Play } from "../Go/Types"; import type { Play } from "../Go/Types";
import { GoColor } from "@enums";
import { Go } from "../Go/Go"; import { Go } from "../Go/Go";
import { helpers } from "../Netscript/NetscriptHelpers"; import { helpers } from "../Netscript/NetscriptHelpers";
import { simpleBoardFromBoard } from "../Go/boardAnalysis/boardAnalysis"; import { simpleBoardFromBoard } from "../Go/boardAnalysis/boardAnalysis";
@ -15,36 +14,23 @@ import {
checkCheatApiAccess, checkCheatApiAccess,
getChains, getChains,
getControlledEmptyNodes, getControlledEmptyNodes,
getCurrentPlayer,
getGameState,
getLiberties, getLiberties,
getOpponentNextMove,
getValidMoves, getValidMoves,
handlePassTurn, handlePassTurn,
invalidMoveResponse,
makePlayerMove, makePlayerMove,
resetBoardState, resetBoardState,
throwError, throwError,
validateMove,
validateTurn,
} from "../Go/effects/netscriptGoImplementation"; } from "../Go/effects/netscriptGoImplementation";
import { getEnumHelper } from "../utils/EnumHelper"; import { getEnumHelper } from "../utils/EnumHelper";
const logger = (ctx: NetscriptContext) => (message: string) => helpers.log(ctx, () => message); const logger = (ctx: NetscriptContext) => (message: string) => helpers.log(ctx, () => message);
const error = (ctx: NetscriptContext) => (message: string) => throwError(ctx.workerScript, 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 * Go API implementation
*/ */
@ -52,24 +38,29 @@ export function NetscriptGo(): InternalAPI<NSGo> {
return { return {
makeMove: makeMove:
(ctx: NetscriptContext) => (ctx: NetscriptContext) =>
async (_x, _y): Promise<Play> => { (_x, _y): Promise<Play> => {
const x = helpers.number(ctx, "x", _x); const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y); const y = helpers.number(ctx, "y", _y);
validateRowAndColumn(ctx, x, y); validateMove(error(ctx), x, y, "makeMove");
return makePlayerMove(logger(ctx), error(ctx), x, y);
return makePlayerMove(logger(ctx), x, y);
}, },
passTurn: (ctx: NetscriptContext) => async (): Promise<Play> => { passTurn: (ctx: NetscriptContext) => (): Promise<Play> => {
if (Go.currentGame.previousPlayer === GoColor.black) { validateTurn(error(ctx), "passTurn()");
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);
}
return handlePassTurn(logger(ctx)); return handlePassTurn(logger(ctx));
}, },
opponentNextTurn: (ctx: NetscriptContext) => (_logOpponentMove) => {
const logOpponentMove = typeof _logOpponentMove === "boolean" ? _logOpponentMove : true;
return getOpponentNextMove(logOpponentMove, logger(ctx));
},
getBoardState: () => () => { getBoardState: () => () => {
return simpleBoardFromBoard(Go.currentGame.board); return simpleBoardFromBoard(Go.currentGame.board);
}, },
getCurrentPlayer: () => () => {
return getCurrentPlayer();
},
getGameState: () => () => {
return getGameState();
},
getOpponent: () => () => { getOpponent: () => () => {
return Go.currentGame.ai; return Go.currentGame.ai;
}, },
@ -100,44 +91,65 @@ export function NetscriptGo(): InternalAPI<NSGo> {
}, },
removeRouter: removeRouter:
(ctx: NetscriptContext) => (ctx: NetscriptContext) =>
async (_x, _y): Promise<Play> => { (_x, _y): Promise<Play> => {
checkCheatApiAccess(error(ctx)); checkCheatApiAccess(error(ctx));
const x = helpers.number(ctx, "x", _x); const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y); 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); return cheatRemoveRouter(logger(ctx), x, y);
}, },
playTwoMoves: playTwoMoves:
(ctx: NetscriptContext) => (ctx: NetscriptContext) =>
async (_x1, _y1, _x2, _y2): Promise<Play> => { (_x1, _y1, _x2, _y2): Promise<Play> => {
checkCheatApiAccess(error(ctx)); checkCheatApiAccess(error(ctx));
const x1 = helpers.number(ctx, "x", _x1); const x1 = helpers.number(ctx, "x", _x1);
const y1 = helpers.number(ctx, "y", _y1); 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 x2 = helpers.number(ctx, "x", _x2);
const y2 = helpers.number(ctx, "y", _y2); 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); return cheatPlayTwoMoves(logger(ctx), x1, y1, x2, y2);
}, },
repairOfflineNode: repairOfflineNode:
(ctx: NetscriptContext) => (ctx: NetscriptContext) =>
async (_x, _y): Promise<Play> => { (_x, _y): Promise<Play> => {
checkCheatApiAccess(error(ctx)); checkCheatApiAccess(error(ctx));
const x = helpers.number(ctx, "x", _x); const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y); 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); return cheatRepairOfflineNode(logger(ctx), x, y);
}, },
destroyNode: destroyNode:
(ctx: NetscriptContext) => (ctx: NetscriptContext) =>
async (_x, _y): Promise<Play> => { (_x, _y): Promise<Play> => {
checkCheatApiAccess(error(ctx)); checkCheatApiAccess(error(ctx));
const x = helpers.number(ctx, "x", _x); const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y); 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); return cheatDestroyNode(logger(ctx), x, y);
}, },

@ -1058,9 +1058,6 @@ export type SleeveTask =
| SleeveSupportTask | SleeveSupportTask
| SleeveSynchroTask; | SleeveSynchroTask;
/** @public */
type GoOpponent = "Netburners" | "Slum Snakes" | "The Black Hand" | "Tetrads" | "Daedalus" | "Illuminati";
/** Object representing a port. A port is a serialized queue. /** Object representing a port. A port is a serialized queue.
* @public */ * @public */
export interface NetscriptPort { export interface NetscriptPort {
@ -3923,6 +3920,9 @@ export interface Gang {
nextUpdate(): Promise<number>; nextUpdate(): Promise<number>;
} }
/** @public */
type GoOpponent = "Netburners" | "Slum Snakes" | "The Black Hand" | "Tetrads" | "Daedalus" | "Illuminati";
/** /**
* IPvGO api * IPvGO api
* @public * @public
@ -3941,10 +3941,9 @@ export interface Go {
x: number, x: number,
y: number, y: number,
): Promise<{ ): Promise<{
type: "invalid" | "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number; x: number | null;
y: number; y: number | null;
success: boolean;
}>; }>;
/** /**
@ -3961,10 +3960,26 @@ export interface Go {
* *
*/ */
passTurn(): Promise<{ passTurn(): Promise<{
type: "invalid" | "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number; x: number | null;
y: number; y: number | null;
success: boolean; }>;
/**
* 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.) * "#" 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: * For example, a 5x5 board might look like this:
``` *
[ * <pre lang="javascript">
"XX.O.", * [
"X..OO", * "XX.O.",
".XO..", * "X..OO",
"XXO.#", * ".XO..",
".XO.#", * "XXO.#",
] * ".XO.#",
``` * ]
* </pre>
* *
* Each string represents a vertical column on the board, and each character in the string represents a point. * 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[]; 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. * 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. * 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. * 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 validMoves = ns.go.analysis.getValidMoves(); *
* `const moveIsValid = validMoves[x][y];`
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 * 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 * 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 * 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. * 2 and 3 on the right, and a large chain 0 taking up the center of the board.
* * <pre lang="javascript">
``` * [
[ * [ 0,0,0,3,4],
[ 0,0,0,3,4], * [ 1,0,0,3,3],
[ 1,0,0,3,3], * [ 1,1,0,0,0],
[ 1,1,0,0,0], * [null,1,0,2,2],
[null,1,0,2,2], * [null,1,0,2,5],
[null,1,0,2,5], * ]
] * </pre>
```
* @remarks * @remarks
* RAM cost: 16 GB * RAM cost: 16 GB
* (This is intentionally expensive; you can derive this info from just getBoardState() ) * (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 * 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! * in the center touches four. The group in the bottom-right only has one liberty; it is in danger of being captured!
* *
``` * <pre lang="javascript">
[ * [
[-1, 5,-1,-1, 2], * [-1, 5,-1,-1, 2],
[ 5, 5,-1,-1,-1], * [ 5, 5,-1,-1,-1],
[-1,-1, 4,-1,-1], * [-1,-1, 4,-1,-1],
[ 3,-1,-1, 3, 1], * [ 3,-1,-1, 3, 1],
[ 3,-1,-1, 3, 1], * [ 3,-1,-1, 3, 1],
] * ]
``` * </pre>
* *
* @remarks * @remarks
* RAM cost: 16 GB * RAM cost: 16 GB
@ -4096,15 +4127,16 @@ export interface Go {
* Filled points of any color are indicated with '.' * 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: * 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:
``` *
[ * <pre lang="javascript">
"OO..?", * [
"OO.?.", * "OO..?",
"O.?.X", * "OO.?.",
".?.XX", * "O.?.X",
"?..X#", * ".?.XX",
] * "?..X#",
``` * ]
* </pre>
* *
* @remarks * @remarks
* RAM cost: 16 GB * RAM cost: 16 GB
@ -4147,10 +4179,9 @@ export interface Go {
x: number, x: number,
y: number, y: number,
): Promise<{ ): Promise<{
type: "invalid" | "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number; x: number | null;
y: number; y: number | null;
success: boolean;
}>; }>;
/** /**
* Attempts to place two routers at once on empty nodes. Note that this ignores other move restrictions, so you can * 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, x2: number,
y2: number, y2: number,
): Promise<{ ): Promise<{
type: "invalid" | "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number; x: number | null;
y: number; y: number | null;
success: boolean;
}>; }>;
/** /**
@ -4197,10 +4227,9 @@ export interface Go {
x: number, x: number,
y: number, y: number,
): Promise<{ ): Promise<{
type: "invalid" | "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number; x: number | null;
y: number; y: number | null;
success: boolean;
}>; }>;
/** /**
@ -4222,10 +4251,9 @@ export interface Go {
x: number, x: number,
y: number, y: number,
): Promise<{ ): Promise<{
type: "invalid" | "move" | "pass" | "gameOver"; type: "move" | "pass" | "gameOver";
x: number; x: number | null;
y: number; y: number | null;
success: boolean;
}>; }>;
}; };
} }

@ -8,12 +8,13 @@ import {
cheatRepairOfflineNode, cheatRepairOfflineNode,
getChains, getChains,
getControlledEmptyNodes, getControlledEmptyNodes,
getGameState,
getLiberties, getLiberties,
getValidMoves, getValidMoves,
handlePassTurn, handlePassTurn,
invalidMoveResponse,
makePlayerMove, makePlayerMove,
resetBoardState, resetBoardState,
validateMove,
} from "../../../src/Go/effects/netscriptGoImplementation"; } from "../../../src/Go/effects/netscriptGoImplementation";
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject"; import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
import "../../../src/Faction/Factions"; import "../../../src/Faction/Factions";
@ -31,11 +32,13 @@ describe("Netscript Go API unit tests", () => {
const board = ["XOO..", ".....", ".....", ".....", "....."]; const board = ["XOO..", ".....", ".....", ".....", "....."];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
const mockLogger = jest.fn(); 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(mockError).toHaveBeenCalledWith("Invalid move: 0 0. That node is already occupied by a piece.");
expect(mockLogger).toHaveBeenCalledWith("ERROR: Invalid move: That node is already occupied by a piece");
}); });
it("should update the board with valid player moves", async () => { 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); const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
Go.currentGame = boardState; Go.currentGame = boardState;
const mockLogger = jest.fn(); 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(mockLogger).toHaveBeenCalledWith("Go move played: 1, 0");
expect(boardState.board[1]?.[0]?.color).toEqual(GoColor.black); expect(boardState.board[1]?.[0]?.color).toEqual(GoColor.black);
expect(boardState.board[0]?.[0]?.color).toEqual(GoColor.empty); expect(boardState.board[0]?.[0]?.color).toEqual(GoColor.empty);
@ -59,7 +62,6 @@ describe("Netscript Go API unit tests", () => {
const result = await handlePassTurn(mockLogger); const result = await handlePassTurn(mockLogger);
expect(result.success).toEqual(true);
expect(result.type).toEqual(GoPlayType.move); 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", () => { describe("resetBoardState() tests", () => {
it("should set the player's board to the requested size and opponent", () => { it("should set the player's board to the requested size and opponent", () => {
const board = ["OXX..", ".....", ".....", ".....", "..###"]; const board = ["OXX..", ".....", ".....", ".....", "..###"];
@ -170,12 +190,14 @@ describe("Netscript Go API unit tests", () => {
it("should handle invalid moves", async () => { it("should handle invalid moves", async () => {
const board = ["XOO..", ".....", ".....", ".....", "....."]; const board = ["XOO..", ".....", ".....", ".....", "....."];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
const mockLogger = jest.fn(); const mockError = jest.fn();
validateMove(mockError, 0, 0, "playTwoMoves", {
const result = await cheatPlayTwoMoves(mockLogger, 0, 0, 1, 0, 0, 0); repeat: false,
suicide: false,
expect(result).toEqual(invalidMoveResponse); });
expect(mockLogger).toHaveBeenCalledWith("The point 0,0 is not empty, so you cannot place a router there."); 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 () => { 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); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
const mockLogger = jest.fn(); 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(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[4]?.[3]?.color).toEqual(GoColor.black);
expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.black); expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.black);
expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.empty); 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); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
const mockLogger = jest.fn(); 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(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[4]?.[3]?.color).toEqual(GoColor.empty);
expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.empty); expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.empty);
expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.white); expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.white);
@ -210,9 +230,8 @@ describe("Netscript Go API unit tests", () => {
Go.currentGame.cheatCount = 1; Go.currentGame.cheatCount = 1;
const mockLogger = jest.fn(); 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(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
expect(result.success).toEqual(false);
expect(Go.currentGame.previousBoard).toEqual(null); expect(Go.currentGame.previousBoard).toEqual(null);
}); });
}); });
@ -220,12 +239,14 @@ describe("Netscript Go API unit tests", () => {
it("should handle invalid moves", async () => { it("should handle invalid moves", async () => {
const board = ["XOO..", ".....", ".....", ".....", "....."]; const board = ["XOO..", ".....", ".....", ".....", "....."];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
const mockLogger = jest.fn(); const mockError = jest.fn();
validateMove(mockError, 1, 0, "removeRouter", {
const result = await cheatRemoveRouter(mockLogger, 1, 0, 0, 0); emptyNode: false,
requireNonEmptyNode: true,
expect(result).toEqual(invalidMoveResponse); repeat: false,
expect(mockLogger).toHaveBeenCalledWith( suicide: false,
});
expect(mockError).toHaveBeenCalledWith(
"The point 1,0 does not have a router on it, so you cannot clear this point with removeRouter().", "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); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
const mockLogger = jest.fn(); 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(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 0,0 was cleared.");
expect(Go.currentGame.board[0][0]?.color).toEqual(GoColor.empty); expect(Go.currentGame.board[0][0]?.color).toEqual(GoColor.empty);
}); });
@ -248,9 +268,8 @@ describe("Netscript Go API unit tests", () => {
Go.currentGame.cheatCount = 1; Go.currentGame.cheatCount = 1;
const mockLogger = jest.fn(); 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(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
expect(result.success).toEqual(false);
expect(Go.currentGame.previousBoard).toEqual(null); expect(Go.currentGame.previousBoard).toEqual(null);
}); });
}); });
@ -258,12 +277,16 @@ describe("Netscript Go API unit tests", () => {
it("should handle invalid moves", async () => { it("should handle invalid moves", async () => {
const board = ["XOO..", ".....", ".....", ".....", "....#"]; const board = ["XOO..", ".....", ".....", ".....", "....#"];
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white); 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(mockError).toHaveBeenCalledWith("The node 0,0 is not offline, so you cannot repair the node.");
expect(result).toEqual(invalidMoveResponse);
expect(mockLogger).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 () => { 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); Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
const mockLogger = jest.fn(); 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(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); expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.empty);
}); });
}); });