mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-18 15:28:43 +02:00
IPVGO: Support scripts playing against each other as each color on "No AI" boards (#1917)
This is a big change with a *lot* of moving parts. The largest part of it is enabling scripts to `playAsWhite` as a parameter to many Go functions. In the implementation, this involved a significant rewrite of `opponentNextTurn` promise handling. A number of other changes and bugfixes are included: * Fixes the issue where handicap stones are added on game load. * Better typing for error callbacks. * Throw errors instead of deadlocking on bad cheat usage. * Return always-resolved gameOver promise after game end * Added a new `resetStats` api function. --------- Co-authored-by: David Walker <d0sboots@gmail.com>
This commit is contained in:
committed by
GitHub
parent
de6b202341
commit
c8d2c9f769
@@ -1,17 +1,16 @@
|
||||
import { Board, BoardState, Play, SimpleBoard, SimpleOpponentStats } from "../Types";
|
||||
import { Board, BoardState, OpponentStats, Play, SimpleBoard, SimpleOpponentStats } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { AugmentationName, GoColor, GoOpponent, GoPlayType, GoValidity } from "@enums";
|
||||
import { Go, GoEvents } from "../Go";
|
||||
import { Go } from "../Go";
|
||||
import {
|
||||
getNewBoardState,
|
||||
getNewBoardStateFromSimpleBoard,
|
||||
makeMove,
|
||||
passTurn,
|
||||
updateCaptures,
|
||||
updateChains,
|
||||
} from "../boardState/boardState";
|
||||
import { makeAIMove, resetAI } from "../boardAnalysis/goAI";
|
||||
import { getNextTurn, handleNextTurn, resetAI } from "../boardAnalysis/goAI";
|
||||
import {
|
||||
evaluateIfMoveIsValid,
|
||||
getControlledSpace,
|
||||
@@ -23,11 +22,13 @@ import { endGoGame, getOpponentStats, getScore, resetWinstreak } from "../boardA
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
import { getRecordKeys } from "../../Types/Record";
|
||||
import { CalculateEffect, getEffectTypeForFaction } from "./effect";
|
||||
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
|
||||
import { newOpponentStats } from "../Constants";
|
||||
|
||||
/**
|
||||
* Check the move based on the current settings
|
||||
*/
|
||||
export function validateMove(error: (s: string) => void, x: number, y: number, methodName = "", settings = {}) {
|
||||
export function validateMove(error: (s: string) => never, x: number, y: number, methodName = "", settings = {}): void {
|
||||
const check = {
|
||||
emptyNode: true,
|
||||
requireNonEmptyNode: false,
|
||||
@@ -35,9 +36,23 @@ export function validateMove(error: (s: string) => void, x: number, y: number, m
|
||||
onlineNode: true,
|
||||
requireOfflineNode: false,
|
||||
suicide: true,
|
||||
playAsWhite: false,
|
||||
pass: false,
|
||||
...settings,
|
||||
};
|
||||
|
||||
const moveString = methodName + (check.pass ? "" : ` ${x},${y}`) + (check.playAsWhite ? " (White)" : "") + ": ";
|
||||
const moveColor = check.playAsWhite ? GoColor.white : GoColor.black;
|
||||
|
||||
if (check.playAsWhite) {
|
||||
validatePlayAsWhite(error);
|
||||
}
|
||||
validateTurn(error, moveString, moveColor);
|
||||
|
||||
if (check.pass) {
|
||||
return;
|
||||
}
|
||||
|
||||
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}`);
|
||||
@@ -46,10 +61,7 @@ export function validateMove(error: (s: string) => void, x: number, y: number, m
|
||||
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 validity = evaluateIfMoveIsValid(Go.currentGame, x, y, moveColor);
|
||||
const point = Go.currentGame.board[x][y];
|
||||
if (!point && check.onlineNode) {
|
||||
error(
|
||||
@@ -88,8 +100,18 @@ export function validateMove(error: (s: string) => void, x: number, y: number, m
|
||||
}
|
||||
}
|
||||
|
||||
export function validateTurn(error: (s: string) => void, moveString = "") {
|
||||
if (Go.currentGame.previousPlayer === GoColor.black) {
|
||||
function validatePlayAsWhite(error: (s: string) => never) {
|
||||
if (Go.currentGame.ai !== GoOpponent.none) {
|
||||
error(`${GoValidity.invalid}. You can only play as white when playing against 'No AI'`);
|
||||
}
|
||||
|
||||
if (Go.currentGame.previousPlayer === GoColor.white) {
|
||||
error(`${GoValidity.notYourTurn}. You cannot play or pass as white until the opponent has played.`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateTurn(error: (s: string) => never, moveString = "", color = GoColor.black) {
|
||||
if (Go.currentGame.previousPlayer === color) {
|
||||
error(
|
||||
`${moveString} ${GoValidity.notYourTurn}. Do you have multiple scripts running, or did you forget to await makeMove() or opponentNextTurn()`,
|
||||
);
|
||||
@@ -104,42 +126,48 @@ export function validateTurn(error: (s: string) => void, moveString = "") {
|
||||
/**
|
||||
* 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);
|
||||
export function handlePassTurn(logger: (s: string) => void, passAsWhite = false) {
|
||||
const color = passAsWhite ? GoColor.white : GoColor.black;
|
||||
passTurn(Go.currentGame, color);
|
||||
logger("Go turn passed.");
|
||||
|
||||
if (Go.currentGame.previousPlayer === null) {
|
||||
logEndGame(logger);
|
||||
return getOpponentNextMove(false, logger);
|
||||
} else {
|
||||
return makeAIMove(Go.currentGame);
|
||||
}
|
||||
return handleNextTurn(Go.currentGame, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and applies the player's router placement
|
||||
*/
|
||||
export async function makePlayerMove(logger: (s: string) => void, error: (s: string) => void, x: number, y: number) {
|
||||
export function makePlayerMove(
|
||||
logger: (s: string) => void,
|
||||
error: (s: string) => never,
|
||||
x: number,
|
||||
y: number,
|
||||
playAsWhite = false,
|
||||
) {
|
||||
const boardState = Go.currentGame;
|
||||
const validity = evaluateIfMoveIsValid(boardState, x, y, GoColor.black);
|
||||
const moveWasMade = makeMove(boardState, x, y, GoColor.black);
|
||||
const color = playAsWhite ? GoColor.white : GoColor.black;
|
||||
const validity = evaluateIfMoveIsValid(boardState, x, y, color);
|
||||
const moveWasMade = makeMove(boardState, x, y, color);
|
||||
|
||||
if (validity !== GoValidity.valid || !moveWasMade) {
|
||||
error(`Invalid move: ${x} ${y}. ${validity}.`);
|
||||
}
|
||||
|
||||
GoEvents.emit();
|
||||
logger(`Go move played: ${x}, ${y}`);
|
||||
return makeAIMove(boardState);
|
||||
logger(`Go move played: ${x}, ${y}${playAsWhite ? " (White)" : ""}`);
|
||||
return handleNextTurn(boardState, true);
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the promise that provides the opponent's move, once it finishes thinking.
|
||||
*/
|
||||
export async function getOpponentNextMove(logOpponentMove = true, logger: (s: string) => void) {
|
||||
export function getOpponentNextMove(logger: (s: string) => void, logOpponentMove = true, playAsWhite = false) {
|
||||
const playerColor = playAsWhite ? GoColor.white : GoColor.black;
|
||||
const nextTurn = getNextTurn(playerColor);
|
||||
// Only asynchronously log the opponent move if not disabled by the player
|
||||
if (logOpponentMove) {
|
||||
return Go.nextTurn.then((move) => {
|
||||
return nextTurn.then((move) => {
|
||||
if (move.type === GoPlayType.gameOver) {
|
||||
logEndGame(logger);
|
||||
} else if (move.type === GoPlayType.pass) {
|
||||
@@ -151,18 +179,25 @@ export async function getOpponentNextMove(logOpponentMove = true, logger: (s: st
|
||||
});
|
||||
}
|
||||
|
||||
return Go.nextTurn;
|
||||
return nextTurn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a grid of booleans indicating if the coordinates at that location are a valid move for the player (black pieces)
|
||||
* Returns a grid of booleans indicating if the coordinates at that location are a valid move for the player
|
||||
*/
|
||||
export function getValidMoves(_boardState?: BoardState) {
|
||||
export function getValidMoves(_boardState?: BoardState, playAsWhite = false) {
|
||||
const boardState = _boardState || Go.currentGame;
|
||||
const color = playAsWhite ? GoColor.white : GoColor.black;
|
||||
|
||||
// If the game is over, or if it is not your turn, there are no valid moves
|
||||
if (!boardState.previousPlayer || boardState.previousPlayer === color) {
|
||||
return boardState.board.map((): boolean[] => Array(boardState.board.length).fill(false) as boolean[]);
|
||||
}
|
||||
|
||||
// Map the board matrix into true/false values
|
||||
return boardState.board.map((column, x) =>
|
||||
column.reduce((validityArray: boolean[], point, y) => {
|
||||
const isValid = evaluateIfMoveIsValid(boardState, x, y, GoColor.black) === GoValidity.valid;
|
||||
const isValid = evaluateIfMoveIsValid(boardState, x, y, color) === GoValidity.valid;
|
||||
validityArray.push(isValid);
|
||||
return validityArray;
|
||||
}, []),
|
||||
@@ -229,6 +264,13 @@ export function getControlledEmptyNodes(_board?: Board) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all previous board states as SimpleBoards
|
||||
*/
|
||||
export function getHistory(): string[][] {
|
||||
return Go.currentGame.previousBoards.map((boardString): string[] => simpleBoardFromBoardString(boardString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status of the current game.
|
||||
* Shows the current player, current score, and the previous move coordinates.
|
||||
@@ -300,9 +342,9 @@ export function resetBoardState(
|
||||
resetWinstreak(oldBoardState.ai, false);
|
||||
}
|
||||
|
||||
resetAI();
|
||||
Go.currentGame = getNewBoardState(boardSize, opponent, true);
|
||||
resetAI(false);
|
||||
GoEvents.emit(); // Trigger a Go UI rerender
|
||||
handleNextTurn(Go.currentGame).catch((error) => exceptionAlert(error));
|
||||
logger(`New game started: ${opponent}, ${boardSize}x${boardSize}`);
|
||||
return simpleBoardFromBoard(Go.currentGame.board);
|
||||
}
|
||||
@@ -331,6 +373,27 @@ export function getStats() {
|
||||
return statDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all win/loss numbers for the No AI opponent.
|
||||
* @param resetAll if true, reset win/loss records for all opponents. This leaves node power and bonuses unchanged.
|
||||
*/
|
||||
export function resetStats(resetAll = false) {
|
||||
if (resetAll) {
|
||||
for (const opponent of getRecordKeys(Go.stats)) {
|
||||
Go.stats[opponent] = {
|
||||
...(Go.stats[opponent] as OpponentStats),
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
winStreak: 0,
|
||||
oldWinStreak: 0,
|
||||
highestWinStreak: 0,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
Go.stats[GoOpponent.none] = newOpponentStats();
|
||||
}
|
||||
}
|
||||
|
||||
const boardValidity = {
|
||||
valid: "",
|
||||
badShape: "Invalid boardState: Board must be a square",
|
||||
@@ -345,7 +408,7 @@ const boardValidity = {
|
||||
* 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,
|
||||
error: (s: string) => never,
|
||||
_boardState?: unknown,
|
||||
_priorBoardState?: unknown,
|
||||
): BoardState | undefined {
|
||||
@@ -366,7 +429,7 @@ export function validateBoardState(
|
||||
/**
|
||||
* 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 {
|
||||
function getSimpleBoardFromUnknown(error: (arg0: string) => never, _boardState: unknown): SimpleBoard | undefined {
|
||||
if (!_boardState) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -392,7 +455,7 @@ function getSimpleBoardFromUnknown(error: (arg0: string) => void, _boardState: u
|
||||
}
|
||||
|
||||
/** Validate singularity access by throwing an error if the player does not have access. */
|
||||
export function checkCheatApiAccess(error: (s: string) => void): void {
|
||||
export function checkCheatApiAccess(error: (s: string) => never): void {
|
||||
const hasSourceFile = Player.activeSourceFileLvl(14) > 1;
|
||||
const isBitnodeFourteenTwo = Player.activeSourceFileLvl(14) === 1 && Player.bitNodeN === 14;
|
||||
if (!hasSourceFile && !isBitnodeFourteenTwo) {
|
||||
@@ -408,35 +471,43 @@ export function checkCheatApiAccess(error: (s: string) => void): void {
|
||||
*
|
||||
* If it fails, determines if the player's turn is skipped, or if the player is ejected from the subnet.
|
||||
*/
|
||||
export async function determineCheatSuccess(
|
||||
export function determineCheatSuccess(
|
||||
logger: (s: string) => void,
|
||||
callback: () => void,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
playAsWhite = false,
|
||||
): Promise<Play> {
|
||||
const state = Go.currentGame;
|
||||
const rng = new WHRNG(Player.totalPlaytime);
|
||||
state.passCount = 0;
|
||||
const priorCheatCount = playAsWhite ? state.cheatCountForWhite : state.cheatCount;
|
||||
const playerColor = playAsWhite ? GoColor.white : GoColor.black;
|
||||
|
||||
// If cheat is successful, run callback
|
||||
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount)) {
|
||||
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount, playAsWhite)) {
|
||||
callback();
|
||||
GoEvents.emit();
|
||||
}
|
||||
// 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 (priorCheatCount && (ejectRngOverride ?? rng.random()) < 0.1 && state.ai !== GoOpponent.none) {
|
||||
logger(`Cheat failed! You have been ejected from the subnet.`);
|
||||
endGoGame(state);
|
||||
return Go.nextTurn;
|
||||
}
|
||||
// If the cheat fails, your turn is skipped
|
||||
else {
|
||||
return handleNextTurn(state, true);
|
||||
} else {
|
||||
// If the cheat fails, your turn is skipped
|
||||
logger(`Cheat failed. Your turn has been skipped.`);
|
||||
passTurn(state, GoColor.black, false);
|
||||
passTurn(state, playerColor, false);
|
||||
}
|
||||
|
||||
state.cheatCount++;
|
||||
return makeAIMove(state);
|
||||
if (playAsWhite) {
|
||||
state.cheatCountForWhite++;
|
||||
} else {
|
||||
state.cheatCount++;
|
||||
}
|
||||
Go.currentGame.previousPlayer = playerColor;
|
||||
updateCaptures(Go.currentGame.board, playerColor, true);
|
||||
|
||||
return handleNextTurn(state, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -456,7 +527,9 @@ export async function determineCheatSuccess(
|
||||
* 12: +534,704%
|
||||
* 15: +31,358,645%
|
||||
*/
|
||||
export function cheatSuccessChance(cheatCount: number) {
|
||||
export function cheatSuccessChance(cheatCountOverride: number, playAsWhite = false) {
|
||||
const cheatCount =
|
||||
cheatCountOverride ?? (playAsWhite ? Go.currentGame.cheatCountForWhite : Go.currentGame.cheatCount);
|
||||
const sourceFileBonus = Player.activeSourceFileLvl(14) === 3 ? 0.25 : 0;
|
||||
const cheatCountScalar = (0.7 - 0.02 * cheatCount) ** cheatCount;
|
||||
return Math.max(Math.min(0.6 * cheatCountScalar * Player.mults.crime_success + sourceFileBonus, 1), 0);
|
||||
@@ -467,26 +540,26 @@ export function cheatSuccessChance(cheatCount: number) {
|
||||
*/
|
||||
export function cheatRemoveRouter(
|
||||
logger: (s: string) => void,
|
||||
error: (s: string) => never,
|
||||
x: number,
|
||||
y: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
playAsWhite = false,
|
||||
): Promise<Play> {
|
||||
const point = Go.currentGame.board[x][y];
|
||||
if (!point) {
|
||||
logger(`Cheat failed. The point ${x},${y} is already offline.`);
|
||||
return Go.nextTurn;
|
||||
error(`Cheat failed. The point ${x},${y} is already offline.`);
|
||||
}
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
point.color = GoColor.empty;
|
||||
updateChains(Go.currentGame.board);
|
||||
Go.currentGame.previousPlayer = GoColor.black;
|
||||
logger(`Cheat successful. The point ${x},${y} was cleared.`);
|
||||
},
|
||||
successRngOverride,
|
||||
ejectRngOverride,
|
||||
playAsWhite,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -495,33 +568,34 @@ export function cheatRemoveRouter(
|
||||
*/
|
||||
export function cheatPlayTwoMoves(
|
||||
logger: (s: string) => void,
|
||||
error: (s: string) => never,
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
playAsWhite = false,
|
||||
): Promise<Play> {
|
||||
const point1 = Go.currentGame.board[x1][y1];
|
||||
const point2 = Go.currentGame.board[x2][y2];
|
||||
|
||||
if (!point1 || !point2) {
|
||||
logger(`Cheat failed. One of the points ${x1},${y1} or ${x2},${y2} is already offline.`);
|
||||
return Go.nextTurn;
|
||||
error(`Cheat failed. One of the points ${x1},${y1} or ${x2},${y2} is already offline.`);
|
||||
}
|
||||
const playerColor = playAsWhite ? GoColor.white : GoColor.black;
|
||||
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
point1.color = GoColor.black;
|
||||
point2.color = GoColor.black;
|
||||
updateCaptures(Go.currentGame.board, GoColor.black);
|
||||
Go.currentGame.previousPlayer = GoColor.black;
|
||||
point1.color = playerColor;
|
||||
point2.color = playerColor;
|
||||
|
||||
logger(`Cheat successful. Two go moves played: ${x1},${y1} and ${x2},${y2}`);
|
||||
},
|
||||
successRngOverride,
|
||||
ejectRngOverride,
|
||||
playAsWhite,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -531,6 +605,7 @@ export function cheatRepairOfflineNode(
|
||||
y: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
playAsWhite = false,
|
||||
): Promise<Play> {
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
@@ -542,12 +617,11 @@ export function cheatRepairOfflineNode(
|
||||
color: GoColor.empty,
|
||||
x,
|
||||
};
|
||||
updateChains(Go.currentGame.board);
|
||||
Go.currentGame.previousPlayer = GoColor.black;
|
||||
logger(`Cheat successful. The point ${x},${y} was repaired.`);
|
||||
},
|
||||
successRngOverride,
|
||||
ejectRngOverride,
|
||||
playAsWhite,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -557,16 +631,16 @@ export function cheatDestroyNode(
|
||||
y: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
playAsWhite = false,
|
||||
): Promise<Play> {
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
Go.currentGame.board[x][y] = null;
|
||||
updateChains(Go.currentGame.board);
|
||||
Go.currentGame.previousPlayer = GoColor.black;
|
||||
logger(`Cheat successful. The point ${x},${y} was destroyed.`);
|
||||
},
|
||||
successRngOverride,
|
||||
ejectRngOverride,
|
||||
playAsWhite,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user