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
22 changed files with 541 additions and 308 deletions

View File

@@ -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,
() => {