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:
Michael Ficocelli
2025-02-02 23:47:16 -05:00
committed by GitHub
parent de6b202341
commit c8d2c9f769
30 changed files with 502 additions and 263 deletions
+8 -8
View File
@@ -1,26 +1,26 @@
import type { BoardState, OpponentStats, Play } from "./Types";
import type { BoardState, OpponentStats } from "./Types";
import { GoPlayType, type GoOpponent } from "@enums";
import { getRecordValues, PartialRecord } from "../Types/Record";
import type { GoOpponent } from "@enums";
import { getRecordKeys, PartialRecord } from "../Types/Record";
import { resetAI } from "./boardAnalysis/goAI";
import { getNewBoardState } from "./boardState/boardState";
import { EventEmitter } from "../utils/EventEmitter";
import { newOpponentStats } from "./Constants";
export class GoObject {
// Todo: Make previous game a slimmer interface
previousGame: BoardState | null = null;
currentGame: BoardState = getNewBoardState(7);
stats: PartialRecord<GoOpponent, OpponentStats> = {};
nextTurn: Promise<Play> = Promise.resolve({ type: GoPlayType.gameOver, x: null, y: null });
storedCycles: number = 0;
prestigeAugmentation() {
for (const stats of getRecordValues(this.stats)) {
stats.nodePower = 0;
stats.nodes = 0;
stats.winStreak = 0;
for (const opponent of getRecordKeys(Go.stats)) {
Go.stats[opponent] = newOpponentStats();
}
}
prestigeSourceFile() {
resetAI();
this.previousGame = null;
this.currentGame = getNewBoardState(7);
this.stats = {};
+19 -22
View File
@@ -2,18 +2,20 @@ import type { BoardState, OpponentStats, SimpleBoard } from "./Types";
import type { PartialRecord } from "../Types/Record";
import { Truthy } from "lodash";
import { GoColor, GoOpponent, GoPlayType } from "@enums";
import { GoColor, GoOpponent } from "@enums";
import { Go } from "./Go";
import { boardStateFromSimpleBoard, getPreviousMove, simpleBoardFromBoard } from "./boardAnalysis/boardAnalysis";
import { boardStateFromSimpleBoard, simpleBoardFromBoard } from "./boardAnalysis/boardAnalysis";
import { assertLoadingType } from "../utils/TypeAssertion";
import { getEnumHelper } from "../utils/EnumHelper";
import { boardSizes } from "./Constants";
import { isInteger, isNumber } from "../types";
import { makeAIMove } from "./boardAnalysis/goAI";
import { handleNextTurn, resetAI } from "./boardAnalysis/goAI";
type PreviousGameSaveData = { ai: GoOpponent; board: SimpleBoard; previousPlayer: GoColor | null } | null;
type CurrentGameSaveData = PreviousGameSaveData & {
previousBoard?: string;
cheatCount: number;
cheatCountForWhite: number;
passCount: number;
};
@@ -36,8 +38,10 @@ export function getGoSave(): SaveFormat {
currentGame: {
ai: Go.currentGame.ai,
board: simpleBoardFromBoard(Go.currentGame.board),
previousBoard: Go.currentGame.previousBoards[0] ?? "",
previousPlayer: Go.currentGame.previousPlayer,
cheatCount: Go.currentGame.cheatCount,
cheatCountForWhite: Go.currentGame.cheatCount,
passCount: Go.currentGame.passCount,
},
stats: Go.stats,
@@ -82,21 +86,10 @@ export function loadGo(data: unknown): boolean {
Go.stats = stats;
Go.storeCycles(loadStoredCycles(parsedData.storedCycles));
// If it's the AI's turn, initiate their turn, which will populate nextTurn
if (currentGame.previousPlayer === GoColor.black && currentGame.ai !== GoOpponent.none) {
makeAIMove(currentGame).catch((error) => {
showError(new Error(`Error while making first IPvGO AI move: ${error}`, { cause: error }));
});
}
// If it's not the AI's turn and we're not in gameover status, initialize nextTurn promise based on the previous move/pass
else if (currentGame.previousPlayer) {
const previousMove = getPreviousMove();
Go.nextTurn = Promise.resolve(
previousMove
? { type: GoPlayType.move, x: previousMove[0], y: previousMove[1] }
: { type: GoPlayType.pass, x: null, y: null },
);
}
resetAI();
handleNextTurn(currentGame).catch((error) => {
showError(new Error(`Error while initializing first IPvGO move: ${error}`, { cause: error }));
});
return true;
}
@@ -113,15 +106,19 @@ function loadCurrentGame(currentGame: unknown): BoardState | string {
const board = loadSimpleBoard(currentGame.board, requiredSize);
if (typeof board === "string") return board;
const previousPlayer = getEnumHelper("GoColor").getMember(currentGame.previousPlayer) ?? null;
if (!isInteger(currentGame.cheatCount) || currentGame.cheatCount < 0)
return "invalid number for currentGame.cheatCount";
const normalizedCheatCount = isInteger(currentGame.cheatCount) ? Math.max(0, currentGame.cheatCount || 0) : 0;
const normalizedCheatCountForWhite = isInteger(currentGame.cheatCountForWhite)
? Math.max(0, currentGame.cheatCountForWhite || 0)
: 0;
if (!isInteger(currentGame.passCount) || currentGame.passCount < 0) return "invalid number for currentGame.passCount";
const previousBoards = typeof currentGame.previousBoard === "string" ? [currentGame.previousBoard] : [];
const boardState = boardStateFromSimpleBoard(board, ai);
boardState.previousPlayer = previousPlayer;
boardState.cheatCount = currentGame.cheatCount;
boardState.cheatCount = normalizedCheatCount;
boardState.cheatCountForWhite = normalizedCheatCountForWhite;
boardState.passCount = currentGame.passCount;
boardState.previousBoards = [];
boardState.previousBoards = previousBoards;
return boardState;
}
+1
View File
@@ -53,6 +53,7 @@ export type BoardState = {
ai: GoOpponent;
passCount: number;
cheatCount: number;
cheatCountForWhite: number;
};
export type PointState = {
+2 -2
View File
@@ -691,7 +691,7 @@ export function getColorOnBoardString(boardString: string, x: number, y: number)
/** Find a move made by the previous player, if present. */
export function getPreviousMove(): [number, number] | null {
const priorBoard = Go.currentGame?.previousBoards[0];
const priorBoard = Go.currentGame.previousBoards[0];
if (Go.currentGame.passCount || !priorBoard) {
return null;
}
@@ -725,7 +725,7 @@ export function getPreviousMoveDetails(): Play {
}
return {
type: !priorMove && Go.currentGame?.passCount ? GoPlayType.pass : GoPlayType.gameOver,
type: Go.currentGame.previousPlayer ? GoPlayType.pass : GoPlayType.gameOver,
x: null,
y: null,
};
+90 -58
View File
@@ -14,51 +14,75 @@ import {
getAllEyes,
getAllEyesByChainId,
getAllNeighboringChains,
getAllValidMoves,
getPreviousMoveDetails,
} from "./boardAnalysis";
import { findDisputedTerritory } from "./controlledTerritory";
import { findAnyMatchedPatterns } from "./patternMatching";
import { WHRNG } from "../../Casino/RNG";
import { Go, GoEvents } from "../Go";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
let isAiThinking: boolean = false;
let currentTurnResolver: (() => void) | null = null;
type PlayerPromise = {
nextTurn: Promise<Play>;
resolver: ((play?: Play) => void) | null;
};
const gameOver = { type: GoPlayType.gameOver, x: null, y: null } as const;
const playerPromises: Record<GoColor.black | GoColor.white, PlayerPromise> = {
[GoColor.black]: { nextTurn: Promise.resolve(gameOver), resolver: null },
[GoColor.white]: { nextTurn: Promise.resolve(gameOver), resolver: null },
};
export function getNextTurn(color: GoColor.black | GoColor.white): Promise<Play> {
return playerPromises[color].nextTurn;
}
/**
* Retrieves a move from the current faction in response to the player's move
* Does common processing in response to a move being made.
*
* Due to asynchronous and/or timer-based functions, this function might be
* called multiple times per turn. Therefore, it is (and must be) idempotent.
* It is also used to handle the first turn of the game, and post-load
* processing.
* On the AI's turn, it starts AI processing. On all turns, it does promise
* handling and dispatches common events.
* @returns the nextTurn promise for the player who just moved
*/
export function makeAIMove(boardState: BoardState, useOfflineCycles = true): Promise<Play> {
// If AI is already taking their turn, return the existing turn.
if (isAiThinking) {
return Go.nextTurn;
export function handleNextTurn(boardState: BoardState, useOfflineCycles = true): Promise<Play> {
const previousColor = boardState.previousPlayer;
if (previousColor === null) {
// The game is over. We shouldn't get here in most circumstances,
// because when the game ends resetAI() will be called to resolve promises.
// Return an already-resolved promise until a new game is started.
return Promise.resolve(gameOver);
}
isAiThinking = true;
let encounteredError = false;
const currentColor = previousColor === GoColor.black ? GoColor.white : GoColor.black;
// Promises are indexed by who wants to wait on them, not by who triggers them.
// So the index color is reversed here.
const previousPromise = playerPromises[currentColor];
const currentPromise = playerPromises[currentColor === GoColor.black ? GoColor.white : GoColor.black];
// If we've already handled this turn, return the existing promise.
if (previousPromise.resolver === null) {
return currentPromise.nextTurn;
}
previousPromise.resolver();
previousPromise.resolver = null;
GoEvents.emit();
// If the AI is disabled, simply make a promise to be resolved once the player makes a move as white
if (boardState.ai === GoOpponent.none) {
resetAI();
}
// If an AI is in use, find the faction's move in response, and resolve the Go.nextTurn promise once it is found and played.
else {
// If an AI is in use, find the faction's move in response, and recursively call handleNextTurn to resolve the nextTurn promise once it is found and played.
if (boardState.ai !== GoOpponent.none && currentColor == GoColor.white) {
const currentMoveCount = Go.currentGame.previousBoards.length;
Go.nextTurn = getMove(boardState, GoColor.white, Go.currentGame.ai, useOfflineCycles).then(
async (play): Promise<Play> => {
if (boardState !== Go.currentGame) {
getMove(boardState, currentColor, Go.currentGame.ai, useOfflineCycles)
.then(async (play) => {
if (currentMoveCount !== Go.currentGame.previousBoards.length || boardState !== Go.currentGame) {
//Stale game
encounteredError = true;
return play;
return;
}
// Handle AI passing
if (play.type === GoPlayType.pass) {
passTurn(boardState, GoColor.white);
// if passTurn called endGoGame, or the player has no valid moves left, the move should be shown as a game over
if (boardState.previousPlayer === null || !getAllValidMoves(boardState, GoColor.black).length) {
return { type: GoPlayType.gameOver, x: null, y: null };
}
return play;
passTurn(boardState, currentColor);
return handleNextTurn(boardState, useOfflineCycles);
}
// Handle AI making a move
@@ -66,50 +90,58 @@ export function makeAIMove(boardState: BoardState, useOfflineCycles = true): Pro
if (currentMoveCount !== Go.currentGame.previousBoards.length || boardState !== Go.currentGame) {
console.warn("AI move attempted, but the board state has changed.");
encounteredError = true;
return play;
return;
}
const aiUpdatedBoard = makeMove(boardState, play.x, play.y, GoColor.white);
const aiUpdatedBoard = makeMove(boardState, play.x, play.y, currentColor);
// Handle the AI breaking. This shouldn't ever happen.
if (!aiUpdatedBoard) {
boardState.previousPlayer = GoColor.white;
boardState.previousPlayer = currentColor;
console.error(`Invalid AI move attempted: ${play.x}, ${play.y}. This should not happen.`);
}
return play;
},
);
// Recursively update promises for the next turn. This can't create an
// infinite loop because the recursion is happenning asynchronously from a
// delayed promise.
return handleNextTurn(boardState, useOfflineCycles);
})
.catch((error) => exceptionAlert(error));
}
// Once the AI moves (or the player playing as white with No AI moves),
// clear the isAiThinking semaphore and update the board UI.
Go.nextTurn = Go.nextTurn.finally(() => {
if (!encounteredError) {
isAiThinking = false;
}
GoEvents.emit();
});
return Go.nextTurn;
}
export function resetAI(thinking = true) {
isAiThinking = thinking;
GoEvents.emit();
// Update currentTurnResolver to call Go.nextTurn's resolve function with the last played move's details
Go.nextTurn = new Promise((resolve) => (currentTurnResolver = () => resolve(getPreviousMoveDetails())));
// If we haven't resolved currentPromise yet (for instance, at game start),
// we should continue to use it instead of resolving it and creating a new one.
if (!currentPromise.resolver) {
createPromise(currentPromise);
}
return currentPromise.nextTurn;
}
/**
* Resolves the current turn.
* This is used for players manually playing against their script on the no-ai board.
* Reset the promises for white and black turns.
* This will notify scripts waiting on the old promises with gameOver,
* potentially even when it is not their turn.
* If the game has already ended, it won't re-notify (that was handled in
* endGoGame()), which is why it is important to call this *before* resetting
* the board state.
*/
export function resolveCurrentTurn() {
// Call the resolve function on Go.nextTurn, if it exists
currentTurnResolver?.();
currentTurnResolver = null;
export function resetAI(endOfGame = false): void {
for (const playerPromise of Object.values(playerPromises)) {
if (playerPromise.resolver) {
playerPromise.resolver(gameOver);
playerPromise.resolver = null;
}
if (!endOfGame && !playerPromise.resolver) {
createPromise(playerPromise);
}
}
}
// Returns a promise that resolves with the previous move details when the other player / script / AI makes a move
function createPromise(promiseObj: PlayerPromise): void {
promiseObj.resolver?.();
promiseObj.nextTurn = new Promise((resolve) => {
promiseObj.resolver = (play?: Play) => resolve(play ?? getPreviousMoveDetails());
});
}
/*
+8 -10
View File
@@ -1,15 +1,15 @@
import type { Board, BoardState, PointState } from "../Types";
import { Player } from "@player";
import { GoOpponent, GoColor, GoPlayType } from "@enums";
import { GoOpponent, GoColor } from "@enums";
import { newOpponentStats } from "../Constants";
import { getAllChains, getPlayerNeighbors } from "./boardAnalysis";
import { getKomi } from "./goAI";
import { getKomi, resetAI } from "./goAI";
import { getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
import { isNotNullish } from "../boardState/boardState";
import { Factions } from "../../Faction/Factions";
import { getEnumHelper } from "../../utils/EnumHelper";
import { Go } from "../Go";
import { Go, GoEvents } from "../Go";
/**
* Returns the score of the current board.
@@ -46,11 +46,6 @@ export function endGoGame(boardState: BoardState) {
if (boardState.previousPlayer === null) {
return;
}
Go.nextTurn = Promise.resolve({
type: GoPlayType.gameOver,
x: null,
y: null,
});
boardState.previousPlayer = null;
const statusToUpdate = getOpponentStats(boardState.ai);
@@ -59,7 +54,6 @@ export function endGoGame(boardState: BoardState) {
if (score[GoColor.black].sum < score[GoColor.white].sum) {
resetWinstreak(boardState.ai, true);
statusToUpdate.nodePower += Math.floor(score[GoColor.black].sum * 0.25);
} else {
statusToUpdate.wins++;
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
@@ -89,6 +83,8 @@ export function endGoGame(boardState: BoardState) {
statusToUpdate.nodes += score[GoColor.black].sum;
Go.currentGame = boardState;
Go.previousGame = boardState;
resetAI(true);
GoEvents.emit();
// Update multipliers with new bonuses, once at the end of the game
Player.applyEntropy(Player.entropy);
@@ -123,7 +119,9 @@ function getColoredPieceCount(boardState: BoardState, color: GoColor) {
* Finds all empty spaces fully surrounded by a single player's stones
*/
function getTerritoryScores(board: Board) {
const emptyTerritoryChains = getAllChains(board).filter((chain) => chain?.[0]?.color === GoColor.empty);
const emptyTerritoryChains = getAllChains(board).filter(
(chain) => chain?.[0]?.color === GoColor.empty && chain.length <= board.length * 2,
);
return emptyTerritoryChains.reduce(
(scores, currentChain) => {
+13 -1
View File
@@ -34,6 +34,7 @@ export function getNewBoardState(
ai: ai,
passCount: 0,
cheatCount: 0,
cheatCountForWhite: 0,
board: Array.from({ length: boardSize }, (_, x) =>
Array.from({ length: boardSize }, (_, y) =>
!boardToCopy || boardToCopy?.[x]?.[y]
@@ -151,7 +152,18 @@ export function passTurn(boardState: BoardState, player: GoColor, allowEndGame =
* Modifies the board in place.
*/
export function applyHandicap(board: Board, handicap: number): void {
const availableMoves = getEmptySpaces(board);
const availableMoves = [];
for (const column of board) {
for (const point of column) {
if (point) {
if (point.color !== GoColor.empty) {
// Game is in progress, don't apply handicap
return;
}
availableMoves.push(point);
}
}
}
const handicapMoveOptions = getExpansionMoveArray(board, availableMoves);
const handicapMoves: Move[] = [];
+134 -60
View File
@@ -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,
);
}
+7 -27
View File
@@ -18,7 +18,7 @@ import { GoScoreModal } from "./GoScoreModal";
import { GoGameboard } from "./GoGameboard";
import { GoSubnetSearch } from "./GoSubnetSearch";
import { CorruptableText } from "../../ui/React/CorruptableText";
import { makeAIMove, resetAI, resolveCurrentTurn } from "../boardAnalysis/goAI";
import { handleNextTurn, resetAI } from "../boardAnalysis/goAI";
import { GoScoreExplanation } from "./GoScoreExplanation";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
@@ -94,48 +94,29 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
const didUpdateBoard = makeMove(boardState, x, y, currentPlayer);
if (didUpdateBoard) {
rerender();
takeAiTurn(boardState).catch((error) => exceptionAlert(error));
}
}
function passPlayerTurn() {
if (boardState.previousPlayer === GoColor.white) {
passTurn(boardState, GoColor.black);
rerender();
}
if (boardState.previousPlayer === GoColor.black && boardState.ai === GoOpponent.none) {
passTurn(boardState, GoColor.white);
rerender();
}
if (boardState.previousPlayer === null) {
setScoreOpen(true);
return;
}
setTimeout(() => {
takeAiTurn(boardState).catch((error) => exceptionAlert(error));
}, 100);
passTurn(boardState, boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black);
takeAiTurn(boardState).catch((error) => exceptionAlert(error));
}
async function takeAiTurn(boardState: BoardState) {
// If white is being played manually, halt and notify any scripts playing as black if present, instead of making an AI move
if (Go.currentGame.ai === GoOpponent.none) {
Go.currentGame.previousPlayer && resolveCurrentTurn();
return;
}
const move = await makeAIMove(boardState, false);
const move = await handleNextTurn(boardState, false);
if (move.type === GoPlayType.pass) {
SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000);
rerender();
return;
}
if (move.type === GoPlayType.gameOver || move.x === null || move.y === null) {
if (boardState.previousPlayer === null) {
setScoreOpen(true);
return;
}
}
@@ -152,10 +133,9 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
resetWinstreak(boardState.ai, false);
}
resetAI();
Go.currentGame = getNewBoardState(newBoardSize, newOpponent, true);
resetAI(false);
rerender();
resolveCurrentTurn();
handleNextTurn(Go.currentGame).catch((error) => exceptionAlert(error));
}
function getPriorMove() {
+7 -3
View File
@@ -63,18 +63,22 @@ export const GoHistoryPage = (): React.ReactElement => {
<Table sx={{ display: "table", mb: 1, width: "100%" }}>
<TableBody>
<TableRow>
<TableCell className={classes.cellNone}>Wins:</TableCell>
<TableCell className={classes.cellNone}>
Wins:{faction === GoOpponent.none ? " (Black / White)" : ""}
</TableCell>
<TableCell className={classes.cellNone}>
{data.wins} / {data.losses + data.wins}
</TableCell>
</TableRow>
<TableRow>
<TableCell className={classes.cellNone}>Current winstreak:</TableCell>
<TableCell className={classes.cellNone}>
Current winstreak{faction === GoOpponent.none ? " for black" : ""}:
</TableCell>
<TableCell className={classes.cellNone}>{data.winStreak}</TableCell>
</TableRow>
<TableRow>
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
Highest winstreak:
Highest winstreak{faction === GoOpponent.none ? " for black" : ""}:
</TableCell>
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
{data.highestWinStreak}
+1
View File
@@ -270,6 +270,7 @@ const go = {
getLiberties: 16,
getControlledEmptyNodes: 16,
getStats: 0,
resetStats: 0,
},
cheat: {
getCheatSuccessChance: 1,
+41 -30
View File
@@ -24,9 +24,9 @@ import {
handlePassTurn,
makePlayerMove,
resetBoardState,
resetStats,
validateBoardState,
validateMove,
validateTurn,
} from "../Go/effects/netscriptGoImplementation";
import { getEnumHelper } from "../utils/EnumHelper";
import { errorMessage } from "../Netscript/ErrorMessages";
@@ -43,19 +43,20 @@ export function NetscriptGo(): InternalAPI<NSGo> {
return {
makeMove:
(ctx: NetscriptContext) =>
(_x, _y): Promise<Play> => {
(_x, _y, playAsWhite): Promise<Play> => {
const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y);
validateMove(error(ctx), x, y, "makeMove");
return makePlayerMove(logger(ctx), error(ctx), x, y);
validateMove(error(ctx), x, y, "makeMove", { playAsWhite });
return makePlayerMove(logger(ctx), error(ctx), x, y, !!playAsWhite);
},
passTurn: (ctx: NetscriptContext) => async (): Promise<Play> => {
validateTurn(error(ctx), "passTurn()");
return handlePassTurn(logger(ctx));
},
opponentNextTurn: (ctx: NetscriptContext) => async (_logOpponentMove) => {
const logOpponentMove = typeof _logOpponentMove === "boolean" ? _logOpponentMove : true;
return getOpponentNextMove(logOpponentMove, logger(ctx));
passTurn:
(ctx: NetscriptContext) =>
(playAsWhite): Promise<Play> => {
validateMove(error(ctx), -1, -1, "passTurn", { playAsWhite, pass: true });
return handlePassTurn(logger(ctx), !!playAsWhite);
},
opponentNextTurn: (ctx: NetscriptContext) => async (logOpponentMove, playAsWhite) => {
return getOpponentNextMove(logger(ctx), !!logOpponentMove, !!playAsWhite);
},
getBoardState: () => () => {
return simpleBoardFromBoard(Go.currentGame.board);
@@ -79,9 +80,9 @@ export function NetscriptGo(): InternalAPI<NSGo> {
return resetBoardState(logger(ctx), error(ctx), opponent, boardSize);
},
analysis: {
getValidMoves: (ctx) => (_boardState, _priorBoardState) => {
getValidMoves: (ctx) => (_boardState, _priorBoardState, playAsWhite) => {
const State = validateBoardState(error(ctx), _boardState, _priorBoardState);
return getValidMoves(State);
return getValidMoves(State, !!playAsWhite);
},
getChains: (ctx) => (_boardState) => {
const State = validateBoardState(error(ctx), _boardState);
@@ -98,22 +99,27 @@ export function NetscriptGo(): InternalAPI<NSGo> {
getStats: () => () => {
return getStats();
},
resetStats:
() =>
(resetAll = false) => {
resetStats(!!resetAll);
},
},
cheat: {
getCheatSuccessChance:
(ctx: NetscriptContext) =>
(_cheatCount = Go.currentGame.cheatCount) => {
checkCheatApiAccess(error(ctx));
const cheatCount = helpers.number(ctx, "cheatCount", _cheatCount);
return cheatSuccessChance(cheatCount);
},
getCheatCount: (ctx: NetscriptContext) => () => {
getCheatSuccessChance: (ctx: NetscriptContext) => (_cheatCount, playAsWhite) => {
checkCheatApiAccess(error(ctx));
return Go.currentGame.cheatCount;
const normalizedCheatCount =
_cheatCount ?? (playAsWhite ? Go.currentGame.cheatCountForWhite : Go.currentGame.cheatCount);
const cheatCount = helpers.number(ctx, "cheatCount", normalizedCheatCount);
return cheatSuccessChance(cheatCount, !!playAsWhite);
},
getCheatCount: (ctx: NetscriptContext) => (playAsWhite) => {
checkCheatApiAccess(error(ctx));
return playAsWhite ? Go.currentGame.cheatCountForWhite : Go.currentGame.cheatCount;
},
removeRouter:
(ctx: NetscriptContext) =>
(_x, _y): Promise<Play> => {
(_x, _y, playAsWhite): Promise<Play> => {
checkCheatApiAccess(error(ctx));
const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y);
@@ -122,32 +128,35 @@ export function NetscriptGo(): InternalAPI<NSGo> {
requireNonEmptyNode: true,
repeat: false,
suicide: false,
playAsWhite: playAsWhite,
});
return cheatRemoveRouter(logger(ctx), x, y);
return cheatRemoveRouter(logger(ctx), error(ctx), x, y, undefined, undefined, !!playAsWhite);
},
playTwoMoves:
(ctx: NetscriptContext) =>
(_x1, _y1, _x2, _y2): Promise<Play> => {
(_x1, _y1, _x2, _y2, playAsWhite): Promise<Play> => {
checkCheatApiAccess(error(ctx));
const x1 = helpers.number(ctx, "x", _x1);
const y1 = helpers.number(ctx, "y", _y1);
validateMove(error(ctx), x1, y1, "playTwoMoves", {
repeat: false,
suicide: false,
playAsWhite,
});
const x2 = helpers.number(ctx, "x", _x2);
const y2 = helpers.number(ctx, "y", _y2);
validateMove(error(ctx), x2, y2, "playTwoMoves", {
repeat: false,
suicide: false,
playAsWhite,
});
return cheatPlayTwoMoves(logger(ctx), x1, y1, x2, y2);
return cheatPlayTwoMoves(logger(ctx), error(ctx), x1, y1, x2, y2, undefined, undefined, !!playAsWhite);
},
repairOfflineNode:
(ctx: NetscriptContext) =>
(_x, _y): Promise<Play> => {
(_x, _y, playAsWhite): Promise<Play> => {
checkCheatApiAccess(error(ctx));
const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y);
@@ -157,13 +166,14 @@ export function NetscriptGo(): InternalAPI<NSGo> {
onlineNode: false,
requireOfflineNode: true,
suicide: false,
playAsWhite,
});
return cheatRepairOfflineNode(logger(ctx), x, y);
return cheatRepairOfflineNode(logger(ctx), x, y, undefined, undefined, !!playAsWhite);
},
destroyNode:
(ctx: NetscriptContext) =>
(_x, _y): Promise<Play> => {
(_x, _y, playAsWhite): Promise<Play> => {
checkCheatApiAccess(error(ctx));
const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y);
@@ -171,9 +181,10 @@ export function NetscriptGo(): InternalAPI<NSGo> {
repeat: false,
onlineNode: true,
suicide: false,
playAsWhite,
});
return cheatDestroyNode(logger(ctx), x, y);
return cheatDestroyNode(logger(ctx), x, y, undefined, undefined, !!playAsWhite);
},
},
};
+48 -5
View File
@@ -4360,11 +4360,13 @@ export interface GoAnalysis {
* 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.
*
* playAsWhite is optional, and gets the current valid moves for the white player. Intended to be used when playing as white when the opponent is set to "No AI"
*
* @remarks
* RAM cost: 8 GB
* (This is intentionally expensive; you can derive this info from just getBoardState() )
*/
getValidMoves(boardState?: string[], priorBoardState?: string[]): boolean[][];
getValidMoves(boardState?: string[], priorBoardState?: string[], playAsWhite = false): boolean[][];
/**
* Returns an ID for each point. All points that share an ID are part of the same network (or "chain"). Empty points
@@ -4463,6 +4465,12 @@ export interface GoAnalysis {
* </pre>
*/
getStats(): Partial<Record<GoOpponent, SimpleOpponentStats>>;
/**
* Reset all win/loss and winstreak records for the No AI opponent.
* @param resetAll if true, reset win/loss records for all opponents. Leaves node power and bonuses unchanged.
*/
resetStats(resetAll = false): void;
}
/**
@@ -4480,20 +4488,22 @@ export interface GoCheat {
* small (~10%) chance you will instantly be ejected from the subnet.
*
* @param cheatCount - Optional override for the number of cheats already attempted. Defaults to the number of cheats attempted in the current game.
* @param playAsWhite - Optional override for playing as white. Can only be used when playing on a 'No AI' board.
*
* @remarks
* RAM cost: 1 GB
* Requires BitNode 14.2 to use
*/
getCheatSuccessChance(cheatCount?: number): number;
getCheatSuccessChance(cheatCount?: number, playAsWhite = false): number;
/**
* Returns the number of times you've attempted to cheat in the current game.
* @param playAsWhite - Optional override for playing as white. Can only be used when playing on a 'No AI' board.
*
* @remarks
* RAM cost: 1 GB
* Requires BitNode 14.2 to use
*/
getCheatCount(): number;
getCheatCount(playAsWhite = false): number;
/**
* Attempts to remove an existing router, leaving an empty node behind.
*
@@ -4502,6 +4512,11 @@ export interface GoCheat {
* Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a
* small (~10%) chance you will instantly be ejected from the subnet.
*
*
* @param x - x coordinate of router to remove
* @param y - y coordinate of router to remove
* @param playAsWhite - Optional override for playing as white. Can only be used when playing on a 'No AI' board.
*
* @remarks
* RAM cost: 8 GB
* Requires BitNode 14.2 to use
@@ -4511,6 +4526,7 @@ export interface GoCheat {
removeRouter(
x: number,
y: number,
playAsWhite = false,
): Promise<{
type: "move" | "pass" | "gameOver";
x: number | null;
@@ -4525,6 +4541,13 @@ export interface GoCheat {
* Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a
* small (~10%) chance you will instantly be ejected from the subnet.
*
*
* @param x1 - x coordinate of first move to make
* @param y1 - y coordinate of first move to make
* @param x2 - x coordinate of second move to make
* @param y2 - y coordinate of second move to make
* @param playAsWhite - Optional override for playing as white. Can only be used when playing on a 'No AI' board.
*
* @remarks
* RAM cost: 8 GB
* Requires BitNode 14.2 to use
@@ -4536,6 +4559,7 @@ export interface GoCheat {
y1: number,
x2: number,
y2: number,
playAsWhite = false,
): Promise<{
type: "move" | "pass" | "gameOver";
x: number | null;
@@ -4550,6 +4574,10 @@ export interface GoCheat {
* Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a
* small (~10%) chance you will instantly be ejected from the subnet.
*
* @param x - x coordinate of offline node to repair
* @param y - y coordinate of offline node to repair
* @param playAsWhite - Optional override for playing as white. Can only be used when playing on a 'No AI' board.
*
* @remarks
* RAM cost: 8 GB
* Requires BitNode 14.2 to use
@@ -4559,6 +4587,7 @@ export interface GoCheat {
repairOfflineNode(
x: number,
y: number,
playAsWhite = false,
): Promise<{
type: "move" | "pass" | "gameOver";
x: number | null;
@@ -4574,6 +4603,10 @@ export interface GoCheat {
* Warning: if you fail to play a cheat move, your turn will be skipped. After your first cheat attempt, if you fail, there is a
* small (~10%) chance you will instantly be ejected from the subnet.
*
* @param x - x coordinate of empty node to destroy
* @param y - y coordinate of empty node to destroy
* @param playAsWhite - Optional override for playing as white. Can only be used when playing on a 'No AI' board.
*
* @remarks
* RAM cost: 8 GB
* Requires BitNode 14.2 to use
@@ -4583,6 +4616,7 @@ export interface GoCheat {
destroyNode(
x: number,
y: number,
playAsWhite = false,
): Promise<{
type: "move" | "pass" | "gameOver";
x: number | null;
@@ -4599,6 +4633,8 @@ export interface Go {
* Make a move on the IPvGO subnet game board, and await the opponent's response.
* x:0 y:0 represents the bottom-left corner of the board in the UI.
*
* playAsWhite is optional, and attempts to make a move as the white player. Only can be used when playing against "No AI".
*
* @remarks
* RAM cost: 4 GB
*
@@ -4607,6 +4643,7 @@ export interface Go {
makeMove(
x: number,
y: number,
playAsWhite = false,
): Promise<{
type: "move" | "pass" | "gameOver";
x: number | null;
@@ -4620,13 +4657,15 @@ export interface Go {
* 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.
*
* passAsWhite is optional, and attempts to pass while playing as the white player. Only can be used when playing against "No AI".
*
* @returns a promise that contains 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
*
*/
passTurn(): Promise<{
passTurn(passAsWhite = false): Promise<{
type: "move" | "pass" | "gameOver";
x: number | null;
y: number | null;
@@ -4637,13 +4676,17 @@ export interface Go {
* x:0 y:0 represents the bottom-left corner of the board in the UI.
*
* @param logOpponentMove - optional, defaults to true. if false prevents logging opponent move
* @param playAsWhite - optional. If true, waits to get the next move the black player makes. Intended to be used when playing as white when the opponent is set to "No AI"
*
* @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): Promise<{
opponentNextTurn(
logOpponentMove?: boolean,
playAsWhite = false,
): Promise<{
type: "move" | "pass" | "gameOver";
x: number | null;
y: number | null;