mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-28 20:07:04 +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
+8
-8
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ export type BoardState = {
|
||||
ai: GoOpponent;
|
||||
passCount: number;
|
||||
cheatCount: number;
|
||||
cheatCountForWhite: number;
|
||||
};
|
||||
|
||||
export type PointState = {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -270,6 +270,7 @@ const go = {
|
||||
getLiberties: 16,
|
||||
getControlledEmptyNodes: 16,
|
||||
getStats: 0,
|
||||
resetStats: 0,
|
||||
},
|
||||
cheat: {
|
||||
getCheatSuccessChance: 1,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user