mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-18 07:18:38 +02:00
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:
committed by
GitHub
parent
6aaeb6b59e
commit
d81358c80f
@@ -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<Play> {
|
||||
export async function getAIMove(boardState: BoardState): Promise<Play> {
|
||||
let resolve: (value: Play) => void;
|
||||
const aiMoveResult = new Promise<Play>((res) => {
|
||||
Go.nextTurn = new Promise<Play>((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,
|
||||
() => {
|
||||
|
||||
Reference in New Issue
Block a user