mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-24 16:43:51 +01:00
GO: Various changes before 2.6.0 (#1120)
This commit is contained in:
parent
f6871f0911
commit
373ced2efe
@ -10,7 +10,7 @@ import { ButtonWithTooltip } from "../../ui/Components/ButtonWithTooltip";
|
||||
import { MaxSleevesFromCovenant } from "../../PersonObjects/Sleeve/SleeveCovenantPurchases";
|
||||
|
||||
// Update as additional BitNodes get implemented
|
||||
const validSFN = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
||||
const validSFN = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
|
||||
const useStyles = makeStyles({
|
||||
group: {
|
||||
display: "inline-flex",
|
||||
|
@ -6,6 +6,7 @@ export * from "./Company/Enums";
|
||||
export * from "./Corporation/Enums";
|
||||
export * from "./Crime/Enums";
|
||||
export * from "./Faction/Enums";
|
||||
export * from "./Go/Enums";
|
||||
export * from "./Literature/Enums";
|
||||
export * from "./Locations/Enums";
|
||||
export * from "./Message/Enums";
|
||||
|
107
src/Go/Constants.ts
Normal file
107
src/Go/Constants.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import type { OpponentStats, SimpleBoard } from "./Types";
|
||||
|
||||
import { GoOpponent } from "@enums";
|
||||
|
||||
export const opponentDetails = {
|
||||
[GoOpponent.none]: {
|
||||
komi: 5.5,
|
||||
description: "Practice Board",
|
||||
flavorText: "Practice on a subnet where you place both colors of routers.",
|
||||
bonusDescription: "",
|
||||
bonusPower: 0,
|
||||
},
|
||||
[GoOpponent.Netburners]: {
|
||||
komi: 1.5,
|
||||
description: "Easy AI",
|
||||
flavorText:
|
||||
"The Netburners faction are a mysterious group with only the most tenuous control over their subnets. Concentrating mainly on their hacknet server business, IPvGO is not their main strength.",
|
||||
bonusDescription: "increased hacknet production",
|
||||
bonusPower: 1.3,
|
||||
},
|
||||
[GoOpponent.SlumSnakes]: {
|
||||
komi: 3.5,
|
||||
description: "Spread AI",
|
||||
flavorText:
|
||||
"The Slum Snakes faction are a small-time street gang who turned to organized crime using their subnets. They are known to use long router chains snaking across the subnet to encircle territory.",
|
||||
bonusDescription: "crime success rate",
|
||||
bonusPower: 1.2,
|
||||
},
|
||||
[GoOpponent.TheBlackHand]: {
|
||||
komi: 3.5,
|
||||
description: "Aggro AI",
|
||||
flavorText:
|
||||
"The Black Hand faction is a black-hat hacking group who uses their subnets to launch targeted DDOS attacks. They are famous for their unrelenting aggression, surrounding and strangling any foothold their opponents try to establish.",
|
||||
bonusDescription: "hacking money",
|
||||
bonusPower: 0.9,
|
||||
},
|
||||
[GoOpponent.Tetrads]: {
|
||||
komi: 5.5,
|
||||
description: "Martial AI",
|
||||
flavorText:
|
||||
"The faction known as Tetrads prefers to get up close and personal. Their combat style excels at circling around and cutting through their opponents, both on and off of the subnets.",
|
||||
bonusDescription: "strength, dex, and agility levels",
|
||||
bonusPower: 0.7,
|
||||
},
|
||||
[GoOpponent.Daedalus]: {
|
||||
komi: 5.5,
|
||||
description: "Mid AI",
|
||||
flavorText:
|
||||
"Not much is known about this shadowy faction. They do not easily let go of subnets that they control, and are known to lease IPvGO cycles in exchange for reputation among other factions.",
|
||||
bonusDescription: "reputation gain",
|
||||
bonusPower: 1.1,
|
||||
},
|
||||
[GoOpponent.Illuminati]: {
|
||||
komi: 7.5,
|
||||
description: "Hard AI",
|
||||
flavorText:
|
||||
"The Illuminati are thought to only exist in myth. Said to always have prepared defenses in their IPvGO subnets. Provoke them at your own risk.",
|
||||
bonusDescription: "faster hack(), grow(), and weaken()",
|
||||
bonusPower: 0.7,
|
||||
},
|
||||
[GoOpponent.w0r1d_d43m0n]: {
|
||||
komi: 9.5,
|
||||
description: "???",
|
||||
flavorText: "What you have seen is only the shadow of the truth. It's time to leave the cave.",
|
||||
bonusDescription: "hacking level",
|
||||
bonusPower: 2,
|
||||
},
|
||||
};
|
||||
|
||||
export const boardSizes = [5, 7, 9, 13];
|
||||
|
||||
export const columnIndexes = "ABCDEFGHJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
export function newOpponentStats(): OpponentStats {
|
||||
return {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
nodes: 0,
|
||||
nodePower: 0,
|
||||
winStreak: 0,
|
||||
oldWinStreak: 0,
|
||||
highestWinStreak: 0,
|
||||
favor: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export const bitverseBoardShape: SimpleBoard = [
|
||||
"########...########",
|
||||
"######.#...#.######",
|
||||
"###.#..#...#..#.###",
|
||||
".#..#..#...#..#..#.",
|
||||
".#.....#...#.....#.",
|
||||
"...................",
|
||||
"...................",
|
||||
"...................",
|
||||
"...................",
|
||||
".....##.....##.....",
|
||||
"....###.....###....",
|
||||
"....##.......##....",
|
||||
"....#.........#....",
|
||||
".........#.........",
|
||||
"#........#........#",
|
||||
"##.......#.......##",
|
||||
"##.......#.......##",
|
||||
"###.............###",
|
||||
"####...........####",
|
||||
];
|
34
src/Go/Enums.ts
Normal file
34
src/Go/Enums.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export enum GoOpponent {
|
||||
none = "No AI",
|
||||
Netburners = "Netburners",
|
||||
SlumSnakes = "Slum Snakes",
|
||||
TheBlackHand = "The Black Hand",
|
||||
Tetrads = "Tetrads",
|
||||
Daedalus = "Daedalus",
|
||||
Illuminati = "Illuminati",
|
||||
w0r1d_d43m0n = "????????????",
|
||||
}
|
||||
|
||||
export enum GoColor {
|
||||
white = "White",
|
||||
black = "Black",
|
||||
empty = "Empty",
|
||||
}
|
||||
|
||||
export enum GoValidity {
|
||||
pointBroken = "That node is offline; a piece cannot be placed there",
|
||||
pointNotEmpty = "That node is already occupied by a piece",
|
||||
boardRepeated = "It is illegal to repeat prior board states",
|
||||
noSuicide = "It is illegal to cause your own pieces to be captured",
|
||||
notYourTurn = "It is not your turn to play",
|
||||
gameOver = "The game is over",
|
||||
invalid = "Invalid move",
|
||||
valid = "Valid move",
|
||||
}
|
||||
|
||||
export enum GoPlayType {
|
||||
invalid = "invalid",
|
||||
move = "move",
|
||||
pass = "pass",
|
||||
gameOver = "gameOver",
|
||||
}
|
31
src/Go/Go.ts
Normal file
31
src/Go/Go.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { GoOpponent } from "./Enums";
|
||||
import type { BoardState, OpponentStats } from "./Types";
|
||||
|
||||
import { getRecordValues, PartialRecord } from "../Types/Record";
|
||||
import { getNewBoardState } from "./boardState/boardState";
|
||||
import { EventEmitter } from "../utils/EventEmitter";
|
||||
|
||||
export class GoObject {
|
||||
// Todo: Make previous game a slimmer interface
|
||||
previousGame: BoardState | null = null;
|
||||
currentGame: BoardState = getNewBoardState(7);
|
||||
stats: PartialRecord<GoOpponent, OpponentStats> = {};
|
||||
|
||||
prestigeAugmentation() {
|
||||
for (const stats of getRecordValues(this.stats)) {
|
||||
stats.nodePower = 0;
|
||||
stats.nodes = 0;
|
||||
stats.winStreak = 0;
|
||||
}
|
||||
}
|
||||
prestigeSourceFile() {
|
||||
this.previousGame = null;
|
||||
this.currentGame = getNewBoardState(7);
|
||||
this.stats = {};
|
||||
}
|
||||
}
|
||||
|
||||
export const Go = new GoObject();
|
||||
|
||||
/** Event emitter to allow the UI to subscribe to Go gameplay updates in order to trigger rerenders properly */
|
||||
export const GoEvents = new EventEmitter();
|
171
src/Go/SaveLoad.ts
Normal file
171
src/Go/SaveLoad.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import type { BoardState, OpponentStats, SimpleBoard } from "./Types";
|
||||
import type { PartialRecord } from "../Types/Record";
|
||||
|
||||
import { Truthy } from "lodash";
|
||||
import { GoColor, GoOpponent } from "@enums";
|
||||
import { Go } from "./Go";
|
||||
import { boardStateFromSimpleBoard, simpleBoardFromBoard } from "./boardAnalysis/boardAnalysis";
|
||||
import { assertLoadingType } from "../utils/JSONReviver";
|
||||
import { getEnumHelper } from "../utils/EnumHelper";
|
||||
import { boardSizes } from "./Constants";
|
||||
import { isInteger, isNumber } from "../types";
|
||||
|
||||
type PreviousGameSaveData = { ai: GoOpponent; board: SimpleBoard; previousPlayer: GoColor | null } | null;
|
||||
type CurrentGameSaveData = PreviousGameSaveData & {
|
||||
previousBoard: SimpleBoard | null;
|
||||
cheatCount: number;
|
||||
passCount: number;
|
||||
};
|
||||
|
||||
type SaveFormat = {
|
||||
previousGame: PreviousGameSaveData;
|
||||
currentGame: CurrentGameSaveData;
|
||||
stats: PartialRecord<GoOpponent, OpponentStats>;
|
||||
};
|
||||
|
||||
export function getGoSave(): SaveFormat {
|
||||
return {
|
||||
previousGame: Go.previousGame
|
||||
? {
|
||||
ai: Go.previousGame.ai,
|
||||
board: simpleBoardFromBoard(Go.previousGame.board),
|
||||
previousPlayer: Go.previousGame.previousPlayer,
|
||||
}
|
||||
: null,
|
||||
currentGame: {
|
||||
ai: Go.currentGame.ai,
|
||||
board: simpleBoardFromBoard(Go.currentGame.board),
|
||||
previousBoard: Go.currentGame.previousBoard,
|
||||
previousPlayer: Go.currentGame.previousPlayer,
|
||||
cheatCount: Go.currentGame.cheatCount,
|
||||
passCount: Go.currentGame.passCount,
|
||||
},
|
||||
stats: Go.stats,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadGo(data: unknown): boolean {
|
||||
/** Function for ending the loading process, showing an error if there is one, and indicating load success/failure */
|
||||
function showError(error: unknown): boolean {
|
||||
console.warn("Encountered the following issue while loading Go savedata:");
|
||||
console.error(error);
|
||||
console.warn("Savedata:");
|
||||
console.error(data);
|
||||
return false;
|
||||
}
|
||||
if (!data) return showError("There was no go savedata");
|
||||
// Parsing the savedata
|
||||
if (typeof data !== "string") return showError("Savedata was not a string");
|
||||
let parsedData;
|
||||
try {
|
||||
parsedData = JSON.parse(data) as unknown;
|
||||
} catch (e) {
|
||||
return showError(`Cannot JSON.parse the savedata: ${data}`);
|
||||
}
|
||||
if (!parsedData || typeof parsedData !== "object") return showError("Parsed savedata was not an object");
|
||||
assertLoadingType<SaveFormat>(parsedData);
|
||||
// currentGame
|
||||
const currentGame = loadCurrentGame(parsedData.currentGame);
|
||||
if (typeof currentGame === "string") return showError(currentGame);
|
||||
|
||||
// previousGame
|
||||
const previousGame = loadPreviousGame(parsedData.previousGame);
|
||||
if (typeof previousGame === "string") return showError(previousGame);
|
||||
|
||||
// stats
|
||||
const stats = loadStats(parsedData.stats);
|
||||
if (typeof stats === "string") return showError(stats);
|
||||
|
||||
Go.currentGame = currentGame;
|
||||
Go.previousGame = previousGame;
|
||||
Go.stats = stats;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Loading for Go.currentGame
|
||||
* @returns The currentGame object if it can be loaded with no issues. IF there is an issue, a string is returned instead describing the issue. */
|
||||
function loadCurrentGame(currentGame: unknown): BoardState | string {
|
||||
if (!currentGame) return "Savedata did not contain a currentGame";
|
||||
assertLoadingType<CurrentGameSaveData>(currentGame);
|
||||
const ai = getEnumHelper("GoOpponent").getMember(currentGame.ai);
|
||||
if (!ai) return `currentGame had an invalid opponent: ${currentGame.ai}`;
|
||||
|
||||
if (!Array.isArray(currentGame.board)) return "Non-array encountered while trying to load a board.";
|
||||
const requiredSize = currentGame.board.length;
|
||||
const board = loadSimpleBoard(currentGame.board, requiredSize);
|
||||
if (typeof board === "string") return board;
|
||||
const previousBoard = currentGame.previousBoard ? loadSimpleBoard(currentGame.previousBoard, requiredSize) : null;
|
||||
if (typeof previousBoard === "string") return previousBoard;
|
||||
const previousPlayer = getEnumHelper("GoColor").getMember(currentGame.previousPlayer) ?? null;
|
||||
if (!isInteger(currentGame.cheatCount) || currentGame.cheatCount < 0)
|
||||
return "invalid number for currentGame.cheatCount";
|
||||
if (!isInteger(currentGame.passCount) || currentGame.passCount < 0) return "invalid number for currentGame.passCount";
|
||||
|
||||
const boardState = boardStateFromSimpleBoard(board, ai);
|
||||
boardState.previousPlayer = previousPlayer;
|
||||
boardState.cheatCount = currentGame.cheatCount;
|
||||
boardState.passCount = currentGame.passCount;
|
||||
boardState.previousBoard = previousBoard;
|
||||
return boardState;
|
||||
}
|
||||
|
||||
/** Loading for Go.previousGame
|
||||
* @returns The previousGame object if it can be loaded with no issues. IF there is an issue, a string is returned instead describing the issue. */
|
||||
function loadPreviousGame(previousGame: unknown): BoardState | null | string {
|
||||
if (!previousGame) return null;
|
||||
assertLoadingType<Truthy<PreviousGameSaveData>>(previousGame);
|
||||
const ai = getEnumHelper("GoOpponent").getMember(previousGame.ai);
|
||||
if (!ai) return `currentGame had an invalid opponent: ${previousGame.ai}`;
|
||||
|
||||
if (!Array.isArray(previousGame.board)) return "Non-array encountered while trying to load a board.";
|
||||
const board = loadSimpleBoard(previousGame.board);
|
||||
if (typeof board === "string") return board;
|
||||
const previousPlayer = getEnumHelper("GoColor").getMember(previousGame.previousPlayer) ?? null;
|
||||
|
||||
const boardState = boardStateFromSimpleBoard(board, ai);
|
||||
boardState.previousPlayer = previousPlayer;
|
||||
return boardState;
|
||||
}
|
||||
|
||||
/** Loading for Go.stats
|
||||
* @returns The stats object if it can be loaded with no issues. IF there is an issue, a string is returned instead describing the issue. */
|
||||
function loadStats(stats: unknown): PartialRecord<GoOpponent, OpponentStats> | string {
|
||||
const finalStats: PartialRecord<GoOpponent, OpponentStats> = {};
|
||||
if (!stats) return "Savedata did not contain a stats object.";
|
||||
if (typeof stats !== "object") return "Non-object encountered for Go.stats";
|
||||
const entries = Object.entries(stats);
|
||||
for (const [opponent, opponentStats] of entries) {
|
||||
if (!getEnumHelper("GoOpponent").isMember(opponent)) return `Invalid opponent in Go.stats: ${opponent}`;
|
||||
if (!opponentStats || typeof opponentStats !== "object") "Non-object encountered for an opponent's stats";
|
||||
assertLoadingType<OpponentStats>(opponentStats);
|
||||
const { favor, highestWinStreak, losses, nodes, wins, oldWinStreak, winStreak, nodePower } = opponentStats;
|
||||
// Integers >= 0. Todo: make a better helper for this.
|
||||
if (!isInteger(favor) || favor < 0) return "A favor entry in Go.stats was invalid";
|
||||
if (!isInteger(highestWinStreak) || highestWinStreak < 0) return "A highestWinStreak entry in Go.stats was invalid";
|
||||
if (!isInteger(losses) || losses < 0) return "A losses entry in Go.stats was invalid";
|
||||
if (!isInteger(nodes) || nodes < 0) return "A nodes entry in Go.stats was invalid";
|
||||
if (!isInteger(wins) || wins < 0) return "A wins entry in Go.stats was invalid";
|
||||
|
||||
// Integers with no clamping
|
||||
if (!isInteger(oldWinStreak)) return "An oldWinStreak entry in Go.stats was invalid";
|
||||
if (!isInteger(winStreak)) return "An oldWinStreak entry in Go.stats was invalid";
|
||||
|
||||
// Numbers >= 0
|
||||
if (!isNumber(nodePower) || nodePower < 0) return "A nodePower entry in Go.stats was invalid";
|
||||
finalStats[opponent] = { favor, highestWinStreak, losses, nodes, wins, oldWinStreak, winStreak, nodePower };
|
||||
}
|
||||
return finalStats;
|
||||
}
|
||||
|
||||
/** Loading for a SimpleBoard. Also used to load real boards, which are converted from simple boards higher up.
|
||||
* @returns The SimpleBoard object if it can be loaded with no issues. If there is an issue, a string is returned instead describing the issue. */
|
||||
function loadSimpleBoard(simpleBoard: unknown, requiredSize?: number): SimpleBoard | string {
|
||||
if (!Array.isArray(simpleBoard)) return "Non-array encountered while trying to load a SimpleBoard.";
|
||||
requiredSize ??= simpleBoard.length;
|
||||
if (!boardSizes.includes(requiredSize)) return `Invalid board size when loading a SimpleBoard: ${requiredSize}`;
|
||||
if (simpleBoard.length !== requiredSize) return "Incorrect size while trying to load a SimpleBoard";
|
||||
if (!simpleBoard.every((column) => typeof column === "string" && column.length === requiredSize)) {
|
||||
return "Incorrect types or column size while loading a SimpleBoard.";
|
||||
}
|
||||
return simpleBoard;
|
||||
}
|
82
src/Go/Types.ts
Normal file
82
src/Go/Types.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import type { GoColor, GoOpponent, GoPlayType } from "@enums";
|
||||
|
||||
export type Board = (PointState | null)[][];
|
||||
|
||||
export type SimpleBoard = string[];
|
||||
|
||||
export type Move = {
|
||||
point: PointState;
|
||||
oldLibertyCount?: number | null;
|
||||
newLibertyCount?: number | null;
|
||||
createsLife?: boolean;
|
||||
};
|
||||
|
||||
type MoveType =
|
||||
| "capture"
|
||||
| "defendCapture"
|
||||
| "eyeMove"
|
||||
| "eyeBlock"
|
||||
| "pattern"
|
||||
| "growth"
|
||||
| "expansion"
|
||||
| "jump"
|
||||
| "defend"
|
||||
| "surround"
|
||||
| "corner"
|
||||
| "random";
|
||||
|
||||
type MoveFunction = () => Promise<Move | null>;
|
||||
export type MoveOptions = Record<MoveType, MoveFunction>;
|
||||
|
||||
export type EyeMove = {
|
||||
point: PointState;
|
||||
createsLife: boolean;
|
||||
};
|
||||
|
||||
export type BoardState = {
|
||||
board: Board;
|
||||
previousPlayer: GoColor | null;
|
||||
/** The previous board position as a SimpleBoard */
|
||||
previousBoard: SimpleBoard | null;
|
||||
ai: GoOpponent;
|
||||
passCount: number;
|
||||
cheatCount: number;
|
||||
};
|
||||
|
||||
export type PointState = {
|
||||
color: GoColor;
|
||||
chain: string;
|
||||
liberties: (PointState | null)[] | null;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Play = {
|
||||
success: boolean;
|
||||
type: GoPlayType;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Neighbor = {
|
||||
north: PointState | null;
|
||||
east: PointState | null;
|
||||
south: PointState | null;
|
||||
west: PointState | null;
|
||||
};
|
||||
|
||||
export type GoScore = {
|
||||
White: { pieces: number; territory: number; komi: number; sum: number };
|
||||
Black: { pieces: number; territory: number; komi: number; sum: number };
|
||||
};
|
||||
|
||||
export type OpponentStats = {
|
||||
wins: number;
|
||||
losses: number;
|
||||
nodes: number;
|
||||
nodePower: number;
|
||||
winStreak: number;
|
||||
oldWinStreak: number;
|
||||
highestWinStreak: number;
|
||||
favor: number;
|
||||
};
|
@ -1,13 +1,6 @@
|
||||
import {
|
||||
Board,
|
||||
BoardState,
|
||||
Neighbor,
|
||||
opponents,
|
||||
PlayerColor,
|
||||
playerColors,
|
||||
PointState,
|
||||
validityReason,
|
||||
} from "../boardState/goConstants";
|
||||
import type { Board, BoardState, Neighbor, PointState, SimpleBoard } from "../Types";
|
||||
|
||||
import { GoValidity, GoOpponent, GoColor } from "@enums";
|
||||
import {
|
||||
findAdjacentPointsInChain,
|
||||
findNeighbors,
|
||||
@ -15,7 +8,6 @@ import {
|
||||
getBoardCopy,
|
||||
getEmptySpaces,
|
||||
getNewBoardState,
|
||||
getStateCopy,
|
||||
isDefined,
|
||||
isNotNull,
|
||||
updateCaptures,
|
||||
@ -35,120 +27,106 @@ import {
|
||||
*
|
||||
* @returns a validity explanation for if the move is legal or not
|
||||
*/
|
||||
export function evaluateIfMoveIsValid(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: PlayerColor,
|
||||
shortcut = true,
|
||||
) {
|
||||
const point = boardState.board?.[x]?.[y];
|
||||
export function evaluateIfMoveIsValid(boardState: BoardState, x: number, y: number, player: GoColor, shortcut = true) {
|
||||
const point = boardState.board[x]?.[y];
|
||||
|
||||
if (boardState.previousPlayer === null) {
|
||||
return validityReason.gameOver;
|
||||
return GoValidity.gameOver;
|
||||
}
|
||||
if (boardState.previousPlayer === player) {
|
||||
return validityReason.notYourTurn;
|
||||
return GoValidity.notYourTurn;
|
||||
}
|
||||
if (!point) {
|
||||
return validityReason.pointBroken;
|
||||
return GoValidity.pointBroken;
|
||||
}
|
||||
if (point.player !== playerColors.empty) {
|
||||
return validityReason.pointNotEmpty;
|
||||
if (point.color !== GoColor.empty) {
|
||||
return GoValidity.pointNotEmpty;
|
||||
}
|
||||
|
||||
// Detect if the current player has ever previously played this move. Used to detect potential repeated board states
|
||||
const moveHasBeenPlayedBefore = !!boardState.history.find((board) => board[x]?.[y]?.player === player);
|
||||
// Detect if the move might be an immediate repeat (only one board of history is saved to check)
|
||||
const possibleRepeat = boardState.previousBoard && getColorOnSimpleBoard(boardState.previousBoard, x, y) === player;
|
||||
|
||||
if (shortcut) {
|
||||
// If the current point has some adjacent open spaces, it is not suicide. If the move is not repeated, it is legal
|
||||
const liberties = findAdjacentLibertiesForPoint(boardState, x, y);
|
||||
const liberties = findAdjacentLibertiesForPoint(boardState.board, x, y);
|
||||
const hasLiberty = liberties.north || liberties.east || liberties.south || liberties.west;
|
||||
if (!moveHasBeenPlayedBefore && hasLiberty) {
|
||||
return validityReason.valid;
|
||||
if (!possibleRepeat && hasLiberty) {
|
||||
return GoValidity.valid;
|
||||
}
|
||||
|
||||
// If a connected friendly chain has more than one liberty, the move is not suicide. If the move is not repeated, it is legal
|
||||
const neighborChainLibertyCount = findMaxLibertyCountOfAdjacentChains(boardState, x, y, player);
|
||||
if (!moveHasBeenPlayedBefore && neighborChainLibertyCount > 1) {
|
||||
return validityReason.valid;
|
||||
if (!possibleRepeat && neighborChainLibertyCount > 1) {
|
||||
return GoValidity.valid;
|
||||
}
|
||||
|
||||
// If there is any neighboring enemy chain with only one liberty, and the move is not repeated, it is valid,
|
||||
// because it would capture the enemy chain and free up some liberties for itself
|
||||
const potentialCaptureChainLibertyCount = findMinLibertyCountOfAdjacentChains(
|
||||
boardState,
|
||||
boardState.board,
|
||||
x,
|
||||
y,
|
||||
player === playerColors.black ? playerColors.white : playerColors.black,
|
||||
player === GoColor.black ? GoColor.white : GoColor.black,
|
||||
);
|
||||
if (!moveHasBeenPlayedBefore && potentialCaptureChainLibertyCount < 2) {
|
||||
return validityReason.valid;
|
||||
if (!possibleRepeat && potentialCaptureChainLibertyCount < 2) {
|
||||
return GoValidity.valid;
|
||||
}
|
||||
|
||||
// If there is no direct liberties for the move, no captures, and no neighboring friendly chains with multiple liberties,
|
||||
// the move is not valid because it would suicide the piece
|
||||
if (!hasLiberty && potentialCaptureChainLibertyCount >= 2 && neighborChainLibertyCount <= 1) {
|
||||
return validityReason.noSuicide;
|
||||
return GoValidity.noSuicide;
|
||||
}
|
||||
}
|
||||
|
||||
// If the move has been played before and is not obviously illegal, we have to actually play it out to determine
|
||||
// if it is a repeated move, or if it is a valid move
|
||||
const evaluationBoard = evaluateMoveResult(boardState, x, y, player, true);
|
||||
if (evaluationBoard.board[x]?.[y]?.player !== player) {
|
||||
return validityReason.noSuicide;
|
||||
const evaluationBoard = evaluateMoveResult(boardState.board, x, y, player, true);
|
||||
if (evaluationBoard[x]?.[y]?.color !== player) {
|
||||
return GoValidity.noSuicide;
|
||||
}
|
||||
if (moveHasBeenPlayedBefore && checkIfBoardStateIsRepeated(evaluationBoard)) {
|
||||
return validityReason.boardRepeated;
|
||||
if (possibleRepeat && boardState.previousBoard) {
|
||||
const simpleEvalBoard = simpleBoardFromBoard(evaluationBoard);
|
||||
if (areSimpleBoardsIdentical(simpleEvalBoard, boardState.previousBoard)) return GoValidity.boardRepeated;
|
||||
}
|
||||
|
||||
return validityReason.valid;
|
||||
return GoValidity.valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new evaluation board and play out the results of the given move on the new board
|
||||
* @returns the evaluation board
|
||||
*/
|
||||
export function evaluateMoveResult(
|
||||
initialBoardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: playerColors,
|
||||
resetChains = false,
|
||||
) {
|
||||
const boardState = getStateCopy(initialBoardState);
|
||||
boardState.history.push(getBoardCopy(boardState).board);
|
||||
const point = boardState.board[x]?.[y];
|
||||
if (!point) {
|
||||
return initialBoardState;
|
||||
}
|
||||
export function evaluateMoveResult(board: Board, x: number, y: number, player: GoColor, resetChains = false): Board {
|
||||
const evaluationBoard = getBoardCopy(board);
|
||||
const point = evaluationBoard[x]?.[y];
|
||||
if (!point) return board;
|
||||
|
||||
point.player = player;
|
||||
boardState.previousPlayer = player;
|
||||
point.color = player;
|
||||
|
||||
const neighbors = getArrayFromNeighbor(findNeighbors(boardState, x, y));
|
||||
const neighbors = getArrayFromNeighbor(findNeighbors(board, x, y));
|
||||
const chainIdsToUpdate = [point.chain, ...neighbors.map((point) => point.chain)];
|
||||
resetChainsById(boardState, chainIdsToUpdate);
|
||||
|
||||
return updateCaptures(boardState, player, resetChains);
|
||||
resetChainsById(evaluationBoard, chainIdsToUpdate);
|
||||
updateCaptures(evaluationBoard, player, resetChains);
|
||||
return evaluationBoard;
|
||||
}
|
||||
|
||||
export function getControlledSpace(boardState: BoardState) {
|
||||
const chains = getAllChains(boardState);
|
||||
const length = boardState.board[0].length;
|
||||
const whiteControlledEmptyNodes = getAllPotentialEyes(boardState, chains, playerColors.white, length * 2)
|
||||
export function getControlledSpace(board: Board) {
|
||||
const chains = getAllChains(board);
|
||||
const length = board[0].length;
|
||||
const whiteControlledEmptyNodes = getAllPotentialEyes(board, chains, GoColor.white, length * 2)
|
||||
.map((eye) => eye.chain)
|
||||
.flat();
|
||||
const blackControlledEmptyNodes = getAllPotentialEyes(boardState, chains, playerColors.black, length * 2)
|
||||
const blackControlledEmptyNodes = getAllPotentialEyes(board, chains, GoColor.black, length * 2)
|
||||
.map((eye) => eye.chain)
|
||||
.flat();
|
||||
|
||||
const ownedPointGrid = Array.from({ length }, () => Array.from({ length }, () => playerColors.empty));
|
||||
const ownedPointGrid = Array.from({ length }, () => Array.from({ length }, () => GoColor.empty));
|
||||
whiteControlledEmptyNodes.forEach((node) => {
|
||||
ownedPointGrid[node.x][node.y] = playerColors.white;
|
||||
ownedPointGrid[node.x][node.y] = GoColor.white;
|
||||
});
|
||||
blackControlledEmptyNodes.forEach((node) => {
|
||||
ownedPointGrid[node.x][node.y] = playerColors.black;
|
||||
ownedPointGrid[node.x][node.y] = GoColor.black;
|
||||
});
|
||||
|
||||
return ownedPointGrid;
|
||||
@ -157,30 +135,28 @@ export function getControlledSpace(boardState: BoardState) {
|
||||
/**
|
||||
Clear the chain and liberty data of all points in the given chains
|
||||
*/
|
||||
const resetChainsById = (boardState: BoardState, chainIds: string[]) => {
|
||||
const pointsToUpdate = boardState.board
|
||||
.flat()
|
||||
.filter(isDefined)
|
||||
.filter(isNotNull)
|
||||
.filter((point) => chainIds.includes(point.chain));
|
||||
pointsToUpdate.forEach((point) => {
|
||||
const resetChainsById = (board: Board, chainIds: string[]) => {
|
||||
for (const column of board) {
|
||||
for (const point of column) {
|
||||
if (!point || !chainIds.includes(point.chain)) continue;
|
||||
point.chain = "";
|
||||
point.liberties = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* For a potential move, determine what the liberty of the point would be if played, by looking at adjacent empty nodes
|
||||
* as well as the remaining liberties of neighboring friendly chains
|
||||
*/
|
||||
export function findEffectiveLibertiesOfNewMove(boardState: BoardState, x: number, y: number, player: PlayerColor) {
|
||||
const friendlyChains = getAllChains(boardState).filter((chain) => chain[0].player === player);
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
|
||||
export function findEffectiveLibertiesOfNewMove(board: Board, x: number, y: number, player: GoColor) {
|
||||
const friendlyChains = getAllChains(board).filter((chain) => chain[0].color === player);
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(board, x, y, player);
|
||||
const neighborPoints = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined);
|
||||
// Get all chains that the new move will connect to
|
||||
const allyNeighbors = neighborPoints.filter((neighbor) => neighbor.player === player);
|
||||
const allyNeighbors = neighborPoints.filter((neighbor) => neighbor.color === player);
|
||||
const allyNeighborChainLiberties = allyNeighbors
|
||||
.map((neighbor) => {
|
||||
const chain = friendlyChains.find((chain) => chain[0].chain === neighbor.chain);
|
||||
@ -190,7 +166,7 @@ export function findEffectiveLibertiesOfNewMove(boardState: BoardState, x: numbe
|
||||
.filter(isNotNull);
|
||||
|
||||
// Get all empty spaces that the new move connects to that aren't already part of friendly liberties
|
||||
const directLiberties = neighborPoints.filter((neighbor) => neighbor.player === playerColors.empty);
|
||||
const directLiberties = neighborPoints.filter((neighbor) => neighbor.color === GoColor.empty);
|
||||
|
||||
const allLiberties = [...directLiberties, ...allyNeighborChainLiberties];
|
||||
|
||||
@ -206,17 +182,12 @@ export function findEffectiveLibertiesOfNewMove(boardState: BoardState, x: numbe
|
||||
/**
|
||||
* Find the number of open spaces that are connected to chains adjacent to a given point, and return the maximum
|
||||
*/
|
||||
export function findMaxLibertyCountOfAdjacentChains(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: playerColors,
|
||||
) {
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
|
||||
export function findMaxLibertyCountOfAdjacentChains(boardState: BoardState, x: number, y: number, player: GoColor) {
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState.board, x, y, player);
|
||||
const friendlyNeighbors = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined)
|
||||
.filter((neighbor) => neighbor.player === player);
|
||||
.filter((neighbor) => neighbor.color === player);
|
||||
|
||||
return friendlyNeighbors.reduce((max, neighbor) => Math.max(max, neighbor?.liberties?.length ?? 0), 0);
|
||||
}
|
||||
@ -224,28 +195,18 @@ export function findMaxLibertyCountOfAdjacentChains(
|
||||
/**
|
||||
* Find the number of open spaces that are connected to chains adjacent to a given point, and return the minimum
|
||||
*/
|
||||
export function findMinLibertyCountOfAdjacentChains(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: playerColors,
|
||||
) {
|
||||
const chain = findEnemyNeighborChainWithFewestLiberties(boardState, x, y, player);
|
||||
export function findMinLibertyCountOfAdjacentChains(board: Board, x: number, y: number, player: GoColor) {
|
||||
const chain = findEnemyNeighborChainWithFewestLiberties(board, x, y, player);
|
||||
return chain?.[0]?.liberties?.length ?? 99;
|
||||
}
|
||||
|
||||
export function findEnemyNeighborChainWithFewestLiberties(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: playerColors,
|
||||
) {
|
||||
const chains = getAllChains(boardState);
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
|
||||
export function findEnemyNeighborChainWithFewestLiberties(board: Board, x: number, y: number, player: GoColor) {
|
||||
const chains = getAllChains(board);
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(board, x, y, player);
|
||||
const friendlyNeighbors = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined)
|
||||
.filter((neighbor) => neighbor.player === player);
|
||||
.filter((neighbor) => neighbor.color === player);
|
||||
|
||||
const minimumLiberties = friendlyNeighbors.reduce(
|
||||
(min, neighbor) => Math.min(min, neighbor?.liberties?.length ?? 0),
|
||||
@ -259,9 +220,9 @@ export function findEnemyNeighborChainWithFewestLiberties(
|
||||
/**
|
||||
* Returns a list of points that are valid moves for the given player
|
||||
*/
|
||||
export function getAllValidMoves(boardState: BoardState, player: PlayerColor) {
|
||||
return getEmptySpaces(boardState).filter(
|
||||
(point) => evaluateIfMoveIsValid(boardState, point.x, point.y, player) === validityReason.valid,
|
||||
export function getAllValidMoves(boardState: BoardState, player: GoColor) {
|
||||
return getEmptySpaces(boardState.board).filter(
|
||||
(point) => evaluateIfMoveIsValid(boardState, point.x, point.y, player) === GoValidity.valid,
|
||||
);
|
||||
}
|
||||
|
||||
@ -272,9 +233,9 @@ export function getAllValidMoves(boardState: BoardState, player: PlayerColor) {
|
||||
|
||||
Eyes are important, because a chain of pieces cannot be captured if it fully surrounds two or more eyes.
|
||||
*/
|
||||
export function getAllEyesByChainId(boardState: BoardState, player: playerColors) {
|
||||
const allChains = getAllChains(boardState);
|
||||
const eyeCandidates = getAllPotentialEyes(boardState, allChains, player);
|
||||
export function getAllEyesByChainId(board: Board, player: GoColor) {
|
||||
const allChains = getAllChains(board);
|
||||
const eyeCandidates = getAllPotentialEyes(board, allChains, player);
|
||||
const eyes: { [s: string]: PointState[][] } = {};
|
||||
|
||||
eyeCandidates.forEach((candidate) => {
|
||||
@ -292,7 +253,7 @@ export function getAllEyesByChainId(boardState: BoardState, player: playerColors
|
||||
|
||||
// If any chain fully encircles the empty space (even if there are other chains encircled as well), the eye is true
|
||||
const neighborsEncirclingEye = findNeighboringChainsThatFullyEncircleEmptySpace(
|
||||
boardState,
|
||||
board,
|
||||
candidate.chain,
|
||||
candidate.neighbors,
|
||||
allChains,
|
||||
@ -310,8 +271,8 @@ export function getAllEyesByChainId(boardState: BoardState, player: playerColors
|
||||
/**
|
||||
* Get a list of all eyes, grouped by the chain they are adjacent to
|
||||
*/
|
||||
export function getAllEyes(boardState: BoardState, player: playerColors, eyesObject?: { [s: string]: PointState[][] }) {
|
||||
const eyes = eyesObject ?? getAllEyesByChainId(boardState, player);
|
||||
export function getAllEyes(board: Board, player: GoColor, eyesObject?: { [s: string]: PointState[][] }) {
|
||||
const eyes = eyesObject ?? getAllEyesByChainId(board, player);
|
||||
return Object.keys(eyes).map((key) => eyes[key]);
|
||||
}
|
||||
|
||||
@ -320,33 +281,28 @@ export function getAllEyes(boardState: BoardState, player: playerColors, eyesObj
|
||||
For each player chain number, add any empty space chains that are completely surrounded by a single player's color to
|
||||
an array at that chain number's index.
|
||||
*/
|
||||
export function getAllPotentialEyes(
|
||||
boardState: BoardState,
|
||||
allChains: PointState[][],
|
||||
player: playerColors,
|
||||
_maxSize?: number,
|
||||
) {
|
||||
const nodeCount = boardState.board.map((row) => row.filter((p) => p)).flat().length;
|
||||
export function getAllPotentialEyes(board: Board, allChains: PointState[][], player: GoColor, _maxSize?: number) {
|
||||
const nodeCount = board.map((row) => row.filter((p) => p)).flat().length;
|
||||
const maxSize = _maxSize ?? Math.min(nodeCount * 0.4, 11);
|
||||
const emptyPointChains = allChains.filter((chain) => chain[0].player === playerColors.empty);
|
||||
const emptyPointChains = allChains.filter((chain) => chain[0].color === GoColor.empty);
|
||||
const eyeCandidates: { neighbors: PointState[][]; chain: PointState[]; id: string }[] = [];
|
||||
|
||||
emptyPointChains
|
||||
.filter((chain) => chain.length <= maxSize)
|
||||
.forEach((chain) => {
|
||||
const neighboringChains = getAllNeighboringChains(boardState, chain, allChains);
|
||||
const neighboringChains = getAllNeighboringChains(board, chain, allChains);
|
||||
|
||||
const hasWhitePieceNeighbor = neighboringChains.find(
|
||||
(neighborChain) => neighborChain[0]?.player === playerColors.white,
|
||||
(neighborChain) => neighborChain[0]?.color === GoColor.white,
|
||||
);
|
||||
const hasBlackPieceNeighbor = neighboringChains.find(
|
||||
(neighborChain) => neighborChain[0]?.player === playerColors.black,
|
||||
(neighborChain) => neighborChain[0]?.color === GoColor.black,
|
||||
);
|
||||
|
||||
// Record the neighbor chains of the eye candidate empty chain, if all of its neighbors are the same color piece
|
||||
if (
|
||||
(hasWhitePieceNeighbor && !hasBlackPieceNeighbor && player === playerColors.white) ||
|
||||
(!hasWhitePieceNeighbor && hasBlackPieceNeighbor && player === playerColors.black)
|
||||
(hasWhitePieceNeighbor && !hasBlackPieceNeighbor && player === GoColor.white) ||
|
||||
(!hasWhitePieceNeighbor && hasBlackPieceNeighbor && player === GoColor.black)
|
||||
) {
|
||||
eyeCandidates.push({
|
||||
neighbors: neighboringChains,
|
||||
@ -366,12 +322,12 @@ export function getAllPotentialEyes(
|
||||
* If so, the original candidate is a true eye.
|
||||
*/
|
||||
function findNeighboringChainsThatFullyEncircleEmptySpace(
|
||||
boardState: BoardState,
|
||||
board: Board,
|
||||
candidateChain: PointState[],
|
||||
neighborChainList: PointState[][],
|
||||
allChains: PointState[][],
|
||||
) {
|
||||
const boardMax = boardState.board[0].length - 1;
|
||||
const boardMax = board[0].length - 1;
|
||||
const candidateSpread = findFurthestPointsOfChain(candidateChain);
|
||||
return neighborChainList.filter((neighborChain, index) => {
|
||||
// If the chain does not go far enough to surround the eye in question, don't bother building an eval board
|
||||
@ -392,23 +348,23 @@ function findNeighboringChainsThatFullyEncircleEmptySpace(
|
||||
return false;
|
||||
}
|
||||
|
||||
const evaluationBoard = getStateCopy(boardState);
|
||||
const evaluationBoard = getBoardCopy(board);
|
||||
const examplePoint = candidateChain[0];
|
||||
const otherChainNeighborPoints = removePointAtIndex(neighborChainList, index)
|
||||
.flat()
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined);
|
||||
otherChainNeighborPoints.forEach((point) => {
|
||||
const pointToEdit = evaluationBoard.board[point.x]?.[point.y];
|
||||
const pointToEdit = evaluationBoard[point.x]?.[point.y];
|
||||
if (pointToEdit) {
|
||||
pointToEdit.player = playerColors.empty;
|
||||
pointToEdit.color = GoColor.empty;
|
||||
}
|
||||
});
|
||||
const updatedBoard = updateChains(evaluationBoard);
|
||||
const newChains = getAllChains(updatedBoard);
|
||||
const newChainID = updatedBoard.board[examplePoint.x]?.[examplePoint.y]?.chain;
|
||||
updateChains(evaluationBoard);
|
||||
const newChains = getAllChains(evaluationBoard);
|
||||
const newChainID = evaluationBoard[examplePoint.x]?.[examplePoint.y]?.chain;
|
||||
const chain = newChains.find((chain) => chain[0].chain === newChainID) || [];
|
||||
const newNeighborChains = getAllNeighboringChains(boardState, chain, allChains);
|
||||
const newNeighborChains = getAllNeighboringChains(board, chain, allChains);
|
||||
|
||||
return newNeighborChains.length === 1;
|
||||
});
|
||||
@ -456,8 +412,8 @@ function removePointAtIndex(arr: PointState[][], index: number) {
|
||||
/**
|
||||
* Get all player chains that are adjacent / touching the current chain
|
||||
*/
|
||||
export function getAllNeighboringChains(boardState: BoardState, chain: PointState[], allChains: PointState[][]) {
|
||||
const playerNeighbors = getPlayerNeighbors(boardState, chain);
|
||||
export function getAllNeighboringChains(board: Board, chain: PointState[], allChains: PointState[][]) {
|
||||
const playerNeighbors = getPlayerNeighbors(board, chain);
|
||||
|
||||
const neighboringChains = playerNeighbors.reduce(
|
||||
(neighborChains, neighbor) =>
|
||||
@ -471,16 +427,16 @@ export function getAllNeighboringChains(boardState: BoardState, chain: PointStat
|
||||
/**
|
||||
* Gets all points that have player pieces adjacent to the given point
|
||||
*/
|
||||
export function getPlayerNeighbors(boardState: BoardState, chain: PointState[]) {
|
||||
return getAllNeighbors(boardState, chain).filter((neighbor) => neighbor && neighbor.player !== playerColors.empty);
|
||||
export function getPlayerNeighbors(board: Board, chain: PointState[]) {
|
||||
return getAllNeighbors(board, chain).filter((neighbor) => neighbor && neighbor.color !== GoColor.empty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all points adjacent to the given point
|
||||
*/
|
||||
export function getAllNeighbors(boardState: BoardState, chain: PointState[]) {
|
||||
export function getAllNeighbors(board: Board, chain: PointState[]) {
|
||||
const allNeighbors = chain.reduce((chainNeighbors: Set<PointState>, point: PointState) => {
|
||||
getArrayFromNeighbor(findNeighbors(boardState, point.x, point.y))
|
||||
getArrayFromNeighbor(findNeighbors(board, point.x, point.y))
|
||||
.filter((neighborPoint) => !isPointInChain(neighborPoint, chain))
|
||||
.forEach((neighborPoint) => chainNeighbors.add(neighborPoint));
|
||||
return chainNeighbors;
|
||||
@ -495,33 +451,15 @@ export function isPointInChain(point: PointState, chain: PointState[]) {
|
||||
return !!chain.find((chainPoint) => chainPoint.x === point.x && chainPoint.y === point.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks through the board history to see if the current state is identical to any previous state
|
||||
* Capped at 5 for calculation speed, because loops of size 6 are essentially impossible
|
||||
*/
|
||||
function checkIfBoardStateIsRepeated(boardState: BoardState) {
|
||||
const currentBoard = boardState.board;
|
||||
return boardState.history.slice(-5).find((state) => {
|
||||
for (let x = 0; x < state.length; x++) {
|
||||
for (let y = 0; y < state[x].length; y++) {
|
||||
if (currentBoard[x]?.[y]?.player && currentBoard[x]?.[y]?.player !== state[x]?.[y]?.player) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all groups of connected pieces, or empty space groups
|
||||
*/
|
||||
export function getAllChains(boardState: BoardState): PointState[][] {
|
||||
export function getAllChains(board: Board): PointState[][] {
|
||||
const chains: { [s: string]: PointState[] } = {};
|
||||
|
||||
for (let x = 0; x < boardState.board.length; x++) {
|
||||
for (let y = 0; y < boardState.board[x].length; y++) {
|
||||
const point = boardState.board[x]?.[y];
|
||||
for (let x = 0; x < board.length; x++) {
|
||||
for (let y = 0; y < board[x].length; y++) {
|
||||
const point = board[x]?.[y];
|
||||
// If the current chain is already analyzed, skip it
|
||||
if (!point || point.chain === "") {
|
||||
continue;
|
||||
@ -538,8 +476,8 @@ export function getAllChains(boardState: BoardState): PointState[][] {
|
||||
/**
|
||||
* Find any group of stones with no liberties (who therefore are to be removed from the board)
|
||||
*/
|
||||
export function findAllCapturedChains(chainList: PointState[][], playerWhoMoved: PlayerColor) {
|
||||
const opposingPlayer = playerWhoMoved === playerColors.white ? playerColors.black : playerColors.white;
|
||||
export function findAllCapturedChains(chainList: PointState[][], playerWhoMoved: GoColor) {
|
||||
const opposingPlayer = playerWhoMoved === GoColor.white ? GoColor.black : GoColor.white;
|
||||
const enemyChainsToCapture = findCapturedChainOfColor(chainList, opposingPlayer);
|
||||
|
||||
if (enemyChainsToCapture) {
|
||||
@ -552,36 +490,36 @@ export function findAllCapturedChains(chainList: PointState[][], playerWhoMoved:
|
||||
}
|
||||
}
|
||||
|
||||
function findCapturedChainOfColor(chainList: PointState[][], playerColor: PlayerColor) {
|
||||
return chainList.filter((chain) => chain?.[0].player === playerColor && chain?.[0].liberties?.length === 0);
|
||||
function findCapturedChainOfColor(chainList: PointState[][], playerColor: GoColor) {
|
||||
return chainList.filter((chain) => chain?.[0].color === playerColor && chain?.[0].liberties?.length === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all empty points adjacent to any piece in a given chain
|
||||
*/
|
||||
export function findLibertiesForChain(boardState: BoardState, chain: PointState[]): PointState[] {
|
||||
return getAllNeighbors(boardState, chain).filter((neighbor) => neighbor && neighbor.player === playerColors.empty);
|
||||
export function findLibertiesForChain(board: Board, chain: PointState[]): PointState[] {
|
||||
return getAllNeighbors(board, chain).filter((neighbor) => neighbor && neighbor.color === GoColor.empty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all empty points adjacent to any piece in the chain that a given point belongs to
|
||||
*/
|
||||
export function findChainLibertiesForPoint(boardState: BoardState, x: number, y: number): PointState[] {
|
||||
const chain = findAdjacentPointsInChain(boardState, x, y);
|
||||
return findLibertiesForChain(boardState, chain);
|
||||
export function findChainLibertiesForPoint(board: Board, x: number, y: number): PointState[] {
|
||||
const chain = findAdjacentPointsInChain(board, x, y);
|
||||
return findLibertiesForChain(board, chain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object that includes which of the cardinal neighbors are empty
|
||||
* (adjacent 'liberties' of the current piece )
|
||||
*/
|
||||
export function findAdjacentLibertiesForPoint(boardState: BoardState, x: number, y: number): Neighbor {
|
||||
const neighbors = findNeighbors(boardState, x, y);
|
||||
export function findAdjacentLibertiesForPoint(board: Board, x: number, y: number): Neighbor {
|
||||
const neighbors = findNeighbors(board, x, y);
|
||||
|
||||
const hasNorthLiberty = neighbors.north && neighbors.north.player === playerColors.empty;
|
||||
const hasEastLiberty = neighbors.east && neighbors.east.player === playerColors.empty;
|
||||
const hasSouthLiberty = neighbors.south && neighbors.south.player === playerColors.empty;
|
||||
const hasWestLiberty = neighbors.west && neighbors.west.player === playerColors.empty;
|
||||
const hasNorthLiberty = neighbors.north && neighbors.north.color === GoColor.empty;
|
||||
const hasEastLiberty = neighbors.east && neighbors.east.color === GoColor.empty;
|
||||
const hasSouthLiberty = neighbors.south && neighbors.south.color === GoColor.empty;
|
||||
const hasWestLiberty = neighbors.west && neighbors.west.color === GoColor.empty;
|
||||
|
||||
return {
|
||||
north: hasNorthLiberty ? neighbors.north : null,
|
||||
@ -596,22 +534,21 @@ export function findAdjacentLibertiesForPoint(boardState: BoardState, x: number,
|
||||
* current player's pieces. Used for making the connection map on the board
|
||||
*/
|
||||
export function findAdjacentLibertiesAndAlliesForPoint(
|
||||
boardState: BoardState,
|
||||
board: Board,
|
||||
x: number,
|
||||
y: number,
|
||||
_player?: PlayerColor,
|
||||
_player?: GoColor,
|
||||
): Neighbor {
|
||||
const currentPoint = boardState.board[x]?.[y];
|
||||
const player =
|
||||
_player || (!currentPoint || currentPoint.player === playerColors.empty ? undefined : currentPoint.player);
|
||||
const adjacentLiberties = findAdjacentLibertiesForPoint(boardState, x, y);
|
||||
const neighbors = findNeighbors(boardState, x, y);
|
||||
const currentPoint = board[x]?.[y];
|
||||
const player = _player || (!currentPoint || currentPoint.color === GoColor.empty ? undefined : currentPoint.color);
|
||||
const adjacentLiberties = findAdjacentLibertiesForPoint(board, x, y);
|
||||
const neighbors = findNeighbors(board, x, y);
|
||||
|
||||
return {
|
||||
north: adjacentLiberties.north || neighbors.north?.player === player ? neighbors.north : null,
|
||||
east: adjacentLiberties.east || neighbors.east?.player === player ? neighbors.east : null,
|
||||
south: adjacentLiberties.south || neighbors.south?.player === player ? neighbors.south : null,
|
||||
west: adjacentLiberties.west || neighbors.west?.player === player ? neighbors.west : null,
|
||||
north: adjacentLiberties.north || neighbors.north?.color === player ? neighbors.north : null,
|
||||
east: adjacentLiberties.east || neighbors.east?.color === player ? neighbors.east : null,
|
||||
south: adjacentLiberties.south || neighbors.south?.color === player ? neighbors.south : null,
|
||||
west: adjacentLiberties.west || neighbors.west?.color === player ? neighbors.west : null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -638,16 +575,16 @@ export function findAdjacentLibertiesAndAlliesForPoint(
|
||||
* be rotated 90 degrees clockwise compared to the board UI as shown in the IPvGO game.
|
||||
*
|
||||
*/
|
||||
export function getSimplifiedBoardState(board: Board): string[] {
|
||||
export function simpleBoardFromBoard(board: Board): string[] {
|
||||
return board.map((column) =>
|
||||
column.reduce((str, point) => {
|
||||
if (!point) {
|
||||
return str + "#";
|
||||
}
|
||||
if (point.player === playerColors.black) {
|
||||
if (point.color === GoColor.black) {
|
||||
return str + "X";
|
||||
}
|
||||
if (point.player === playerColors.white) {
|
||||
if (point.color === GoColor.white) {
|
||||
return str + "O";
|
||||
}
|
||||
return str + ".";
|
||||
@ -655,29 +592,47 @@ export function getSimplifiedBoardState(board: Board): string[] {
|
||||
);
|
||||
}
|
||||
|
||||
export function getBoardFromSimplifiedBoardState(
|
||||
boardStrings: string[],
|
||||
ai = opponents.Daedalus,
|
||||
lastPlayer = playerColors.black,
|
||||
) {
|
||||
const newBoardState = getNewBoardState(boardStrings[0].length, ai);
|
||||
/** Creates a board object from a simple board. The resulting board has no analytics (liberties/chains) */
|
||||
export function boardFromSimpleBoard(simpleBoard: SimpleBoard): Board {
|
||||
return simpleBoard.map((column, x) =>
|
||||
column.split("").map((char, y) => {
|
||||
if (char === "#") return null;
|
||||
if (char === "X") return blankPointState(GoColor.black, x, y);
|
||||
if (char === "O") return blankPointState(GoColor.white, x, y);
|
||||
return blankPointState(GoColor.empty, x, y);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function boardStateFromSimpleBoard(
|
||||
simpleBoard: SimpleBoard,
|
||||
ai = GoOpponent.Daedalus,
|
||||
lastPlayer = GoColor.black,
|
||||
): BoardState {
|
||||
const newBoardState = getNewBoardState(simpleBoard[0].length, ai, false, boardFromSimpleBoard(simpleBoard));
|
||||
newBoardState.previousPlayer = lastPlayer;
|
||||
|
||||
for (let x = 0; x < boardStrings[0].length; x++) {
|
||||
for (let y = 0; y < boardStrings[0].length; y++) {
|
||||
const boardStringPoint = boardStrings[x]?.[y];
|
||||
const newBoardPoint = newBoardState.board[x]?.[y];
|
||||
if (boardStringPoint === "#") {
|
||||
newBoardState.board[x][y] = null;
|
||||
}
|
||||
if (boardStringPoint === "X" && newBoardPoint?.player) {
|
||||
newBoardPoint.player = playerColors.black;
|
||||
}
|
||||
if (boardStringPoint === "O" && newBoardPoint?.player) {
|
||||
newBoardPoint.player = playerColors.white;
|
||||
}
|
||||
}
|
||||
updateCaptures(newBoardState.board, lastPlayer);
|
||||
return newBoardState;
|
||||
}
|
||||
|
||||
return updateCaptures(newBoardState, lastPlayer);
|
||||
export function blankPointState(color: GoColor, x: number, y: number): PointState {
|
||||
return {
|
||||
color: color,
|
||||
y,
|
||||
x,
|
||||
chain: "",
|
||||
liberties: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function areSimpleBoardsIdentical(simpleBoard1: SimpleBoard, simpleBoard2: SimpleBoard) {
|
||||
return simpleBoard1.every((column, x) => column === simpleBoard2[x]);
|
||||
}
|
||||
|
||||
export function getColorOnSimpleBoard(simpleBoard: SimpleBoard, x: number, y: number): GoColor | null {
|
||||
const char = simpleBoard[x]?.[y];
|
||||
if (char === "X") return GoColor.black;
|
||||
if (char === "O") return GoColor.white;
|
||||
if (char === ".") return GoColor.empty;
|
||||
return null;
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { BoardState, playerColors, type PointState } from "../boardState/goConstants";
|
||||
import type { Board, BoardState, PointState } from "../Types";
|
||||
|
||||
import { GoColor } from "@enums";
|
||||
import {
|
||||
getAllChains,
|
||||
getAllEyes,
|
||||
@ -19,18 +21,18 @@ import { contains, isNotNull } from "../boardState/boardState";
|
||||
* In which case, only the liberties of that one weak chain are worth considering. Other parts of that fully-encircled
|
||||
* enemy space, and other similar spaces, should be ignored, otherwise the game drags on too long
|
||||
*/
|
||||
export function findDisputedTerritory(boardState: BoardState, player: playerColors, excludeFriendlyEyes?: boolean) {
|
||||
export function findDisputedTerritory(boardState: BoardState, player: GoColor, excludeFriendlyEyes?: boolean) {
|
||||
let validMoves = getAllValidMoves(boardState, player);
|
||||
if (excludeFriendlyEyes) {
|
||||
const friendlyEyes = getAllEyes(boardState, player)
|
||||
const friendlyEyes = getAllEyes(boardState.board, player)
|
||||
.filter((eye) => eye.length >= 2)
|
||||
.flat()
|
||||
.flat();
|
||||
validMoves = validMoves.filter((point) => !contains(friendlyEyes, point));
|
||||
}
|
||||
const opponent = player === playerColors.white ? playerColors.black : playerColors.white;
|
||||
const chains = getAllChains(boardState);
|
||||
const emptySpacesToAnalyze = getAllPotentialEyes(boardState, chains, opponent);
|
||||
const opponent = player === GoColor.white ? GoColor.black : GoColor.white;
|
||||
const chains = getAllChains(boardState.board);
|
||||
const emptySpacesToAnalyze = getAllPotentialEyes(boardState.board, chains, opponent);
|
||||
const nodesInsideEyeSpacesToAnalyze = emptySpacesToAnalyze.map((space) => space.chain).flat();
|
||||
|
||||
const playableNodesInsideOfEnemySpace = emptySpacesToAnalyze.reduce((playableNodes: PointState[], space) => {
|
||||
@ -45,12 +47,12 @@ export function findDisputedTerritory(boardState: BoardState, player: playerColo
|
||||
}
|
||||
|
||||
// Get all opponent chains that make up the border of the opponent-controlled space
|
||||
const neighborChains = getAllNeighboringChains(boardState, neighborChain, chains);
|
||||
const neighborChains = getAllNeighboringChains(boardState.board, neighborChain, chains);
|
||||
|
||||
// Ignore border chains that do not touch the current player's pieces somewhere, as they are likely fully interior
|
||||
// to the empty space in question, or only share a border with the edge of the board and the space, or are not yet
|
||||
// surrounded on the exterior and ready to be attacked within
|
||||
if (!neighborChains.find((chain) => chain?.[0]?.player === player)) {
|
||||
if (!neighborChains.find((chain) => chain?.[0]?.color === player)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -87,12 +89,8 @@ export function findDisputedTerritory(boardState: BoardState, player: playerColo
|
||||
|
||||
Note that this does not detect mutual eyes formed by two chains making an eye together, or eyes via seki, or some other edge cases.
|
||||
*/
|
||||
export function findClaimedTerritory(boardState: BoardState) {
|
||||
const whiteClaimedTerritory = getAllEyes(boardState, playerColors.white).filter(
|
||||
(eyesForChainN) => eyesForChainN.length >= 2,
|
||||
);
|
||||
const blackClaimedTerritory = getAllEyes(boardState, playerColors.black).filter(
|
||||
(eyesForChainN) => eyesForChainN.length >= 2,
|
||||
);
|
||||
export function findClaimedTerritory(board: Board) {
|
||||
const whiteClaimedTerritory = getAllEyes(board, GoColor.white).filter((eyesForChainN) => eyesForChainN.length >= 2);
|
||||
const blackClaimedTerritory = getAllEyes(board, GoColor.black).filter((eyesForChainN) => eyesForChainN.length >= 2);
|
||||
return [...blackClaimedTerritory, ...whiteClaimedTerritory].flat().flat();
|
||||
}
|
||||
|
@ -1,15 +1,8 @@
|
||||
import {
|
||||
BoardState,
|
||||
EyeMove,
|
||||
Move,
|
||||
MoveOptions,
|
||||
opponentDetails,
|
||||
opponents,
|
||||
PlayerColor,
|
||||
playerColors,
|
||||
playTypes,
|
||||
PointState,
|
||||
} from "../boardState/goConstants";
|
||||
import type { Board, BoardState, EyeMove, Move, MoveOptions, PointState } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { AugmentationName, GoOpponent, GoColor, GoPlayType } from "@enums";
|
||||
import { opponentDetails } from "../Constants";
|
||||
import { findNeighbors, floor, isDefined, isNotNull, passTurn } from "../boardState/boardState";
|
||||
import {
|
||||
evaluateIfMoveIsValid,
|
||||
@ -26,8 +19,6 @@ import {
|
||||
import { findDisputedTerritory } from "./controlledTerritory";
|
||||
import { findAnyMatchedPatterns } from "./patternMatching";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
import { Player } from "@player";
|
||||
import { AugmentationName } from "@enums";
|
||||
|
||||
/*
|
||||
Basic GO AIs, each with some personality and weaknesses
|
||||
@ -47,7 +38,7 @@ import { AugmentationName } from "@enums";
|
||||
*
|
||||
* @returns a promise that will resolve with a move (or pass) from the designated AI opponent.
|
||||
*/
|
||||
export async function getMove(boardState: BoardState, player: PlayerColor, opponent: opponents, rngOverride?: number) {
|
||||
export async function getMove(boardState: BoardState, player: GoColor, opponent: GoOpponent, rngOverride?: number) {
|
||||
await sleep(300);
|
||||
const rng = new WHRNG(rngOverride || Player.totalPlaytime);
|
||||
const smart = isSmart(opponent, rng.random());
|
||||
@ -56,7 +47,7 @@ export async function getMove(boardState: BoardState, player: PlayerColor, oppon
|
||||
const priorityMove = await getFactionMove(moves, opponent, rng.random());
|
||||
if (priorityMove) {
|
||||
return {
|
||||
type: playTypes.move,
|
||||
type: GoPlayType.move,
|
||||
x: priorityMove.x,
|
||||
y: priorityMove.y,
|
||||
};
|
||||
@ -80,14 +71,14 @@ export async function getMove(boardState: BoardState, player: PlayerColor, oppon
|
||||
|
||||
if (chosenMove) {
|
||||
await sleep(200);
|
||||
console.debug(`Non-priority move chosen: ${chosenMove.x} ${chosenMove.y}`);
|
||||
//console.debug(`Non-priority move chosen: ${chosenMove.x} ${chosenMove.y}`);
|
||||
return {
|
||||
type: playTypes.move,
|
||||
type: GoPlayType.move,
|
||||
x: chosenMove.x,
|
||||
y: chosenMove.y,
|
||||
};
|
||||
} else {
|
||||
console.debug("No valid moves found");
|
||||
//console.debug("No valid moves found");
|
||||
return handleNoMoveFound(boardState, player);
|
||||
}
|
||||
}
|
||||
@ -98,19 +89,19 @@ export async function getMove(boardState: BoardState, player: PlayerColor, oppon
|
||||
* Ends the game if the player passed on the previous turn before the AI passes,
|
||||
* or if the player will be forced to pass their next turn after the AI passes.
|
||||
*/
|
||||
function handleNoMoveFound(boardState: BoardState, player: playerColors) {
|
||||
function handleNoMoveFound(boardState: BoardState, player: GoColor) {
|
||||
passTurn(boardState, player);
|
||||
const opposingPlayer = player === playerColors.white ? playerColors.black : playerColors.white;
|
||||
const opposingPlayer = player === GoColor.white ? GoColor.black : GoColor.white;
|
||||
const remainingTerritory = getAllValidMoves(boardState, opposingPlayer).length;
|
||||
if (remainingTerritory > 0 && boardState.passCount < 2) {
|
||||
return {
|
||||
type: playTypes.pass,
|
||||
type: GoPlayType.pass,
|
||||
x: -1,
|
||||
y: -1,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: playTypes.gameOver,
|
||||
type: GoPlayType.gameOver,
|
||||
x: -1,
|
||||
y: -1,
|
||||
};
|
||||
@ -120,20 +111,20 @@ function handleNoMoveFound(boardState: BoardState, player: playerColors) {
|
||||
/**
|
||||
* Given a group of move options, chooses one based on the given opponent's personality (if any fit their priorities)
|
||||
*/
|
||||
async function getFactionMove(moves: MoveOptions, faction: opponents, rng: number): Promise<PointState | null> {
|
||||
if (faction === opponents.Netburners) {
|
||||
async function getFactionMove(moves: MoveOptions, faction: GoOpponent, rng: number): Promise<PointState | null> {
|
||||
if (faction === GoOpponent.Netburners) {
|
||||
return getNetburnersPriorityMove(moves, rng);
|
||||
}
|
||||
if (faction === opponents.SlumSnakes) {
|
||||
if (faction === GoOpponent.SlumSnakes) {
|
||||
return getSlumSnakesPriorityMove(moves, rng);
|
||||
}
|
||||
if (faction === opponents.TheBlackHand) {
|
||||
if (faction === GoOpponent.TheBlackHand) {
|
||||
return getBlackHandPriorityMove(moves, rng);
|
||||
}
|
||||
if (faction === opponents.Tetrads) {
|
||||
if (faction === GoOpponent.Tetrads) {
|
||||
return getTetradPriorityMove(moves, rng);
|
||||
}
|
||||
if (faction === opponents.Daedalus) {
|
||||
if (faction === GoOpponent.Daedalus) {
|
||||
return getDaedalusPriorityMove(moves, rng);
|
||||
}
|
||||
|
||||
@ -143,14 +134,14 @@ async function getFactionMove(moves: MoveOptions, faction: opponents, rng: numbe
|
||||
/**
|
||||
* Determines if certain failsafes and mistake avoidance are enabled for the given move
|
||||
*/
|
||||
function isSmart(faction: opponents, rng: number) {
|
||||
if (faction === opponents.Netburners) {
|
||||
function isSmart(faction: GoOpponent, rng: number) {
|
||||
if (faction === GoOpponent.Netburners) {
|
||||
return false;
|
||||
}
|
||||
if (faction === opponents.SlumSnakes) {
|
||||
if (faction === GoOpponent.SlumSnakes) {
|
||||
return rng < 0.3;
|
||||
}
|
||||
if (faction === opponents.TheBlackHand) {
|
||||
if (faction === GoOpponent.TheBlackHand) {
|
||||
return rng < 0.8;
|
||||
}
|
||||
|
||||
@ -198,24 +189,24 @@ async function getSlumSnakesPriorityMove(moves: MoveOptions, rng: number): Promi
|
||||
*/
|
||||
async function getBlackHandPriorityMove(moves: MoveOptions, rng: number): Promise<PointState | null> {
|
||||
if (await moves.capture()) {
|
||||
console.debug("capture: capture move chosen");
|
||||
//console.debug("capture: capture move chosen");
|
||||
return (await moves.capture())?.point ?? null;
|
||||
}
|
||||
|
||||
const surround = await moves.surround();
|
||||
|
||||
if (surround && surround.point && (surround.newLibertyCount ?? 999) <= 1) {
|
||||
console.debug("surround move chosen");
|
||||
//console.debug("surround move chosen");
|
||||
return surround.point;
|
||||
}
|
||||
|
||||
if (await moves.defendCapture()) {
|
||||
console.debug("defend capture: defend move chosen");
|
||||
//console.debug("defend capture: defend move chosen");
|
||||
return (await moves.defendCapture())?.point ?? null;
|
||||
}
|
||||
|
||||
if (surround && surround.point && (surround?.newLibertyCount ?? 999) <= 2) {
|
||||
console.debug("surround move chosen");
|
||||
//console.debug("surround move chosen");
|
||||
return surround.point;
|
||||
}
|
||||
|
||||
@ -235,23 +226,23 @@ async function getBlackHandPriorityMove(moves: MoveOptions, rng: number): Promis
|
||||
*/
|
||||
async function getTetradPriorityMove(moves: MoveOptions, rng: number): Promise<PointState | null> {
|
||||
if (await moves.capture()) {
|
||||
console.debug("capture: capture move chosen");
|
||||
//console.debug("capture: capture move chosen");
|
||||
return (await moves.capture())?.point ?? null;
|
||||
}
|
||||
|
||||
if (await moves.defendCapture()) {
|
||||
console.debug("defend capture: defend move chosen");
|
||||
//console.debug("defend capture: defend move chosen");
|
||||
return (await moves.defendCapture())?.point ?? null;
|
||||
}
|
||||
|
||||
if (await moves.pattern()) {
|
||||
console.debug("pattern match move chosen");
|
||||
//console.debug("pattern match move chosen");
|
||||
return (await moves.pattern())?.point ?? null;
|
||||
}
|
||||
|
||||
const surround = await moves.surround();
|
||||
if (surround && surround.point && (surround?.newLibertyCount ?? 9) <= 1) {
|
||||
console.debug("surround move chosen");
|
||||
//console.debug("surround move chosen");
|
||||
return surround.point;
|
||||
}
|
||||
|
||||
@ -283,33 +274,33 @@ async function getDaedalusPriorityMove(moves: MoveOptions, rng: number): Promise
|
||||
*/
|
||||
async function getIlluminatiPriorityMove(moves: MoveOptions, rng: number): Promise<PointState | null> {
|
||||
if (await moves.capture()) {
|
||||
console.debug("capture: capture move chosen");
|
||||
//console.debug("capture: capture move chosen");
|
||||
return (await moves.capture())?.point ?? null;
|
||||
}
|
||||
|
||||
if (await moves.defendCapture()) {
|
||||
console.debug("defend capture: defend move chosen");
|
||||
//console.debug("defend capture: defend move chosen");
|
||||
return (await moves.defendCapture())?.point ?? null;
|
||||
}
|
||||
|
||||
if (await moves.eyeMove()) {
|
||||
console.debug("Create eye move chosen");
|
||||
//console.debug("Create eye move chosen");
|
||||
return (await moves.eyeMove())?.point ?? null;
|
||||
}
|
||||
|
||||
const surround = await moves.surround();
|
||||
if (surround && surround.point && (surround?.newLibertyCount ?? 9) <= 1) {
|
||||
console.debug("surround move chosen");
|
||||
//console.debug("surround move chosen");
|
||||
return surround.point;
|
||||
}
|
||||
|
||||
if (await moves.eyeBlock()) {
|
||||
console.debug("Block eye move chosen");
|
||||
//console.debug("Block eye move chosen");
|
||||
return (await moves.eyeBlock())?.point ?? null;
|
||||
}
|
||||
|
||||
if (await moves.corner()) {
|
||||
console.debug("Corner move chosen");
|
||||
//console.debug("Corner move chosen");
|
||||
return (await moves.corner())?.point ?? null;
|
||||
}
|
||||
|
||||
@ -319,17 +310,17 @@ async function getIlluminatiPriorityMove(moves: MoveOptions, rng: number): Promi
|
||||
const usePattern = rng > 0.25 || !hasMoves;
|
||||
|
||||
if ((await moves.pattern()) && usePattern) {
|
||||
console.debug("pattern match move chosen");
|
||||
//console.debug("pattern match move chosen");
|
||||
return (await moves.pattern())?.point ?? null;
|
||||
}
|
||||
|
||||
if (rng > 0.4 && (await moves.jump())) {
|
||||
console.debug("Jump move chosen");
|
||||
//console.debug("Jump move chosen");
|
||||
return (await moves.jump())?.point ?? null;
|
||||
}
|
||||
|
||||
if (rng < 0.6 && surround && surround.point && (surround?.newLibertyCount ?? 9) <= 2) {
|
||||
console.debug("surround move chosen");
|
||||
//console.debug("surround move chosen");
|
||||
return surround.point;
|
||||
}
|
||||
|
||||
@ -339,20 +330,20 @@ async function getIlluminatiPriorityMove(moves: MoveOptions, rng: number): Promi
|
||||
/**
|
||||
* Get a move that places a piece to influence (and later control) a corner
|
||||
*/
|
||||
function getCornerMove(boardState: BoardState) {
|
||||
const boardEdge = boardState.board[0].length - 1;
|
||||
function getCornerMove(board: Board) {
|
||||
const boardEdge = board[0].length - 1;
|
||||
const cornerMax = boardEdge - 2;
|
||||
if (isCornerAvailableForMove(boardState, cornerMax, cornerMax, boardEdge, boardEdge)) {
|
||||
return boardState.board[cornerMax][cornerMax];
|
||||
if (isCornerAvailableForMove(board, cornerMax, cornerMax, boardEdge, boardEdge)) {
|
||||
return board[cornerMax][cornerMax];
|
||||
}
|
||||
if (isCornerAvailableForMove(boardState, 0, cornerMax, cornerMax, boardEdge)) {
|
||||
return boardState.board[2][cornerMax];
|
||||
if (isCornerAvailableForMove(board, 0, cornerMax, cornerMax, boardEdge)) {
|
||||
return board[2][cornerMax];
|
||||
}
|
||||
if (isCornerAvailableForMove(boardState, 0, 0, 2, 2)) {
|
||||
return boardState.board[2][2];
|
||||
if (isCornerAvailableForMove(board, 0, 0, 2, 2)) {
|
||||
return board[2][2];
|
||||
}
|
||||
if (isCornerAvailableForMove(boardState, cornerMax, 0, boardEdge, 2)) {
|
||||
return boardState.board[cornerMax][2];
|
||||
if (isCornerAvailableForMove(board, cornerMax, 0, boardEdge, 2)) {
|
||||
return board[cornerMax][2];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -360,9 +351,9 @@ function getCornerMove(boardState: BoardState) {
|
||||
/**
|
||||
* Find all non-offline nodes in a given area
|
||||
*/
|
||||
function findLiveNodesInArea(boardState: BoardState, x1: number, y1: number, x2: number, y2: number) {
|
||||
function findLiveNodesInArea(board: Board, x1: number, y1: number, x2: number, y2: number) {
|
||||
const foundPoints: PointState[] = [];
|
||||
boardState.board.forEach((column) =>
|
||||
board.forEach((column) =>
|
||||
column.forEach(
|
||||
(point) => point && point.x >= x1 && point.x <= x2 && point.y >= y1 && point.y <= y2 && foundPoints.push(point),
|
||||
),
|
||||
@ -373,23 +364,17 @@ function findLiveNodesInArea(boardState: BoardState, x1: number, y1: number, x2:
|
||||
/**
|
||||
* Determine if a corner is largely intact and currently empty, and thus a good target for corner takeover moves
|
||||
*/
|
||||
function isCornerAvailableForMove(boardState: BoardState, x1: number, y1: number, x2: number, y2: number) {
|
||||
const foundPoints = findLiveNodesInArea(boardState, x1, y1, x2, y2);
|
||||
const foundPieces = foundPoints.filter((point) => point.player !== playerColors.empty);
|
||||
function isCornerAvailableForMove(board: Board, x1: number, y1: number, x2: number, y2: number) {
|
||||
const foundPoints = findLiveNodesInArea(board, x1, y1, x2, y2);
|
||||
const foundPieces = foundPoints.filter((point) => point.color !== GoColor.empty);
|
||||
return foundPoints.length >= 7 ? foundPieces.length === 0 : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a move from the list of open-area moves
|
||||
*/
|
||||
function getExpansionMove(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
rng: number,
|
||||
moveArray?: Move[],
|
||||
) {
|
||||
const moveOptions = moveArray ?? getExpansionMoveArray(boardState, player, availableSpaces);
|
||||
function getExpansionMove(board: Board, availableSpaces: PointState[], rng: number, moveArray?: Move[]) {
|
||||
const moveOptions = moveArray ?? getExpansionMoveArray(board, availableSpaces);
|
||||
const randomIndex = floor(rng * moveOptions.length);
|
||||
return moveOptions[randomIndex];
|
||||
}
|
||||
@ -397,21 +382,14 @@ function getExpansionMove(
|
||||
/**
|
||||
* Get a move in open space that is nearby a friendly piece
|
||||
*/
|
||||
function getJumpMove(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
rng: number,
|
||||
moveArray?: Move[],
|
||||
) {
|
||||
const board = boardState.board;
|
||||
const moveOptions = (moveArray ?? getExpansionMoveArray(boardState, player, availableSpaces)).filter(({ point }) =>
|
||||
function getJumpMove(board: Board, player: GoColor, availableSpaces: PointState[], rng: number, moveArray?: Move[]) {
|
||||
const moveOptions = (moveArray ?? getExpansionMoveArray(board, availableSpaces)).filter(({ point }) =>
|
||||
[
|
||||
board[point.x]?.[point.y + 2],
|
||||
board[point.x + 2]?.[point.y],
|
||||
board[point.x]?.[point.y - 2],
|
||||
board[point.x - 2]?.[point.y],
|
||||
].some((point) => point?.player === player),
|
||||
].some((point) => point?.color === player),
|
||||
);
|
||||
|
||||
const randomIndex = floor(rng * moveOptions.length);
|
||||
@ -421,24 +399,20 @@ function getJumpMove(
|
||||
/**
|
||||
* Finds a move in an open area to expand influence and later build on
|
||||
*/
|
||||
export function getExpansionMoveArray(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
): Move[] {
|
||||
export function getExpansionMoveArray(board: Board, availableSpaces: PointState[]): Move[] {
|
||||
// Look for any empty spaces fully surrounded by empty spaces to expand into
|
||||
const emptySpaces = availableSpaces.filter((space) => {
|
||||
const neighbors = findNeighbors(boardState, space.x, space.y);
|
||||
const neighbors = findNeighbors(board, space.x, space.y);
|
||||
return (
|
||||
[neighbors.north, neighbors.east, neighbors.south, neighbors.west].filter(
|
||||
(point) => point && point.player === playerColors.empty,
|
||||
(point) => point && point.color === GoColor.empty,
|
||||
).length === 4
|
||||
);
|
||||
});
|
||||
|
||||
// Once no such empty areas exist anymore, instead expand into any disputed territory
|
||||
// to gain a few more points in endgame
|
||||
const disputedSpaces = emptySpaces.length ? [] : getDisputedTerritoryMoves(boardState, player, availableSpaces, 1);
|
||||
const disputedSpaces = emptySpaces.length ? [] : getDisputedTerritoryMoves(board, availableSpaces, 1);
|
||||
|
||||
const moveOptions = [...emptySpaces, ...disputedSpaces];
|
||||
|
||||
@ -451,23 +425,14 @@ export function getExpansionMoveArray(
|
||||
});
|
||||
}
|
||||
|
||||
function getDisputedTerritoryMoves(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
maxChainSize = 99,
|
||||
) {
|
||||
const chains = getAllChains(boardState).filter((chain) => chain.length <= maxChainSize);
|
||||
function getDisputedTerritoryMoves(board: Board, availableSpaces: PointState[], maxChainSize = 99) {
|
||||
const chains = getAllChains(board).filter((chain) => chain.length <= maxChainSize);
|
||||
|
||||
return availableSpaces.filter((space) => {
|
||||
const chain = chains.find((chain) => chain[0].chain === space.chain) ?? [];
|
||||
const playerNeighbors = getAllNeighboringChains(boardState, chain, chains);
|
||||
const hasWhitePieceNeighbor = playerNeighbors.find(
|
||||
(neighborChain) => neighborChain[0]?.player === playerColors.white,
|
||||
);
|
||||
const hasBlackPieceNeighbor = playerNeighbors.find(
|
||||
(neighborChain) => neighborChain[0]?.player === playerColors.black,
|
||||
);
|
||||
const playerNeighbors = getAllNeighboringChains(board, chain, chains);
|
||||
const hasWhitePieceNeighbor = playerNeighbors.find((neighborChain) => neighborChain[0]?.color === GoColor.white);
|
||||
const hasBlackPieceNeighbor = playerNeighbors.find((neighborChain) => neighborChain[0]?.color === GoColor.black);
|
||||
|
||||
return hasWhitePieceNeighbor && hasBlackPieceNeighbor;
|
||||
});
|
||||
@ -476,8 +441,8 @@ function getDisputedTerritoryMoves(
|
||||
/**
|
||||
* Finds all moves that increases the liberties of the player's pieces, making them harder to capture and occupy more space on the board.
|
||||
*/
|
||||
async function getLibertyGrowthMoves(boardState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
|
||||
const friendlyChains = getAllChains(boardState).filter((chain) => chain[0].player === player);
|
||||
async function getLibertyGrowthMoves(board: Board, player: GoColor, availableSpaces: PointState[]) {
|
||||
const friendlyChains = getAllChains(board).filter((chain) => chain[0].color === player);
|
||||
|
||||
if (!friendlyChains.length) {
|
||||
return [];
|
||||
@ -503,10 +468,10 @@ async function getLibertyGrowthMoves(boardState: BoardState, player: PlayerColor
|
||||
.map((liberty) => {
|
||||
const move = liberty.libertyPoint;
|
||||
|
||||
const newLibertyCount = findEffectiveLibertiesOfNewMove(boardState, move.x, move.y, player).length;
|
||||
const newLibertyCount = findEffectiveLibertiesOfNewMove(board, move.x, move.y, player).length;
|
||||
|
||||
// Get the smallest liberty count of connected chains to represent the old state
|
||||
const oldLibertyCount = findMinLibertyCountOfAdjacentChains(boardState, move.x, move.y, player);
|
||||
const oldLibertyCount = findMinLibertyCountOfAdjacentChains(board, move.x, move.y, player);
|
||||
|
||||
return {
|
||||
point: move,
|
||||
@ -520,13 +485,8 @@ async function getLibertyGrowthMoves(boardState: BoardState, player: PlayerColor
|
||||
/**
|
||||
* Find a move that increases the player's liberties by the maximum amount
|
||||
*/
|
||||
async function getGrowthMove(
|
||||
initialState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
rng: number,
|
||||
) {
|
||||
const growthMoves = await getLibertyGrowthMoves(initialState, player, availableSpaces);
|
||||
async function getGrowthMove(board: Board, player: GoColor, availableSpaces: PointState[], rng: number) {
|
||||
const growthMoves = await getLibertyGrowthMoves(board, player, availableSpaces);
|
||||
|
||||
const maxLibertyCount = Math.max(...growthMoves.map((l) => l.newLibertyCount - l.oldLibertyCount));
|
||||
|
||||
@ -537,8 +497,8 @@ async function getGrowthMove(
|
||||
/**
|
||||
* Find a move that specifically increases a chain's liberties from 1 to more than 1, preventing capture
|
||||
*/
|
||||
async function getDefendMove(initialState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
|
||||
const growthMoves = await getLibertyGrowthMoves(initialState, player, availableSpaces);
|
||||
async function getDefendMove(board: Board, player: GoColor, availableSpaces: PointState[]) {
|
||||
const growthMoves = await getLibertyGrowthMoves(board, player, availableSpaces);
|
||||
const libertyIncreases =
|
||||
growthMoves?.filter((move) => move.oldLibertyCount <= 1 && move.newLibertyCount > move.oldLibertyCount) ?? [];
|
||||
|
||||
@ -556,14 +516,9 @@ async function getDefendMove(initialState: BoardState, player: PlayerColor, avai
|
||||
* Find a move that reduces the opponent's liberties as much as possible,
|
||||
* capturing (or making it easier to capture) their pieces
|
||||
*/
|
||||
async function getSurroundMove(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
smart = true,
|
||||
) {
|
||||
const opposingPlayer = player === playerColors.black ? playerColors.white : playerColors.black;
|
||||
const enemyChains = getAllChains(boardState).filter((chain) => chain[0].player === opposingPlayer);
|
||||
async function getSurroundMove(board: Board, player: GoColor, availableSpaces: PointState[], smart = true) {
|
||||
const opposingPlayer = player === GoColor.black ? GoColor.white : GoColor.black;
|
||||
const enemyChains = getAllChains(board).filter((chain) => chain[0].color === opposingPlayer);
|
||||
|
||||
if (!enemyChains.length || !availableSpaces.length) {
|
||||
return null;
|
||||
@ -580,13 +535,13 @@ async function getSurroundMove(
|
||||
const surroundMoves: Move[] = [];
|
||||
|
||||
enemyLiberties.forEach((move) => {
|
||||
const newLibertyCount = findEffectiveLibertiesOfNewMove(boardState, move.x, move.y, player).length;
|
||||
const newLibertyCount = findEffectiveLibertiesOfNewMove(board, move.x, move.y, player).length;
|
||||
|
||||
const weakestEnemyChain = findEnemyNeighborChainWithFewestLiberties(
|
||||
boardState,
|
||||
board,
|
||||
move.x,
|
||||
move.y,
|
||||
player === playerColors.black ? playerColors.white : playerColors.black,
|
||||
player === GoColor.black ? GoColor.white : GoColor.black,
|
||||
);
|
||||
const weakestEnemyChainLength = weakestEnemyChain?.length ?? 99;
|
||||
|
||||
@ -646,22 +601,17 @@ async function getSurroundMove(
|
||||
* If a chain has multiple eyes, it cannot be captured by the opponent (since they can only fill one eye at a time,
|
||||
* and suiciding your own pieces is not legal unless it captures the opponents' first)
|
||||
*/
|
||||
function getEyeCreationMoves(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
maxLiberties = 99,
|
||||
) {
|
||||
const allEyes = getAllEyesByChainId(boardState, player);
|
||||
const currentEyes = getAllEyes(boardState, player, allEyes);
|
||||
function getEyeCreationMoves(board: Board, player: GoColor, availableSpaces: PointState[], maxLiberties = 99) {
|
||||
const allEyes = getAllEyesByChainId(board, player);
|
||||
const currentEyes = getAllEyes(board, player, allEyes);
|
||||
|
||||
const currentLivingGroupIDs = Object.keys(allEyes).filter((chainId) => allEyes[chainId].length >= 2);
|
||||
const currentLivingGroupsCount = currentLivingGroupIDs.length;
|
||||
const currentEyeCount = currentEyes.filter((eye) => eye.length).length;
|
||||
|
||||
const chains = getAllChains(boardState);
|
||||
const chains = getAllChains(board);
|
||||
const friendlyLiberties = chains
|
||||
.filter((chain) => chain[0].player === player)
|
||||
.filter((chain) => chain[0].color === player)
|
||||
.filter((chain) => chain.length > 1)
|
||||
.filter((chain) => chain[0].liberties && chain[0].liberties?.length <= maxLiberties)
|
||||
.filter((chain) => !currentLivingGroupIDs.includes(chain[0].chain))
|
||||
@ -672,16 +622,16 @@ function getEyeCreationMoves(
|
||||
availableSpaces.find((availablePoint) => availablePoint.x === point.x && availablePoint.y === point.y),
|
||||
)
|
||||
.filter((point: PointState) => {
|
||||
const neighbors = findNeighbors(boardState, point.x, point.y);
|
||||
const neighbors = findNeighbors(board, point.x, point.y);
|
||||
const neighborhood = [neighbors.north, neighbors.east, neighbors.south, neighbors.west];
|
||||
return (
|
||||
neighborhood.filter((point) => !point || point?.player === player).length >= 2 &&
|
||||
neighborhood.some((point) => point?.player === playerColors.empty)
|
||||
neighborhood.filter((point) => !point || point?.color === player).length >= 2 &&
|
||||
neighborhood.some((point) => point?.color === GoColor.empty)
|
||||
);
|
||||
});
|
||||
|
||||
const eyeCreationMoves = friendlyLiberties.reduce((moveOptions: EyeMove[], point: PointState) => {
|
||||
const evaluationBoard = evaluateMoveResult(boardState, point.x, point.y, player);
|
||||
const evaluationBoard = evaluateMoveResult(board, point.x, point.y, player);
|
||||
const newEyes = getAllEyes(evaluationBoard, player);
|
||||
const newLivingGroupsCount = newEyes.filter((eye) => eye.length >= 2).length;
|
||||
const newEyeCount = newEyes.filter((eye) => eye.length).length;
|
||||
@ -700,16 +650,16 @@ function getEyeCreationMoves(
|
||||
return eyeCreationMoves.sort((moveA, moveB) => +moveB.createsLife - +moveA.createsLife);
|
||||
}
|
||||
|
||||
function getEyeCreationMove(boardState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
|
||||
return getEyeCreationMoves(boardState, player, availableSpaces)[0];
|
||||
function getEyeCreationMove(board: Board, player: GoColor, availableSpaces: PointState[]) {
|
||||
return getEyeCreationMoves(board, player, availableSpaces)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is only one move that would create two eyes for the opponent, it should be blocked if possible
|
||||
*/
|
||||
function getEyeBlockingMove(boardState: BoardState, player: PlayerColor, availablePoints: PointState[]) {
|
||||
const opposingPlayer = player === playerColors.white ? playerColors.black : playerColors.white;
|
||||
const opponentEyeMoves = getEyeCreationMoves(boardState, opposingPlayer, availablePoints, 5);
|
||||
function getEyeBlockingMove(board: Board, player: GoColor, availablePoints: PointState[]) {
|
||||
const opposingPlayer = player === GoColor.white ? GoColor.black : GoColor.white;
|
||||
const opponentEyeMoves = getEyeCreationMoves(board, opposingPlayer, availablePoints, 5);
|
||||
const twoEyeMoves = opponentEyeMoves.filter((move) => move.createsLife);
|
||||
const oneEyeMoves = opponentEyeMoves.filter((move) => !move.createsLife);
|
||||
|
||||
@ -727,13 +677,14 @@ function getEyeBlockingMove(boardState: BoardState, player: PlayerColor, availab
|
||||
*/
|
||||
function getMoveOptions(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
player: GoColor,
|
||||
rng: number,
|
||||
smart = true,
|
||||
): { [s in keyof MoveOptions]: () => Promise<Move | null> } {
|
||||
const board = boardState.board;
|
||||
const availableSpaces = findDisputedTerritory(boardState, player, smart);
|
||||
const contestedPoints = getDisputedTerritoryMoves(boardState, player, availableSpaces);
|
||||
const expansionMoves = getExpansionMoveArray(boardState, player, availableSpaces);
|
||||
const contestedPoints = getDisputedTerritoryMoves(board, availableSpaces);
|
||||
const expansionMoves = getExpansionMoveArray(board, availableSpaces);
|
||||
|
||||
// If the player is passing, and all territory is surrounded by a single color: do not suggest moves that
|
||||
// needlessly extend the game, unless they actually can change the score
|
||||
@ -768,22 +719,19 @@ function getMoveOptions(
|
||||
? defendMove
|
||||
: null;
|
||||
},
|
||||
eyeMove: async () => (endGameAvailable ? null : getEyeCreationMove(boardState, player, availableSpaces) ?? null),
|
||||
eyeBlock: async () => (endGameAvailable ? null : getEyeBlockingMove(boardState, player, availableSpaces) ?? null),
|
||||
eyeMove: async () => (endGameAvailable ? null : getEyeCreationMove(board, player, availableSpaces) ?? null),
|
||||
eyeBlock: async () => (endGameAvailable ? null : getEyeBlockingMove(board, player, availableSpaces) ?? null),
|
||||
pattern: async () => {
|
||||
const point = endGameAvailable
|
||||
? null
|
||||
: await findAnyMatchedPatterns(boardState, player, availableSpaces, smart, rng);
|
||||
const point = endGameAvailable ? null : await findAnyMatchedPatterns(board, player, availableSpaces, smart, rng);
|
||||
return point ? { point } : null;
|
||||
},
|
||||
growth: async () =>
|
||||
endGameAvailable ? null : (await getGrowthMove(boardState, player, availableSpaces, rng)) ?? null,
|
||||
expansion: async () => (await getExpansionMove(boardState, player, availableSpaces, rng, expansionMoves)) ?? null,
|
||||
jump: async () => (await getJumpMove(boardState, player, availableSpaces, rng, expansionMoves)) ?? null,
|
||||
defend: async () => (await getDefendMove(boardState, player, availableSpaces)) ?? null,
|
||||
surround: async () => (await getSurroundMove(boardState, player, availableSpaces, smart)) ?? null,
|
||||
growth: async () => (endGameAvailable ? null : (await getGrowthMove(board, player, availableSpaces, rng)) ?? null),
|
||||
expansion: async () => (await getExpansionMove(board, availableSpaces, rng, expansionMoves)) ?? null,
|
||||
jump: async () => (await getJumpMove(board, player, availableSpaces, rng, expansionMoves)) ?? null,
|
||||
defend: async () => (await getDefendMove(board, player, availableSpaces)) ?? null,
|
||||
surround: async () => (await getSurroundMove(board, player, availableSpaces, smart)) ?? null,
|
||||
corner: async () => {
|
||||
const point = getCornerMove(boardState);
|
||||
const point = getCornerMove(board);
|
||||
return point ? { point } : null;
|
||||
},
|
||||
random: async () => {
|
||||
@ -811,7 +759,7 @@ function getMoveOptions(
|
||||
/**
|
||||
* Gets the starting score for white.
|
||||
*/
|
||||
export function getKomi(opponent: opponents) {
|
||||
export function getKomi(opponent: GoOpponent) {
|
||||
return opponentDetails[opponent].komi;
|
||||
}
|
||||
|
||||
@ -823,5 +771,5 @@ export function sleep(ms: number): Promise<void> {
|
||||
}
|
||||
|
||||
export function showWorldDemon() {
|
||||
return Player.augmentations.some((a) => a.name === AugmentationName.TheRedPill) && Player.sourceFileLvl(1);
|
||||
return Player.hasAugmentation(AugmentationName.TheRedPill, true) && Player.sourceFileLvl(1);
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
// Inspired by https://github.com/pasky/michi/blob/master/michi.py
|
||||
import { BoardState, PlayerColor, playerColors, PointState } from "../boardState/goConstants";
|
||||
import type { Board, PointState } from "../Types";
|
||||
|
||||
import { GoColor } from "@enums";
|
||||
import { sleep } from "./goAI";
|
||||
import { findEffectiveLibertiesOfNewMove } from "./boardAnalysis";
|
||||
import { floor } from "../boardState/boardState";
|
||||
@ -78,25 +80,24 @@ export const threeByThreePatterns = [
|
||||
* Searches the board for any point that matches the expanded pattern set
|
||||
*/
|
||||
export async function findAnyMatchedPatterns(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
board: Board,
|
||||
player: GoColor,
|
||||
availableSpaces: PointState[],
|
||||
smart = true,
|
||||
rng: number,
|
||||
) {
|
||||
const board = boardState.board;
|
||||
const boardSize = board[0].length;
|
||||
const patterns = expandAllThreeByThreePatterns();
|
||||
const moves = [];
|
||||
for (let x = 0; x < boardSize; x++) {
|
||||
for (let y = 0; y < boardSize; y++) {
|
||||
const neighborhood = getNeighborhood(boardState, x, y);
|
||||
const neighborhood = getNeighborhood(board, x, y);
|
||||
const matchedPattern = patterns.find((pattern) => checkMatch(neighborhood, pattern, player));
|
||||
|
||||
if (
|
||||
matchedPattern &&
|
||||
availableSpaces.find((availablePoint) => availablePoint.x === x && availablePoint.y === y) &&
|
||||
(!smart || findEffectiveLibertiesOfNewMove(boardState, x, y, player).length > 1)
|
||||
(!smart || findEffectiveLibertiesOfNewMove(board, x, y, player).length > 1)
|
||||
) {
|
||||
moves.push(board[x][y]);
|
||||
}
|
||||
@ -109,7 +110,7 @@ export async function findAnyMatchedPatterns(
|
||||
/**
|
||||
Returns false if any point does not match the pattern, and true if it matches fully.
|
||||
*/
|
||||
function checkMatch(neighborhood: (PointState | null)[][], pattern: string[], player: PlayerColor) {
|
||||
function checkMatch(neighborhood: (PointState | null)[][], pattern: string[], player: GoColor) {
|
||||
const patternArr = pattern.join("").split("");
|
||||
const neighborhoodArray = neighborhood.flat();
|
||||
return patternArr.every((str, index) => matches(str, neighborhoodArray[index], player));
|
||||
@ -118,8 +119,7 @@ function checkMatch(neighborhood: (PointState | null)[][], pattern: string[], pl
|
||||
/**
|
||||
* Gets the 8 points adjacent and diagonally adjacent to the given point
|
||||
*/
|
||||
function getNeighborhood(boardState: BoardState, x: number, y: number) {
|
||||
const board = boardState.board;
|
||||
function getNeighborhood(board: Board, x: number, y: number) {
|
||||
return [
|
||||
[board[x - 1]?.[y - 1], board[x - 1]?.[y], board[x - 1]?.[y + 1]],
|
||||
[board[x]?.[y - 1], board[x]?.[y], board[x]?.[y + 1]],
|
||||
@ -136,23 +136,23 @@ function getNeighborhood(boardState: BoardState, x: number, y: number) {
|
||||
* A space " " only matches the edge of the board
|
||||
* question mark "?" matches anything
|
||||
*/
|
||||
function matches(stringPoint: string, point: PointState | null, player: PlayerColor) {
|
||||
const opponent = player === playerColors.white ? playerColors.black : playerColors.white;
|
||||
function matches(stringPoint: string, point: PointState | null, player: GoColor) {
|
||||
const opponent = player === GoColor.white ? GoColor.black : GoColor.white;
|
||||
switch (stringPoint) {
|
||||
case "X": {
|
||||
return point?.player === player;
|
||||
return point?.color === player;
|
||||
}
|
||||
case "O": {
|
||||
return point?.player === opponent;
|
||||
return point?.color === opponent;
|
||||
}
|
||||
case "x": {
|
||||
return point?.player !== opponent;
|
||||
return point?.color !== opponent;
|
||||
}
|
||||
case "o": {
|
||||
return point?.player !== player;
|
||||
return point?.color !== player;
|
||||
}
|
||||
case ".": {
|
||||
return point?.player === playerColors.empty;
|
||||
return point?.color === GoColor.empty;
|
||||
}
|
||||
case " ": {
|
||||
return point === null;
|
||||
|
@ -1,18 +1,15 @@
|
||||
import {
|
||||
BoardState,
|
||||
getGoPlayerStartingState,
|
||||
opponents,
|
||||
PlayerColor,
|
||||
playerColors,
|
||||
PointState,
|
||||
} from "../boardState/goConstants";
|
||||
import type { Board, BoardState, PointState } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { GoOpponent, GoColor } from "@enums";
|
||||
import { newOpponentStats } from "../Constants";
|
||||
import { getAllChains, getPlayerNeighbors } from "./boardAnalysis";
|
||||
import { getKomi } from "./goAI";
|
||||
import { Player } from "@player";
|
||||
import { getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
|
||||
import { floor, isNotNull } from "../boardState/boardState";
|
||||
import { Factions } from "../../Faction/Factions";
|
||||
import { FactionName } from "@enums";
|
||||
import { getEnumHelper } from "../../utils/EnumHelper";
|
||||
import { Go } from "../Go";
|
||||
|
||||
/**
|
||||
* Returns the score of the current board.
|
||||
@ -21,22 +18,22 @@ import { FactionName } from "@enums";
|
||||
*/
|
||||
export function getScore(boardState: BoardState) {
|
||||
const komi = getKomi(boardState.ai) ?? 6.5;
|
||||
const whitePieces = getColoredPieceCount(boardState, playerColors.white);
|
||||
const blackPieces = getColoredPieceCount(boardState, playerColors.black);
|
||||
const territoryScores = getTerritoryScores(boardState);
|
||||
const whitePieces = getColoredPieceCount(boardState, GoColor.white);
|
||||
const blackPieces = getColoredPieceCount(boardState, GoColor.black);
|
||||
const territoryScores = getTerritoryScores(boardState.board);
|
||||
|
||||
return {
|
||||
[playerColors.white]: {
|
||||
[GoColor.white]: {
|
||||
pieces: whitePieces,
|
||||
territory: territoryScores[playerColors.white],
|
||||
territory: territoryScores[GoColor.white],
|
||||
komi: komi,
|
||||
sum: whitePieces + territoryScores[playerColors.white] + komi,
|
||||
sum: whitePieces + territoryScores[GoColor.white] + komi,
|
||||
},
|
||||
[playerColors.black]: {
|
||||
[GoColor.black]: {
|
||||
pieces: blackPieces,
|
||||
territory: territoryScores[playerColors.black],
|
||||
territory: territoryScores[GoColor.black],
|
||||
komi: 0,
|
||||
sum: blackPieces + territoryScores[playerColors.black],
|
||||
sum: blackPieces + territoryScores[GoColor.black],
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -50,13 +47,13 @@ export function endGoGame(boardState: BoardState) {
|
||||
return;
|
||||
}
|
||||
boardState.previousPlayer = null;
|
||||
const statusToUpdate = getPlayerStats(boardState.ai);
|
||||
const statusToUpdate = getOpponentStats(boardState.ai);
|
||||
statusToUpdate.favor = statusToUpdate.favor ?? 0;
|
||||
const score = getScore(boardState);
|
||||
|
||||
if (score[playerColors.black].sum < score[playerColors.white].sum) {
|
||||
if (score[GoColor.black].sum < score[GoColor.white].sum) {
|
||||
resetWinstreak(boardState.ai, true);
|
||||
statusToUpdate.nodePower += floor(score[playerColors.black].sum * 0.25);
|
||||
statusToUpdate.nodePower += floor(score[GoColor.black].sum * 0.25);
|
||||
} else {
|
||||
statusToUpdate.wins++;
|
||||
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
|
||||
@ -66,12 +63,12 @@ export function endGoGame(boardState: BoardState) {
|
||||
statusToUpdate.highestWinStreak = statusToUpdate.winStreak;
|
||||
}
|
||||
|
||||
const factionName = boardState.ai as unknown as FactionName;
|
||||
const factionName = getEnumHelper("FactionName").getMember(boardState.ai);
|
||||
if (
|
||||
factionName &&
|
||||
statusToUpdate.winStreak % 2 === 0 &&
|
||||
Player.factions.includes(factionName) &&
|
||||
statusToUpdate.favor < getMaxFavor() &&
|
||||
Factions?.[factionName]
|
||||
statusToUpdate.favor < getMaxFavor()
|
||||
) {
|
||||
Factions[factionName].favor++;
|
||||
statusToUpdate.favor++;
|
||||
@ -79,13 +76,13 @@ export function endGoGame(boardState: BoardState) {
|
||||
}
|
||||
|
||||
statusToUpdate.nodePower +=
|
||||
score[playerColors.black].sum *
|
||||
getDifficultyMultiplier(score[playerColors.white].komi, boardState.board[0].length) *
|
||||
score[GoColor.black].sum *
|
||||
getDifficultyMultiplier(score[GoColor.white].komi, boardState.board[0].length) *
|
||||
getWinstreakMultiplier(statusToUpdate.winStreak, statusToUpdate.oldWinStreak);
|
||||
|
||||
statusToUpdate.nodes += score[playerColors.black].sum;
|
||||
Player.go.boardState = boardState;
|
||||
Player.go.previousGameFinalBoardState = boardState;
|
||||
statusToUpdate.nodes += score[GoColor.black].sum;
|
||||
Go.currentGame = boardState;
|
||||
Go.previousGame = boardState;
|
||||
|
||||
// Update multipliers with new bonuses, once at the end of the game
|
||||
Player.applyEntropy(Player.entropy);
|
||||
@ -94,8 +91,8 @@ export function endGoGame(boardState: BoardState) {
|
||||
/**
|
||||
* Sets the winstreak to zero for the given opponent, and adds a loss
|
||||
*/
|
||||
export function resetWinstreak(opponent: opponents, gameComplete: boolean) {
|
||||
const statusToUpdate = getPlayerStats(opponent);
|
||||
export function resetWinstreak(opponent: GoOpponent, gameComplete: boolean) {
|
||||
const statusToUpdate = getOpponentStats(opponent);
|
||||
statusToUpdate.losses++;
|
||||
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
|
||||
if (statusToUpdate.winStreak >= 0) {
|
||||
@ -109,9 +106,9 @@ export function resetWinstreak(opponent: opponents, gameComplete: boolean) {
|
||||
/**
|
||||
* Gets the number pieces of a given color on the board
|
||||
*/
|
||||
function getColoredPieceCount(boardState: BoardState, color: PlayerColor) {
|
||||
function getColoredPieceCount(boardState: BoardState, color: GoColor) {
|
||||
return boardState.board.reduce(
|
||||
(sum, row) => sum + row.filter(isNotNull).filter((point) => point.player === color).length,
|
||||
(sum, row) => sum + row.filter(isNotNull).filter((point) => point.color === color).length,
|
||||
0,
|
||||
);
|
||||
}
|
||||
@ -119,22 +116,20 @@ function getColoredPieceCount(boardState: BoardState, color: PlayerColor) {
|
||||
/**
|
||||
* Finds all empty spaces fully surrounded by a single player's stones
|
||||
*/
|
||||
function getTerritoryScores(boardState: BoardState) {
|
||||
const emptyTerritoryChains = getAllChains(boardState).filter((chain) => chain?.[0]?.player === playerColors.empty);
|
||||
function getTerritoryScores(board: Board) {
|
||||
const emptyTerritoryChains = getAllChains(board).filter((chain) => chain?.[0]?.color === GoColor.empty);
|
||||
|
||||
return emptyTerritoryChains.reduce(
|
||||
(scores, currentChain) => {
|
||||
const chainColor = checkTerritoryOwnership(boardState, currentChain);
|
||||
const chainColor = checkTerritoryOwnership(board, currentChain);
|
||||
return {
|
||||
[playerColors.white]:
|
||||
scores[playerColors.white] + (chainColor === playerColors.white ? currentChain.length : 0),
|
||||
[playerColors.black]:
|
||||
scores[playerColors.black] + (chainColor === playerColors.black ? currentChain.length : 0),
|
||||
[GoColor.white]: scores[GoColor.white] + (chainColor === GoColor.white ? currentChain.length : 0),
|
||||
[GoColor.black]: scores[GoColor.black] + (chainColor === GoColor.black ? currentChain.length : 0),
|
||||
};
|
||||
},
|
||||
{
|
||||
[playerColors.white]: 0,
|
||||
[playerColors.black]: 0,
|
||||
[GoColor.white]: 0,
|
||||
[GoColor.black]: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -142,17 +137,17 @@ function getTerritoryScores(boardState: BoardState) {
|
||||
/**
|
||||
* Finds all neighbors of the empty points in question. If they are all one color, that player controls that space
|
||||
*/
|
||||
function checkTerritoryOwnership(boardState: BoardState, emptyPointChain: PointState[]) {
|
||||
if (emptyPointChain.length > boardState.board[0].length ** 2 - 3) {
|
||||
function checkTerritoryOwnership(board: Board, emptyPointChain: PointState[]) {
|
||||
if (emptyPointChain.length > board[0].length ** 2 - 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const playerNeighbors = getPlayerNeighbors(boardState, emptyPointChain);
|
||||
const hasWhitePieceNeighbors = playerNeighbors.find((p) => p.player === playerColors.white);
|
||||
const hasBlackPieceNeighbors = playerNeighbors.find((p) => p.player === playerColors.black);
|
||||
const playerNeighbors = getPlayerNeighbors(board, emptyPointChain);
|
||||
const hasWhitePieceNeighbors = playerNeighbors.find((p) => p.color === GoColor.white);
|
||||
const hasBlackPieceNeighbors = playerNeighbors.find((p) => p.color === GoColor.black);
|
||||
const isWhiteTerritory = hasWhitePieceNeighbors && !hasBlackPieceNeighbors;
|
||||
const isBlackTerritory = hasBlackPieceNeighbors && !hasWhitePieceNeighbors;
|
||||
return isWhiteTerritory ? playerColors.white : isBlackTerritory ? playerColors.black : null;
|
||||
return isWhiteTerritory ? GoColor.white : isBlackTerritory ? GoColor.black : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -171,9 +166,6 @@ export function logBoard(boardState: BoardState): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function getPlayerStats(opponent: opponents) {
|
||||
if (!Player.go.status[opponent]) {
|
||||
Player.go = getGoPlayerStartingState();
|
||||
}
|
||||
return Player.go.status[opponent];
|
||||
export function getOpponentStats(opponent: GoOpponent) {
|
||||
return Go.stats[opponent] ?? (Go.stats[opponent] = newOpponentStats());
|
||||
}
|
||||
|
@ -1,42 +1,33 @@
|
||||
import {
|
||||
bitverseBoardShape,
|
||||
Board,
|
||||
BoardState,
|
||||
Move,
|
||||
Neighbor,
|
||||
opponents,
|
||||
PlayerColor,
|
||||
playerColors,
|
||||
PointState,
|
||||
validityReason,
|
||||
} from "./goConstants";
|
||||
import type { Board, BoardState, Move, Neighbor, PointState } from "../Types";
|
||||
|
||||
import { GoOpponent, GoColor, GoValidity } from "@enums";
|
||||
import { bitverseBoardShape } from "../Constants";
|
||||
import { getExpansionMoveArray } from "../boardAnalysis/goAI";
|
||||
import {
|
||||
evaluateIfMoveIsValid,
|
||||
findAllCapturedChains,
|
||||
findLibertiesForChain,
|
||||
getAllChains,
|
||||
getBoardFromSimplifiedBoardState,
|
||||
boardFromSimpleBoard,
|
||||
simpleBoardFromBoard,
|
||||
} from "../boardAnalysis/boardAnalysis";
|
||||
import { endGoGame } from "../boardAnalysis/scoring";
|
||||
import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes";
|
||||
|
||||
/**
|
||||
* Generates a new BoardState object with the given opponent and size
|
||||
*/
|
||||
/** Generates a new BoardState object with the given opponent and size. Optionally use an existing board. */
|
||||
export function getNewBoardState(
|
||||
boardSize: number,
|
||||
ai = opponents.Netburners,
|
||||
ai = GoOpponent.Netburners,
|
||||
applyObstacles = false,
|
||||
boardToCopy?: Board,
|
||||
): BoardState {
|
||||
if (ai === opponents.w0r1d_d43m0n) {
|
||||
boardToCopy = resetCoordinates(rotate90Degrees(getBoardFromSimplifiedBoardState(bitverseBoardShape).board));
|
||||
if (ai === GoOpponent.w0r1d_d43m0n) {
|
||||
boardToCopy = resetCoordinates(rotate90Degrees(boardFromSimpleBoard(bitverseBoardShape)));
|
||||
}
|
||||
|
||||
const newBoardState = {
|
||||
history: [],
|
||||
previousPlayer: playerColors.white,
|
||||
const newBoardState: BoardState = {
|
||||
previousBoard: null,
|
||||
previousPlayer: GoColor.white,
|
||||
ai: ai,
|
||||
passCount: 0,
|
||||
cheatCount: 0,
|
||||
@ -44,7 +35,7 @@ export function getNewBoardState(
|
||||
Array.from({ length: boardSize }, (_, y) =>
|
||||
!boardToCopy || boardToCopy?.[x]?.[y]
|
||||
? {
|
||||
player: boardToCopy?.[x]?.[y]?.player ?? playerColors.empty,
|
||||
color: boardToCopy?.[x]?.[y]?.color ?? GoColor.empty,
|
||||
chain: "",
|
||||
liberties: null,
|
||||
x,
|
||||
@ -61,7 +52,7 @@ export function getNewBoardState(
|
||||
|
||||
const handicap = getHandicap(newBoardState.board[0].length, ai);
|
||||
if (handicap) {
|
||||
applyHandicap(newBoardState, handicap);
|
||||
applyHandicap(newBoardState.board, handicap);
|
||||
}
|
||||
return newBoardState;
|
||||
}
|
||||
@ -69,9 +60,9 @@ export function getNewBoardState(
|
||||
/**
|
||||
* Determines how many starting pieces the opponent has on the board
|
||||
*/
|
||||
export function getHandicap(boardSize: number, opponent: opponents) {
|
||||
export function getHandicap(boardSize: number, opponent: GoOpponent) {
|
||||
// Illuminati and WD get a few starting routers
|
||||
if (opponent === opponents.Illuminati || opponent === opponents.w0r1d_d43m0n) {
|
||||
if (opponent === GoOpponent.Illuminati || opponent === GoOpponent.w0r1d_d43m0n) {
|
||||
return ceil(boardSize * 0.35);
|
||||
}
|
||||
return 0;
|
||||
@ -79,38 +70,38 @@ export function getHandicap(boardSize: number, opponent: opponents) {
|
||||
|
||||
/**
|
||||
* Make a new move on the given board, and update the board state accordingly
|
||||
* Modifies the board state in place
|
||||
* @returns a boolean representing whether the move was successful
|
||||
*/
|
||||
export function makeMove(boardState: BoardState, x: number, y: number, player: PlayerColor) {
|
||||
export function makeMove(boardState: BoardState, x: number, y: number, player: GoColor) {
|
||||
// Do not update on invalid moves
|
||||
const validity = evaluateIfMoveIsValid(boardState, x, y, player, false);
|
||||
if (validity !== validityReason.valid || !boardState.board[x][y]?.player) {
|
||||
console.debug(`Invalid move attempted! ${x} ${y} ${player} : ${validity}`);
|
||||
if (validity !== GoValidity.valid || !boardState.board[x][y]?.color) {
|
||||
//console.debug(`Invalid move attempted! ${x} ${y} ${player} : ${validity}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
boardState.history.push(getBoardCopy(boardState).board);
|
||||
boardState.history = boardState.history.slice(-4);
|
||||
boardState.previousBoard = simpleBoardFromBoard(boardState.board);
|
||||
const point = boardState.board[x][y];
|
||||
if (!point) {
|
||||
return false;
|
||||
}
|
||||
point.player = player;
|
||||
if (!point) return false;
|
||||
|
||||
point.color = player;
|
||||
boardState.previousPlayer = player;
|
||||
boardState.passCount = 0;
|
||||
|
||||
return updateCaptures(boardState, player);
|
||||
updateCaptures(boardState.board, player);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass the current player's turn without making a move.
|
||||
* Ends the game if this is the second pass in a row.
|
||||
*/
|
||||
export function passTurn(boardState: BoardState, player: playerColors, allowEndGame = true) {
|
||||
export function passTurn(boardState: BoardState, player: GoColor, allowEndGame = true) {
|
||||
if (boardState.previousPlayer === null || boardState.previousPlayer === player) {
|
||||
return;
|
||||
}
|
||||
boardState.previousPlayer =
|
||||
boardState.previousPlayer === playerColors.black ? playerColors.white : playerColors.black;
|
||||
boardState.previousPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
|
||||
boardState.passCount++;
|
||||
|
||||
if (boardState.passCount >= 2 && allowEndGame) {
|
||||
@ -120,10 +111,11 @@ export function passTurn(boardState: BoardState, player: playerColors, allowEndG
|
||||
|
||||
/**
|
||||
* Makes a number of random moves on the board before the game starts, to give one player an edge.
|
||||
* Modifies the board in place.
|
||||
*/
|
||||
export function applyHandicap(boardState: BoardState, handicap: number) {
|
||||
const availableMoves = getEmptySpaces(boardState);
|
||||
const handicapMoveOptions = getExpansionMoveArray(boardState, playerColors.black, availableMoves);
|
||||
export function applyHandicap(board: Board, handicap: number): void {
|
||||
const availableMoves = getEmptySpaces(board);
|
||||
const handicapMoveOptions = getExpansionMoveArray(board, availableMoves);
|
||||
const handicapMoves: Move[] = [];
|
||||
|
||||
// select random distinct moves from the move options list up to the specified handicap amount
|
||||
@ -134,29 +126,28 @@ export function applyHandicap(boardState: BoardState, handicap: number) {
|
||||
}
|
||||
|
||||
handicapMoves.forEach((move: Move) => {
|
||||
const point = boardState.board[move.point.x][move.point.y];
|
||||
return move.point && point && (point.player = playerColors.white);
|
||||
const point = board[move.point.x][move.point.y];
|
||||
return move.point && point && (point.color = GoColor.white);
|
||||
});
|
||||
return updateChains(boardState);
|
||||
updateChains(board);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all groups of connected stones on the board, and updates the points in them with their
|
||||
* chain information and liberties.
|
||||
* Updates a board in-place.
|
||||
*/
|
||||
export function updateChains(boardState: BoardState, resetChains = true) {
|
||||
resetChains && clearChains(boardState);
|
||||
export function updateChains(board: Board, resetChains = true): void {
|
||||
resetChains && clearChains(board);
|
||||
|
||||
for (let x = 0; x < boardState.board.length; x++) {
|
||||
for (let y = 0; y < boardState.board[x].length; y++) {
|
||||
const point = boardState.board[x][y];
|
||||
for (let x = 0; x < board.length; x++) {
|
||||
for (let y = 0; y < board[x].length; y++) {
|
||||
const point = board[x][y];
|
||||
// If the current point is already analyzed, skip it
|
||||
if (!point || point.chain !== "") {
|
||||
continue;
|
||||
}
|
||||
if (!point || point.chain !== "") continue;
|
||||
|
||||
const chainMembers = findAdjacentPointsInChain(boardState, x, y);
|
||||
const libertiesForChain = findLibertiesForChain(boardState, chainMembers);
|
||||
const chainMembers = findAdjacentPointsInChain(board, x, y);
|
||||
const libertiesForChain = findLibertiesForChain(board, chainMembers);
|
||||
const id = `${point.x},${point.y}`;
|
||||
|
||||
chainMembers.forEach((member) => {
|
||||
@ -165,8 +156,6 @@ export function updateChains(boardState: BoardState, resetChains = true) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return boardState;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -174,10 +163,11 @@ export function updateChains(boardState: BoardState, resetChains = true) {
|
||||
* adjacent to some point on the chain including the current point).
|
||||
*
|
||||
* Then, remove any chains with no liberties.
|
||||
* Modifies the board in place.
|
||||
*/
|
||||
export function updateCaptures(initialState: BoardState, playerWhoMoved: PlayerColor, resetChains = true): BoardState {
|
||||
const boardState = updateChains(initialState, resetChains);
|
||||
const chains = getAllChains(boardState);
|
||||
export function updateCaptures(board: Board, playerWhoMoved: GoColor, resetChains = true): void {
|
||||
const boardState = updateChains(board, resetChains);
|
||||
const chains = getAllChains(board);
|
||||
|
||||
const chainsToCapture = findAllCapturedChains(chains, playerWhoMoved);
|
||||
if (!chainsToCapture?.length) {
|
||||
@ -185,7 +175,7 @@ export function updateCaptures(initialState: BoardState, playerWhoMoved: PlayerC
|
||||
}
|
||||
|
||||
chainsToCapture?.forEach((chain) => captureChain(chain));
|
||||
return updateChains(boardState);
|
||||
updateChains(board);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -193,27 +183,25 @@ export function updateCaptures(initialState: BoardState, playerWhoMoved: PlayerC
|
||||
*/
|
||||
function captureChain(chain: PointState[]) {
|
||||
chain.forEach((point) => {
|
||||
point.player = playerColors.empty;
|
||||
point.color = GoColor.empty;
|
||||
point.chain = "";
|
||||
point.liberties = [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the chain data from given points, in preparation for being recalculated later
|
||||
* Removes the chain data from all points on a board, in preparation for being recalculated later
|
||||
* Updates the board in-place
|
||||
*/
|
||||
function clearChains(boardState: BoardState): BoardState {
|
||||
for (const x in boardState.board) {
|
||||
for (const y in boardState.board[x]) {
|
||||
const point = boardState.board[x][y];
|
||||
if (point && point.chain && point.liberties) {
|
||||
function clearChains(board: Board): void {
|
||||
for (const column of board) {
|
||||
for (const point of column) {
|
||||
if (!point) continue;
|
||||
point.chain = "";
|
||||
point.liberties = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return boardState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all the pieces in the current continuous group, or 'chain'
|
||||
@ -221,8 +209,8 @@ function clearChains(boardState: BoardState): BoardState {
|
||||
* Iteratively traverse the adjacent pieces of the same color to find all the pieces in the same chain,
|
||||
* which are the pieces connected directly via a path consisting only of only up/down/left/right
|
||||
*/
|
||||
export function findAdjacentPointsInChain(boardState: BoardState, x: number, y: number) {
|
||||
const point = boardState.board[x][y];
|
||||
export function findAdjacentPointsInChain(board: Board, x: number, y: number) {
|
||||
const point = board[x][y];
|
||||
if (!point) {
|
||||
return [];
|
||||
}
|
||||
@ -237,13 +225,13 @@ export function findAdjacentPointsInChain(boardState: BoardState, x: number, y:
|
||||
}
|
||||
|
||||
checkedPoints.push(currentPoint);
|
||||
const neighbors = findNeighbors(boardState, currentPoint.x, currentPoint.y);
|
||||
const neighbors = findNeighbors(board, currentPoint.x, currentPoint.y);
|
||||
|
||||
[neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined)
|
||||
.forEach((neighbor) => {
|
||||
if (neighbor && neighbor.player === currentPoint.player && !contains(checkedPoints, neighbor)) {
|
||||
if (neighbor && neighbor.color === currentPoint.color && !contains(checkedPoints, neighbor)) {
|
||||
adjacentPoints.push(neighbor);
|
||||
pointsToCheckNeighbors.push(neighbor);
|
||||
}
|
||||
@ -257,12 +245,12 @@ export function findAdjacentPointsInChain(boardState: BoardState, x: number, y:
|
||||
/**
|
||||
* Finds all empty spaces on the board.
|
||||
*/
|
||||
export function getEmptySpaces(boardState: BoardState): PointState[] {
|
||||
export function getEmptySpaces(board: Board): PointState[] {
|
||||
const emptySpaces: PointState[] = [];
|
||||
|
||||
boardState.board.forEach((column) => {
|
||||
board.forEach((column) => {
|
||||
column.forEach((point) => {
|
||||
if (point && point.player === playerColors.empty) {
|
||||
if (point && point.color === GoColor.empty) {
|
||||
emptySpaces.push(point);
|
||||
}
|
||||
});
|
||||
@ -277,7 +265,7 @@ export function getEmptySpaces(boardState: BoardState): PointState[] {
|
||||
export function getStateCopy(initialState: BoardState) {
|
||||
const boardState = structuredClone(initialState);
|
||||
|
||||
boardState.history = [...initialState.history];
|
||||
boardState.previousBoard = initialState.previousBoard ? [...initialState.previousBoard] : null;
|
||||
boardState.previousPlayer = initialState.previousPlayer;
|
||||
boardState.ai = initialState.ai;
|
||||
boardState.passCount = initialState.passCount;
|
||||
@ -285,34 +273,16 @@ export function getStateCopy(initialState: BoardState) {
|
||||
return boardState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a deep copy of the given BoardState's board
|
||||
*/
|
||||
export function getBoardCopy(boardState: BoardState) {
|
||||
const boardCopy = getNewBoardState(boardState.board[0].length);
|
||||
const board = boardState.board;
|
||||
|
||||
for (let x = 0; x < board.length; x++) {
|
||||
for (let y = 0; y < board[x].length; y++) {
|
||||
const pointToEdit = boardCopy.board[x][y];
|
||||
const point = board[x][y];
|
||||
if (!point || !pointToEdit) {
|
||||
boardCopy.board[x][y] = null;
|
||||
} else {
|
||||
pointToEdit.player = point.player;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return boardCopy;
|
||||
/** Make a deep copy of a board */
|
||||
export function getBoardCopy(board: Board): Board {
|
||||
return structuredClone(board);
|
||||
}
|
||||
|
||||
export function contains(arr: PointState[], point: PointState) {
|
||||
return !!arr.find((p) => p && p.x === point.x && p.y === point.y);
|
||||
}
|
||||
|
||||
export function findNeighbors(boardState: BoardState, x: number, y: number): Neighbor {
|
||||
const board = boardState.board;
|
||||
export function findNeighbors(board: Board, x: number, y: number): Neighbor {
|
||||
return {
|
||||
north: board[x]?.[y + 1],
|
||||
east: board[x + 1]?.[y],
|
||||
|
@ -1,312 +0,0 @@
|
||||
import { getNewBoardState } from "./boardState";
|
||||
import { FactionName } from "@enums";
|
||||
|
||||
export enum playerColors {
|
||||
white = "White",
|
||||
black = "Black",
|
||||
empty = "Empty",
|
||||
}
|
||||
|
||||
export enum validityReason {
|
||||
pointBroken = "That node is offline; a piece cannot be placed there",
|
||||
pointNotEmpty = "That node is already occupied by a piece",
|
||||
boardRepeated = "It is illegal to repeat prior board states",
|
||||
noSuicide = "It is illegal to cause your own pieces to be captured",
|
||||
notYourTurn = "It is not your turn to play",
|
||||
gameOver = "The game is over",
|
||||
invalid = "Invalid move",
|
||||
valid = "Valid move",
|
||||
}
|
||||
|
||||
export enum opponents {
|
||||
none = "No AI",
|
||||
Netburners = FactionName.Netburners,
|
||||
SlumSnakes = FactionName.SlumSnakes,
|
||||
TheBlackHand = FactionName.TheBlackHand,
|
||||
Tetrads = FactionName.Tetrads,
|
||||
Daedalus = FactionName.Daedalus,
|
||||
Illuminati = FactionName.Illuminati,
|
||||
w0r1d_d43m0n = "????????????",
|
||||
}
|
||||
|
||||
export const opponentList = [
|
||||
opponents.Netburners,
|
||||
opponents.SlumSnakes,
|
||||
opponents.TheBlackHand,
|
||||
opponents.Tetrads,
|
||||
opponents.Daedalus,
|
||||
opponents.Illuminati,
|
||||
];
|
||||
|
||||
export const opponentDetails = {
|
||||
[opponents.none]: {
|
||||
komi: 5.5,
|
||||
description: "Practice Board",
|
||||
flavorText: "Practice on a subnet where you place both colors of routers.",
|
||||
bonusDescription: "",
|
||||
bonusPower: 0,
|
||||
},
|
||||
[opponents.Netburners]: {
|
||||
komi: 1.5,
|
||||
description: "Easy AI",
|
||||
flavorText:
|
||||
"The Netburners faction are a mysterious group with only the most tenuous control over their subnets. Concentrating mainly on their hacknet server business, IPvGO is not their main strength.",
|
||||
bonusDescription: "increased hacknet production",
|
||||
bonusPower: 1.3,
|
||||
},
|
||||
[opponents.SlumSnakes]: {
|
||||
komi: 3.5,
|
||||
description: "Spread AI",
|
||||
flavorText:
|
||||
"The Slum Snakes faction are a small-time street gang who turned to organized crime using their subnets. They are known to use long router chains snaking across the subnet to encircle territory.",
|
||||
bonusDescription: "crime success rate",
|
||||
bonusPower: 1.2,
|
||||
},
|
||||
[opponents.TheBlackHand]: {
|
||||
komi: 3.5,
|
||||
description: "Aggro AI",
|
||||
flavorText:
|
||||
"The Black Hand faction is a black-hat hacking group who uses their subnets to launch targeted DDOS attacks. They are famous for their unrelenting aggression, surrounding and strangling any foothold their opponents try to establish.",
|
||||
bonusDescription: "hacking money",
|
||||
bonusPower: 0.9,
|
||||
},
|
||||
[opponents.Tetrads]: {
|
||||
komi: 5.5,
|
||||
description: "Martial AI",
|
||||
flavorText:
|
||||
"The faction known as Tetrads prefers to get up close and personal. Their combat style excels at circling around and cutting through their opponents, both on and off of the subnets.",
|
||||
bonusDescription: "strength, dex, and agility levels",
|
||||
bonusPower: 0.7,
|
||||
},
|
||||
[opponents.Daedalus]: {
|
||||
komi: 5.5,
|
||||
description: "Mid AI",
|
||||
flavorText:
|
||||
"Not much is known about this shadowy faction. They do not easily let go of subnets that they control, and are known to lease IPvGO cycles in exchange for reputation among other factions.",
|
||||
bonusDescription: "reputation gain",
|
||||
bonusPower: 1.1,
|
||||
},
|
||||
[opponents.Illuminati]: {
|
||||
komi: 7.5,
|
||||
description: "Hard AI",
|
||||
flavorText:
|
||||
"The Illuminati are thought to only exist in myth. Said to always have prepared defenses in their IPvGO subnets. Provoke them at your own risk.",
|
||||
bonusDescription: "faster hack(), grow(), and weaken()",
|
||||
bonusPower: 0.7,
|
||||
},
|
||||
[opponents.w0r1d_d43m0n]: {
|
||||
komi: 9.5,
|
||||
description: "???",
|
||||
flavorText: "What you have seen is only the shadow of the truth. It's time to leave the cave.",
|
||||
bonusDescription: "hacking level",
|
||||
bonusPower: 2,
|
||||
},
|
||||
};
|
||||
|
||||
export const boardSizes = [5, 7, 9, 13];
|
||||
|
||||
export type PlayerColor = playerColors.white | playerColors.black | playerColors.empty;
|
||||
|
||||
export type Board = (PointState | null)[][];
|
||||
|
||||
export type MoveOptions = {
|
||||
capture: () => Promise<Move | null>;
|
||||
defendCapture: () => Promise<Move | null>;
|
||||
eyeMove: () => Promise<Move | null>;
|
||||
eyeBlock: () => Promise<Move | null>;
|
||||
pattern: () => Promise<Move | null>;
|
||||
growth: () => Promise<Move | null>;
|
||||
expansion: () => Promise<Move | null>;
|
||||
jump: () => Promise<Move | null>;
|
||||
defend: () => Promise<Move | null>;
|
||||
surround: () => Promise<Move | null>;
|
||||
corner: () => Promise<Move | null>;
|
||||
random: () => Promise<Move | null>;
|
||||
};
|
||||
|
||||
export type Move = {
|
||||
point: PointState;
|
||||
oldLibertyCount?: number | null;
|
||||
newLibertyCount?: number | null;
|
||||
createsLife?: boolean;
|
||||
};
|
||||
|
||||
export type EyeMove = {
|
||||
point: PointState;
|
||||
createsLife: boolean;
|
||||
};
|
||||
|
||||
export type BoardState = {
|
||||
board: Board;
|
||||
previousPlayer: PlayerColor | null;
|
||||
history: Board[];
|
||||
ai: opponents;
|
||||
passCount: number;
|
||||
cheatCount: number;
|
||||
};
|
||||
|
||||
export type PointState = {
|
||||
player: PlayerColor;
|
||||
chain: string;
|
||||
liberties: (PointState | null)[] | null;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* "invalid" or "move" or "pass" or "gameOver"
|
||||
*/
|
||||
export enum playTypes {
|
||||
invalid = "invalid",
|
||||
move = "move",
|
||||
pass = "pass",
|
||||
gameOver = "gameOver",
|
||||
}
|
||||
|
||||
export type Play = {
|
||||
success: boolean;
|
||||
type: playTypes;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Neighbor = {
|
||||
north: PointState | null;
|
||||
east: PointState | null;
|
||||
south: PointState | null;
|
||||
west: PointState | null;
|
||||
};
|
||||
|
||||
export type goScore = {
|
||||
White: { pieces: number; territory: number; komi: number; sum: number };
|
||||
Black: { pieces: number; territory: number; komi: number; sum: number };
|
||||
};
|
||||
|
||||
export const columnIndexes = "ABCDEFGHJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
type opponentHistory = {
|
||||
wins: number;
|
||||
losses: number;
|
||||
nodes: number;
|
||||
nodePower: number;
|
||||
winStreak: number;
|
||||
oldWinStreak: number;
|
||||
highestWinStreak: number;
|
||||
favor: number;
|
||||
};
|
||||
|
||||
export function getGoPlayerStartingState(): {
|
||||
previousGameFinalBoardState: BoardState | null;
|
||||
boardState: BoardState;
|
||||
status: { [o in opponents]: opponentHistory };
|
||||
} {
|
||||
const previousGame: BoardState | null = null;
|
||||
return {
|
||||
boardState: getNewBoardState(7),
|
||||
status: {
|
||||
[opponents.none]: {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
nodes: 0,
|
||||
nodePower: 0,
|
||||
winStreak: 0,
|
||||
oldWinStreak: 0,
|
||||
highestWinStreak: 0,
|
||||
favor: 0,
|
||||
},
|
||||
[opponents.Netburners]: {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
nodes: 0,
|
||||
nodePower: 0,
|
||||
winStreak: 0,
|
||||
oldWinStreak: 0,
|
||||
highestWinStreak: 0,
|
||||
favor: 0,
|
||||
},
|
||||
[opponents.SlumSnakes]: {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
nodes: 0,
|
||||
nodePower: 0,
|
||||
winStreak: 0,
|
||||
oldWinStreak: 0,
|
||||
highestWinStreak: 0,
|
||||
favor: 0,
|
||||
},
|
||||
[opponents.TheBlackHand]: {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
nodes: 0,
|
||||
nodePower: 0,
|
||||
winStreak: 0,
|
||||
oldWinStreak: 0,
|
||||
highestWinStreak: 0,
|
||||
favor: 0,
|
||||
},
|
||||
[opponents.Tetrads]: {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
nodes: 0,
|
||||
nodePower: 0,
|
||||
winStreak: 0,
|
||||
oldWinStreak: 0,
|
||||
highestWinStreak: 0,
|
||||
favor: 0,
|
||||
},
|
||||
[opponents.Daedalus]: {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
nodes: 0,
|
||||
nodePower: 0,
|
||||
winStreak: 0,
|
||||
oldWinStreak: 0,
|
||||
highestWinStreak: 0,
|
||||
favor: 0,
|
||||
},
|
||||
[opponents.Illuminati]: {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
nodes: 0,
|
||||
nodePower: 0,
|
||||
winStreak: 0,
|
||||
oldWinStreak: 0,
|
||||
highestWinStreak: 0,
|
||||
favor: 0,
|
||||
},
|
||||
[opponents.w0r1d_d43m0n]: {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
nodes: 0,
|
||||
nodePower: 0,
|
||||
winStreak: 0,
|
||||
oldWinStreak: 0,
|
||||
highestWinStreak: 0,
|
||||
favor: 0,
|
||||
},
|
||||
},
|
||||
previousGameFinalBoardState: previousGame,
|
||||
};
|
||||
}
|
||||
|
||||
export const bitverseBoardShape = [
|
||||
"########...########",
|
||||
"######.#...#.######",
|
||||
"###.#..#...#..#.###",
|
||||
".#..#..#...#..#..#.",
|
||||
".#.....#...#.....#.",
|
||||
"...................",
|
||||
"...................",
|
||||
"...................",
|
||||
"...................",
|
||||
".....##.....##.....",
|
||||
"....###.....###....",
|
||||
"....##.......##....",
|
||||
"....#.........#....",
|
||||
".........#.........",
|
||||
"#........#........#",
|
||||
"##.......#.......##",
|
||||
"##.......#.......##",
|
||||
"###.............###",
|
||||
"####...........####",
|
||||
];
|
@ -1,6 +1,8 @@
|
||||
import { Board, boardSizes, BoardState, PointState } from "./goConstants";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
import type { Board, BoardState, PointState } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { boardSizes } from "../Constants";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
import { floor } from "./boardState";
|
||||
|
||||
type rand = (n1: number, n2: number) => number;
|
||||
@ -122,6 +124,6 @@ function rotateNTimes(board: Board, rotations: number) {
|
||||
return board;
|
||||
}
|
||||
|
||||
export function rotate90Degrees(board: Board) {
|
||||
export function rotate90Degrees(board: Board): Board {
|
||||
return board[0].map((_, index: number) => board.map((row: (PointState | null)[]) => row[index]).reverse());
|
||||
}
|
||||
|
@ -1,16 +1,19 @@
|
||||
import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
|
||||
import { getGoPlayerStartingState, opponentDetails, opponentList, opponents } from "../boardState/goConstants";
|
||||
import { Player } from "@player";
|
||||
|
||||
import { GoOpponent } from "@enums";
|
||||
import { Go } from "../Go";
|
||||
import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
|
||||
import { opponentDetails } from "../Constants";
|
||||
import { defaultMultipliers, mergeMultipliers, Multipliers } from "../../PersonObjects/Multipliers";
|
||||
import { PlayerObject } from "../../PersonObjects/Player/PlayerObject";
|
||||
import { formatPercent } from "../../ui/formatNumber";
|
||||
import { getPlayerStats } from "../boardAnalysis/scoring";
|
||||
import { getOpponentStats } from "../boardAnalysis/scoring";
|
||||
import { getRecordEntries, getRecordValues } from "../../Types/Record";
|
||||
|
||||
/**
|
||||
* Calculates the effect size of the given player boost, based on the node power (points based on number of subnet
|
||||
* nodes captured and player wins) and effect power (scalar for individual boosts)
|
||||
*/
|
||||
export function CalculateEffect(nodes: number, faction: opponents): number {
|
||||
export function CalculateEffect(nodes: number, faction: GoOpponent): number {
|
||||
const power = getEffectPowerForFaction(faction);
|
||||
const sourceFileBonus = Player.sourceFileLvl(14) ? 1.25 : 1;
|
||||
return (
|
||||
@ -37,8 +40,8 @@ export function getMaxFavor() {
|
||||
/**
|
||||
* Gets a formatted description of the current bonus from this faction
|
||||
*/
|
||||
export function getBonusText(opponent: opponents) {
|
||||
const nodePower = getPlayerStats(opponent).nodePower;
|
||||
export function getBonusText(opponent: GoOpponent) {
|
||||
const nodePower = getOpponentStats(opponent).nodePower;
|
||||
const effectPercent = formatPercent(CalculateEffect(nodePower, opponent) - 1);
|
||||
const effectDescription = getEffectTypeForFaction(opponent);
|
||||
return `${effectPercent} ${effectDescription}`;
|
||||
@ -58,35 +61,31 @@ export function updateGoMults(): void {
|
||||
*/
|
||||
function calculateMults(): Multipliers {
|
||||
const mults = defaultMultipliers();
|
||||
[...opponentList, opponents.w0r1d_d43m0n].forEach((opponent) => {
|
||||
if (!Player.go?.status?.[opponent]) {
|
||||
Player.go = getGoPlayerStartingState();
|
||||
}
|
||||
|
||||
const effect = CalculateEffect(getPlayerStats(opponent).nodePower, opponent);
|
||||
getRecordEntries(Go.stats).forEach(([opponent, stats]) => {
|
||||
const effect = CalculateEffect(stats.nodePower, opponent);
|
||||
switch (opponent) {
|
||||
case opponents.Netburners:
|
||||
case GoOpponent.Netburners:
|
||||
mults.hacknet_node_money *= effect;
|
||||
break;
|
||||
case opponents.SlumSnakes:
|
||||
case GoOpponent.SlumSnakes:
|
||||
mults.crime_success *= effect;
|
||||
break;
|
||||
case opponents.TheBlackHand:
|
||||
case GoOpponent.TheBlackHand:
|
||||
mults.hacking_money *= effect;
|
||||
break;
|
||||
case opponents.Tetrads:
|
||||
case GoOpponent.Tetrads:
|
||||
mults.strength *= effect;
|
||||
mults.dexterity *= effect;
|
||||
mults.agility *= effect;
|
||||
break;
|
||||
case opponents.Daedalus:
|
||||
case GoOpponent.Daedalus:
|
||||
mults.company_rep *= effect;
|
||||
mults.faction_rep *= effect;
|
||||
break;
|
||||
case opponents.Illuminati:
|
||||
case GoOpponent.Illuminati:
|
||||
mults.hacking_speed *= effect;
|
||||
break;
|
||||
case opponents.w0r1d_d43m0n:
|
||||
case GoOpponent.w0r1d_d43m0n:
|
||||
mults.hacking *= effect;
|
||||
break;
|
||||
}
|
||||
@ -94,27 +93,19 @@ function calculateMults(): Multipliers {
|
||||
return mults;
|
||||
}
|
||||
|
||||
export function resetGoNodePower(player: PlayerObject) {
|
||||
opponentList.forEach((opponent) => {
|
||||
player.go.status[opponent].nodePower = 0;
|
||||
player.go.status[opponent].nodes = 0;
|
||||
player.go.status[opponent].winStreak = 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function playerHasDiscoveredGo() {
|
||||
const playedGame = Player.go.boardState.history.length || Player.go.previousGameFinalBoardState?.history?.length;
|
||||
const hasRecords = opponentList.find((opponent) => getPlayerStats(opponent).wins + getPlayerStats(opponent).losses);
|
||||
const playedGame = Go.currentGame.previousBoard;
|
||||
const hasRecords = getRecordValues(Go.stats).some((stats) => stats.wins + stats.losses);
|
||||
const isInBn14 = Player.bitNodeN === 14;
|
||||
|
||||
return !!(playedGame || hasRecords || isInBn14);
|
||||
}
|
||||
|
||||
function getEffectPowerForFaction(opponent: opponents) {
|
||||
function getEffectPowerForFaction(opponent: GoOpponent) {
|
||||
return opponentDetails[opponent].bonusPower;
|
||||
}
|
||||
|
||||
export function getEffectTypeForFaction(opponent: opponents) {
|
||||
export function getEffectTypeForFaction(opponent: GoOpponent) {
|
||||
return opponentDetails[opponent].bonusDescription;
|
||||
}
|
||||
|
||||
@ -132,6 +123,6 @@ export function getWinstreakMultiplier(winStreak: number, previousWinStreak: num
|
||||
}
|
||||
|
||||
export function getDifficultyMultiplier(komi: number, boardSize: number) {
|
||||
const isTinyBoardVsIlluminati = boardSize === 5 && komi === opponentDetails[opponents.Illuminati].komi;
|
||||
const isTinyBoardVsIlluminati = boardSize === 5 && komi === opponentDetails[GoOpponent.Illuminati].komi;
|
||||
return isTinyBoardVsIlluminati ? 8 : (komi + 0.5) * 0.25;
|
||||
}
|
||||
|
@ -1,15 +1,11 @@
|
||||
import { BoardState, opponentList, Play, playerColors, playTypes, validityReason } from "../boardState/goConstants";
|
||||
import { getMove, sleep } from "../boardAnalysis/goAI";
|
||||
import type { BoardState, Play } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import {
|
||||
getNewBoardState,
|
||||
getStateCopy,
|
||||
makeMove,
|
||||
passTurn,
|
||||
updateCaptures,
|
||||
updateChains,
|
||||
} from "../boardState/boardState";
|
||||
import { evaluateIfMoveIsValid, getControlledSpace, getSimplifiedBoardState } from "../boardAnalysis/boardAnalysis";
|
||||
import { GoColor, GoPlayType, GoValidity, GoOpponent } from "@enums";
|
||||
import { Go, GoEvents } from "../Go";
|
||||
import { getMove, sleep } from "../boardAnalysis/goAI";
|
||||
import { getNewBoardState, makeMove, passTurn, updateCaptures, updateChains } from "../boardState/boardState";
|
||||
import { evaluateIfMoveIsValid, getControlledSpace, simpleBoardFromBoard } from "../boardAnalysis/boardAnalysis";
|
||||
import { getScore, resetWinstreak } from "../boardAnalysis/scoring";
|
||||
import { WorkerScript } from "../../Netscript/WorkerScript";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
@ -18,41 +14,41 @@ import { WHRNG } from "../../Casino/RNG";
|
||||
* 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(Player.go.boardState, playerColors.black);
|
||||
if (Player.go.boardState.previousPlayer === null) {
|
||||
passTurn(Go.currentGame, GoColor.black);
|
||||
if (Go.currentGame.previousPlayer === null) {
|
||||
logEndGame(logger);
|
||||
return Promise.resolve({
|
||||
type: playTypes.gameOver,
|
||||
type: GoPlayType.gameOver,
|
||||
x: -1,
|
||||
y: -1,
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
return getAIMove(logger, Player.go.boardState);
|
||||
return getAIMove(logger, Go.currentGame);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and applies the player's router placement
|
||||
*/
|
||||
export async function makePlayerMove(logger: (s: string) => void, x: number, y: number) {
|
||||
const validity = evaluateIfMoveIsValid(Player.go.boardState, x, y, playerColors.black);
|
||||
const result = makeMove(Player.go.boardState, x, y, playerColors.black);
|
||||
const boardState = Go.currentGame;
|
||||
const validity = evaluateIfMoveIsValid(boardState, x, y, GoColor.black);
|
||||
const moveWasMade = makeMove(boardState, x, y, GoColor.black);
|
||||
|
||||
if (validity !== validityReason.valid || !result) {
|
||||
if (validity !== GoValidity.valid || !moveWasMade) {
|
||||
await sleep(500);
|
||||
logger(`ERROR: Invalid move: ${validity}`);
|
||||
|
||||
if (validity === validityReason.notYourTurn) {
|
||||
if (validity === GoValidity.notYourTurn) {
|
||||
logger("Do you have multiple scripts running, or did you forget to await makeMove() ?");
|
||||
}
|
||||
|
||||
return Promise.resolve(invalidMoveResponse);
|
||||
}
|
||||
|
||||
GoEvents.emit();
|
||||
logger(`Go move played: ${x}, ${y}`);
|
||||
|
||||
const playerUpdatedBoard = getStateCopy(result);
|
||||
const response = getAIMove(logger, playerUpdatedBoard);
|
||||
const response = getAIMove(logger, boardState);
|
||||
await sleep(300);
|
||||
return response;
|
||||
}
|
||||
@ -66,30 +62,27 @@ async function getAIMove(logger: (s: string) => void, boardState: BoardState, su
|
||||
resolve = res;
|
||||
});
|
||||
|
||||
getMove(boardState, playerColors.white, Player.go.boardState.ai).then(async (result) => {
|
||||
getMove(boardState, GoColor.white, Go.currentGame.ai).then(async (result) => {
|
||||
// If a new game has started while this async code ran, drop it
|
||||
if (boardState.history.length > Player.go.boardState.history.length) {
|
||||
if (boardState !== Go.currentGame) {
|
||||
return resolve({ ...result, success: false });
|
||||
}
|
||||
if (result.type === "gameOver") {
|
||||
logEndGame(logger);
|
||||
}
|
||||
if (result.type !== playTypes.move) {
|
||||
Player.go.boardState = boardState;
|
||||
if (result.type !== GoPlayType.move) {
|
||||
return resolve({ ...result, success });
|
||||
}
|
||||
|
||||
const aiUpdatedBoard = makeMove(boardState, result.x, result.y, playerColors.white);
|
||||
await sleep(400);
|
||||
const aiUpdatedBoard = makeMove(boardState, result.x, result.y, GoColor.white);
|
||||
if (!aiUpdatedBoard) {
|
||||
boardState.previousPlayer = playerColors.white;
|
||||
Player.go.boardState = boardState;
|
||||
boardState.previousPlayer = GoColor.white;
|
||||
logger(`Invalid AI move attempted: ${result.x}, ${result.y}. This should not happen.`);
|
||||
} else {
|
||||
Player.go.boardState = aiUpdatedBoard;
|
||||
logger(`Opponent played move: ${result.x}, ${result.y}`);
|
||||
}
|
||||
|
||||
await sleep(400);
|
||||
GoEvents.emit();
|
||||
resolve({ ...result, success });
|
||||
});
|
||||
return aiMoveResult;
|
||||
@ -99,11 +92,11 @@ async function getAIMove(logger: (s: string) => void, boardState: BoardState, su
|
||||
* Returns a grid of booleans indicating if the coordinates at that location are a valid move for the player (black pieces)
|
||||
*/
|
||||
export function getValidMoves() {
|
||||
const boardState = Player.go.boardState;
|
||||
const boardState = Go.currentGame;
|
||||
// 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, playerColors.black) === validityReason.valid;
|
||||
const isValid = evaluateIfMoveIsValid(boardState, x, y, GoColor.black) === GoValidity.valid;
|
||||
validityArray.push(isValid);
|
||||
return validityArray;
|
||||
}, []),
|
||||
@ -116,7 +109,7 @@ export function getValidMoves() {
|
||||
export function getChains() {
|
||||
const chains: string[] = [];
|
||||
// Turn the internal chain IDs into nice consecutive numbers for display to the player
|
||||
return Player.go.boardState.board.map((column) =>
|
||||
return Go.currentGame.board.map((column) =>
|
||||
column.reduce((chainIdArray: (number | null)[], point) => {
|
||||
if (!point) {
|
||||
chainIdArray.push(null);
|
||||
@ -135,7 +128,7 @@ export function getChains() {
|
||||
* Returns a grid of numbers representing the number of open-node connections each player-owned chain has.
|
||||
*/
|
||||
export function getLiberties() {
|
||||
return Player.go.boardState.board.map((column) =>
|
||||
return Go.currentGame.board.map((column) =>
|
||||
column.reduce((libertyArray: number[], point) => {
|
||||
libertyArray.push(point?.liberties?.length || -1);
|
||||
return libertyArray;
|
||||
@ -147,20 +140,20 @@ export function getLiberties() {
|
||||
* Returns a grid indicating which player, if any, controls the empty nodes by fully encircling it with their routers
|
||||
*/
|
||||
export function getControlledEmptyNodes() {
|
||||
const boardState = Player.go.boardState;
|
||||
const controlled = getControlledSpace(boardState);
|
||||
const board = Go.currentGame.board;
|
||||
const controlled = getControlledSpace(board);
|
||||
return controlled.map((column, x: number) =>
|
||||
column.reduce((ownedPoints: string, owner: playerColors, y: number) => {
|
||||
if (owner === playerColors.white) {
|
||||
column.reduce((ownedPoints: string, owner: GoColor, y: number) => {
|
||||
if (owner === GoColor.white) {
|
||||
return ownedPoints + "O";
|
||||
}
|
||||
if (owner === playerColors.black) {
|
||||
if (owner === GoColor.black) {
|
||||
return ownedPoints + "X";
|
||||
}
|
||||
if (!boardState.board[x][y]) {
|
||||
if (!board[x][y]) {
|
||||
return ownedPoints + "#";
|
||||
}
|
||||
if (boardState.board[x][y]?.player === playerColors.empty) {
|
||||
if (board[x][y]?.color === GoColor.empty) {
|
||||
return ownedPoints + "?";
|
||||
}
|
||||
return ownedPoints + ".";
|
||||
@ -172,37 +165,30 @@ export function getControlledEmptyNodes() {
|
||||
* Handle post-game logging
|
||||
*/
|
||||
function logEndGame(logger: (s: string) => void) {
|
||||
const boardState = Player.go.boardState;
|
||||
const boardState = Go.currentGame;
|
||||
const score = getScore(boardState);
|
||||
logger(
|
||||
`Subnet complete! Final score: ${boardState.ai}: ${score[playerColors.white].sum}, Player: ${
|
||||
score[playerColors.black].sum
|
||||
}`,
|
||||
`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(error: (s: string) => void, opponentString: string, boardSize: number) {
|
||||
const opponent = opponentList.find((faction) => faction === opponentString);
|
||||
|
||||
export function resetBoardState(error: (s: string) => void, opponent: GoOpponent, boardSize: number) {
|
||||
if (![5, 7, 9, 13].includes(boardSize)) {
|
||||
error(`Invalid subnet size requested (${boardSize}, size must be 5, 7, 9, or 13`);
|
||||
return;
|
||||
}
|
||||
if (!opponent) {
|
||||
error(`Invalid opponent requested (${opponentString}), valid options are ${opponentList.join(", ")}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const oldBoardState = Player.go.boardState;
|
||||
if (oldBoardState.previousPlayer !== null && oldBoardState.history.length) {
|
||||
const oldBoardState = Go.currentGame;
|
||||
if (oldBoardState.previousPlayer !== null && oldBoardState.previousBoard) {
|
||||
resetWinstreak(oldBoardState.ai, false);
|
||||
}
|
||||
|
||||
Player.go.boardState = getNewBoardState(boardSize, opponent, true);
|
||||
return getSimplifiedBoardState(Player.go.boardState.board);
|
||||
Go.currentGame = getNewBoardState(boardSize, opponent, true);
|
||||
GoEvents.emit(); // Trigger a Go UI rerender
|
||||
return simpleBoardFromBoard(Go.currentGame.board);
|
||||
}
|
||||
|
||||
/** Validate singularity access by throwing an error if the player does not have access. */
|
||||
@ -219,7 +205,7 @@ export function checkCheatApiAccess(error: (s: string) => void): void {
|
||||
|
||||
export const invalidMoveResponse: Play = {
|
||||
success: false,
|
||||
type: playTypes.invalid,
|
||||
type: GoPlayType.invalid,
|
||||
x: -1,
|
||||
y: -1,
|
||||
};
|
||||
@ -235,12 +221,13 @@ export async function determineCheatSuccess(
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
): Promise<Play> {
|
||||
const state = Player.go.boardState;
|
||||
const state = Go.currentGame;
|
||||
const rng = new WHRNG(Player.totalPlaytime);
|
||||
// If cheat is successful, run callback
|
||||
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount)) {
|
||||
callback();
|
||||
state.cheatCount++;
|
||||
GoEvents.emit();
|
||||
return getAIMove(logger, state, true);
|
||||
}
|
||||
// If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly losing
|
||||
@ -248,7 +235,7 @@ export async function determineCheatSuccess(
|
||||
logger(`Cheat failed! You have been ejected from the subnet.`);
|
||||
resetBoardState(logger, state.ai, state.board[0].length);
|
||||
return {
|
||||
type: playTypes.gameOver,
|
||||
type: GoPlayType.gameOver,
|
||||
x: -1,
|
||||
y: -1,
|
||||
success: false,
|
||||
@ -257,7 +244,7 @@ export async function determineCheatSuccess(
|
||||
// If the cheat fails, your turn is skipped
|
||||
else {
|
||||
logger(`Cheat failed. Your turn has been skipped.`);
|
||||
passTurn(state, playerColors.black, false);
|
||||
passTurn(state, GoColor.black, false);
|
||||
state.cheatCount++;
|
||||
return getAIMove(logger, state, false);
|
||||
}
|
||||
@ -287,21 +274,21 @@ export function cheatRemoveRouter(
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
) {
|
||||
const point = Player.go.boardState.board[x][y];
|
||||
const point = Go.currentGame.board[x][y];
|
||||
if (!point) {
|
||||
logger(`The node ${x},${y} is offline, so you cannot clear this point with removeRouter().`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
if (point.player === playerColors.empty) {
|
||||
if (point.color === GoColor.empty) {
|
||||
logger(`The point ${x},${y} does not have a router on it, so you cannot clear this point with removeRouter().`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
point.player = playerColors.empty;
|
||||
Player.go.boardState = updateChains(Player.go.boardState);
|
||||
Player.go.boardState.previousPlayer = playerColors.black;
|
||||
point.color = GoColor.empty;
|
||||
updateChains(Go.currentGame.board);
|
||||
Go.currentGame.previousPlayer = GoColor.black;
|
||||
logger(`Cheat successful. The point ${x},${y} was cleared.`);
|
||||
},
|
||||
successRngOverride,
|
||||
@ -321,21 +308,21 @@ export function cheatPlayTwoMoves(
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
) {
|
||||
const point1 = Player.go.boardState.board[x1][y1];
|
||||
const point1 = Go.currentGame.board[x1][y1];
|
||||
if (!point1) {
|
||||
logger(`The node ${x1},${y1} is offline, so you cannot place a router there.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
if (point1.player !== playerColors.empty) {
|
||||
if (point1.color !== GoColor.empty) {
|
||||
logger(`The point ${x1},${y1} is not empty, so you cannot place a router there.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
const point2 = Player.go.boardState.board[x2][y2];
|
||||
const point2 = Go.currentGame.board[x2][y2];
|
||||
if (!point2) {
|
||||
logger(`The node ${x2},${y2} is offline, so you cannot place a router there.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
if (point2.player !== playerColors.empty) {
|
||||
if (point2.color !== GoColor.empty) {
|
||||
logger(`The point ${x2},${y2} is not empty, so you cannot place a router there.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
@ -343,10 +330,10 @@ export function cheatPlayTwoMoves(
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
point1.player = playerColors.black;
|
||||
point2.player = playerColors.black;
|
||||
Player.go.boardState = updateCaptures(Player.go.boardState, playerColors.black);
|
||||
Player.go.boardState.previousPlayer = playerColors.black;
|
||||
point1.color = GoColor.black;
|
||||
point2.color = GoColor.black;
|
||||
updateCaptures(Go.currentGame.board, GoColor.black);
|
||||
Go.currentGame.previousPlayer = GoColor.black;
|
||||
|
||||
logger(`Cheat successful. Two go moves played: ${x1},${y1} and ${x2},${y2}`);
|
||||
},
|
||||
@ -362,7 +349,7 @@ export function cheatRepairOfflineNode(
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
) {
|
||||
const point = Player.go.boardState.board[x][y];
|
||||
const point = Go.currentGame.board[x][y];
|
||||
if (point) {
|
||||
logger(`The node ${x},${y} is not offline, so you cannot repair the node.`);
|
||||
return invalidMoveResponse;
|
||||
@ -371,15 +358,15 @@ export function cheatRepairOfflineNode(
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
Player.go.boardState.board[x][y] = {
|
||||
Go.currentGame.board[x][y] = {
|
||||
chain: "",
|
||||
liberties: null,
|
||||
y,
|
||||
player: playerColors.empty,
|
||||
color: GoColor.empty,
|
||||
x,
|
||||
};
|
||||
Player.go.boardState = updateChains(Player.go.boardState);
|
||||
Player.go.boardState.previousPlayer = playerColors.black;
|
||||
updateChains(Go.currentGame.board);
|
||||
Go.currentGame.previousPlayer = GoColor.black;
|
||||
logger(`Cheat successful. The point ${x},${y} was repaired.`);
|
||||
},
|
||||
successRngOverride,
|
||||
@ -394,12 +381,12 @@ export function cheatDestroyNode(
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
) {
|
||||
const point = Player.go.boardState.board[x][y];
|
||||
const point = Go.currentGame.board[x][y];
|
||||
if (!point) {
|
||||
logger(`The node ${x},${y} is already offline, so you cannot destroy the node.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
if (point.player !== playerColors.empty) {
|
||||
if (point.color !== GoColor.empty) {
|
||||
logger(`The point ${x},${y} is not empty, so you cannot destroy this node.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
@ -407,10 +394,10 @@ export function cheatDestroyNode(
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
Player.go.boardState.board[x][y] = null;
|
||||
Player.go.boardState = updateChains(Player.go.boardState);
|
||||
Player.go.boardState.previousPlayer = playerColors.black;
|
||||
logger(`Cheat successful. The point ${x},${y} was repaired.`);
|
||||
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,
|
||||
|
@ -1,33 +1,26 @@
|
||||
import React, { useMemo } from "react";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import type { BoardState } from "../Types";
|
||||
|
||||
import React from "react";
|
||||
import { Grid } from "@mui/material";
|
||||
|
||||
import { GoOpponent, GoColor } from "@enums";
|
||||
import { getSizeClass, GoPoint } from "./GoPoint";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { getAllValidMoves, getControlledSpace } from "../boardAnalysis/boardAnalysis";
|
||||
import { BoardState, opponents, playerColors } from "../boardState/goConstants";
|
||||
|
||||
interface IProps {
|
||||
interface GoGameboardProps {
|
||||
boardState: BoardState;
|
||||
traditional: boolean;
|
||||
clickHandler: (x: number, y: number) => any;
|
||||
hover: boolean;
|
||||
}
|
||||
|
||||
export function GoGameboard({ boardState, traditional, clickHandler, hover }: IProps): React.ReactElement {
|
||||
useRerender(400);
|
||||
|
||||
export function GoGameboard({ boardState, traditional, clickHandler, hover }: GoGameboardProps): React.ReactElement {
|
||||
const currentPlayer =
|
||||
boardState.ai !== opponents.none || boardState.previousPlayer === playerColors.white
|
||||
? playerColors.black
|
||||
: playerColors.white;
|
||||
boardState.ai !== GoOpponent.none || boardState.previousPlayer === GoColor.white ? GoColor.black : GoColor.white;
|
||||
|
||||
const availablePoints = useMemo(
|
||||
() => (hover ? getAllValidMoves(boardState, currentPlayer) : []),
|
||||
[boardState, hover, currentPlayer],
|
||||
);
|
||||
|
||||
const ownedEmptyNodes = useMemo(() => getControlledSpace(boardState), [boardState]);
|
||||
const availablePoints = hover ? getAllValidMoves(boardState, currentPlayer) : [];
|
||||
const ownedEmptyNodes = getControlledSpace(boardState.board);
|
||||
|
||||
function pointIsValid(x: number, y: number) {
|
||||
return !!availablePoints.find((point) => point.x === x && point.y === y);
|
||||
@ -37,13 +30,12 @@ export function GoGameboard({ boardState, traditional, clickHandler, hover }: IP
|
||||
const classes = boardStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container id="goGameboard" className={`${classes.board} ${traditional ? classes.traditional : ""}`}>
|
||||
{boardState.board.map((row, y) => {
|
||||
{boardState.board.map((column, y) => {
|
||||
const yIndex = boardState.board[0].length - y - 1;
|
||||
return (
|
||||
<Grid container key={`column_${yIndex}`} item className={getSizeClass(boardSize, classes)}>
|
||||
{row.map((point, x: number) => {
|
||||
{column.map((point, x: number) => {
|
||||
const xIndex = x;
|
||||
return (
|
||||
<Grid
|
||||
@ -68,6 +60,5 @@ export function GoGameboard({ boardState, traditional, clickHandler, hover }: IP
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,25 +1,26 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { SnackbarEvents } from "../../ui/React/Snackbar";
|
||||
import { ToastVariant } from "@enums";
|
||||
import type { BoardState } from "../Types";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Box, Button, Typography } from "@mui/material";
|
||||
|
||||
import { BoardState, opponents, playerColors, playTypes, validityReason } from "../boardState/goConstants";
|
||||
import { getNewBoardState, getStateCopy, makeMove, passTurn } from "../boardState/boardState";
|
||||
import { GoOpponent, GoColor, GoPlayType, GoValidity, ToastVariant } from "@enums";
|
||||
import { Go, GoEvents } from "../Go";
|
||||
import { SnackbarEvents } from "../../ui/React/Snackbar";
|
||||
import { getNewBoardState, getStateCopy, makeMove, passTurn, updateCaptures } from "../boardState/boardState";
|
||||
import { getMove } from "../boardAnalysis/goAI";
|
||||
import { bitverseArt, weiArt } from "../boardState/asciiArt";
|
||||
import { getScore, resetWinstreak } from "../boardAnalysis/scoring";
|
||||
import { evaluateIfMoveIsValid, getAllValidMoves } from "../boardAnalysis/boardAnalysis";
|
||||
import { evaluateIfMoveIsValid, getAllValidMoves, boardFromSimpleBoard } from "../boardAnalysis/boardAnalysis";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { OptionSwitch } from "../../ui/React/OptionSwitch";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { Player } from "@player";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { GoScoreModal } from "./GoScoreModal";
|
||||
import { GoGameboard } from "./GoGameboard";
|
||||
import { GoSubnetSearch } from "./GoSubnetSearch";
|
||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||
|
||||
interface IProps {
|
||||
interface GoGameboardWrapperProps {
|
||||
showInstructions: () => void;
|
||||
}
|
||||
|
||||
@ -32,30 +33,37 @@ interface IProps {
|
||||
* play two moves that don't capture
|
||||
*/
|
||||
|
||||
export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactElement {
|
||||
const rerender = useRerender(400);
|
||||
export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps): React.ReactElement {
|
||||
const rerender = useRerender();
|
||||
useEffect(() => {
|
||||
const unsubscribe = GoEvents.subscribe(rerender);
|
||||
return unsubscribe;
|
||||
}, [rerender]);
|
||||
|
||||
const boardState = Player.go.boardState;
|
||||
const boardState = Go.currentGame;
|
||||
// Destructure boardState to allow useMemo to trigger correctly
|
||||
const traditional = Settings.GoTraditionalStyle;
|
||||
const [showPriorMove, setShowPriorMove] = useState(false);
|
||||
const [opponent, setOpponent] = useState<opponents>(boardState.ai);
|
||||
const [opponent, setOpponent] = useState<GoOpponent>(boardState.ai);
|
||||
const [scoreOpen, setScoreOpen] = useState(false);
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [waitingOnAI, setWaitingOnAI] = useState(false);
|
||||
|
||||
const classes = boardStyles();
|
||||
const boardSize = boardState.board[0].length;
|
||||
const currentPlayer = boardState.previousPlayer === playerColors.white ? playerColors.black : playerColors.white;
|
||||
const currentPlayer = boardState.previousPlayer === GoColor.white ? GoColor.black : GoColor.white;
|
||||
const score = getScore(boardState);
|
||||
|
||||
// Only run this once on first component mount, to handle scenarios where the game was saved or closed while waiting on the AI to make a move
|
||||
useEffect(() => {
|
||||
if (boardState.previousPlayer === playerColors.black && !waitingOnAI) {
|
||||
takeAiTurn(Player.go.boardState);
|
||||
if (boardState.previousPlayer === GoColor.black && !waitingOnAI) {
|
||||
takeAiTurn(Go.currentGame);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Do not implement useCallback for this function without ensuring GoGameboard still rerenders for every move
|
||||
// Currently this function changing is what triggers a GoGameboard rerender, which is needed
|
||||
async function clickHandler(x: number, y: number) {
|
||||
if (showPriorMove) {
|
||||
SnackbarEvents.emit(
|
||||
@ -68,7 +76,7 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
||||
|
||||
// Lock the board when it isn't the player's turn
|
||||
const gameOver = boardState.previousPlayer === null;
|
||||
const notYourTurn = boardState.previousPlayer === playerColors.black && opponent !== opponents.none;
|
||||
const notYourTurn = boardState.previousPlayer === GoColor.black && opponent !== GoOpponent.none;
|
||||
if (notYourTurn) {
|
||||
SnackbarEvents.emit(`It is not your turn to play.`, ToastVariant.WARNING, 2000);
|
||||
return;
|
||||
@ -79,65 +87,54 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
||||
}
|
||||
|
||||
const validity = evaluateIfMoveIsValid(boardState, x, y, currentPlayer);
|
||||
if (validity != validityReason.valid) {
|
||||
if (validity != GoValidity.valid) {
|
||||
SnackbarEvents.emit(`Invalid move: ${validity}`, ToastVariant.ERROR, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedBoard = makeMove(boardState, x, y, currentPlayer);
|
||||
if (updatedBoard) {
|
||||
updateBoard(updatedBoard);
|
||||
opponent !== opponents.none && takeAiTurn(updatedBoard);
|
||||
const didUpdateBoard = makeMove(boardState, x, y, currentPlayer);
|
||||
if (didUpdateBoard) {
|
||||
rerender();
|
||||
opponent !== GoOpponent.none && takeAiTurn(boardState);
|
||||
}
|
||||
}
|
||||
|
||||
function passPlayerTurn() {
|
||||
if (boardState.previousPlayer === playerColors.white) {
|
||||
passTurn(boardState, playerColors.black);
|
||||
updateBoard(boardState);
|
||||
if (boardState.previousPlayer === GoColor.white) {
|
||||
passTurn(boardState, GoColor.black);
|
||||
rerender();
|
||||
}
|
||||
if (boardState.previousPlayer === null) {
|
||||
endGame();
|
||||
setScoreOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
opponent !== opponents.none && takeAiTurn(boardState);
|
||||
opponent !== GoOpponent.none && takeAiTurn(boardState);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async function takeAiTurn(board: BoardState) {
|
||||
if (board.previousPlayer === null) {
|
||||
return;
|
||||
}
|
||||
async function takeAiTurn(boardState: BoardState) {
|
||||
setWaitingOnAI(true);
|
||||
const initialState = getStateCopy(board);
|
||||
const move = await getMove(initialState, playerColors.white, opponent);
|
||||
const move = await getMove(boardState, GoColor.white, opponent);
|
||||
|
||||
// If a new game has started while this async code ran, just drop it
|
||||
if (boardState.history.length > Player.go.boardState.history.length) {
|
||||
return;
|
||||
}
|
||||
if (boardState !== Go.currentGame) return;
|
||||
|
||||
if (move.type === playTypes.pass) {
|
||||
if (move.type === GoPlayType.pass) {
|
||||
SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000);
|
||||
updateBoard(initialState);
|
||||
rerender();
|
||||
return;
|
||||
}
|
||||
|
||||
if (move.type === playTypes.gameOver || move.x === null || move.y === null) {
|
||||
endGame(initialState);
|
||||
if (move.type === GoPlayType.gameOver || move.x === null || move.y === null) {
|
||||
setScoreOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedBoard = await makeMove(initialState, move.x, move.y, playerColors.white);
|
||||
const didUpdateBoard = makeMove(boardState, move.x, move.y, GoColor.white);
|
||||
|
||||
if (updatedBoard) {
|
||||
setTimeout(() => {
|
||||
updateBoard(updatedBoard);
|
||||
setWaitingOnAI(false);
|
||||
}, 500);
|
||||
}
|
||||
if (didUpdateBoard) setWaitingOnAI(false);
|
||||
}
|
||||
|
||||
function newSubnet() {
|
||||
@ -149,39 +146,25 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
||||
setScoreOpen(false);
|
||||
setSearchOpen(false);
|
||||
setOpponent(newOpponent);
|
||||
if (boardState.previousPlayer !== null && boardState.history.length) {
|
||||
if (boardState.previousPlayer !== null && boardState.previousBoard) {
|
||||
resetWinstreak(boardState.ai, false);
|
||||
}
|
||||
|
||||
const newBoardState = getNewBoardState(newBoardSize, newOpponent, false);
|
||||
updateBoard(newBoardState);
|
||||
}
|
||||
|
||||
function updateBoard(initialBoardState: BoardState) {
|
||||
Player.go.boardState = getStateCopy(initialBoardState);
|
||||
Go.currentGame = getNewBoardState(newBoardSize, newOpponent, false);
|
||||
rerender();
|
||||
}
|
||||
|
||||
function endGame(state = boardState) {
|
||||
setScoreOpen(true);
|
||||
updateBoard(state);
|
||||
}
|
||||
|
||||
function getPriorMove() {
|
||||
if (!boardState.history.length) {
|
||||
return boardState;
|
||||
}
|
||||
const priorBoard = boardState.history.slice(-1)[0];
|
||||
const updatedState = getStateCopy(boardState);
|
||||
updatedState.board = priorBoard;
|
||||
updatedState.previousPlayer =
|
||||
boardState.previousPlayer === playerColors.black ? playerColors.white : playerColors.black;
|
||||
|
||||
return updatedState;
|
||||
if (!boardState.previousBoard) return boardState;
|
||||
const priorState = getStateCopy(boardState);
|
||||
priorState.previousPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
|
||||
priorState.board = boardFromSimpleBoard(boardState.previousBoard);
|
||||
updateCaptures(priorState.board, priorState.previousPlayer);
|
||||
return priorState;
|
||||
}
|
||||
|
||||
function showPreviousMove(newValue: boolean) {
|
||||
if (boardState.history.length) {
|
||||
if (boardState.previousBoard) {
|
||||
setShowPriorMove(newValue);
|
||||
}
|
||||
}
|
||||
@ -190,16 +173,13 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
||||
Settings.GoTraditionalStyle = newValue;
|
||||
}
|
||||
|
||||
const endGameAvailable = boardState.previousPlayer === playerColors.white && boardState.passCount;
|
||||
const noLegalMoves = useMemo(
|
||||
() => boardState.previousPlayer === playerColors.white && !getAllValidMoves(boardState, playerColors.black).length,
|
||||
[boardState],
|
||||
);
|
||||
const disablePassButton =
|
||||
opponent !== opponents.none && boardState.previousPlayer === playerColors.black && waitingOnAI;
|
||||
const endGameAvailable = boardState.previousPlayer === GoColor.white && boardState.passCount;
|
||||
const noLegalMoves =
|
||||
boardState.previousPlayer === GoColor.white && !getAllValidMoves(boardState, GoColor.black).length;
|
||||
const disablePassButton = opponent !== GoOpponent.none && boardState.previousPlayer === GoColor.black && waitingOnAI;
|
||||
|
||||
const scoreBoxText = boardState.history.length
|
||||
? `Score: Black: ${score[playerColors.black].sum} White: ${score[playerColors.white].sum}`
|
||||
const scoreBoxText = boardState.previousBoard
|
||||
? `Score: Black: ${score[GoColor.black].sum} White: ${score[GoColor.white].sum}`
|
||||
: "Place a router to begin!";
|
||||
|
||||
const getPassButtonLabel = () => {
|
||||
@ -209,11 +189,11 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
||||
if (boardState.previousPlayer === null) {
|
||||
return "View Final Score";
|
||||
}
|
||||
if (boardState.previousPlayer === playerColors.black && waitingOnAI) {
|
||||
if (boardState.previousPlayer === GoColor.black && waitingOnAI) {
|
||||
return "Waiting for opponent";
|
||||
}
|
||||
const currentPlayer = boardState.previousPlayer === playerColors.black ? playerColors.white : playerColors.black;
|
||||
return `Pass Turn${boardState.ai === opponents.none ? ` (${currentPlayer})` : ""}`;
|
||||
const currentPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
|
||||
return `Pass Turn${boardState.ai === GoOpponent.none ? ` (${currentPlayer})` : ""}`;
|
||||
};
|
||||
|
||||
return (
|
||||
@ -242,8 +222,8 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||
<br />
|
||||
<Typography variant={"h6"} className={classes.opponentLabel}>
|
||||
{opponent !== opponents.none ? "Subnet owner: " : ""}{" "}
|
||||
{opponent === opponents.w0r1d_d43m0n ? <CorruptableText content={opponent} spoiler={false} /> : opponent}
|
||||
{opponent !== GoOpponent.none ? "Subnet owner: " : ""}{" "}
|
||||
{opponent === GoOpponent.w0r1d_d43m0n ? <CorruptableText content={opponent} spoiler={false} /> : opponent}
|
||||
</Typography>
|
||||
<br />
|
||||
</Box>
|
||||
@ -279,7 +259,7 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
||||
/>
|
||||
<OptionSwitch
|
||||
checked={showPriorMove}
|
||||
disabled={!boardState.history.length}
|
||||
disabled={!boardState.previousBoard}
|
||||
onChange={(newValue) => showPreviousMove(newValue)}
|
||||
text="Show previous move"
|
||||
tooltip={<>Show the board as it was before the last move</>}
|
||||
|
@ -1,10 +1,9 @@
|
||||
import React from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Grid, Table, TableBody, TableCell, TableRow, Tooltip } from "@mui/material";
|
||||
import { Grid, Table, TableBody, TableCell, TableRow, Tooltip, Typography } from "@mui/material";
|
||||
|
||||
import { opponentList, opponents } from "../boardState/goConstants";
|
||||
import { getPlayerStats, getScore } from "../boardAnalysis/scoring";
|
||||
import { Player } from "@player";
|
||||
import { GoOpponent } from "@enums";
|
||||
import { Go } from "../Go";
|
||||
import { getOpponentStats, getScore } from "../boardAnalysis/scoring";
|
||||
import { GoGameboard } from "./GoGameboard";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
@ -13,15 +12,15 @@ import { formatNumber } from "../../ui/formatNumber";
|
||||
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
|
||||
import { getNewBoardState } from "../boardState/boardState";
|
||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||
import { showWorldDemon } from "../boardAnalysis/goAI";
|
||||
import { getRecordKeys } from "../../Types/Record";
|
||||
|
||||
export const GoHistoryPage = (): React.ReactElement => {
|
||||
useRerender(400);
|
||||
const classes = boardStyles();
|
||||
const priorBoard = Player.go.previousGameFinalBoardState ?? getNewBoardState(7);
|
||||
const priorBoard = Go.previousGame ?? getNewBoardState(7);
|
||||
const score = getScore(priorBoard);
|
||||
const opponent = priorBoard.ai;
|
||||
const opponentsToShow = showWorldDemon() ? [...opponentList, opponents.w0r1d_d43m0n] : opponentList;
|
||||
const opponentsToShow = getRecordKeys(Go.stats);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -48,13 +47,13 @@ export const GoHistoryPage = (): React.ReactElement => {
|
||||
<Typography variant="h5">Faction Stats:</Typography>
|
||||
<Grid container style={{ maxWidth: "1020px" }}>
|
||||
{opponentsToShow.map((faction, index) => {
|
||||
const data = getPlayerStats(faction);
|
||||
const data = getOpponentStats(faction);
|
||||
return (
|
||||
<Grid item key={opponentsToShow[index]} className={classes.factionStatus}>
|
||||
<Typography>
|
||||
{" "}
|
||||
<strong className={classes.keyText}>
|
||||
{faction === opponents.w0r1d_d43m0n ? (
|
||||
{faction === GoOpponent.w0r1d_d43m0n ? (
|
||||
<CorruptableText content="????????????" spoiler={false} />
|
||||
) : (
|
||||
faction
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React from "react";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { Grid, Link, Typography } from "@mui/material";
|
||||
import { getBoardFromSimplifiedBoardState } from "../boardAnalysis/boardAnalysis";
|
||||
import { opponents, playerColors } from "../boardState/goConstants";
|
||||
|
||||
import { GoOpponent, GoColor } from "@enums";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { boardStateFromSimpleBoard } from "../boardAnalysis/boardAnalysis";
|
||||
import { GoTutorialChallenge } from "./GoTutorialChallenge";
|
||||
import { Router } from "../../ui/GameRoot";
|
||||
import { Page } from "../../ui/Router";
|
||||
@ -10,11 +11,7 @@ import { getMaxFavor } from "../effects/effect";
|
||||
|
||||
const captureChallenge = (
|
||||
<GoTutorialChallenge
|
||||
state={getBoardFromSimplifiedBoardState(
|
||||
[".....", "OX...", "OXX..", "OOX.O", "OOX.."],
|
||||
opponents.none,
|
||||
playerColors.white,
|
||||
)}
|
||||
state={boardStateFromSimpleBoard([".....", "OX...", "OXX..", "OOX.O", "OOX.."], GoOpponent.none, GoColor.white)}
|
||||
description={
|
||||
"CHALLENGE: This white network on the bottom is vulnerable! Click on the board to place a router. Capture some white pieces by cutting off their access to any empty nodes."
|
||||
}
|
||||
@ -28,11 +25,7 @@ const captureChallenge = (
|
||||
|
||||
const saveTheNetworkChallenge = (
|
||||
<GoTutorialChallenge
|
||||
state={getBoardFromSimplifiedBoardState(
|
||||
["OO.##", "XO..#", "XX..#", "XO...", "XO..."],
|
||||
opponents.none,
|
||||
playerColors.white,
|
||||
)}
|
||||
state={boardStateFromSimpleBoard(["OO.##", "XO..#", "XX..#", "XO...", "XO..."], GoOpponent.none, GoColor.white)}
|
||||
description={
|
||||
"CHALLENGE: Your routers are in trouble! They only have one open port. Save the black network by connecting them to more empty nodes."
|
||||
}
|
||||
@ -48,11 +41,7 @@ const saveTheNetworkChallenge = (
|
||||
|
||||
const onlyGoodMoveChallenge = (
|
||||
<GoTutorialChallenge
|
||||
state={getBoardFromSimplifiedBoardState(
|
||||
["XXO.O", "XO.O.", ".OOOO", "XXXXX", "X.X.X"],
|
||||
opponents.none,
|
||||
playerColors.white,
|
||||
)}
|
||||
state={boardStateFromSimpleBoard(["XXO.O", "XO.O.", ".OOOO", "XXXXX", "X.X.X"], GoOpponent.none, GoColor.white)}
|
||||
description={"CHALLENGE: Save the black network on the left! Connect the network to more than one empty node."}
|
||||
correctMoves={[{ x: 2, y: 0 }]}
|
||||
correctText={
|
||||
@ -66,11 +55,7 @@ const onlyGoodMoveChallenge = (
|
||||
|
||||
const makeTwoEyesChallenge = (
|
||||
<GoTutorialChallenge
|
||||
state={getBoardFromSimplifiedBoardState(
|
||||
["XXOO.", ".XXOO", ".XXO.", ".XXOO", "XXOO."],
|
||||
opponents.none,
|
||||
playerColors.white,
|
||||
)}
|
||||
state={boardStateFromSimpleBoard(["XXOO.", ".XXOO", ".XXO.", ".XXOO", "XXOO."], GoOpponent.none, GoColor.white)}
|
||||
description={
|
||||
"CHALLENGE: The black routers are only connected to one empty-node group. Place a router such that they are connected to TWO empty node groups instead."
|
||||
}
|
||||
|
@ -1,31 +1,33 @@
|
||||
import type { BoardState } from "../Types";
|
||||
|
||||
import React from "react";
|
||||
import { ClassNameMap } from "@mui/styles";
|
||||
|
||||
import { BoardState, columnIndexes, playerColors } from "../boardState/goConstants";
|
||||
import { GoColor } from "@enums";
|
||||
import { columnIndexes } from "../Constants";
|
||||
import { findNeighbors } from "../boardState/boardState";
|
||||
import { pointStyle } from "../boardState/goStyles";
|
||||
import { findAdjacentLibertiesAndAlliesForPoint } from "../boardAnalysis/boardAnalysis";
|
||||
import { findAdjacentLibertiesAndAlliesForPoint, getColorOnSimpleBoard } from "../boardAnalysis/boardAnalysis";
|
||||
|
||||
interface IProps {
|
||||
interface GoPointProps {
|
||||
state: BoardState;
|
||||
x: number;
|
||||
y: number;
|
||||
traditional: boolean;
|
||||
hover: boolean;
|
||||
valid: boolean;
|
||||
emptyPointOwner: playerColors;
|
||||
emptyPointOwner: GoColor;
|
||||
}
|
||||
|
||||
export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwner }: IProps): React.ReactElement {
|
||||
export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwner }: GoPointProps): React.ReactElement {
|
||||
const classes = pointStyle();
|
||||
|
||||
const currentPoint = state.board[x]?.[y];
|
||||
const player = currentPoint?.player;
|
||||
const player = currentPoint?.color;
|
||||
|
||||
const isInAtari =
|
||||
currentPoint && currentPoint.liberties?.length === 1 && player !== playerColors.empty && !traditional;
|
||||
const liberties = player !== playerColors.empty ? findAdjacentLibertiesAndAlliesForPoint(state, x, y) : null;
|
||||
const neighbors = findNeighbors(state, x, y);
|
||||
const isInAtari = currentPoint && currentPoint.liberties?.length === 1 && player !== GoColor.empty && !traditional;
|
||||
const liberties = player !== GoColor.empty ? findAdjacentLibertiesAndAlliesForPoint(state.board, x, y) : null;
|
||||
const neighbors = findNeighbors(state.board, x, y);
|
||||
|
||||
const hasNorthLiberty = traditional ? neighbors.north : liberties?.north;
|
||||
const hasEastLiberty = traditional ? neighbors.east : liberties?.east;
|
||||
@ -33,25 +35,19 @@ export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwne
|
||||
const hasWestLiberty = traditional ? neighbors.west : liberties?.west;
|
||||
|
||||
const pointClass =
|
||||
player === playerColors.white
|
||||
? classes.whitePoint
|
||||
: player === playerColors.black
|
||||
? classes.blackPoint
|
||||
: classes.emptyPoint;
|
||||
player === GoColor.white ? classes.whitePoint : player === GoColor.black ? classes.blackPoint : classes.emptyPoint;
|
||||
|
||||
const colorLiberty = `${player === playerColors.white ? classes.libertyWhite : classes.libertyBlack} ${
|
||||
classes.liberty
|
||||
}`;
|
||||
const colorLiberty = `${player === GoColor.white ? classes.libertyWhite : classes.libertyBlack} ${classes.liberty}`;
|
||||
|
||||
const sizeClass = getSizeClass(state.board[0].length, classes);
|
||||
|
||||
const isNewStone = state.history?.[state.history?.length - 1]?.[x]?.[y]?.player === playerColors.empty;
|
||||
const isNewStone = state.previousBoard && getColorOnSimpleBoard(state.previousBoard, x, y) === GoColor.empty;
|
||||
const isPriorMove = player === state.previousPlayer && isNewStone;
|
||||
|
||||
const emptyPointColorClass =
|
||||
emptyPointOwner === playerColors.white
|
||||
emptyPointOwner === GoColor.white
|
||||
? classes.libertyWhite
|
||||
: emptyPointOwner === playerColors.black
|
||||
: emptyPointOwner === GoColor.black
|
||||
? classes.libertyBlack
|
||||
: "";
|
||||
|
||||
@ -70,7 +66,7 @@ export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwne
|
||||
<div className={hasWestLiberty ? `${classes.westLiberty} ${colorLiberty}` : classes.liberty}></div>
|
||||
<div className={`${classes.innerPoint} `}>
|
||||
<div
|
||||
className={`${pointClass} ${player !== playerColors.empty ? classes.filledPoint : emptyPointColorClass}`}
|
||||
className={`${pointClass} ${player !== GoColor.empty ? classes.filledPoint : emptyPointColorClass}`}
|
||||
></div>
|
||||
</div>
|
||||
<div className={`${pointClass} ${classes.tradStone}`} />
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Container, Tab, Tabs } from "@mui/material";
|
||||
import React from "react";
|
||||
import { GoInstructionsPage } from "./ui/GoInstructionsPage";
|
||||
import { Container, Tab, Tabs } from "@mui/material";
|
||||
|
||||
import { GoInstructionsPage } from "./GoInstructionsPage";
|
||||
import { BorderInnerSharp, Help, ManageSearch, History } from "@mui/icons-material";
|
||||
import { GoStatusPage } from "./ui/GoStatusPage";
|
||||
import { GoHistoryPage } from "./ui/GoHistoryPage";
|
||||
import { GoGameboardWrapper } from "./ui/GoGameboardWrapper";
|
||||
import { boardStyles } from "./boardState/goStyles";
|
||||
import { GoStatusPage } from "./GoStatusPage";
|
||||
import { GoHistoryPage } from "./GoHistoryPage";
|
||||
import { GoGameboardWrapper } from "./GoGameboardWrapper";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
|
||||
export function GoRoot(): React.ReactElement {
|
||||
const classes = boardStyles();
|
@ -1,28 +1,29 @@
|
||||
import type { GoScore } from "../Types";
|
||||
import React from "react";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
|
||||
import { GoOpponent, GoColor } from "@enums";
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
import { goScore, opponents, playerColors } from "../boardState/goConstants";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { GoScorePowerSummary } from "./GoScorePowerSummary";
|
||||
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
|
||||
|
||||
interface IProps {
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
finalScore: goScore;
|
||||
finalScore: GoScore;
|
||||
newSubnet: () => void;
|
||||
opponent: opponents;
|
||||
opponent: GoOpponent;
|
||||
}
|
||||
|
||||
export const GoScoreModal = ({ open, onClose, finalScore, newSubnet, opponent }: IProps): React.ReactElement => {
|
||||
export const GoScoreModal = ({ open, onClose, finalScore, newSubnet, opponent }: Props): React.ReactElement => {
|
||||
const classes = boardStyles();
|
||||
|
||||
const blackScore = finalScore[playerColors.black];
|
||||
const whiteScore = finalScore[playerColors.white];
|
||||
const blackScore = finalScore[GoColor.black];
|
||||
const whiteScore = finalScore[GoColor.white];
|
||||
|
||||
const playerWinsText = opponent === opponents.none ? "Black wins!" : "You win!";
|
||||
const opponentWinsText = opponent === opponents.none ? "White wins!" : `Winner: ${opponent}`;
|
||||
const playerWinsText = opponent === GoOpponent.none ? "Black wins!" : "You win!";
|
||||
const opponentWinsText = opponent === GoOpponent.none ? "White wins!" : `Winner: ${opponent}`;
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
@ -37,7 +38,7 @@ export const GoScoreModal = ({ open, onClose, finalScore, newSubnet, opponent }:
|
||||
{blackScore.sum > whiteScore.sum ? playerWinsText : opponentWinsText}
|
||||
</Typography>
|
||||
<br />
|
||||
{opponent !== opponents.none ? (
|
||||
{opponent !== GoOpponent.none ? (
|
||||
<>
|
||||
<GoScorePowerSummary opponent={opponent} finalScore={finalScore} />
|
||||
<br />
|
||||
|
@ -1,32 +1,36 @@
|
||||
import type { GoScore } from "../Types";
|
||||
|
||||
import React from "react";
|
||||
import { Table, TableBody, TableCell, TableRow, Typography, Tooltip } from "@mui/material";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { GoOpponent, GoColor } from "@enums";
|
||||
import { Go } from "../Go";
|
||||
import { getBonusText, getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
|
||||
import { goScore, opponents, playerColors } from "../boardState/goConstants";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { formatNumber } from "../../ui/formatNumber";
|
||||
import { FactionName } from "@enums";
|
||||
import { getPlayerStats } from "../boardAnalysis/scoring";
|
||||
import { getOpponentStats } from "../boardAnalysis/scoring";
|
||||
import { getEnumHelper } from "../../utils/EnumHelper";
|
||||
|
||||
interface IProps {
|
||||
finalScore: goScore;
|
||||
opponent: opponents;
|
||||
interface Props {
|
||||
finalScore: GoScore;
|
||||
opponent: GoOpponent;
|
||||
}
|
||||
|
||||
export const GoScorePowerSummary = ({ finalScore, opponent }: IProps) => {
|
||||
export const GoScorePowerSummary = ({ finalScore, opponent }: Props) => {
|
||||
const classes = boardStyles();
|
||||
const status = getPlayerStats(opponent);
|
||||
const status = getOpponentStats(opponent);
|
||||
const winStreak = status.winStreak;
|
||||
const oldWinStreak = status.winStreak;
|
||||
const nodePower = formatNumber(status.nodePower, 2);
|
||||
const blackScore = finalScore[playerColors.black];
|
||||
const whiteScore = finalScore[playerColors.white];
|
||||
const blackScore = finalScore[GoColor.black];
|
||||
const whiteScore = finalScore[GoColor.white];
|
||||
const faction = getEnumHelper("FactionName").getMember(opponent);
|
||||
|
||||
const difficultyMultiplier = getDifficultyMultiplier(whiteScore.komi, Player.go.boardState.board[0].length);
|
||||
const difficultyMultiplier = getDifficultyMultiplier(whiteScore.komi, Go.currentGame.board[0].length);
|
||||
const winstreakMultiplier = getWinstreakMultiplier(winStreak, oldWinStreak);
|
||||
const nodePowerIncrease = formatNumber(blackScore.sum * difficultyMultiplier * winstreakMultiplier, 2);
|
||||
const showFavorGain =
|
||||
winStreak > 0 && winStreak % 2 === 0 && Player.factions.includes(opponent as unknown as FactionName);
|
||||
const showFavorGain = faction && winStreak > 0 && winStreak % 2 === 0 && Player.factions.includes(faction);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -1,19 +1,22 @@
|
||||
import type { GoScore } from "../Types";
|
||||
|
||||
import React from "react";
|
||||
import { Table, TableBody, TableCell, TableRow, Tooltip } from "@mui/material";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { goScore, opponents, playerColors } from "../boardState/goConstants";
|
||||
|
||||
interface IProps {
|
||||
score: goScore;
|
||||
opponent: opponents;
|
||||
import { GoOpponent, GoColor } from "@enums";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
|
||||
interface GoScoreSummaryTableProps {
|
||||
score: GoScore;
|
||||
opponent: GoOpponent;
|
||||
}
|
||||
|
||||
export const GoScoreSummaryTable = ({ score, opponent }: IProps) => {
|
||||
export const GoScoreSummaryTable = ({ score, opponent }: GoScoreSummaryTableProps) => {
|
||||
const classes = boardStyles();
|
||||
const blackScore = score[playerColors.black];
|
||||
const whiteScore = score[playerColors.white];
|
||||
const blackPlayerName = opponent === opponents.none ? "Black" : "You";
|
||||
const whitePlayerName = opponent === opponents.none ? "White" : opponent;
|
||||
const blackScore = score[GoColor.black];
|
||||
const whiteScore = score[GoColor.white];
|
||||
const blackPlayerName = opponent === GoOpponent.none ? "Black" : "You";
|
||||
const whitePlayerName = opponent === GoOpponent.none ? "White" : opponent;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -1,21 +1,20 @@
|
||||
import React from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Grid, Table, TableBody, TableCell, TableRow, Typography } from "@mui/material";
|
||||
|
||||
import { opponentList } from "../boardState/goConstants";
|
||||
import { Go } from "../Go";
|
||||
import { getScore } from "../boardAnalysis/scoring";
|
||||
import { Player } from "@player";
|
||||
import { Grid, Table, TableBody, TableCell, TableRow } from "@mui/material";
|
||||
import { GoGameboard } from "./GoGameboard";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { getBonusText } from "../effects/effect";
|
||||
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
|
||||
import { getRecordKeys } from "../../Types/Record";
|
||||
|
||||
export const GoStatusPage = (): React.ReactElement => {
|
||||
useRerender(400);
|
||||
const classes = boardStyles();
|
||||
const score = getScore(Player.go.boardState);
|
||||
const opponent = Player.go.boardState.ai;
|
||||
const score = getScore(Go.currentGame);
|
||||
const opponent = Go.currentGame.ai;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -29,7 +28,7 @@ export const GoStatusPage = (): React.ReactElement => {
|
||||
<Grid item>
|
||||
<div className={classes.statusPageGameboard}>
|
||||
<GoGameboard
|
||||
boardState={Player.go.boardState}
|
||||
boardState={Go.currentGame}
|
||||
traditional={false}
|
||||
clickHandler={(x, y) => ({ x, y })}
|
||||
hover={false}
|
||||
@ -50,7 +49,7 @@ export const GoStatusPage = (): React.ReactElement => {
|
||||
<strong>Effect:</strong>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{opponentList.map((faction, index) => {
|
||||
{getRecordKeys(Go.stats).map((faction, index) => {
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell className={classes.cellNone}>
|
||||
|
@ -1,44 +1,45 @@
|
||||
import { Box, Button, MenuItem, Select, SelectChangeEvent, Tooltip, Typography } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import { boardSizes, opponentDetails, opponentList, opponents } from "../boardState/goConstants";
|
||||
import { Player } from "@player";
|
||||
import { Box, Button, MenuItem, Select, SelectChangeEvent, Tooltip, Typography } from "@mui/material";
|
||||
|
||||
import { GoOpponent } from "@enums";
|
||||
import { Go } from "../Go";
|
||||
import { boardSizes, opponentDetails } from "../Constants";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
import { getHandicap } from "../boardState/boardState";
|
||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { getPlayerStats } from "../boardAnalysis/scoring";
|
||||
import { getOpponentStats } from "../boardAnalysis/scoring";
|
||||
import { showWorldDemon } from "../boardAnalysis/goAI";
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
search: (size: number, opponent: opponents) => void;
|
||||
search: (size: number, opponent: GoOpponent) => void;
|
||||
cancel: () => void;
|
||||
showInstructions: () => void;
|
||||
}
|
||||
|
||||
export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProps): React.ReactElement => {
|
||||
const classes = boardStyles();
|
||||
const [opponent, setOpponent] = useState<opponents>(Player.go.boardState?.ai ?? opponents.SlumSnakes);
|
||||
const [opponent, setOpponent] = useState<GoOpponent>(Go.currentGame?.ai ?? GoOpponent.SlumSnakes);
|
||||
const preselectedBoardSize =
|
||||
opponent === opponents.w0r1d_d43m0n ? 19 : Math.min(Player.go.boardState?.board?.[0]?.length ?? 7, 13);
|
||||
opponent === GoOpponent.w0r1d_d43m0n ? 19 : Math.min(Go.currentGame?.board?.[0]?.length ?? 7, 13);
|
||||
const [boardSize, setBoardSize] = useState(preselectedBoardSize);
|
||||
|
||||
const opponentFactions = [opponents.none, ...opponentList];
|
||||
if (showWorldDemon()) {
|
||||
opponentFactions.push(opponents.w0r1d_d43m0n);
|
||||
}
|
||||
const opponentFactions = Object.values(GoOpponent).filter(
|
||||
(opponent) => opponent !== GoOpponent.w0r1d_d43m0n || showWorldDemon(),
|
||||
);
|
||||
|
||||
const handicap = getHandicap(boardSize, opponent);
|
||||
|
||||
function changeOpponent(event: SelectChangeEvent): void {
|
||||
const newOpponent = event.target.value as opponents;
|
||||
const newOpponent = event.target.value as GoOpponent;
|
||||
setOpponent(newOpponent);
|
||||
if (newOpponent === opponents.w0r1d_d43m0n) {
|
||||
if (newOpponent === GoOpponent.w0r1d_d43m0n) {
|
||||
setBoardSize(19);
|
||||
|
||||
const stats = getPlayerStats(opponents.w0r1d_d43m0n);
|
||||
if (stats?.wins + stats?.losses === 0) {
|
||||
const stats = getOpponentStats(GoOpponent.w0r1d_d43m0n);
|
||||
if (stats.wins + stats.losses === 0) {
|
||||
Settings.GoTraditionalStyle = false;
|
||||
}
|
||||
} else if (boardSize > 13) {
|
||||
@ -67,12 +68,12 @@ export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProp
|
||||
<br />
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||
<Typography className={classes.opponentLabel}>
|
||||
{opponent !== opponents.none ? "Opponent Faction: " : ""}
|
||||
{opponent !== GoOpponent.none ? "Opponent Faction: " : ""}
|
||||
</Typography>
|
||||
<Select value={opponent} onChange={changeOpponent} sx={{ mr: 1 }}>
|
||||
{opponentFactions.map((faction) => (
|
||||
<MenuItem key={faction} value={faction}>
|
||||
{faction === opponents.w0r1d_d43m0n ? (
|
||||
{faction === GoOpponent.w0r1d_d43m0n ? (
|
||||
<CorruptableText content="???????????????" spoiler={false} />
|
||||
) : (
|
||||
`${faction} (${opponentDetails[faction].description})`
|
||||
@ -83,7 +84,7 @@ export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProp
|
||||
</Box>
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||
<Typography className={classes.opponentLabel}>Subnet size: </Typography>
|
||||
{opponent === opponents.w0r1d_d43m0n ? (
|
||||
{opponent === GoOpponent.w0r1d_d43m0n ? (
|
||||
<Typography>????</Typography>
|
||||
) : (
|
||||
<Select value={`${boardSize}`} onChange={changeBoardSize} sx={{ mr: 1 }}>
|
||||
@ -118,7 +119,7 @@ export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProp
|
||||
<br />
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle} ${classes.flavorText}`}>
|
||||
<Typography>
|
||||
{opponent === opponents.w0r1d_d43m0n ? (
|
||||
{opponent === GoOpponent.w0r1d_d43m0n ? (
|
||||
<>
|
||||
<CorruptableText content={opponentDetails[opponent].flavorText.slice(0, 40)} spoiler={false} />
|
||||
<CorruptableText content={opponentDetails[opponent].flavorText.slice(40)} spoiler={false} />
|
||||
@ -132,7 +133,7 @@ export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProp
|
||||
<br />
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||
<Typography>
|
||||
{opponent !== opponents.none ? "Faction subnet bonus:" : ""} {opponentDetails[opponent].bonusDescription}
|
||||
{opponent !== GoOpponent.none ? "Faction subnet bonus:" : ""} {opponentDetails[opponent].bonusDescription}
|
||||
</Typography>
|
||||
</Box>
|
||||
<br />
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Typography, Button } from "@mui/material";
|
||||
|
||||
import { BoardState, playerColors, validityReason } from "../boardState/goConstants";
|
||||
import { GoColor, GoValidity, ToastVariant } from "@enums";
|
||||
import { BoardState } from "../Types";
|
||||
import { GoGameboard } from "./GoGameboard";
|
||||
import { evaluateIfMoveIsValid } from "../boardAnalysis/boardAnalysis";
|
||||
import { SnackbarEvents } from "../../ui/React/Snackbar";
|
||||
import { ToastVariant } from "@enums";
|
||||
import { getStateCopy, makeMove } from "../boardState/boardState";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
|
||||
@ -32,31 +32,27 @@ export function GoTutorialChallenge({
|
||||
incorrectMoves2,
|
||||
incorrectText2,
|
||||
}: IProps): React.ReactElement {
|
||||
const stateRef = useRef(getStateCopy(state));
|
||||
const classes = boardStyles();
|
||||
const [currentState, setCurrentState] = useState(getStateCopy(state));
|
||||
const [displayText, setDisplayText] = useState(description);
|
||||
const [showReset, setShowReset] = useState(false);
|
||||
|
||||
const handleClick = (x: number, y: number) => {
|
||||
if (currentState.history.length) {
|
||||
if (stateRef.current.previousBoard) {
|
||||
SnackbarEvents.emit(`Hit 'Reset' to try again`, ToastVariant.WARNING, 2000);
|
||||
return;
|
||||
}
|
||||
setShowReset(true);
|
||||
|
||||
const validity = evaluateIfMoveIsValid(currentState, x, y, playerColors.black);
|
||||
if (validity != validityReason.valid) {
|
||||
const validity = evaluateIfMoveIsValid(stateRef.current, x, y, GoColor.black);
|
||||
if (validity != GoValidity.valid) {
|
||||
setDisplayText(
|
||||
"Invalid move: You cannot suicide your routers by placing them with no access to any empty ports.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedBoard = makeMove(currentState, x, y, playerColors.black);
|
||||
|
||||
if (updatedBoard) {
|
||||
setCurrentState(getStateCopy(updatedBoard));
|
||||
|
||||
if (makeMove(stateRef.current, x, y, GoColor.black)) {
|
||||
if (correctMoves.find((move) => move.x === x && move.y === y)) {
|
||||
setDisplayText(correctText);
|
||||
} else if (incorrectMoves1?.find((move) => move.x === x && move.y === y)) {
|
||||
@ -70,7 +66,7 @@ export function GoTutorialChallenge({
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setCurrentState(getStateCopy(state));
|
||||
stateRef.current = getStateCopy(state);
|
||||
setDisplayText(description);
|
||||
setShowReset(false);
|
||||
};
|
||||
@ -78,7 +74,7 @@ export function GoTutorialChallenge({
|
||||
return (
|
||||
<div>
|
||||
<div className={classes.instructionBoard}>
|
||||
<GoGameboard boardState={currentState} traditional={false} clickHandler={handleClick} hover={true} />
|
||||
<GoGameboard boardState={stateRef.current} traditional={false} clickHandler={handleClick} hover={true} />
|
||||
</div>
|
||||
<Typography>{displayText}</Typography>
|
||||
{showReset ? <Button onClick={reset}>Reset</Button> : ""}
|
||||
|
@ -121,7 +121,7 @@ export function setRemovedFunctions(api: object, infos: Record<string, RemovedFu
|
||||
for (const [key, { version, replacement, replaceMsg }] of Object.entries(infos)) {
|
||||
Object.defineProperty(api, key, {
|
||||
value: (ctx: NetscriptContext) => () => {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`Function removed in ${version}. ${replaceMsg ? replacement : `Please use ${replacement} instead.`}`,
|
||||
"REMOVED FUNCTION",
|
||||
|
107
src/Netscript/ErrorMessages.ts
Normal file
107
src/Netscript/ErrorMessages.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import type { WorkerScript } from "./WorkerScript";
|
||||
import { ScriptDeath } from "./ScriptDeath";
|
||||
import type { NetscriptContext } from "./APIWrapper";
|
||||
import { dialogBoxCreate } from "../ui/React/DialogBox";
|
||||
|
||||
/** Log a message to a script's logs */
|
||||
export function log(ctx: NetscriptContext, message: () => string) {
|
||||
ctx.workerScript.log(ctx.functionPath, message);
|
||||
}
|
||||
|
||||
/** Creates an error message string containing hostname, scriptname, and the error message msg */
|
||||
export function basicErrorMessage(ws: WorkerScript | ScriptDeath, msg: string, type = "RUNTIME"): string {
|
||||
if (!(ws instanceof ScriptDeath)) {
|
||||
for (const [scriptUrl, script] of ws.scriptRef.dependencies) {
|
||||
msg = msg.replace(new RegExp(scriptUrl, "g"), script.filename);
|
||||
}
|
||||
}
|
||||
return `${type} ERROR\n${ws.name}@${ws.hostname} (PID - ${ws.pid})\n\n${msg}`;
|
||||
}
|
||||
|
||||
/** Creates an error message string with a stack trace. */
|
||||
export function errorMessage(ctx: NetscriptContext, msg: string, type = "RUNTIME"): string {
|
||||
const errstack = new Error().stack;
|
||||
if (errstack === undefined) throw new Error("how did we not throw an error?");
|
||||
const stack = errstack.split("\n").slice(1);
|
||||
const ws = ctx.workerScript;
|
||||
const caller = ctx.functionPath;
|
||||
const userstack = [];
|
||||
for (const stackline of stack) {
|
||||
const filename = (() => {
|
||||
// Check urls for dependencies
|
||||
for (const [url, script] of ws.scriptRef.dependencies) if (stackline.includes(url)) return script.filename;
|
||||
// Check for filenames directly if no URL found
|
||||
if (stackline.includes(ws.scriptRef.filename)) return ws.scriptRef.filename;
|
||||
for (const script of ws.scriptRef.dependencies.values()) {
|
||||
if (stackline.includes(script.filename)) return script.filename;
|
||||
}
|
||||
})();
|
||||
if (!filename) continue;
|
||||
|
||||
let call = { line: "-1", func: "unknown" };
|
||||
const chromeCall = parseChromeStackline(stackline);
|
||||
if (chromeCall) {
|
||||
call = chromeCall;
|
||||
}
|
||||
|
||||
const firefoxCall = parseFirefoxStackline(stackline);
|
||||
if (firefoxCall) {
|
||||
call = firefoxCall;
|
||||
}
|
||||
|
||||
userstack.push(`${filename}:L${call.line}@${call.func}`);
|
||||
}
|
||||
|
||||
log(ctx, () => msg);
|
||||
let rejectMsg = `${caller}: ${msg}`;
|
||||
if (userstack.length !== 0) rejectMsg += `\n\nStack:\n${userstack.join("\n")}`;
|
||||
return basicErrorMessage(ws, rejectMsg, type);
|
||||
|
||||
interface ILine {
|
||||
line: string;
|
||||
func: string;
|
||||
}
|
||||
function parseChromeStackline(line: string): ILine | null {
|
||||
const lineMatch = line.match(/.*:(\d+):\d+.*/);
|
||||
const funcMatch = line.match(/.*at (.+) \(.*/);
|
||||
if (lineMatch && funcMatch) return { line: lineMatch[1], func: funcMatch[1] };
|
||||
return null;
|
||||
}
|
||||
function parseFirefoxStackline(line: string): ILine | null {
|
||||
const lineMatch = line.match(/.*:(\d+):\d+$/);
|
||||
const lio = line.lastIndexOf("@");
|
||||
if (lineMatch && lio !== -1) return { line: lineMatch[1], func: line.slice(0, lio) };
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Generate an error dialog when workerscript is known */
|
||||
export function handleUnknownError(e: unknown, ws: WorkerScript | ScriptDeath | null = null, initialText = "") {
|
||||
if (e instanceof ScriptDeath) {
|
||||
//No dialog for an empty ScriptDeath
|
||||
if (e.errorMessage === "") return;
|
||||
if (!ws) {
|
||||
ws = e;
|
||||
e = ws.errorMessage;
|
||||
}
|
||||
}
|
||||
if (ws && typeof e === "string") {
|
||||
const headerText = basicErrorMessage(ws, "", "");
|
||||
if (!e.includes(headerText)) e = basicErrorMessage(ws, e);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
const msg = `${e.message} (sorry we can't be more helpful)`;
|
||||
e = ws ? basicErrorMessage(ws, msg, "SYNTAX") : `SYNTAX ERROR:\n\n${msg}`;
|
||||
} else if (e instanceof Error) {
|
||||
// Ignore any cancellation errors from Monaco that get here
|
||||
if (e.name === "Canceled" && e.message === "Canceled") return;
|
||||
const msg = `${e.message}${e.stack ? `\nstack:\n${e.stack.toString()}` : ""}`;
|
||||
e = ws ? basicErrorMessage(ws, msg) : `RUNTIME ERROR:\n\n${msg}`;
|
||||
}
|
||||
if (typeof e !== "string") {
|
||||
console.error("Unexpected error:", e);
|
||||
const msg = `Unexpected type of error thrown. This error was likely thrown manually within a script.
|
||||
Error has been logged to the console.\n\nType of error: ${typeof e}\nValue of error: ${e}`;
|
||||
e = ws ? basicErrorMessage(ws, msg, "UNKNOWN") : msg;
|
||||
}
|
||||
dialogBoxCreate(initialText + e);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import type { NetscriptContext } from "./APIWrapper";
|
||||
|
||||
import React from "react";
|
||||
import { NetscriptContext } from "./APIWrapper";
|
||||
import { WorkerScript } from "./WorkerScript";
|
||||
import { killWorkerScript } from "./killWorkerScript";
|
||||
import { GetAllServers, GetServer } from "../Server/AllServers";
|
||||
import { Player } from "@player";
|
||||
@ -31,7 +31,6 @@ import { findRunningScripts, findRunningScriptByPid } from "../Script/ScriptHelp
|
||||
import { arrayToString } from "../utils/helpers/ArrayHelpers";
|
||||
import { HacknetServer } from "../Hacknet/HacknetServer";
|
||||
import { BaseServer } from "../Server/BaseServer";
|
||||
import { dialogBoxCreate } from "../ui/React/DialogBox";
|
||||
import { RamCostConstants } from "./RamCostGenerator";
|
||||
import { isPositiveInteger, PositiveInteger, Unknownify, isPositiveNumber, PositiveNumber } from "../types";
|
||||
import { Engine } from "../engine";
|
||||
@ -39,6 +38,8 @@ import { resolveFilePath, FilePath } from "../Paths/FilePath";
|
||||
import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath";
|
||||
import { CustomBoundary } from "../ui/Components/CustomBoundary";
|
||||
import { ServerConstants } from "../Server/data/Constants";
|
||||
import { basicErrorMessage, errorMessage, log } from "./ErrorMessages";
|
||||
import { assertString, debugType } from "./TypeAssertion";
|
||||
|
||||
export const helpers = {
|
||||
string,
|
||||
@ -48,8 +49,8 @@ export const helpers = {
|
||||
runOptions,
|
||||
spawnOptions,
|
||||
argsToString,
|
||||
makeBasicErrorMsg,
|
||||
makeRuntimeErrorMsg,
|
||||
basicErrorMessage,
|
||||
errorMessage,
|
||||
validateHGWOptions,
|
||||
checkEnvFlags,
|
||||
checkSingularityAccess,
|
||||
@ -92,33 +93,6 @@ export interface CompleteHGWOptions {
|
||||
additionalMsec: number;
|
||||
}
|
||||
|
||||
export function assertString(ctx: NetscriptContext, argName: string, v: unknown): asserts v is string {
|
||||
if (typeof v !== "string")
|
||||
throw makeRuntimeErrorMsg(ctx, `${argName} expected to be a string. ${debugType(v)}`, "TYPE");
|
||||
}
|
||||
|
||||
const userFriendlyString = (v: unknown): string => {
|
||||
const clip = (s: string): string => {
|
||||
if (s.length > 15) return s.slice(0, 12) + "...";
|
||||
return s;
|
||||
};
|
||||
if (typeof v === "number") return String(v);
|
||||
if (typeof v === "string") {
|
||||
if (v === "") return "empty string";
|
||||
return `'${clip(v)}'`;
|
||||
}
|
||||
const json = JSON.stringify(v);
|
||||
if (!json) return "???";
|
||||
return `'${clip(json)}'`;
|
||||
};
|
||||
|
||||
const debugType = (v: unknown): string => {
|
||||
if (v === null) return `Is null.`;
|
||||
if (v === undefined) return "Is undefined.";
|
||||
if (typeof v === "function") return "Is a function.";
|
||||
return `Is of type '${typeof v}', value: ${userFriendlyString(v)}`;
|
||||
};
|
||||
|
||||
/** Convert a provided value v for argument argName to string. If it wasn't originally a string or number, throw. */
|
||||
function string(ctx: NetscriptContext, argName: string, v: unknown): string {
|
||||
if (typeof v === "number") v = v + ""; // cast to string;
|
||||
@ -132,17 +106,17 @@ function number(ctx: NetscriptContext, argName: string, v: unknown): number {
|
||||
const x = parseFloat(v);
|
||||
if (!isNaN(x)) return x; // otherwise it wasn't even a string representing a number.
|
||||
} else if (typeof v === "number") {
|
||||
if (isNaN(v)) throw makeRuntimeErrorMsg(ctx, `'${argName}' is NaN.`);
|
||||
if (isNaN(v)) throw errorMessage(ctx, `'${argName}' is NaN.`);
|
||||
return v;
|
||||
}
|
||||
throw makeRuntimeErrorMsg(ctx, `'${argName}' should be a number. ${debugType(v)}`, "TYPE");
|
||||
throw errorMessage(ctx, `'${argName}' should be a number. ${debugType(v)}`, "TYPE");
|
||||
}
|
||||
|
||||
/** Convert provided value v for argument argName to a positive integer, throwing if it looks like something else. */
|
||||
function positiveInteger(ctx: NetscriptContext, argName: string, v: unknown): PositiveInteger {
|
||||
const n = number(ctx, argName, v);
|
||||
if (!isPositiveInteger(n)) {
|
||||
throw makeRuntimeErrorMsg(ctx, `${argName} should be a positive integer, was ${n}`, "TYPE");
|
||||
throw errorMessage(ctx, `${argName} should be a positive integer, was ${n}`, "TYPE");
|
||||
}
|
||||
return n;
|
||||
}
|
||||
@ -150,13 +124,13 @@ function positiveInteger(ctx: NetscriptContext, argName: string, v: unknown): Po
|
||||
function positiveNumber(ctx: NetscriptContext, argName: string, v: unknown): PositiveNumber {
|
||||
const n = number(ctx, argName, v);
|
||||
if (!isPositiveNumber(n)) {
|
||||
throw makeRuntimeErrorMsg(ctx, `${argName} should be a positive number, was ${n}`, "TYPE");
|
||||
throw errorMessage(ctx, `${argName} should be a positive number, was ${n}`, "TYPE");
|
||||
}
|
||||
return n;
|
||||
}
|
||||
/** Returns args back if it is a ScriptArg[]. Throws an error if it is not. */
|
||||
function scriptArgs(ctx: NetscriptContext, args: unknown) {
|
||||
if (!isScriptArgs(args)) throw makeRuntimeErrorMsg(ctx, "'args' is not an array of script args", "TYPE");
|
||||
if (!isScriptArgs(args)) throw errorMessage(ctx, "'args' is not an array of script args", "TYPE");
|
||||
return args;
|
||||
}
|
||||
|
||||
@ -183,7 +157,7 @@ function runOptions(ctx: NetscriptContext, threadOrOption: unknown): CompleteRun
|
||||
if (options.ramOverride !== undefined && options.ramOverride !== null) {
|
||||
result.ramOverride = number(ctx, "RunOptions.ramOverride", options.ramOverride);
|
||||
if (result.ramOverride < RamCostConstants.Base) {
|
||||
throw makeRuntimeErrorMsg(
|
||||
throw errorMessage(
|
||||
ctx,
|
||||
`RunOptions.ramOverride must be >= baseCost (${RamCostConstants.Base}), was ${result.ramOverride}`,
|
||||
);
|
||||
@ -235,73 +209,6 @@ function argsToString(args: unknown[]): string {
|
||||
}, "") as string;
|
||||
}
|
||||
|
||||
/** Creates an error message string containing hostname, scriptname, and the error message msg */
|
||||
function makeBasicErrorMsg(ws: WorkerScript | ScriptDeath, msg: string, type = "RUNTIME"): string {
|
||||
if (ws instanceof WorkerScript) {
|
||||
for (const [scriptUrl, script] of ws.scriptRef.dependencies) {
|
||||
msg = msg.replace(new RegExp(scriptUrl, "g"), script.filename);
|
||||
}
|
||||
}
|
||||
return `${type} ERROR\n${ws.name}@${ws.hostname} (PID - ${ws.pid})\n\n${msg}`;
|
||||
}
|
||||
|
||||
/** Creates an error message string with a stack trace. */
|
||||
export function makeRuntimeErrorMsg(ctx: NetscriptContext, msg: string, type = "RUNTIME"): string {
|
||||
const errstack = new Error().stack;
|
||||
if (errstack === undefined) throw new Error("how did we not throw an error?");
|
||||
const stack = errstack.split("\n").slice(1);
|
||||
const ws = ctx.workerScript;
|
||||
const caller = ctx.functionPath;
|
||||
const userstack = [];
|
||||
for (const stackline of stack) {
|
||||
const filename = (() => {
|
||||
// Check urls for dependencies
|
||||
for (const [url, script] of ws.scriptRef.dependencies) if (stackline.includes(url)) return script.filename;
|
||||
// Check for filenames directly if no URL found
|
||||
if (stackline.includes(ws.scriptRef.filename)) return ws.scriptRef.filename;
|
||||
for (const script of ws.scriptRef.dependencies.values()) {
|
||||
if (stackline.includes(script.filename)) return script.filename;
|
||||
}
|
||||
})();
|
||||
if (!filename) continue;
|
||||
|
||||
let call = { line: "-1", func: "unknown" };
|
||||
const chromeCall = parseChromeStackline(stackline);
|
||||
if (chromeCall) {
|
||||
call = chromeCall;
|
||||
}
|
||||
|
||||
const firefoxCall = parseFirefoxStackline(stackline);
|
||||
if (firefoxCall) {
|
||||
call = firefoxCall;
|
||||
}
|
||||
|
||||
userstack.push(`${filename}:L${call.line}@${call.func}`);
|
||||
}
|
||||
|
||||
log(ctx, () => msg);
|
||||
let rejectMsg = `${caller}: ${msg}`;
|
||||
if (userstack.length !== 0) rejectMsg += `\n\nStack:\n${userstack.join("\n")}`;
|
||||
return makeBasicErrorMsg(ws, rejectMsg, type);
|
||||
|
||||
interface ILine {
|
||||
line: string;
|
||||
func: string;
|
||||
}
|
||||
function parseChromeStackline(line: string): ILine | null {
|
||||
const lineMatch = line.match(/.*:(\d+):\d+.*/);
|
||||
const funcMatch = line.match(/.*at (.+) \(.*/);
|
||||
if (lineMatch && funcMatch) return { line: lineMatch[1], func: funcMatch[1] };
|
||||
return null;
|
||||
}
|
||||
function parseFirefoxStackline(line: string): ILine | null {
|
||||
const lineMatch = line.match(/.*:(\d+):\d+$/);
|
||||
const lio = line.lastIndexOf("@");
|
||||
if (lineMatch && lio !== -1) return { line: lineMatch[1], func: line.slice(0, lio) };
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function validateHGWOptions(ctx: NetscriptContext, opts: unknown): CompleteHGWOptions {
|
||||
const result: CompleteHGWOptions = {
|
||||
threads: ctx.workerScript.scriptRef.threads,
|
||||
@ -312,17 +219,17 @@ function validateHGWOptions(ctx: NetscriptContext, opts: unknown): CompleteHGWOp
|
||||
return result;
|
||||
}
|
||||
if (typeof opts !== "object") {
|
||||
throw makeRuntimeErrorMsg(ctx, `BasicHGWOptions must be an object if specified, was ${opts}`);
|
||||
throw errorMessage(ctx, `BasicHGWOptions must be an object if specified, was ${opts}`);
|
||||
}
|
||||
// Safe assertion since threadOrOption type has been narrowed to a non-null object
|
||||
const options = opts as Unknownify<CompleteHGWOptions>;
|
||||
result.stock = !!options.stock;
|
||||
result.additionalMsec = number(ctx, "opts.additionalMsec", options.additionalMsec ?? 0);
|
||||
if (result.additionalMsec < 0) {
|
||||
throw makeRuntimeErrorMsg(ctx, `additionalMsec must be non-negative, got ${options.additionalMsec}`);
|
||||
throw errorMessage(ctx, `additionalMsec must be non-negative, got ${options.additionalMsec}`);
|
||||
}
|
||||
if (result.additionalMsec > 1e9) {
|
||||
throw makeRuntimeErrorMsg(ctx, `additionalMsec too large (>1e9), got ${options.additionalMsec}`);
|
||||
throw errorMessage(ctx, `additionalMsec too large (>1e9), got ${options.additionalMsec}`);
|
||||
}
|
||||
const requestedThreads = options.threads;
|
||||
const threads = ctx.workerScript.scriptRef.threads;
|
||||
@ -331,7 +238,7 @@ function validateHGWOptions(ctx: NetscriptContext, opts: unknown): CompleteHGWOp
|
||||
} else {
|
||||
const positiveThreads = positiveNumber(ctx, "opts.threads", requestedThreads);
|
||||
if (positiveThreads > threads) {
|
||||
throw makeRuntimeErrorMsg(
|
||||
throw errorMessage(
|
||||
ctx,
|
||||
`Too many threads requested by ${ctx.function}. Requested: ${positiveThreads}. Has: ${threads}.`,
|
||||
);
|
||||
@ -345,7 +252,7 @@ function validateHGWOptions(ctx: NetscriptContext, opts: unknown): CompleteHGWOp
|
||||
/** Validate singularity access by throwing an error if the player does not have access. */
|
||||
function checkSingularityAccess(ctx: NetscriptContext): void {
|
||||
if (Player.bitNodeN !== 4 && Player.sourceFileLvl(4) === 0) {
|
||||
throw makeRuntimeErrorMsg(
|
||||
throw errorMessage(
|
||||
ctx,
|
||||
`This singularity function requires Source-File 4 to run. A power up you obtain later in the game.
|
||||
It will be very obvious when and how you can obtain it.`,
|
||||
@ -363,7 +270,7 @@ function checkEnvFlags(ctx: NetscriptContext): void {
|
||||
}
|
||||
if (ws.env.runningFn && ctx.function !== "asleep") {
|
||||
log(ctx, () => "Failed to run due to failed concurrency check.");
|
||||
const err = makeRuntimeErrorMsg(
|
||||
const err = errorMessage(
|
||||
ctx,
|
||||
`Concurrent calls to Netscript functions are not allowed!
|
||||
Did you forget to await hack(), grow(), or some other
|
||||
@ -404,7 +311,7 @@ function updateDynamicRam(ctx: NetscriptContext, ramCost: number): void {
|
||||
// rounding issues without exposing rounding exploits in ramUsage.
|
||||
if (ws.dynamicRamUsage > 1.00000000000001 * ws.scriptRef.ramUsage) {
|
||||
log(ctx, () => "Insufficient static ram available.");
|
||||
const err = makeRuntimeErrorMsg(
|
||||
const err = errorMessage(
|
||||
ctx,
|
||||
`Dynamic RAM usage calculated to be greater than RAM allocation.
|
||||
This is probably because you somehow circumvented the static RAM calculation.
|
||||
@ -449,7 +356,7 @@ function scriptIdentifier(
|
||||
args,
|
||||
};
|
||||
}
|
||||
throw makeRuntimeErrorMsg(ctx, "An unknown type of input was provided as a script identifier.", "TYPE");
|
||||
throw errorMessage(ctx, "An unknown type of input was provided as a script identifier.", "TYPE");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -463,7 +370,7 @@ function getServer(ctx: NetscriptContext, hostname: string) {
|
||||
const server = GetServer(hostname);
|
||||
if (server == null || (server.serversOnNetwork.length == 0 && server.hostname != "home")) {
|
||||
const str = hostname === "" ? "'' (empty string)" : "'" + hostname + "'";
|
||||
throw makeRuntimeErrorMsg(ctx, `Invalid hostname: ${str}`);
|
||||
throw errorMessage(ctx, `Invalid hostname: ${str}`);
|
||||
}
|
||||
return server;
|
||||
}
|
||||
@ -478,7 +385,7 @@ function hack(ctx: NetscriptContext, hostname: string, manual: boolean, opts: un
|
||||
const { threads, stock, additionalMsec } = validateHGWOptions(ctx, opts);
|
||||
const server = getServer(ctx, hostname);
|
||||
if (!(server instanceof Server)) {
|
||||
throw makeRuntimeErrorMsg(ctx, "Cannot be executed on this server.");
|
||||
throw errorMessage(ctx, "Cannot be executed on this server.");
|
||||
}
|
||||
|
||||
// Calculate the hacking time
|
||||
@ -488,7 +395,7 @@ function hack(ctx: NetscriptContext, hostname: string, manual: boolean, opts: un
|
||||
// No root access or skill level too low
|
||||
const canHack = netscriptCanHack(server);
|
||||
if (!canHack.res) {
|
||||
throw makeRuntimeErrorMsg(ctx, canHack.msg || "");
|
||||
throw errorMessage(ctx, canHack.msg || "");
|
||||
}
|
||||
|
||||
log(
|
||||
@ -578,7 +485,7 @@ function hack(ctx: NetscriptContext, hostname: string, manual: boolean, opts: un
|
||||
function portNumber(ctx: NetscriptContext, _n: unknown): PortNumber {
|
||||
const n = positiveInteger(ctx, "portNumber", _n);
|
||||
if (n > CONSTANTS.NumNetscriptPorts) {
|
||||
throw makeRuntimeErrorMsg(
|
||||
throw errorMessage(
|
||||
ctx,
|
||||
`Trying to use an invalid port: ${n}. Must be less or equal to ${CONSTANTS.NumNetscriptPorts}.`,
|
||||
);
|
||||
@ -594,7 +501,7 @@ function person(ctx: NetscriptContext, p: unknown): IPerson {
|
||||
city: undefined,
|
||||
};
|
||||
const error = missingKey(fakePerson, p);
|
||||
if (error) throw makeRuntimeErrorMsg(ctx, `person should be a Person.\n${error}`, "TYPE");
|
||||
if (error) throw errorMessage(ctx, `person should be a Person.\n${error}`, "TYPE");
|
||||
return p as IPerson;
|
||||
}
|
||||
|
||||
@ -616,7 +523,7 @@ function server(ctx: NetscriptContext, s: unknown): IServer {
|
||||
purchasedByPlayer: undefined,
|
||||
};
|
||||
const error = missingKey(fakeServer, s);
|
||||
if (error) throw makeRuntimeErrorMsg(ctx, `server should be a Server.\n${error}`, "TYPE");
|
||||
if (error) throw errorMessage(ctx, `server should be a Server.\n${error}`, "TYPE");
|
||||
return s as IServer;
|
||||
}
|
||||
|
||||
@ -632,37 +539,33 @@ function missingKey(expect: object, actual: unknown): string | false {
|
||||
|
||||
function gang(ctx: NetscriptContext, g: unknown): FormulaGang {
|
||||
const error = missingKey({ respect: 0, territory: 0, wantedLevel: 0 }, g);
|
||||
if (error) throw makeRuntimeErrorMsg(ctx, `gang should be a Gang.\n${error}`, "TYPE");
|
||||
if (error) throw errorMessage(ctx, `gang should be a Gang.\n${error}`, "TYPE");
|
||||
return g as FormulaGang;
|
||||
}
|
||||
|
||||
function gangMember(ctx: NetscriptContext, m: unknown): GangMember {
|
||||
const error = missingKey(new GangMember(), m);
|
||||
if (error) throw makeRuntimeErrorMsg(ctx, `member should be a GangMember.\n${error}`, "TYPE");
|
||||
if (error) throw errorMessage(ctx, `member should be a GangMember.\n${error}`, "TYPE");
|
||||
return m as GangMember;
|
||||
}
|
||||
|
||||
function gangTask(ctx: NetscriptContext, t: unknown): GangMemberTask {
|
||||
const error = missingKey(new GangMemberTask("", "", false, false, { hackWeight: 100 }), t);
|
||||
if (error) throw makeRuntimeErrorMsg(ctx, `task should be a GangMemberTask.\n${error}`, "TYPE");
|
||||
if (error) throw errorMessage(ctx, `task should be a GangMemberTask.\n${error}`, "TYPE");
|
||||
return t as GangMemberTask;
|
||||
}
|
||||
|
||||
function log(ctx: NetscriptContext, message: () => string) {
|
||||
ctx.workerScript.log(ctx.functionPath, message);
|
||||
}
|
||||
|
||||
export function filePath(ctx: NetscriptContext, argName: string, filename: unknown): FilePath {
|
||||
assertString(ctx, argName, filename);
|
||||
const path = resolveFilePath(filename, ctx.workerScript.name);
|
||||
if (path) return path;
|
||||
throw makeRuntimeErrorMsg(ctx, `Invalid ${argName}, was not a valid path: ${filename}`);
|
||||
throw errorMessage(ctx, `Invalid ${argName}, was not a valid path: ${filename}`);
|
||||
}
|
||||
|
||||
export function scriptPath(ctx: NetscriptContext, argName: string, filename: unknown): ScriptFilePath {
|
||||
const path = filePath(ctx, argName, filename);
|
||||
if (hasScriptExtension(path)) return path;
|
||||
throw makeRuntimeErrorMsg(ctx, `Invalid ${argName}, must be a script: ${filename}`);
|
||||
throw errorMessage(ctx, `Invalid ${argName}, must be a script: ${filename}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -682,7 +585,7 @@ export function getRunningScriptsByArgs(
|
||||
scriptArgs: ScriptArg[],
|
||||
): Map<number, RunningScript> | null {
|
||||
if (!Array.isArray(scriptArgs)) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
"Invalid scriptArgs argument passed into getRunningScriptByArgs().\n" +
|
||||
"This is probably a bug. Please report to game developer",
|
||||
@ -785,37 +688,6 @@ function failOnHacknetServer(ctx: NetscriptContext, server: BaseServer): boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** Generate an error dialog when workerscript is known */
|
||||
export function handleUnknownError(e: unknown, ws: WorkerScript | ScriptDeath | null = null, initialText = "") {
|
||||
if (e instanceof ScriptDeath) {
|
||||
//No dialog for an empty ScriptDeath
|
||||
if (e.errorMessage === "") return;
|
||||
if (!ws) {
|
||||
ws = e;
|
||||
e = ws.errorMessage;
|
||||
}
|
||||
}
|
||||
if (ws && typeof e === "string") {
|
||||
const headerText = makeBasicErrorMsg(ws, "", "");
|
||||
if (!e.includes(headerText)) e = makeBasicErrorMsg(ws, e);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
const msg = `${e.message} (sorry we can't be more helpful)`;
|
||||
e = ws ? makeBasicErrorMsg(ws, msg, "SYNTAX") : `SYNTAX ERROR:\n\n${msg}`;
|
||||
} else if (e instanceof Error) {
|
||||
// Ignore any cancellation errors from Monaco that get here
|
||||
if (e.name === "Canceled" && e.message === "Canceled") return;
|
||||
const msg = `${e.message}${e.stack ? `\nstack:\n${e.stack.toString()}` : ""}`;
|
||||
e = ws ? makeBasicErrorMsg(ws, msg) : `RUNTIME ERROR:\n\n${msg}`;
|
||||
}
|
||||
if (typeof e !== "string") {
|
||||
console.error("Unexpected error:", e);
|
||||
const msg = `Unexpected type of error thrown. This error was likely thrown manually within a script.
|
||||
Error has been logged to the console.\n\nType of error: ${typeof e}\nValue of error: ${e}`;
|
||||
e = ws ? makeBasicErrorMsg(ws, msg, "UNKNOWN") : msg;
|
||||
}
|
||||
dialogBoxCreate(initialText + e);
|
||||
}
|
||||
|
||||
// Incrementing value for custom element keys
|
||||
let customElementKey = 0;
|
||||
|
||||
|
28
src/Netscript/TypeAssertion.ts
Normal file
28
src/Netscript/TypeAssertion.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type { NetscriptContext } from "./APIWrapper";
|
||||
import { errorMessage } from "./ErrorMessages";
|
||||
|
||||
const userFriendlyString = (v: unknown): string => {
|
||||
const clip = (s: string): string => {
|
||||
if (s.length > 15) return s.slice(0, 12) + "...";
|
||||
return s;
|
||||
};
|
||||
if (typeof v === "number") return String(v);
|
||||
if (typeof v === "string") {
|
||||
if (v === "") return "empty string";
|
||||
return `'${clip(v)}'`;
|
||||
}
|
||||
const json = JSON.stringify(v);
|
||||
if (!json) return "???";
|
||||
return `'${clip(json)}'`;
|
||||
};
|
||||
|
||||
export const debugType = (v: unknown): string => {
|
||||
if (v === null) return `Is null.`;
|
||||
if (v === undefined) return "Is undefined.";
|
||||
if (typeof v === "function") return "Is a function.";
|
||||
return `Is of type '${typeof v}', value: ${userFriendlyString(v)}`;
|
||||
};
|
||||
|
||||
export function assertString(ctx: NetscriptContext, argName: string, v: unknown): asserts v is string {
|
||||
if (typeof v !== "string") throw errorMessage(ctx, `${argName} expected to be a string. ${debugType(v)}`, "TYPE");
|
||||
}
|
@ -10,7 +10,7 @@ import { GetServer } from "../Server/AllServers";
|
||||
import { AddRecentScript } from "./RecentScripts";
|
||||
import { ITutorial } from "../InteractiveTutorial";
|
||||
import { AlertEvents } from "../ui/React/AlertManager";
|
||||
import { handleUnknownError } from "./NetscriptHelpers";
|
||||
import { handleUnknownError } from "./ErrorMessages";
|
||||
import { roundToTwo } from "../utils/helpers/roundToTwo";
|
||||
|
||||
export function killWorkerScript(ws: WorkerScript): boolean {
|
||||
|
@ -173,7 +173,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
return -1;
|
||||
}
|
||||
if (isNaN(hackAmount)) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`Invalid hackAmount argument passed into hackAnalyzeThreads: ${hackAmount}. Must be numeric.`,
|
||||
);
|
||||
@ -269,7 +269,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
// No root access or skill level too low
|
||||
const canHack = netscriptCanGrow(server);
|
||||
if (!canHack.res) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || "");
|
||||
throw helpers.errorMessage(ctx, canHack.msg || "");
|
||||
}
|
||||
|
||||
const growTime = calculateGrowTime(server, Player) + additionalMsec / 1000.0;
|
||||
@ -318,11 +318,11 @@ export const ns: InternalAPI<NSFull> = {
|
||||
return 0;
|
||||
}
|
||||
if (mult < 1 || !isFinite(mult)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid argument: multiplier must be finite and >= 1, is ${mult}.`);
|
||||
throw helpers.errorMessage(ctx, `Invalid argument: multiplier must be finite and >= 1, is ${mult}.`);
|
||||
}
|
||||
// TODO 2.3: Add assertion function for positive integer, there are a lot of places everywhere that can use this
|
||||
if (!Number.isInteger(cores) || cores < 1) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Cores should be a positive integer. Cores provided: ${cores}`);
|
||||
throw helpers.errorMessage(ctx, `Cores should be a positive integer. Cores provided: ${cores}`);
|
||||
}
|
||||
|
||||
return numCycleForGrowth(server, mult, cores);
|
||||
@ -363,7 +363,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
// No root access or skill level too low
|
||||
const canHack = netscriptCanWeaken(server);
|
||||
if (!canHack.res) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || "");
|
||||
throw helpers.errorMessage(ctx, canHack.msg || "");
|
||||
}
|
||||
|
||||
const weakenTime = calculateWeakenTime(server, Player) + additionalMsec / 1000.0;
|
||||
@ -424,7 +424,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
(ctx) =>
|
||||
(...args) => {
|
||||
if (args.length === 0) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Takes at least 1 argument.");
|
||||
throw helpers.errorMessage(ctx, "Takes at least 1 argument.");
|
||||
}
|
||||
ctx.workerScript.print(helpers.argsToString(args));
|
||||
},
|
||||
@ -433,7 +433,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
(_format, ...args) => {
|
||||
const format = helpers.string(ctx, "format", _format);
|
||||
if (typeof format !== "string") {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "First argument must be string for the format.");
|
||||
throw helpers.errorMessage(ctx, "First argument must be string for the format.");
|
||||
}
|
||||
ctx.workerScript.print(vsprintf(format, args));
|
||||
},
|
||||
@ -441,7 +441,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
(ctx) =>
|
||||
(...args) => {
|
||||
if (args.length === 0) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Takes at least 1 argument.");
|
||||
throw helpers.errorMessage(ctx, "Takes at least 1 argument.");
|
||||
}
|
||||
const str = helpers.argsToString(args);
|
||||
if (str.startsWith("ERROR") || str.startsWith("FAIL")) {
|
||||
@ -497,7 +497,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
}
|
||||
helpers.log(ctx, () => `Disabled logging for all functions`);
|
||||
} else if (possibleLogs[fn] === undefined) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid argument: ${fn}.`);
|
||||
throw helpers.errorMessage(ctx, `Invalid argument: ${fn}.`);
|
||||
} else {
|
||||
ctx.workerScript.disableLogs[fn] = true;
|
||||
helpers.log(ctx, () => `Disabled logging for ${fn}`);
|
||||
@ -511,7 +511,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
}
|
||||
helpers.log(ctx, () => `Enabled logging for all functions`);
|
||||
} else if (possibleLogs[fn] === undefined) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid argument: ${fn}.`);
|
||||
throw helpers.errorMessage(ctx, `Invalid argument: ${fn}.`);
|
||||
}
|
||||
delete ctx.workerScript.disableLogs[fn];
|
||||
helpers.log(ctx, () => `Enabled logging for ${fn}`);
|
||||
@ -519,7 +519,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
isLogEnabled: (ctx) => (_fn) => {
|
||||
const fn = helpers.string(ctx, "fn", _fn);
|
||||
if (possibleLogs[fn] === undefined) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid argument: ${fn}.`);
|
||||
throw helpers.errorMessage(ctx, `Invalid argument: ${fn}.`);
|
||||
}
|
||||
return !ctx.workerScript.disableLogs[fn];
|
||||
},
|
||||
@ -605,10 +605,10 @@ export const ns: InternalAPI<NSFull> = {
|
||||
return true;
|
||||
}
|
||||
if (!Player.hasProgram(CompletedProgramName.nuke)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the NUKE.exe virus!");
|
||||
throw helpers.errorMessage(ctx, "You do not have the NUKE.exe virus!");
|
||||
}
|
||||
if (server.openPortCount < server.numOpenPortsRequired) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Not enough ports opened to use NUKE.exe virus.");
|
||||
throw helpers.errorMessage(ctx, "Not enough ports opened to use NUKE.exe virus.");
|
||||
}
|
||||
server.hasAdminRights = true;
|
||||
helpers.log(ctx, () => `Executed NUKE.exe virus on '${server.hostname}' to gain root access.`);
|
||||
@ -622,7 +622,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
return false;
|
||||
}
|
||||
if (!Player.hasProgram(CompletedProgramName.bruteSsh)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the BruteSSH.exe program!");
|
||||
throw helpers.errorMessage(ctx, "You do not have the BruteSSH.exe program!");
|
||||
}
|
||||
if (!server.sshPortOpen) {
|
||||
helpers.log(ctx, () => `Executed BruteSSH.exe on '${server.hostname}' to open SSH port (22).`);
|
||||
@ -641,7 +641,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
return false;
|
||||
}
|
||||
if (!Player.hasProgram(CompletedProgramName.ftpCrack)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the FTPCrack.exe program!");
|
||||
throw helpers.errorMessage(ctx, "You do not have the FTPCrack.exe program!");
|
||||
}
|
||||
if (!server.ftpPortOpen) {
|
||||
helpers.log(ctx, () => `Executed FTPCrack.exe on '${server.hostname}' to open FTP port (21).`);
|
||||
@ -660,7 +660,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
return false;
|
||||
}
|
||||
if (!Player.hasProgram(CompletedProgramName.relaySmtp)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the relaySMTP.exe program!");
|
||||
throw helpers.errorMessage(ctx, "You do not have the relaySMTP.exe program!");
|
||||
}
|
||||
if (!server.smtpPortOpen) {
|
||||
helpers.log(ctx, () => `Executed relaySMTP.exe on '${server.hostname}' to open SMTP port (25).`);
|
||||
@ -679,7 +679,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
return false;
|
||||
}
|
||||
if (!Player.hasProgram(CompletedProgramName.httpWorm)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the HTTPWorm.exe program!");
|
||||
throw helpers.errorMessage(ctx, "You do not have the HTTPWorm.exe program!");
|
||||
}
|
||||
if (!server.httpPortOpen) {
|
||||
helpers.log(ctx, () => `Executed HTTPWorm.exe on '${server.hostname}' to open HTTP port (80).`);
|
||||
@ -698,7 +698,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
return false;
|
||||
}
|
||||
if (!Player.hasProgram(CompletedProgramName.sqlInject)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have the SQLInject.exe program!");
|
||||
throw helpers.errorMessage(ctx, "You do not have the SQLInject.exe program!");
|
||||
}
|
||||
if (!server.sqlPortOpen) {
|
||||
helpers.log(ctx, () => `Executed SQLInject.exe on '${server.hostname}' to open SQL port (1433).`);
|
||||
@ -738,7 +738,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
setTimeout(() => {
|
||||
const scriptServer = GetServer(ctx.workerScript.hostname);
|
||||
if (scriptServer == null) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Could not find server. This is a bug. Report to dev");
|
||||
throw helpers.errorMessage(ctx, "Could not find server. This is a bug. Report to dev");
|
||||
}
|
||||
|
||||
return runScriptFromScript("spawn", scriptServer, path, args, ctx.workerScript, runOpts);
|
||||
@ -762,7 +762,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
} else {
|
||||
// Kill by filename/hostname
|
||||
if (scriptID === undefined) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Usage: kill(scriptname, server, [arg1], [arg2]...)");
|
||||
throw helpers.errorMessage(ctx, "Usage: kill(scriptname, server, [arg1], [arg2]...)");
|
||||
}
|
||||
|
||||
const byPid = helpers.getRunningScriptsByArgs(ctx, ident.scriptname, ident.hostname, ident.args);
|
||||
@ -837,7 +837,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
continue;
|
||||
}
|
||||
if (!path.endsWith(".lit")) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Only works for scripts, .lit and .txt files.");
|
||||
throw helpers.errorMessage(ctx, "Only works for scripts, .lit and .txt files.");
|
||||
}
|
||||
lits.push(path);
|
||||
}
|
||||
@ -954,7 +954,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
(ctx) =>
|
||||
(_n = Player.bitNodeN, _lvl = Player.sourceFileLvl(Player.bitNodeN) + 1) => {
|
||||
if (Player.sourceFileLvl(5) <= 0 && Player.bitNodeN !== 5)
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Requires Source-File 5 to run.");
|
||||
throw helpers.errorMessage(ctx, "Requires Source-File 5 to run.");
|
||||
const n = Math.round(helpers.number(ctx, "n", _n));
|
||||
const lvl = Math.round(helpers.number(ctx, "lvl", _lvl));
|
||||
if (n < 1 || n > 14) throw new Error("n must be between 1 and 14");
|
||||
@ -1350,7 +1350,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
return;
|
||||
}
|
||||
if (!hasTextExtension(filepath)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `File path should be a text file or script. ${filepath} is invalid.`);
|
||||
throw helpers.errorMessage(ctx, `File path should be a text file or script. ${filepath} is invalid.`);
|
||||
}
|
||||
if (mode === "w") {
|
||||
server.writeToTextFile(filepath, data);
|
||||
@ -1385,11 +1385,11 @@ export const ns: InternalAPI<NSFull> = {
|
||||
clear: (ctx) => (_file) => {
|
||||
const path = helpers.filePath(ctx, "file", _file);
|
||||
if (!hasScriptExtension(path) && !hasTextExtension(path)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid file path or extension: ${_file}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid file path or extension: ${_file}`);
|
||||
}
|
||||
const server = ctx.workerScript.getServer();
|
||||
const file = server.getContentFile(path);
|
||||
if (!file) throw helpers.makeRuntimeErrorMsg(ctx, `${path} does not exist on ${server.hostname}`);
|
||||
if (!file) throw helpers.errorMessage(ctx, `${path} does not exist on ${server.hostname}`);
|
||||
// The content setter handles invalidating script modules where applicable.
|
||||
file.content = "";
|
||||
},
|
||||
@ -1603,23 +1603,23 @@ export const ns: InternalAPI<NSFull> = {
|
||||
_options ??= options;
|
||||
const txt = helpers.string(ctx, "txt", _txt);
|
||||
assert(_options, objectAssert, (type) =>
|
||||
helpers.makeRuntimeErrorMsg(ctx, `Invalid type for options: ${type}. Should be object.`, "TYPE"),
|
||||
helpers.errorMessage(ctx, `Invalid type for options: ${type}. Should be object.`, "TYPE"),
|
||||
);
|
||||
if (_options.type !== undefined) {
|
||||
assert(_options.type, stringAssert, (type) =>
|
||||
helpers.makeRuntimeErrorMsg(ctx, `Invalid type for options.type: ${type}. Should be string.`, "TYPE"),
|
||||
helpers.errorMessage(ctx, `Invalid type for options.type: ${type}. Should be string.`, "TYPE"),
|
||||
);
|
||||
options.type = _options.type;
|
||||
const validTypes = ["boolean", "text", "select"];
|
||||
if (!["boolean", "text", "select"].includes(options.type)) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`Invalid value for options.type: ${options.type}. Must be one of ${validTypes.join(", ")}.`,
|
||||
);
|
||||
}
|
||||
if (options.type === "select") {
|
||||
assert(_options.choices, arrayAssert, (type) =>
|
||||
helpers.makeRuntimeErrorMsg(
|
||||
helpers.errorMessage(
|
||||
ctx,
|
||||
`Invalid type for options.choices: ${type}. If options.type is "select", options.choices must be an array.`,
|
||||
"TYPE",
|
||||
@ -1709,7 +1709,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
}),
|
||||
atExit: (ctx) => (f) => {
|
||||
if (typeof f !== "function") {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "argument should be function");
|
||||
throw helpers.errorMessage(ctx, "argument should be function");
|
||||
}
|
||||
ctx.workerScript.atExit = () => {
|
||||
f();
|
||||
@ -1725,7 +1725,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
(!hasTextExtension(sourcePath) && !hasScriptExtension(sourcePath)) ||
|
||||
(!hasTextExtension(destinationPath) && !hasScriptExtension(destinationPath))
|
||||
) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `'mv' can only be used on scripts and text files (.txt)`);
|
||||
throw helpers.errorMessage(ctx, `'mv' can only be used on scripts and text files (.txt)`);
|
||||
}
|
||||
if (sourcePath === destinationPath) {
|
||||
helpers.log(ctx, () => "WARNING: Did nothing, source and destination paths were the same.");
|
||||
@ -1733,7 +1733,7 @@ export const ns: InternalAPI<NSFull> = {
|
||||
}
|
||||
const sourceContentFile = server.getContentFile(sourcePath);
|
||||
if (!sourceContentFile) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Source text file ${sourcePath} does not exist on ${hostname}`);
|
||||
throw helpers.errorMessage(ctx, `Source text file ${sourcePath} does not exist on ${hostname}`);
|
||||
}
|
||||
const success = sourceContentFile.deleteFromServer(server);
|
||||
if (success) {
|
||||
|
@ -17,11 +17,11 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
const getBladeburner = function (ctx: NetscriptContext): Bladeburner {
|
||||
const apiAccess = Player.bitNodeN === 7 || Player.sourceFileLvl(7) > 0;
|
||||
if (!apiAccess) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You have not unlocked the bladeburner API.", "API ACCESS");
|
||||
throw helpers.errorMessage(ctx, "You have not unlocked the bladeburner API.", "API ACCESS");
|
||||
}
|
||||
const bladeburner = Player.bladeburner;
|
||||
if (!bladeburner)
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You must be a member of the Bladeburner division to use this API.");
|
||||
throw helpers.errorMessage(ctx, "You must be a member of the Bladeburner division to use this API.");
|
||||
return bladeburner;
|
||||
};
|
||||
|
||||
@ -30,11 +30,11 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
if (bladeburner === null) throw new Error("Must have joined bladeburner");
|
||||
const actionId = bladeburner.getActionIdFromTypeAndName(type, name);
|
||||
if (!actionId) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid action type='${type}', name='${name}'`);
|
||||
throw helpers.errorMessage(ctx, `Invalid action type='${type}', name='${name}'`);
|
||||
}
|
||||
const actionObj = bladeburner.getActionObject(actionId);
|
||||
if (!actionObj) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid action type='${type}', name='${name}'`);
|
||||
throw helpers.errorMessage(ctx, `Invalid action type='${type}', name='${name}'`);
|
||||
}
|
||||
|
||||
return actionObj;
|
||||
@ -80,7 +80,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
try {
|
||||
return bladeburner.startActionNetscriptFn(type, name, ctx.workerScript);
|
||||
} catch (e: unknown) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
||||
throw helpers.errorMessage(ctx, String(e));
|
||||
}
|
||||
},
|
||||
stopBladeburnerAction: (ctx) => () => {
|
||||
@ -105,7 +105,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
return time;
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
||||
throw helpers.errorMessage(ctx, String(e));
|
||||
}
|
||||
},
|
||||
getActionCurrentTime: (ctx) => () => {
|
||||
@ -116,7 +116,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
1000;
|
||||
return timecomputed;
|
||||
} catch (e: unknown) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
||||
throw helpers.errorMessage(ctx, String(e));
|
||||
}
|
||||
},
|
||||
getActionEstimatedSuccessChance: (ctx) => (_type, _name) => {
|
||||
@ -133,7 +133,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
return chance;
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
||||
throw helpers.errorMessage(ctx, String(e));
|
||||
}
|
||||
},
|
||||
getActionRepGain: (ctx) => (_type, _name, _level) => {
|
||||
@ -152,7 +152,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
try {
|
||||
return bladeburner.getActionCountRemainingNetscriptFn(type, name, ctx.workerScript);
|
||||
} catch (e: unknown) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
||||
throw helpers.errorMessage(ctx, String(e));
|
||||
}
|
||||
},
|
||||
getActionMaxLevel: (ctx) => (_type, _name) => {
|
||||
@ -202,7 +202,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
checkBladeburnerAccess(ctx);
|
||||
const action = getBladeburnerActionObject(ctx, type, name);
|
||||
if (level < 1 || level > action.maxLevel) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Level must be between 1 and ${action.maxLevel}, is ${level}`);
|
||||
throw helpers.errorMessage(ctx, `Level must be between 1 and ${action.maxLevel}, is ${level}`);
|
||||
}
|
||||
action.level = level;
|
||||
},
|
||||
@ -220,7 +220,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
try {
|
||||
return bladeburner.getSkillLevelNetscriptFn(skillName, ctx.workerScript);
|
||||
} catch (e: unknown) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
||||
throw helpers.errorMessage(ctx, String(e));
|
||||
}
|
||||
},
|
||||
getSkillUpgradeCost:
|
||||
@ -232,7 +232,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
try {
|
||||
return bladeburner.getSkillUpgradeCostNetscriptFn(skillName, count, ctx.workerScript);
|
||||
} catch (e: unknown) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
||||
throw helpers.errorMessage(ctx, String(e));
|
||||
}
|
||||
},
|
||||
upgradeSkill:
|
||||
@ -244,7 +244,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
try {
|
||||
return bladeburner.upgradeSkillNetscriptFn(skillName, count, ctx.workerScript);
|
||||
} catch (e: unknown) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
||||
throw helpers.errorMessage(ctx, String(e));
|
||||
}
|
||||
},
|
||||
getTeamSize: (ctx) => (_type, _name) => {
|
||||
@ -254,7 +254,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
try {
|
||||
return bladeburner.getTeamSizeNetscriptFn(type, name, ctx.workerScript);
|
||||
} catch (e: unknown) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
||||
throw helpers.errorMessage(ctx, String(e));
|
||||
}
|
||||
},
|
||||
setTeamSize: (ctx) => (_type, _name, _size) => {
|
||||
@ -265,7 +265,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
||||
try {
|
||||
return bladeburner.setTeamSizeNetscriptFn(type, name, size, ctx.workerScript);
|
||||
} catch (e: unknown) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
||||
throw helpers.errorMessage(ctx, String(e));
|
||||
}
|
||||
},
|
||||
getCityEstimatedPopulation: (ctx) => (_cityName) => {
|
||||
|
@ -11,7 +11,7 @@ export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
|
||||
const server = helpers.getServer(ctx, hostname);
|
||||
const contract = server.getContract(filename);
|
||||
if (contract == null) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Cannot find contract '${filename}' on server '${hostname}'`);
|
||||
throw helpers.errorMessage(ctx, `Cannot find contract '${filename}' on server '${hostname}'`);
|
||||
}
|
||||
|
||||
return contract;
|
||||
|
@ -178,10 +178,10 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
}
|
||||
|
||||
function checkAccess(ctx: NetscriptContext, api?: CorpUnlockName): void {
|
||||
if (!player.corporation) throw helpers.makeRuntimeErrorMsg(ctx, "Must own a corporation.");
|
||||
if (!player.corporation) throw helpers.errorMessage(ctx, "Must own a corporation.");
|
||||
if (!api) return;
|
||||
if (!player.corporation.unlocks.has(api)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You do not have access to this API.");
|
||||
throw helpers.errorMessage(ctx, "You do not have access to this API.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -216,7 +216,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
const cityName = getEnumHelper("CityName").nsGetMember(ctx, _cityName);
|
||||
const amt = helpers.number(ctx, "amount", _amt);
|
||||
if (amt < 1) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You must provide a positive number");
|
||||
throw helpers.errorMessage(ctx, "You must provide a positive number");
|
||||
}
|
||||
const warehouse = getWarehouse(divisionName, cityName);
|
||||
return UpgradeWarehouseCost(warehouse, amt);
|
||||
@ -306,7 +306,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
const amt = helpers.number(ctx, "amount", _amt);
|
||||
const corporation = getCorporation();
|
||||
if (amt < 1) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You must provide a positive number");
|
||||
throw helpers.errorMessage(ctx, "You must provide a positive number");
|
||||
}
|
||||
UpgradeWarehouse(corporation, getDivision(divisionName), getWarehouse(divisionName, cityName), amt);
|
||||
},
|
||||
@ -346,7 +346,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
const enabled = !!_enabled;
|
||||
const warehouse = getWarehouse(divisionName, cityName);
|
||||
if (!hasUnlock(CorpUnlockName.SmartSupply))
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `You have not purchased the Smart Supply upgrade!`);
|
||||
throw helpers.errorMessage(ctx, `You have not purchased the Smart Supply upgrade!`);
|
||||
SetSmartSupply(warehouse, enabled);
|
||||
},
|
||||
setSmartSupplyOption: (ctx) => (_divisionName, _cityName, _materialName, _option) => {
|
||||
@ -358,14 +358,14 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
const material = getMaterial(divisionName, cityName, materialName);
|
||||
const option = getEnumHelper("SmartSupplyOption").nsGetMember(ctx, _option);
|
||||
if (!hasUnlock(CorpUnlockName.SmartSupply))
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `You have not purchased the Smart Supply upgrade!`);
|
||||
throw helpers.errorMessage(ctx, `You have not purchased the Smart Supply upgrade!`);
|
||||
SetSmartSupplyOption(warehouse, material, option);
|
||||
},
|
||||
buyMaterial: (ctx) => (_divisionName, _cityName, _materialName, _amt) => {
|
||||
checkAccess(ctx, CorpUnlockName.WarehouseAPI);
|
||||
const divisionName = helpers.string(ctx, "divisionName", _divisionName);
|
||||
const division = getCorporation().divisions.get(divisionName);
|
||||
if (!division) throw helpers.makeRuntimeErrorMsg(ctx, `No division with provided name ${divisionName}`);
|
||||
if (!division) throw helpers.errorMessage(ctx, `No division with provided name ${divisionName}`);
|
||||
const cityName = getEnumHelper("CityName").nsGetMember(ctx, _cityName);
|
||||
const materialName = getEnumHelper("CorpMaterialName").nsGetMember(ctx, _materialName, "materialName");
|
||||
const amt = helpers.number(ctx, "amt", _amt);
|
||||
@ -378,7 +378,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
checkAccess(ctx, CorpUnlockName.WarehouseAPI);
|
||||
const divisionName = helpers.string(ctx, "divisionName", _divisionName);
|
||||
const division = getCorporation().divisions.get(divisionName);
|
||||
if (!division) throw helpers.makeRuntimeErrorMsg(ctx, `No division with provided name ${divisionName}`);
|
||||
if (!division) throw helpers.errorMessage(ctx, `No division with provided name ${divisionName}`);
|
||||
const corporation = getCorporation();
|
||||
const cityName = getEnumHelper("CityName").nsGetMember(ctx, _cityName);
|
||||
const materialName = getEnumHelper("CorpMaterialName").nsGetMember(ctx, _materialName, "materialName");
|
||||
@ -446,7 +446,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
const materialName = getEnumHelper("CorpMaterialName").nsGetMember(ctx, _materialName, "materialName");
|
||||
const on = !!_on;
|
||||
if (!getDivision(divisionName).hasResearch("Market-TA.I"))
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `You have not researched MarketTA.I for division: ${divisionName}`);
|
||||
throw helpers.errorMessage(ctx, `You have not researched MarketTA.I for division: ${divisionName}`);
|
||||
SetMaterialMarketTA1(getMaterial(divisionName, cityName, materialName), on);
|
||||
},
|
||||
setMaterialMarketTA2: (ctx) => (_divisionName, _cityName, _materialName, _on) => {
|
||||
@ -456,7 +456,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
const materialName = getEnumHelper("CorpMaterialName").nsGetMember(ctx, _materialName, "materialName");
|
||||
const on = !!_on;
|
||||
if (!getDivision(divisionName).hasResearch("Market-TA.II"))
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `You have not researched MarketTA.II for division: ${divisionName}`);
|
||||
throw helpers.errorMessage(ctx, `You have not researched MarketTA.II for division: ${divisionName}`);
|
||||
SetMaterialMarketTA2(getMaterial(divisionName, cityName, materialName), on);
|
||||
},
|
||||
setProductMarketTA1: (ctx) => (_divisionName, _productName, _on) => {
|
||||
@ -465,7 +465,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
const productName = helpers.string(ctx, "productName", _productName);
|
||||
const on = !!_on;
|
||||
if (!getDivision(divisionName).hasResearch("Market-TA.I"))
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `You have not researched MarketTA.I for division: ${divisionName}`);
|
||||
throw helpers.errorMessage(ctx, `You have not researched MarketTA.I for division: ${divisionName}`);
|
||||
SetProductMarketTA1(getProduct(divisionName, productName), on);
|
||||
},
|
||||
setProductMarketTA2: (ctx) => (_divisionName, _productName, _on) => {
|
||||
@ -474,7 +474,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
const productName = helpers.string(ctx, "productName", _productName);
|
||||
const on = !!_on;
|
||||
if (!getDivision(divisionName).hasResearch("Market-TA.II"))
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `You have not researched MarketTA.II for division: ${divisionName}`);
|
||||
throw helpers.errorMessage(ctx, `You have not researched MarketTA.II for division: ${divisionName}`);
|
||||
SetProductMarketTA2(getProduct(divisionName, productName), on);
|
||||
},
|
||||
};
|
||||
@ -528,7 +528,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
|
||||
if (job === CorpEmployeeJob.Unassigned) return false;
|
||||
if (amount < 0 || !Number.isInteger(amount))
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`Invalid value for amount! Must be an integer and greater than or be 0". Amount:'${amount}'`,
|
||||
);
|
||||
@ -538,7 +538,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
const totalNewEmployees = amount - office.employeeNextJobs[job];
|
||||
|
||||
if (office.employeeNextJobs[CorpEmployeeJob.Unassigned] < totalNewEmployees)
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`Unable to bring '${job} employees to ${amount}. Requires ${totalNewEmployees} unassigned employees`,
|
||||
);
|
||||
@ -678,7 +678,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
if (rate < 0 || rate > max)
|
||||
throw new Error(`Invalid value for rate field! Must be numeric, greater than 0, and less than ${max}`);
|
||||
const corporation = getCorporation();
|
||||
if (!corporation.public) throw helpers.makeRuntimeErrorMsg(ctx, `Your company has not gone public!`);
|
||||
if (!corporation.public) throw helpers.errorMessage(ctx, `Your company has not gone public!`);
|
||||
IssueDividends(corporation, rate);
|
||||
},
|
||||
issueNewShares: (ctx) => (_amount) => {
|
||||
@ -773,7 +773,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
||||
goPublic: (ctx) => (_numShares) => {
|
||||
checkAccess(ctx);
|
||||
const corporation = getCorporation();
|
||||
if (corporation.public) throw helpers.makeRuntimeErrorMsg(ctx, "corporation is already public");
|
||||
if (corporation.public) throw helpers.errorMessage(ctx, "corporation is already public");
|
||||
const numShares = helpers.number(ctx, "numShares", _numShares);
|
||||
GoPublic(corporation, numShares);
|
||||
return true;
|
||||
|
@ -63,7 +63,7 @@ import { findCrime } from "../Crime/CrimeHelpers";
|
||||
export function NetscriptFormulas(): InternalAPI<IFormulas> {
|
||||
const checkFormulasAccess = function (ctx: NetscriptContext): void {
|
||||
if (!player.hasProgram(CompletedProgramName.formulas)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Requires Formulas.exe to run.`);
|
||||
throw helpers.errorMessage(ctx, `Requires Formulas.exe to run.`);
|
||||
}
|
||||
};
|
||||
const formulasFunctions: InternalAPI<IFormulas> = {
|
||||
@ -316,7 +316,7 @@ export function NetscriptFormulas(): InternalAPI<IFormulas> {
|
||||
checkFormulasAccess(ctx);
|
||||
const upg = player.hashManager.getUpgrade(upgName);
|
||||
if (!upg) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid Hash Upgrade: ${upgName}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid Hash Upgrade: ${upgName}`);
|
||||
}
|
||||
return upg.getCost(level);
|
||||
},
|
||||
|
@ -17,20 +17,20 @@ import { getEnumHelper } from "../utils/EnumHelper";
|
||||
export function NetscriptGang(): InternalAPI<IGang> {
|
||||
/** Functions as an API check and also returns the gang object */
|
||||
const getGang = function (ctx: NetscriptContext): Gang {
|
||||
if (!Player.gang) throw helpers.makeRuntimeErrorMsg(ctx, "Must have joined gang", "API ACCESS");
|
||||
if (!Player.gang) throw helpers.errorMessage(ctx, "Must have joined gang", "API ACCESS");
|
||||
return Player.gang;
|
||||
};
|
||||
|
||||
const getGangMember = function (ctx: NetscriptContext, name: string): GangMember {
|
||||
const gang = getGang(ctx);
|
||||
for (const member of gang.members) if (member.name === name) return member;
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid gang member: '${name}'`);
|
||||
throw helpers.errorMessage(ctx, `Invalid gang member: '${name}'`);
|
||||
};
|
||||
|
||||
const getGangTask = function (ctx: NetscriptContext, name: string): GangMemberTask {
|
||||
const task = GangMemberTasks[name];
|
||||
if (!task) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid task: '${name}'`);
|
||||
throw helpers.errorMessage(ctx, `Invalid task: '${name}'`);
|
||||
}
|
||||
|
||||
return task;
|
||||
@ -60,13 +60,13 @@ export function NetscriptGang(): InternalAPI<IGang> {
|
||||
const newName = helpers.string(ctx, "newName", _newName);
|
||||
const member = gang.members.find((m) => m.name === memberName);
|
||||
if (!memberName) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid memberName: "" (empty string)`);
|
||||
throw helpers.errorMessage(ctx, `Invalid memberName: "" (empty string)`);
|
||||
}
|
||||
if (!newName) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid newName: "" (empty string)`);
|
||||
throw helpers.errorMessage(ctx, `Invalid newName: "" (empty string)`);
|
||||
}
|
||||
if (newName === memberName) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `newName and memberName must be different, but both were: ${newName}`);
|
||||
throw helpers.errorMessage(ctx, `newName and memberName must be different, but both were: ${newName}`);
|
||||
}
|
||||
if (!member) {
|
||||
helpers.log(ctx, () => `Failed to rename member: No member exists with memberName: ${memberName}`);
|
||||
@ -253,7 +253,7 @@ export function NetscriptGang(): InternalAPI<IGang> {
|
||||
getGang(ctx);
|
||||
const equipment = GangMemberUpgrades[equipName];
|
||||
if (!equipment) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid equipment: ${equipName}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid equipment: ${equipName}`);
|
||||
}
|
||||
const typecheck: EquipmentStats = equipment.mults;
|
||||
return Object.assign({}, typecheck);
|
||||
@ -328,7 +328,7 @@ export function NetscriptGang(): InternalAPI<IGang> {
|
||||
const otherGang = helpers.string(ctx, "otherGang", _otherGang);
|
||||
const gang = getGang(ctx);
|
||||
if (AllGangs[otherGang] == null) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid gang: ${otherGang}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid gang: ${otherGang}`);
|
||||
}
|
||||
|
||||
const playerPower = AllGangs[gang.facName].power;
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
|
||||
import type { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
|
||||
import type { Go as NSGo } from "@nsdefs";
|
||||
import type { Play } from "../Go/Types";
|
||||
|
||||
import { GoColor } from "@enums";
|
||||
import { Go } from "../Go/Go";
|
||||
import { helpers } from "../Netscript/NetscriptHelpers";
|
||||
import { Player } from "@player";
|
||||
import { Go } from "@nsdefs";
|
||||
import { Play, playerColors } from "../Go/boardState/goConstants";
|
||||
import { getSimplifiedBoardState } from "../Go/boardAnalysis/boardAnalysis";
|
||||
import { simpleBoardFromBoard } from "../Go/boardAnalysis/boardAnalysis";
|
||||
import {
|
||||
cheatDestroyNode,
|
||||
cheatPlayTwoMoves,
|
||||
@ -21,6 +23,7 @@ import {
|
||||
resetBoardState,
|
||||
throwError,
|
||||
} from "../Go/effects/netscriptGoImplementation";
|
||||
import { getEnumHelper } from "../utils/EnumHelper";
|
||||
|
||||
const logger = (ctx: NetscriptContext) => (message: string) => helpers.log(ctx, () => message);
|
||||
const error = (ctx: NetscriptContext) => (message: string) => throwError(ctx.workerScript, message);
|
||||
@ -29,7 +32,7 @@ const error = (ctx: NetscriptContext) => (message: string) => throwError(ctx.wor
|
||||
* Ensures the given coordinates are valid for the current board size
|
||||
*/
|
||||
function validateRowAndColumn(ctx: NetscriptContext, x: number, y: number) {
|
||||
const boardSize = Player.go.boardState.board.length;
|
||||
const boardSize = Go.currentGame.board.length;
|
||||
|
||||
if (x < 0 || x >= boardSize) {
|
||||
throwError(
|
||||
@ -45,7 +48,7 @@ function validateRowAndColumn(ctx: NetscriptContext, x: number, y: number) {
|
||||
/**
|
||||
* Go API implementation
|
||||
*/
|
||||
export function NetscriptGo(): InternalAPI<Go> {
|
||||
export function NetscriptGo(): InternalAPI<NSGo> {
|
||||
return {
|
||||
makeMove:
|
||||
(ctx: NetscriptContext) =>
|
||||
@ -57,7 +60,7 @@ export function NetscriptGo(): InternalAPI<Go> {
|
||||
return makePlayerMove(logger(ctx), x, y);
|
||||
},
|
||||
passTurn: (ctx: NetscriptContext) => async (): Promise<Play> => {
|
||||
if (Player.go.boardState.previousPlayer === playerColors.black) {
|
||||
if (Go.currentGame.previousPlayer === GoColor.black) {
|
||||
helpers.log(ctx, () => `It is not your turn; you cannot pass.`);
|
||||
helpers.log(ctx, () => `Do you have multiple scripts running, or did you forget to await makeMove() ?`);
|
||||
return Promise.resolve(invalidMoveResponse);
|
||||
@ -65,16 +68,16 @@ export function NetscriptGo(): InternalAPI<Go> {
|
||||
return handlePassTurn(logger(ctx));
|
||||
},
|
||||
getBoardState: () => () => {
|
||||
return getSimplifiedBoardState(Player.go.boardState.board);
|
||||
return simpleBoardFromBoard(Go.currentGame.board);
|
||||
},
|
||||
getOpponent: () => () => {
|
||||
return Player.go.boardState.ai;
|
||||
return Go.currentGame.ai;
|
||||
},
|
||||
resetBoardState: (ctx) => (_opponent, _boardSize) => {
|
||||
const opponentString = helpers.string(ctx, "opponent", _opponent);
|
||||
const opponent = getEnumHelper("GoOpponent").nsGetMember(ctx, _opponent);
|
||||
const boardSize = helpers.number(ctx, "boardSize", _boardSize);
|
||||
|
||||
return resetBoardState(error(ctx), opponentString, boardSize);
|
||||
return resetBoardState(error(ctx), opponent, boardSize);
|
||||
},
|
||||
analysis: {
|
||||
getValidMoves: () => () => {
|
||||
@ -93,7 +96,7 @@ export function NetscriptGo(): InternalAPI<Go> {
|
||||
cheat: {
|
||||
getCheatSuccessChance: (ctx: NetscriptContext) => () => {
|
||||
checkCheatApiAccess(error(ctx));
|
||||
return cheatSuccessChance(Player.go.boardState.cheatCount);
|
||||
return cheatSuccessChance(Go.currentGame.cheatCount);
|
||||
},
|
||||
removeRouter:
|
||||
(ctx: NetscriptContext) =>
|
||||
|
@ -16,7 +16,7 @@ import { getEnumHelper } from "../utils/EnumHelper";
|
||||
export function NetscriptGrafting(): InternalAPI<IGrafting> {
|
||||
const checkGraftingAPIAccess = (ctx: NetscriptContext): void => {
|
||||
if (!Player.canAccessGrafting()) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
"You do not currently have access to the Grafting API. This is either because you are not in BitNode 10 or because you do not have Source-File 10",
|
||||
);
|
||||
@ -30,7 +30,7 @@ export function NetscriptGrafting(): InternalAPI<IGrafting> {
|
||||
const augName = getEnumHelper("AugmentationName").nsGetMember(ctx, _augName);
|
||||
checkGraftingAPIAccess(ctx);
|
||||
if (!isValidGraftingAugName(augName)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid aug: ${augName}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid aug: ${augName}`);
|
||||
}
|
||||
const graftableAug = new GraftableAugmentation(Augmentations[augName]);
|
||||
return graftableAug.cost;
|
||||
@ -40,7 +40,7 @@ export function NetscriptGrafting(): InternalAPI<IGrafting> {
|
||||
const augName = getEnumHelper("AugmentationName").nsGetMember(ctx, _augName);
|
||||
checkGraftingAPIAccess(ctx);
|
||||
if (!isValidGraftingAugName(augName)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid aug: ${augName}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid aug: ${augName}`);
|
||||
}
|
||||
const graftableAug = new GraftableAugmentation(Augmentations[augName]);
|
||||
return calculateGraftingTimeWithBonus(graftableAug);
|
||||
@ -59,7 +59,7 @@ export function NetscriptGrafting(): InternalAPI<IGrafting> {
|
||||
const focus = !!_focus;
|
||||
checkGraftingAPIAccess(ctx);
|
||||
if (Player.city !== CityName.NewTokyo) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You must be in New Tokyo to begin grafting an Augmentation.");
|
||||
throw helpers.errorMessage(ctx, "You must be in New Tokyo to begin grafting an Augmentation.");
|
||||
}
|
||||
if (!isValidGraftingAugName(augName)) {
|
||||
helpers.log(ctx, () => `Invalid aug: ${augName}`);
|
||||
|
@ -26,7 +26,7 @@ export function NetscriptHacknet(): InternalAPI<IHacknet> {
|
||||
// Utility function to get Hacknet Node object
|
||||
const getHacknetNode = function (ctx: NetscriptContext, i: number): HacknetNode | HacknetServer {
|
||||
if (i < 0 || i >= player.hacknetNodes.length) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Index specified for Hacknet Node is out-of-bounds: " + i);
|
||||
throw helpers.errorMessage(ctx, "Index specified for Hacknet Node is out-of-bounds: " + i);
|
||||
}
|
||||
|
||||
if (hasHacknetServers()) {
|
||||
@ -35,7 +35,7 @@ export function NetscriptHacknet(): InternalAPI<IHacknet> {
|
||||
const hserver = GetServer(hi);
|
||||
if (!(hserver instanceof HacknetServer)) throw new Error("hacknet server was not actually hacknet server");
|
||||
if (hserver == null) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`Could not get Hacknet Server for index ${i}. This is probably a bug, please report to game dev`,
|
||||
);
|
||||
@ -217,7 +217,7 @@ export function NetscriptHacknet(): InternalAPI<IHacknet> {
|
||||
const upgName = helpers.string(ctx, "upgName", _upgName);
|
||||
const level = player.hashManager.upgrades[upgName];
|
||||
if (level === undefined) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid Hash Upgrade: ${upgName}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid Hash Upgrade: ${upgName}`);
|
||||
}
|
||||
return level;
|
||||
},
|
||||
|
@ -21,9 +21,9 @@ export function NetscriptInfiltration(): InternalAPI<NetscriptInfiltation> {
|
||||
|
||||
const calculateInfiltrationData = (ctx: NetscriptContext, locationName: LocationName): InfiltrationLocation => {
|
||||
const location = Locations[locationName];
|
||||
if (location === undefined) throw helpers.makeRuntimeErrorMsg(ctx, `Location '${location}' does not exists.`);
|
||||
if (location === undefined) throw helpers.errorMessage(ctx, `Location '${location}' does not exists.`);
|
||||
if (location.infiltrationData === undefined)
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Location '${location}' does not provide infiltrations.`);
|
||||
throw helpers.errorMessage(ctx, `Location '${location}' does not provide infiltrations.`);
|
||||
const startingSecurityLevel = location.infiltrationData.startingSecurityLevel;
|
||||
const difficulty = calculateDifficulty(startingSecurityLevel);
|
||||
const reward = calculateReward(startingSecurityLevel);
|
||||
|
@ -190,7 +190,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
const cbScript = _cbScript
|
||||
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
||||
: false;
|
||||
if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`);
|
||||
if (cbScript === null) throw helpers.errorMessage(ctx, `Could not resolve file path: ${_cbScript}`);
|
||||
|
||||
helpers.log(ctx, () => "Soft resetting. This will cause this script to be killed");
|
||||
installAugmentations(true);
|
||||
@ -201,7 +201,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
const cbScript = _cbScript
|
||||
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
||||
: false;
|
||||
if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`);
|
||||
if (cbScript === null) throw helpers.errorMessage(ctx, `Could not resolve file path: ${_cbScript}`);
|
||||
|
||||
if (Player.queuedAugmentations.length === 0) {
|
||||
helpers.log(ctx, () => "You do not have any Augmentations to be installed.");
|
||||
@ -409,7 +409,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain / 50000);
|
||||
return true;
|
||||
default:
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid city name: '${cityName}'.`);
|
||||
throw helpers.errorMessage(ctx, `Invalid city name: '${cityName}'.`);
|
||||
}
|
||||
},
|
||||
|
||||
@ -428,7 +428,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
Player.loseMoney(CONSTANTS.TorRouterCost, "other");
|
||||
|
||||
const darkweb = GetServer(SpecialServers.DarkWeb);
|
||||
if (!darkweb) throw helpers.makeRuntimeErrorMsg(ctx, "DarkWeb was not a server but should have been");
|
||||
if (!darkweb) throw helpers.errorMessage(ctx, "DarkWeb was not a server but should have been");
|
||||
|
||||
Player.getHomeComputer().serversOnNetwork.push(darkweb.hostname);
|
||||
darkweb.serversOnNetwork.push(Player.getHomeComputer().hostname);
|
||||
@ -483,12 +483,12 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
helpers.checkSingularityAccess(ctx);
|
||||
const hostname = helpers.string(ctx, "hostname", _hostname);
|
||||
if (!hostname) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid hostname: '${hostname}'`);
|
||||
throw helpers.errorMessage(ctx, `Invalid hostname: '${hostname}'`);
|
||||
}
|
||||
|
||||
const target = GetServer(hostname);
|
||||
if (target == null) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid hostname: '${hostname}'`);
|
||||
throw helpers.errorMessage(ctx, `Invalid hostname: '${hostname}'`);
|
||||
}
|
||||
|
||||
//Home case
|
||||
@ -545,7 +545,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
// No root access or skill level too low
|
||||
const canHack = netscriptCanHack(server);
|
||||
if (!canHack.res) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || "");
|
||||
throw helpers.errorMessage(ctx, canHack.msg || "");
|
||||
}
|
||||
|
||||
helpers.log(
|
||||
@ -573,7 +573,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
helpers.checkSingularityAccess(ctx);
|
||||
const focus = !!_focus;
|
||||
if (Player.currentWork === null) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Not currently working");
|
||||
throw helpers.errorMessage(ctx, "Not currently working");
|
||||
}
|
||||
|
||||
if (!Player.focus && focus) {
|
||||
@ -682,7 +682,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
const company = Companies[companyName];
|
||||
|
||||
if (!company.hasPosition(positionName)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Company '${companyName}' does not have position '${positionName}'`);
|
||||
throw helpers.errorMessage(ctx, `Company '${companyName}' does not have position '${positionName}'`);
|
||||
}
|
||||
|
||||
const job = CompanyPositions[positionName];
|
||||
@ -706,7 +706,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
const jobName = Player.jobs[companyName];
|
||||
// Make sure player is actually employed at the company
|
||||
if (!jobName) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `You do not have a job at: '${companyName}'`);
|
||||
throw helpers.errorMessage(ctx, `You do not have a job at: '${companyName}'`);
|
||||
}
|
||||
|
||||
const wasFocused = Player.focus;
|
||||
@ -1005,7 +1005,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
|
||||
// If input isn't a crimeType, use search using roughname.
|
||||
const crime = findCrime(crimeType);
|
||||
if (crime == null) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid crime: '${crimeType}'`);
|
||||
if (crime == null) throw helpers.errorMessage(ctx, `Invalid crime: '${crimeType}'`);
|
||||
|
||||
helpers.log(ctx, () => `Attempting to commit ${crime.type}...`);
|
||||
const crimeTime = crime.commit(1, ctx.workerScript);
|
||||
@ -1024,7 +1024,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
|
||||
// If input isn't a crimeType, use search using roughname.
|
||||
const crime = findCrime(crimeType);
|
||||
if (crime == null) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid crime: '${crimeType}'`);
|
||||
if (crime == null) throw helpers.errorMessage(ctx, `Invalid crime: '${crimeType}'`);
|
||||
|
||||
return crime.successRate(Player);
|
||||
},
|
||||
@ -1034,7 +1034,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
|
||||
// If input isn't a crimeType, use search using roughname.
|
||||
const crime = findCrime(crimeType);
|
||||
if (crime == null) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid crime: '${crimeType}'`);
|
||||
if (crime == null) throw helpers.errorMessage(ctx, `Invalid crime: '${crimeType}'`);
|
||||
|
||||
const crimeStatsWithMultipliers = calculateCrimeWorkStats(Player, crime);
|
||||
|
||||
@ -1079,7 +1079,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
// doesn't exist, it's the first time they've run the script. So throw an error to let them know
|
||||
// that they need to fix it.
|
||||
if (item == null) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`No such exploit ('${programName}') found on the darkweb! ` +
|
||||
`\nThis function is not case-sensitive. Did you perhaps forget .exe at the end?`,
|
||||
@ -1098,7 +1098,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
const cbScript = _cbScript
|
||||
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
||||
: false;
|
||||
if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`);
|
||||
if (cbScript === null) throw helpers.errorMessage(ctx, `Could not resolve file path: ${_cbScript}`);
|
||||
enterBitNode(true, Player.bitNodeN, nextBN);
|
||||
if (cbScript) setTimeout(() => runAfterReset(cbScript), 500);
|
||||
},
|
||||
@ -1111,7 +1111,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
const cbScript = _cbScript
|
||||
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
||||
: false;
|
||||
if (cbScript === null) throw helpers.makeRuntimeErrorMsg(ctx, `Could not resolve file path: ${_cbScript}`);
|
||||
if (cbScript === null) throw helpers.errorMessage(ctx, `Could not resolve file path: ${_cbScript}`);
|
||||
|
||||
const wd = GetServer(SpecialServers.WorldDaemon);
|
||||
if (!(wd instanceof Server)) throw new Error("WorldDaemon was not a normal server. This is a bug contact dev.");
|
||||
|
@ -16,7 +16,7 @@ import { Factions } from "../Faction/Factions";
|
||||
export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
|
||||
const checkSleeveAPIAccess = function (ctx: NetscriptContext) {
|
||||
if (Player.bitNodeN !== 10 && !Player.sourceFileLvl(10)) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
"You do not currently have access to the Sleeve API. This is either because you are not in BitNode-10 or because you do not have Source-File 10",
|
||||
);
|
||||
@ -27,7 +27,7 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
|
||||
if (sleeveNumber >= Player.sleeves.length || sleeveNumber < 0) {
|
||||
const msg = `Invalid sleeve number: ${sleeveNumber}`;
|
||||
helpers.log(ctx, () => msg);
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, msg);
|
||||
throw helpers.errorMessage(ctx, msg);
|
||||
}
|
||||
};
|
||||
|
||||
@ -91,7 +91,7 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
|
||||
}
|
||||
const other = Player.sleeves[i];
|
||||
if (isSleeveCompanyWork(other.currentWork) && other.currentWork.companyName === companyName) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`Sleeve ${sleeveNumber} cannot work for company ${companyName} because Sleeve ${i} is already working for them.`,
|
||||
);
|
||||
@ -108,7 +108,7 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
|
||||
checkSleeveNumber(ctx, sleeveNumber);
|
||||
|
||||
if (!Factions[factionName].isMember) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Cannot work for faction ${factionName} without being a member.`);
|
||||
throw helpers.errorMessage(ctx, `Cannot work for faction ${factionName} without being a member.`);
|
||||
}
|
||||
|
||||
// Cannot work at the same faction that another sleeve is working at
|
||||
@ -118,7 +118,7 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
|
||||
}
|
||||
const other = Player.sleeves[i];
|
||||
if (isSleeveFactionWork(other.currentWork) && other.currentWork.factionName === factionName) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`Sleeve ${sleeveNumber} cannot work for faction ${factionName} because Sleeve ${i} is already working for them.`,
|
||||
);
|
||||
@ -126,7 +126,7 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
|
||||
}
|
||||
|
||||
if (Player.gang && Player.gang.facName == factionName) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`Sleeve ${sleeveNumber} cannot work for faction ${factionName} because you have started a gang with them.`,
|
||||
);
|
||||
@ -208,12 +208,12 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
|
||||
checkSleeveNumber(ctx, sleeveNumber);
|
||||
|
||||
if (Player.sleeves[sleeveNumber].shock > 0) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Sleeve shock too high: Sleeve ${sleeveNumber}`);
|
||||
throw helpers.errorMessage(ctx, `Sleeve shock too high: Sleeve ${sleeveNumber}`);
|
||||
}
|
||||
|
||||
const aug = Augmentations[augName];
|
||||
if (!aug) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid aug: ${augName}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid aug: ${augName}`);
|
||||
}
|
||||
|
||||
return Player.sleeves[sleeveNumber].tryBuyAugmentation(aug);
|
||||
@ -250,7 +250,7 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
|
||||
}
|
||||
const other = Player.sleeves[i];
|
||||
if (isSleeveBladeburnerWork(other.currentWork) && other.currentWork.actionName === contract) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`Sleeve ${sleeveNumber} cannot take on contracts because Sleeve ${i} is already performing that action.`,
|
||||
);
|
||||
|
@ -16,7 +16,7 @@ import { getCoreBonus } from "../Server/ServerHelpers";
|
||||
export function NetscriptStanek(): InternalAPI<IStanek> {
|
||||
function checkStanekAPIAccess(ctx: NetscriptContext): void {
|
||||
if (!Player.hasAugmentation(AugmentationName.StaneksGift1, true)) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "Stanek's Gift is not installed");
|
||||
throw helpers.errorMessage(ctx, "Stanek's Gift is not installed");
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,9 +36,9 @@ export function NetscriptStanek(): InternalAPI<IStanek> {
|
||||
checkStanekAPIAccess(ctx);
|
||||
const fragment = staneksGift.findFragment(rootX, rootY);
|
||||
//Check whether the selected fragment can ge charged
|
||||
if (!fragment) throw helpers.makeRuntimeErrorMsg(ctx, `No fragment with root (${rootX}, ${rootY}).`);
|
||||
if (!fragment) throw helpers.errorMessage(ctx, `No fragment with root (${rootX}, ${rootY}).`);
|
||||
if (fragment.fragment().type == FragmentType.Booster) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw helpers.errorMessage(
|
||||
ctx,
|
||||
`The fragment with root (${rootX}, ${rootY}) is a Booster Fragment and thus cannot be charged.`,
|
||||
);
|
||||
@ -79,7 +79,7 @@ export function NetscriptStanek(): InternalAPI<IStanek> {
|
||||
const fragmentId = helpers.number(ctx, "fragmentId", _fragmentId);
|
||||
checkStanekAPIAccess(ctx);
|
||||
const fragment = FragmentById(fragmentId);
|
||||
if (!fragment) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid fragment id: ${fragmentId}`);
|
||||
if (!fragment) throw helpers.errorMessage(ctx, `Invalid fragment id: ${fragmentId}`);
|
||||
const can = staneksGift.canPlace(rootX, rootY, rotation, fragment);
|
||||
return can;
|
||||
},
|
||||
@ -90,7 +90,7 @@ export function NetscriptStanek(): InternalAPI<IStanek> {
|
||||
const fragmentId = helpers.number(ctx, "fragmentId", _fragmentId);
|
||||
checkStanekAPIAccess(ctx);
|
||||
const fragment = FragmentById(fragmentId);
|
||||
if (!fragment) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid fragment id: ${fragmentId}`);
|
||||
if (!fragment) throw helpers.errorMessage(ctx, `Invalid fragment id: ${fragmentId}`);
|
||||
return staneksGift.place(rootX, rootY, rotation, fragment);
|
||||
},
|
||||
getFragment: (ctx) => (_rootX, _rootY) => {
|
||||
|
@ -26,17 +26,17 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
/** Checks if the player has TIX API access. Throws an error if the player does not */
|
||||
const checkTixApiAccess = function (ctx: NetscriptContext): void {
|
||||
if (!Player.hasWseAccount) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `You don't have WSE Access! Cannot use ${ctx.function}()`);
|
||||
throw helpers.errorMessage(ctx, `You don't have WSE Access! Cannot use ${ctx.function}()`);
|
||||
}
|
||||
if (!Player.hasTixApiAccess) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `You don't have TIX API Access! Cannot use ${ctx.function}()`);
|
||||
throw helpers.errorMessage(ctx, `You don't have TIX API Access! Cannot use ${ctx.function}()`);
|
||||
}
|
||||
};
|
||||
|
||||
const getStockFromSymbol = function (ctx: NetscriptContext, symbol: string): Stock {
|
||||
const stock = SymbolToStockMap[symbol];
|
||||
if (stock == null) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid stock symbol: '${symbol}'`);
|
||||
throw helpers.errorMessage(ctx, `Invalid stock symbol: '${symbol}'`);
|
||||
}
|
||||
|
||||
return stock;
|
||||
@ -85,7 +85,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
checkTixApiAccess(ctx);
|
||||
const stock = SymbolToStockMap[symbol];
|
||||
if (stock == null) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid stock symbol: ${symbol}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid stock symbol: ${symbol}`);
|
||||
}
|
||||
return [stock.playerShares, stock.playerAvgPx, stock.playerShortShares, stock.playerAvgShortPx];
|
||||
},
|
||||
@ -169,10 +169,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
checkTixApiAccess(ctx);
|
||||
if (Player.bitNodeN !== 8) {
|
||||
if (Player.sourceFileLvl(8) <= 1) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
ctx,
|
||||
"You must either be in BitNode-8 or you must have Source-File 8 Level 2.",
|
||||
);
|
||||
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 2.");
|
||||
}
|
||||
}
|
||||
const stock = getStockFromSymbol(ctx, symbol);
|
||||
@ -186,10 +183,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
checkTixApiAccess(ctx);
|
||||
if (Player.bitNodeN !== 8) {
|
||||
if (Player.sourceFileLvl(8) <= 1) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
ctx,
|
||||
"You must either be in BitNode-8 or you must have Source-File 8 Level 2.",
|
||||
);
|
||||
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 2.");
|
||||
}
|
||||
}
|
||||
const stock = getStockFromSymbol(ctx, symbol);
|
||||
@ -206,10 +200,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
checkTixApiAccess(ctx);
|
||||
if (Player.bitNodeN !== 8) {
|
||||
if (Player.sourceFileLvl(8) <= 2) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
ctx,
|
||||
"You must either be in BitNode-8 or you must have Source-File 8 Level 3.",
|
||||
);
|
||||
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 3.");
|
||||
}
|
||||
}
|
||||
const stock = getStockFromSymbol(ctx, symbol);
|
||||
@ -226,7 +217,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
} else if (ltype.includes("stop") && ltype.includes("sell")) {
|
||||
orderType = OrderType.StopSell;
|
||||
} else {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid order type: ${type}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid order type: ${type}`);
|
||||
}
|
||||
|
||||
const lpos = pos.toLowerCase();
|
||||
@ -235,7 +226,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
} else if (lpos.includes("s")) {
|
||||
orderPos = PositionType.Short;
|
||||
} else {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid position type: ${pos}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid position type: ${pos}`);
|
||||
}
|
||||
|
||||
return placeOrder(stock, shares, price, orderType, orderPos, ctx);
|
||||
@ -249,18 +240,12 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
checkTixApiAccess(ctx);
|
||||
if (Player.bitNodeN !== 8) {
|
||||
if (Player.sourceFileLvl(8) <= 2) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
ctx,
|
||||
"You must either be in BitNode-8 or you must have Source-File 8 Level 3.",
|
||||
);
|
||||
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 3.");
|
||||
}
|
||||
}
|
||||
const stock = getStockFromSymbol(ctx, symbol);
|
||||
if (isNaN(shares) || isNaN(price)) {
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
ctx,
|
||||
`Invalid shares or price. Must be numeric. shares=${shares}, price=${price}`,
|
||||
);
|
||||
throw helpers.errorMessage(ctx, `Invalid shares or price. Must be numeric. shares=${shares}, price=${price}`);
|
||||
}
|
||||
let orderType;
|
||||
let orderPos;
|
||||
@ -274,7 +259,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
} else if (ltype.includes("stop") && ltype.includes("sell")) {
|
||||
orderType = OrderType.StopSell;
|
||||
} else {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid order type: ${type}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid order type: ${type}`);
|
||||
}
|
||||
|
||||
const lpos = pos.toLowerCase();
|
||||
@ -283,7 +268,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
} else if (lpos.includes("s")) {
|
||||
orderPos = PositionType.Short;
|
||||
} else {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid position type: ${pos}`);
|
||||
throw helpers.errorMessage(ctx, `Invalid position type: ${pos}`);
|
||||
}
|
||||
const params = {
|
||||
stock: stock,
|
||||
@ -298,7 +283,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
checkTixApiAccess(ctx);
|
||||
if (Player.bitNodeN !== 8) {
|
||||
if (Player.sourceFileLvl(8) <= 2) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You must either be in BitNode-8 or have Source-File 8 Level 3.");
|
||||
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or have Source-File 8 Level 3.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -325,7 +310,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
getVolatility: (ctx) => (_symbol) => {
|
||||
const symbol = helpers.string(ctx, "symbol", _symbol);
|
||||
if (!Player.has4SDataTixApi) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You don't have 4S Market Data TIX API Access!");
|
||||
throw helpers.errorMessage(ctx, "You don't have 4S Market Data TIX API Access!");
|
||||
}
|
||||
const stock = getStockFromSymbol(ctx, symbol);
|
||||
|
||||
@ -334,7 +319,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
||||
getForecast: (ctx) => (_symbol) => {
|
||||
const symbol = helpers.string(ctx, "symbol", _symbol);
|
||||
if (!Player.has4SDataTixApi) {
|
||||
throw helpers.makeRuntimeErrorMsg(ctx, "You don't have 4S Market Data TIX API Access!");
|
||||
throw helpers.errorMessage(ctx, "You don't have 4S Market Data TIX API Access!");
|
||||
}
|
||||
const stock = getStockFromSymbol(ctx, symbol);
|
||||
|
||||
|
@ -7,7 +7,8 @@ import { CONSTANTS } from "../Constants";
|
||||
import { hash } from "../hash/hash";
|
||||
import { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
|
||||
import { Terminal } from "../../src/Terminal";
|
||||
import { helpers, makeRuntimeErrorMsg } from "../Netscript/NetscriptHelpers";
|
||||
import { helpers } from "../Netscript/NetscriptHelpers";
|
||||
import { errorMessage } from "../Netscript/ErrorMessages";
|
||||
|
||||
/** Will probably remove the below function in favor of a different approach to object type assertion.
|
||||
* This method cannot be used to handle optional properties. */
|
||||
@ -18,7 +19,7 @@ export function assertObjectType<T extends object>(
|
||||
desiredObject: T,
|
||||
): asserts obj is T {
|
||||
if (typeof obj !== "object" || obj === null) {
|
||||
throw makeRuntimeErrorMsg(
|
||||
throw errorMessage(
|
||||
ctx,
|
||||
`Type ${obj === null ? "null" : typeof obj} provided for ${name}. Must be an object.`,
|
||||
"TYPE",
|
||||
@ -26,15 +27,11 @@ export function assertObjectType<T extends object>(
|
||||
}
|
||||
for (const [key, val] of Object.entries(desiredObject)) {
|
||||
if (!Object.hasOwn(obj, key)) {
|
||||
throw makeRuntimeErrorMsg(
|
||||
ctx,
|
||||
`Object provided for argument ${name} is missing required property ${key}.`,
|
||||
"TYPE",
|
||||
);
|
||||
throw errorMessage(ctx, `Object provided for argument ${name} is missing required property ${key}.`, "TYPE");
|
||||
}
|
||||
const objVal = (obj as Record<string, unknown>)[key];
|
||||
if (typeof val !== typeof objVal) {
|
||||
throw makeRuntimeErrorMsg(
|
||||
throw errorMessage(
|
||||
ctx,
|
||||
`Incorrect type ${typeof objVal} provided for property ${key} on ${name} argument. Should be type ${typeof val}.`,
|
||||
"TYPE",
|
||||
|
@ -32,7 +32,8 @@ import { simple as walksimple } from "acorn-walk";
|
||||
import { parseCommand } from "./Terminal/Parser";
|
||||
import { Terminal } from "./Terminal";
|
||||
import { ScriptArg } from "@nsdefs";
|
||||
import { handleUnknownError, CompleteRunOptions, getRunningScriptsByArgs } from "./Netscript/NetscriptHelpers";
|
||||
import { CompleteRunOptions, getRunningScriptsByArgs } from "./Netscript/NetscriptHelpers";
|
||||
import { handleUnknownError } from "./Netscript/ErrorMessages";
|
||||
import { resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath";
|
||||
import { root } from "./Paths/Directory";
|
||||
|
||||
|
@ -28,7 +28,6 @@ import { CONSTANTS } from "../../Constants";
|
||||
import { Person } from "../Person";
|
||||
import { isMember } from "../../utils/EnumHelper";
|
||||
import { PartialRecord } from "../../Types/Record";
|
||||
import { getGoPlayerStartingState } from "../../Go/boardState/goConstants";
|
||||
|
||||
export class PlayerObject extends Person implements IPlayer {
|
||||
// Player-specific properties
|
||||
@ -37,7 +36,6 @@ export class PlayerObject extends Person implements IPlayer {
|
||||
gang: Gang | null = null;
|
||||
bladeburner: Bladeburner | null = null;
|
||||
currentServer = "";
|
||||
go = getGoPlayerStartingState();
|
||||
factions: FactionName[] = [];
|
||||
factionInvitations: FactionName[] = [];
|
||||
factionRumors = new JSONSet<FactionName>();
|
||||
|
@ -51,8 +51,6 @@ import { achievements } from "../../Achievements/Achievements";
|
||||
|
||||
import { isCompanyWork } from "../../Work/CompanyWork";
|
||||
import { isMember } from "../../utils/EnumHelper";
|
||||
import { getGoPlayerStartingState } from "../../Go/boardState/goConstants";
|
||||
import { resetGoNodePower } from "../../Go/effects/effect";
|
||||
|
||||
export function init(this: PlayerObject): void {
|
||||
/* Initialize Player's home computer */
|
||||
@ -114,8 +112,6 @@ export function prestigeAugmentation(this: PlayerObject): void {
|
||||
|
||||
this.sleeves.forEach((sleeve) => (sleeve.shock <= 0 ? sleeve.synchronize() : sleeve.shockRecovery()));
|
||||
|
||||
resetGoNodePower(this);
|
||||
|
||||
this.lastUpdate = new Date().getTime();
|
||||
|
||||
// Statistics Trackers
|
||||
@ -152,7 +148,6 @@ export function prestigeSourceFile(this: PlayerObject): void {
|
||||
resetGangs();
|
||||
this.corporation = null;
|
||||
this.bladeburner = null;
|
||||
this.go = getGoPlayerStartingState();
|
||||
|
||||
// Reset Stock market
|
||||
this.hasWseAccount = false;
|
||||
|
@ -24,6 +24,7 @@ import { InvitationsSeen } from "./Faction/ui/FactionsRoot";
|
||||
import { CONSTANTS } from "./Constants";
|
||||
import { LogBoxClearEvents } from "./ui/React/LogBoxManager";
|
||||
import { initCircadianModulator } from "./Augmentation/Augmentations";
|
||||
import { Go } from "./Go/Go";
|
||||
|
||||
const BitNode8StartingMoney = 250e6;
|
||||
function delayedDialog(message: string) {
|
||||
@ -46,6 +47,7 @@ export function prestigeAugmentation(): void {
|
||||
}
|
||||
|
||||
Player.prestigeAugmentation();
|
||||
Go.prestigeAugmentation();
|
||||
|
||||
// Delete all Worker Scripts objects
|
||||
prestigeWorkerScripts();
|
||||
@ -178,6 +180,8 @@ export function prestigeSourceFile(isFlume: boolean): void {
|
||||
initBitNodeMultipliers();
|
||||
|
||||
Player.prestigeSourceFile();
|
||||
Go.prestigeSourceFile();
|
||||
|
||||
prestigeWorkerScripts(); // Delete all Worker Scripts objects
|
||||
|
||||
const homeComp = Player.getHomeComputer();
|
||||
|
@ -38,6 +38,7 @@ import { Corporation } from "./Corporation/Corporation";
|
||||
import { Terminal } from "./Terminal";
|
||||
import { getRecordValues } from "./Types/Record";
|
||||
import { ExportMaterial } from "./Corporation/Actions";
|
||||
import { getGoSave, loadGo } from "./Go/SaveLoad";
|
||||
|
||||
/* SaveObject.js
|
||||
* Defines the object used to save/load games
|
||||
@ -86,6 +87,7 @@ class BitburnerSaveObject {
|
||||
AllGangsSave = "";
|
||||
LastExportBonus = "0";
|
||||
StaneksGiftSave = "";
|
||||
GoSave = "";
|
||||
|
||||
getSaveString(forceExcludeRunningScripts = false): string {
|
||||
this.PlayerSave = JSON.stringify(Player);
|
||||
@ -105,6 +107,7 @@ class BitburnerSaveObject {
|
||||
this.VersionSave = JSON.stringify(CONSTANTS.VersionNumber);
|
||||
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
|
||||
this.StaneksGiftSave = JSON.stringify(staneksGift);
|
||||
this.GoSave = JSON.stringify(getGoSave());
|
||||
|
||||
if (Player.gang) this.AllGangsSave = JSON.stringify(AllGangs);
|
||||
|
||||
@ -704,6 +707,18 @@ Error: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
v2_60: if (ver < 38 && "go" in Player) {
|
||||
const goData = Player.go;
|
||||
// Remove outdated savedata
|
||||
delete Player.go;
|
||||
// Attempt to load back in at least the stats object. The current game will not be loaded.
|
||||
if (!goData || typeof goData !== "object") break v2_60;
|
||||
const stats = "status" in goData ? goData.status : "stats" in goData ? goData.stats : null;
|
||||
if (!stats || typeof stats !== "object") break v2_60;
|
||||
const freshSaveData = getGoSave();
|
||||
Object.assign(freshSaveData.stats, stats);
|
||||
loadGo(JSON.stringify(freshSaveData));
|
||||
}
|
||||
}
|
||||
|
||||
function loadGame(saveString: string): boolean {
|
||||
@ -717,6 +732,7 @@ function loadGame(saveString: string): boolean {
|
||||
loadAllServers(saveObj.AllServersSave);
|
||||
loadCompanies(saveObj.CompaniesSave);
|
||||
loadFactions(saveObj.FactionsSave, Player);
|
||||
loadGo(saveObj.GoSave);
|
||||
|
||||
if (Object.hasOwn(saveObj, "StaneksGiftSave")) {
|
||||
loadStaneksGift(saveObj.StaneksGiftSave);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { handleUnknownError } from "./Netscript/NetscriptHelpers";
|
||||
import { handleUnknownError } from "./Netscript/ErrorMessages";
|
||||
|
||||
export function setupUncaughtPromiseHandler(): void {
|
||||
window.addEventListener("unhandledrejection", (e) => {
|
||||
|
@ -71,7 +71,7 @@ import { V2Modal } from "../utils/V2Modal";
|
||||
import { MathJaxContext } from "better-react-mathjax";
|
||||
import { useRerender } from "./React/hooks";
|
||||
import { HistoryProvider } from "./React/Documentation";
|
||||
import { GoRoot } from "../Go/GoRoot";
|
||||
import { GoRoot } from "../Go/ui/GoRoot";
|
||||
|
||||
const htmlLocation = location;
|
||||
|
||||
|
@ -2,7 +2,8 @@ import type { Member } from "../types";
|
||||
import type { NetscriptContext } from "../Netscript/APIWrapper";
|
||||
|
||||
import * as allEnums from "../Enums";
|
||||
import { assertString, helpers } from "../Netscript/NetscriptHelpers";
|
||||
import { assertString } from "../Netscript/TypeAssertion";
|
||||
import { errorMessage } from "../Netscript/ErrorMessages";
|
||||
import { getRandomInt } from "./helpers/getRandomInt";
|
||||
|
||||
interface GetMemberOptions {
|
||||
@ -50,7 +51,7 @@ class EnumHelper<EnumObj extends object, EnumMember extends Member<EnumObj> & st
|
||||
);
|
||||
allowableValues = `See the developer console for allowable values.`;
|
||||
}
|
||||
throw helpers.makeRuntimeErrorMsg(
|
||||
throw errorMessage(
|
||||
ctx,
|
||||
`Argument ${argName} should be a ${this.name} enum member.\nProvided value: "${toValidate}".\n${allowableValues}`,
|
||||
);
|
||||
|
@ -13,7 +13,7 @@ describe("Check Save File Continuity", () => {
|
||||
// Calling getSaveString forces save info to update
|
||||
saveObject.getSaveString();
|
||||
|
||||
const savesToTest = ["FactionsSave", "PlayerSave", "CompaniesSave"] as const;
|
||||
const savesToTest = ["FactionsSave", "PlayerSave", "CompaniesSave", "GoSave"] as const;
|
||||
for (const saveToTest of savesToTest) {
|
||||
test(`${saveToTest} continuity`, () => {
|
||||
const parsed = JSON.parse(saveObject[saveToTest]);
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { getBoardFromSimplifiedBoardState, getSimplifiedBoardState } from "../../../src/Go/boardAnalysis/boardAnalysis";
|
||||
import { setPlayer } from "@player";
|
||||
import { GoOpponent, GoColor, GoPlayType } from "@enums";
|
||||
import { Go } from "../../../src/Go/Go";
|
||||
import { boardStateFromSimpleBoard, simpleBoardFromBoard } from "../../../src/Go/boardAnalysis/boardAnalysis";
|
||||
import {
|
||||
cheatPlayTwoMoves,
|
||||
cheatRemoveRouter,
|
||||
@ -12,10 +15,8 @@ import {
|
||||
makePlayerMove,
|
||||
resetBoardState,
|
||||
} from "../../../src/Go/effects/netscriptGoImplementation";
|
||||
import { Player, setPlayer } from "@player";
|
||||
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
|
||||
import "../../../src/Faction/Factions";
|
||||
import { opponents, playerColors, playTypes } from "../../../src/Go/boardState/goConstants";
|
||||
import { getNewBoardState } from "../../../src/Go/boardState/boardState";
|
||||
|
||||
jest.mock("../../../src/Faction/Factions", () => ({
|
||||
@ -28,7 +29,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
describe("makeMove() tests", () => {
|
||||
it("should handle invalid moves", async () => {
|
||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await makePlayerMove(mockLogger, 0, 0);
|
||||
@ -39,35 +40,36 @@ describe("Netscript Go API unit tests", () => {
|
||||
|
||||
it("should update the board with valid player moves", async () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "....."];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
Go.currentGame = boardState;
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await makePlayerMove(mockLogger, 1, 0);
|
||||
|
||||
expect(result.success).toEqual(true);
|
||||
expect(mockLogger).toHaveBeenCalledWith("Go move played: 1, 0");
|
||||
expect(Player.go.boardState.board[1]?.[0]?.player).toEqual(playerColors.black);
|
||||
expect(Player.go.boardState.board[0]?.[0]?.player).toEqual(playerColors.empty);
|
||||
expect(boardState.board[1]?.[0]?.color).toEqual(GoColor.black);
|
||||
expect(boardState.board[0]?.[0]?.color).toEqual(GoColor.empty);
|
||||
});
|
||||
});
|
||||
describe("passTurn() tests", () => {
|
||||
it("should handle pass attempts", async () => {
|
||||
Player.go.boardState = getNewBoardState(7);
|
||||
Go.currentGame = getNewBoardState(7);
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await handlePassTurn(mockLogger);
|
||||
|
||||
expect(result.success).toEqual(true);
|
||||
expect(result.type).toEqual(playTypes.move);
|
||||
expect(result.type).toEqual(GoPlayType.move);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBoardState() tests", () => {
|
||||
it("should correctly return a string version of the bard state", () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
||||
const boardState = getBoardFromSimplifiedBoardState(board);
|
||||
const boardState = boardStateFromSimpleBoard(board);
|
||||
|
||||
const result = getSimplifiedBoardState(boardState.board);
|
||||
const result = simpleBoardFromBoard(boardState.board);
|
||||
|
||||
expect(result).toEqual(board);
|
||||
});
|
||||
@ -76,18 +78,19 @@ describe("Netscript Go API unit tests", () => {
|
||||
describe("resetBoardState() tests", () => {
|
||||
it("should set the player's board to the requested size and opponent", () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board);
|
||||
const mockError = jest.fn();
|
||||
|
||||
const newBoard = resetBoardState(mockError, opponents.SlumSnakes, 9);
|
||||
const newBoard = resetBoardState(mockError, GoOpponent.SlumSnakes, 9);
|
||||
|
||||
expect(newBoard?.[0].length).toEqual(9);
|
||||
expect(Player.go.boardState.board.length).toEqual(9);
|
||||
expect(Player.go.boardState.ai).toEqual(opponents.SlumSnakes);
|
||||
expect(Go.currentGame.board.length).toEqual(9);
|
||||
expect(Go.currentGame.ai).toEqual(GoOpponent.SlumSnakes);
|
||||
});
|
||||
/* This typechecking is now done prior to calling resetBoardState (it's checked in the ns function via getEnumHelper("GoOpponent".nsGetMember()))
|
||||
it("should throw an error if an invalid opponent is requested", () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board);
|
||||
Go.boardState = getBoardFromSimplifiedBoardState(board);
|
||||
const mockError = jest.fn();
|
||||
|
||||
resetBoardState(mockError, "fake opponent", 9);
|
||||
@ -96,13 +99,13 @@ describe("Netscript Go API unit tests", () => {
|
||||
"Invalid opponent requested (fake opponent), valid options are Netburners, Slum Snakes, The Black Hand, Tetrads, Daedalus, Illuminati",
|
||||
);
|
||||
});
|
||||
|
||||
*/
|
||||
it("should throw an error if an invalid size is requested", () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board);
|
||||
const mockError = jest.fn();
|
||||
|
||||
resetBoardState(mockError, opponents.TheBlackHand, 31337);
|
||||
resetBoardState(mockError, GoOpponent.TheBlackHand, 31337);
|
||||
|
||||
expect(mockError).toHaveBeenCalledWith("Invalid subnet size requested (31337, size must be 5, 7, 9, or 13");
|
||||
});
|
||||
@ -111,7 +114,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
describe("getValidMoves() unit tests", () => {
|
||||
it("should return all valid and invalid moves on the board", () => {
|
||||
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
|
||||
const result = getValidMoves();
|
||||
|
||||
@ -128,7 +131,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
describe("getChains() unit tests", () => {
|
||||
it("should assign an ID to all contiguous chains on the board", () => {
|
||||
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
|
||||
const result = getChains();
|
||||
|
||||
@ -142,7 +145,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
describe("getLiberties() unit tests", () => {
|
||||
it("should display the number of connected empty nodes for each chain on the board", () => {
|
||||
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
|
||||
const result = getLiberties();
|
||||
|
||||
@ -158,7 +161,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
describe("getControlledEmptyNodes() unit tests", () => {
|
||||
it("should show the owner of each empty node, if a single player has fully encircled it", () => {
|
||||
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
|
||||
const result = getControlledEmptyNodes();
|
||||
|
||||
@ -168,7 +171,7 @@ describe("Netscript Go API unit tests", () => {
|
||||
describe("cheatPlayTwoMoves() tests", () => {
|
||||
it("should handle invalid moves", async () => {
|
||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await cheatPlayTwoMoves(mockLogger, 0, 0, 1, 0, 0, 0);
|
||||
@ -179,48 +182,48 @@ describe("Netscript Go API unit tests", () => {
|
||||
|
||||
it("should update the board with both player moves if nodes are unoccupied and cheat is successful", async () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 0, 0);
|
||||
expect(mockLogger).toHaveBeenCalledWith("Cheat successful. Two go moves played: 4,3 and 3,4");
|
||||
expect(result.success).toEqual(true);
|
||||
expect(Player.go.boardState.board[4]?.[3]?.player).toEqual(playerColors.black);
|
||||
expect(Player.go.boardState.board[3]?.[4]?.player).toEqual(playerColors.black);
|
||||
expect(Player.go.boardState.board[4]?.[4]?.player).toEqual(playerColors.empty);
|
||||
expect(Go.currentGame.board[4]?.[3]?.color).toEqual(GoColor.black);
|
||||
expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.black);
|
||||
expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.empty);
|
||||
});
|
||||
|
||||
it("should pass player turn to AI if the cheat is unsuccessful but player is not ejected", async () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 2, 1);
|
||||
console.log(result);
|
||||
expect(mockLogger).toHaveBeenCalledWith("Cheat failed. Your turn has been skipped.");
|
||||
expect(result.success).toEqual(false);
|
||||
expect(Player.go.boardState.board[4]?.[3]?.player).toEqual(playerColors.empty);
|
||||
expect(Player.go.boardState.board[3]?.[4]?.player).toEqual(playerColors.empty);
|
||||
expect(Player.go.boardState.board[4]?.[4]?.player).toEqual(playerColors.white);
|
||||
expect(Go.currentGame.board[4]?.[3]?.color).toEqual(GoColor.empty);
|
||||
expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.empty);
|
||||
expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.white);
|
||||
});
|
||||
|
||||
it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Player.go.boardState.cheatCount = 1;
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
Go.currentGame.cheatCount = 1;
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 1, 0);
|
||||
console.log(result);
|
||||
expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
|
||||
expect(result.success).toEqual(false);
|
||||
expect(Player.go.boardState.history.length).toEqual(0);
|
||||
expect(Go.currentGame.previousBoard).toEqual(null);
|
||||
});
|
||||
});
|
||||
describe("cheatRemoveRouter() tests", () => {
|
||||
it("should handle invalid moves", async () => {
|
||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await cheatRemoveRouter(mockLogger, 1, 0, 0, 0);
|
||||
@ -233,33 +236,33 @@ describe("Netscript Go API unit tests", () => {
|
||||
|
||||
it("should remove the router if the move is valid", async () => {
|
||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await cheatRemoveRouter(mockLogger, 0, 0, 0, 0);
|
||||
|
||||
expect(result.success).toEqual(true);
|
||||
expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 0,0 was cleared.");
|
||||
expect(Player.go.boardState.board[0][0]?.player).toEqual(playerColors.empty);
|
||||
expect(Go.currentGame.board[0][0]?.color).toEqual(GoColor.empty);
|
||||
});
|
||||
|
||||
it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Player.go.boardState.cheatCount = 1;
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
Go.currentGame.cheatCount = 1;
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await cheatRemoveRouter(mockLogger, 0, 0, 1, 0);
|
||||
console.log(result);
|
||||
expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
|
||||
expect(result.success).toEqual(false);
|
||||
expect(Player.go.boardState.history.length).toEqual(0);
|
||||
expect(Go.currentGame.previousBoard).toEqual(null);
|
||||
});
|
||||
});
|
||||
describe("cheatRepairOfflineNode() tests", () => {
|
||||
it("should handle invalid moves", async () => {
|
||||
const board = ["XOO..", ".....", ".....", ".....", "....#"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await cheatRepairOfflineNode(mockLogger, 0, 0);
|
||||
@ -270,13 +273,13 @@ describe("Netscript Go API unit tests", () => {
|
||||
|
||||
it("should update the board with the repaired node if the cheat is successful", async () => {
|
||||
const board = ["OXX..", ".....", ".....", ".....", "....#"];
|
||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
||||
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||
const mockLogger = jest.fn();
|
||||
|
||||
const result = await cheatRepairOfflineNode(mockLogger, 4, 4, 0, 0);
|
||||
expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 4,4 was repaired.");
|
||||
expect(result.success).toEqual(true);
|
||||
expect(Player.go.boardState.board[4]?.[4]?.player).toEqual(playerColors.empty);
|
||||
expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.empty);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,7 @@
|
||||
import { setPlayer } from "@player";
|
||||
import { GoColor } from "@enums";
|
||||
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
|
||||
import {
|
||||
getAllEyes,
|
||||
getAllValidMoves,
|
||||
getBoardFromSimplifiedBoardState,
|
||||
} from "../../../src/Go/boardAnalysis/boardAnalysis";
|
||||
import { playerColors } from "../../../src/Go/boardState/goConstants";
|
||||
import { getAllEyes, getAllValidMoves, boardStateFromSimpleBoard } from "../../../src/Go/boardAnalysis/boardAnalysis";
|
||||
import { findAnyMatchedPatterns } from "../../../src/Go/boardAnalysis/patternMatching";
|
||||
|
||||
setPlayer(new PlayerObject());
|
||||
@ -13,7 +9,7 @@ setPlayer(new PlayerObject());
|
||||
describe("Go board analysis tests", () => {
|
||||
it("identifies chains and liberties", async () => {
|
||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
||||
const boardState = getBoardFromSimplifiedBoardState(board);
|
||||
const boardState = boardStateFromSimpleBoard(board);
|
||||
|
||||
expect(boardState.board[0]?.[0]?.liberties?.length).toEqual(1);
|
||||
expect(boardState.board[0]?.[1]?.liberties?.length).toEqual(3);
|
||||
@ -21,10 +17,10 @@ describe("Go board analysis tests", () => {
|
||||
|
||||
it("identifies all points that are part of 'eyes' on the board", async () => {
|
||||
const board = ["..O..", "OOOOO", "..XXX", "..XX.", "..X.X"];
|
||||
const boardState = getBoardFromSimplifiedBoardState(board);
|
||||
const boardState = boardStateFromSimpleBoard(board);
|
||||
|
||||
const whitePlayerEyes = getAllEyes(boardState, playerColors.white).flat().flat();
|
||||
const blackPlayerEyes = getAllEyes(boardState, playerColors.black).flat().flat();
|
||||
const whitePlayerEyes = getAllEyes(boardState.board, GoColor.white).flat().flat();
|
||||
const blackPlayerEyes = getAllEyes(boardState.board, GoColor.black).flat().flat();
|
||||
|
||||
expect(whitePlayerEyes?.length).toEqual(4);
|
||||
expect(blackPlayerEyes?.length).toEqual(2);
|
||||
@ -32,11 +28,11 @@ describe("Go board analysis tests", () => {
|
||||
|
||||
it("identifies strong patterns on the board", async () => {
|
||||
const board = [".....", ".....", ".....", ".....", ".OXO."];
|
||||
const boardState = getBoardFromSimplifiedBoardState(board);
|
||||
const boardState = boardStateFromSimpleBoard(board);
|
||||
const point = await findAnyMatchedPatterns(
|
||||
boardState,
|
||||
playerColors.white,
|
||||
getAllValidMoves(boardState, playerColors.white),
|
||||
boardState.board,
|
||||
GoColor.white,
|
||||
getAllValidMoves(boardState, GoColor.white),
|
||||
true,
|
||||
0,
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { getBoardFromSimplifiedBoardState } from "../../../src/Go/boardAnalysis/boardAnalysis";
|
||||
import { opponents, playerColors } from "../../../src/Go/boardState/goConstants";
|
||||
import { getMove } from "../../../src/Go/boardAnalysis/goAI";
|
||||
import { setPlayer } from "@player";
|
||||
import { GoOpponent, GoColor } from "@enums";
|
||||
import { boardStateFromSimpleBoard } from "../../../src/Go/boardAnalysis/boardAnalysis";
|
||||
import { getMove } from "../../../src/Go/boardAnalysis/goAI";
|
||||
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
|
||||
import "../../../src/Faction/Factions";
|
||||
|
||||
@ -14,24 +14,24 @@ setPlayer(new PlayerObject());
|
||||
describe("Go AI tests", () => {
|
||||
it("prioritizes capture for Black Hand", async () => {
|
||||
const board = ["XO...", ".....", ".....", ".....", "....."];
|
||||
const boardState = getBoardFromSimplifiedBoardState(board, opponents.TheBlackHand);
|
||||
const move = await getMove(boardState, playerColors.white, opponents.TheBlackHand);
|
||||
const boardState = boardStateFromSimpleBoard(board, GoOpponent.TheBlackHand);
|
||||
const move = await getMove(boardState, GoColor.white, GoOpponent.TheBlackHand);
|
||||
|
||||
expect([move.x, move.y]).toEqual([1, 0]);
|
||||
});
|
||||
|
||||
it("prioritizes defense for Slum Snakes", async () => {
|
||||
const board = ["OX...", ".....", ".....", ".....", "....."];
|
||||
const boardState = getBoardFromSimplifiedBoardState(board, opponents.SlumSnakes);
|
||||
const move = await getMove(boardState, playerColors.white, opponents.SlumSnakes);
|
||||
const boardState = boardStateFromSimpleBoard(board, GoOpponent.SlumSnakes);
|
||||
const move = await getMove(boardState, GoColor.white, GoOpponent.SlumSnakes);
|
||||
|
||||
expect([move.x, move.y]).toEqual([1, 0]);
|
||||
});
|
||||
|
||||
it("prioritizes eye creation moves for Illuminati", async () => {
|
||||
const board = ["...O...", "OOOO...", ".......", ".......", ".......", ".......", "......."];
|
||||
const boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus);
|
||||
const move = await getMove(boardState, playerColors.white, opponents.Daedalus, 0);
|
||||
const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus);
|
||||
const move = await getMove(boardState, GoColor.white, GoOpponent.Daedalus, 0);
|
||||
|
||||
console.log(move);
|
||||
|
||||
|
@ -548,6 +548,29 @@ exports[`Check Save File Continuity FactionsSave continuity 1`] = `
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Check Save File Continuity GoSave continuity 1`] = `
|
||||
{
|
||||
"currentGame": {
|
||||
"ai": "Netburners",
|
||||
"board": [
|
||||
".......",
|
||||
".......",
|
||||
".......",
|
||||
".......",
|
||||
".......",
|
||||
".......",
|
||||
".......",
|
||||
],
|
||||
"cheatCount": 0,
|
||||
"passCount": 0,
|
||||
"previousBoard": null,
|
||||
"previousPlayer": "White",
|
||||
},
|
||||
"previousGame": null,
|
||||
"stats": {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Check Save File Continuity PlayerSave continuity 1`] = `
|
||||
{
|
||||
"ctor": "PlayerObject",
|
||||
@ -1198,457 +1221,6 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
|
||||
"wantedGainRate": 0,
|
||||
},
|
||||
},
|
||||
"go": {
|
||||
"boardState": {
|
||||
"ai": "Netburners",
|
||||
"board": [
|
||||
[
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 0,
|
||||
"y": 1,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 0,
|
||||
"y": 2,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 0,
|
||||
"y": 3,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 0,
|
||||
"y": 4,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 0,
|
||||
"y": 5,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 0,
|
||||
"y": 6,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 1,
|
||||
"y": 3,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 1,
|
||||
"y": 4,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 1,
|
||||
"y": 5,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 1,
|
||||
"y": 6,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 2,
|
||||
"y": 0,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 2,
|
||||
"y": 2,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 2,
|
||||
"y": 3,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 2,
|
||||
"y": 4,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 2,
|
||||
"y": 5,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 2,
|
||||
"y": 6,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 3,
|
||||
"y": 0,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 3,
|
||||
"y": 1,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 3,
|
||||
"y": 2,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 3,
|
||||
"y": 3,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 3,
|
||||
"y": 4,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 3,
|
||||
"y": 5,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 3,
|
||||
"y": 6,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 4,
|
||||
"y": 0,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 4,
|
||||
"y": 1,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 4,
|
||||
"y": 2,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 4,
|
||||
"y": 3,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 4,
|
||||
"y": 4,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 4,
|
||||
"y": 5,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 4,
|
||||
"y": 6,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 5,
|
||||
"y": 0,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 5,
|
||||
"y": 1,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 5,
|
||||
"y": 2,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 5,
|
||||
"y": 3,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 5,
|
||||
"y": 4,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 5,
|
||||
"y": 5,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 5,
|
||||
"y": 6,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 6,
|
||||
"y": 0,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 6,
|
||||
"y": 1,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 6,
|
||||
"y": 2,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 6,
|
||||
"y": 3,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 6,
|
||||
"y": 4,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 6,
|
||||
"y": 5,
|
||||
},
|
||||
{
|
||||
"chain": "",
|
||||
"liberties": null,
|
||||
"player": "Empty",
|
||||
"x": 6,
|
||||
"y": 6,
|
||||
},
|
||||
],
|
||||
],
|
||||
"cheatCount": 0,
|
||||
"history": [],
|
||||
"passCount": 0,
|
||||
"previousPlayer": "White",
|
||||
},
|
||||
"previousGameFinalBoardState": null,
|
||||
"status": {
|
||||
"????????????": {
|
||||
"favor": 0,
|
||||
"highestWinStreak": 0,
|
||||
"losses": 0,
|
||||
"nodePower": 0,
|
||||
"nodes": 0,
|
||||
"oldWinStreak": 0,
|
||||
"winStreak": 0,
|
||||
"wins": 0,
|
||||
},
|
||||
"Daedalus": {
|
||||
"favor": 0,
|
||||
"highestWinStreak": 0,
|
||||
"losses": 0,
|
||||
"nodePower": 0,
|
||||
"nodes": 0,
|
||||
"oldWinStreak": 0,
|
||||
"winStreak": 0,
|
||||
"wins": 0,
|
||||
},
|
||||
"Illuminati": {
|
||||
"favor": 0,
|
||||
"highestWinStreak": 0,
|
||||
"losses": 0,
|
||||
"nodePower": 0,
|
||||
"nodes": 0,
|
||||
"oldWinStreak": 0,
|
||||
"winStreak": 0,
|
||||
"wins": 0,
|
||||
},
|
||||
"Netburners": {
|
||||
"favor": 0,
|
||||
"highestWinStreak": 0,
|
||||
"losses": 0,
|
||||
"nodePower": 0,
|
||||
"nodes": 0,
|
||||
"oldWinStreak": 0,
|
||||
"winStreak": 0,
|
||||
"wins": 0,
|
||||
},
|
||||
"No AI": {
|
||||
"favor": 0,
|
||||
"highestWinStreak": 0,
|
||||
"losses": 0,
|
||||
"nodePower": 0,
|
||||
"nodes": 0,
|
||||
"oldWinStreak": 0,
|
||||
"winStreak": 0,
|
||||
"wins": 0,
|
||||
},
|
||||
"Slum Snakes": {
|
||||
"favor": 0,
|
||||
"highestWinStreak": 0,
|
||||
"losses": 0,
|
||||
"nodePower": 0,
|
||||
"nodes": 0,
|
||||
"oldWinStreak": 0,
|
||||
"winStreak": 0,
|
||||
"wins": 0,
|
||||
},
|
||||
"Tetrads": {
|
||||
"favor": 0,
|
||||
"highestWinStreak": 0,
|
||||
"losses": 0,
|
||||
"nodePower": 0,
|
||||
"nodes": 0,
|
||||
"oldWinStreak": 0,
|
||||
"winStreak": 0,
|
||||
"wins": 0,
|
||||
},
|
||||
"The Black Hand": {
|
||||
"favor": 0,
|
||||
"highestWinStreak": 0,
|
||||
"losses": 0,
|
||||
"nodePower": 0,
|
||||
"nodes": 0,
|
||||
"oldWinStreak": 0,
|
||||
"winStreak": 0,
|
||||
"wins": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
"hacknetNodes": [],
|
||||
"has4SData": false,
|
||||
"has4SDataTixApi": false,
|
||||
|
Loading…
Reference in New Issue
Block a user