mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2025-02-17 18:33:44 +01:00
GO: Various changes before 2.6.0 (#1120)
This commit is contained in:
@ -10,7 +10,7 @@ import { ButtonWithTooltip } from "../../ui/Components/ButtonWithTooltip";
|
|||||||
import { MaxSleevesFromCovenant } from "../../PersonObjects/Sleeve/SleeveCovenantPurchases";
|
import { MaxSleevesFromCovenant } from "../../PersonObjects/Sleeve/SleeveCovenantPurchases";
|
||||||
|
|
||||||
// Update as additional BitNodes get implemented
|
// 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({
|
const useStyles = makeStyles({
|
||||||
group: {
|
group: {
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
|
@ -6,6 +6,7 @@ export * from "./Company/Enums";
|
|||||||
export * from "./Corporation/Enums";
|
export * from "./Corporation/Enums";
|
||||||
export * from "./Crime/Enums";
|
export * from "./Crime/Enums";
|
||||||
export * from "./Faction/Enums";
|
export * from "./Faction/Enums";
|
||||||
|
export * from "./Go/Enums";
|
||||||
export * from "./Literature/Enums";
|
export * from "./Literature/Enums";
|
||||||
export * from "./Locations/Enums";
|
export * from "./Locations/Enums";
|
||||||
export * from "./Message/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 {
|
import type { Board, BoardState, Neighbor, PointState, SimpleBoard } from "../Types";
|
||||||
Board,
|
|
||||||
BoardState,
|
import { GoValidity, GoOpponent, GoColor } from "@enums";
|
||||||
Neighbor,
|
|
||||||
opponents,
|
|
||||||
PlayerColor,
|
|
||||||
playerColors,
|
|
||||||
PointState,
|
|
||||||
validityReason,
|
|
||||||
} from "../boardState/goConstants";
|
|
||||||
import {
|
import {
|
||||||
findAdjacentPointsInChain,
|
findAdjacentPointsInChain,
|
||||||
findNeighbors,
|
findNeighbors,
|
||||||
@ -15,7 +8,6 @@ import {
|
|||||||
getBoardCopy,
|
getBoardCopy,
|
||||||
getEmptySpaces,
|
getEmptySpaces,
|
||||||
getNewBoardState,
|
getNewBoardState,
|
||||||
getStateCopy,
|
|
||||||
isDefined,
|
isDefined,
|
||||||
isNotNull,
|
isNotNull,
|
||||||
updateCaptures,
|
updateCaptures,
|
||||||
@ -35,120 +27,106 @@ import {
|
|||||||
*
|
*
|
||||||
* @returns a validity explanation for if the move is legal or not
|
* @returns a validity explanation for if the move is legal or not
|
||||||
*/
|
*/
|
||||||
export function evaluateIfMoveIsValid(
|
export function evaluateIfMoveIsValid(boardState: BoardState, x: number, y: number, player: GoColor, shortcut = true) {
|
||||||
boardState: BoardState,
|
const point = boardState.board[x]?.[y];
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
player: PlayerColor,
|
|
||||||
shortcut = true,
|
|
||||||
) {
|
|
||||||
const point = boardState.board?.[x]?.[y];
|
|
||||||
|
|
||||||
if (boardState.previousPlayer === null) {
|
if (boardState.previousPlayer === null) {
|
||||||
return validityReason.gameOver;
|
return GoValidity.gameOver;
|
||||||
}
|
}
|
||||||
if (boardState.previousPlayer === player) {
|
if (boardState.previousPlayer === player) {
|
||||||
return validityReason.notYourTurn;
|
return GoValidity.notYourTurn;
|
||||||
}
|
}
|
||||||
if (!point) {
|
if (!point) {
|
||||||
return validityReason.pointBroken;
|
return GoValidity.pointBroken;
|
||||||
}
|
}
|
||||||
if (point.player !== playerColors.empty) {
|
if (point.color !== GoColor.empty) {
|
||||||
return validityReason.pointNotEmpty;
|
return GoValidity.pointNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect if the current player has ever previously played this move. Used to detect potential repeated board states
|
// Detect if the move might be an immediate repeat (only one board of history is saved to check)
|
||||||
const moveHasBeenPlayedBefore = !!boardState.history.find((board) => board[x]?.[y]?.player === player);
|
const possibleRepeat = boardState.previousBoard && getColorOnSimpleBoard(boardState.previousBoard, x, y) === player;
|
||||||
|
|
||||||
if (shortcut) {
|
if (shortcut) {
|
||||||
// If the current point has some adjacent open spaces, it is not suicide. If the move is not repeated, it is legal
|
// 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;
|
const hasLiberty = liberties.north || liberties.east || liberties.south || liberties.west;
|
||||||
if (!moveHasBeenPlayedBefore && hasLiberty) {
|
if (!possibleRepeat && hasLiberty) {
|
||||||
return validityReason.valid;
|
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
|
// 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);
|
const neighborChainLibertyCount = findMaxLibertyCountOfAdjacentChains(boardState, x, y, player);
|
||||||
if (!moveHasBeenPlayedBefore && neighborChainLibertyCount > 1) {
|
if (!possibleRepeat && neighborChainLibertyCount > 1) {
|
||||||
return validityReason.valid;
|
return GoValidity.valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is any neighboring enemy chain with only one liberty, and the move is not repeated, it is 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
|
// because it would capture the enemy chain and free up some liberties for itself
|
||||||
const potentialCaptureChainLibertyCount = findMinLibertyCountOfAdjacentChains(
|
const potentialCaptureChainLibertyCount = findMinLibertyCountOfAdjacentChains(
|
||||||
boardState,
|
boardState.board,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
player === playerColors.black ? playerColors.white : playerColors.black,
|
player === GoColor.black ? GoColor.white : GoColor.black,
|
||||||
);
|
);
|
||||||
if (!moveHasBeenPlayedBefore && potentialCaptureChainLibertyCount < 2) {
|
if (!possibleRepeat && potentialCaptureChainLibertyCount < 2) {
|
||||||
return validityReason.valid;
|
return GoValidity.valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no direct liberties for the move, no captures, and no neighboring friendly chains with multiple liberties,
|
// 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
|
// the move is not valid because it would suicide the piece
|
||||||
if (!hasLiberty && potentialCaptureChainLibertyCount >= 2 && neighborChainLibertyCount <= 1) {
|
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 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
|
// if it is a repeated move, or if it is a valid move
|
||||||
const evaluationBoard = evaluateMoveResult(boardState, x, y, player, true);
|
const evaluationBoard = evaluateMoveResult(boardState.board, x, y, player, true);
|
||||||
if (evaluationBoard.board[x]?.[y]?.player !== player) {
|
if (evaluationBoard[x]?.[y]?.color !== player) {
|
||||||
return validityReason.noSuicide;
|
return GoValidity.noSuicide;
|
||||||
}
|
}
|
||||||
if (moveHasBeenPlayedBefore && checkIfBoardStateIsRepeated(evaluationBoard)) {
|
if (possibleRepeat && boardState.previousBoard) {
|
||||||
return validityReason.boardRepeated;
|
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
|
* 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(
|
export function evaluateMoveResult(board: Board, x: number, y: number, player: GoColor, resetChains = false): Board {
|
||||||
initialBoardState: BoardState,
|
const evaluationBoard = getBoardCopy(board);
|
||||||
x: number,
|
const point = evaluationBoard[x]?.[y];
|
||||||
y: number,
|
if (!point) return board;
|
||||||
player: playerColors,
|
|
||||||
resetChains = false,
|
|
||||||
) {
|
|
||||||
const boardState = getStateCopy(initialBoardState);
|
|
||||||
boardState.history.push(getBoardCopy(boardState).board);
|
|
||||||
const point = boardState.board[x]?.[y];
|
|
||||||
if (!point) {
|
|
||||||
return initialBoardState;
|
|
||||||
}
|
|
||||||
|
|
||||||
point.player = player;
|
point.color = player;
|
||||||
boardState.previousPlayer = player;
|
|
||||||
|
|
||||||
const neighbors = getArrayFromNeighbor(findNeighbors(boardState, x, y));
|
const neighbors = getArrayFromNeighbor(findNeighbors(board, x, y));
|
||||||
const chainIdsToUpdate = [point.chain, ...neighbors.map((point) => point.chain)];
|
const chainIdsToUpdate = [point.chain, ...neighbors.map((point) => point.chain)];
|
||||||
resetChainsById(boardState, chainIdsToUpdate);
|
resetChainsById(evaluationBoard, chainIdsToUpdate);
|
||||||
|
updateCaptures(evaluationBoard, player, resetChains);
|
||||||
return updateCaptures(boardState, player, resetChains);
|
return evaluationBoard;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getControlledSpace(boardState: BoardState) {
|
export function getControlledSpace(board: Board) {
|
||||||
const chains = getAllChains(boardState);
|
const chains = getAllChains(board);
|
||||||
const length = boardState.board[0].length;
|
const length = board[0].length;
|
||||||
const whiteControlledEmptyNodes = getAllPotentialEyes(boardState, chains, playerColors.white, length * 2)
|
const whiteControlledEmptyNodes = getAllPotentialEyes(board, chains, GoColor.white, length * 2)
|
||||||
.map((eye) => eye.chain)
|
.map((eye) => eye.chain)
|
||||||
.flat();
|
.flat();
|
||||||
const blackControlledEmptyNodes = getAllPotentialEyes(boardState, chains, playerColors.black, length * 2)
|
const blackControlledEmptyNodes = getAllPotentialEyes(board, chains, GoColor.black, length * 2)
|
||||||
.map((eye) => eye.chain)
|
.map((eye) => eye.chain)
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
const ownedPointGrid = Array.from({ length }, () => Array.from({ length }, () => playerColors.empty));
|
const ownedPointGrid = Array.from({ length }, () => Array.from({ length }, () => GoColor.empty));
|
||||||
whiteControlledEmptyNodes.forEach((node) => {
|
whiteControlledEmptyNodes.forEach((node) => {
|
||||||
ownedPointGrid[node.x][node.y] = playerColors.white;
|
ownedPointGrid[node.x][node.y] = GoColor.white;
|
||||||
});
|
});
|
||||||
blackControlledEmptyNodes.forEach((node) => {
|
blackControlledEmptyNodes.forEach((node) => {
|
||||||
ownedPointGrid[node.x][node.y] = playerColors.black;
|
ownedPointGrid[node.x][node.y] = GoColor.black;
|
||||||
});
|
});
|
||||||
|
|
||||||
return ownedPointGrid;
|
return ownedPointGrid;
|
||||||
@ -157,30 +135,28 @@ export function getControlledSpace(boardState: BoardState) {
|
|||||||
/**
|
/**
|
||||||
Clear the chain and liberty data of all points in the given chains
|
Clear the chain and liberty data of all points in the given chains
|
||||||
*/
|
*/
|
||||||
const resetChainsById = (boardState: BoardState, chainIds: string[]) => {
|
const resetChainsById = (board: Board, chainIds: string[]) => {
|
||||||
const pointsToUpdate = boardState.board
|
for (const column of board) {
|
||||||
.flat()
|
for (const point of column) {
|
||||||
.filter(isDefined)
|
if (!point || !chainIds.includes(point.chain)) continue;
|
||||||
.filter(isNotNull)
|
|
||||||
.filter((point) => chainIds.includes(point.chain));
|
|
||||||
pointsToUpdate.forEach((point) => {
|
|
||||||
point.chain = "";
|
point.chain = "";
|
||||||
point.liberties = [];
|
point.liberties = [];
|
||||||
});
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For a potential move, determine what the liberty of the point would be if played, by looking at adjacent empty nodes
|
* 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
|
* as well as the remaining liberties of neighboring friendly chains
|
||||||
*/
|
*/
|
||||||
export function findEffectiveLibertiesOfNewMove(boardState: BoardState, x: number, y: number, player: PlayerColor) {
|
export function findEffectiveLibertiesOfNewMove(board: Board, x: number, y: number, player: GoColor) {
|
||||||
const friendlyChains = getAllChains(boardState).filter((chain) => chain[0].player === player);
|
const friendlyChains = getAllChains(board).filter((chain) => chain[0].color === player);
|
||||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
|
const neighbors = findAdjacentLibertiesAndAlliesForPoint(board, x, y, player);
|
||||||
const neighborPoints = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
const neighborPoints = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||||
.filter(isNotNull)
|
.filter(isNotNull)
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
// Get all chains that the new move will connect to
|
// 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
|
const allyNeighborChainLiberties = allyNeighbors
|
||||||
.map((neighbor) => {
|
.map((neighbor) => {
|
||||||
const chain = friendlyChains.find((chain) => chain[0].chain === neighbor.chain);
|
const chain = friendlyChains.find((chain) => chain[0].chain === neighbor.chain);
|
||||||
@ -190,7 +166,7 @@ export function findEffectiveLibertiesOfNewMove(boardState: BoardState, x: numbe
|
|||||||
.filter(isNotNull);
|
.filter(isNotNull);
|
||||||
|
|
||||||
// Get all empty spaces that the new move connects to that aren't already part of friendly liberties
|
// 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];
|
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
|
* Find the number of open spaces that are connected to chains adjacent to a given point, and return the maximum
|
||||||
*/
|
*/
|
||||||
export function findMaxLibertyCountOfAdjacentChains(
|
export function findMaxLibertyCountOfAdjacentChains(boardState: BoardState, x: number, y: number, player: GoColor) {
|
||||||
boardState: BoardState,
|
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState.board, x, y, player);
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
player: playerColors,
|
|
||||||
) {
|
|
||||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
|
|
||||||
const friendlyNeighbors = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
const friendlyNeighbors = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||||
.filter(isNotNull)
|
.filter(isNotNull)
|
||||||
.filter(isDefined)
|
.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);
|
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
|
* Find the number of open spaces that are connected to chains adjacent to a given point, and return the minimum
|
||||||
*/
|
*/
|
||||||
export function findMinLibertyCountOfAdjacentChains(
|
export function findMinLibertyCountOfAdjacentChains(board: Board, x: number, y: number, player: GoColor) {
|
||||||
boardState: BoardState,
|
const chain = findEnemyNeighborChainWithFewestLiberties(board, x, y, player);
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
player: playerColors,
|
|
||||||
) {
|
|
||||||
const chain = findEnemyNeighborChainWithFewestLiberties(boardState, x, y, player);
|
|
||||||
return chain?.[0]?.liberties?.length ?? 99;
|
return chain?.[0]?.liberties?.length ?? 99;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findEnemyNeighborChainWithFewestLiberties(
|
export function findEnemyNeighborChainWithFewestLiberties(board: Board, x: number, y: number, player: GoColor) {
|
||||||
boardState: BoardState,
|
const chains = getAllChains(board);
|
||||||
x: number,
|
const neighbors = findAdjacentLibertiesAndAlliesForPoint(board, x, y, player);
|
||||||
y: number,
|
|
||||||
player: playerColors,
|
|
||||||
) {
|
|
||||||
const chains = getAllChains(boardState);
|
|
||||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
|
|
||||||
const friendlyNeighbors = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
const friendlyNeighbors = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||||
.filter(isNotNull)
|
.filter(isNotNull)
|
||||||
.filter(isDefined)
|
.filter(isDefined)
|
||||||
.filter((neighbor) => neighbor.player === player);
|
.filter((neighbor) => neighbor.color === player);
|
||||||
|
|
||||||
const minimumLiberties = friendlyNeighbors.reduce(
|
const minimumLiberties = friendlyNeighbors.reduce(
|
||||||
(min, neighbor) => Math.min(min, neighbor?.liberties?.length ?? 0),
|
(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
|
* Returns a list of points that are valid moves for the given player
|
||||||
*/
|
*/
|
||||||
export function getAllValidMoves(boardState: BoardState, player: PlayerColor) {
|
export function getAllValidMoves(boardState: BoardState, player: GoColor) {
|
||||||
return getEmptySpaces(boardState).filter(
|
return getEmptySpaces(boardState.board).filter(
|
||||||
(point) => evaluateIfMoveIsValid(boardState, point.x, point.y, player) === validityReason.valid,
|
(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.
|
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) {
|
export function getAllEyesByChainId(board: Board, player: GoColor) {
|
||||||
const allChains = getAllChains(boardState);
|
const allChains = getAllChains(board);
|
||||||
const eyeCandidates = getAllPotentialEyes(boardState, allChains, player);
|
const eyeCandidates = getAllPotentialEyes(board, allChains, player);
|
||||||
const eyes: { [s: string]: PointState[][] } = {};
|
const eyes: { [s: string]: PointState[][] } = {};
|
||||||
|
|
||||||
eyeCandidates.forEach((candidate) => {
|
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
|
// If any chain fully encircles the empty space (even if there are other chains encircled as well), the eye is true
|
||||||
const neighborsEncirclingEye = findNeighboringChainsThatFullyEncircleEmptySpace(
|
const neighborsEncirclingEye = findNeighboringChainsThatFullyEncircleEmptySpace(
|
||||||
boardState,
|
board,
|
||||||
candidate.chain,
|
candidate.chain,
|
||||||
candidate.neighbors,
|
candidate.neighbors,
|
||||||
allChains,
|
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
|
* 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[][] }) {
|
export function getAllEyes(board: Board, player: GoColor, eyesObject?: { [s: string]: PointState[][] }) {
|
||||||
const eyes = eyesObject ?? getAllEyesByChainId(boardState, player);
|
const eyes = eyesObject ?? getAllEyesByChainId(board, player);
|
||||||
return Object.keys(eyes).map((key) => eyes[key]);
|
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
|
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.
|
an array at that chain number's index.
|
||||||
*/
|
*/
|
||||||
export function getAllPotentialEyes(
|
export function getAllPotentialEyes(board: Board, allChains: PointState[][], player: GoColor, _maxSize?: number) {
|
||||||
boardState: BoardState,
|
const nodeCount = board.map((row) => row.filter((p) => p)).flat().length;
|
||||||
allChains: PointState[][],
|
|
||||||
player: playerColors,
|
|
||||||
_maxSize?: number,
|
|
||||||
) {
|
|
||||||
const nodeCount = boardState.board.map((row) => row.filter((p) => p)).flat().length;
|
|
||||||
const maxSize = _maxSize ?? Math.min(nodeCount * 0.4, 11);
|
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 }[] = [];
|
const eyeCandidates: { neighbors: PointState[][]; chain: PointState[]; id: string }[] = [];
|
||||||
|
|
||||||
emptyPointChains
|
emptyPointChains
|
||||||
.filter((chain) => chain.length <= maxSize)
|
.filter((chain) => chain.length <= maxSize)
|
||||||
.forEach((chain) => {
|
.forEach((chain) => {
|
||||||
const neighboringChains = getAllNeighboringChains(boardState, chain, allChains);
|
const neighboringChains = getAllNeighboringChains(board, chain, allChains);
|
||||||
|
|
||||||
const hasWhitePieceNeighbor = neighboringChains.find(
|
const hasWhitePieceNeighbor = neighboringChains.find(
|
||||||
(neighborChain) => neighborChain[0]?.player === playerColors.white,
|
(neighborChain) => neighborChain[0]?.color === GoColor.white,
|
||||||
);
|
);
|
||||||
const hasBlackPieceNeighbor = neighboringChains.find(
|
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
|
// Record the neighbor chains of the eye candidate empty chain, if all of its neighbors are the same color piece
|
||||||
if (
|
if (
|
||||||
(hasWhitePieceNeighbor && !hasBlackPieceNeighbor && player === playerColors.white) ||
|
(hasWhitePieceNeighbor && !hasBlackPieceNeighbor && player === GoColor.white) ||
|
||||||
(!hasWhitePieceNeighbor && hasBlackPieceNeighbor && player === playerColors.black)
|
(!hasWhitePieceNeighbor && hasBlackPieceNeighbor && player === GoColor.black)
|
||||||
) {
|
) {
|
||||||
eyeCandidates.push({
|
eyeCandidates.push({
|
||||||
neighbors: neighboringChains,
|
neighbors: neighboringChains,
|
||||||
@ -366,12 +322,12 @@ export function getAllPotentialEyes(
|
|||||||
* If so, the original candidate is a true eye.
|
* If so, the original candidate is a true eye.
|
||||||
*/
|
*/
|
||||||
function findNeighboringChainsThatFullyEncircleEmptySpace(
|
function findNeighboringChainsThatFullyEncircleEmptySpace(
|
||||||
boardState: BoardState,
|
board: Board,
|
||||||
candidateChain: PointState[],
|
candidateChain: PointState[],
|
||||||
neighborChainList: PointState[][],
|
neighborChainList: PointState[][],
|
||||||
allChains: PointState[][],
|
allChains: PointState[][],
|
||||||
) {
|
) {
|
||||||
const boardMax = boardState.board[0].length - 1;
|
const boardMax = board[0].length - 1;
|
||||||
const candidateSpread = findFurthestPointsOfChain(candidateChain);
|
const candidateSpread = findFurthestPointsOfChain(candidateChain);
|
||||||
return neighborChainList.filter((neighborChain, index) => {
|
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
|
// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const evaluationBoard = getStateCopy(boardState);
|
const evaluationBoard = getBoardCopy(board);
|
||||||
const examplePoint = candidateChain[0];
|
const examplePoint = candidateChain[0];
|
||||||
const otherChainNeighborPoints = removePointAtIndex(neighborChainList, index)
|
const otherChainNeighborPoints = removePointAtIndex(neighborChainList, index)
|
||||||
.flat()
|
.flat()
|
||||||
.filter(isNotNull)
|
.filter(isNotNull)
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
otherChainNeighborPoints.forEach((point) => {
|
otherChainNeighborPoints.forEach((point) => {
|
||||||
const pointToEdit = evaluationBoard.board[point.x]?.[point.y];
|
const pointToEdit = evaluationBoard[point.x]?.[point.y];
|
||||||
if (pointToEdit) {
|
if (pointToEdit) {
|
||||||
pointToEdit.player = playerColors.empty;
|
pointToEdit.color = GoColor.empty;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const updatedBoard = updateChains(evaluationBoard);
|
updateChains(evaluationBoard);
|
||||||
const newChains = getAllChains(updatedBoard);
|
const newChains = getAllChains(evaluationBoard);
|
||||||
const newChainID = updatedBoard.board[examplePoint.x]?.[examplePoint.y]?.chain;
|
const newChainID = evaluationBoard[examplePoint.x]?.[examplePoint.y]?.chain;
|
||||||
const chain = newChains.find((chain) => chain[0].chain === newChainID) || [];
|
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;
|
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
|
* Get all player chains that are adjacent / touching the current chain
|
||||||
*/
|
*/
|
||||||
export function getAllNeighboringChains(boardState: BoardState, chain: PointState[], allChains: PointState[][]) {
|
export function getAllNeighboringChains(board: Board, chain: PointState[], allChains: PointState[][]) {
|
||||||
const playerNeighbors = getPlayerNeighbors(boardState, chain);
|
const playerNeighbors = getPlayerNeighbors(board, chain);
|
||||||
|
|
||||||
const neighboringChains = playerNeighbors.reduce(
|
const neighboringChains = playerNeighbors.reduce(
|
||||||
(neighborChains, neighbor) =>
|
(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
|
* Gets all points that have player pieces adjacent to the given point
|
||||||
*/
|
*/
|
||||||
export function getPlayerNeighbors(boardState: BoardState, chain: PointState[]) {
|
export function getPlayerNeighbors(board: Board, chain: PointState[]) {
|
||||||
return getAllNeighbors(boardState, chain).filter((neighbor) => neighbor && neighbor.player !== playerColors.empty);
|
return getAllNeighbors(board, chain).filter((neighbor) => neighbor && neighbor.color !== GoColor.empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all points adjacent to the given point
|
* 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) => {
|
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))
|
.filter((neighborPoint) => !isPointInChain(neighborPoint, chain))
|
||||||
.forEach((neighborPoint) => chainNeighbors.add(neighborPoint));
|
.forEach((neighborPoint) => chainNeighbors.add(neighborPoint));
|
||||||
return chainNeighbors;
|
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);
|
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
|
* 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[] } = {};
|
const chains: { [s: string]: PointState[] } = {};
|
||||||
|
|
||||||
for (let x = 0; x < boardState.board.length; x++) {
|
for (let x = 0; x < board.length; x++) {
|
||||||
for (let y = 0; y < boardState.board[x].length; y++) {
|
for (let y = 0; y < board[x].length; y++) {
|
||||||
const point = boardState.board[x]?.[y];
|
const point = board[x]?.[y];
|
||||||
// If the current chain is already analyzed, skip it
|
// If the current chain is already analyzed, skip it
|
||||||
if (!point || point.chain === "") {
|
if (!point || point.chain === "") {
|
||||||
continue;
|
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)
|
* Find any group of stones with no liberties (who therefore are to be removed from the board)
|
||||||
*/
|
*/
|
||||||
export function findAllCapturedChains(chainList: PointState[][], playerWhoMoved: PlayerColor) {
|
export function findAllCapturedChains(chainList: PointState[][], playerWhoMoved: GoColor) {
|
||||||
const opposingPlayer = playerWhoMoved === playerColors.white ? playerColors.black : playerColors.white;
|
const opposingPlayer = playerWhoMoved === GoColor.white ? GoColor.black : GoColor.white;
|
||||||
const enemyChainsToCapture = findCapturedChainOfColor(chainList, opposingPlayer);
|
const enemyChainsToCapture = findCapturedChainOfColor(chainList, opposingPlayer);
|
||||||
|
|
||||||
if (enemyChainsToCapture) {
|
if (enemyChainsToCapture) {
|
||||||
@ -552,36 +490,36 @@ export function findAllCapturedChains(chainList: PointState[][], playerWhoMoved:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findCapturedChainOfColor(chainList: PointState[][], playerColor: PlayerColor) {
|
function findCapturedChainOfColor(chainList: PointState[][], playerColor: GoColor) {
|
||||||
return chainList.filter((chain) => chain?.[0].player === playerColor && chain?.[0].liberties?.length === 0);
|
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
|
* Find all empty points adjacent to any piece in a given chain
|
||||||
*/
|
*/
|
||||||
export function findLibertiesForChain(boardState: BoardState, chain: PointState[]): PointState[] {
|
export function findLibertiesForChain(board: Board, chain: PointState[]): PointState[] {
|
||||||
return getAllNeighbors(boardState, chain).filter((neighbor) => neighbor && neighbor.player === playerColors.empty);
|
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
|
* 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[] {
|
export function findChainLibertiesForPoint(board: Board, x: number, y: number): PointState[] {
|
||||||
const chain = findAdjacentPointsInChain(boardState, x, y);
|
const chain = findAdjacentPointsInChain(board, x, y);
|
||||||
return findLibertiesForChain(boardState, chain);
|
return findLibertiesForChain(board, chain);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object that includes which of the cardinal neighbors are empty
|
* Returns an object that includes which of the cardinal neighbors are empty
|
||||||
* (adjacent 'liberties' of the current piece )
|
* (adjacent 'liberties' of the current piece )
|
||||||
*/
|
*/
|
||||||
export function findAdjacentLibertiesForPoint(boardState: BoardState, x: number, y: number): Neighbor {
|
export function findAdjacentLibertiesForPoint(board: Board, x: number, y: number): Neighbor {
|
||||||
const neighbors = findNeighbors(boardState, x, y);
|
const neighbors = findNeighbors(board, x, y);
|
||||||
|
|
||||||
const hasNorthLiberty = neighbors.north && neighbors.north.player === playerColors.empty;
|
const hasNorthLiberty = neighbors.north && neighbors.north.color === GoColor.empty;
|
||||||
const hasEastLiberty = neighbors.east && neighbors.east.player === playerColors.empty;
|
const hasEastLiberty = neighbors.east && neighbors.east.color === GoColor.empty;
|
||||||
const hasSouthLiberty = neighbors.south && neighbors.south.player === playerColors.empty;
|
const hasSouthLiberty = neighbors.south && neighbors.south.color === GoColor.empty;
|
||||||
const hasWestLiberty = neighbors.west && neighbors.west.player === playerColors.empty;
|
const hasWestLiberty = neighbors.west && neighbors.west.color === GoColor.empty;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
north: hasNorthLiberty ? neighbors.north : null,
|
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
|
* current player's pieces. Used for making the connection map on the board
|
||||||
*/
|
*/
|
||||||
export function findAdjacentLibertiesAndAlliesForPoint(
|
export function findAdjacentLibertiesAndAlliesForPoint(
|
||||||
boardState: BoardState,
|
board: Board,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
_player?: PlayerColor,
|
_player?: GoColor,
|
||||||
): Neighbor {
|
): Neighbor {
|
||||||
const currentPoint = boardState.board[x]?.[y];
|
const currentPoint = board[x]?.[y];
|
||||||
const player =
|
const player = _player || (!currentPoint || currentPoint.color === GoColor.empty ? undefined : currentPoint.color);
|
||||||
_player || (!currentPoint || currentPoint.player === playerColors.empty ? undefined : currentPoint.player);
|
const adjacentLiberties = findAdjacentLibertiesForPoint(board, x, y);
|
||||||
const adjacentLiberties = findAdjacentLibertiesForPoint(boardState, x, y);
|
const neighbors = findNeighbors(board, x, y);
|
||||||
const neighbors = findNeighbors(boardState, x, y);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
north: adjacentLiberties.north || neighbors.north?.player === player ? neighbors.north : null,
|
north: adjacentLiberties.north || neighbors.north?.color === player ? neighbors.north : null,
|
||||||
east: adjacentLiberties.east || neighbors.east?.player === player ? neighbors.east : null,
|
east: adjacentLiberties.east || neighbors.east?.color === player ? neighbors.east : null,
|
||||||
south: adjacentLiberties.south || neighbors.south?.player === player ? neighbors.south : null,
|
south: adjacentLiberties.south || neighbors.south?.color === player ? neighbors.south : null,
|
||||||
west: adjacentLiberties.west || neighbors.west?.player === player ? neighbors.west : 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.
|
* 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) =>
|
return board.map((column) =>
|
||||||
column.reduce((str, point) => {
|
column.reduce((str, point) => {
|
||||||
if (!point) {
|
if (!point) {
|
||||||
return str + "#";
|
return str + "#";
|
||||||
}
|
}
|
||||||
if (point.player === playerColors.black) {
|
if (point.color === GoColor.black) {
|
||||||
return str + "X";
|
return str + "X";
|
||||||
}
|
}
|
||||||
if (point.player === playerColors.white) {
|
if (point.color === GoColor.white) {
|
||||||
return str + "O";
|
return str + "O";
|
||||||
}
|
}
|
||||||
return str + ".";
|
return str + ".";
|
||||||
@ -655,29 +592,47 @@ export function getSimplifiedBoardState(board: Board): string[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBoardFromSimplifiedBoardState(
|
/** Creates a board object from a simple board. The resulting board has no analytics (liberties/chains) */
|
||||||
boardStrings: string[],
|
export function boardFromSimpleBoard(simpleBoard: SimpleBoard): Board {
|
||||||
ai = opponents.Daedalus,
|
return simpleBoard.map((column, x) =>
|
||||||
lastPlayer = playerColors.black,
|
column.split("").map((char, y) => {
|
||||||
) {
|
if (char === "#") return null;
|
||||||
const newBoardState = getNewBoardState(boardStrings[0].length, ai);
|
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;
|
newBoardState.previousPlayer = lastPlayer;
|
||||||
|
updateCaptures(newBoardState.board, lastPlayer);
|
||||||
for (let x = 0; x < boardStrings[0].length; x++) {
|
return newBoardState;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
import {
|
||||||
getAllChains,
|
getAllChains,
|
||||||
getAllEyes,
|
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
|
* 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
|
* 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);
|
let validMoves = getAllValidMoves(boardState, player);
|
||||||
if (excludeFriendlyEyes) {
|
if (excludeFriendlyEyes) {
|
||||||
const friendlyEyes = getAllEyes(boardState, player)
|
const friendlyEyes = getAllEyes(boardState.board, player)
|
||||||
.filter((eye) => eye.length >= 2)
|
.filter((eye) => eye.length >= 2)
|
||||||
.flat()
|
.flat()
|
||||||
.flat();
|
.flat();
|
||||||
validMoves = validMoves.filter((point) => !contains(friendlyEyes, point));
|
validMoves = validMoves.filter((point) => !contains(friendlyEyes, point));
|
||||||
}
|
}
|
||||||
const opponent = player === playerColors.white ? playerColors.black : playerColors.white;
|
const opponent = player === GoColor.white ? GoColor.black : GoColor.white;
|
||||||
const chains = getAllChains(boardState);
|
const chains = getAllChains(boardState.board);
|
||||||
const emptySpacesToAnalyze = getAllPotentialEyes(boardState, chains, opponent);
|
const emptySpacesToAnalyze = getAllPotentialEyes(boardState.board, chains, opponent);
|
||||||
const nodesInsideEyeSpacesToAnalyze = emptySpacesToAnalyze.map((space) => space.chain).flat();
|
const nodesInsideEyeSpacesToAnalyze = emptySpacesToAnalyze.map((space) => space.chain).flat();
|
||||||
|
|
||||||
const playableNodesInsideOfEnemySpace = emptySpacesToAnalyze.reduce((playableNodes: PointState[], space) => {
|
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
|
// 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
|
// 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
|
// 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
|
// 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 [];
|
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.
|
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) {
|
export function findClaimedTerritory(board: Board) {
|
||||||
const whiteClaimedTerritory = getAllEyes(boardState, playerColors.white).filter(
|
const whiteClaimedTerritory = getAllEyes(board, GoColor.white).filter((eyesForChainN) => eyesForChainN.length >= 2);
|
||||||
(eyesForChainN) => eyesForChainN.length >= 2,
|
const blackClaimedTerritory = getAllEyes(board, GoColor.black).filter((eyesForChainN) => eyesForChainN.length >= 2);
|
||||||
);
|
|
||||||
const blackClaimedTerritory = getAllEyes(boardState, playerColors.black).filter(
|
|
||||||
(eyesForChainN) => eyesForChainN.length >= 2,
|
|
||||||
);
|
|
||||||
return [...blackClaimedTerritory, ...whiteClaimedTerritory].flat().flat();
|
return [...blackClaimedTerritory, ...whiteClaimedTerritory].flat().flat();
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,8 @@
|
|||||||
import {
|
import type { Board, BoardState, EyeMove, Move, MoveOptions, PointState } from "../Types";
|
||||||
BoardState,
|
|
||||||
EyeMove,
|
import { Player } from "@player";
|
||||||
Move,
|
import { AugmentationName, GoOpponent, GoColor, GoPlayType } from "@enums";
|
||||||
MoveOptions,
|
import { opponentDetails } from "../Constants";
|
||||||
opponentDetails,
|
|
||||||
opponents,
|
|
||||||
PlayerColor,
|
|
||||||
playerColors,
|
|
||||||
playTypes,
|
|
||||||
PointState,
|
|
||||||
} from "../boardState/goConstants";
|
|
||||||
import { findNeighbors, floor, isDefined, isNotNull, passTurn } from "../boardState/boardState";
|
import { findNeighbors, floor, isDefined, isNotNull, passTurn } from "../boardState/boardState";
|
||||||
import {
|
import {
|
||||||
evaluateIfMoveIsValid,
|
evaluateIfMoveIsValid,
|
||||||
@ -26,8 +19,6 @@ import {
|
|||||||
import { findDisputedTerritory } from "./controlledTerritory";
|
import { findDisputedTerritory } from "./controlledTerritory";
|
||||||
import { findAnyMatchedPatterns } from "./patternMatching";
|
import { findAnyMatchedPatterns } from "./patternMatching";
|
||||||
import { WHRNG } from "../../Casino/RNG";
|
import { WHRNG } from "../../Casino/RNG";
|
||||||
import { Player } from "@player";
|
|
||||||
import { AugmentationName } from "@enums";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Basic GO AIs, each with some personality and weaknesses
|
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.
|
* @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);
|
await sleep(300);
|
||||||
const rng = new WHRNG(rngOverride || Player.totalPlaytime);
|
const rng = new WHRNG(rngOverride || Player.totalPlaytime);
|
||||||
const smart = isSmart(opponent, rng.random());
|
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());
|
const priorityMove = await getFactionMove(moves, opponent, rng.random());
|
||||||
if (priorityMove) {
|
if (priorityMove) {
|
||||||
return {
|
return {
|
||||||
type: playTypes.move,
|
type: GoPlayType.move,
|
||||||
x: priorityMove.x,
|
x: priorityMove.x,
|
||||||
y: priorityMove.y,
|
y: priorityMove.y,
|
||||||
};
|
};
|
||||||
@ -80,14 +71,14 @@ export async function getMove(boardState: BoardState, player: PlayerColor, oppon
|
|||||||
|
|
||||||
if (chosenMove) {
|
if (chosenMove) {
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
console.debug(`Non-priority move chosen: ${chosenMove.x} ${chosenMove.y}`);
|
//console.debug(`Non-priority move chosen: ${chosenMove.x} ${chosenMove.y}`);
|
||||||
return {
|
return {
|
||||||
type: playTypes.move,
|
type: GoPlayType.move,
|
||||||
x: chosenMove.x,
|
x: chosenMove.x,
|
||||||
y: chosenMove.y,
|
y: chosenMove.y,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.debug("No valid moves found");
|
//console.debug("No valid moves found");
|
||||||
return handleNoMoveFound(boardState, player);
|
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,
|
* 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.
|
* 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);
|
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;
|
const remainingTerritory = getAllValidMoves(boardState, opposingPlayer).length;
|
||||||
if (remainingTerritory > 0 && boardState.passCount < 2) {
|
if (remainingTerritory > 0 && boardState.passCount < 2) {
|
||||||
return {
|
return {
|
||||||
type: playTypes.pass,
|
type: GoPlayType.pass,
|
||||||
x: -1,
|
x: -1,
|
||||||
y: -1,
|
y: -1,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
type: playTypes.gameOver,
|
type: GoPlayType.gameOver,
|
||||||
x: -1,
|
x: -1,
|
||||||
y: -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)
|
* 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> {
|
async function getFactionMove(moves: MoveOptions, faction: GoOpponent, rng: number): Promise<PointState | null> {
|
||||||
if (faction === opponents.Netburners) {
|
if (faction === GoOpponent.Netburners) {
|
||||||
return getNetburnersPriorityMove(moves, rng);
|
return getNetburnersPriorityMove(moves, rng);
|
||||||
}
|
}
|
||||||
if (faction === opponents.SlumSnakes) {
|
if (faction === GoOpponent.SlumSnakes) {
|
||||||
return getSlumSnakesPriorityMove(moves, rng);
|
return getSlumSnakesPriorityMove(moves, rng);
|
||||||
}
|
}
|
||||||
if (faction === opponents.TheBlackHand) {
|
if (faction === GoOpponent.TheBlackHand) {
|
||||||
return getBlackHandPriorityMove(moves, rng);
|
return getBlackHandPriorityMove(moves, rng);
|
||||||
}
|
}
|
||||||
if (faction === opponents.Tetrads) {
|
if (faction === GoOpponent.Tetrads) {
|
||||||
return getTetradPriorityMove(moves, rng);
|
return getTetradPriorityMove(moves, rng);
|
||||||
}
|
}
|
||||||
if (faction === opponents.Daedalus) {
|
if (faction === GoOpponent.Daedalus) {
|
||||||
return getDaedalusPriorityMove(moves, rng);
|
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
|
* Determines if certain failsafes and mistake avoidance are enabled for the given move
|
||||||
*/
|
*/
|
||||||
function isSmart(faction: opponents, rng: number) {
|
function isSmart(faction: GoOpponent, rng: number) {
|
||||||
if (faction === opponents.Netburners) {
|
if (faction === GoOpponent.Netburners) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (faction === opponents.SlumSnakes) {
|
if (faction === GoOpponent.SlumSnakes) {
|
||||||
return rng < 0.3;
|
return rng < 0.3;
|
||||||
}
|
}
|
||||||
if (faction === opponents.TheBlackHand) {
|
if (faction === GoOpponent.TheBlackHand) {
|
||||||
return rng < 0.8;
|
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> {
|
async function getBlackHandPriorityMove(moves: MoveOptions, rng: number): Promise<PointState | null> {
|
||||||
if (await moves.capture()) {
|
if (await moves.capture()) {
|
||||||
console.debug("capture: capture move chosen");
|
//console.debug("capture: capture move chosen");
|
||||||
return (await moves.capture())?.point ?? null;
|
return (await moves.capture())?.point ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const surround = await moves.surround();
|
const surround = await moves.surround();
|
||||||
|
|
||||||
if (surround && surround.point && (surround.newLibertyCount ?? 999) <= 1) {
|
if (surround && surround.point && (surround.newLibertyCount ?? 999) <= 1) {
|
||||||
console.debug("surround move chosen");
|
//console.debug("surround move chosen");
|
||||||
return surround.point;
|
return surround.point;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await moves.defendCapture()) {
|
if (await moves.defendCapture()) {
|
||||||
console.debug("defend capture: defend move chosen");
|
//console.debug("defend capture: defend move chosen");
|
||||||
return (await moves.defendCapture())?.point ?? null;
|
return (await moves.defendCapture())?.point ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (surround && surround.point && (surround?.newLibertyCount ?? 999) <= 2) {
|
if (surround && surround.point && (surround?.newLibertyCount ?? 999) <= 2) {
|
||||||
console.debug("surround move chosen");
|
//console.debug("surround move chosen");
|
||||||
return surround.point;
|
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> {
|
async function getTetradPriorityMove(moves: MoveOptions, rng: number): Promise<PointState | null> {
|
||||||
if (await moves.capture()) {
|
if (await moves.capture()) {
|
||||||
console.debug("capture: capture move chosen");
|
//console.debug("capture: capture move chosen");
|
||||||
return (await moves.capture())?.point ?? null;
|
return (await moves.capture())?.point ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await moves.defendCapture()) {
|
if (await moves.defendCapture()) {
|
||||||
console.debug("defend capture: defend move chosen");
|
//console.debug("defend capture: defend move chosen");
|
||||||
return (await moves.defendCapture())?.point ?? null;
|
return (await moves.defendCapture())?.point ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await moves.pattern()) {
|
if (await moves.pattern()) {
|
||||||
console.debug("pattern match move chosen");
|
//console.debug("pattern match move chosen");
|
||||||
return (await moves.pattern())?.point ?? null;
|
return (await moves.pattern())?.point ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const surround = await moves.surround();
|
const surround = await moves.surround();
|
||||||
if (surround && surround.point && (surround?.newLibertyCount ?? 9) <= 1) {
|
if (surround && surround.point && (surround?.newLibertyCount ?? 9) <= 1) {
|
||||||
console.debug("surround move chosen");
|
//console.debug("surround move chosen");
|
||||||
return surround.point;
|
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> {
|
async function getIlluminatiPriorityMove(moves: MoveOptions, rng: number): Promise<PointState | null> {
|
||||||
if (await moves.capture()) {
|
if (await moves.capture()) {
|
||||||
console.debug("capture: capture move chosen");
|
//console.debug("capture: capture move chosen");
|
||||||
return (await moves.capture())?.point ?? null;
|
return (await moves.capture())?.point ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await moves.defendCapture()) {
|
if (await moves.defendCapture()) {
|
||||||
console.debug("defend capture: defend move chosen");
|
//console.debug("defend capture: defend move chosen");
|
||||||
return (await moves.defendCapture())?.point ?? null;
|
return (await moves.defendCapture())?.point ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await moves.eyeMove()) {
|
if (await moves.eyeMove()) {
|
||||||
console.debug("Create eye move chosen");
|
//console.debug("Create eye move chosen");
|
||||||
return (await moves.eyeMove())?.point ?? null;
|
return (await moves.eyeMove())?.point ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const surround = await moves.surround();
|
const surround = await moves.surround();
|
||||||
if (surround && surround.point && (surround?.newLibertyCount ?? 9) <= 1) {
|
if (surround && surround.point && (surround?.newLibertyCount ?? 9) <= 1) {
|
||||||
console.debug("surround move chosen");
|
//console.debug("surround move chosen");
|
||||||
return surround.point;
|
return surround.point;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await moves.eyeBlock()) {
|
if (await moves.eyeBlock()) {
|
||||||
console.debug("Block eye move chosen");
|
//console.debug("Block eye move chosen");
|
||||||
return (await moves.eyeBlock())?.point ?? null;
|
return (await moves.eyeBlock())?.point ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await moves.corner()) {
|
if (await moves.corner()) {
|
||||||
console.debug("Corner move chosen");
|
//console.debug("Corner move chosen");
|
||||||
return (await moves.corner())?.point ?? null;
|
return (await moves.corner())?.point ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,17 +310,17 @@ async function getIlluminatiPriorityMove(moves: MoveOptions, rng: number): Promi
|
|||||||
const usePattern = rng > 0.25 || !hasMoves;
|
const usePattern = rng > 0.25 || !hasMoves;
|
||||||
|
|
||||||
if ((await moves.pattern()) && usePattern) {
|
if ((await moves.pattern()) && usePattern) {
|
||||||
console.debug("pattern match move chosen");
|
//console.debug("pattern match move chosen");
|
||||||
return (await moves.pattern())?.point ?? null;
|
return (await moves.pattern())?.point ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rng > 0.4 && (await moves.jump())) {
|
if (rng > 0.4 && (await moves.jump())) {
|
||||||
console.debug("Jump move chosen");
|
//console.debug("Jump move chosen");
|
||||||
return (await moves.jump())?.point ?? null;
|
return (await moves.jump())?.point ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rng < 0.6 && surround && surround.point && (surround?.newLibertyCount ?? 9) <= 2) {
|
if (rng < 0.6 && surround && surround.point && (surround?.newLibertyCount ?? 9) <= 2) {
|
||||||
console.debug("surround move chosen");
|
//console.debug("surround move chosen");
|
||||||
return surround.point;
|
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
|
* Get a move that places a piece to influence (and later control) a corner
|
||||||
*/
|
*/
|
||||||
function getCornerMove(boardState: BoardState) {
|
function getCornerMove(board: Board) {
|
||||||
const boardEdge = boardState.board[0].length - 1;
|
const boardEdge = board[0].length - 1;
|
||||||
const cornerMax = boardEdge - 2;
|
const cornerMax = boardEdge - 2;
|
||||||
if (isCornerAvailableForMove(boardState, cornerMax, cornerMax, boardEdge, boardEdge)) {
|
if (isCornerAvailableForMove(board, cornerMax, cornerMax, boardEdge, boardEdge)) {
|
||||||
return boardState.board[cornerMax][cornerMax];
|
return board[cornerMax][cornerMax];
|
||||||
}
|
}
|
||||||
if (isCornerAvailableForMove(boardState, 0, cornerMax, cornerMax, boardEdge)) {
|
if (isCornerAvailableForMove(board, 0, cornerMax, cornerMax, boardEdge)) {
|
||||||
return boardState.board[2][cornerMax];
|
return board[2][cornerMax];
|
||||||
}
|
}
|
||||||
if (isCornerAvailableForMove(boardState, 0, 0, 2, 2)) {
|
if (isCornerAvailableForMove(board, 0, 0, 2, 2)) {
|
||||||
return boardState.board[2][2];
|
return board[2][2];
|
||||||
}
|
}
|
||||||
if (isCornerAvailableForMove(boardState, cornerMax, 0, boardEdge, 2)) {
|
if (isCornerAvailableForMove(board, cornerMax, 0, boardEdge, 2)) {
|
||||||
return boardState.board[cornerMax][2];
|
return board[cornerMax][2];
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -360,9 +351,9 @@ function getCornerMove(boardState: BoardState) {
|
|||||||
/**
|
/**
|
||||||
* Find all non-offline nodes in a given area
|
* 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[] = [];
|
const foundPoints: PointState[] = [];
|
||||||
boardState.board.forEach((column) =>
|
board.forEach((column) =>
|
||||||
column.forEach(
|
column.forEach(
|
||||||
(point) => point && point.x >= x1 && point.x <= x2 && point.y >= y1 && point.y <= y2 && foundPoints.push(point),
|
(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
|
* 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) {
|
function isCornerAvailableForMove(board: Board, x1: number, y1: number, x2: number, y2: number) {
|
||||||
const foundPoints = findLiveNodesInArea(boardState, x1, y1, x2, y2);
|
const foundPoints = findLiveNodesInArea(board, x1, y1, x2, y2);
|
||||||
const foundPieces = foundPoints.filter((point) => point.player !== playerColors.empty);
|
const foundPieces = foundPoints.filter((point) => point.color !== GoColor.empty);
|
||||||
return foundPoints.length >= 7 ? foundPieces.length === 0 : false;
|
return foundPoints.length >= 7 ? foundPieces.length === 0 : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select a move from the list of open-area moves
|
* Select a move from the list of open-area moves
|
||||||
*/
|
*/
|
||||||
function getExpansionMove(
|
function getExpansionMove(board: Board, availableSpaces: PointState[], rng: number, moveArray?: Move[]) {
|
||||||
boardState: BoardState,
|
const moveOptions = moveArray ?? getExpansionMoveArray(board, availableSpaces);
|
||||||
player: PlayerColor,
|
|
||||||
availableSpaces: PointState[],
|
|
||||||
rng: number,
|
|
||||||
moveArray?: Move[],
|
|
||||||
) {
|
|
||||||
const moveOptions = moveArray ?? getExpansionMoveArray(boardState, player, availableSpaces);
|
|
||||||
const randomIndex = floor(rng * moveOptions.length);
|
const randomIndex = floor(rng * moveOptions.length);
|
||||||
return moveOptions[randomIndex];
|
return moveOptions[randomIndex];
|
||||||
}
|
}
|
||||||
@ -397,21 +382,14 @@ function getExpansionMove(
|
|||||||
/**
|
/**
|
||||||
* Get a move in open space that is nearby a friendly piece
|
* Get a move in open space that is nearby a friendly piece
|
||||||
*/
|
*/
|
||||||
function getJumpMove(
|
function getJumpMove(board: Board, player: GoColor, availableSpaces: PointState[], rng: number, moveArray?: Move[]) {
|
||||||
boardState: BoardState,
|
const moveOptions = (moveArray ?? getExpansionMoveArray(board, availableSpaces)).filter(({ point }) =>
|
||||||
player: PlayerColor,
|
|
||||||
availableSpaces: PointState[],
|
|
||||||
rng: number,
|
|
||||||
moveArray?: Move[],
|
|
||||||
) {
|
|
||||||
const board = boardState.board;
|
|
||||||
const moveOptions = (moveArray ?? getExpansionMoveArray(boardState, player, availableSpaces)).filter(({ point }) =>
|
|
||||||
[
|
[
|
||||||
board[point.x]?.[point.y + 2],
|
board[point.x]?.[point.y + 2],
|
||||||
board[point.x + 2]?.[point.y],
|
board[point.x + 2]?.[point.y],
|
||||||
board[point.x]?.[point.y - 2],
|
board[point.x]?.[point.y - 2],
|
||||||
board[point.x - 2]?.[point.y],
|
board[point.x - 2]?.[point.y],
|
||||||
].some((point) => point?.player === player),
|
].some((point) => point?.color === player),
|
||||||
);
|
);
|
||||||
|
|
||||||
const randomIndex = floor(rng * moveOptions.length);
|
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
|
* Finds a move in an open area to expand influence and later build on
|
||||||
*/
|
*/
|
||||||
export function getExpansionMoveArray(
|
export function getExpansionMoveArray(board: Board, availableSpaces: PointState[]): Move[] {
|
||||||
boardState: BoardState,
|
|
||||||
player: PlayerColor,
|
|
||||||
availableSpaces: PointState[],
|
|
||||||
): Move[] {
|
|
||||||
// Look for any empty spaces fully surrounded by empty spaces to expand into
|
// Look for any empty spaces fully surrounded by empty spaces to expand into
|
||||||
const emptySpaces = availableSpaces.filter((space) => {
|
const emptySpaces = availableSpaces.filter((space) => {
|
||||||
const neighbors = findNeighbors(boardState, space.x, space.y);
|
const neighbors = findNeighbors(board, space.x, space.y);
|
||||||
return (
|
return (
|
||||||
[neighbors.north, neighbors.east, neighbors.south, neighbors.west].filter(
|
[neighbors.north, neighbors.east, neighbors.south, neighbors.west].filter(
|
||||||
(point) => point && point.player === playerColors.empty,
|
(point) => point && point.color === GoColor.empty,
|
||||||
).length === 4
|
).length === 4
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Once no such empty areas exist anymore, instead expand into any disputed territory
|
// Once no such empty areas exist anymore, instead expand into any disputed territory
|
||||||
// to gain a few more points in endgame
|
// 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];
|
const moveOptions = [...emptySpaces, ...disputedSpaces];
|
||||||
|
|
||||||
@ -451,23 +425,14 @@ export function getExpansionMoveArray(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisputedTerritoryMoves(
|
function getDisputedTerritoryMoves(board: Board, availableSpaces: PointState[], maxChainSize = 99) {
|
||||||
boardState: BoardState,
|
const chains = getAllChains(board).filter((chain) => chain.length <= maxChainSize);
|
||||||
player: PlayerColor,
|
|
||||||
availableSpaces: PointState[],
|
|
||||||
maxChainSize = 99,
|
|
||||||
) {
|
|
||||||
const chains = getAllChains(boardState).filter((chain) => chain.length <= maxChainSize);
|
|
||||||
|
|
||||||
return availableSpaces.filter((space) => {
|
return availableSpaces.filter((space) => {
|
||||||
const chain = chains.find((chain) => chain[0].chain === space.chain) ?? [];
|
const chain = chains.find((chain) => chain[0].chain === space.chain) ?? [];
|
||||||
const playerNeighbors = getAllNeighboringChains(boardState, chain, chains);
|
const playerNeighbors = getAllNeighboringChains(board, chain, chains);
|
||||||
const hasWhitePieceNeighbor = playerNeighbors.find(
|
const hasWhitePieceNeighbor = playerNeighbors.find((neighborChain) => neighborChain[0]?.color === GoColor.white);
|
||||||
(neighborChain) => neighborChain[0]?.player === playerColors.white,
|
const hasBlackPieceNeighbor = playerNeighbors.find((neighborChain) => neighborChain[0]?.color === GoColor.black);
|
||||||
);
|
|
||||||
const hasBlackPieceNeighbor = playerNeighbors.find(
|
|
||||||
(neighborChain) => neighborChain[0]?.player === playerColors.black,
|
|
||||||
);
|
|
||||||
|
|
||||||
return hasWhitePieceNeighbor && hasBlackPieceNeighbor;
|
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.
|
* 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[]) {
|
async function getLibertyGrowthMoves(board: Board, player: GoColor, availableSpaces: PointState[]) {
|
||||||
const friendlyChains = getAllChains(boardState).filter((chain) => chain[0].player === player);
|
const friendlyChains = getAllChains(board).filter((chain) => chain[0].color === player);
|
||||||
|
|
||||||
if (!friendlyChains.length) {
|
if (!friendlyChains.length) {
|
||||||
return [];
|
return [];
|
||||||
@ -503,10 +468,10 @@ async function getLibertyGrowthMoves(boardState: BoardState, player: PlayerColor
|
|||||||
.map((liberty) => {
|
.map((liberty) => {
|
||||||
const move = liberty.libertyPoint;
|
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
|
// 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 {
|
return {
|
||||||
point: move,
|
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
|
* Find a move that increases the player's liberties by the maximum amount
|
||||||
*/
|
*/
|
||||||
async function getGrowthMove(
|
async function getGrowthMove(board: Board, player: GoColor, availableSpaces: PointState[], rng: number) {
|
||||||
initialState: BoardState,
|
const growthMoves = await getLibertyGrowthMoves(board, player, availableSpaces);
|
||||||
player: PlayerColor,
|
|
||||||
availableSpaces: PointState[],
|
|
||||||
rng: number,
|
|
||||||
) {
|
|
||||||
const growthMoves = await getLibertyGrowthMoves(initialState, player, availableSpaces);
|
|
||||||
|
|
||||||
const maxLibertyCount = Math.max(...growthMoves.map((l) => l.newLibertyCount - l.oldLibertyCount));
|
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
|
* 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[]) {
|
async function getDefendMove(board: Board, player: GoColor, availableSpaces: PointState[]) {
|
||||||
const growthMoves = await getLibertyGrowthMoves(initialState, player, availableSpaces);
|
const growthMoves = await getLibertyGrowthMoves(board, player, availableSpaces);
|
||||||
const libertyIncreases =
|
const libertyIncreases =
|
||||||
growthMoves?.filter((move) => move.oldLibertyCount <= 1 && move.newLibertyCount > move.oldLibertyCount) ?? [];
|
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,
|
* Find a move that reduces the opponent's liberties as much as possible,
|
||||||
* capturing (or making it easier to capture) their pieces
|
* capturing (or making it easier to capture) their pieces
|
||||||
*/
|
*/
|
||||||
async function getSurroundMove(
|
async function getSurroundMove(board: Board, player: GoColor, availableSpaces: PointState[], smart = true) {
|
||||||
boardState: BoardState,
|
const opposingPlayer = player === GoColor.black ? GoColor.white : GoColor.black;
|
||||||
player: PlayerColor,
|
const enemyChains = getAllChains(board).filter((chain) => chain[0].color === opposingPlayer);
|
||||||
availableSpaces: PointState[],
|
|
||||||
smart = true,
|
|
||||||
) {
|
|
||||||
const opposingPlayer = player === playerColors.black ? playerColors.white : playerColors.black;
|
|
||||||
const enemyChains = getAllChains(boardState).filter((chain) => chain[0].player === opposingPlayer);
|
|
||||||
|
|
||||||
if (!enemyChains.length || !availableSpaces.length) {
|
if (!enemyChains.length || !availableSpaces.length) {
|
||||||
return null;
|
return null;
|
||||||
@ -580,13 +535,13 @@ async function getSurroundMove(
|
|||||||
const surroundMoves: Move[] = [];
|
const surroundMoves: Move[] = [];
|
||||||
|
|
||||||
enemyLiberties.forEach((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(
|
const weakestEnemyChain = findEnemyNeighborChainWithFewestLiberties(
|
||||||
boardState,
|
board,
|
||||||
move.x,
|
move.x,
|
||||||
move.y,
|
move.y,
|
||||||
player === playerColors.black ? playerColors.white : playerColors.black,
|
player === GoColor.black ? GoColor.white : GoColor.black,
|
||||||
);
|
);
|
||||||
const weakestEnemyChainLength = weakestEnemyChain?.length ?? 99;
|
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,
|
* 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)
|
* and suiciding your own pieces is not legal unless it captures the opponents' first)
|
||||||
*/
|
*/
|
||||||
function getEyeCreationMoves(
|
function getEyeCreationMoves(board: Board, player: GoColor, availableSpaces: PointState[], maxLiberties = 99) {
|
||||||
boardState: BoardState,
|
const allEyes = getAllEyesByChainId(board, player);
|
||||||
player: PlayerColor,
|
const currentEyes = getAllEyes(board, player, allEyes);
|
||||||
availableSpaces: PointState[],
|
|
||||||
maxLiberties = 99,
|
|
||||||
) {
|
|
||||||
const allEyes = getAllEyesByChainId(boardState, player);
|
|
||||||
const currentEyes = getAllEyes(boardState, player, allEyes);
|
|
||||||
|
|
||||||
const currentLivingGroupIDs = Object.keys(allEyes).filter((chainId) => allEyes[chainId].length >= 2);
|
const currentLivingGroupIDs = Object.keys(allEyes).filter((chainId) => allEyes[chainId].length >= 2);
|
||||||
const currentLivingGroupsCount = currentLivingGroupIDs.length;
|
const currentLivingGroupsCount = currentLivingGroupIDs.length;
|
||||||
const currentEyeCount = currentEyes.filter((eye) => eye.length).length;
|
const currentEyeCount = currentEyes.filter((eye) => eye.length).length;
|
||||||
|
|
||||||
const chains = getAllChains(boardState);
|
const chains = getAllChains(board);
|
||||||
const friendlyLiberties = chains
|
const friendlyLiberties = chains
|
||||||
.filter((chain) => chain[0].player === player)
|
.filter((chain) => chain[0].color === player)
|
||||||
.filter((chain) => chain.length > 1)
|
.filter((chain) => chain.length > 1)
|
||||||
.filter((chain) => chain[0].liberties && chain[0].liberties?.length <= maxLiberties)
|
.filter((chain) => chain[0].liberties && chain[0].liberties?.length <= maxLiberties)
|
||||||
.filter((chain) => !currentLivingGroupIDs.includes(chain[0].chain))
|
.filter((chain) => !currentLivingGroupIDs.includes(chain[0].chain))
|
||||||
@ -672,16 +622,16 @@ function getEyeCreationMoves(
|
|||||||
availableSpaces.find((availablePoint) => availablePoint.x === point.x && availablePoint.y === point.y),
|
availableSpaces.find((availablePoint) => availablePoint.x === point.x && availablePoint.y === point.y),
|
||||||
)
|
)
|
||||||
.filter((point: PointState) => {
|
.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];
|
const neighborhood = [neighbors.north, neighbors.east, neighbors.south, neighbors.west];
|
||||||
return (
|
return (
|
||||||
neighborhood.filter((point) => !point || point?.player === player).length >= 2 &&
|
neighborhood.filter((point) => !point || point?.color === player).length >= 2 &&
|
||||||
neighborhood.some((point) => point?.player === playerColors.empty)
|
neighborhood.some((point) => point?.color === GoColor.empty)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const eyeCreationMoves = friendlyLiberties.reduce((moveOptions: EyeMove[], point: PointState) => {
|
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 newEyes = getAllEyes(evaluationBoard, player);
|
||||||
const newLivingGroupsCount = newEyes.filter((eye) => eye.length >= 2).length;
|
const newLivingGroupsCount = newEyes.filter((eye) => eye.length >= 2).length;
|
||||||
const newEyeCount = newEyes.filter((eye) => eye.length).length;
|
const newEyeCount = newEyes.filter((eye) => eye.length).length;
|
||||||
@ -700,16 +650,16 @@ function getEyeCreationMoves(
|
|||||||
return eyeCreationMoves.sort((moveA, moveB) => +moveB.createsLife - +moveA.createsLife);
|
return eyeCreationMoves.sort((moveA, moveB) => +moveB.createsLife - +moveA.createsLife);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEyeCreationMove(boardState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
|
function getEyeCreationMove(board: Board, player: GoColor, availableSpaces: PointState[]) {
|
||||||
return getEyeCreationMoves(boardState, player, availableSpaces)[0];
|
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
|
* 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[]) {
|
function getEyeBlockingMove(board: Board, player: GoColor, availablePoints: PointState[]) {
|
||||||
const opposingPlayer = player === playerColors.white ? playerColors.black : playerColors.white;
|
const opposingPlayer = player === GoColor.white ? GoColor.black : GoColor.white;
|
||||||
const opponentEyeMoves = getEyeCreationMoves(boardState, opposingPlayer, availablePoints, 5);
|
const opponentEyeMoves = getEyeCreationMoves(board, opposingPlayer, availablePoints, 5);
|
||||||
const twoEyeMoves = opponentEyeMoves.filter((move) => move.createsLife);
|
const twoEyeMoves = opponentEyeMoves.filter((move) => move.createsLife);
|
||||||
const oneEyeMoves = 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(
|
function getMoveOptions(
|
||||||
boardState: BoardState,
|
boardState: BoardState,
|
||||||
player: PlayerColor,
|
player: GoColor,
|
||||||
rng: number,
|
rng: number,
|
||||||
smart = true,
|
smart = true,
|
||||||
): { [s in keyof MoveOptions]: () => Promise<Move | null> } {
|
): { [s in keyof MoveOptions]: () => Promise<Move | null> } {
|
||||||
|
const board = boardState.board;
|
||||||
const availableSpaces = findDisputedTerritory(boardState, player, smart);
|
const availableSpaces = findDisputedTerritory(boardState, player, smart);
|
||||||
const contestedPoints = getDisputedTerritoryMoves(boardState, player, availableSpaces);
|
const contestedPoints = getDisputedTerritoryMoves(board, availableSpaces);
|
||||||
const expansionMoves = getExpansionMoveArray(boardState, player, 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
|
// 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
|
// needlessly extend the game, unless they actually can change the score
|
||||||
@ -768,22 +719,19 @@ function getMoveOptions(
|
|||||||
? defendMove
|
? defendMove
|
||||||
: null;
|
: null;
|
||||||
},
|
},
|
||||||
eyeMove: async () => (endGameAvailable ? null : getEyeCreationMove(boardState, player, availableSpaces) ?? null),
|
eyeMove: async () => (endGameAvailable ? null : getEyeCreationMove(board, player, availableSpaces) ?? null),
|
||||||
eyeBlock: async () => (endGameAvailable ? null : getEyeBlockingMove(boardState, player, availableSpaces) ?? null),
|
eyeBlock: async () => (endGameAvailable ? null : getEyeBlockingMove(board, player, availableSpaces) ?? null),
|
||||||
pattern: async () => {
|
pattern: async () => {
|
||||||
const point = endGameAvailable
|
const point = endGameAvailable ? null : await findAnyMatchedPatterns(board, player, availableSpaces, smart, rng);
|
||||||
? null
|
|
||||||
: await findAnyMatchedPatterns(boardState, player, availableSpaces, smart, rng);
|
|
||||||
return point ? { point } : null;
|
return point ? { point } : null;
|
||||||
},
|
},
|
||||||
growth: async () =>
|
growth: async () => (endGameAvailable ? null : (await getGrowthMove(board, player, availableSpaces, rng)) ?? null),
|
||||||
endGameAvailable ? null : (await getGrowthMove(boardState, player, availableSpaces, rng)) ?? null,
|
expansion: async () => (await getExpansionMove(board, availableSpaces, rng, expansionMoves)) ?? null,
|
||||||
expansion: async () => (await getExpansionMove(boardState, player, availableSpaces, rng, expansionMoves)) ?? null,
|
jump: async () => (await getJumpMove(board, player, availableSpaces, rng, expansionMoves)) ?? null,
|
||||||
jump: async () => (await getJumpMove(boardState, player, availableSpaces, rng, expansionMoves)) ?? null,
|
defend: async () => (await getDefendMove(board, player, availableSpaces)) ?? null,
|
||||||
defend: async () => (await getDefendMove(boardState, player, availableSpaces)) ?? null,
|
surround: async () => (await getSurroundMove(board, player, availableSpaces, smart)) ?? null,
|
||||||
surround: async () => (await getSurroundMove(boardState, player, availableSpaces, smart)) ?? null,
|
|
||||||
corner: async () => {
|
corner: async () => {
|
||||||
const point = getCornerMove(boardState);
|
const point = getCornerMove(board);
|
||||||
return point ? { point } : null;
|
return point ? { point } : null;
|
||||||
},
|
},
|
||||||
random: async () => {
|
random: async () => {
|
||||||
@ -811,7 +759,7 @@ function getMoveOptions(
|
|||||||
/**
|
/**
|
||||||
* Gets the starting score for white.
|
* Gets the starting score for white.
|
||||||
*/
|
*/
|
||||||
export function getKomi(opponent: opponents) {
|
export function getKomi(opponent: GoOpponent) {
|
||||||
return opponentDetails[opponent].komi;
|
return opponentDetails[opponent].komi;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -823,5 +771,5 @@ export function sleep(ms: number): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function showWorldDemon() {
|
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
|
// 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 { sleep } from "./goAI";
|
||||||
import { findEffectiveLibertiesOfNewMove } from "./boardAnalysis";
|
import { findEffectiveLibertiesOfNewMove } from "./boardAnalysis";
|
||||||
import { floor } from "../boardState/boardState";
|
import { floor } from "../boardState/boardState";
|
||||||
@ -78,25 +80,24 @@ export const threeByThreePatterns = [
|
|||||||
* Searches the board for any point that matches the expanded pattern set
|
* Searches the board for any point that matches the expanded pattern set
|
||||||
*/
|
*/
|
||||||
export async function findAnyMatchedPatterns(
|
export async function findAnyMatchedPatterns(
|
||||||
boardState: BoardState,
|
board: Board,
|
||||||
player: PlayerColor,
|
player: GoColor,
|
||||||
availableSpaces: PointState[],
|
availableSpaces: PointState[],
|
||||||
smart = true,
|
smart = true,
|
||||||
rng: number,
|
rng: number,
|
||||||
) {
|
) {
|
||||||
const board = boardState.board;
|
|
||||||
const boardSize = board[0].length;
|
const boardSize = board[0].length;
|
||||||
const patterns = expandAllThreeByThreePatterns();
|
const patterns = expandAllThreeByThreePatterns();
|
||||||
const moves = [];
|
const moves = [];
|
||||||
for (let x = 0; x < boardSize; x++) {
|
for (let x = 0; x < boardSize; x++) {
|
||||||
for (let y = 0; y < boardSize; y++) {
|
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));
|
const matchedPattern = patterns.find((pattern) => checkMatch(neighborhood, pattern, player));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
matchedPattern &&
|
matchedPattern &&
|
||||||
availableSpaces.find((availablePoint) => availablePoint.x === x && availablePoint.y === y) &&
|
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]);
|
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.
|
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 patternArr = pattern.join("").split("");
|
||||||
const neighborhoodArray = neighborhood.flat();
|
const neighborhoodArray = neighborhood.flat();
|
||||||
return patternArr.every((str, index) => matches(str, neighborhoodArray[index], player));
|
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
|
* Gets the 8 points adjacent and diagonally adjacent to the given point
|
||||||
*/
|
*/
|
||||||
function getNeighborhood(boardState: BoardState, x: number, y: number) {
|
function getNeighborhood(board: Board, x: number, y: number) {
|
||||||
const board = boardState.board;
|
|
||||||
return [
|
return [
|
||||||
[board[x - 1]?.[y - 1], board[x - 1]?.[y], board[x - 1]?.[y + 1]],
|
[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]],
|
[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
|
* A space " " only matches the edge of the board
|
||||||
* question mark "?" matches anything
|
* question mark "?" matches anything
|
||||||
*/
|
*/
|
||||||
function matches(stringPoint: string, point: PointState | null, player: PlayerColor) {
|
function matches(stringPoint: string, point: PointState | null, player: GoColor) {
|
||||||
const opponent = player === playerColors.white ? playerColors.black : playerColors.white;
|
const opponent = player === GoColor.white ? GoColor.black : GoColor.white;
|
||||||
switch (stringPoint) {
|
switch (stringPoint) {
|
||||||
case "X": {
|
case "X": {
|
||||||
return point?.player === player;
|
return point?.color === player;
|
||||||
}
|
}
|
||||||
case "O": {
|
case "O": {
|
||||||
return point?.player === opponent;
|
return point?.color === opponent;
|
||||||
}
|
}
|
||||||
case "x": {
|
case "x": {
|
||||||
return point?.player !== opponent;
|
return point?.color !== opponent;
|
||||||
}
|
}
|
||||||
case "o": {
|
case "o": {
|
||||||
return point?.player !== player;
|
return point?.color !== player;
|
||||||
}
|
}
|
||||||
case ".": {
|
case ".": {
|
||||||
return point?.player === playerColors.empty;
|
return point?.color === GoColor.empty;
|
||||||
}
|
}
|
||||||
case " ": {
|
case " ": {
|
||||||
return point === null;
|
return point === null;
|
||||||
|
@ -1,18 +1,15 @@
|
|||||||
import {
|
import type { Board, BoardState, PointState } from "../Types";
|
||||||
BoardState,
|
|
||||||
getGoPlayerStartingState,
|
import { Player } from "@player";
|
||||||
opponents,
|
import { GoOpponent, GoColor } from "@enums";
|
||||||
PlayerColor,
|
import { newOpponentStats } from "../Constants";
|
||||||
playerColors,
|
|
||||||
PointState,
|
|
||||||
} from "../boardState/goConstants";
|
|
||||||
import { getAllChains, getPlayerNeighbors } from "./boardAnalysis";
|
import { getAllChains, getPlayerNeighbors } from "./boardAnalysis";
|
||||||
import { getKomi } from "./goAI";
|
import { getKomi } from "./goAI";
|
||||||
import { Player } from "@player";
|
|
||||||
import { getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
|
import { getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
|
||||||
import { floor, isNotNull } from "../boardState/boardState";
|
import { floor, isNotNull } from "../boardState/boardState";
|
||||||
import { Factions } from "../../Faction/Factions";
|
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.
|
* Returns the score of the current board.
|
||||||
@ -21,22 +18,22 @@ import { FactionName } from "@enums";
|
|||||||
*/
|
*/
|
||||||
export function getScore(boardState: BoardState) {
|
export function getScore(boardState: BoardState) {
|
||||||
const komi = getKomi(boardState.ai) ?? 6.5;
|
const komi = getKomi(boardState.ai) ?? 6.5;
|
||||||
const whitePieces = getColoredPieceCount(boardState, playerColors.white);
|
const whitePieces = getColoredPieceCount(boardState, GoColor.white);
|
||||||
const blackPieces = getColoredPieceCount(boardState, playerColors.black);
|
const blackPieces = getColoredPieceCount(boardState, GoColor.black);
|
||||||
const territoryScores = getTerritoryScores(boardState);
|
const territoryScores = getTerritoryScores(boardState.board);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
[playerColors.white]: {
|
[GoColor.white]: {
|
||||||
pieces: whitePieces,
|
pieces: whitePieces,
|
||||||
territory: territoryScores[playerColors.white],
|
territory: territoryScores[GoColor.white],
|
||||||
komi: komi,
|
komi: komi,
|
||||||
sum: whitePieces + territoryScores[playerColors.white] + komi,
|
sum: whitePieces + territoryScores[GoColor.white] + komi,
|
||||||
},
|
},
|
||||||
[playerColors.black]: {
|
[GoColor.black]: {
|
||||||
pieces: blackPieces,
|
pieces: blackPieces,
|
||||||
territory: territoryScores[playerColors.black],
|
territory: territoryScores[GoColor.black],
|
||||||
komi: 0,
|
komi: 0,
|
||||||
sum: blackPieces + territoryScores[playerColors.black],
|
sum: blackPieces + territoryScores[GoColor.black],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -50,13 +47,13 @@ export function endGoGame(boardState: BoardState) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boardState.previousPlayer = null;
|
boardState.previousPlayer = null;
|
||||||
const statusToUpdate = getPlayerStats(boardState.ai);
|
const statusToUpdate = getOpponentStats(boardState.ai);
|
||||||
statusToUpdate.favor = statusToUpdate.favor ?? 0;
|
statusToUpdate.favor = statusToUpdate.favor ?? 0;
|
||||||
const score = getScore(boardState);
|
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);
|
resetWinstreak(boardState.ai, true);
|
||||||
statusToUpdate.nodePower += floor(score[playerColors.black].sum * 0.25);
|
statusToUpdate.nodePower += floor(score[GoColor.black].sum * 0.25);
|
||||||
} else {
|
} else {
|
||||||
statusToUpdate.wins++;
|
statusToUpdate.wins++;
|
||||||
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
|
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
|
||||||
@ -66,12 +63,12 @@ export function endGoGame(boardState: BoardState) {
|
|||||||
statusToUpdate.highestWinStreak = statusToUpdate.winStreak;
|
statusToUpdate.highestWinStreak = statusToUpdate.winStreak;
|
||||||
}
|
}
|
||||||
|
|
||||||
const factionName = boardState.ai as unknown as FactionName;
|
const factionName = getEnumHelper("FactionName").getMember(boardState.ai);
|
||||||
if (
|
if (
|
||||||
|
factionName &&
|
||||||
statusToUpdate.winStreak % 2 === 0 &&
|
statusToUpdate.winStreak % 2 === 0 &&
|
||||||
Player.factions.includes(factionName) &&
|
Player.factions.includes(factionName) &&
|
||||||
statusToUpdate.favor < getMaxFavor() &&
|
statusToUpdate.favor < getMaxFavor()
|
||||||
Factions?.[factionName]
|
|
||||||
) {
|
) {
|
||||||
Factions[factionName].favor++;
|
Factions[factionName].favor++;
|
||||||
statusToUpdate.favor++;
|
statusToUpdate.favor++;
|
||||||
@ -79,13 +76,13 @@ export function endGoGame(boardState: BoardState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
statusToUpdate.nodePower +=
|
statusToUpdate.nodePower +=
|
||||||
score[playerColors.black].sum *
|
score[GoColor.black].sum *
|
||||||
getDifficultyMultiplier(score[playerColors.white].komi, boardState.board[0].length) *
|
getDifficultyMultiplier(score[GoColor.white].komi, boardState.board[0].length) *
|
||||||
getWinstreakMultiplier(statusToUpdate.winStreak, statusToUpdate.oldWinStreak);
|
getWinstreakMultiplier(statusToUpdate.winStreak, statusToUpdate.oldWinStreak);
|
||||||
|
|
||||||
statusToUpdate.nodes += score[playerColors.black].sum;
|
statusToUpdate.nodes += score[GoColor.black].sum;
|
||||||
Player.go.boardState = boardState;
|
Go.currentGame = boardState;
|
||||||
Player.go.previousGameFinalBoardState = boardState;
|
Go.previousGame = boardState;
|
||||||
|
|
||||||
// Update multipliers with new bonuses, once at the end of the game
|
// Update multipliers with new bonuses, once at the end of the game
|
||||||
Player.applyEntropy(Player.entropy);
|
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
|
* Sets the winstreak to zero for the given opponent, and adds a loss
|
||||||
*/
|
*/
|
||||||
export function resetWinstreak(opponent: opponents, gameComplete: boolean) {
|
export function resetWinstreak(opponent: GoOpponent, gameComplete: boolean) {
|
||||||
const statusToUpdate = getPlayerStats(opponent);
|
const statusToUpdate = getOpponentStats(opponent);
|
||||||
statusToUpdate.losses++;
|
statusToUpdate.losses++;
|
||||||
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
|
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
|
||||||
if (statusToUpdate.winStreak >= 0) {
|
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
|
* 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(
|
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,
|
0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -119,22 +116,20 @@ function getColoredPieceCount(boardState: BoardState, color: PlayerColor) {
|
|||||||
/**
|
/**
|
||||||
* Finds all empty spaces fully surrounded by a single player's stones
|
* Finds all empty spaces fully surrounded by a single player's stones
|
||||||
*/
|
*/
|
||||||
function getTerritoryScores(boardState: BoardState) {
|
function getTerritoryScores(board: Board) {
|
||||||
const emptyTerritoryChains = getAllChains(boardState).filter((chain) => chain?.[0]?.player === playerColors.empty);
|
const emptyTerritoryChains = getAllChains(board).filter((chain) => chain?.[0]?.color === GoColor.empty);
|
||||||
|
|
||||||
return emptyTerritoryChains.reduce(
|
return emptyTerritoryChains.reduce(
|
||||||
(scores, currentChain) => {
|
(scores, currentChain) => {
|
||||||
const chainColor = checkTerritoryOwnership(boardState, currentChain);
|
const chainColor = checkTerritoryOwnership(board, currentChain);
|
||||||
return {
|
return {
|
||||||
[playerColors.white]:
|
[GoColor.white]: scores[GoColor.white] + (chainColor === GoColor.white ? currentChain.length : 0),
|
||||||
scores[playerColors.white] + (chainColor === playerColors.white ? currentChain.length : 0),
|
[GoColor.black]: scores[GoColor.black] + (chainColor === GoColor.black ? currentChain.length : 0),
|
||||||
[playerColors.black]:
|
|
||||||
scores[playerColors.black] + (chainColor === playerColors.black ? currentChain.length : 0),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[playerColors.white]: 0,
|
[GoColor.white]: 0,
|
||||||
[playerColors.black]: 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
|
* 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[]) {
|
function checkTerritoryOwnership(board: Board, emptyPointChain: PointState[]) {
|
||||||
if (emptyPointChain.length > boardState.board[0].length ** 2 - 3) {
|
if (emptyPointChain.length > board[0].length ** 2 - 3) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerNeighbors = getPlayerNeighbors(boardState, emptyPointChain);
|
const playerNeighbors = getPlayerNeighbors(board, emptyPointChain);
|
||||||
const hasWhitePieceNeighbors = playerNeighbors.find((p) => p.player === playerColors.white);
|
const hasWhitePieceNeighbors = playerNeighbors.find((p) => p.color === GoColor.white);
|
||||||
const hasBlackPieceNeighbors = playerNeighbors.find((p) => p.player === playerColors.black);
|
const hasBlackPieceNeighbors = playerNeighbors.find((p) => p.color === GoColor.black);
|
||||||
const isWhiteTerritory = hasWhitePieceNeighbors && !hasBlackPieceNeighbors;
|
const isWhiteTerritory = hasWhitePieceNeighbors && !hasBlackPieceNeighbors;
|
||||||
const isBlackTerritory = hasBlackPieceNeighbors && !hasWhitePieceNeighbors;
|
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) {
|
export function getOpponentStats(opponent: GoOpponent) {
|
||||||
if (!Player.go.status[opponent]) {
|
return Go.stats[opponent] ?? (Go.stats[opponent] = newOpponentStats());
|
||||||
Player.go = getGoPlayerStartingState();
|
|
||||||
}
|
|
||||||
return Player.go.status[opponent];
|
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,33 @@
|
|||||||
import {
|
import type { Board, BoardState, Move, Neighbor, PointState } from "../Types";
|
||||||
bitverseBoardShape,
|
|
||||||
Board,
|
import { GoOpponent, GoColor, GoValidity } from "@enums";
|
||||||
BoardState,
|
import { bitverseBoardShape } from "../Constants";
|
||||||
Move,
|
|
||||||
Neighbor,
|
|
||||||
opponents,
|
|
||||||
PlayerColor,
|
|
||||||
playerColors,
|
|
||||||
PointState,
|
|
||||||
validityReason,
|
|
||||||
} from "./goConstants";
|
|
||||||
import { getExpansionMoveArray } from "../boardAnalysis/goAI";
|
import { getExpansionMoveArray } from "../boardAnalysis/goAI";
|
||||||
import {
|
import {
|
||||||
evaluateIfMoveIsValid,
|
evaluateIfMoveIsValid,
|
||||||
findAllCapturedChains,
|
findAllCapturedChains,
|
||||||
findLibertiesForChain,
|
findLibertiesForChain,
|
||||||
getAllChains,
|
getAllChains,
|
||||||
getBoardFromSimplifiedBoardState,
|
boardFromSimpleBoard,
|
||||||
|
simpleBoardFromBoard,
|
||||||
} from "../boardAnalysis/boardAnalysis";
|
} from "../boardAnalysis/boardAnalysis";
|
||||||
import { endGoGame } from "../boardAnalysis/scoring";
|
import { endGoGame } from "../boardAnalysis/scoring";
|
||||||
import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes";
|
import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes";
|
||||||
|
|
||||||
/**
|
/** Generates a new BoardState object with the given opponent and size. Optionally use an existing board. */
|
||||||
* Generates a new BoardState object with the given opponent and size
|
|
||||||
*/
|
|
||||||
export function getNewBoardState(
|
export function getNewBoardState(
|
||||||
boardSize: number,
|
boardSize: number,
|
||||||
ai = opponents.Netburners,
|
ai = GoOpponent.Netburners,
|
||||||
applyObstacles = false,
|
applyObstacles = false,
|
||||||
boardToCopy?: Board,
|
boardToCopy?: Board,
|
||||||
): BoardState {
|
): BoardState {
|
||||||
if (ai === opponents.w0r1d_d43m0n) {
|
if (ai === GoOpponent.w0r1d_d43m0n) {
|
||||||
boardToCopy = resetCoordinates(rotate90Degrees(getBoardFromSimplifiedBoardState(bitverseBoardShape).board));
|
boardToCopy = resetCoordinates(rotate90Degrees(boardFromSimpleBoard(bitverseBoardShape)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBoardState = {
|
const newBoardState: BoardState = {
|
||||||
history: [],
|
previousBoard: null,
|
||||||
previousPlayer: playerColors.white,
|
previousPlayer: GoColor.white,
|
||||||
ai: ai,
|
ai: ai,
|
||||||
passCount: 0,
|
passCount: 0,
|
||||||
cheatCount: 0,
|
cheatCount: 0,
|
||||||
@ -44,7 +35,7 @@ export function getNewBoardState(
|
|||||||
Array.from({ length: boardSize }, (_, y) =>
|
Array.from({ length: boardSize }, (_, y) =>
|
||||||
!boardToCopy || boardToCopy?.[x]?.[y]
|
!boardToCopy || boardToCopy?.[x]?.[y]
|
||||||
? {
|
? {
|
||||||
player: boardToCopy?.[x]?.[y]?.player ?? playerColors.empty,
|
color: boardToCopy?.[x]?.[y]?.color ?? GoColor.empty,
|
||||||
chain: "",
|
chain: "",
|
||||||
liberties: null,
|
liberties: null,
|
||||||
x,
|
x,
|
||||||
@ -61,7 +52,7 @@ export function getNewBoardState(
|
|||||||
|
|
||||||
const handicap = getHandicap(newBoardState.board[0].length, ai);
|
const handicap = getHandicap(newBoardState.board[0].length, ai);
|
||||||
if (handicap) {
|
if (handicap) {
|
||||||
applyHandicap(newBoardState, handicap);
|
applyHandicap(newBoardState.board, handicap);
|
||||||
}
|
}
|
||||||
return newBoardState;
|
return newBoardState;
|
||||||
}
|
}
|
||||||
@ -69,9 +60,9 @@ export function getNewBoardState(
|
|||||||
/**
|
/**
|
||||||
* Determines how many starting pieces the opponent has on the board
|
* 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
|
// 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 ceil(boardSize * 0.35);
|
||||||
}
|
}
|
||||||
return 0;
|
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
|
* 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
|
// Do not update on invalid moves
|
||||||
const validity = evaluateIfMoveIsValid(boardState, x, y, player, false);
|
const validity = evaluateIfMoveIsValid(boardState, x, y, player, false);
|
||||||
if (validity !== validityReason.valid || !boardState.board[x][y]?.player) {
|
if (validity !== GoValidity.valid || !boardState.board[x][y]?.color) {
|
||||||
console.debug(`Invalid move attempted! ${x} ${y} ${player} : ${validity}`);
|
//console.debug(`Invalid move attempted! ${x} ${y} ${player} : ${validity}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
boardState.history.push(getBoardCopy(boardState).board);
|
boardState.previousBoard = simpleBoardFromBoard(boardState.board);
|
||||||
boardState.history = boardState.history.slice(-4);
|
|
||||||
const point = boardState.board[x][y];
|
const point = boardState.board[x][y];
|
||||||
if (!point) {
|
if (!point) return false;
|
||||||
return false;
|
|
||||||
}
|
point.color = player;
|
||||||
point.player = player;
|
|
||||||
boardState.previousPlayer = player;
|
boardState.previousPlayer = player;
|
||||||
boardState.passCount = 0;
|
boardState.passCount = 0;
|
||||||
|
|
||||||
return updateCaptures(boardState, player);
|
updateCaptures(boardState.board, player);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pass the current player's turn without making a move.
|
* Pass the current player's turn without making a move.
|
||||||
* Ends the game if this is the second pass in a row.
|
* 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) {
|
if (boardState.previousPlayer === null || boardState.previousPlayer === player) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boardState.previousPlayer =
|
boardState.previousPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
|
||||||
boardState.previousPlayer === playerColors.black ? playerColors.white : playerColors.black;
|
|
||||||
boardState.passCount++;
|
boardState.passCount++;
|
||||||
|
|
||||||
if (boardState.passCount >= 2 && allowEndGame) {
|
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.
|
* 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) {
|
export function applyHandicap(board: Board, handicap: number): void {
|
||||||
const availableMoves = getEmptySpaces(boardState);
|
const availableMoves = getEmptySpaces(board);
|
||||||
const handicapMoveOptions = getExpansionMoveArray(boardState, playerColors.black, availableMoves);
|
const handicapMoveOptions = getExpansionMoveArray(board, availableMoves);
|
||||||
const handicapMoves: Move[] = [];
|
const handicapMoves: Move[] = [];
|
||||||
|
|
||||||
// select random distinct moves from the move options list up to the specified handicap amount
|
// 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) => {
|
handicapMoves.forEach((move: Move) => {
|
||||||
const point = boardState.board[move.point.x][move.point.y];
|
const point = board[move.point.x][move.point.y];
|
||||||
return move.point && point && (point.player = playerColors.white);
|
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
|
* Finds all groups of connected stones on the board, and updates the points in them with their
|
||||||
* chain information and liberties.
|
* chain information and liberties.
|
||||||
|
* Updates a board in-place.
|
||||||
*/
|
*/
|
||||||
export function updateChains(boardState: BoardState, resetChains = true) {
|
export function updateChains(board: Board, resetChains = true): void {
|
||||||
resetChains && clearChains(boardState);
|
resetChains && clearChains(board);
|
||||||
|
|
||||||
for (let x = 0; x < boardState.board.length; x++) {
|
for (let x = 0; x < board.length; x++) {
|
||||||
for (let y = 0; y < boardState.board[x].length; y++) {
|
for (let y = 0; y < board[x].length; y++) {
|
||||||
const point = boardState.board[x][y];
|
const point = board[x][y];
|
||||||
// If the current point is already analyzed, skip it
|
// If the current point is already analyzed, skip it
|
||||||
if (!point || point.chain !== "") {
|
if (!point || point.chain !== "") continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chainMembers = findAdjacentPointsInChain(boardState, x, y);
|
const chainMembers = findAdjacentPointsInChain(board, x, y);
|
||||||
const libertiesForChain = findLibertiesForChain(boardState, chainMembers);
|
const libertiesForChain = findLibertiesForChain(board, chainMembers);
|
||||||
const id = `${point.x},${point.y}`;
|
const id = `${point.x},${point.y}`;
|
||||||
|
|
||||||
chainMembers.forEach((member) => {
|
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).
|
* adjacent to some point on the chain including the current point).
|
||||||
*
|
*
|
||||||
* Then, remove any chains with no liberties.
|
* Then, remove any chains with no liberties.
|
||||||
|
* Modifies the board in place.
|
||||||
*/
|
*/
|
||||||
export function updateCaptures(initialState: BoardState, playerWhoMoved: PlayerColor, resetChains = true): BoardState {
|
export function updateCaptures(board: Board, playerWhoMoved: GoColor, resetChains = true): void {
|
||||||
const boardState = updateChains(initialState, resetChains);
|
const boardState = updateChains(board, resetChains);
|
||||||
const chains = getAllChains(boardState);
|
const chains = getAllChains(board);
|
||||||
|
|
||||||
const chainsToCapture = findAllCapturedChains(chains, playerWhoMoved);
|
const chainsToCapture = findAllCapturedChains(chains, playerWhoMoved);
|
||||||
if (!chainsToCapture?.length) {
|
if (!chainsToCapture?.length) {
|
||||||
@ -185,7 +175,7 @@ export function updateCaptures(initialState: BoardState, playerWhoMoved: PlayerC
|
|||||||
}
|
}
|
||||||
|
|
||||||
chainsToCapture?.forEach((chain) => captureChain(chain));
|
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[]) {
|
function captureChain(chain: PointState[]) {
|
||||||
chain.forEach((point) => {
|
chain.forEach((point) => {
|
||||||
point.player = playerColors.empty;
|
point.color = GoColor.empty;
|
||||||
point.chain = "";
|
point.chain = "";
|
||||||
point.liberties = [];
|
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 {
|
function clearChains(board: Board): void {
|
||||||
for (const x in boardState.board) {
|
for (const column of board) {
|
||||||
for (const y in boardState.board[x]) {
|
for (const point of column) {
|
||||||
const point = boardState.board[x][y];
|
if (!point) continue;
|
||||||
if (point && point.chain && point.liberties) {
|
|
||||||
point.chain = "";
|
point.chain = "";
|
||||||
point.liberties = null;
|
point.liberties = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return boardState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds all the pieces in the current continuous group, or 'chain'
|
* 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,
|
* 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
|
* 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) {
|
export function findAdjacentPointsInChain(board: Board, x: number, y: number) {
|
||||||
const point = boardState.board[x][y];
|
const point = board[x][y];
|
||||||
if (!point) {
|
if (!point) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -237,13 +225,13 @@ export function findAdjacentPointsInChain(boardState: BoardState, x: number, y:
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkedPoints.push(currentPoint);
|
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]
|
[neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||||
.filter(isNotNull)
|
.filter(isNotNull)
|
||||||
.filter(isDefined)
|
.filter(isDefined)
|
||||||
.forEach((neighbor) => {
|
.forEach((neighbor) => {
|
||||||
if (neighbor && neighbor.player === currentPoint.player && !contains(checkedPoints, neighbor)) {
|
if (neighbor && neighbor.color === currentPoint.color && !contains(checkedPoints, neighbor)) {
|
||||||
adjacentPoints.push(neighbor);
|
adjacentPoints.push(neighbor);
|
||||||
pointsToCheckNeighbors.push(neighbor);
|
pointsToCheckNeighbors.push(neighbor);
|
||||||
}
|
}
|
||||||
@ -257,12 +245,12 @@ export function findAdjacentPointsInChain(boardState: BoardState, x: number, y:
|
|||||||
/**
|
/**
|
||||||
* Finds all empty spaces on the board.
|
* Finds all empty spaces on the board.
|
||||||
*/
|
*/
|
||||||
export function getEmptySpaces(boardState: BoardState): PointState[] {
|
export function getEmptySpaces(board: Board): PointState[] {
|
||||||
const emptySpaces: PointState[] = [];
|
const emptySpaces: PointState[] = [];
|
||||||
|
|
||||||
boardState.board.forEach((column) => {
|
board.forEach((column) => {
|
||||||
column.forEach((point) => {
|
column.forEach((point) => {
|
||||||
if (point && point.player === playerColors.empty) {
|
if (point && point.color === GoColor.empty) {
|
||||||
emptySpaces.push(point);
|
emptySpaces.push(point);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -277,7 +265,7 @@ export function getEmptySpaces(boardState: BoardState): PointState[] {
|
|||||||
export function getStateCopy(initialState: BoardState) {
|
export function getStateCopy(initialState: BoardState) {
|
||||||
const boardState = structuredClone(initialState);
|
const boardState = structuredClone(initialState);
|
||||||
|
|
||||||
boardState.history = [...initialState.history];
|
boardState.previousBoard = initialState.previousBoard ? [...initialState.previousBoard] : null;
|
||||||
boardState.previousPlayer = initialState.previousPlayer;
|
boardState.previousPlayer = initialState.previousPlayer;
|
||||||
boardState.ai = initialState.ai;
|
boardState.ai = initialState.ai;
|
||||||
boardState.passCount = initialState.passCount;
|
boardState.passCount = initialState.passCount;
|
||||||
@ -285,34 +273,16 @@ export function getStateCopy(initialState: BoardState) {
|
|||||||
return boardState;
|
return boardState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Make a deep copy of a board */
|
||||||
* Makes a deep copy of the given BoardState's board
|
export function getBoardCopy(board: Board): Board {
|
||||||
*/
|
return structuredClone(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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function contains(arr: PointState[], point: PointState) {
|
export function contains(arr: PointState[], point: PointState) {
|
||||||
return !!arr.find((p) => p && p.x === point.x && p.y === point.y);
|
return !!arr.find((p) => p && p.x === point.x && p.y === point.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findNeighbors(boardState: BoardState, x: number, y: number): Neighbor {
|
export function findNeighbors(board: Board, x: number, y: number): Neighbor {
|
||||||
const board = boardState.board;
|
|
||||||
return {
|
return {
|
||||||
north: board[x]?.[y + 1],
|
north: board[x]?.[y + 1],
|
||||||
east: board[x + 1]?.[y],
|
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 type { Board, BoardState, PointState } from "../Types";
|
||||||
import { WHRNG } from "../../Casino/RNG";
|
|
||||||
import { Player } from "@player";
|
import { Player } from "@player";
|
||||||
|
import { boardSizes } from "../Constants";
|
||||||
|
import { WHRNG } from "../../Casino/RNG";
|
||||||
import { floor } from "./boardState";
|
import { floor } from "./boardState";
|
||||||
|
|
||||||
type rand = (n1: number, n2: number) => number;
|
type rand = (n1: number, n2: number) => number;
|
||||||
@ -122,6 +124,6 @@ function rotateNTimes(board: Board, rotations: number) {
|
|||||||
return board;
|
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());
|
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 { 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 { defaultMultipliers, mergeMultipliers, Multipliers } from "../../PersonObjects/Multipliers";
|
||||||
import { PlayerObject } from "../../PersonObjects/Player/PlayerObject";
|
|
||||||
import { formatPercent } from "../../ui/formatNumber";
|
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
|
* 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)
|
* 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 power = getEffectPowerForFaction(faction);
|
||||||
const sourceFileBonus = Player.sourceFileLvl(14) ? 1.25 : 1;
|
const sourceFileBonus = Player.sourceFileLvl(14) ? 1.25 : 1;
|
||||||
return (
|
return (
|
||||||
@ -37,8 +40,8 @@ export function getMaxFavor() {
|
|||||||
/**
|
/**
|
||||||
* Gets a formatted description of the current bonus from this faction
|
* Gets a formatted description of the current bonus from this faction
|
||||||
*/
|
*/
|
||||||
export function getBonusText(opponent: opponents) {
|
export function getBonusText(opponent: GoOpponent) {
|
||||||
const nodePower = getPlayerStats(opponent).nodePower;
|
const nodePower = getOpponentStats(opponent).nodePower;
|
||||||
const effectPercent = formatPercent(CalculateEffect(nodePower, opponent) - 1);
|
const effectPercent = formatPercent(CalculateEffect(nodePower, opponent) - 1);
|
||||||
const effectDescription = getEffectTypeForFaction(opponent);
|
const effectDescription = getEffectTypeForFaction(opponent);
|
||||||
return `${effectPercent} ${effectDescription}`;
|
return `${effectPercent} ${effectDescription}`;
|
||||||
@ -58,35 +61,31 @@ export function updateGoMults(): void {
|
|||||||
*/
|
*/
|
||||||
function calculateMults(): Multipliers {
|
function calculateMults(): Multipliers {
|
||||||
const mults = defaultMultipliers();
|
const mults = defaultMultipliers();
|
||||||
[...opponentList, opponents.w0r1d_d43m0n].forEach((opponent) => {
|
getRecordEntries(Go.stats).forEach(([opponent, stats]) => {
|
||||||
if (!Player.go?.status?.[opponent]) {
|
const effect = CalculateEffect(stats.nodePower, opponent);
|
||||||
Player.go = getGoPlayerStartingState();
|
|
||||||
}
|
|
||||||
|
|
||||||
const effect = CalculateEffect(getPlayerStats(opponent).nodePower, opponent);
|
|
||||||
switch (opponent) {
|
switch (opponent) {
|
||||||
case opponents.Netburners:
|
case GoOpponent.Netburners:
|
||||||
mults.hacknet_node_money *= effect;
|
mults.hacknet_node_money *= effect;
|
||||||
break;
|
break;
|
||||||
case opponents.SlumSnakes:
|
case GoOpponent.SlumSnakes:
|
||||||
mults.crime_success *= effect;
|
mults.crime_success *= effect;
|
||||||
break;
|
break;
|
||||||
case opponents.TheBlackHand:
|
case GoOpponent.TheBlackHand:
|
||||||
mults.hacking_money *= effect;
|
mults.hacking_money *= effect;
|
||||||
break;
|
break;
|
||||||
case opponents.Tetrads:
|
case GoOpponent.Tetrads:
|
||||||
mults.strength *= effect;
|
mults.strength *= effect;
|
||||||
mults.dexterity *= effect;
|
mults.dexterity *= effect;
|
||||||
mults.agility *= effect;
|
mults.agility *= effect;
|
||||||
break;
|
break;
|
||||||
case opponents.Daedalus:
|
case GoOpponent.Daedalus:
|
||||||
mults.company_rep *= effect;
|
mults.company_rep *= effect;
|
||||||
mults.faction_rep *= effect;
|
mults.faction_rep *= effect;
|
||||||
break;
|
break;
|
||||||
case opponents.Illuminati:
|
case GoOpponent.Illuminati:
|
||||||
mults.hacking_speed *= effect;
|
mults.hacking_speed *= effect;
|
||||||
break;
|
break;
|
||||||
case opponents.w0r1d_d43m0n:
|
case GoOpponent.w0r1d_d43m0n:
|
||||||
mults.hacking *= effect;
|
mults.hacking *= effect;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -94,27 +93,19 @@ function calculateMults(): Multipliers {
|
|||||||
return mults;
|
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() {
|
export function playerHasDiscoveredGo() {
|
||||||
const playedGame = Player.go.boardState.history.length || Player.go.previousGameFinalBoardState?.history?.length;
|
const playedGame = Go.currentGame.previousBoard;
|
||||||
const hasRecords = opponentList.find((opponent) => getPlayerStats(opponent).wins + getPlayerStats(opponent).losses);
|
const hasRecords = getRecordValues(Go.stats).some((stats) => stats.wins + stats.losses);
|
||||||
const isInBn14 = Player.bitNodeN === 14;
|
const isInBn14 = Player.bitNodeN === 14;
|
||||||
|
|
||||||
return !!(playedGame || hasRecords || isInBn14);
|
return !!(playedGame || hasRecords || isInBn14);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectPowerForFaction(opponent: opponents) {
|
function getEffectPowerForFaction(opponent: GoOpponent) {
|
||||||
return opponentDetails[opponent].bonusPower;
|
return opponentDetails[opponent].bonusPower;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEffectTypeForFaction(opponent: opponents) {
|
export function getEffectTypeForFaction(opponent: GoOpponent) {
|
||||||
return opponentDetails[opponent].bonusDescription;
|
return opponentDetails[opponent].bonusDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,6 +123,6 @@ export function getWinstreakMultiplier(winStreak: number, previousWinStreak: num
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getDifficultyMultiplier(komi: number, boardSize: number) {
|
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;
|
return isTinyBoardVsIlluminati ? 8 : (komi + 0.5) * 0.25;
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import { BoardState, opponentList, Play, playerColors, playTypes, validityReason } from "../boardState/goConstants";
|
import type { BoardState, Play } from "../Types";
|
||||||
import { getMove, sleep } from "../boardAnalysis/goAI";
|
|
||||||
import { Player } from "@player";
|
import { Player } from "@player";
|
||||||
import {
|
import { GoColor, GoPlayType, GoValidity, GoOpponent } from "@enums";
|
||||||
getNewBoardState,
|
import { Go, GoEvents } from "../Go";
|
||||||
getStateCopy,
|
import { getMove, sleep } from "../boardAnalysis/goAI";
|
||||||
makeMove,
|
import { getNewBoardState, makeMove, passTurn, updateCaptures, updateChains } from "../boardState/boardState";
|
||||||
passTurn,
|
import { evaluateIfMoveIsValid, getControlledSpace, simpleBoardFromBoard } from "../boardAnalysis/boardAnalysis";
|
||||||
updateCaptures,
|
|
||||||
updateChains,
|
|
||||||
} from "../boardState/boardState";
|
|
||||||
import { evaluateIfMoveIsValid, getControlledSpace, getSimplifiedBoardState } from "../boardAnalysis/boardAnalysis";
|
|
||||||
import { getScore, resetWinstreak } from "../boardAnalysis/scoring";
|
import { getScore, resetWinstreak } from "../boardAnalysis/scoring";
|
||||||
import { WorkerScript } from "../../Netscript/WorkerScript";
|
import { WorkerScript } from "../../Netscript/WorkerScript";
|
||||||
import { WHRNG } from "../../Casino/RNG";
|
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)
|
* 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) {
|
export async function handlePassTurn(logger: (s: string) => void) {
|
||||||
passTurn(Player.go.boardState, playerColors.black);
|
passTurn(Go.currentGame, GoColor.black);
|
||||||
if (Player.go.boardState.previousPlayer === null) {
|
if (Go.currentGame.previousPlayer === null) {
|
||||||
logEndGame(logger);
|
logEndGame(logger);
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
type: playTypes.gameOver,
|
type: GoPlayType.gameOver,
|
||||||
x: -1,
|
x: -1,
|
||||||
y: -1,
|
y: -1,
|
||||||
success: true,
|
success: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return getAIMove(logger, Player.go.boardState);
|
return getAIMove(logger, Go.currentGame);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates and applies the player's router placement
|
* Validates and applies the player's router placement
|
||||||
*/
|
*/
|
||||||
export async function makePlayerMove(logger: (s: string) => void, x: number, y: number) {
|
export async function makePlayerMove(logger: (s: string) => void, x: number, y: number) {
|
||||||
const validity = evaluateIfMoveIsValid(Player.go.boardState, x, y, playerColors.black);
|
const boardState = Go.currentGame;
|
||||||
const result = makeMove(Player.go.boardState, x, y, playerColors.black);
|
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);
|
await sleep(500);
|
||||||
logger(`ERROR: Invalid move: ${validity}`);
|
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() ?");
|
logger("Do you have multiple scripts running, or did you forget to await makeMove() ?");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(invalidMoveResponse);
|
return Promise.resolve(invalidMoveResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GoEvents.emit();
|
||||||
logger(`Go move played: ${x}, ${y}`);
|
logger(`Go move played: ${x}, ${y}`);
|
||||||
|
const response = getAIMove(logger, boardState);
|
||||||
const playerUpdatedBoard = getStateCopy(result);
|
|
||||||
const response = getAIMove(logger, playerUpdatedBoard);
|
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -66,30 +62,27 @@ async function getAIMove(logger: (s: string) => void, boardState: BoardState, su
|
|||||||
resolve = res;
|
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 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 });
|
return resolve({ ...result, success: false });
|
||||||
}
|
}
|
||||||
if (result.type === "gameOver") {
|
if (result.type === "gameOver") {
|
||||||
logEndGame(logger);
|
logEndGame(logger);
|
||||||
}
|
}
|
||||||
if (result.type !== playTypes.move) {
|
if (result.type !== GoPlayType.move) {
|
||||||
Player.go.boardState = boardState;
|
|
||||||
return resolve({ ...result, success });
|
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) {
|
if (!aiUpdatedBoard) {
|
||||||
boardState.previousPlayer = playerColors.white;
|
boardState.previousPlayer = GoColor.white;
|
||||||
Player.go.boardState = boardState;
|
|
||||||
logger(`Invalid AI move attempted: ${result.x}, ${result.y}. This should not happen.`);
|
logger(`Invalid AI move attempted: ${result.x}, ${result.y}. This should not happen.`);
|
||||||
} else {
|
} else {
|
||||||
Player.go.boardState = aiUpdatedBoard;
|
|
||||||
logger(`Opponent played move: ${result.x}, ${result.y}`);
|
logger(`Opponent played move: ${result.x}, ${result.y}`);
|
||||||
}
|
}
|
||||||
|
GoEvents.emit();
|
||||||
await sleep(400);
|
|
||||||
resolve({ ...result, success });
|
resolve({ ...result, success });
|
||||||
});
|
});
|
||||||
return aiMoveResult;
|
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)
|
* Returns a grid of booleans indicating if the coordinates at that location are a valid move for the player (black pieces)
|
||||||
*/
|
*/
|
||||||
export function getValidMoves() {
|
export function getValidMoves() {
|
||||||
const boardState = Player.go.boardState;
|
const boardState = Go.currentGame;
|
||||||
// Map the board matrix into true/false values
|
// Map the board matrix into true/false values
|
||||||
return boardState.board.map((column, x) =>
|
return boardState.board.map((column, x) =>
|
||||||
column.reduce((validityArray: boolean[], point, y) => {
|
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);
|
validityArray.push(isValid);
|
||||||
return validityArray;
|
return validityArray;
|
||||||
}, []),
|
}, []),
|
||||||
@ -116,7 +109,7 @@ export function getValidMoves() {
|
|||||||
export function getChains() {
|
export function getChains() {
|
||||||
const chains: string[] = [];
|
const chains: string[] = [];
|
||||||
// Turn the internal chain IDs into nice consecutive numbers for display to the player
|
// 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) => {
|
column.reduce((chainIdArray: (number | null)[], point) => {
|
||||||
if (!point) {
|
if (!point) {
|
||||||
chainIdArray.push(null);
|
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.
|
* Returns a grid of numbers representing the number of open-node connections each player-owned chain has.
|
||||||
*/
|
*/
|
||||||
export function getLiberties() {
|
export function getLiberties() {
|
||||||
return Player.go.boardState.board.map((column) =>
|
return Go.currentGame.board.map((column) =>
|
||||||
column.reduce((libertyArray: number[], point) => {
|
column.reduce((libertyArray: number[], point) => {
|
||||||
libertyArray.push(point?.liberties?.length || -1);
|
libertyArray.push(point?.liberties?.length || -1);
|
||||||
return libertyArray;
|
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
|
* Returns a grid indicating which player, if any, controls the empty nodes by fully encircling it with their routers
|
||||||
*/
|
*/
|
||||||
export function getControlledEmptyNodes() {
|
export function getControlledEmptyNodes() {
|
||||||
const boardState = Player.go.boardState;
|
const board = Go.currentGame.board;
|
||||||
const controlled = getControlledSpace(boardState);
|
const controlled = getControlledSpace(board);
|
||||||
return controlled.map((column, x: number) =>
|
return controlled.map((column, x: number) =>
|
||||||
column.reduce((ownedPoints: string, owner: playerColors, y: number) => {
|
column.reduce((ownedPoints: string, owner: GoColor, y: number) => {
|
||||||
if (owner === playerColors.white) {
|
if (owner === GoColor.white) {
|
||||||
return ownedPoints + "O";
|
return ownedPoints + "O";
|
||||||
}
|
}
|
||||||
if (owner === playerColors.black) {
|
if (owner === GoColor.black) {
|
||||||
return ownedPoints + "X";
|
return ownedPoints + "X";
|
||||||
}
|
}
|
||||||
if (!boardState.board[x][y]) {
|
if (!board[x][y]) {
|
||||||
return ownedPoints + "#";
|
return ownedPoints + "#";
|
||||||
}
|
}
|
||||||
if (boardState.board[x][y]?.player === playerColors.empty) {
|
if (board[x][y]?.color === GoColor.empty) {
|
||||||
return ownedPoints + "?";
|
return ownedPoints + "?";
|
||||||
}
|
}
|
||||||
return ownedPoints + ".";
|
return ownedPoints + ".";
|
||||||
@ -172,37 +165,30 @@ export function getControlledEmptyNodes() {
|
|||||||
* Handle post-game logging
|
* Handle post-game logging
|
||||||
*/
|
*/
|
||||||
function logEndGame(logger: (s: string) => void) {
|
function logEndGame(logger: (s: string) => void) {
|
||||||
const boardState = Player.go.boardState;
|
const boardState = Go.currentGame;
|
||||||
const score = getScore(boardState);
|
const score = getScore(boardState);
|
||||||
logger(
|
logger(
|
||||||
`Subnet complete! Final score: ${boardState.ai}: ${score[playerColors.white].sum}, Player: ${
|
`Subnet complete! Final score: ${boardState.ai}: ${score[GoColor.white].sum}, Player: ${score[GoColor.black].sum}`,
|
||||||
score[playerColors.black].sum
|
|
||||||
}`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the board, resets winstreak if applicable
|
* Clears the board, resets winstreak if applicable
|
||||||
*/
|
*/
|
||||||
export function resetBoardState(error: (s: string) => void, opponentString: string, boardSize: number) {
|
export function resetBoardState(error: (s: string) => void, opponent: GoOpponent, boardSize: number) {
|
||||||
const opponent = opponentList.find((faction) => faction === opponentString);
|
|
||||||
|
|
||||||
if (![5, 7, 9, 13].includes(boardSize)) {
|
if (![5, 7, 9, 13].includes(boardSize)) {
|
||||||
error(`Invalid subnet size requested (${boardSize}, size must be 5, 7, 9, or 13`);
|
error(`Invalid subnet size requested (${boardSize}, size must be 5, 7, 9, or 13`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!opponent) {
|
|
||||||
error(`Invalid opponent requested (${opponentString}), valid options are ${opponentList.join(", ")}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldBoardState = Player.go.boardState;
|
const oldBoardState = Go.currentGame;
|
||||||
if (oldBoardState.previousPlayer !== null && oldBoardState.history.length) {
|
if (oldBoardState.previousPlayer !== null && oldBoardState.previousBoard) {
|
||||||
resetWinstreak(oldBoardState.ai, false);
|
resetWinstreak(oldBoardState.ai, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Player.go.boardState = getNewBoardState(boardSize, opponent, true);
|
Go.currentGame = getNewBoardState(boardSize, opponent, true);
|
||||||
return getSimplifiedBoardState(Player.go.boardState.board);
|
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. */
|
/** 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 = {
|
export const invalidMoveResponse: Play = {
|
||||||
success: false,
|
success: false,
|
||||||
type: playTypes.invalid,
|
type: GoPlayType.invalid,
|
||||||
x: -1,
|
x: -1,
|
||||||
y: -1,
|
y: -1,
|
||||||
};
|
};
|
||||||
@ -235,12 +221,13 @@ export async function determineCheatSuccess(
|
|||||||
successRngOverride?: number,
|
successRngOverride?: number,
|
||||||
ejectRngOverride?: number,
|
ejectRngOverride?: number,
|
||||||
): Promise<Play> {
|
): Promise<Play> {
|
||||||
const state = Player.go.boardState;
|
const state = Go.currentGame;
|
||||||
const rng = new WHRNG(Player.totalPlaytime);
|
const rng = new WHRNG(Player.totalPlaytime);
|
||||||
// If cheat is successful, run callback
|
// If cheat is successful, run callback
|
||||||
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount)) {
|
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount)) {
|
||||||
callback();
|
callback();
|
||||||
state.cheatCount++;
|
state.cheatCount++;
|
||||||
|
GoEvents.emit();
|
||||||
return getAIMove(logger, state, true);
|
return getAIMove(logger, state, true);
|
||||||
}
|
}
|
||||||
// If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly losing
|
// 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.`);
|
logger(`Cheat failed! You have been ejected from the subnet.`);
|
||||||
resetBoardState(logger, state.ai, state.board[0].length);
|
resetBoardState(logger, state.ai, state.board[0].length);
|
||||||
return {
|
return {
|
||||||
type: playTypes.gameOver,
|
type: GoPlayType.gameOver,
|
||||||
x: -1,
|
x: -1,
|
||||||
y: -1,
|
y: -1,
|
||||||
success: false,
|
success: false,
|
||||||
@ -257,7 +244,7 @@ export async function determineCheatSuccess(
|
|||||||
// If the cheat fails, your turn is skipped
|
// If the cheat fails, your turn is skipped
|
||||||
else {
|
else {
|
||||||
logger(`Cheat failed. Your turn has been skipped.`);
|
logger(`Cheat failed. Your turn has been skipped.`);
|
||||||
passTurn(state, playerColors.black, false);
|
passTurn(state, GoColor.black, false);
|
||||||
state.cheatCount++;
|
state.cheatCount++;
|
||||||
return getAIMove(logger, state, false);
|
return getAIMove(logger, state, false);
|
||||||
}
|
}
|
||||||
@ -287,21 +274,21 @@ export function cheatRemoveRouter(
|
|||||||
successRngOverride?: number,
|
successRngOverride?: number,
|
||||||
ejectRngOverride?: number,
|
ejectRngOverride?: number,
|
||||||
) {
|
) {
|
||||||
const point = Player.go.boardState.board[x][y];
|
const point = Go.currentGame.board[x][y];
|
||||||
if (!point) {
|
if (!point) {
|
||||||
logger(`The node ${x},${y} is offline, so you cannot clear this point with removeRouter().`);
|
logger(`The node ${x},${y} is offline, so you cannot clear this point with removeRouter().`);
|
||||||
return invalidMoveResponse;
|
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().`);
|
logger(`The point ${x},${y} does not have a router on it, so you cannot clear this point with removeRouter().`);
|
||||||
return invalidMoveResponse;
|
return invalidMoveResponse;
|
||||||
}
|
}
|
||||||
return determineCheatSuccess(
|
return determineCheatSuccess(
|
||||||
logger,
|
logger,
|
||||||
() => {
|
() => {
|
||||||
point.player = playerColors.empty;
|
point.color = GoColor.empty;
|
||||||
Player.go.boardState = updateChains(Player.go.boardState);
|
updateChains(Go.currentGame.board);
|
||||||
Player.go.boardState.previousPlayer = playerColors.black;
|
Go.currentGame.previousPlayer = GoColor.black;
|
||||||
logger(`Cheat successful. The point ${x},${y} was cleared.`);
|
logger(`Cheat successful. The point ${x},${y} was cleared.`);
|
||||||
},
|
},
|
||||||
successRngOverride,
|
successRngOverride,
|
||||||
@ -321,21 +308,21 @@ export function cheatPlayTwoMoves(
|
|||||||
successRngOverride?: number,
|
successRngOverride?: number,
|
||||||
ejectRngOverride?: number,
|
ejectRngOverride?: number,
|
||||||
) {
|
) {
|
||||||
const point1 = Player.go.boardState.board[x1][y1];
|
const point1 = Go.currentGame.board[x1][y1];
|
||||||
if (!point1) {
|
if (!point1) {
|
||||||
logger(`The node ${x1},${y1} is offline, so you cannot place a router there.`);
|
logger(`The node ${x1},${y1} is offline, so you cannot place a router there.`);
|
||||||
return invalidMoveResponse;
|
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.`);
|
logger(`The point ${x1},${y1} is not empty, so you cannot place a router there.`);
|
||||||
return invalidMoveResponse;
|
return invalidMoveResponse;
|
||||||
}
|
}
|
||||||
const point2 = Player.go.boardState.board[x2][y2];
|
const point2 = Go.currentGame.board[x2][y2];
|
||||||
if (!point2) {
|
if (!point2) {
|
||||||
logger(`The node ${x2},${y2} is offline, so you cannot place a router there.`);
|
logger(`The node ${x2},${y2} is offline, so you cannot place a router there.`);
|
||||||
return invalidMoveResponse;
|
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.`);
|
logger(`The point ${x2},${y2} is not empty, so you cannot place a router there.`);
|
||||||
return invalidMoveResponse;
|
return invalidMoveResponse;
|
||||||
}
|
}
|
||||||
@ -343,10 +330,10 @@ export function cheatPlayTwoMoves(
|
|||||||
return determineCheatSuccess(
|
return determineCheatSuccess(
|
||||||
logger,
|
logger,
|
||||||
() => {
|
() => {
|
||||||
point1.player = playerColors.black;
|
point1.color = GoColor.black;
|
||||||
point2.player = playerColors.black;
|
point2.color = GoColor.black;
|
||||||
Player.go.boardState = updateCaptures(Player.go.boardState, playerColors.black);
|
updateCaptures(Go.currentGame.board, GoColor.black);
|
||||||
Player.go.boardState.previousPlayer = playerColors.black;
|
Go.currentGame.previousPlayer = GoColor.black;
|
||||||
|
|
||||||
logger(`Cheat successful. Two go moves played: ${x1},${y1} and ${x2},${y2}`);
|
logger(`Cheat successful. Two go moves played: ${x1},${y1} and ${x2},${y2}`);
|
||||||
},
|
},
|
||||||
@ -362,7 +349,7 @@ export function cheatRepairOfflineNode(
|
|||||||
successRngOverride?: number,
|
successRngOverride?: number,
|
||||||
ejectRngOverride?: number,
|
ejectRngOverride?: number,
|
||||||
) {
|
) {
|
||||||
const point = Player.go.boardState.board[x][y];
|
const point = Go.currentGame.board[x][y];
|
||||||
if (point) {
|
if (point) {
|
||||||
logger(`The node ${x},${y} is not offline, so you cannot repair the node.`);
|
logger(`The node ${x},${y} is not offline, so you cannot repair the node.`);
|
||||||
return invalidMoveResponse;
|
return invalidMoveResponse;
|
||||||
@ -371,15 +358,15 @@ export function cheatRepairOfflineNode(
|
|||||||
return determineCheatSuccess(
|
return determineCheatSuccess(
|
||||||
logger,
|
logger,
|
||||||
() => {
|
() => {
|
||||||
Player.go.boardState.board[x][y] = {
|
Go.currentGame.board[x][y] = {
|
||||||
chain: "",
|
chain: "",
|
||||||
liberties: null,
|
liberties: null,
|
||||||
y,
|
y,
|
||||||
player: playerColors.empty,
|
color: GoColor.empty,
|
||||||
x,
|
x,
|
||||||
};
|
};
|
||||||
Player.go.boardState = updateChains(Player.go.boardState);
|
updateChains(Go.currentGame.board);
|
||||||
Player.go.boardState.previousPlayer = playerColors.black;
|
Go.currentGame.previousPlayer = GoColor.black;
|
||||||
logger(`Cheat successful. The point ${x},${y} was repaired.`);
|
logger(`Cheat successful. The point ${x},${y} was repaired.`);
|
||||||
},
|
},
|
||||||
successRngOverride,
|
successRngOverride,
|
||||||
@ -394,12 +381,12 @@ export function cheatDestroyNode(
|
|||||||
successRngOverride?: number,
|
successRngOverride?: number,
|
||||||
ejectRngOverride?: number,
|
ejectRngOverride?: number,
|
||||||
) {
|
) {
|
||||||
const point = Player.go.boardState.board[x][y];
|
const point = Go.currentGame.board[x][y];
|
||||||
if (!point) {
|
if (!point) {
|
||||||
logger(`The node ${x},${y} is already offline, so you cannot destroy the node.`);
|
logger(`The node ${x},${y} is already offline, so you cannot destroy the node.`);
|
||||||
return invalidMoveResponse;
|
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.`);
|
logger(`The point ${x},${y} is not empty, so you cannot destroy this node.`);
|
||||||
return invalidMoveResponse;
|
return invalidMoveResponse;
|
||||||
}
|
}
|
||||||
@ -407,10 +394,10 @@ export function cheatDestroyNode(
|
|||||||
return determineCheatSuccess(
|
return determineCheatSuccess(
|
||||||
logger,
|
logger,
|
||||||
() => {
|
() => {
|
||||||
Player.go.boardState.board[x][y] = null;
|
Go.currentGame.board[x][y] = null;
|
||||||
Player.go.boardState = updateChains(Player.go.boardState);
|
updateChains(Go.currentGame.board);
|
||||||
Player.go.boardState.previousPlayer = playerColors.black;
|
Go.currentGame.previousPlayer = GoColor.black;
|
||||||
logger(`Cheat successful. The point ${x},${y} was repaired.`);
|
logger(`Cheat successful. The point ${x},${y} was destroyed.`);
|
||||||
},
|
},
|
||||||
successRngOverride,
|
successRngOverride,
|
||||||
ejectRngOverride,
|
ejectRngOverride,
|
||||||
|
@ -1,33 +1,26 @@
|
|||||||
import React, { useMemo } from "react";
|
import type { BoardState } from "../Types";
|
||||||
import Grid from "@mui/material/Grid";
|
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Grid } from "@mui/material";
|
||||||
|
|
||||||
|
import { GoOpponent, GoColor } from "@enums";
|
||||||
import { getSizeClass, GoPoint } from "./GoPoint";
|
import { getSizeClass, GoPoint } from "./GoPoint";
|
||||||
import { useRerender } from "../../ui/React/hooks";
|
|
||||||
import { boardStyles } from "../boardState/goStyles";
|
import { boardStyles } from "../boardState/goStyles";
|
||||||
import { getAllValidMoves, getControlledSpace } from "../boardAnalysis/boardAnalysis";
|
import { getAllValidMoves, getControlledSpace } from "../boardAnalysis/boardAnalysis";
|
||||||
import { BoardState, opponents, playerColors } from "../boardState/goConstants";
|
|
||||||
|
|
||||||
interface IProps {
|
interface GoGameboardProps {
|
||||||
boardState: BoardState;
|
boardState: BoardState;
|
||||||
traditional: boolean;
|
traditional: boolean;
|
||||||
clickHandler: (x: number, y: number) => any;
|
clickHandler: (x: number, y: number) => any;
|
||||||
hover: boolean;
|
hover: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GoGameboard({ boardState, traditional, clickHandler, hover }: IProps): React.ReactElement {
|
export function GoGameboard({ boardState, traditional, clickHandler, hover }: GoGameboardProps): React.ReactElement {
|
||||||
useRerender(400);
|
|
||||||
|
|
||||||
const currentPlayer =
|
const currentPlayer =
|
||||||
boardState.ai !== opponents.none || boardState.previousPlayer === playerColors.white
|
boardState.ai !== GoOpponent.none || boardState.previousPlayer === GoColor.white ? GoColor.black : GoColor.white;
|
||||||
? playerColors.black
|
|
||||||
: playerColors.white;
|
|
||||||
|
|
||||||
const availablePoints = useMemo(
|
const availablePoints = hover ? getAllValidMoves(boardState, currentPlayer) : [];
|
||||||
() => (hover ? getAllValidMoves(boardState, currentPlayer) : []),
|
const ownedEmptyNodes = getControlledSpace(boardState.board);
|
||||||
[boardState, hover, currentPlayer],
|
|
||||||
);
|
|
||||||
|
|
||||||
const ownedEmptyNodes = useMemo(() => getControlledSpace(boardState), [boardState]);
|
|
||||||
|
|
||||||
function pointIsValid(x: number, y: number) {
|
function pointIsValid(x: number, y: number) {
|
||||||
return !!availablePoints.find((point) => point.x === x && point.y === y);
|
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();
|
const classes = boardStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Grid container id="goGameboard" className={`${classes.board} ${traditional ? classes.traditional : ""}`}>
|
<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;
|
const yIndex = boardState.board[0].length - y - 1;
|
||||||
return (
|
return (
|
||||||
<Grid container key={`column_${yIndex}`} item className={getSizeClass(boardSize, classes)}>
|
<Grid container key={`column_${yIndex}`} item className={getSizeClass(boardSize, classes)}>
|
||||||
{row.map((point, x: number) => {
|
{column.map((point, x: number) => {
|
||||||
const xIndex = x;
|
const xIndex = x;
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
@ -68,6 +60,5 @@ export function GoGameboard({ boardState, traditional, clickHandler, hover }: IP
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import type { BoardState } from "../Types";
|
||||||
import { SnackbarEvents } from "../../ui/React/Snackbar";
|
|
||||||
import { ToastVariant } from "@enums";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Box, Button, Typography } from "@mui/material";
|
import { Box, Button, Typography } from "@mui/material";
|
||||||
|
|
||||||
import { BoardState, opponents, playerColors, playTypes, validityReason } from "../boardState/goConstants";
|
import { GoOpponent, GoColor, GoPlayType, GoValidity, ToastVariant } from "@enums";
|
||||||
import { getNewBoardState, getStateCopy, makeMove, passTurn } from "../boardState/boardState";
|
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 { getMove } from "../boardAnalysis/goAI";
|
||||||
import { bitverseArt, weiArt } from "../boardState/asciiArt";
|
import { bitverseArt, weiArt } from "../boardState/asciiArt";
|
||||||
import { getScore, resetWinstreak } from "../boardAnalysis/scoring";
|
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 { useRerender } from "../../ui/React/hooks";
|
||||||
import { OptionSwitch } from "../../ui/React/OptionSwitch";
|
import { OptionSwitch } from "../../ui/React/OptionSwitch";
|
||||||
import { boardStyles } from "../boardState/goStyles";
|
import { boardStyles } from "../boardState/goStyles";
|
||||||
import { Player } from "@player";
|
|
||||||
import { Settings } from "../../Settings/Settings";
|
import { Settings } from "../../Settings/Settings";
|
||||||
import { GoScoreModal } from "./GoScoreModal";
|
import { GoScoreModal } from "./GoScoreModal";
|
||||||
import { GoGameboard } from "./GoGameboard";
|
import { GoGameboard } from "./GoGameboard";
|
||||||
import { GoSubnetSearch } from "./GoSubnetSearch";
|
import { GoSubnetSearch } from "./GoSubnetSearch";
|
||||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||||
|
|
||||||
interface IProps {
|
interface GoGameboardWrapperProps {
|
||||||
showInstructions: () => void;
|
showInstructions: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,30 +33,37 @@ interface IProps {
|
|||||||
* play two moves that don't capture
|
* play two moves that don't capture
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactElement {
|
export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps): React.ReactElement {
|
||||||
const rerender = useRerender(400);
|
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 traditional = Settings.GoTraditionalStyle;
|
||||||
const [showPriorMove, setShowPriorMove] = useState(false);
|
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 [scoreOpen, setScoreOpen] = useState(false);
|
||||||
const [searchOpen, setSearchOpen] = useState(false);
|
const [searchOpen, setSearchOpen] = useState(false);
|
||||||
const [waitingOnAI, setWaitingOnAI] = useState(false);
|
const [waitingOnAI, setWaitingOnAI] = useState(false);
|
||||||
|
|
||||||
const classes = boardStyles();
|
const classes = boardStyles();
|
||||||
const boardSize = boardState.board[0].length;
|
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);
|
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
|
// 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(() => {
|
useEffect(() => {
|
||||||
if (boardState.previousPlayer === playerColors.black && !waitingOnAI) {
|
if (boardState.previousPlayer === GoColor.black && !waitingOnAI) {
|
||||||
takeAiTurn(Player.go.boardState);
|
takeAiTurn(Go.currentGame);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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) {
|
async function clickHandler(x: number, y: number) {
|
||||||
if (showPriorMove) {
|
if (showPriorMove) {
|
||||||
SnackbarEvents.emit(
|
SnackbarEvents.emit(
|
||||||
@ -68,7 +76,7 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
|||||||
|
|
||||||
// Lock the board when it isn't the player's turn
|
// Lock the board when it isn't the player's turn
|
||||||
const gameOver = boardState.previousPlayer === null;
|
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) {
|
if (notYourTurn) {
|
||||||
SnackbarEvents.emit(`It is not your turn to play.`, ToastVariant.WARNING, 2000);
|
SnackbarEvents.emit(`It is not your turn to play.`, ToastVariant.WARNING, 2000);
|
||||||
return;
|
return;
|
||||||
@ -79,65 +87,54 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
|||||||
}
|
}
|
||||||
|
|
||||||
const validity = evaluateIfMoveIsValid(boardState, x, y, currentPlayer);
|
const validity = evaluateIfMoveIsValid(boardState, x, y, currentPlayer);
|
||||||
if (validity != validityReason.valid) {
|
if (validity != GoValidity.valid) {
|
||||||
SnackbarEvents.emit(`Invalid move: ${validity}`, ToastVariant.ERROR, 2000);
|
SnackbarEvents.emit(`Invalid move: ${validity}`, ToastVariant.ERROR, 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedBoard = makeMove(boardState, x, y, currentPlayer);
|
const didUpdateBoard = makeMove(boardState, x, y, currentPlayer);
|
||||||
if (updatedBoard) {
|
if (didUpdateBoard) {
|
||||||
updateBoard(updatedBoard);
|
rerender();
|
||||||
opponent !== opponents.none && takeAiTurn(updatedBoard);
|
opponent !== GoOpponent.none && takeAiTurn(boardState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function passPlayerTurn() {
|
function passPlayerTurn() {
|
||||||
if (boardState.previousPlayer === playerColors.white) {
|
if (boardState.previousPlayer === GoColor.white) {
|
||||||
passTurn(boardState, playerColors.black);
|
passTurn(boardState, GoColor.black);
|
||||||
updateBoard(boardState);
|
rerender();
|
||||||
}
|
}
|
||||||
if (boardState.previousPlayer === null) {
|
if (boardState.previousPlayer === null) {
|
||||||
endGame();
|
setScoreOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
opponent !== opponents.none && takeAiTurn(boardState);
|
opponent !== GoOpponent.none && takeAiTurn(boardState);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function takeAiTurn(board: BoardState) {
|
async function takeAiTurn(boardState: BoardState) {
|
||||||
if (board.previousPlayer === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setWaitingOnAI(true);
|
setWaitingOnAI(true);
|
||||||
const initialState = getStateCopy(board);
|
const move = await getMove(boardState, GoColor.white, opponent);
|
||||||
const move = await getMove(initialState, playerColors.white, opponent);
|
|
||||||
|
|
||||||
// If a new game has started while this async code ran, just drop it
|
// If a new game has started while this async code ran, just drop it
|
||||||
if (boardState.history.length > Player.go.boardState.history.length) {
|
if (boardState !== Go.currentGame) return;
|
||||||
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);
|
SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000);
|
||||||
updateBoard(initialState);
|
rerender();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (move.type === playTypes.gameOver || move.x === null || move.y === null) {
|
if (move.type === GoPlayType.gameOver || move.x === null || move.y === null) {
|
||||||
endGame(initialState);
|
setScoreOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedBoard = await makeMove(initialState, move.x, move.y, playerColors.white);
|
const didUpdateBoard = makeMove(boardState, move.x, move.y, GoColor.white);
|
||||||
|
|
||||||
if (updatedBoard) {
|
if (didUpdateBoard) setWaitingOnAI(false);
|
||||||
setTimeout(() => {
|
|
||||||
updateBoard(updatedBoard);
|
|
||||||
setWaitingOnAI(false);
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function newSubnet() {
|
function newSubnet() {
|
||||||
@ -149,39 +146,25 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
|||||||
setScoreOpen(false);
|
setScoreOpen(false);
|
||||||
setSearchOpen(false);
|
setSearchOpen(false);
|
||||||
setOpponent(newOpponent);
|
setOpponent(newOpponent);
|
||||||
if (boardState.previousPlayer !== null && boardState.history.length) {
|
if (boardState.previousPlayer !== null && boardState.previousBoard) {
|
||||||
resetWinstreak(boardState.ai, false);
|
resetWinstreak(boardState.ai, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBoardState = getNewBoardState(newBoardSize, newOpponent, false);
|
Go.currentGame = getNewBoardState(newBoardSize, newOpponent, false);
|
||||||
updateBoard(newBoardState);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateBoard(initialBoardState: BoardState) {
|
|
||||||
Player.go.boardState = getStateCopy(initialBoardState);
|
|
||||||
rerender();
|
rerender();
|
||||||
}
|
}
|
||||||
|
|
||||||
function endGame(state = boardState) {
|
|
||||||
setScoreOpen(true);
|
|
||||||
updateBoard(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPriorMove() {
|
function getPriorMove() {
|
||||||
if (!boardState.history.length) {
|
if (!boardState.previousBoard) return boardState;
|
||||||
return boardState;
|
const priorState = getStateCopy(boardState);
|
||||||
}
|
priorState.previousPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
|
||||||
const priorBoard = boardState.history.slice(-1)[0];
|
priorState.board = boardFromSimpleBoard(boardState.previousBoard);
|
||||||
const updatedState = getStateCopy(boardState);
|
updateCaptures(priorState.board, priorState.previousPlayer);
|
||||||
updatedState.board = priorBoard;
|
return priorState;
|
||||||
updatedState.previousPlayer =
|
|
||||||
boardState.previousPlayer === playerColors.black ? playerColors.white : playerColors.black;
|
|
||||||
|
|
||||||
return updatedState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPreviousMove(newValue: boolean) {
|
function showPreviousMove(newValue: boolean) {
|
||||||
if (boardState.history.length) {
|
if (boardState.previousBoard) {
|
||||||
setShowPriorMove(newValue);
|
setShowPriorMove(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,16 +173,13 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
|||||||
Settings.GoTraditionalStyle = newValue;
|
Settings.GoTraditionalStyle = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const endGameAvailable = boardState.previousPlayer === playerColors.white && boardState.passCount;
|
const endGameAvailable = boardState.previousPlayer === GoColor.white && boardState.passCount;
|
||||||
const noLegalMoves = useMemo(
|
const noLegalMoves =
|
||||||
() => boardState.previousPlayer === playerColors.white && !getAllValidMoves(boardState, playerColors.black).length,
|
boardState.previousPlayer === GoColor.white && !getAllValidMoves(boardState, GoColor.black).length;
|
||||||
[boardState],
|
const disablePassButton = opponent !== GoOpponent.none && boardState.previousPlayer === GoColor.black && waitingOnAI;
|
||||||
);
|
|
||||||
const disablePassButton =
|
|
||||||
opponent !== opponents.none && boardState.previousPlayer === playerColors.black && waitingOnAI;
|
|
||||||
|
|
||||||
const scoreBoxText = boardState.history.length
|
const scoreBoxText = boardState.previousBoard
|
||||||
? `Score: Black: ${score[playerColors.black].sum} White: ${score[playerColors.white].sum}`
|
? `Score: Black: ${score[GoColor.black].sum} White: ${score[GoColor.white].sum}`
|
||||||
: "Place a router to begin!";
|
: "Place a router to begin!";
|
||||||
|
|
||||||
const getPassButtonLabel = () => {
|
const getPassButtonLabel = () => {
|
||||||
@ -209,11 +189,11 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
|||||||
if (boardState.previousPlayer === null) {
|
if (boardState.previousPlayer === null) {
|
||||||
return "View Final Score";
|
return "View Final Score";
|
||||||
}
|
}
|
||||||
if (boardState.previousPlayer === playerColors.black && waitingOnAI) {
|
if (boardState.previousPlayer === GoColor.black && waitingOnAI) {
|
||||||
return "Waiting for opponent";
|
return "Waiting for opponent";
|
||||||
}
|
}
|
||||||
const currentPlayer = boardState.previousPlayer === playerColors.black ? playerColors.white : playerColors.black;
|
const currentPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
|
||||||
return `Pass Turn${boardState.ai === opponents.none ? ` (${currentPlayer})` : ""}`;
|
return `Pass Turn${boardState.ai === GoOpponent.none ? ` (${currentPlayer})` : ""}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -242,8 +222,8 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
|||||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||||
<br />
|
<br />
|
||||||
<Typography variant={"h6"} className={classes.opponentLabel}>
|
<Typography variant={"h6"} className={classes.opponentLabel}>
|
||||||
{opponent !== opponents.none ? "Subnet owner: " : ""}{" "}
|
{opponent !== GoOpponent.none ? "Subnet owner: " : ""}{" "}
|
||||||
{opponent === opponents.w0r1d_d43m0n ? <CorruptableText content={opponent} spoiler={false} /> : opponent}
|
{opponent === GoOpponent.w0r1d_d43m0n ? <CorruptableText content={opponent} spoiler={false} /> : opponent}
|
||||||
</Typography>
|
</Typography>
|
||||||
<br />
|
<br />
|
||||||
</Box>
|
</Box>
|
||||||
@ -279,7 +259,7 @@ export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactEle
|
|||||||
/>
|
/>
|
||||||
<OptionSwitch
|
<OptionSwitch
|
||||||
checked={showPriorMove}
|
checked={showPriorMove}
|
||||||
disabled={!boardState.history.length}
|
disabled={!boardState.previousBoard}
|
||||||
onChange={(newValue) => showPreviousMove(newValue)}
|
onChange={(newValue) => showPreviousMove(newValue)}
|
||||||
text="Show previous move"
|
text="Show previous move"
|
||||||
tooltip={<>Show the board as it was before the last move</>}
|
tooltip={<>Show the board as it was before the last move</>}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Typography from "@mui/material/Typography";
|
import { Grid, Table, TableBody, TableCell, TableRow, Tooltip, Typography } from "@mui/material";
|
||||||
import { Grid, Table, TableBody, TableCell, TableRow, Tooltip } from "@mui/material";
|
|
||||||
|
|
||||||
import { opponentList, opponents } from "../boardState/goConstants";
|
import { GoOpponent } from "@enums";
|
||||||
import { getPlayerStats, getScore } from "../boardAnalysis/scoring";
|
import { Go } from "../Go";
|
||||||
import { Player } from "@player";
|
import { getOpponentStats, getScore } from "../boardAnalysis/scoring";
|
||||||
import { GoGameboard } from "./GoGameboard";
|
import { GoGameboard } from "./GoGameboard";
|
||||||
import { boardStyles } from "../boardState/goStyles";
|
import { boardStyles } from "../boardState/goStyles";
|
||||||
import { useRerender } from "../../ui/React/hooks";
|
import { useRerender } from "../../ui/React/hooks";
|
||||||
@ -13,15 +12,15 @@ import { formatNumber } from "../../ui/formatNumber";
|
|||||||
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
|
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
|
||||||
import { getNewBoardState } from "../boardState/boardState";
|
import { getNewBoardState } from "../boardState/boardState";
|
||||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||||
import { showWorldDemon } from "../boardAnalysis/goAI";
|
import { getRecordKeys } from "../../Types/Record";
|
||||||
|
|
||||||
export const GoHistoryPage = (): React.ReactElement => {
|
export const GoHistoryPage = (): React.ReactElement => {
|
||||||
useRerender(400);
|
useRerender(400);
|
||||||
const classes = boardStyles();
|
const classes = boardStyles();
|
||||||
const priorBoard = Player.go.previousGameFinalBoardState ?? getNewBoardState(7);
|
const priorBoard = Go.previousGame ?? getNewBoardState(7);
|
||||||
const score = getScore(priorBoard);
|
const score = getScore(priorBoard);
|
||||||
const opponent = priorBoard.ai;
|
const opponent = priorBoard.ai;
|
||||||
const opponentsToShow = showWorldDemon() ? [...opponentList, opponents.w0r1d_d43m0n] : opponentList;
|
const opponentsToShow = getRecordKeys(Go.stats);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -48,13 +47,13 @@ export const GoHistoryPage = (): React.ReactElement => {
|
|||||||
<Typography variant="h5">Faction Stats:</Typography>
|
<Typography variant="h5">Faction Stats:</Typography>
|
||||||
<Grid container style={{ maxWidth: "1020px" }}>
|
<Grid container style={{ maxWidth: "1020px" }}>
|
||||||
{opponentsToShow.map((faction, index) => {
|
{opponentsToShow.map((faction, index) => {
|
||||||
const data = getPlayerStats(faction);
|
const data = getOpponentStats(faction);
|
||||||
return (
|
return (
|
||||||
<Grid item key={opponentsToShow[index]} className={classes.factionStatus}>
|
<Grid item key={opponentsToShow[index]} className={classes.factionStatus}>
|
||||||
<Typography>
|
<Typography>
|
||||||
{" "}
|
{" "}
|
||||||
<strong className={classes.keyText}>
|
<strong className={classes.keyText}>
|
||||||
{faction === opponents.w0r1d_d43m0n ? (
|
{faction === GoOpponent.w0r1d_d43m0n ? (
|
||||||
<CorruptableText content="????????????" spoiler={false} />
|
<CorruptableText content="????????????" spoiler={false} />
|
||||||
) : (
|
) : (
|
||||||
faction
|
faction
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { boardStyles } from "../boardState/goStyles";
|
|
||||||
import { Grid, Link, Typography } from "@mui/material";
|
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 { GoTutorialChallenge } from "./GoTutorialChallenge";
|
||||||
import { Router } from "../../ui/GameRoot";
|
import { Router } from "../../ui/GameRoot";
|
||||||
import { Page } from "../../ui/Router";
|
import { Page } from "../../ui/Router";
|
||||||
@ -10,11 +11,7 @@ import { getMaxFavor } from "../effects/effect";
|
|||||||
|
|
||||||
const captureChallenge = (
|
const captureChallenge = (
|
||||||
<GoTutorialChallenge
|
<GoTutorialChallenge
|
||||||
state={getBoardFromSimplifiedBoardState(
|
state={boardStateFromSimpleBoard([".....", "OX...", "OXX..", "OOX.O", "OOX.."], GoOpponent.none, GoColor.white)}
|
||||||
[".....", "OX...", "OXX..", "OOX.O", "OOX.."],
|
|
||||||
opponents.none,
|
|
||||||
playerColors.white,
|
|
||||||
)}
|
|
||||||
description={
|
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."
|
"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 = (
|
const saveTheNetworkChallenge = (
|
||||||
<GoTutorialChallenge
|
<GoTutorialChallenge
|
||||||
state={getBoardFromSimplifiedBoardState(
|
state={boardStateFromSimpleBoard(["OO.##", "XO..#", "XX..#", "XO...", "XO..."], GoOpponent.none, GoColor.white)}
|
||||||
["OO.##", "XO..#", "XX..#", "XO...", "XO..."],
|
|
||||||
opponents.none,
|
|
||||||
playerColors.white,
|
|
||||||
)}
|
|
||||||
description={
|
description={
|
||||||
"CHALLENGE: Your routers are in trouble! They only have one open port. Save the black network by connecting them to more empty nodes."
|
"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 = (
|
const onlyGoodMoveChallenge = (
|
||||||
<GoTutorialChallenge
|
<GoTutorialChallenge
|
||||||
state={getBoardFromSimplifiedBoardState(
|
state={boardStateFromSimpleBoard(["XXO.O", "XO.O.", ".OOOO", "XXXXX", "X.X.X"], GoOpponent.none, GoColor.white)}
|
||||||
["XXO.O", "XO.O.", ".OOOO", "XXXXX", "X.X.X"],
|
|
||||||
opponents.none,
|
|
||||||
playerColors.white,
|
|
||||||
)}
|
|
||||||
description={"CHALLENGE: Save the black network on the left! Connect the network to more than one empty node."}
|
description={"CHALLENGE: Save the black network on the left! Connect the network to more than one empty node."}
|
||||||
correctMoves={[{ x: 2, y: 0 }]}
|
correctMoves={[{ x: 2, y: 0 }]}
|
||||||
correctText={
|
correctText={
|
||||||
@ -66,11 +55,7 @@ const onlyGoodMoveChallenge = (
|
|||||||
|
|
||||||
const makeTwoEyesChallenge = (
|
const makeTwoEyesChallenge = (
|
||||||
<GoTutorialChallenge
|
<GoTutorialChallenge
|
||||||
state={getBoardFromSimplifiedBoardState(
|
state={boardStateFromSimpleBoard(["XXOO.", ".XXOO", ".XXO.", ".XXOO", "XXOO."], GoOpponent.none, GoColor.white)}
|
||||||
["XXOO.", ".XXOO", ".XXO.", ".XXOO", "XXOO."],
|
|
||||||
opponents.none,
|
|
||||||
playerColors.white,
|
|
||||||
)}
|
|
||||||
description={
|
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."
|
"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 React from "react";
|
||||||
import { ClassNameMap } from "@mui/styles";
|
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 { findNeighbors } from "../boardState/boardState";
|
||||||
import { pointStyle } from "../boardState/goStyles";
|
import { pointStyle } from "../boardState/goStyles";
|
||||||
import { findAdjacentLibertiesAndAlliesForPoint } from "../boardAnalysis/boardAnalysis";
|
import { findAdjacentLibertiesAndAlliesForPoint, getColorOnSimpleBoard } from "../boardAnalysis/boardAnalysis";
|
||||||
|
|
||||||
interface IProps {
|
interface GoPointProps {
|
||||||
state: BoardState;
|
state: BoardState;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
traditional: boolean;
|
traditional: boolean;
|
||||||
hover: boolean;
|
hover: boolean;
|
||||||
valid: 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 classes = pointStyle();
|
||||||
|
|
||||||
const currentPoint = state.board[x]?.[y];
|
const currentPoint = state.board[x]?.[y];
|
||||||
const player = currentPoint?.player;
|
const player = currentPoint?.color;
|
||||||
|
|
||||||
const isInAtari =
|
const isInAtari = currentPoint && currentPoint.liberties?.length === 1 && player !== GoColor.empty && !traditional;
|
||||||
currentPoint && currentPoint.liberties?.length === 1 && player !== playerColors.empty && !traditional;
|
const liberties = player !== GoColor.empty ? findAdjacentLibertiesAndAlliesForPoint(state.board, x, y) : null;
|
||||||
const liberties = player !== playerColors.empty ? findAdjacentLibertiesAndAlliesForPoint(state, x, y) : null;
|
const neighbors = findNeighbors(state.board, x, y);
|
||||||
const neighbors = findNeighbors(state, x, y);
|
|
||||||
|
|
||||||
const hasNorthLiberty = traditional ? neighbors.north : liberties?.north;
|
const hasNorthLiberty = traditional ? neighbors.north : liberties?.north;
|
||||||
const hasEastLiberty = traditional ? neighbors.east : liberties?.east;
|
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 hasWestLiberty = traditional ? neighbors.west : liberties?.west;
|
||||||
|
|
||||||
const pointClass =
|
const pointClass =
|
||||||
player === playerColors.white
|
player === GoColor.white ? classes.whitePoint : player === GoColor.black ? classes.blackPoint : classes.emptyPoint;
|
||||||
? classes.whitePoint
|
|
||||||
: player === playerColors.black
|
|
||||||
? classes.blackPoint
|
|
||||||
: classes.emptyPoint;
|
|
||||||
|
|
||||||
const colorLiberty = `${player === playerColors.white ? classes.libertyWhite : classes.libertyBlack} ${
|
const colorLiberty = `${player === GoColor.white ? classes.libertyWhite : classes.libertyBlack} ${classes.liberty}`;
|
||||||
classes.liberty
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const sizeClass = getSizeClass(state.board[0].length, classes);
|
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 isPriorMove = player === state.previousPlayer && isNewStone;
|
||||||
|
|
||||||
const emptyPointColorClass =
|
const emptyPointColorClass =
|
||||||
emptyPointOwner === playerColors.white
|
emptyPointOwner === GoColor.white
|
||||||
? classes.libertyWhite
|
? classes.libertyWhite
|
||||||
: emptyPointOwner === playerColors.black
|
: emptyPointOwner === GoColor.black
|
||||||
? classes.libertyBlack
|
? 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={hasWestLiberty ? `${classes.westLiberty} ${colorLiberty}` : classes.liberty}></div>
|
||||||
<div className={`${classes.innerPoint} `}>
|
<div className={`${classes.innerPoint} `}>
|
||||||
<div
|
<div
|
||||||
className={`${pointClass} ${player !== playerColors.empty ? classes.filledPoint : emptyPointColorClass}`}
|
className={`${pointClass} ${player !== GoColor.empty ? classes.filledPoint : emptyPointColorClass}`}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${pointClass} ${classes.tradStone}`} />
|
<div className={`${pointClass} ${classes.tradStone}`} />
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Container, Tab, Tabs } from "@mui/material";
|
|
||||||
import React from "react";
|
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 { BorderInnerSharp, Help, ManageSearch, History } from "@mui/icons-material";
|
||||||
import { GoStatusPage } from "./ui/GoStatusPage";
|
import { GoStatusPage } from "./GoStatusPage";
|
||||||
import { GoHistoryPage } from "./ui/GoHistoryPage";
|
import { GoHistoryPage } from "./GoHistoryPage";
|
||||||
import { GoGameboardWrapper } from "./ui/GoGameboardWrapper";
|
import { GoGameboardWrapper } from "./GoGameboardWrapper";
|
||||||
import { boardStyles } from "./boardState/goStyles";
|
import { boardStyles } from "../boardState/goStyles";
|
||||||
|
|
||||||
export function GoRoot(): React.ReactElement {
|
export function GoRoot(): React.ReactElement {
|
||||||
const classes = boardStyles();
|
const classes = boardStyles();
|
@ -1,28 +1,29 @@
|
|||||||
|
import type { GoScore } from "../Types";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, Typography } from "@mui/material";
|
import { Button, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
import { GoOpponent, GoColor } from "@enums";
|
||||||
import { Modal } from "../../ui/React/Modal";
|
import { Modal } from "../../ui/React/Modal";
|
||||||
import { goScore, opponents, playerColors } from "../boardState/goConstants";
|
|
||||||
import { boardStyles } from "../boardState/goStyles";
|
import { boardStyles } from "../boardState/goStyles";
|
||||||
import { GoScorePowerSummary } from "./GoScorePowerSummary";
|
import { GoScorePowerSummary } from "./GoScorePowerSummary";
|
||||||
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
|
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
|
||||||
|
|
||||||
interface IProps {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
finalScore: goScore;
|
finalScore: GoScore;
|
||||||
newSubnet: () => void;
|
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 classes = boardStyles();
|
||||||
|
|
||||||
const blackScore = finalScore[playerColors.black];
|
const blackScore = finalScore[GoColor.black];
|
||||||
const whiteScore = finalScore[playerColors.white];
|
const whiteScore = finalScore[GoColor.white];
|
||||||
|
|
||||||
const playerWinsText = opponent === opponents.none ? "Black wins!" : "You win!";
|
const playerWinsText = opponent === GoOpponent.none ? "Black wins!" : "You win!";
|
||||||
const opponentWinsText = opponent === opponents.none ? "White wins!" : `Winner: ${opponent}`;
|
const opponentWinsText = opponent === GoOpponent.none ? "White wins!" : `Winner: ${opponent}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={open} onClose={onClose}>
|
<Modal open={open} onClose={onClose}>
|
||||||
@ -37,7 +38,7 @@ export const GoScoreModal = ({ open, onClose, finalScore, newSubnet, opponent }:
|
|||||||
{blackScore.sum > whiteScore.sum ? playerWinsText : opponentWinsText}
|
{blackScore.sum > whiteScore.sum ? playerWinsText : opponentWinsText}
|
||||||
</Typography>
|
</Typography>
|
||||||
<br />
|
<br />
|
||||||
{opponent !== opponents.none ? (
|
{opponent !== GoOpponent.none ? (
|
||||||
<>
|
<>
|
||||||
<GoScorePowerSummary opponent={opponent} finalScore={finalScore} />
|
<GoScorePowerSummary opponent={opponent} finalScore={finalScore} />
|
||||||
<br />
|
<br />
|
||||||
|
@ -1,32 +1,36 @@
|
|||||||
|
import type { GoScore } from "../Types";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Table, TableBody, TableCell, TableRow, Typography, Tooltip } from "@mui/material";
|
import { Table, TableBody, TableCell, TableRow, Typography, Tooltip } from "@mui/material";
|
||||||
|
|
||||||
import { Player } from "@player";
|
import { Player } from "@player";
|
||||||
|
import { GoOpponent, GoColor } from "@enums";
|
||||||
|
import { Go } from "../Go";
|
||||||
import { getBonusText, getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
|
import { getBonusText, getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
|
||||||
import { goScore, opponents, playerColors } from "../boardState/goConstants";
|
|
||||||
import { boardStyles } from "../boardState/goStyles";
|
import { boardStyles } from "../boardState/goStyles";
|
||||||
import { formatNumber } from "../../ui/formatNumber";
|
import { formatNumber } from "../../ui/formatNumber";
|
||||||
import { FactionName } from "@enums";
|
import { getOpponentStats } from "../boardAnalysis/scoring";
|
||||||
import { getPlayerStats } from "../boardAnalysis/scoring";
|
import { getEnumHelper } from "../../utils/EnumHelper";
|
||||||
|
|
||||||
interface IProps {
|
interface Props {
|
||||||
finalScore: goScore;
|
finalScore: GoScore;
|
||||||
opponent: opponents;
|
opponent: GoOpponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GoScorePowerSummary = ({ finalScore, opponent }: IProps) => {
|
export const GoScorePowerSummary = ({ finalScore, opponent }: Props) => {
|
||||||
const classes = boardStyles();
|
const classes = boardStyles();
|
||||||
const status = getPlayerStats(opponent);
|
const status = getOpponentStats(opponent);
|
||||||
const winStreak = status.winStreak;
|
const winStreak = status.winStreak;
|
||||||
const oldWinStreak = status.winStreak;
|
const oldWinStreak = status.winStreak;
|
||||||
const nodePower = formatNumber(status.nodePower, 2);
|
const nodePower = formatNumber(status.nodePower, 2);
|
||||||
const blackScore = finalScore[playerColors.black];
|
const blackScore = finalScore[GoColor.black];
|
||||||
const whiteScore = finalScore[playerColors.white];
|
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 winstreakMultiplier = getWinstreakMultiplier(winStreak, oldWinStreak);
|
||||||
const nodePowerIncrease = formatNumber(blackScore.sum * difficultyMultiplier * winstreakMultiplier, 2);
|
const nodePowerIncrease = formatNumber(blackScore.sum * difficultyMultiplier * winstreakMultiplier, 2);
|
||||||
const showFavorGain =
|
const showFavorGain = faction && winStreak > 0 && winStreak % 2 === 0 && Player.factions.includes(faction);
|
||||||
winStreak > 0 && winStreak % 2 === 0 && Player.factions.includes(opponent as unknown as FactionName);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,19 +1,22 @@
|
|||||||
|
import type { GoScore } from "../Types";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Table, TableBody, TableCell, TableRow, Tooltip } from "@mui/material";
|
import { Table, TableBody, TableCell, TableRow, Tooltip } from "@mui/material";
|
||||||
import { boardStyles } from "../boardState/goStyles";
|
|
||||||
import { goScore, opponents, playerColors } from "../boardState/goConstants";
|
|
||||||
|
|
||||||
interface IProps {
|
import { GoOpponent, GoColor } from "@enums";
|
||||||
score: goScore;
|
import { boardStyles } from "../boardState/goStyles";
|
||||||
opponent: opponents;
|
|
||||||
|
interface GoScoreSummaryTableProps {
|
||||||
|
score: GoScore;
|
||||||
|
opponent: GoOpponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GoScoreSummaryTable = ({ score, opponent }: IProps) => {
|
export const GoScoreSummaryTable = ({ score, opponent }: GoScoreSummaryTableProps) => {
|
||||||
const classes = boardStyles();
|
const classes = boardStyles();
|
||||||
const blackScore = score[playerColors.black];
|
const blackScore = score[GoColor.black];
|
||||||
const whiteScore = score[playerColors.white];
|
const whiteScore = score[GoColor.white];
|
||||||
const blackPlayerName = opponent === opponents.none ? "Black" : "You";
|
const blackPlayerName = opponent === GoOpponent.none ? "Black" : "You";
|
||||||
const whitePlayerName = opponent === opponents.none ? "White" : opponent;
|
const whitePlayerName = opponent === GoOpponent.none ? "White" : opponent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,21 +1,20 @@
|
|||||||
import React from "react";
|
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 { getScore } from "../boardAnalysis/scoring";
|
||||||
import { Player } from "@player";
|
|
||||||
import { Grid, Table, TableBody, TableCell, TableRow } from "@mui/material";
|
|
||||||
import { GoGameboard } from "./GoGameboard";
|
import { GoGameboard } from "./GoGameboard";
|
||||||
import { boardStyles } from "../boardState/goStyles";
|
import { boardStyles } from "../boardState/goStyles";
|
||||||
import { useRerender } from "../../ui/React/hooks";
|
import { useRerender } from "../../ui/React/hooks";
|
||||||
import { getBonusText } from "../effects/effect";
|
import { getBonusText } from "../effects/effect";
|
||||||
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
|
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
|
||||||
|
import { getRecordKeys } from "../../Types/Record";
|
||||||
|
|
||||||
export const GoStatusPage = (): React.ReactElement => {
|
export const GoStatusPage = (): React.ReactElement => {
|
||||||
useRerender(400);
|
useRerender(400);
|
||||||
const classes = boardStyles();
|
const classes = boardStyles();
|
||||||
const score = getScore(Player.go.boardState);
|
const score = getScore(Go.currentGame);
|
||||||
const opponent = Player.go.boardState.ai;
|
const opponent = Go.currentGame.ai;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -29,7 +28,7 @@ export const GoStatusPage = (): React.ReactElement => {
|
|||||||
<Grid item>
|
<Grid item>
|
||||||
<div className={classes.statusPageGameboard}>
|
<div className={classes.statusPageGameboard}>
|
||||||
<GoGameboard
|
<GoGameboard
|
||||||
boardState={Player.go.boardState}
|
boardState={Go.currentGame}
|
||||||
traditional={false}
|
traditional={false}
|
||||||
clickHandler={(x, y) => ({ x, y })}
|
clickHandler={(x, y) => ({ x, y })}
|
||||||
hover={false}
|
hover={false}
|
||||||
@ -50,7 +49,7 @@ export const GoStatusPage = (): React.ReactElement => {
|
|||||||
<strong>Effect:</strong>
|
<strong>Effect:</strong>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{opponentList.map((faction, index) => {
|
{getRecordKeys(Go.stats).map((faction, index) => {
|
||||||
return (
|
return (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
<TableCell className={classes.cellNone}>
|
<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 React, { useState } from "react";
|
||||||
import { boardSizes, opponentDetails, opponentList, opponents } from "../boardState/goConstants";
|
import { Box, Button, MenuItem, Select, SelectChangeEvent, Tooltip, Typography } from "@mui/material";
|
||||||
import { Player } from "@player";
|
|
||||||
|
import { GoOpponent } from "@enums";
|
||||||
|
import { Go } from "../Go";
|
||||||
|
import { boardSizes, opponentDetails } from "../Constants";
|
||||||
import { boardStyles } from "../boardState/goStyles";
|
import { boardStyles } from "../boardState/goStyles";
|
||||||
import { Modal } from "../../ui/React/Modal";
|
import { Modal } from "../../ui/React/Modal";
|
||||||
import { getHandicap } from "../boardState/boardState";
|
import { getHandicap } from "../boardState/boardState";
|
||||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||||
import { Settings } from "../../Settings/Settings";
|
import { Settings } from "../../Settings/Settings";
|
||||||
import { getPlayerStats } from "../boardAnalysis/scoring";
|
import { getOpponentStats } from "../boardAnalysis/scoring";
|
||||||
import { showWorldDemon } from "../boardAnalysis/goAI";
|
import { showWorldDemon } from "../boardAnalysis/goAI";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
search: (size: number, opponent: opponents) => void;
|
search: (size: number, opponent: GoOpponent) => void;
|
||||||
cancel: () => void;
|
cancel: () => void;
|
||||||
showInstructions: () => void;
|
showInstructions: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProps): React.ReactElement => {
|
export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProps): React.ReactElement => {
|
||||||
const classes = boardStyles();
|
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 =
|
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 [boardSize, setBoardSize] = useState(preselectedBoardSize);
|
||||||
|
|
||||||
const opponentFactions = [opponents.none, ...opponentList];
|
const opponentFactions = Object.values(GoOpponent).filter(
|
||||||
if (showWorldDemon()) {
|
(opponent) => opponent !== GoOpponent.w0r1d_d43m0n || showWorldDemon(),
|
||||||
opponentFactions.push(opponents.w0r1d_d43m0n);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const handicap = getHandicap(boardSize, opponent);
|
const handicap = getHandicap(boardSize, opponent);
|
||||||
|
|
||||||
function changeOpponent(event: SelectChangeEvent): void {
|
function changeOpponent(event: SelectChangeEvent): void {
|
||||||
const newOpponent = event.target.value as opponents;
|
const newOpponent = event.target.value as GoOpponent;
|
||||||
setOpponent(newOpponent);
|
setOpponent(newOpponent);
|
||||||
if (newOpponent === opponents.w0r1d_d43m0n) {
|
if (newOpponent === GoOpponent.w0r1d_d43m0n) {
|
||||||
setBoardSize(19);
|
setBoardSize(19);
|
||||||
|
|
||||||
const stats = getPlayerStats(opponents.w0r1d_d43m0n);
|
const stats = getOpponentStats(GoOpponent.w0r1d_d43m0n);
|
||||||
if (stats?.wins + stats?.losses === 0) {
|
if (stats.wins + stats.losses === 0) {
|
||||||
Settings.GoTraditionalStyle = false;
|
Settings.GoTraditionalStyle = false;
|
||||||
}
|
}
|
||||||
} else if (boardSize > 13) {
|
} else if (boardSize > 13) {
|
||||||
@ -67,12 +68,12 @@ export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProp
|
|||||||
<br />
|
<br />
|
||||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||||
<Typography className={classes.opponentLabel}>
|
<Typography className={classes.opponentLabel}>
|
||||||
{opponent !== opponents.none ? "Opponent Faction: " : ""}
|
{opponent !== GoOpponent.none ? "Opponent Faction: " : ""}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Select value={opponent} onChange={changeOpponent} sx={{ mr: 1 }}>
|
<Select value={opponent} onChange={changeOpponent} sx={{ mr: 1 }}>
|
||||||
{opponentFactions.map((faction) => (
|
{opponentFactions.map((faction) => (
|
||||||
<MenuItem key={faction} value={faction}>
|
<MenuItem key={faction} value={faction}>
|
||||||
{faction === opponents.w0r1d_d43m0n ? (
|
{faction === GoOpponent.w0r1d_d43m0n ? (
|
||||||
<CorruptableText content="???????????????" spoiler={false} />
|
<CorruptableText content="???????????????" spoiler={false} />
|
||||||
) : (
|
) : (
|
||||||
`${faction} (${opponentDetails[faction].description})`
|
`${faction} (${opponentDetails[faction].description})`
|
||||||
@ -83,7 +84,7 @@ export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProp
|
|||||||
</Box>
|
</Box>
|
||||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||||
<Typography className={classes.opponentLabel}>Subnet size: </Typography>
|
<Typography className={classes.opponentLabel}>Subnet size: </Typography>
|
||||||
{opponent === opponents.w0r1d_d43m0n ? (
|
{opponent === GoOpponent.w0r1d_d43m0n ? (
|
||||||
<Typography>????</Typography>
|
<Typography>????</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Select value={`${boardSize}`} onChange={changeBoardSize} sx={{ mr: 1 }}>
|
<Select value={`${boardSize}`} onChange={changeBoardSize} sx={{ mr: 1 }}>
|
||||||
@ -118,7 +119,7 @@ export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProp
|
|||||||
<br />
|
<br />
|
||||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle} ${classes.flavorText}`}>
|
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle} ${classes.flavorText}`}>
|
||||||
<Typography>
|
<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(0, 40)} spoiler={false} />
|
||||||
<CorruptableText content={opponentDetails[opponent].flavorText.slice(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 />
|
<br />
|
||||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||||
<Typography>
|
<Typography>
|
||||||
{opponent !== opponents.none ? "Faction subnet bonus:" : ""} {opponentDetails[opponent].bonusDescription}
|
{opponent !== GoOpponent.none ? "Faction subnet bonus:" : ""} {opponentDetails[opponent].bonusDescription}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<br />
|
<br />
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
import { Typography, Button } from "@mui/material";
|
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 { GoGameboard } from "./GoGameboard";
|
||||||
import { evaluateIfMoveIsValid } from "../boardAnalysis/boardAnalysis";
|
import { evaluateIfMoveIsValid } from "../boardAnalysis/boardAnalysis";
|
||||||
import { SnackbarEvents } from "../../ui/React/Snackbar";
|
import { SnackbarEvents } from "../../ui/React/Snackbar";
|
||||||
import { ToastVariant } from "@enums";
|
|
||||||
import { getStateCopy, makeMove } from "../boardState/boardState";
|
import { getStateCopy, makeMove } from "../boardState/boardState";
|
||||||
import { boardStyles } from "../boardState/goStyles";
|
import { boardStyles } from "../boardState/goStyles";
|
||||||
|
|
||||||
@ -32,31 +32,27 @@ export function GoTutorialChallenge({
|
|||||||
incorrectMoves2,
|
incorrectMoves2,
|
||||||
incorrectText2,
|
incorrectText2,
|
||||||
}: IProps): React.ReactElement {
|
}: IProps): React.ReactElement {
|
||||||
|
const stateRef = useRef(getStateCopy(state));
|
||||||
const classes = boardStyles();
|
const classes = boardStyles();
|
||||||
const [currentState, setCurrentState] = useState(getStateCopy(state));
|
|
||||||
const [displayText, setDisplayText] = useState(description);
|
const [displayText, setDisplayText] = useState(description);
|
||||||
const [showReset, setShowReset] = useState(false);
|
const [showReset, setShowReset] = useState(false);
|
||||||
|
|
||||||
const handleClick = (x: number, y: number) => {
|
const handleClick = (x: number, y: number) => {
|
||||||
if (currentState.history.length) {
|
if (stateRef.current.previousBoard) {
|
||||||
SnackbarEvents.emit(`Hit 'Reset' to try again`, ToastVariant.WARNING, 2000);
|
SnackbarEvents.emit(`Hit 'Reset' to try again`, ToastVariant.WARNING, 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setShowReset(true);
|
setShowReset(true);
|
||||||
|
|
||||||
const validity = evaluateIfMoveIsValid(currentState, x, y, playerColors.black);
|
const validity = evaluateIfMoveIsValid(stateRef.current, x, y, GoColor.black);
|
||||||
if (validity != validityReason.valid) {
|
if (validity != GoValidity.valid) {
|
||||||
setDisplayText(
|
setDisplayText(
|
||||||
"Invalid move: You cannot suicide your routers by placing them with no access to any empty ports.",
|
"Invalid move: You cannot suicide your routers by placing them with no access to any empty ports.",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedBoard = makeMove(currentState, x, y, playerColors.black);
|
if (makeMove(stateRef.current, x, y, GoColor.black)) {
|
||||||
|
|
||||||
if (updatedBoard) {
|
|
||||||
setCurrentState(getStateCopy(updatedBoard));
|
|
||||||
|
|
||||||
if (correctMoves.find((move) => move.x === x && move.y === y)) {
|
if (correctMoves.find((move) => move.x === x && move.y === y)) {
|
||||||
setDisplayText(correctText);
|
setDisplayText(correctText);
|
||||||
} else if (incorrectMoves1?.find((move) => move.x === x && move.y === y)) {
|
} else if (incorrectMoves1?.find((move) => move.x === x && move.y === y)) {
|
||||||
@ -70,7 +66,7 @@ export function GoTutorialChallenge({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setCurrentState(getStateCopy(state));
|
stateRef.current = getStateCopy(state);
|
||||||
setDisplayText(description);
|
setDisplayText(description);
|
||||||
setShowReset(false);
|
setShowReset(false);
|
||||||
};
|
};
|
||||||
@ -78,7 +74,7 @@ export function GoTutorialChallenge({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={classes.instructionBoard}>
|
<div className={classes.instructionBoard}>
|
||||||
<GoGameboard boardState={currentState} traditional={false} clickHandler={handleClick} hover={true} />
|
<GoGameboard boardState={stateRef.current} traditional={false} clickHandler={handleClick} hover={true} />
|
||||||
</div>
|
</div>
|
||||||
<Typography>{displayText}</Typography>
|
<Typography>{displayText}</Typography>
|
||||||
{showReset ? <Button onClick={reset}>Reset</Button> : ""}
|
{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)) {
|
for (const [key, { version, replacement, replaceMsg }] of Object.entries(infos)) {
|
||||||
Object.defineProperty(api, key, {
|
Object.defineProperty(api, key, {
|
||||||
value: (ctx: NetscriptContext) => () => {
|
value: (ctx: NetscriptContext) => () => {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Function removed in ${version}. ${replaceMsg ? replacement : `Please use ${replacement} instead.`}`,
|
`Function removed in ${version}. ${replaceMsg ? replacement : `Please use ${replacement} instead.`}`,
|
||||||
"REMOVED FUNCTION",
|
"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 React from "react";
|
||||||
import { NetscriptContext } from "./APIWrapper";
|
|
||||||
import { WorkerScript } from "./WorkerScript";
|
|
||||||
import { killWorkerScript } from "./killWorkerScript";
|
import { killWorkerScript } from "./killWorkerScript";
|
||||||
import { GetAllServers, GetServer } from "../Server/AllServers";
|
import { GetAllServers, GetServer } from "../Server/AllServers";
|
||||||
import { Player } from "@player";
|
import { Player } from "@player";
|
||||||
@ -31,7 +31,6 @@ import { findRunningScripts, findRunningScriptByPid } from "../Script/ScriptHelp
|
|||||||
import { arrayToString } from "../utils/helpers/ArrayHelpers";
|
import { arrayToString } from "../utils/helpers/ArrayHelpers";
|
||||||
import { HacknetServer } from "../Hacknet/HacknetServer";
|
import { HacknetServer } from "../Hacknet/HacknetServer";
|
||||||
import { BaseServer } from "../Server/BaseServer";
|
import { BaseServer } from "../Server/BaseServer";
|
||||||
import { dialogBoxCreate } from "../ui/React/DialogBox";
|
|
||||||
import { RamCostConstants } from "./RamCostGenerator";
|
import { RamCostConstants } from "./RamCostGenerator";
|
||||||
import { isPositiveInteger, PositiveInteger, Unknownify, isPositiveNumber, PositiveNumber } from "../types";
|
import { isPositiveInteger, PositiveInteger, Unknownify, isPositiveNumber, PositiveNumber } from "../types";
|
||||||
import { Engine } from "../engine";
|
import { Engine } from "../engine";
|
||||||
@ -39,6 +38,8 @@ import { resolveFilePath, FilePath } from "../Paths/FilePath";
|
|||||||
import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath";
|
import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath";
|
||||||
import { CustomBoundary } from "../ui/Components/CustomBoundary";
|
import { CustomBoundary } from "../ui/Components/CustomBoundary";
|
||||||
import { ServerConstants } from "../Server/data/Constants";
|
import { ServerConstants } from "../Server/data/Constants";
|
||||||
|
import { basicErrorMessage, errorMessage, log } from "./ErrorMessages";
|
||||||
|
import { assertString, debugType } from "./TypeAssertion";
|
||||||
|
|
||||||
export const helpers = {
|
export const helpers = {
|
||||||
string,
|
string,
|
||||||
@ -48,8 +49,8 @@ export const helpers = {
|
|||||||
runOptions,
|
runOptions,
|
||||||
spawnOptions,
|
spawnOptions,
|
||||||
argsToString,
|
argsToString,
|
||||||
makeBasicErrorMsg,
|
basicErrorMessage,
|
||||||
makeRuntimeErrorMsg,
|
errorMessage,
|
||||||
validateHGWOptions,
|
validateHGWOptions,
|
||||||
checkEnvFlags,
|
checkEnvFlags,
|
||||||
checkSingularityAccess,
|
checkSingularityAccess,
|
||||||
@ -92,33 +93,6 @@ export interface CompleteHGWOptions {
|
|||||||
additionalMsec: number;
|
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. */
|
/** 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 {
|
function string(ctx: NetscriptContext, argName: string, v: unknown): string {
|
||||||
if (typeof v === "number") v = v + ""; // cast to 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);
|
const x = parseFloat(v);
|
||||||
if (!isNaN(x)) return x; // otherwise it wasn't even a string representing a number.
|
if (!isNaN(x)) return x; // otherwise it wasn't even a string representing a number.
|
||||||
} else if (typeof v === "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;
|
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. */
|
/** 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 {
|
function positiveInteger(ctx: NetscriptContext, argName: string, v: unknown): PositiveInteger {
|
||||||
const n = number(ctx, argName, v);
|
const n = number(ctx, argName, v);
|
||||||
if (!isPositiveInteger(n)) {
|
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;
|
return n;
|
||||||
}
|
}
|
||||||
@ -150,13 +124,13 @@ function positiveInteger(ctx: NetscriptContext, argName: string, v: unknown): Po
|
|||||||
function positiveNumber(ctx: NetscriptContext, argName: string, v: unknown): PositiveNumber {
|
function positiveNumber(ctx: NetscriptContext, argName: string, v: unknown): PositiveNumber {
|
||||||
const n = number(ctx, argName, v);
|
const n = number(ctx, argName, v);
|
||||||
if (!isPositiveNumber(n)) {
|
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;
|
return n;
|
||||||
}
|
}
|
||||||
/** Returns args back if it is a ScriptArg[]. Throws an error if it is not. */
|
/** Returns args back if it is a ScriptArg[]. Throws an error if it is not. */
|
||||||
function scriptArgs(ctx: NetscriptContext, args: unknown) {
|
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;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +157,7 @@ function runOptions(ctx: NetscriptContext, threadOrOption: unknown): CompleteRun
|
|||||||
if (options.ramOverride !== undefined && options.ramOverride !== null) {
|
if (options.ramOverride !== undefined && options.ramOverride !== null) {
|
||||||
result.ramOverride = number(ctx, "RunOptions.ramOverride", options.ramOverride);
|
result.ramOverride = number(ctx, "RunOptions.ramOverride", options.ramOverride);
|
||||||
if (result.ramOverride < RamCostConstants.Base) {
|
if (result.ramOverride < RamCostConstants.Base) {
|
||||||
throw makeRuntimeErrorMsg(
|
throw errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`RunOptions.ramOverride must be >= baseCost (${RamCostConstants.Base}), was ${result.ramOverride}`,
|
`RunOptions.ramOverride must be >= baseCost (${RamCostConstants.Base}), was ${result.ramOverride}`,
|
||||||
);
|
);
|
||||||
@ -235,73 +209,6 @@ function argsToString(args: unknown[]): string {
|
|||||||
}, "") as 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 {
|
function validateHGWOptions(ctx: NetscriptContext, opts: unknown): CompleteHGWOptions {
|
||||||
const result: CompleteHGWOptions = {
|
const result: CompleteHGWOptions = {
|
||||||
threads: ctx.workerScript.scriptRef.threads,
|
threads: ctx.workerScript.scriptRef.threads,
|
||||||
@ -312,17 +219,17 @@ function validateHGWOptions(ctx: NetscriptContext, opts: unknown): CompleteHGWOp
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
if (typeof opts !== "object") {
|
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
|
// Safe assertion since threadOrOption type has been narrowed to a non-null object
|
||||||
const options = opts as Unknownify<CompleteHGWOptions>;
|
const options = opts as Unknownify<CompleteHGWOptions>;
|
||||||
result.stock = !!options.stock;
|
result.stock = !!options.stock;
|
||||||
result.additionalMsec = number(ctx, "opts.additionalMsec", options.additionalMsec ?? 0);
|
result.additionalMsec = number(ctx, "opts.additionalMsec", options.additionalMsec ?? 0);
|
||||||
if (result.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) {
|
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 requestedThreads = options.threads;
|
||||||
const threads = ctx.workerScript.scriptRef.threads;
|
const threads = ctx.workerScript.scriptRef.threads;
|
||||||
@ -331,7 +238,7 @@ function validateHGWOptions(ctx: NetscriptContext, opts: unknown): CompleteHGWOp
|
|||||||
} else {
|
} else {
|
||||||
const positiveThreads = positiveNumber(ctx, "opts.threads", requestedThreads);
|
const positiveThreads = positiveNumber(ctx, "opts.threads", requestedThreads);
|
||||||
if (positiveThreads > threads) {
|
if (positiveThreads > threads) {
|
||||||
throw makeRuntimeErrorMsg(
|
throw errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Too many threads requested by ${ctx.function}. Requested: ${positiveThreads}. Has: ${threads}.`,
|
`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. */
|
/** Validate singularity access by throwing an error if the player does not have access. */
|
||||||
function checkSingularityAccess(ctx: NetscriptContext): void {
|
function checkSingularityAccess(ctx: NetscriptContext): void {
|
||||||
if (Player.bitNodeN !== 4 && Player.sourceFileLvl(4) === 0) {
|
if (Player.bitNodeN !== 4 && Player.sourceFileLvl(4) === 0) {
|
||||||
throw makeRuntimeErrorMsg(
|
throw errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`This singularity function requires Source-File 4 to run. A power up you obtain later in the game.
|
`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.`,
|
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") {
|
if (ws.env.runningFn && ctx.function !== "asleep") {
|
||||||
log(ctx, () => "Failed to run due to failed concurrency check.");
|
log(ctx, () => "Failed to run due to failed concurrency check.");
|
||||||
const err = makeRuntimeErrorMsg(
|
const err = errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Concurrent calls to Netscript functions are not allowed!
|
`Concurrent calls to Netscript functions are not allowed!
|
||||||
Did you forget to await hack(), grow(), or some other
|
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.
|
// rounding issues without exposing rounding exploits in ramUsage.
|
||||||
if (ws.dynamicRamUsage > 1.00000000000001 * ws.scriptRef.ramUsage) {
|
if (ws.dynamicRamUsage > 1.00000000000001 * ws.scriptRef.ramUsage) {
|
||||||
log(ctx, () => "Insufficient static ram available.");
|
log(ctx, () => "Insufficient static ram available.");
|
||||||
const err = makeRuntimeErrorMsg(
|
const err = errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Dynamic RAM usage calculated to be greater than RAM allocation.
|
`Dynamic RAM usage calculated to be greater than RAM allocation.
|
||||||
This is probably because you somehow circumvented the static RAM calculation.
|
This is probably because you somehow circumvented the static RAM calculation.
|
||||||
@ -449,7 +356,7 @@ function scriptIdentifier(
|
|||||||
args,
|
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);
|
const server = GetServer(hostname);
|
||||||
if (server == null || (server.serversOnNetwork.length == 0 && server.hostname != "home")) {
|
if (server == null || (server.serversOnNetwork.length == 0 && server.hostname != "home")) {
|
||||||
const str = hostname === "" ? "'' (empty string)" : "'" + hostname + "'";
|
const str = hostname === "" ? "'' (empty string)" : "'" + hostname + "'";
|
||||||
throw makeRuntimeErrorMsg(ctx, `Invalid hostname: ${str}`);
|
throw errorMessage(ctx, `Invalid hostname: ${str}`);
|
||||||
}
|
}
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
@ -478,7 +385,7 @@ function hack(ctx: NetscriptContext, hostname: string, manual: boolean, opts: un
|
|||||||
const { threads, stock, additionalMsec } = validateHGWOptions(ctx, opts);
|
const { threads, stock, additionalMsec } = validateHGWOptions(ctx, opts);
|
||||||
const server = getServer(ctx, hostname);
|
const server = getServer(ctx, hostname);
|
||||||
if (!(server instanceof Server)) {
|
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
|
// 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
|
// No root access or skill level too low
|
||||||
const canHack = netscriptCanHack(server);
|
const canHack = netscriptCanHack(server);
|
||||||
if (!canHack.res) {
|
if (!canHack.res) {
|
||||||
throw makeRuntimeErrorMsg(ctx, canHack.msg || "");
|
throw errorMessage(ctx, canHack.msg || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
log(
|
log(
|
||||||
@ -578,7 +485,7 @@ function hack(ctx: NetscriptContext, hostname: string, manual: boolean, opts: un
|
|||||||
function portNumber(ctx: NetscriptContext, _n: unknown): PortNumber {
|
function portNumber(ctx: NetscriptContext, _n: unknown): PortNumber {
|
||||||
const n = positiveInteger(ctx, "portNumber", _n);
|
const n = positiveInteger(ctx, "portNumber", _n);
|
||||||
if (n > CONSTANTS.NumNetscriptPorts) {
|
if (n > CONSTANTS.NumNetscriptPorts) {
|
||||||
throw makeRuntimeErrorMsg(
|
throw errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Trying to use an invalid port: ${n}. Must be less or equal to ${CONSTANTS.NumNetscriptPorts}.`,
|
`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,
|
city: undefined,
|
||||||
};
|
};
|
||||||
const error = missingKey(fakePerson, p);
|
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;
|
return p as IPerson;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,7 +523,7 @@ function server(ctx: NetscriptContext, s: unknown): IServer {
|
|||||||
purchasedByPlayer: undefined,
|
purchasedByPlayer: undefined,
|
||||||
};
|
};
|
||||||
const error = missingKey(fakeServer, s);
|
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;
|
return s as IServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -632,37 +539,33 @@ function missingKey(expect: object, actual: unknown): string | false {
|
|||||||
|
|
||||||
function gang(ctx: NetscriptContext, g: unknown): FormulaGang {
|
function gang(ctx: NetscriptContext, g: unknown): FormulaGang {
|
||||||
const error = missingKey({ respect: 0, territory: 0, wantedLevel: 0 }, g);
|
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;
|
return g as FormulaGang;
|
||||||
}
|
}
|
||||||
|
|
||||||
function gangMember(ctx: NetscriptContext, m: unknown): GangMember {
|
function gangMember(ctx: NetscriptContext, m: unknown): GangMember {
|
||||||
const error = missingKey(new GangMember(), m);
|
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;
|
return m as GangMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
function gangTask(ctx: NetscriptContext, t: unknown): GangMemberTask {
|
function gangTask(ctx: NetscriptContext, t: unknown): GangMemberTask {
|
||||||
const error = missingKey(new GangMemberTask("", "", false, false, { hackWeight: 100 }), t);
|
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;
|
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 {
|
export function filePath(ctx: NetscriptContext, argName: string, filename: unknown): FilePath {
|
||||||
assertString(ctx, argName, filename);
|
assertString(ctx, argName, filename);
|
||||||
const path = resolveFilePath(filename, ctx.workerScript.name);
|
const path = resolveFilePath(filename, ctx.workerScript.name);
|
||||||
if (path) return path;
|
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 {
|
export function scriptPath(ctx: NetscriptContext, argName: string, filename: unknown): ScriptFilePath {
|
||||||
const path = filePath(ctx, argName, filename);
|
const path = filePath(ctx, argName, filename);
|
||||||
if (hasScriptExtension(path)) return path;
|
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[],
|
scriptArgs: ScriptArg[],
|
||||||
): Map<number, RunningScript> | null {
|
): Map<number, RunningScript> | null {
|
||||||
if (!Array.isArray(scriptArgs)) {
|
if (!Array.isArray(scriptArgs)) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
"Invalid scriptArgs argument passed into getRunningScriptByArgs().\n" +
|
"Invalid scriptArgs argument passed into getRunningScriptByArgs().\n" +
|
||||||
"This is probably a bug. Please report to game developer",
|
"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
|
// Incrementing value for custom element keys
|
||||||
let customElementKey = 0;
|
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 { AddRecentScript } from "./RecentScripts";
|
||||||
import { ITutorial } from "../InteractiveTutorial";
|
import { ITutorial } from "../InteractiveTutorial";
|
||||||
import { AlertEvents } from "../ui/React/AlertManager";
|
import { AlertEvents } from "../ui/React/AlertManager";
|
||||||
import { handleUnknownError } from "./NetscriptHelpers";
|
import { handleUnknownError } from "./ErrorMessages";
|
||||||
import { roundToTwo } from "../utils/helpers/roundToTwo";
|
import { roundToTwo } from "../utils/helpers/roundToTwo";
|
||||||
|
|
||||||
export function killWorkerScript(ws: WorkerScript): boolean {
|
export function killWorkerScript(ws: WorkerScript): boolean {
|
||||||
|
@ -173,7 +173,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (isNaN(hackAmount)) {
|
if (isNaN(hackAmount)) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Invalid hackAmount argument passed into hackAnalyzeThreads: ${hackAmount}. Must be numeric.`,
|
`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
|
// No root access or skill level too low
|
||||||
const canHack = netscriptCanGrow(server);
|
const canHack = netscriptCanGrow(server);
|
||||||
if (!canHack.res) {
|
if (!canHack.res) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || "");
|
throw helpers.errorMessage(ctx, canHack.msg || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
const growTime = calculateGrowTime(server, Player) + additionalMsec / 1000.0;
|
const growTime = calculateGrowTime(server, Player) + additionalMsec / 1000.0;
|
||||||
@ -318,11 +318,11 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (mult < 1 || !isFinite(mult)) {
|
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
|
// 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) {
|
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);
|
return numCycleForGrowth(server, mult, cores);
|
||||||
@ -363,7 +363,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
// No root access or skill level too low
|
// No root access or skill level too low
|
||||||
const canHack = netscriptCanWeaken(server);
|
const canHack = netscriptCanWeaken(server);
|
||||||
if (!canHack.res) {
|
if (!canHack.res) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || "");
|
throw helpers.errorMessage(ctx, canHack.msg || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
const weakenTime = calculateWeakenTime(server, Player) + additionalMsec / 1000.0;
|
const weakenTime = calculateWeakenTime(server, Player) + additionalMsec / 1000.0;
|
||||||
@ -424,7 +424,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
(ctx) =>
|
(ctx) =>
|
||||||
(...args) => {
|
(...args) => {
|
||||||
if (args.length === 0) {
|
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));
|
ctx.workerScript.print(helpers.argsToString(args));
|
||||||
},
|
},
|
||||||
@ -433,7 +433,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
(_format, ...args) => {
|
(_format, ...args) => {
|
||||||
const format = helpers.string(ctx, "format", _format);
|
const format = helpers.string(ctx, "format", _format);
|
||||||
if (typeof format !== "string") {
|
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));
|
ctx.workerScript.print(vsprintf(format, args));
|
||||||
},
|
},
|
||||||
@ -441,7 +441,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
(ctx) =>
|
(ctx) =>
|
||||||
(...args) => {
|
(...args) => {
|
||||||
if (args.length === 0) {
|
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);
|
const str = helpers.argsToString(args);
|
||||||
if (str.startsWith("ERROR") || str.startsWith("FAIL")) {
|
if (str.startsWith("ERROR") || str.startsWith("FAIL")) {
|
||||||
@ -497,7 +497,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
}
|
}
|
||||||
helpers.log(ctx, () => `Disabled logging for all functions`);
|
helpers.log(ctx, () => `Disabled logging for all functions`);
|
||||||
} else if (possibleLogs[fn] === undefined) {
|
} else if (possibleLogs[fn] === undefined) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid argument: ${fn}.`);
|
throw helpers.errorMessage(ctx, `Invalid argument: ${fn}.`);
|
||||||
} else {
|
} else {
|
||||||
ctx.workerScript.disableLogs[fn] = true;
|
ctx.workerScript.disableLogs[fn] = true;
|
||||||
helpers.log(ctx, () => `Disabled logging for ${fn}`);
|
helpers.log(ctx, () => `Disabled logging for ${fn}`);
|
||||||
@ -511,7 +511,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
}
|
}
|
||||||
helpers.log(ctx, () => `Enabled logging for all functions`);
|
helpers.log(ctx, () => `Enabled logging for all functions`);
|
||||||
} else if (possibleLogs[fn] === undefined) {
|
} else if (possibleLogs[fn] === undefined) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid argument: ${fn}.`);
|
throw helpers.errorMessage(ctx, `Invalid argument: ${fn}.`);
|
||||||
}
|
}
|
||||||
delete ctx.workerScript.disableLogs[fn];
|
delete ctx.workerScript.disableLogs[fn];
|
||||||
helpers.log(ctx, () => `Enabled logging for ${fn}`);
|
helpers.log(ctx, () => `Enabled logging for ${fn}`);
|
||||||
@ -519,7 +519,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
isLogEnabled: (ctx) => (_fn) => {
|
isLogEnabled: (ctx) => (_fn) => {
|
||||||
const fn = helpers.string(ctx, "fn", _fn);
|
const fn = helpers.string(ctx, "fn", _fn);
|
||||||
if (possibleLogs[fn] === undefined) {
|
if (possibleLogs[fn] === undefined) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid argument: ${fn}.`);
|
throw helpers.errorMessage(ctx, `Invalid argument: ${fn}.`);
|
||||||
}
|
}
|
||||||
return !ctx.workerScript.disableLogs[fn];
|
return !ctx.workerScript.disableLogs[fn];
|
||||||
},
|
},
|
||||||
@ -605,10 +605,10 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!Player.hasProgram(CompletedProgramName.nuke)) {
|
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) {
|
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;
|
server.hasAdminRights = true;
|
||||||
helpers.log(ctx, () => `Executed NUKE.exe virus on '${server.hostname}' to gain root access.`);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (!Player.hasProgram(CompletedProgramName.bruteSsh)) {
|
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) {
|
if (!server.sshPortOpen) {
|
||||||
helpers.log(ctx, () => `Executed BruteSSH.exe on '${server.hostname}' to open SSH port (22).`);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (!Player.hasProgram(CompletedProgramName.ftpCrack)) {
|
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) {
|
if (!server.ftpPortOpen) {
|
||||||
helpers.log(ctx, () => `Executed FTPCrack.exe on '${server.hostname}' to open FTP port (21).`);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (!Player.hasProgram(CompletedProgramName.relaySmtp)) {
|
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) {
|
if (!server.smtpPortOpen) {
|
||||||
helpers.log(ctx, () => `Executed relaySMTP.exe on '${server.hostname}' to open SMTP port (25).`);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (!Player.hasProgram(CompletedProgramName.httpWorm)) {
|
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) {
|
if (!server.httpPortOpen) {
|
||||||
helpers.log(ctx, () => `Executed HTTPWorm.exe on '${server.hostname}' to open HTTP port (80).`);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
if (!Player.hasProgram(CompletedProgramName.sqlInject)) {
|
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) {
|
if (!server.sqlPortOpen) {
|
||||||
helpers.log(ctx, () => `Executed SQLInject.exe on '${server.hostname}' to open SQL port (1433).`);
|
helpers.log(ctx, () => `Executed SQLInject.exe on '${server.hostname}' to open SQL port (1433).`);
|
||||||
@ -738,7 +738,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const scriptServer = GetServer(ctx.workerScript.hostname);
|
const scriptServer = GetServer(ctx.workerScript.hostname);
|
||||||
if (scriptServer == null) {
|
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);
|
return runScriptFromScript("spawn", scriptServer, path, args, ctx.workerScript, runOpts);
|
||||||
@ -762,7 +762,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
} else {
|
} else {
|
||||||
// Kill by filename/hostname
|
// Kill by filename/hostname
|
||||||
if (scriptID === undefined) {
|
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);
|
const byPid = helpers.getRunningScriptsByArgs(ctx, ident.scriptname, ident.hostname, ident.args);
|
||||||
@ -837,7 +837,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!path.endsWith(".lit")) {
|
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);
|
lits.push(path);
|
||||||
}
|
}
|
||||||
@ -954,7 +954,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
(ctx) =>
|
(ctx) =>
|
||||||
(_n = Player.bitNodeN, _lvl = Player.sourceFileLvl(Player.bitNodeN) + 1) => {
|
(_n = Player.bitNodeN, _lvl = Player.sourceFileLvl(Player.bitNodeN) + 1) => {
|
||||||
if (Player.sourceFileLvl(5) <= 0 && Player.bitNodeN !== 5)
|
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 n = Math.round(helpers.number(ctx, "n", _n));
|
||||||
const lvl = Math.round(helpers.number(ctx, "lvl", _lvl));
|
const lvl = Math.round(helpers.number(ctx, "lvl", _lvl));
|
||||||
if (n < 1 || n > 14) throw new Error("n must be between 1 and 14");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (!hasTextExtension(filepath)) {
|
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") {
|
if (mode === "w") {
|
||||||
server.writeToTextFile(filepath, data);
|
server.writeToTextFile(filepath, data);
|
||||||
@ -1385,11 +1385,11 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
clear: (ctx) => (_file) => {
|
clear: (ctx) => (_file) => {
|
||||||
const path = helpers.filePath(ctx, "file", _file);
|
const path = helpers.filePath(ctx, "file", _file);
|
||||||
if (!hasScriptExtension(path) && !hasTextExtension(path)) {
|
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 server = ctx.workerScript.getServer();
|
||||||
const file = server.getContentFile(path);
|
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.
|
// The content setter handles invalidating script modules where applicable.
|
||||||
file.content = "";
|
file.content = "";
|
||||||
},
|
},
|
||||||
@ -1603,23 +1603,23 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
_options ??= options;
|
_options ??= options;
|
||||||
const txt = helpers.string(ctx, "txt", _txt);
|
const txt = helpers.string(ctx, "txt", _txt);
|
||||||
assert(_options, objectAssert, (type) =>
|
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) {
|
if (_options.type !== undefined) {
|
||||||
assert(_options.type, stringAssert, (type) =>
|
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;
|
options.type = _options.type;
|
||||||
const validTypes = ["boolean", "text", "select"];
|
const validTypes = ["boolean", "text", "select"];
|
||||||
if (!["boolean", "text", "select"].includes(options.type)) {
|
if (!["boolean", "text", "select"].includes(options.type)) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Invalid value for options.type: ${options.type}. Must be one of ${validTypes.join(", ")}.`,
|
`Invalid value for options.type: ${options.type}. Must be one of ${validTypes.join(", ")}.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (options.type === "select") {
|
if (options.type === "select") {
|
||||||
assert(_options.choices, arrayAssert, (type) =>
|
assert(_options.choices, arrayAssert, (type) =>
|
||||||
helpers.makeRuntimeErrorMsg(
|
helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Invalid type for options.choices: ${type}. If options.type is "select", options.choices must be an array.`,
|
`Invalid type for options.choices: ${type}. If options.type is "select", options.choices must be an array.`,
|
||||||
"TYPE",
|
"TYPE",
|
||||||
@ -1709,7 +1709,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
}),
|
}),
|
||||||
atExit: (ctx) => (f) => {
|
atExit: (ctx) => (f) => {
|
||||||
if (typeof f !== "function") {
|
if (typeof f !== "function") {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, "argument should be function");
|
throw helpers.errorMessage(ctx, "argument should be function");
|
||||||
}
|
}
|
||||||
ctx.workerScript.atExit = () => {
|
ctx.workerScript.atExit = () => {
|
||||||
f();
|
f();
|
||||||
@ -1725,7 +1725,7 @@ export const ns: InternalAPI<NSFull> = {
|
|||||||
(!hasTextExtension(sourcePath) && !hasScriptExtension(sourcePath)) ||
|
(!hasTextExtension(sourcePath) && !hasScriptExtension(sourcePath)) ||
|
||||||
(!hasTextExtension(destinationPath) && !hasScriptExtension(destinationPath))
|
(!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) {
|
if (sourcePath === destinationPath) {
|
||||||
helpers.log(ctx, () => "WARNING: Did nothing, source and destination paths were the same.");
|
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);
|
const sourceContentFile = server.getContentFile(sourcePath);
|
||||||
if (!sourceContentFile) {
|
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);
|
const success = sourceContentFile.deleteFromServer(server);
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -17,11 +17,11 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
const getBladeburner = function (ctx: NetscriptContext): Bladeburner {
|
const getBladeburner = function (ctx: NetscriptContext): Bladeburner {
|
||||||
const apiAccess = Player.bitNodeN === 7 || Player.sourceFileLvl(7) > 0;
|
const apiAccess = Player.bitNodeN === 7 || Player.sourceFileLvl(7) > 0;
|
||||||
if (!apiAccess) {
|
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;
|
const bladeburner = Player.bladeburner;
|
||||||
if (!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;
|
return bladeburner;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -30,11 +30,11 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
if (bladeburner === null) throw new Error("Must have joined bladeburner");
|
if (bladeburner === null) throw new Error("Must have joined bladeburner");
|
||||||
const actionId = bladeburner.getActionIdFromTypeAndName(type, name);
|
const actionId = bladeburner.getActionIdFromTypeAndName(type, name);
|
||||||
if (!actionId) {
|
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);
|
const actionObj = bladeburner.getActionObject(actionId);
|
||||||
if (!actionObj) {
|
if (!actionObj) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid action type='${type}', name='${name}'`);
|
throw helpers.errorMessage(ctx, `Invalid action type='${type}', name='${name}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return actionObj;
|
return actionObj;
|
||||||
@ -80,7 +80,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
try {
|
try {
|
||||||
return bladeburner.startActionNetscriptFn(type, name, ctx.workerScript);
|
return bladeburner.startActionNetscriptFn(type, name, ctx.workerScript);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
throw helpers.errorMessage(ctx, String(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
stopBladeburnerAction: (ctx) => () => {
|
stopBladeburnerAction: (ctx) => () => {
|
||||||
@ -105,7 +105,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
return time;
|
return time;
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
throw helpers.errorMessage(ctx, String(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getActionCurrentTime: (ctx) => () => {
|
getActionCurrentTime: (ctx) => () => {
|
||||||
@ -116,7 +116,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
1000;
|
1000;
|
||||||
return timecomputed;
|
return timecomputed;
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
throw helpers.errorMessage(ctx, String(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getActionEstimatedSuccessChance: (ctx) => (_type, _name) => {
|
getActionEstimatedSuccessChance: (ctx) => (_type, _name) => {
|
||||||
@ -133,7 +133,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
return chance;
|
return chance;
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
throw helpers.errorMessage(ctx, String(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getActionRepGain: (ctx) => (_type, _name, _level) => {
|
getActionRepGain: (ctx) => (_type, _name, _level) => {
|
||||||
@ -152,7 +152,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
try {
|
try {
|
||||||
return bladeburner.getActionCountRemainingNetscriptFn(type, name, ctx.workerScript);
|
return bladeburner.getActionCountRemainingNetscriptFn(type, name, ctx.workerScript);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
throw helpers.errorMessage(ctx, String(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getActionMaxLevel: (ctx) => (_type, _name) => {
|
getActionMaxLevel: (ctx) => (_type, _name) => {
|
||||||
@ -202,7 +202,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
checkBladeburnerAccess(ctx);
|
checkBladeburnerAccess(ctx);
|
||||||
const action = getBladeburnerActionObject(ctx, type, name);
|
const action = getBladeburnerActionObject(ctx, type, name);
|
||||||
if (level < 1 || level > action.maxLevel) {
|
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;
|
action.level = level;
|
||||||
},
|
},
|
||||||
@ -220,7 +220,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
try {
|
try {
|
||||||
return bladeburner.getSkillLevelNetscriptFn(skillName, ctx.workerScript);
|
return bladeburner.getSkillLevelNetscriptFn(skillName, ctx.workerScript);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
throw helpers.errorMessage(ctx, String(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getSkillUpgradeCost:
|
getSkillUpgradeCost:
|
||||||
@ -232,7 +232,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
try {
|
try {
|
||||||
return bladeburner.getSkillUpgradeCostNetscriptFn(skillName, count, ctx.workerScript);
|
return bladeburner.getSkillUpgradeCostNetscriptFn(skillName, count, ctx.workerScript);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
throw helpers.errorMessage(ctx, String(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
upgradeSkill:
|
upgradeSkill:
|
||||||
@ -244,7 +244,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
try {
|
try {
|
||||||
return bladeburner.upgradeSkillNetscriptFn(skillName, count, ctx.workerScript);
|
return bladeburner.upgradeSkillNetscriptFn(skillName, count, ctx.workerScript);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
throw helpers.errorMessage(ctx, String(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getTeamSize: (ctx) => (_type, _name) => {
|
getTeamSize: (ctx) => (_type, _name) => {
|
||||||
@ -254,7 +254,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
try {
|
try {
|
||||||
return bladeburner.getTeamSizeNetscriptFn(type, name, ctx.workerScript);
|
return bladeburner.getTeamSizeNetscriptFn(type, name, ctx.workerScript);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
throw helpers.errorMessage(ctx, String(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setTeamSize: (ctx) => (_type, _name, _size) => {
|
setTeamSize: (ctx) => (_type, _name, _size) => {
|
||||||
@ -265,7 +265,7 @@ export function NetscriptBladeburner(): InternalAPI<INetscriptBladeburner> {
|
|||||||
try {
|
try {
|
||||||
return bladeburner.setTeamSizeNetscriptFn(type, name, size, ctx.workerScript);
|
return bladeburner.setTeamSizeNetscriptFn(type, name, size, ctx.workerScript);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, String(e));
|
throw helpers.errorMessage(ctx, String(e));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getCityEstimatedPopulation: (ctx) => (_cityName) => {
|
getCityEstimatedPopulation: (ctx) => (_cityName) => {
|
||||||
|
@ -11,7 +11,7 @@ export function NetscriptCodingContract(): InternalAPI<ICodingContract> {
|
|||||||
const server = helpers.getServer(ctx, hostname);
|
const server = helpers.getServer(ctx, hostname);
|
||||||
const contract = server.getContract(filename);
|
const contract = server.getContract(filename);
|
||||||
if (contract == null) {
|
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;
|
return contract;
|
||||||
|
@ -178,10 +178,10 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkAccess(ctx: NetscriptContext, api?: CorpUnlockName): void {
|
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 (!api) return;
|
||||||
if (!player.corporation.unlocks.has(api)) {
|
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 cityName = getEnumHelper("CityName").nsGetMember(ctx, _cityName);
|
||||||
const amt = helpers.number(ctx, "amount", _amt);
|
const amt = helpers.number(ctx, "amount", _amt);
|
||||||
if (amt < 1) {
|
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);
|
const warehouse = getWarehouse(divisionName, cityName);
|
||||||
return UpgradeWarehouseCost(warehouse, amt);
|
return UpgradeWarehouseCost(warehouse, amt);
|
||||||
@ -306,7 +306,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
|||||||
const amt = helpers.number(ctx, "amount", _amt);
|
const amt = helpers.number(ctx, "amount", _amt);
|
||||||
const corporation = getCorporation();
|
const corporation = getCorporation();
|
||||||
if (amt < 1) {
|
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);
|
UpgradeWarehouse(corporation, getDivision(divisionName), getWarehouse(divisionName, cityName), amt);
|
||||||
},
|
},
|
||||||
@ -346,7 +346,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
|||||||
const enabled = !!_enabled;
|
const enabled = !!_enabled;
|
||||||
const warehouse = getWarehouse(divisionName, cityName);
|
const warehouse = getWarehouse(divisionName, cityName);
|
||||||
if (!hasUnlock(CorpUnlockName.SmartSupply))
|
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);
|
SetSmartSupply(warehouse, enabled);
|
||||||
},
|
},
|
||||||
setSmartSupplyOption: (ctx) => (_divisionName, _cityName, _materialName, _option) => {
|
setSmartSupplyOption: (ctx) => (_divisionName, _cityName, _materialName, _option) => {
|
||||||
@ -358,14 +358,14 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
|||||||
const material = getMaterial(divisionName, cityName, materialName);
|
const material = getMaterial(divisionName, cityName, materialName);
|
||||||
const option = getEnumHelper("SmartSupplyOption").nsGetMember(ctx, _option);
|
const option = getEnumHelper("SmartSupplyOption").nsGetMember(ctx, _option);
|
||||||
if (!hasUnlock(CorpUnlockName.SmartSupply))
|
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);
|
SetSmartSupplyOption(warehouse, material, option);
|
||||||
},
|
},
|
||||||
buyMaterial: (ctx) => (_divisionName, _cityName, _materialName, _amt) => {
|
buyMaterial: (ctx) => (_divisionName, _cityName, _materialName, _amt) => {
|
||||||
checkAccess(ctx, CorpUnlockName.WarehouseAPI);
|
checkAccess(ctx, CorpUnlockName.WarehouseAPI);
|
||||||
const divisionName = helpers.string(ctx, "divisionName", _divisionName);
|
const divisionName = helpers.string(ctx, "divisionName", _divisionName);
|
||||||
const division = getCorporation().divisions.get(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 cityName = getEnumHelper("CityName").nsGetMember(ctx, _cityName);
|
||||||
const materialName = getEnumHelper("CorpMaterialName").nsGetMember(ctx, _materialName, "materialName");
|
const materialName = getEnumHelper("CorpMaterialName").nsGetMember(ctx, _materialName, "materialName");
|
||||||
const amt = helpers.number(ctx, "amt", _amt);
|
const amt = helpers.number(ctx, "amt", _amt);
|
||||||
@ -378,7 +378,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
|||||||
checkAccess(ctx, CorpUnlockName.WarehouseAPI);
|
checkAccess(ctx, CorpUnlockName.WarehouseAPI);
|
||||||
const divisionName = helpers.string(ctx, "divisionName", _divisionName);
|
const divisionName = helpers.string(ctx, "divisionName", _divisionName);
|
||||||
const division = getCorporation().divisions.get(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 corporation = getCorporation();
|
||||||
const cityName = getEnumHelper("CityName").nsGetMember(ctx, _cityName);
|
const cityName = getEnumHelper("CityName").nsGetMember(ctx, _cityName);
|
||||||
const materialName = getEnumHelper("CorpMaterialName").nsGetMember(ctx, _materialName, "materialName");
|
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 materialName = getEnumHelper("CorpMaterialName").nsGetMember(ctx, _materialName, "materialName");
|
||||||
const on = !!_on;
|
const on = !!_on;
|
||||||
if (!getDivision(divisionName).hasResearch("Market-TA.I"))
|
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);
|
SetMaterialMarketTA1(getMaterial(divisionName, cityName, materialName), on);
|
||||||
},
|
},
|
||||||
setMaterialMarketTA2: (ctx) => (_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 materialName = getEnumHelper("CorpMaterialName").nsGetMember(ctx, _materialName, "materialName");
|
||||||
const on = !!_on;
|
const on = !!_on;
|
||||||
if (!getDivision(divisionName).hasResearch("Market-TA.II"))
|
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);
|
SetMaterialMarketTA2(getMaterial(divisionName, cityName, materialName), on);
|
||||||
},
|
},
|
||||||
setProductMarketTA1: (ctx) => (_divisionName, _productName, _on) => {
|
setProductMarketTA1: (ctx) => (_divisionName, _productName, _on) => {
|
||||||
@ -465,7 +465,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
|||||||
const productName = helpers.string(ctx, "productName", _productName);
|
const productName = helpers.string(ctx, "productName", _productName);
|
||||||
const on = !!_on;
|
const on = !!_on;
|
||||||
if (!getDivision(divisionName).hasResearch("Market-TA.I"))
|
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);
|
SetProductMarketTA1(getProduct(divisionName, productName), on);
|
||||||
},
|
},
|
||||||
setProductMarketTA2: (ctx) => (_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 productName = helpers.string(ctx, "productName", _productName);
|
||||||
const on = !!_on;
|
const on = !!_on;
|
||||||
if (!getDivision(divisionName).hasResearch("Market-TA.II"))
|
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);
|
SetProductMarketTA2(getProduct(divisionName, productName), on);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -528,7 +528,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
|||||||
|
|
||||||
if (job === CorpEmployeeJob.Unassigned) return false;
|
if (job === CorpEmployeeJob.Unassigned) return false;
|
||||||
if (amount < 0 || !Number.isInteger(amount))
|
if (amount < 0 || !Number.isInteger(amount))
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Invalid value for amount! Must be an integer and greater than or be 0". Amount:'${amount}'`,
|
`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];
|
const totalNewEmployees = amount - office.employeeNextJobs[job];
|
||||||
|
|
||||||
if (office.employeeNextJobs[CorpEmployeeJob.Unassigned] < totalNewEmployees)
|
if (office.employeeNextJobs[CorpEmployeeJob.Unassigned] < totalNewEmployees)
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Unable to bring '${job} employees to ${amount}. Requires ${totalNewEmployees} unassigned employees`,
|
`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)
|
if (rate < 0 || rate > max)
|
||||||
throw new Error(`Invalid value for rate field! Must be numeric, greater than 0, and less than ${max}`);
|
throw new Error(`Invalid value for rate field! Must be numeric, greater than 0, and less than ${max}`);
|
||||||
const corporation = getCorporation();
|
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);
|
IssueDividends(corporation, rate);
|
||||||
},
|
},
|
||||||
issueNewShares: (ctx) => (_amount) => {
|
issueNewShares: (ctx) => (_amount) => {
|
||||||
@ -773,7 +773,7 @@ export function NetscriptCorporation(): InternalAPI<NSCorporation> {
|
|||||||
goPublic: (ctx) => (_numShares) => {
|
goPublic: (ctx) => (_numShares) => {
|
||||||
checkAccess(ctx);
|
checkAccess(ctx);
|
||||||
const corporation = getCorporation();
|
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);
|
const numShares = helpers.number(ctx, "numShares", _numShares);
|
||||||
GoPublic(corporation, numShares);
|
GoPublic(corporation, numShares);
|
||||||
return true;
|
return true;
|
||||||
|
@ -63,7 +63,7 @@ import { findCrime } from "../Crime/CrimeHelpers";
|
|||||||
export function NetscriptFormulas(): InternalAPI<IFormulas> {
|
export function NetscriptFormulas(): InternalAPI<IFormulas> {
|
||||||
const checkFormulasAccess = function (ctx: NetscriptContext): void {
|
const checkFormulasAccess = function (ctx: NetscriptContext): void {
|
||||||
if (!player.hasProgram(CompletedProgramName.formulas)) {
|
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> = {
|
const formulasFunctions: InternalAPI<IFormulas> = {
|
||||||
@ -316,7 +316,7 @@ export function NetscriptFormulas(): InternalAPI<IFormulas> {
|
|||||||
checkFormulasAccess(ctx);
|
checkFormulasAccess(ctx);
|
||||||
const upg = player.hashManager.getUpgrade(upgName);
|
const upg = player.hashManager.getUpgrade(upgName);
|
||||||
if (!upg) {
|
if (!upg) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid Hash Upgrade: ${upgName}`);
|
throw helpers.errorMessage(ctx, `Invalid Hash Upgrade: ${upgName}`);
|
||||||
}
|
}
|
||||||
return upg.getCost(level);
|
return upg.getCost(level);
|
||||||
},
|
},
|
||||||
|
@ -17,20 +17,20 @@ import { getEnumHelper } from "../utils/EnumHelper";
|
|||||||
export function NetscriptGang(): InternalAPI<IGang> {
|
export function NetscriptGang(): InternalAPI<IGang> {
|
||||||
/** Functions as an API check and also returns the gang object */
|
/** Functions as an API check and also returns the gang object */
|
||||||
const getGang = function (ctx: NetscriptContext): Gang {
|
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;
|
return Player.gang;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGangMember = function (ctx: NetscriptContext, name: string): GangMember {
|
const getGangMember = function (ctx: NetscriptContext, name: string): GangMember {
|
||||||
const gang = getGang(ctx);
|
const gang = getGang(ctx);
|
||||||
for (const member of gang.members) if (member.name === name) return member;
|
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 getGangTask = function (ctx: NetscriptContext, name: string): GangMemberTask {
|
||||||
const task = GangMemberTasks[name];
|
const task = GangMemberTasks[name];
|
||||||
if (!task) {
|
if (!task) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid task: '${name}'`);
|
throw helpers.errorMessage(ctx, `Invalid task: '${name}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return task;
|
return task;
|
||||||
@ -60,13 +60,13 @@ export function NetscriptGang(): InternalAPI<IGang> {
|
|||||||
const newName = helpers.string(ctx, "newName", _newName);
|
const newName = helpers.string(ctx, "newName", _newName);
|
||||||
const member = gang.members.find((m) => m.name === memberName);
|
const member = gang.members.find((m) => m.name === memberName);
|
||||||
if (!memberName) {
|
if (!memberName) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid memberName: "" (empty string)`);
|
throw helpers.errorMessage(ctx, `Invalid memberName: "" (empty string)`);
|
||||||
}
|
}
|
||||||
if (!newName) {
|
if (!newName) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid newName: "" (empty string)`);
|
throw helpers.errorMessage(ctx, `Invalid newName: "" (empty string)`);
|
||||||
}
|
}
|
||||||
if (newName === memberName) {
|
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) {
|
if (!member) {
|
||||||
helpers.log(ctx, () => `Failed to rename member: No member exists with memberName: ${memberName}`);
|
helpers.log(ctx, () => `Failed to rename member: No member exists with memberName: ${memberName}`);
|
||||||
@ -253,7 +253,7 @@ export function NetscriptGang(): InternalAPI<IGang> {
|
|||||||
getGang(ctx);
|
getGang(ctx);
|
||||||
const equipment = GangMemberUpgrades[equipName];
|
const equipment = GangMemberUpgrades[equipName];
|
||||||
if (!equipment) {
|
if (!equipment) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid equipment: ${equipName}`);
|
throw helpers.errorMessage(ctx, `Invalid equipment: ${equipName}`);
|
||||||
}
|
}
|
||||||
const typecheck: EquipmentStats = equipment.mults;
|
const typecheck: EquipmentStats = equipment.mults;
|
||||||
return Object.assign({}, typecheck);
|
return Object.assign({}, typecheck);
|
||||||
@ -328,7 +328,7 @@ export function NetscriptGang(): InternalAPI<IGang> {
|
|||||||
const otherGang = helpers.string(ctx, "otherGang", _otherGang);
|
const otherGang = helpers.string(ctx, "otherGang", _otherGang);
|
||||||
const gang = getGang(ctx);
|
const gang = getGang(ctx);
|
||||||
if (AllGangs[otherGang] == null) {
|
if (AllGangs[otherGang] == null) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid gang: ${otherGang}`);
|
throw helpers.errorMessage(ctx, `Invalid gang: ${otherGang}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerPower = AllGangs[gang.facName].power;
|
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 { helpers } from "../Netscript/NetscriptHelpers";
|
||||||
import { Player } from "@player";
|
import { simpleBoardFromBoard } from "../Go/boardAnalysis/boardAnalysis";
|
||||||
import { Go } from "@nsdefs";
|
|
||||||
import { Play, playerColors } from "../Go/boardState/goConstants";
|
|
||||||
import { getSimplifiedBoardState } from "../Go/boardAnalysis/boardAnalysis";
|
|
||||||
import {
|
import {
|
||||||
cheatDestroyNode,
|
cheatDestroyNode,
|
||||||
cheatPlayTwoMoves,
|
cheatPlayTwoMoves,
|
||||||
@ -21,6 +23,7 @@ import {
|
|||||||
resetBoardState,
|
resetBoardState,
|
||||||
throwError,
|
throwError,
|
||||||
} from "../Go/effects/netscriptGoImplementation";
|
} from "../Go/effects/netscriptGoImplementation";
|
||||||
|
import { getEnumHelper } from "../utils/EnumHelper";
|
||||||
|
|
||||||
const logger = (ctx: NetscriptContext) => (message: string) => helpers.log(ctx, () => message);
|
const logger = (ctx: NetscriptContext) => (message: string) => helpers.log(ctx, () => message);
|
||||||
const error = (ctx: NetscriptContext) => (message: string) => throwError(ctx.workerScript, 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
|
* Ensures the given coordinates are valid for the current board size
|
||||||
*/
|
*/
|
||||||
function validateRowAndColumn(ctx: NetscriptContext, x: number, y: number) {
|
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) {
|
if (x < 0 || x >= boardSize) {
|
||||||
throwError(
|
throwError(
|
||||||
@ -45,7 +48,7 @@ function validateRowAndColumn(ctx: NetscriptContext, x: number, y: number) {
|
|||||||
/**
|
/**
|
||||||
* Go API implementation
|
* Go API implementation
|
||||||
*/
|
*/
|
||||||
export function NetscriptGo(): InternalAPI<Go> {
|
export function NetscriptGo(): InternalAPI<NSGo> {
|
||||||
return {
|
return {
|
||||||
makeMove:
|
makeMove:
|
||||||
(ctx: NetscriptContext) =>
|
(ctx: NetscriptContext) =>
|
||||||
@ -57,7 +60,7 @@ export function NetscriptGo(): InternalAPI<Go> {
|
|||||||
return makePlayerMove(logger(ctx), x, y);
|
return makePlayerMove(logger(ctx), x, y);
|
||||||
},
|
},
|
||||||
passTurn: (ctx: NetscriptContext) => async (): Promise<Play> => {
|
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, () => `It is not your turn; you cannot pass.`);
|
||||||
helpers.log(ctx, () => `Do you have multiple scripts running, or did you forget to await makeMove() ?`);
|
helpers.log(ctx, () => `Do you have multiple scripts running, or did you forget to await makeMove() ?`);
|
||||||
return Promise.resolve(invalidMoveResponse);
|
return Promise.resolve(invalidMoveResponse);
|
||||||
@ -65,16 +68,16 @@ export function NetscriptGo(): InternalAPI<Go> {
|
|||||||
return handlePassTurn(logger(ctx));
|
return handlePassTurn(logger(ctx));
|
||||||
},
|
},
|
||||||
getBoardState: () => () => {
|
getBoardState: () => () => {
|
||||||
return getSimplifiedBoardState(Player.go.boardState.board);
|
return simpleBoardFromBoard(Go.currentGame.board);
|
||||||
},
|
},
|
||||||
getOpponent: () => () => {
|
getOpponent: () => () => {
|
||||||
return Player.go.boardState.ai;
|
return Go.currentGame.ai;
|
||||||
},
|
},
|
||||||
resetBoardState: (ctx) => (_opponent, _boardSize) => {
|
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);
|
const boardSize = helpers.number(ctx, "boardSize", _boardSize);
|
||||||
|
|
||||||
return resetBoardState(error(ctx), opponentString, boardSize);
|
return resetBoardState(error(ctx), opponent, boardSize);
|
||||||
},
|
},
|
||||||
analysis: {
|
analysis: {
|
||||||
getValidMoves: () => () => {
|
getValidMoves: () => () => {
|
||||||
@ -93,7 +96,7 @@ export function NetscriptGo(): InternalAPI<Go> {
|
|||||||
cheat: {
|
cheat: {
|
||||||
getCheatSuccessChance: (ctx: NetscriptContext) => () => {
|
getCheatSuccessChance: (ctx: NetscriptContext) => () => {
|
||||||
checkCheatApiAccess(error(ctx));
|
checkCheatApiAccess(error(ctx));
|
||||||
return cheatSuccessChance(Player.go.boardState.cheatCount);
|
return cheatSuccessChance(Go.currentGame.cheatCount);
|
||||||
},
|
},
|
||||||
removeRouter:
|
removeRouter:
|
||||||
(ctx: NetscriptContext) =>
|
(ctx: NetscriptContext) =>
|
||||||
|
@ -16,7 +16,7 @@ import { getEnumHelper } from "../utils/EnumHelper";
|
|||||||
export function NetscriptGrafting(): InternalAPI<IGrafting> {
|
export function NetscriptGrafting(): InternalAPI<IGrafting> {
|
||||||
const checkGraftingAPIAccess = (ctx: NetscriptContext): void => {
|
const checkGraftingAPIAccess = (ctx: NetscriptContext): void => {
|
||||||
if (!Player.canAccessGrafting()) {
|
if (!Player.canAccessGrafting()) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
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",
|
"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);
|
const augName = getEnumHelper("AugmentationName").nsGetMember(ctx, _augName);
|
||||||
checkGraftingAPIAccess(ctx);
|
checkGraftingAPIAccess(ctx);
|
||||||
if (!isValidGraftingAugName(augName)) {
|
if (!isValidGraftingAugName(augName)) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid aug: ${augName}`);
|
throw helpers.errorMessage(ctx, `Invalid aug: ${augName}`);
|
||||||
}
|
}
|
||||||
const graftableAug = new GraftableAugmentation(Augmentations[augName]);
|
const graftableAug = new GraftableAugmentation(Augmentations[augName]);
|
||||||
return graftableAug.cost;
|
return graftableAug.cost;
|
||||||
@ -40,7 +40,7 @@ export function NetscriptGrafting(): InternalAPI<IGrafting> {
|
|||||||
const augName = getEnumHelper("AugmentationName").nsGetMember(ctx, _augName);
|
const augName = getEnumHelper("AugmentationName").nsGetMember(ctx, _augName);
|
||||||
checkGraftingAPIAccess(ctx);
|
checkGraftingAPIAccess(ctx);
|
||||||
if (!isValidGraftingAugName(augName)) {
|
if (!isValidGraftingAugName(augName)) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid aug: ${augName}`);
|
throw helpers.errorMessage(ctx, `Invalid aug: ${augName}`);
|
||||||
}
|
}
|
||||||
const graftableAug = new GraftableAugmentation(Augmentations[augName]);
|
const graftableAug = new GraftableAugmentation(Augmentations[augName]);
|
||||||
return calculateGraftingTimeWithBonus(graftableAug);
|
return calculateGraftingTimeWithBonus(graftableAug);
|
||||||
@ -59,7 +59,7 @@ export function NetscriptGrafting(): InternalAPI<IGrafting> {
|
|||||||
const focus = !!_focus;
|
const focus = !!_focus;
|
||||||
checkGraftingAPIAccess(ctx);
|
checkGraftingAPIAccess(ctx);
|
||||||
if (Player.city !== CityName.NewTokyo) {
|
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)) {
|
if (!isValidGraftingAugName(augName)) {
|
||||||
helpers.log(ctx, () => `Invalid aug: ${augName}`);
|
helpers.log(ctx, () => `Invalid aug: ${augName}`);
|
||||||
|
@ -26,7 +26,7 @@ export function NetscriptHacknet(): InternalAPI<IHacknet> {
|
|||||||
// Utility function to get Hacknet Node object
|
// Utility function to get Hacknet Node object
|
||||||
const getHacknetNode = function (ctx: NetscriptContext, i: number): HacknetNode | HacknetServer {
|
const getHacknetNode = function (ctx: NetscriptContext, i: number): HacknetNode | HacknetServer {
|
||||||
if (i < 0 || i >= player.hacknetNodes.length) {
|
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()) {
|
if (hasHacknetServers()) {
|
||||||
@ -35,7 +35,7 @@ export function NetscriptHacknet(): InternalAPI<IHacknet> {
|
|||||||
const hserver = GetServer(hi);
|
const hserver = GetServer(hi);
|
||||||
if (!(hserver instanceof HacknetServer)) throw new Error("hacknet server was not actually hacknet server");
|
if (!(hserver instanceof HacknetServer)) throw new Error("hacknet server was not actually hacknet server");
|
||||||
if (hserver == null) {
|
if (hserver == null) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Could not get Hacknet Server for index ${i}. This is probably a bug, please report to game dev`,
|
`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 upgName = helpers.string(ctx, "upgName", _upgName);
|
||||||
const level = player.hashManager.upgrades[upgName];
|
const level = player.hashManager.upgrades[upgName];
|
||||||
if (level === undefined) {
|
if (level === undefined) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid Hash Upgrade: ${upgName}`);
|
throw helpers.errorMessage(ctx, `Invalid Hash Upgrade: ${upgName}`);
|
||||||
}
|
}
|
||||||
return level;
|
return level;
|
||||||
},
|
},
|
||||||
|
@ -21,9 +21,9 @@ export function NetscriptInfiltration(): InternalAPI<NetscriptInfiltation> {
|
|||||||
|
|
||||||
const calculateInfiltrationData = (ctx: NetscriptContext, locationName: LocationName): InfiltrationLocation => {
|
const calculateInfiltrationData = (ctx: NetscriptContext, locationName: LocationName): InfiltrationLocation => {
|
||||||
const location = Locations[locationName];
|
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)
|
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 startingSecurityLevel = location.infiltrationData.startingSecurityLevel;
|
||||||
const difficulty = calculateDifficulty(startingSecurityLevel);
|
const difficulty = calculateDifficulty(startingSecurityLevel);
|
||||||
const reward = calculateReward(startingSecurityLevel);
|
const reward = calculateReward(startingSecurityLevel);
|
||||||
|
@ -190,7 +190,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
const cbScript = _cbScript
|
const cbScript = _cbScript
|
||||||
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
||||||
: false;
|
: 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");
|
helpers.log(ctx, () => "Soft resetting. This will cause this script to be killed");
|
||||||
installAugmentations(true);
|
installAugmentations(true);
|
||||||
@ -201,7 +201,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
const cbScript = _cbScript
|
const cbScript = _cbScript
|
||||||
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
||||||
: false;
|
: 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) {
|
if (Player.queuedAugmentations.length === 0) {
|
||||||
helpers.log(ctx, () => "You do not have any Augmentations to be installed.");
|
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);
|
Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain / 50000);
|
||||||
return true;
|
return true;
|
||||||
default:
|
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");
|
Player.loseMoney(CONSTANTS.TorRouterCost, "other");
|
||||||
|
|
||||||
const darkweb = GetServer(SpecialServers.DarkWeb);
|
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);
|
Player.getHomeComputer().serversOnNetwork.push(darkweb.hostname);
|
||||||
darkweb.serversOnNetwork.push(Player.getHomeComputer().hostname);
|
darkweb.serversOnNetwork.push(Player.getHomeComputer().hostname);
|
||||||
@ -483,12 +483,12 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
helpers.checkSingularityAccess(ctx);
|
helpers.checkSingularityAccess(ctx);
|
||||||
const hostname = helpers.string(ctx, "hostname", _hostname);
|
const hostname = helpers.string(ctx, "hostname", _hostname);
|
||||||
if (!hostname) {
|
if (!hostname) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid hostname: '${hostname}'`);
|
throw helpers.errorMessage(ctx, `Invalid hostname: '${hostname}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = GetServer(hostname);
|
const target = GetServer(hostname);
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid hostname: '${hostname}'`);
|
throw helpers.errorMessage(ctx, `Invalid hostname: '${hostname}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Home case
|
//Home case
|
||||||
@ -545,7 +545,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
// No root access or skill level too low
|
// No root access or skill level too low
|
||||||
const canHack = netscriptCanHack(server);
|
const canHack = netscriptCanHack(server);
|
||||||
if (!canHack.res) {
|
if (!canHack.res) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, canHack.msg || "");
|
throw helpers.errorMessage(ctx, canHack.msg || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
helpers.log(
|
helpers.log(
|
||||||
@ -573,7 +573,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
helpers.checkSingularityAccess(ctx);
|
helpers.checkSingularityAccess(ctx);
|
||||||
const focus = !!_focus;
|
const focus = !!_focus;
|
||||||
if (Player.currentWork === null) {
|
if (Player.currentWork === null) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, "Not currently working");
|
throw helpers.errorMessage(ctx, "Not currently working");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Player.focus && focus) {
|
if (!Player.focus && focus) {
|
||||||
@ -682,7 +682,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
const company = Companies[companyName];
|
const company = Companies[companyName];
|
||||||
|
|
||||||
if (!company.hasPosition(positionName)) {
|
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];
|
const job = CompanyPositions[positionName];
|
||||||
@ -706,7 +706,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
const jobName = Player.jobs[companyName];
|
const jobName = Player.jobs[companyName];
|
||||||
// Make sure player is actually employed at the company
|
// Make sure player is actually employed at the company
|
||||||
if (!jobName) {
|
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;
|
const wasFocused = Player.focus;
|
||||||
@ -1005,7 +1005,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
|
|
||||||
// If input isn't a crimeType, use search using roughname.
|
// If input isn't a crimeType, use search using roughname.
|
||||||
const crime = findCrime(crimeType);
|
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}...`);
|
helpers.log(ctx, () => `Attempting to commit ${crime.type}...`);
|
||||||
const crimeTime = crime.commit(1, ctx.workerScript);
|
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.
|
// If input isn't a crimeType, use search using roughname.
|
||||||
const crime = findCrime(crimeType);
|
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);
|
return crime.successRate(Player);
|
||||||
},
|
},
|
||||||
@ -1034,7 +1034,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
|
|
||||||
// If input isn't a crimeType, use search using roughname.
|
// If input isn't a crimeType, use search using roughname.
|
||||||
const crime = findCrime(crimeType);
|
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);
|
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
|
// 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.
|
// that they need to fix it.
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`No such exploit ('${programName}') found on the darkweb! ` +
|
`No such exploit ('${programName}') found on the darkweb! ` +
|
||||||
`\nThis function is not case-sensitive. Did you perhaps forget .exe at the end?`,
|
`\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
|
const cbScript = _cbScript
|
||||||
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
||||||
: false;
|
: 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);
|
enterBitNode(true, Player.bitNodeN, nextBN);
|
||||||
if (cbScript) setTimeout(() => runAfterReset(cbScript), 500);
|
if (cbScript) setTimeout(() => runAfterReset(cbScript), 500);
|
||||||
},
|
},
|
||||||
@ -1111,7 +1111,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
|||||||
const cbScript = _cbScript
|
const cbScript = _cbScript
|
||||||
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
? resolveScriptFilePath(helpers.string(ctx, "cbScript", _cbScript), ctx.workerScript.name)
|
||||||
: false;
|
: 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);
|
const wd = GetServer(SpecialServers.WorldDaemon);
|
||||||
if (!(wd instanceof Server)) throw new Error("WorldDaemon was not a normal server. This is a bug contact dev.");
|
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> {
|
export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
|
||||||
const checkSleeveAPIAccess = function (ctx: NetscriptContext) {
|
const checkSleeveAPIAccess = function (ctx: NetscriptContext) {
|
||||||
if (Player.bitNodeN !== 10 && !Player.sourceFileLvl(10)) {
|
if (Player.bitNodeN !== 10 && !Player.sourceFileLvl(10)) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
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",
|
"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) {
|
if (sleeveNumber >= Player.sleeves.length || sleeveNumber < 0) {
|
||||||
const msg = `Invalid sleeve number: ${sleeveNumber}`;
|
const msg = `Invalid sleeve number: ${sleeveNumber}`;
|
||||||
helpers.log(ctx, () => msg);
|
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];
|
const other = Player.sleeves[i];
|
||||||
if (isSleeveCompanyWork(other.currentWork) && other.currentWork.companyName === companyName) {
|
if (isSleeveCompanyWork(other.currentWork) && other.currentWork.companyName === companyName) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Sleeve ${sleeveNumber} cannot work for company ${companyName} because Sleeve ${i} is already working for them.`,
|
`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);
|
checkSleeveNumber(ctx, sleeveNumber);
|
||||||
|
|
||||||
if (!Factions[factionName].isMember) {
|
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
|
// 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];
|
const other = Player.sleeves[i];
|
||||||
if (isSleeveFactionWork(other.currentWork) && other.currentWork.factionName === factionName) {
|
if (isSleeveFactionWork(other.currentWork) && other.currentWork.factionName === factionName) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Sleeve ${sleeveNumber} cannot work for faction ${factionName} because Sleeve ${i} is already working for them.`,
|
`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) {
|
if (Player.gang && Player.gang.facName == factionName) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Sleeve ${sleeveNumber} cannot work for faction ${factionName} because you have started a gang with them.`,
|
`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);
|
checkSleeveNumber(ctx, sleeveNumber);
|
||||||
|
|
||||||
if (Player.sleeves[sleeveNumber].shock > 0) {
|
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];
|
const aug = Augmentations[augName];
|
||||||
if (!aug) {
|
if (!aug) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid aug: ${augName}`);
|
throw helpers.errorMessage(ctx, `Invalid aug: ${augName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Player.sleeves[sleeveNumber].tryBuyAugmentation(aug);
|
return Player.sleeves[sleeveNumber].tryBuyAugmentation(aug);
|
||||||
@ -250,7 +250,7 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
|
|||||||
}
|
}
|
||||||
const other = Player.sleeves[i];
|
const other = Player.sleeves[i];
|
||||||
if (isSleeveBladeburnerWork(other.currentWork) && other.currentWork.actionName === contract) {
|
if (isSleeveBladeburnerWork(other.currentWork) && other.currentWork.actionName === contract) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Sleeve ${sleeveNumber} cannot take on contracts because Sleeve ${i} is already performing that action.`,
|
`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> {
|
export function NetscriptStanek(): InternalAPI<IStanek> {
|
||||||
function checkStanekAPIAccess(ctx: NetscriptContext): void {
|
function checkStanekAPIAccess(ctx: NetscriptContext): void {
|
||||||
if (!Player.hasAugmentation(AugmentationName.StaneksGift1, true)) {
|
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);
|
checkStanekAPIAccess(ctx);
|
||||||
const fragment = staneksGift.findFragment(rootX, rootY);
|
const fragment = staneksGift.findFragment(rootX, rootY);
|
||||||
//Check whether the selected fragment can ge charged
|
//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) {
|
if (fragment.fragment().type == FragmentType.Booster) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`The fragment with root (${rootX}, ${rootY}) is a Booster Fragment and thus cannot be charged.`,
|
`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);
|
const fragmentId = helpers.number(ctx, "fragmentId", _fragmentId);
|
||||||
checkStanekAPIAccess(ctx);
|
checkStanekAPIAccess(ctx);
|
||||||
const fragment = FragmentById(fragmentId);
|
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);
|
const can = staneksGift.canPlace(rootX, rootY, rotation, fragment);
|
||||||
return can;
|
return can;
|
||||||
},
|
},
|
||||||
@ -90,7 +90,7 @@ export function NetscriptStanek(): InternalAPI<IStanek> {
|
|||||||
const fragmentId = helpers.number(ctx, "fragmentId", _fragmentId);
|
const fragmentId = helpers.number(ctx, "fragmentId", _fragmentId);
|
||||||
checkStanekAPIAccess(ctx);
|
checkStanekAPIAccess(ctx);
|
||||||
const fragment = FragmentById(fragmentId);
|
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);
|
return staneksGift.place(rootX, rootY, rotation, fragment);
|
||||||
},
|
},
|
||||||
getFragment: (ctx) => (_rootX, _rootY) => {
|
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 */
|
/** Checks if the player has TIX API access. Throws an error if the player does not */
|
||||||
const checkTixApiAccess = function (ctx: NetscriptContext): void {
|
const checkTixApiAccess = function (ctx: NetscriptContext): void {
|
||||||
if (!Player.hasWseAccount) {
|
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) {
|
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 getStockFromSymbol = function (ctx: NetscriptContext, symbol: string): Stock {
|
||||||
const stock = SymbolToStockMap[symbol];
|
const stock = SymbolToStockMap[symbol];
|
||||||
if (stock == null) {
|
if (stock == null) {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid stock symbol: '${symbol}'`);
|
throw helpers.errorMessage(ctx, `Invalid stock symbol: '${symbol}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return stock;
|
return stock;
|
||||||
@ -85,7 +85,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
|||||||
checkTixApiAccess(ctx);
|
checkTixApiAccess(ctx);
|
||||||
const stock = SymbolToStockMap[symbol];
|
const stock = SymbolToStockMap[symbol];
|
||||||
if (stock == null) {
|
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];
|
return [stock.playerShares, stock.playerAvgPx, stock.playerShortShares, stock.playerAvgShortPx];
|
||||||
},
|
},
|
||||||
@ -169,10 +169,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
|||||||
checkTixApiAccess(ctx);
|
checkTixApiAccess(ctx);
|
||||||
if (Player.bitNodeN !== 8) {
|
if (Player.bitNodeN !== 8) {
|
||||||
if (Player.sourceFileLvl(8) <= 1) {
|
if (Player.sourceFileLvl(8) <= 1) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 2.");
|
||||||
ctx,
|
|
||||||
"You must either be in BitNode-8 or you must have Source-File 8 Level 2.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const stock = getStockFromSymbol(ctx, symbol);
|
const stock = getStockFromSymbol(ctx, symbol);
|
||||||
@ -186,10 +183,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
|||||||
checkTixApiAccess(ctx);
|
checkTixApiAccess(ctx);
|
||||||
if (Player.bitNodeN !== 8) {
|
if (Player.bitNodeN !== 8) {
|
||||||
if (Player.sourceFileLvl(8) <= 1) {
|
if (Player.sourceFileLvl(8) <= 1) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 2.");
|
||||||
ctx,
|
|
||||||
"You must either be in BitNode-8 or you must have Source-File 8 Level 2.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const stock = getStockFromSymbol(ctx, symbol);
|
const stock = getStockFromSymbol(ctx, symbol);
|
||||||
@ -206,10 +200,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
|||||||
checkTixApiAccess(ctx);
|
checkTixApiAccess(ctx);
|
||||||
if (Player.bitNodeN !== 8) {
|
if (Player.bitNodeN !== 8) {
|
||||||
if (Player.sourceFileLvl(8) <= 2) {
|
if (Player.sourceFileLvl(8) <= 2) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 3.");
|
||||||
ctx,
|
|
||||||
"You must either be in BitNode-8 or you must have Source-File 8 Level 3.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const stock = getStockFromSymbol(ctx, symbol);
|
const stock = getStockFromSymbol(ctx, symbol);
|
||||||
@ -226,7 +217,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
|||||||
} else if (ltype.includes("stop") && ltype.includes("sell")) {
|
} else if (ltype.includes("stop") && ltype.includes("sell")) {
|
||||||
orderType = OrderType.StopSell;
|
orderType = OrderType.StopSell;
|
||||||
} else {
|
} else {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid order type: ${type}`);
|
throw helpers.errorMessage(ctx, `Invalid order type: ${type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lpos = pos.toLowerCase();
|
const lpos = pos.toLowerCase();
|
||||||
@ -235,7 +226,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
|||||||
} else if (lpos.includes("s")) {
|
} else if (lpos.includes("s")) {
|
||||||
orderPos = PositionType.Short;
|
orderPos = PositionType.Short;
|
||||||
} else {
|
} 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);
|
return placeOrder(stock, shares, price, orderType, orderPos, ctx);
|
||||||
@ -249,18 +240,12 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
|||||||
checkTixApiAccess(ctx);
|
checkTixApiAccess(ctx);
|
||||||
if (Player.bitNodeN !== 8) {
|
if (Player.bitNodeN !== 8) {
|
||||||
if (Player.sourceFileLvl(8) <= 2) {
|
if (Player.sourceFileLvl(8) <= 2) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(ctx, "You must either be in BitNode-8 or you must have Source-File 8 Level 3.");
|
||||||
ctx,
|
|
||||||
"You must either be in BitNode-8 or you must have Source-File 8 Level 3.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const stock = getStockFromSymbol(ctx, symbol);
|
const stock = getStockFromSymbol(ctx, symbol);
|
||||||
if (isNaN(shares) || isNaN(price)) {
|
if (isNaN(shares) || isNaN(price)) {
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw helpers.errorMessage(ctx, `Invalid shares or price. Must be numeric. shares=${shares}, price=${price}`);
|
||||||
ctx,
|
|
||||||
`Invalid shares or price. Must be numeric. shares=${shares}, price=${price}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let orderType;
|
let orderType;
|
||||||
let orderPos;
|
let orderPos;
|
||||||
@ -274,7 +259,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
|||||||
} else if (ltype.includes("stop") && ltype.includes("sell")) {
|
} else if (ltype.includes("stop") && ltype.includes("sell")) {
|
||||||
orderType = OrderType.StopSell;
|
orderType = OrderType.StopSell;
|
||||||
} else {
|
} else {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid order type: ${type}`);
|
throw helpers.errorMessage(ctx, `Invalid order type: ${type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lpos = pos.toLowerCase();
|
const lpos = pos.toLowerCase();
|
||||||
@ -283,7 +268,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
|||||||
} else if (lpos.includes("s")) {
|
} else if (lpos.includes("s")) {
|
||||||
orderPos = PositionType.Short;
|
orderPos = PositionType.Short;
|
||||||
} else {
|
} else {
|
||||||
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid position type: ${pos}`);
|
throw helpers.errorMessage(ctx, `Invalid position type: ${pos}`);
|
||||||
}
|
}
|
||||||
const params = {
|
const params = {
|
||||||
stock: stock,
|
stock: stock,
|
||||||
@ -298,7 +283,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
|||||||
checkTixApiAccess(ctx);
|
checkTixApiAccess(ctx);
|
||||||
if (Player.bitNodeN !== 8) {
|
if (Player.bitNodeN !== 8) {
|
||||||
if (Player.sourceFileLvl(8) <= 2) {
|
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) => {
|
getVolatility: (ctx) => (_symbol) => {
|
||||||
const symbol = helpers.string(ctx, "symbol", _symbol);
|
const symbol = helpers.string(ctx, "symbol", _symbol);
|
||||||
if (!Player.has4SDataTixApi) {
|
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);
|
const stock = getStockFromSymbol(ctx, symbol);
|
||||||
|
|
||||||
@ -334,7 +319,7 @@ export function NetscriptStockMarket(): InternalAPI<TIX> {
|
|||||||
getForecast: (ctx) => (_symbol) => {
|
getForecast: (ctx) => (_symbol) => {
|
||||||
const symbol = helpers.string(ctx, "symbol", _symbol);
|
const symbol = helpers.string(ctx, "symbol", _symbol);
|
||||||
if (!Player.has4SDataTixApi) {
|
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);
|
const stock = getStockFromSymbol(ctx, symbol);
|
||||||
|
|
||||||
|
@ -7,7 +7,8 @@ import { CONSTANTS } from "../Constants";
|
|||||||
import { hash } from "../hash/hash";
|
import { hash } from "../hash/hash";
|
||||||
import { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
|
import { InternalAPI, NetscriptContext } from "../Netscript/APIWrapper";
|
||||||
import { Terminal } from "../../src/Terminal";
|
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.
|
/** 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. */
|
* This method cannot be used to handle optional properties. */
|
||||||
@ -18,7 +19,7 @@ export function assertObjectType<T extends object>(
|
|||||||
desiredObject: T,
|
desiredObject: T,
|
||||||
): asserts obj is T {
|
): asserts obj is T {
|
||||||
if (typeof obj !== "object" || obj === null) {
|
if (typeof obj !== "object" || obj === null) {
|
||||||
throw makeRuntimeErrorMsg(
|
throw errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Type ${obj === null ? "null" : typeof obj} provided for ${name}. Must be an object.`,
|
`Type ${obj === null ? "null" : typeof obj} provided for ${name}. Must be an object.`,
|
||||||
"TYPE",
|
"TYPE",
|
||||||
@ -26,15 +27,11 @@ export function assertObjectType<T extends object>(
|
|||||||
}
|
}
|
||||||
for (const [key, val] of Object.entries(desiredObject)) {
|
for (const [key, val] of Object.entries(desiredObject)) {
|
||||||
if (!Object.hasOwn(obj, key)) {
|
if (!Object.hasOwn(obj, key)) {
|
||||||
throw makeRuntimeErrorMsg(
|
throw errorMessage(ctx, `Object provided for argument ${name} is missing required property ${key}.`, "TYPE");
|
||||||
ctx,
|
|
||||||
`Object provided for argument ${name} is missing required property ${key}.`,
|
|
||||||
"TYPE",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const objVal = (obj as Record<string, unknown>)[key];
|
const objVal = (obj as Record<string, unknown>)[key];
|
||||||
if (typeof val !== typeof objVal) {
|
if (typeof val !== typeof objVal) {
|
||||||
throw makeRuntimeErrorMsg(
|
throw errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Incorrect type ${typeof objVal} provided for property ${key} on ${name} argument. Should be type ${typeof val}.`,
|
`Incorrect type ${typeof objVal} provided for property ${key} on ${name} argument. Should be type ${typeof val}.`,
|
||||||
"TYPE",
|
"TYPE",
|
||||||
|
@ -32,7 +32,8 @@ import { simple as walksimple } from "acorn-walk";
|
|||||||
import { parseCommand } from "./Terminal/Parser";
|
import { parseCommand } from "./Terminal/Parser";
|
||||||
import { Terminal } from "./Terminal";
|
import { Terminal } from "./Terminal";
|
||||||
import { ScriptArg } from "@nsdefs";
|
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 { resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath";
|
||||||
import { root } from "./Paths/Directory";
|
import { root } from "./Paths/Directory";
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ import { CONSTANTS } from "../../Constants";
|
|||||||
import { Person } from "../Person";
|
import { Person } from "../Person";
|
||||||
import { isMember } from "../../utils/EnumHelper";
|
import { isMember } from "../../utils/EnumHelper";
|
||||||
import { PartialRecord } from "../../Types/Record";
|
import { PartialRecord } from "../../Types/Record";
|
||||||
import { getGoPlayerStartingState } from "../../Go/boardState/goConstants";
|
|
||||||
|
|
||||||
export class PlayerObject extends Person implements IPlayer {
|
export class PlayerObject extends Person implements IPlayer {
|
||||||
// Player-specific properties
|
// Player-specific properties
|
||||||
@ -37,7 +36,6 @@ export class PlayerObject extends Person implements IPlayer {
|
|||||||
gang: Gang | null = null;
|
gang: Gang | null = null;
|
||||||
bladeburner: Bladeburner | null = null;
|
bladeburner: Bladeburner | null = null;
|
||||||
currentServer = "";
|
currentServer = "";
|
||||||
go = getGoPlayerStartingState();
|
|
||||||
factions: FactionName[] = [];
|
factions: FactionName[] = [];
|
||||||
factionInvitations: FactionName[] = [];
|
factionInvitations: FactionName[] = [];
|
||||||
factionRumors = new JSONSet<FactionName>();
|
factionRumors = new JSONSet<FactionName>();
|
||||||
|
@ -51,8 +51,6 @@ import { achievements } from "../../Achievements/Achievements";
|
|||||||
|
|
||||||
import { isCompanyWork } from "../../Work/CompanyWork";
|
import { isCompanyWork } from "../../Work/CompanyWork";
|
||||||
import { isMember } from "../../utils/EnumHelper";
|
import { isMember } from "../../utils/EnumHelper";
|
||||||
import { getGoPlayerStartingState } from "../../Go/boardState/goConstants";
|
|
||||||
import { resetGoNodePower } from "../../Go/effects/effect";
|
|
||||||
|
|
||||||
export function init(this: PlayerObject): void {
|
export function init(this: PlayerObject): void {
|
||||||
/* Initialize Player's home computer */
|
/* 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()));
|
this.sleeves.forEach((sleeve) => (sleeve.shock <= 0 ? sleeve.synchronize() : sleeve.shockRecovery()));
|
||||||
|
|
||||||
resetGoNodePower(this);
|
|
||||||
|
|
||||||
this.lastUpdate = new Date().getTime();
|
this.lastUpdate = new Date().getTime();
|
||||||
|
|
||||||
// Statistics Trackers
|
// Statistics Trackers
|
||||||
@ -152,7 +148,6 @@ export function prestigeSourceFile(this: PlayerObject): void {
|
|||||||
resetGangs();
|
resetGangs();
|
||||||
this.corporation = null;
|
this.corporation = null;
|
||||||
this.bladeburner = null;
|
this.bladeburner = null;
|
||||||
this.go = getGoPlayerStartingState();
|
|
||||||
|
|
||||||
// Reset Stock market
|
// Reset Stock market
|
||||||
this.hasWseAccount = false;
|
this.hasWseAccount = false;
|
||||||
|
@ -24,6 +24,7 @@ import { InvitationsSeen } from "./Faction/ui/FactionsRoot";
|
|||||||
import { CONSTANTS } from "./Constants";
|
import { CONSTANTS } from "./Constants";
|
||||||
import { LogBoxClearEvents } from "./ui/React/LogBoxManager";
|
import { LogBoxClearEvents } from "./ui/React/LogBoxManager";
|
||||||
import { initCircadianModulator } from "./Augmentation/Augmentations";
|
import { initCircadianModulator } from "./Augmentation/Augmentations";
|
||||||
|
import { Go } from "./Go/Go";
|
||||||
|
|
||||||
const BitNode8StartingMoney = 250e6;
|
const BitNode8StartingMoney = 250e6;
|
||||||
function delayedDialog(message: string) {
|
function delayedDialog(message: string) {
|
||||||
@ -46,6 +47,7 @@ export function prestigeAugmentation(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Player.prestigeAugmentation();
|
Player.prestigeAugmentation();
|
||||||
|
Go.prestigeAugmentation();
|
||||||
|
|
||||||
// Delete all Worker Scripts objects
|
// Delete all Worker Scripts objects
|
||||||
prestigeWorkerScripts();
|
prestigeWorkerScripts();
|
||||||
@ -178,6 +180,8 @@ export function prestigeSourceFile(isFlume: boolean): void {
|
|||||||
initBitNodeMultipliers();
|
initBitNodeMultipliers();
|
||||||
|
|
||||||
Player.prestigeSourceFile();
|
Player.prestigeSourceFile();
|
||||||
|
Go.prestigeSourceFile();
|
||||||
|
|
||||||
prestigeWorkerScripts(); // Delete all Worker Scripts objects
|
prestigeWorkerScripts(); // Delete all Worker Scripts objects
|
||||||
|
|
||||||
const homeComp = Player.getHomeComputer();
|
const homeComp = Player.getHomeComputer();
|
||||||
|
@ -38,6 +38,7 @@ import { Corporation } from "./Corporation/Corporation";
|
|||||||
import { Terminal } from "./Terminal";
|
import { Terminal } from "./Terminal";
|
||||||
import { getRecordValues } from "./Types/Record";
|
import { getRecordValues } from "./Types/Record";
|
||||||
import { ExportMaterial } from "./Corporation/Actions";
|
import { ExportMaterial } from "./Corporation/Actions";
|
||||||
|
import { getGoSave, loadGo } from "./Go/SaveLoad";
|
||||||
|
|
||||||
/* SaveObject.js
|
/* SaveObject.js
|
||||||
* Defines the object used to save/load games
|
* Defines the object used to save/load games
|
||||||
@ -86,6 +87,7 @@ class BitburnerSaveObject {
|
|||||||
AllGangsSave = "";
|
AllGangsSave = "";
|
||||||
LastExportBonus = "0";
|
LastExportBonus = "0";
|
||||||
StaneksGiftSave = "";
|
StaneksGiftSave = "";
|
||||||
|
GoSave = "";
|
||||||
|
|
||||||
getSaveString(forceExcludeRunningScripts = false): string {
|
getSaveString(forceExcludeRunningScripts = false): string {
|
||||||
this.PlayerSave = JSON.stringify(Player);
|
this.PlayerSave = JSON.stringify(Player);
|
||||||
@ -105,6 +107,7 @@ class BitburnerSaveObject {
|
|||||||
this.VersionSave = JSON.stringify(CONSTANTS.VersionNumber);
|
this.VersionSave = JSON.stringify(CONSTANTS.VersionNumber);
|
||||||
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
|
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
|
||||||
this.StaneksGiftSave = JSON.stringify(staneksGift);
|
this.StaneksGiftSave = JSON.stringify(staneksGift);
|
||||||
|
this.GoSave = JSON.stringify(getGoSave());
|
||||||
|
|
||||||
if (Player.gang) this.AllGangsSave = JSON.stringify(AllGangs);
|
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 {
|
function loadGame(saveString: string): boolean {
|
||||||
@ -717,6 +732,7 @@ function loadGame(saveString: string): boolean {
|
|||||||
loadAllServers(saveObj.AllServersSave);
|
loadAllServers(saveObj.AllServersSave);
|
||||||
loadCompanies(saveObj.CompaniesSave);
|
loadCompanies(saveObj.CompaniesSave);
|
||||||
loadFactions(saveObj.FactionsSave, Player);
|
loadFactions(saveObj.FactionsSave, Player);
|
||||||
|
loadGo(saveObj.GoSave);
|
||||||
|
|
||||||
if (Object.hasOwn(saveObj, "StaneksGiftSave")) {
|
if (Object.hasOwn(saveObj, "StaneksGiftSave")) {
|
||||||
loadStaneksGift(saveObj.StaneksGiftSave);
|
loadStaneksGift(saveObj.StaneksGiftSave);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { handleUnknownError } from "./Netscript/NetscriptHelpers";
|
import { handleUnknownError } from "./Netscript/ErrorMessages";
|
||||||
|
|
||||||
export function setupUncaughtPromiseHandler(): void {
|
export function setupUncaughtPromiseHandler(): void {
|
||||||
window.addEventListener("unhandledrejection", (e) => {
|
window.addEventListener("unhandledrejection", (e) => {
|
||||||
|
@ -71,7 +71,7 @@ import { V2Modal } from "../utils/V2Modal";
|
|||||||
import { MathJaxContext } from "better-react-mathjax";
|
import { MathJaxContext } from "better-react-mathjax";
|
||||||
import { useRerender } from "./React/hooks";
|
import { useRerender } from "./React/hooks";
|
||||||
import { HistoryProvider } from "./React/Documentation";
|
import { HistoryProvider } from "./React/Documentation";
|
||||||
import { GoRoot } from "../Go/GoRoot";
|
import { GoRoot } from "../Go/ui/GoRoot";
|
||||||
|
|
||||||
const htmlLocation = location;
|
const htmlLocation = location;
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@ import type { Member } from "../types";
|
|||||||
import type { NetscriptContext } from "../Netscript/APIWrapper";
|
import type { NetscriptContext } from "../Netscript/APIWrapper";
|
||||||
|
|
||||||
import * as allEnums from "../Enums";
|
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";
|
import { getRandomInt } from "./helpers/getRandomInt";
|
||||||
|
|
||||||
interface GetMemberOptions {
|
interface GetMemberOptions {
|
||||||
@ -50,7 +51,7 @@ class EnumHelper<EnumObj extends object, EnumMember extends Member<EnumObj> & st
|
|||||||
);
|
);
|
||||||
allowableValues = `See the developer console for allowable values.`;
|
allowableValues = `See the developer console for allowable values.`;
|
||||||
}
|
}
|
||||||
throw helpers.makeRuntimeErrorMsg(
|
throw errorMessage(
|
||||||
ctx,
|
ctx,
|
||||||
`Argument ${argName} should be a ${this.name} enum member.\nProvided value: "${toValidate}".\n${allowableValues}`,
|
`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
|
// Calling getSaveString forces save info to update
|
||||||
saveObject.getSaveString();
|
saveObject.getSaveString();
|
||||||
|
|
||||||
const savesToTest = ["FactionsSave", "PlayerSave", "CompaniesSave"] as const;
|
const savesToTest = ["FactionsSave", "PlayerSave", "CompaniesSave", "GoSave"] as const;
|
||||||
for (const saveToTest of savesToTest) {
|
for (const saveToTest of savesToTest) {
|
||||||
test(`${saveToTest} continuity`, () => {
|
test(`${saveToTest} continuity`, () => {
|
||||||
const parsed = JSON.parse(saveObject[saveToTest]);
|
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 {
|
import {
|
||||||
cheatPlayTwoMoves,
|
cheatPlayTwoMoves,
|
||||||
cheatRemoveRouter,
|
cheatRemoveRouter,
|
||||||
@ -12,10 +15,8 @@ import {
|
|||||||
makePlayerMove,
|
makePlayerMove,
|
||||||
resetBoardState,
|
resetBoardState,
|
||||||
} from "../../../src/Go/effects/netscriptGoImplementation";
|
} from "../../../src/Go/effects/netscriptGoImplementation";
|
||||||
import { Player, setPlayer } from "@player";
|
|
||||||
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
|
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
|
||||||
import "../../../src/Faction/Factions";
|
import "../../../src/Faction/Factions";
|
||||||
import { opponents, playerColors, playTypes } from "../../../src/Go/boardState/goConstants";
|
|
||||||
import { getNewBoardState } from "../../../src/Go/boardState/boardState";
|
import { getNewBoardState } from "../../../src/Go/boardState/boardState";
|
||||||
|
|
||||||
jest.mock("../../../src/Faction/Factions", () => ({
|
jest.mock("../../../src/Faction/Factions", () => ({
|
||||||
@ -28,7 +29,7 @@ describe("Netscript Go API unit tests", () => {
|
|||||||
describe("makeMove() tests", () => {
|
describe("makeMove() tests", () => {
|
||||||
it("should handle invalid moves", async () => {
|
it("should handle invalid moves", async () => {
|
||||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
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 mockLogger = jest.fn();
|
||||||
|
|
||||||
const result = await makePlayerMove(mockLogger, 0, 0);
|
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 () => {
|
it("should update the board with valid player moves", async () => {
|
||||||
const board = ["OXX..", ".....", ".....", ".....", "....."];
|
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 mockLogger = jest.fn();
|
||||||
|
|
||||||
const result = await makePlayerMove(mockLogger, 1, 0);
|
const result = await makePlayerMove(mockLogger, 1, 0);
|
||||||
|
|
||||||
expect(result.success).toEqual(true);
|
expect(result.success).toEqual(true);
|
||||||
expect(mockLogger).toHaveBeenCalledWith("Go move played: 1, 0");
|
expect(mockLogger).toHaveBeenCalledWith("Go move played: 1, 0");
|
||||||
expect(Player.go.boardState.board[1]?.[0]?.player).toEqual(playerColors.black);
|
expect(boardState.board[1]?.[0]?.color).toEqual(GoColor.black);
|
||||||
expect(Player.go.boardState.board[0]?.[0]?.player).toEqual(playerColors.empty);
|
expect(boardState.board[0]?.[0]?.color).toEqual(GoColor.empty);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("passTurn() tests", () => {
|
describe("passTurn() tests", () => {
|
||||||
it("should handle pass attempts", async () => {
|
it("should handle pass attempts", async () => {
|
||||||
Player.go.boardState = getNewBoardState(7);
|
Go.currentGame = getNewBoardState(7);
|
||||||
const mockLogger = jest.fn();
|
const mockLogger = jest.fn();
|
||||||
|
|
||||||
const result = await handlePassTurn(mockLogger);
|
const result = await handlePassTurn(mockLogger);
|
||||||
|
|
||||||
expect(result.success).toEqual(true);
|
expect(result.success).toEqual(true);
|
||||||
expect(result.type).toEqual(playTypes.move);
|
expect(result.type).toEqual(GoPlayType.move);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getBoardState() tests", () => {
|
describe("getBoardState() tests", () => {
|
||||||
it("should correctly return a string version of the bard state", () => {
|
it("should correctly return a string version of the bard state", () => {
|
||||||
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
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);
|
expect(result).toEqual(board);
|
||||||
});
|
});
|
||||||
@ -76,18 +78,19 @@ describe("Netscript Go API unit tests", () => {
|
|||||||
describe("resetBoardState() tests", () => {
|
describe("resetBoardState() tests", () => {
|
||||||
it("should set the player's board to the requested size and opponent", () => {
|
it("should set the player's board to the requested size and opponent", () => {
|
||||||
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
||||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board);
|
Go.currentGame = boardStateFromSimpleBoard(board);
|
||||||
const mockError = jest.fn();
|
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(newBoard?.[0].length).toEqual(9);
|
||||||
expect(Player.go.boardState.board.length).toEqual(9);
|
expect(Go.currentGame.board.length).toEqual(9);
|
||||||
expect(Player.go.boardState.ai).toEqual(opponents.SlumSnakes);
|
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", () => {
|
it("should throw an error if an invalid opponent is requested", () => {
|
||||||
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
||||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board);
|
Go.boardState = getBoardFromSimplifiedBoardState(board);
|
||||||
const mockError = jest.fn();
|
const mockError = jest.fn();
|
||||||
|
|
||||||
resetBoardState(mockError, "fake opponent", 9);
|
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",
|
"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", () => {
|
it("should throw an error if an invalid size is requested", () => {
|
||||||
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
const board = ["OXX..", ".....", ".....", ".....", "..###"];
|
||||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board);
|
Go.currentGame = boardStateFromSimpleBoard(board);
|
||||||
const mockError = jest.fn();
|
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");
|
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", () => {
|
describe("getValidMoves() unit tests", () => {
|
||||||
it("should return all valid and invalid moves on the board", () => {
|
it("should return all valid and invalid moves on the board", () => {
|
||||||
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
|
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();
|
const result = getValidMoves();
|
||||||
|
|
||||||
@ -128,7 +131,7 @@ describe("Netscript Go API unit tests", () => {
|
|||||||
describe("getChains() unit tests", () => {
|
describe("getChains() unit tests", () => {
|
||||||
it("should assign an ID to all contiguous chains on the board", () => {
|
it("should assign an ID to all contiguous chains on the board", () => {
|
||||||
const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
|
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();
|
const result = getChains();
|
||||||
|
|
||||||
@ -142,7 +145,7 @@ describe("Netscript Go API unit tests", () => {
|
|||||||
describe("getLiberties() unit tests", () => {
|
describe("getLiberties() unit tests", () => {
|
||||||
it("should display the number of connected empty nodes for each chain on the board", () => {
|
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"];
|
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();
|
const result = getLiberties();
|
||||||
|
|
||||||
@ -158,7 +161,7 @@ describe("Netscript Go API unit tests", () => {
|
|||||||
describe("getControlledEmptyNodes() unit tests", () => {
|
describe("getControlledEmptyNodes() unit tests", () => {
|
||||||
it("should show the owner of each empty node, if a single player has fully encircled it", () => {
|
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"];
|
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();
|
const result = getControlledEmptyNodes();
|
||||||
|
|
||||||
@ -168,7 +171,7 @@ describe("Netscript Go API unit tests", () => {
|
|||||||
describe("cheatPlayTwoMoves() tests", () => {
|
describe("cheatPlayTwoMoves() tests", () => {
|
||||||
it("should handle invalid moves", async () => {
|
it("should handle invalid moves", async () => {
|
||||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
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 mockLogger = jest.fn();
|
||||||
|
|
||||||
const result = await cheatPlayTwoMoves(mockLogger, 0, 0, 1, 0, 0, 0);
|
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 () => {
|
it("should update the board with both player moves if nodes are unoccupied and cheat is successful", async () => {
|
||||||
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
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 mockLogger = jest.fn();
|
||||||
|
|
||||||
const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 0, 0);
|
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(mockLogger).toHaveBeenCalledWith("Cheat successful. Two go moves played: 4,3 and 3,4");
|
||||||
expect(result.success).toEqual(true);
|
expect(result.success).toEqual(true);
|
||||||
expect(Player.go.boardState.board[4]?.[3]?.player).toEqual(playerColors.black);
|
expect(Go.currentGame.board[4]?.[3]?.color).toEqual(GoColor.black);
|
||||||
expect(Player.go.boardState.board[3]?.[4]?.player).toEqual(playerColors.black);
|
expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.black);
|
||||||
expect(Player.go.boardState.board[4]?.[4]?.player).toEqual(playerColors.empty);
|
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 () => {
|
it("should pass player turn to AI if the cheat is unsuccessful but player is not ejected", async () => {
|
||||||
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
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 mockLogger = jest.fn();
|
||||||
|
|
||||||
const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 2, 1);
|
const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 2, 1);
|
||||||
console.log(result);
|
console.log(result);
|
||||||
expect(mockLogger).toHaveBeenCalledWith("Cheat failed. Your turn has been skipped.");
|
expect(mockLogger).toHaveBeenCalledWith("Cheat failed. Your turn has been skipped.");
|
||||||
expect(result.success).toEqual(false);
|
expect(result.success).toEqual(false);
|
||||||
expect(Player.go.boardState.board[4]?.[3]?.player).toEqual(playerColors.empty);
|
expect(Go.currentGame.board[4]?.[3]?.color).toEqual(GoColor.empty);
|
||||||
expect(Player.go.boardState.board[3]?.[4]?.player).toEqual(playerColors.empty);
|
expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.empty);
|
||||||
expect(Player.go.boardState.board[4]?.[4]?.player).toEqual(playerColors.white);
|
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 () => {
|
it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => {
|
||||||
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
||||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||||
Player.go.boardState.cheatCount = 1;
|
Go.currentGame.cheatCount = 1;
|
||||||
const mockLogger = jest.fn();
|
const mockLogger = jest.fn();
|
||||||
|
|
||||||
const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 1, 0);
|
const result = await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 1, 0);
|
||||||
console.log(result);
|
console.log(result);
|
||||||
expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
|
expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
|
||||||
expect(result.success).toEqual(false);
|
expect(result.success).toEqual(false);
|
||||||
expect(Player.go.boardState.history.length).toEqual(0);
|
expect(Go.currentGame.previousBoard).toEqual(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("cheatRemoveRouter() tests", () => {
|
describe("cheatRemoveRouter() tests", () => {
|
||||||
it("should handle invalid moves", async () => {
|
it("should handle invalid moves", async () => {
|
||||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
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 mockLogger = jest.fn();
|
||||||
|
|
||||||
const result = await cheatRemoveRouter(mockLogger, 1, 0, 0, 0);
|
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 () => {
|
it("should remove the router if the move is valid", async () => {
|
||||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
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 mockLogger = jest.fn();
|
||||||
|
|
||||||
const result = await cheatRemoveRouter(mockLogger, 0, 0, 0, 0);
|
const result = await cheatRemoveRouter(mockLogger, 0, 0, 0, 0);
|
||||||
|
|
||||||
expect(result.success).toEqual(true);
|
expect(result.success).toEqual(true);
|
||||||
expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 0,0 was cleared.");
|
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 () => {
|
it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => {
|
||||||
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
const board = ["OXX..", ".....", ".....", ".....", "....O"];
|
||||||
Player.go.boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus, playerColors.white);
|
Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
|
||||||
Player.go.boardState.cheatCount = 1;
|
Go.currentGame.cheatCount = 1;
|
||||||
const mockLogger = jest.fn();
|
const mockLogger = jest.fn();
|
||||||
|
|
||||||
const result = await cheatRemoveRouter(mockLogger, 0, 0, 1, 0);
|
const result = await cheatRemoveRouter(mockLogger, 0, 0, 1, 0);
|
||||||
console.log(result);
|
console.log(result);
|
||||||
expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
|
expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
|
||||||
expect(result.success).toEqual(false);
|
expect(result.success).toEqual(false);
|
||||||
expect(Player.go.boardState.history.length).toEqual(0);
|
expect(Go.currentGame.previousBoard).toEqual(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("cheatRepairOfflineNode() tests", () => {
|
describe("cheatRepairOfflineNode() tests", () => {
|
||||||
it("should handle invalid moves", async () => {
|
it("should handle invalid moves", async () => {
|
||||||
const board = ["XOO..", ".....", ".....", ".....", "....#"];
|
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 mockLogger = jest.fn();
|
||||||
|
|
||||||
const result = await cheatRepairOfflineNode(mockLogger, 0, 0);
|
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 () => {
|
it("should update the board with the repaired node if the cheat is successful", async () => {
|
||||||
const board = ["OXX..", ".....", ".....", ".....", "....#"];
|
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 mockLogger = jest.fn();
|
||||||
|
|
||||||
const result = await cheatRepairOfflineNode(mockLogger, 4, 4, 0, 0);
|
const result = await cheatRepairOfflineNode(mockLogger, 4, 4, 0, 0);
|
||||||
expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 4,4 was repaired.");
|
expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 4,4 was repaired.");
|
||||||
expect(result.success).toEqual(true);
|
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 { setPlayer } from "@player";
|
||||||
|
import { GoColor } from "@enums";
|
||||||
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
|
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
|
||||||
import {
|
import { getAllEyes, getAllValidMoves, boardStateFromSimpleBoard } from "../../../src/Go/boardAnalysis/boardAnalysis";
|
||||||
getAllEyes,
|
|
||||||
getAllValidMoves,
|
|
||||||
getBoardFromSimplifiedBoardState,
|
|
||||||
} from "../../../src/Go/boardAnalysis/boardAnalysis";
|
|
||||||
import { playerColors } from "../../../src/Go/boardState/goConstants";
|
|
||||||
import { findAnyMatchedPatterns } from "../../../src/Go/boardAnalysis/patternMatching";
|
import { findAnyMatchedPatterns } from "../../../src/Go/boardAnalysis/patternMatching";
|
||||||
|
|
||||||
setPlayer(new PlayerObject());
|
setPlayer(new PlayerObject());
|
||||||
@ -13,7 +9,7 @@ setPlayer(new PlayerObject());
|
|||||||
describe("Go board analysis tests", () => {
|
describe("Go board analysis tests", () => {
|
||||||
it("identifies chains and liberties", async () => {
|
it("identifies chains and liberties", async () => {
|
||||||
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
const board = ["XOO..", ".....", ".....", ".....", "....."];
|
||||||
const boardState = getBoardFromSimplifiedBoardState(board);
|
const boardState = boardStateFromSimpleBoard(board);
|
||||||
|
|
||||||
expect(boardState.board[0]?.[0]?.liberties?.length).toEqual(1);
|
expect(boardState.board[0]?.[0]?.liberties?.length).toEqual(1);
|
||||||
expect(boardState.board[0]?.[1]?.liberties?.length).toEqual(3);
|
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 () => {
|
it("identifies all points that are part of 'eyes' on the board", async () => {
|
||||||
const board = ["..O..", "OOOOO", "..XXX", "..XX.", "..X.X"];
|
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 whitePlayerEyes = getAllEyes(boardState.board, GoColor.white).flat().flat();
|
||||||
const blackPlayerEyes = getAllEyes(boardState, playerColors.black).flat().flat();
|
const blackPlayerEyes = getAllEyes(boardState.board, GoColor.black).flat().flat();
|
||||||
|
|
||||||
expect(whitePlayerEyes?.length).toEqual(4);
|
expect(whitePlayerEyes?.length).toEqual(4);
|
||||||
expect(blackPlayerEyes?.length).toEqual(2);
|
expect(blackPlayerEyes?.length).toEqual(2);
|
||||||
@ -32,11 +28,11 @@ describe("Go board analysis tests", () => {
|
|||||||
|
|
||||||
it("identifies strong patterns on the board", async () => {
|
it("identifies strong patterns on the board", async () => {
|
||||||
const board = [".....", ".....", ".....", ".....", ".OXO."];
|
const board = [".....", ".....", ".....", ".....", ".OXO."];
|
||||||
const boardState = getBoardFromSimplifiedBoardState(board);
|
const boardState = boardStateFromSimpleBoard(board);
|
||||||
const point = await findAnyMatchedPatterns(
|
const point = await findAnyMatchedPatterns(
|
||||||
boardState,
|
boardState.board,
|
||||||
playerColors.white,
|
GoColor.white,
|
||||||
getAllValidMoves(boardState, playerColors.white),
|
getAllValidMoves(boardState, GoColor.white),
|
||||||
true,
|
true,
|
||||||
0,
|
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 { 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 { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
|
||||||
import "../../../src/Faction/Factions";
|
import "../../../src/Faction/Factions";
|
||||||
|
|
||||||
@ -14,24 +14,24 @@ setPlayer(new PlayerObject());
|
|||||||
describe("Go AI tests", () => {
|
describe("Go AI tests", () => {
|
||||||
it("prioritizes capture for Black Hand", async () => {
|
it("prioritizes capture for Black Hand", async () => {
|
||||||
const board = ["XO...", ".....", ".....", ".....", "....."];
|
const board = ["XO...", ".....", ".....", ".....", "....."];
|
||||||
const boardState = getBoardFromSimplifiedBoardState(board, opponents.TheBlackHand);
|
const boardState = boardStateFromSimpleBoard(board, GoOpponent.TheBlackHand);
|
||||||
const move = await getMove(boardState, playerColors.white, opponents.TheBlackHand);
|
const move = await getMove(boardState, GoColor.white, GoOpponent.TheBlackHand);
|
||||||
|
|
||||||
expect([move.x, move.y]).toEqual([1, 0]);
|
expect([move.x, move.y]).toEqual([1, 0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prioritizes defense for Slum Snakes", async () => {
|
it("prioritizes defense for Slum Snakes", async () => {
|
||||||
const board = ["OX...", ".....", ".....", ".....", "....."];
|
const board = ["OX...", ".....", ".....", ".....", "....."];
|
||||||
const boardState = getBoardFromSimplifiedBoardState(board, opponents.SlumSnakes);
|
const boardState = boardStateFromSimpleBoard(board, GoOpponent.SlumSnakes);
|
||||||
const move = await getMove(boardState, playerColors.white, opponents.SlumSnakes);
|
const move = await getMove(boardState, GoColor.white, GoOpponent.SlumSnakes);
|
||||||
|
|
||||||
expect([move.x, move.y]).toEqual([1, 0]);
|
expect([move.x, move.y]).toEqual([1, 0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prioritizes eye creation moves for Illuminati", async () => {
|
it("prioritizes eye creation moves for Illuminati", async () => {
|
||||||
const board = ["...O...", "OOOO...", ".......", ".......", ".......", ".......", "......."];
|
const board = ["...O...", "OOOO...", ".......", ".......", ".......", ".......", "......."];
|
||||||
const boardState = getBoardFromSimplifiedBoardState(board, opponents.Daedalus);
|
const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus);
|
||||||
const move = await getMove(boardState, playerColors.white, opponents.Daedalus, 0);
|
const move = await getMove(boardState, GoColor.white, GoOpponent.Daedalus, 0);
|
||||||
|
|
||||||
console.log(move);
|
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`] = `
|
exports[`Check Save File Continuity PlayerSave continuity 1`] = `
|
||||||
{
|
{
|
||||||
"ctor": "PlayerObject",
|
"ctor": "PlayerObject",
|
||||||
@ -1198,457 +1221,6 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
|
|||||||
"wantedGainRate": 0,
|
"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": [],
|
"hacknetNodes": [],
|
||||||
"has4SData": false,
|
"has4SData": false,
|
||||||
"has4SDataTixApi": false,
|
"has4SDataTixApi": false,
|
||||||
|
Reference in New Issue
Block a user