mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-17 06:48:42 +02:00
IPVGO: Add optional board state argument to the go analysis functions (#1716)
This commit is contained in:
committed by
GitHub
parent
ecc2d92edb
commit
6df3dcdc82
@@ -645,6 +645,16 @@ export function boardFromSimpleBoard(simpleBoard: SimpleBoard): Board {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Board object from the given simpleBoard string array
|
||||
* Also updates the board object with the analytics (liberties/chains) from the simple board
|
||||
*/
|
||||
export const updatedBoardFromSimpleBoard = (simpleBoard: SimpleBoard): Board => {
|
||||
const board = boardFromSimpleBoard(simpleBoard);
|
||||
updateChains(board);
|
||||
return board;
|
||||
};
|
||||
|
||||
export function boardStateFromSimpleBoard(
|
||||
simpleBoard: SimpleBoard,
|
||||
ai = GoOpponent.Daedalus,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import type { Board, BoardState, Move, Neighbor, PointState } from "../Types";
|
||||
import { Board, BoardState, Move, Neighbor, PointState, SimpleBoard } from "../Types";
|
||||
|
||||
import { GoOpponent, GoColor, GoValidity } from "@enums";
|
||||
import { GoColor, GoOpponent, GoValidity } from "@enums";
|
||||
import { bitverseBoardShape } from "../Constants";
|
||||
import { getExpansionMoveArray } from "../boardAnalysis/goAI";
|
||||
import {
|
||||
boardFromSimpleBoard,
|
||||
boardStringFromBoard,
|
||||
evaluateIfMoveIsValid,
|
||||
findAllCapturedChains,
|
||||
findLibertiesForChain,
|
||||
getAllChains,
|
||||
boardFromSimpleBoard,
|
||||
boardStringFromBoard,
|
||||
updatedBoardFromSimpleBoard,
|
||||
} from "../boardAnalysis/boardAnalysis";
|
||||
import { endGoGame } from "../boardAnalysis/scoring";
|
||||
import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes";
|
||||
@@ -59,6 +60,32 @@ export function getNewBoardState(
|
||||
return newBoardState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new BoardState object from a given SimpleBoard string array, and an optional prior move board state
|
||||
*/
|
||||
export function getNewBoardStateFromSimpleBoard(
|
||||
simpleBoard: SimpleBoard,
|
||||
priorSimpleBoard?: SimpleBoard,
|
||||
ai: GoOpponent = GoOpponent.Netburners,
|
||||
): BoardState {
|
||||
const newState = getNewBoardState(simpleBoard.length, ai, false, updatedBoardFromSimpleBoard(simpleBoard));
|
||||
if (priorSimpleBoard) {
|
||||
newState.previousBoards.push(priorSimpleBoard.join(""));
|
||||
|
||||
// Identify the previous player based on the difference in pieces
|
||||
const priorWhitePieces = priorSimpleBoard.join("").match(/O/g)?.length ?? 0;
|
||||
const priorBlackPieces = priorSimpleBoard.join("").match(/X/g)?.length ?? 0;
|
||||
const currentWhitePieces = simpleBoard.join("").match(/O/g)?.length ?? 0;
|
||||
const currentBlackPieces = simpleBoard.join("").match(/X/g)?.length ?? 0;
|
||||
if (priorWhitePieces - priorBlackPieces > currentWhitePieces - currentBlackPieces) {
|
||||
newState.previousPlayer = GoColor.black;
|
||||
}
|
||||
}
|
||||
|
||||
updateCaptures(newState.board, newState.previousPlayer ?? GoColor.white);
|
||||
return newState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines how many starting pieces the opponent has on the board
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { Play, SimpleBoard, SimpleOpponentStats } from "../Types";
|
||||
import { Board, BoardState, Play, SimpleBoard, SimpleOpponentStats } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { AugmentationName, GoColor, GoOpponent, GoPlayType, GoValidity } from "@enums";
|
||||
import { Go, GoEvents } from "../Go";
|
||||
import { getNewBoardState, makeMove, passTurn, updateCaptures, updateChains } from "../boardState/boardState";
|
||||
import {
|
||||
getNewBoardState,
|
||||
getNewBoardStateFromSimpleBoard,
|
||||
makeMove,
|
||||
passTurn,
|
||||
updateCaptures,
|
||||
updateChains,
|
||||
} from "../boardState/boardState";
|
||||
import { makeAIMove, resetAI } from "../boardAnalysis/goAI";
|
||||
import {
|
||||
evaluateIfMoveIsValid,
|
||||
@@ -150,8 +157,8 @@ export async function getOpponentNextMove(logOpponentMove = true, logger: (s: st
|
||||
/**
|
||||
* Returns a grid of booleans indicating if the coordinates at that location are a valid move for the player (black pieces)
|
||||
*/
|
||||
export function getValidMoves() {
|
||||
const boardState = Go.currentGame;
|
||||
export function getValidMoves(_boardState?: BoardState) {
|
||||
const boardState = _boardState || Go.currentGame;
|
||||
// Map the board matrix into true/false values
|
||||
return boardState.board.map((column, x) =>
|
||||
column.reduce((validityArray: boolean[], point, y) => {
|
||||
@@ -165,10 +172,11 @@ export function getValidMoves() {
|
||||
/**
|
||||
* Returns a grid with an ID for each contiguous chain of same-state nodes (excluding dead/offline nodes)
|
||||
*/
|
||||
export function getChains() {
|
||||
export function getChains(_board?: Board) {
|
||||
const board = _board || Go.currentGame.board;
|
||||
const chains: string[] = [];
|
||||
// Turn the internal chain IDs into nice consecutive numbers for display to the player
|
||||
return Go.currentGame.board.map((column) =>
|
||||
return board.map((column) =>
|
||||
column.reduce((chainIdArray: (number | null)[], point) => {
|
||||
if (!point) {
|
||||
chainIdArray.push(null);
|
||||
@@ -186,8 +194,9 @@ export function getChains() {
|
||||
/**
|
||||
* Returns a grid of numbers representing the number of open-node connections each player-owned chain has.
|
||||
*/
|
||||
export function getLiberties() {
|
||||
return Go.currentGame.board.map((column) =>
|
||||
export function getLiberties(_board?: Board) {
|
||||
const board = _board || Go.currentGame.board;
|
||||
return board.map((column) =>
|
||||
column.reduce((libertyArray: number[], point) => {
|
||||
libertyArray.push(point?.liberties?.length || -1);
|
||||
return libertyArray;
|
||||
@@ -198,8 +207,8 @@ export function getLiberties() {
|
||||
/**
|
||||
* Returns a grid indicating which player, if any, controls the empty nodes by fully encircling it with their routers
|
||||
*/
|
||||
export function getControlledEmptyNodes() {
|
||||
const board = Go.currentGame.board;
|
||||
export function getControlledEmptyNodes(_board?: Board) {
|
||||
const board = _board || Go.currentGame.board;
|
||||
const controlled = getControlledSpace(board);
|
||||
return controlled.map((column, x: number) =>
|
||||
column.reduce((ownedPoints: string, owner: GoColor, y: number) => {
|
||||
@@ -322,6 +331,66 @@ export function getStats() {
|
||||
return statDetails;
|
||||
}
|
||||
|
||||
const boardValidity = {
|
||||
valid: "",
|
||||
badShape: "Invalid boardState: Board must be a square",
|
||||
badType: "Invalid boardState: Board must be an array of strings",
|
||||
badSize: "Invalid boardState: Board must be 5, 7, 9, 13, or 19 in size",
|
||||
badCharacters:
|
||||
'Invalid board state: unknown characters found. "X" represents black pieces, "O" white, "." empty points, and "#" offline nodes.',
|
||||
failedToCreateBoard: "Invalid board state: Failed to create board",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Validate the given SimpleBoard and prior board state (if present) and turn it into a full BoardState with updated analytics
|
||||
*/
|
||||
export function validateBoardState(
|
||||
error: (s: string) => void,
|
||||
_boardState?: unknown,
|
||||
_priorBoardState?: unknown,
|
||||
): BoardState | undefined {
|
||||
const simpleBoard = getSimpleBoardFromUnknown(error, _boardState);
|
||||
const priorSimpleBoard = getSimpleBoardFromUnknown(error, _priorBoardState);
|
||||
|
||||
if (!_boardState || !simpleBoard) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return getNewBoardStateFromSimpleBoard(simpleBoard, priorSimpleBoard);
|
||||
} catch (e) {
|
||||
error(boardValidity.failedToCreateBoard);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the given boardState is a valid SimpleBoard, and return it if it is.
|
||||
*/
|
||||
function getSimpleBoardFromUnknown(error: (arg0: string) => void, _boardState: unknown): SimpleBoard | undefined {
|
||||
if (!_boardState) {
|
||||
return undefined;
|
||||
}
|
||||
if (!Array.isArray(_boardState)) {
|
||||
error(boardValidity.badType);
|
||||
}
|
||||
if ((_boardState as unknown[]).find((row) => typeof row !== "string")) {
|
||||
error(boardValidity.badType);
|
||||
}
|
||||
|
||||
const boardState = _boardState as string[];
|
||||
|
||||
if (boardState.find((row) => row.length !== boardState.length)) {
|
||||
error(boardValidity.badShape);
|
||||
}
|
||||
if (![5, 7, 9, 13, 19].includes(boardState.length)) {
|
||||
error(boardValidity.badSize);
|
||||
}
|
||||
if (boardState.find((row) => row.match(/[^XO#.]/))) {
|
||||
error(boardValidity.badCharacters);
|
||||
}
|
||||
return boardState as SimpleBoard;
|
||||
}
|
||||
|
||||
/** Validate singularity access by throwing an error if the player does not have access. */
|
||||
export function checkCheatApiAccess(error: (s: string) => void): void {
|
||||
const hasSourceFile = Player.activeSourceFileLvl(14) > 1;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
handlePassTurn,
|
||||
makePlayerMove,
|
||||
resetBoardState,
|
||||
validateBoardState,
|
||||
validateMove,
|
||||
validateTurn,
|
||||
} from "../Go/effects/netscriptGoImplementation";
|
||||
@@ -78,17 +79,21 @@ export function NetscriptGo(): InternalAPI<NSGo> {
|
||||
return resetBoardState(logger(ctx), error(ctx), opponent, boardSize);
|
||||
},
|
||||
analysis: {
|
||||
getValidMoves: () => () => {
|
||||
return getValidMoves();
|
||||
getValidMoves: (ctx) => (_boardState, _priorBoardState) => {
|
||||
const State = validateBoardState(error(ctx), _boardState, _priorBoardState);
|
||||
return getValidMoves(State);
|
||||
},
|
||||
getChains: () => () => {
|
||||
return getChains();
|
||||
getChains: (ctx) => (_boardState) => {
|
||||
const State = validateBoardState(error(ctx), _boardState);
|
||||
return getChains(State?.board);
|
||||
},
|
||||
getLiberties: () => () => {
|
||||
return getLiberties();
|
||||
getLiberties: (ctx) => (_boardState) => {
|
||||
const State = validateBoardState(error(ctx), _boardState);
|
||||
return getLiberties(State?.board);
|
||||
},
|
||||
getControlledEmptyNodes: () => () => {
|
||||
return getControlledEmptyNodes();
|
||||
getControlledEmptyNodes: (ctx) => (_boardState) => {
|
||||
const State = validateBoardState(error(ctx), _boardState);
|
||||
return getControlledEmptyNodes(State?.board);
|
||||
},
|
||||
getStats: () => () => {
|
||||
return getStats();
|
||||
|
||||
21
src/ScriptEditor/NetscriptDefinitions.d.ts
vendored
21
src/ScriptEditor/NetscriptDefinitions.d.ts
vendored
@@ -4271,6 +4271,8 @@ type SimpleOpponentStats = {
|
||||
export interface GoAnalysis {
|
||||
/**
|
||||
* Shows if each point on the board is a valid move for the player.
|
||||
* By default, analyzes the current board state.
|
||||
* Takes an optional boardState (and an optional prior-move boardState, if desired) to analyze a custom board.
|
||||
*
|
||||
* 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();`
|
||||
@@ -4281,16 +4283,21 @@ export interface GoAnalysis {
|
||||
* 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.
|
||||
*
|
||||
* Also note that, when given a custom board state, only one prior move can be analyzed. This means that the superko rules
|
||||
* (no duplicate board states in the full game history) is not supported; you will have to implement your own analysis for that.
|
||||
*
|
||||
* @remarks
|
||||
* RAM cost: 8 GB
|
||||
* (This is intentionally expensive; you can derive this info from just getBoardState() )
|
||||
*/
|
||||
getValidMoves(): boolean[][];
|
||||
getValidMoves(boardState?: string[], priorBoardState?: string[]): boolean[][];
|
||||
|
||||
/**
|
||||
* Returns an ID for each point. All points that share an ID are part of the same network (or "chain"). Empty points
|
||||
* are also given chain IDs to represent continuous empty space. Dead nodes are given the value `null.`
|
||||
*
|
||||
* Takes an optional boardState argument; by default uses the current board state.
|
||||
*
|
||||
* The data from getChains() can be used with the data from getBoardState() to see which player (or empty) each chain is
|
||||
*
|
||||
* For example, a 5x5 board might look like this. There is a large chain #1 on the left side, smaller chains
|
||||
@@ -4310,12 +4317,14 @@ export interface GoAnalysis {
|
||||
* (This is intentionally expensive; you can derive this info from just getBoardState() )
|
||||
*
|
||||
*/
|
||||
getChains(): (number | null)[][];
|
||||
getChains(boardState?: string[]): (number | null)[][];
|
||||
|
||||
/**
|
||||
* Returns a number for each point, representing how many open nodes its network/chain is connected to.
|
||||
* Empty nodes and dead nodes are shown as -1 liberties.
|
||||
*
|
||||
* Takes an optional boardState argument; by default uses the current board state.
|
||||
*
|
||||
* 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!
|
||||
* <pre lang="javascript">
|
||||
@@ -4332,13 +4341,15 @@ export interface GoAnalysis {
|
||||
* RAM cost: 16 GB
|
||||
* (This is intentionally expensive; you can derive this info from just getBoardState() )
|
||||
*/
|
||||
getLiberties(): number[][];
|
||||
getLiberties(boardState?: string[]): number[][];
|
||||
|
||||
/**
|
||||
* Returns 'X', 'O', or '?' for each empty point to indicate which player controls that empty point.
|
||||
* Returns 'X' for black, 'O' for white, or '?' for each empty point to indicate which player controls that empty point.
|
||||
* If no single player fully encircles the empty space, it is shown as contested with '?'.
|
||||
* "#" are dead nodes that are not part of the subnet.
|
||||
*
|
||||
* Takes an optional boardState argument; by default uses the current board state.
|
||||
*
|
||||
* 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:
|
||||
@@ -4356,7 +4367,7 @@ export interface GoAnalysis {
|
||||
* RAM cost: 16 GB
|
||||
* (This is intentionally expensive; you can derive this info from just getBoardState() )
|
||||
*/
|
||||
getControlledEmptyNodes(): string[];
|
||||
getControlledEmptyNodes(boardState?: string[]): string[];
|
||||
|
||||
/**
|
||||
* Displays the game history, captured nodes, and gained bonuses for each opponent you have played against.
|
||||
|
||||
Reference in New Issue
Block a user