mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-18 07:18:38 +02:00
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>
647 lines
21 KiB
TypeScript
647 lines
21 KiB
TypeScript
import { Board, BoardState, OpponentStats, Play, SimpleBoard, SimpleOpponentStats } from "../Types";
|
|
|
|
import { Player } from "@player";
|
|
import { AugmentationName, GoColor, GoOpponent, GoPlayType, GoValidity } from "@enums";
|
|
import { Go } from "../Go";
|
|
import {
|
|
getNewBoardState,
|
|
getNewBoardStateFromSimpleBoard,
|
|
makeMove,
|
|
passTurn,
|
|
updateCaptures,
|
|
} from "../boardState/boardState";
|
|
import { getNextTurn, handleNextTurn, resetAI } from "../boardAnalysis/goAI";
|
|
import {
|
|
evaluateIfMoveIsValid,
|
|
getControlledSpace,
|
|
getPreviousMove,
|
|
simpleBoardFromBoard,
|
|
simpleBoardFromBoardString,
|
|
} from "../boardAnalysis/boardAnalysis";
|
|
import { endGoGame, getOpponentStats, getScore, resetWinstreak } from "../boardAnalysis/scoring";
|
|
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) => never, x: number, y: number, methodName = "", settings = {}): void {
|
|
const check = {
|
|
emptyNode: true,
|
|
requireNonEmptyNode: false,
|
|
repeat: true,
|
|
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}`);
|
|
}
|
|
if (y < 0 || y >= boardSize) {
|
|
error(`Invalid row number (y = ${y}), row must be a number 0 through ${boardSize - 1}`);
|
|
}
|
|
|
|
const validity = evaluateIfMoveIsValid(Go.currentGame, x, y, moveColor);
|
|
const point = Go.currentGame.board[x][y];
|
|
if (!point && check.onlineNode) {
|
|
error(
|
|
`The node ${x},${y} is offline, so you cannot ${
|
|
methodName === "removeRouter"
|
|
? "clear this point with removeRouter()"
|
|
: methodName === "destroyNode"
|
|
? "destroy the node. (Attempted to destroyNode)"
|
|
: "place a router there"
|
|
}.`,
|
|
);
|
|
}
|
|
if (validity === GoValidity.noSuicide && check.suicide) {
|
|
error(
|
|
`${moveString} ${validity}. That point has no neighboring empty nodes, and is not connected to a network with access to empty nodes, meaning it would be instantly captured if played there.`,
|
|
);
|
|
}
|
|
if (validity === GoValidity.boardRepeated && check.repeat) {
|
|
error(
|
|
`${moveString} ${validity}. That move would repeat the previous board state, which is illegal as it leads to infinite loops.`,
|
|
);
|
|
}
|
|
if (point?.color !== GoColor.empty && check.emptyNode) {
|
|
error(
|
|
`The point ${x},${y} is occupied by a router, so you cannot ${
|
|
methodName === "destroyNode" ? "destroy this node. (Attempted to destroyNode)" : "place a router there"
|
|
}`,
|
|
);
|
|
}
|
|
|
|
if (point?.color === GoColor.empty && check.requireNonEmptyNode) {
|
|
error(`The point ${x},${y} does not have a router on it, so you cannot clear this point with removeRouter().`);
|
|
}
|
|
if (point && check.requireOfflineNode) {
|
|
error(`The node ${x},${y} is not offline, so you cannot repair the node.`);
|
|
}
|
|
}
|
|
|
|
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()`,
|
|
);
|
|
}
|
|
if (Go.currentGame.previousPlayer === null) {
|
|
error(
|
|
`${moveString} ${GoValidity.gameOver}. You cannot make more moves. Start a new game using resetBoardState().`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pass player's turn and await the opponent's response (or logs the end of the game if both players pass)
|
|
*/
|
|
export 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 handleNextTurn(Go.currentGame, true);
|
|
}
|
|
|
|
/**
|
|
* Validates and applies the player's router placement
|
|
*/
|
|
export function makePlayerMove(
|
|
logger: (s: string) => void,
|
|
error: (s: string) => never,
|
|
x: number,
|
|
y: number,
|
|
playAsWhite = false,
|
|
) {
|
|
const boardState = Go.currentGame;
|
|
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}.`);
|
|
}
|
|
|
|
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 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 nextTurn.then((move) => {
|
|
if (move.type === GoPlayType.gameOver) {
|
|
logEndGame(logger);
|
|
} else if (move.type === GoPlayType.pass) {
|
|
logger(`Opponent passed their turn. You can end the game by passing as well.`);
|
|
} else if (move.type === GoPlayType.move) {
|
|
logger(`Opponent played move: ${move.x}, ${move.y}`);
|
|
}
|
|
return move;
|
|
});
|
|
}
|
|
|
|
return nextTurn;
|
|
}
|
|
|
|
/**
|
|
* Returns a grid of booleans indicating if the coordinates at that location are a valid move for the player
|
|
*/
|
|
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, color) === GoValidity.valid;
|
|
validityArray.push(isValid);
|
|
return validityArray;
|
|
}, []),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns a grid with an ID for each contiguous chain of same-state nodes (excluding dead/offline nodes)
|
|
*/
|
|
export function getChains(_board?: Board) {
|
|
const board = _board || Go.currentGame.board;
|
|
const chains: string[] = [];
|
|
// Turn the internal chain IDs into nice consecutive numbers for display to the player
|
|
return board.map((column) =>
|
|
column.reduce((chainIdArray: (number | null)[], point) => {
|
|
if (!point) {
|
|
chainIdArray.push(null);
|
|
return chainIdArray;
|
|
}
|
|
if (!chains.includes(point.chain)) {
|
|
chains.push(point.chain);
|
|
}
|
|
chainIdArray.push(chains.indexOf(point.chain));
|
|
return chainIdArray;
|
|
}, []),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns a grid of numbers representing the number of open-node connections each player-owned chain has.
|
|
*/
|
|
export function getLiberties(_board?: Board) {
|
|
const board = _board || Go.currentGame.board;
|
|
return board.map((column) =>
|
|
column.reduce((libertyArray: number[], point) => {
|
|
libertyArray.push(point?.liberties?.length || -1);
|
|
return libertyArray;
|
|
}, []),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns a grid indicating which player, if any, controls the empty nodes by fully encircling it with their routers
|
|
*/
|
|
export function getControlledEmptyNodes(_board?: Board) {
|
|
const board = _board || Go.currentGame.board;
|
|
const controlled = getControlledSpace(board);
|
|
return controlled.map((column, x: number) =>
|
|
column.reduce((ownedPoints: string, owner: GoColor, y: number) => {
|
|
if (owner === GoColor.white) {
|
|
return ownedPoints + "O";
|
|
}
|
|
if (owner === GoColor.black) {
|
|
return ownedPoints + "X";
|
|
}
|
|
if (!board[x][y]) {
|
|
return ownedPoints + "#";
|
|
}
|
|
if (board[x][y]?.color === GoColor.empty) {
|
|
return ownedPoints + "?";
|
|
}
|
|
return ownedPoints + ".";
|
|
}, ""),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* Previous move coordinates will be [-1, -1] for a pass, or if there are no prior moves.
|
|
*
|
|
* Also provides the white player's komi (bonus starting score), and the amount of bonus cycles from offline time remaining
|
|
*/
|
|
export function getGameState() {
|
|
const currentPlayer = getCurrentPlayer();
|
|
const score = getScore(Go.currentGame);
|
|
const previousMove = getPreviousMove();
|
|
|
|
return {
|
|
currentPlayer,
|
|
whiteScore: score[GoColor.white].sum,
|
|
blackScore: score[GoColor.black].sum,
|
|
previousMove,
|
|
komi: score[GoColor.white].komi,
|
|
bonusCycles: Go.storedCycles,
|
|
};
|
|
}
|
|
|
|
export function getMoveHistory(): SimpleBoard[] {
|
|
return Go.currentGame.previousBoards.map((boardString) => simpleBoardFromBoardString(boardString));
|
|
}
|
|
|
|
/**
|
|
* Returns 'None' if the game is over, otherwise returns the color of the current player's turn
|
|
*/
|
|
export function getCurrentPlayer(): "None" | "White" | "Black" {
|
|
if (Go.currentGame.previousPlayer === null) {
|
|
return "None";
|
|
}
|
|
return Go.currentGame.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
|
|
}
|
|
|
|
/**
|
|
* Handle post-game logging
|
|
*/
|
|
function logEndGame(logger: (s: string) => void) {
|
|
const boardState = Go.currentGame;
|
|
const score = getScore(boardState);
|
|
logger(
|
|
`Subnet complete! Final score: ${boardState.ai}: ${score[GoColor.white].sum}, Player: ${score[GoColor.black].sum}`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clears the board, resets winstreak if applicable
|
|
*/
|
|
export function resetBoardState(
|
|
logger: (s: string) => void,
|
|
error: (s: string) => void,
|
|
opponent: GoOpponent,
|
|
boardSize: number,
|
|
) {
|
|
if (![5, 7, 9, 13].includes(boardSize) && opponent !== GoOpponent.w0r1d_d43m0n) {
|
|
error(`Invalid subnet size requested (${boardSize}), size must be 5, 7, 9, or 13`);
|
|
return;
|
|
}
|
|
|
|
if (opponent === GoOpponent.w0r1d_d43m0n && !Player.hasAugmentation(AugmentationName.TheRedPill, true)) {
|
|
error(`Invalid opponent requested (${opponent}), this opponent has not yet been discovered`);
|
|
return;
|
|
}
|
|
|
|
const oldBoardState = Go.currentGame;
|
|
if (oldBoardState.previousPlayer !== null && oldBoardState.previousBoards.length) {
|
|
resetWinstreak(oldBoardState.ai, false);
|
|
}
|
|
|
|
resetAI();
|
|
Go.currentGame = getNewBoardState(boardSize, opponent, true);
|
|
handleNextTurn(Go.currentGame).catch((error) => exceptionAlert(error));
|
|
logger(`New game started: ${opponent}, ${boardSize}x${boardSize}`);
|
|
return simpleBoardFromBoard(Go.currentGame.board);
|
|
}
|
|
|
|
/**
|
|
* Retrieve and clean up stats for each opponent played against
|
|
*/
|
|
export function getStats() {
|
|
const statDetails: Partial<Record<GoOpponent, SimpleOpponentStats>> = {};
|
|
for (const opponent of getRecordKeys(Go.stats)) {
|
|
const details = getOpponentStats(opponent);
|
|
const nodePower = getOpponentStats(opponent).nodePower;
|
|
const effectPercent = (CalculateEffect(nodePower, opponent) - 1) * 100;
|
|
const effectDescription = getEffectTypeForFaction(opponent);
|
|
statDetails[opponent] = {
|
|
wins: details.wins,
|
|
losses: details.losses,
|
|
winStreak: details.winStreak,
|
|
highestWinStreak: details.highestWinStreak,
|
|
favor: details.favor,
|
|
bonusPercent: effectPercent,
|
|
bonusDescription: effectDescription,
|
|
};
|
|
}
|
|
|
|
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",
|
|
badType: "Invalid boardState: Board must be an array of strings",
|
|
badSize: "Invalid boardState: Board must be 5, 7, 9, 13, or 19 in size",
|
|
badCharacters:
|
|
'Invalid board state: unknown characters found. "X" represents black pieces, "O" white, "." empty points, and "#" offline nodes.',
|
|
failedToCreateBoard: "Invalid board state: Failed to create board",
|
|
} as const;
|
|
|
|
/**
|
|
* 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) => never,
|
|
_boardState?: unknown,
|
|
_priorBoardState?: unknown,
|
|
): BoardState | undefined {
|
|
const simpleBoard = getSimpleBoardFromUnknown(error, _boardState);
|
|
const priorSimpleBoard = getSimpleBoardFromUnknown(error, _priorBoardState);
|
|
|
|
if (!_boardState || !simpleBoard) {
|
|
return undefined;
|
|
}
|
|
|
|
try {
|
|
return getNewBoardStateFromSimpleBoard(simpleBoard, priorSimpleBoard);
|
|
} catch (e) {
|
|
error(boardValidity.failedToCreateBoard);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check that the given boardState is a valid SimpleBoard, and return it if it is.
|
|
*/
|
|
function getSimpleBoardFromUnknown(error: (arg0: string) => never, _boardState: unknown): SimpleBoard | undefined {
|
|
if (!_boardState) {
|
|
return undefined;
|
|
}
|
|
if (!Array.isArray(_boardState)) {
|
|
error(boardValidity.badType);
|
|
}
|
|
if ((_boardState as unknown[]).find((row) => typeof row !== "string")) {
|
|
error(boardValidity.badType);
|
|
}
|
|
|
|
const boardState = _boardState as string[];
|
|
|
|
if (boardState.find((row) => row.length !== boardState.length)) {
|
|
error(boardValidity.badShape);
|
|
}
|
|
if (![5, 7, 9, 13, 19].includes(boardState.length)) {
|
|
error(boardValidity.badSize);
|
|
}
|
|
if (boardState.find((row) => row.match(/[^XO#.]/))) {
|
|
error(boardValidity.badCharacters);
|
|
}
|
|
return boardState as SimpleBoard;
|
|
}
|
|
|
|
/** Validate singularity access by throwing an error if the player does not have access. */
|
|
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) {
|
|
error(
|
|
`The go.cheat API requires Source-File 14.2 to run, a power up you obtain later in the game.
|
|
It will be very obvious when and how you can obtain it.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines if the attempted cheat move is successful. If so, applies the cheat via the callback, and gets the opponent's response.
|
|
*
|
|
* If it fails, determines if the player's turn is skipped, or if the player is ejected from the subnet.
|
|
*/
|
|
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, playAsWhite)) {
|
|
callback();
|
|
}
|
|
// If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly losing
|
|
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 handleNextTurn(state, true);
|
|
} else {
|
|
// If the cheat fails, your turn is skipped
|
|
logger(`Cheat failed. Your turn has been skipped.`);
|
|
passTurn(state, playerColor, false);
|
|
}
|
|
|
|
if (playAsWhite) {
|
|
state.cheatCountForWhite++;
|
|
} else {
|
|
state.cheatCount++;
|
|
}
|
|
Go.currentGame.previousPlayer = playerColor;
|
|
updateCaptures(Go.currentGame.board, playerColor, true);
|
|
|
|
return handleNextTurn(state, true);
|
|
}
|
|
|
|
/**
|
|
* Cheating success rate scales with player's crime success rate, and decreases with prior cheat attempts.
|
|
*
|
|
* The source file bonus is additive success chance on top of the other multipliers.
|
|
*
|
|
* Cheat success chance required for N cheats with 100% success rate in a game:
|
|
*
|
|
* 1 100% success rate cheat requires +66% increased crime success rate
|
|
* 2 100% success cheats: +145% increased crime success rate
|
|
* 3: +282%
|
|
* 4: +535%
|
|
* 5: +1027%
|
|
* 7: +4278%
|
|
* 10: +59,854%
|
|
* 12: +534,704%
|
|
* 15: +31,358,645%
|
|
*/
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Attempts to remove an existing router from the board. Can fail. If failed, can immediately end the game
|
|
*/
|
|
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) {
|
|
error(`Cheat failed. The point ${x},${y} is already offline.`);
|
|
}
|
|
return determineCheatSuccess(
|
|
logger,
|
|
() => {
|
|
point.color = GoColor.empty;
|
|
logger(`Cheat successful. The point ${x},${y} was cleared.`);
|
|
},
|
|
successRngOverride,
|
|
ejectRngOverride,
|
|
playAsWhite,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Attempts play two moves at once. Can fail. If failed, can immediately end the game
|
|
*/
|
|
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) {
|
|
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 = playerColor;
|
|
point2.color = playerColor;
|
|
|
|
logger(`Cheat successful. Two go moves played: ${x1},${y1} and ${x2},${y2}`);
|
|
},
|
|
successRngOverride,
|
|
ejectRngOverride,
|
|
playAsWhite,
|
|
);
|
|
}
|
|
|
|
export function cheatRepairOfflineNode(
|
|
logger: (s: string) => void,
|
|
x: number,
|
|
y: number,
|
|
successRngOverride?: number,
|
|
ejectRngOverride?: number,
|
|
playAsWhite = false,
|
|
): Promise<Play> {
|
|
return determineCheatSuccess(
|
|
logger,
|
|
() => {
|
|
Go.currentGame.board[x][y] = {
|
|
chain: "",
|
|
liberties: null,
|
|
y,
|
|
color: GoColor.empty,
|
|
x,
|
|
};
|
|
logger(`Cheat successful. The point ${x},${y} was repaired.`);
|
|
},
|
|
successRngOverride,
|
|
ejectRngOverride,
|
|
playAsWhite,
|
|
);
|
|
}
|
|
|
|
export function cheatDestroyNode(
|
|
logger: (s: string) => void,
|
|
x: number,
|
|
y: number,
|
|
successRngOverride?: number,
|
|
ejectRngOverride?: number,
|
|
playAsWhite = false,
|
|
): Promise<Play> {
|
|
return determineCheatSuccess(
|
|
logger,
|
|
() => {
|
|
Go.currentGame.board[x][y] = null;
|
|
logger(`Cheat successful. The point ${x},${y} was destroyed.`);
|
|
},
|
|
successRngOverride,
|
|
ejectRngOverride,
|
|
playAsWhite,
|
|
);
|
|
}
|