mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-05-03 14:27:03 +02:00
BITNODE: IPvGO territory control strategy game (#934)
This commit is contained in:
committed by
GitHub
parent
c6141f2adf
commit
7ef12a0323
@@ -0,0 +1,36 @@
|
||||
import { Container, Tab, Tabs } from "@mui/material";
|
||||
import React from "react";
|
||||
import { GoInstructionsPage } from "./ui/GoInstructionsPage";
|
||||
import { BorderInnerSharp, Help, ManageSearch, History } from "@mui/icons-material";
|
||||
import { GoStatusPage } from "./ui/GoStatusPage";
|
||||
import { GoHistoryPage } from "./ui/GoHistoryPage";
|
||||
import { GoGameboardWrapper } from "./ui/GoGameboardWrapper";
|
||||
import { boardStyles } from "./boardState/goStyles";
|
||||
|
||||
export function GoRoot(): React.ReactElement {
|
||||
const classes = boardStyles();
|
||||
const [value, setValue] = React.useState(0);
|
||||
|
||||
function handleChange(event: React.SyntheticEvent, tab: number): void {
|
||||
setValue(tab);
|
||||
}
|
||||
|
||||
function showInstructions() {
|
||||
setValue(3);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container disableGutters maxWidth="lg" sx={{ mx: 0 }}>
|
||||
<Tabs variant="fullWidth" value={value} onChange={handleChange} sx={{ minWidth: "fit-content", maxWidth: "45%" }}>
|
||||
<Tab label="IPvGO Subnet" icon={<BorderInnerSharp />} iconPosition={"start"} className={classes.tab} />
|
||||
<Tab label="Status" icon={<ManageSearch />} iconPosition={"start"} className={classes.tab} />
|
||||
<Tab label="History" icon={<History />} iconPosition={"start"} className={classes.tab} />
|
||||
<Tab label="How to Play" icon={<Help />} iconPosition={"start"} className={classes.tab} />
|
||||
</Tabs>
|
||||
{value === 0 && <GoGameboardWrapper showInstructions={showInstructions} />}
|
||||
{value === 1 && <GoStatusPage />}
|
||||
{value === 2 && <GoHistoryPage />}
|
||||
{value === 3 && <GoInstructionsPage />}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,683 @@
|
||||
import {
|
||||
Board,
|
||||
BoardState,
|
||||
Neighbor,
|
||||
opponents,
|
||||
PlayerColor,
|
||||
playerColors,
|
||||
PointState,
|
||||
validityReason,
|
||||
} from "../boardState/goConstants";
|
||||
import {
|
||||
findAdjacentPointsInChain,
|
||||
findNeighbors,
|
||||
getArrayFromNeighbor,
|
||||
getBoardCopy,
|
||||
getEmptySpaces,
|
||||
getNewBoardState,
|
||||
getStateCopy,
|
||||
isDefined,
|
||||
isNotNull,
|
||||
updateCaptures,
|
||||
updateChains,
|
||||
} from "../boardState/boardState";
|
||||
|
||||
/**
|
||||
* Determines if the given player can legally make a move at the specified coordinates.
|
||||
*
|
||||
* You cannot repeat previous board states, to prevent endless loops (superko rule)
|
||||
*
|
||||
* You cannot make a move that would remove all liberties of your own piece(s) unless it captures opponent's pieces
|
||||
*
|
||||
* You cannot make a move in an occupied space
|
||||
*
|
||||
* You cannot make a move if it is not your turn, or if the game is over
|
||||
*
|
||||
* @returns a validity explanation for if the move is legal or not
|
||||
*/
|
||||
export function evaluateIfMoveIsValid(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: PlayerColor,
|
||||
shortcut = true,
|
||||
) {
|
||||
const point = boardState.board?.[x]?.[y];
|
||||
|
||||
if (boardState.previousPlayer === null) {
|
||||
return validityReason.gameOver;
|
||||
}
|
||||
if (boardState.previousPlayer === player) {
|
||||
return validityReason.notYourTurn;
|
||||
}
|
||||
if (!point) {
|
||||
return validityReason.pointBroken;
|
||||
}
|
||||
if (point.player !== playerColors.empty) {
|
||||
return validityReason.pointNotEmpty;
|
||||
}
|
||||
|
||||
// Detect if the current player has ever previously played this move. Used to detect potential repeated board states
|
||||
const moveHasBeenPlayedBefore = !!boardState.history.find((board) => board[x]?.[y]?.player === player);
|
||||
|
||||
if (shortcut) {
|
||||
// If the current point has some adjacent open spaces, it is not suicide. If the move is not repeated, it is legal
|
||||
const liberties = findAdjacentLibertiesForPoint(boardState, x, y);
|
||||
const hasLiberty = liberties.north || liberties.east || liberties.south || liberties.west;
|
||||
if (!moveHasBeenPlayedBefore && hasLiberty) {
|
||||
return validityReason.valid;
|
||||
}
|
||||
|
||||
// If a connected friendly chain has more than one liberty, the move is not suicide. If the move is not repeated, it is legal
|
||||
const neighborChainLibertyCount = findMaxLibertyCountOfAdjacentChains(boardState, x, y, player);
|
||||
if (!moveHasBeenPlayedBefore && neighborChainLibertyCount > 1) {
|
||||
return validityReason.valid;
|
||||
}
|
||||
|
||||
// If there is any neighboring enemy chain with only one liberty, and the move is not repeated, it is valid,
|
||||
// because it would capture the enemy chain and free up some liberties for itself
|
||||
const potentialCaptureChainLibertyCount = findMinLibertyCountOfAdjacentChains(
|
||||
boardState,
|
||||
x,
|
||||
y,
|
||||
player === playerColors.black ? playerColors.white : playerColors.black,
|
||||
);
|
||||
if (!moveHasBeenPlayedBefore && potentialCaptureChainLibertyCount < 2) {
|
||||
return validityReason.valid;
|
||||
}
|
||||
|
||||
// If there is no direct liberties for the move, no captures, and no neighboring friendly chains with multiple liberties,
|
||||
// the move is not valid because it would suicide the piece
|
||||
if (!hasLiberty && potentialCaptureChainLibertyCount >= 2 && neighborChainLibertyCount <= 1) {
|
||||
return validityReason.noSuicide;
|
||||
}
|
||||
}
|
||||
|
||||
// If the move has been played before and is not obviously illegal, we have to actually play it out to determine
|
||||
// if it is a repeated move, or if it is a valid move
|
||||
const evaluationBoard = evaluateMoveResult(boardState, x, y, player, true);
|
||||
if (evaluationBoard.board[x]?.[y]?.player !== player) {
|
||||
return validityReason.noSuicide;
|
||||
}
|
||||
if (moveHasBeenPlayedBefore && checkIfBoardStateIsRepeated(evaluationBoard)) {
|
||||
return validityReason.boardRepeated;
|
||||
}
|
||||
|
||||
return validityReason.valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new evaluation board and play out the results of the given move on the new board
|
||||
*/
|
||||
export function evaluateMoveResult(
|
||||
initialBoardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: playerColors,
|
||||
resetChains = false,
|
||||
) {
|
||||
const boardState = getStateCopy(initialBoardState);
|
||||
boardState.history.push(getBoardCopy(boardState).board);
|
||||
const point = boardState.board[x]?.[y];
|
||||
if (!point) {
|
||||
return initialBoardState;
|
||||
}
|
||||
|
||||
point.player = player;
|
||||
boardState.previousPlayer = player;
|
||||
|
||||
const neighbors = getArrayFromNeighbor(findNeighbors(boardState, x, y));
|
||||
const chainIdsToUpdate = [point.chain, ...neighbors.map((point) => point.chain)];
|
||||
resetChainsById(boardState, chainIdsToUpdate);
|
||||
|
||||
return updateCaptures(boardState, player, resetChains);
|
||||
}
|
||||
|
||||
export function getControlledSpace(boardState: BoardState) {
|
||||
const chains = getAllChains(boardState);
|
||||
const length = boardState.board[0].length;
|
||||
const whiteControlledEmptyNodes = getAllPotentialEyes(boardState, chains, playerColors.white, length * 2)
|
||||
.map((eye) => eye.chain)
|
||||
.flat();
|
||||
const blackControlledEmptyNodes = getAllPotentialEyes(boardState, chains, playerColors.black, length * 2)
|
||||
.map((eye) => eye.chain)
|
||||
.flat();
|
||||
|
||||
const ownedPointGrid = Array.from({ length }, () => Array.from({ length }, () => playerColors.empty));
|
||||
whiteControlledEmptyNodes.forEach((node) => {
|
||||
ownedPointGrid[node.x][node.y] = playerColors.white;
|
||||
});
|
||||
blackControlledEmptyNodes.forEach((node) => {
|
||||
ownedPointGrid[node.x][node.y] = playerColors.black;
|
||||
});
|
||||
|
||||
return ownedPointGrid;
|
||||
}
|
||||
|
||||
/**
|
||||
Clear the chain and liberty data of all points in the given chains
|
||||
*/
|
||||
const resetChainsById = (boardState: BoardState, chainIds: string[]) => {
|
||||
const pointsToUpdate = boardState.board
|
||||
.flat()
|
||||
.filter(isDefined)
|
||||
.filter(isNotNull)
|
||||
.filter((point) => chainIds.includes(point.chain));
|
||||
pointsToUpdate.forEach((point) => {
|
||||
point.chain = "";
|
||||
point.liberties = [];
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* For a potential move, determine what the liberty of the point would be if played, by looking at adjacent empty nodes
|
||||
* as well as the remaining liberties of neighboring friendly chains
|
||||
*/
|
||||
export function findEffectiveLibertiesOfNewMove(boardState: BoardState, x: number, y: number, player: PlayerColor) {
|
||||
const friendlyChains = getAllChains(boardState).filter((chain) => chain[0].player === player);
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
|
||||
const neighborPoints = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined);
|
||||
// Get all chains that the new move will connect to
|
||||
const allyNeighbors = neighborPoints.filter((neighbor) => neighbor.player === player);
|
||||
const allyNeighborChainLiberties = allyNeighbors
|
||||
.map((neighbor) => {
|
||||
const chain = friendlyChains.find((chain) => chain[0].chain === neighbor.chain);
|
||||
return chain?.[0]?.liberties ?? null;
|
||||
})
|
||||
.flat()
|
||||
.filter(isNotNull);
|
||||
|
||||
// Get all empty spaces that the new move connects to that aren't already part of friendly liberties
|
||||
const directLiberties = neighborPoints.filter((neighbor) => neighbor.player === playerColors.empty);
|
||||
|
||||
const allLiberties = [...directLiberties, ...allyNeighborChainLiberties];
|
||||
|
||||
// filter out duplicates, and starting point
|
||||
return allLiberties
|
||||
.filter(
|
||||
(liberty, index) =>
|
||||
allLiberties.findIndex((neighbor) => liberty.x === neighbor.x && liberty.y === neighbor.y) === index,
|
||||
)
|
||||
.filter((liberty) => liberty.x !== x || liberty.y !== y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the number of open spaces that are connected to chains adjacent to a given point, and return the maximum
|
||||
*/
|
||||
export function findMaxLibertyCountOfAdjacentChains(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: playerColors,
|
||||
) {
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
|
||||
const friendlyNeighbors = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined)
|
||||
.filter((neighbor) => neighbor.player === player);
|
||||
|
||||
return friendlyNeighbors.reduce((max, neighbor) => Math.max(max, neighbor?.liberties?.length ?? 0), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the number of open spaces that are connected to chains adjacent to a given point, and return the minimum
|
||||
*/
|
||||
export function findMinLibertyCountOfAdjacentChains(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: playerColors,
|
||||
) {
|
||||
const chain = findEnemyNeighborChainWithFewestLiberties(boardState, x, y, player);
|
||||
return chain?.[0]?.liberties?.length ?? 99;
|
||||
}
|
||||
|
||||
export function findEnemyNeighborChainWithFewestLiberties(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
player: playerColors,
|
||||
) {
|
||||
const chains = getAllChains(boardState);
|
||||
const neighbors = findAdjacentLibertiesAndAlliesForPoint(boardState, x, y, player);
|
||||
const friendlyNeighbors = [neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined)
|
||||
.filter((neighbor) => neighbor.player === player);
|
||||
|
||||
const minimumLiberties = friendlyNeighbors.reduce(
|
||||
(min, neighbor) => Math.min(min, neighbor?.liberties?.length ?? 0),
|
||||
friendlyNeighbors?.[0]?.liberties?.length ?? 99,
|
||||
);
|
||||
|
||||
const chainId = friendlyNeighbors.find((neighbor) => neighbor?.liberties?.length === minimumLiberties)?.chain;
|
||||
return chains.find((chain) => chain[0].chain === chainId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of points that are valid moves for the given player
|
||||
*/
|
||||
export function getAllValidMoves(boardState: BoardState, player: PlayerColor) {
|
||||
return getEmptySpaces(boardState).filter(
|
||||
(point) => evaluateIfMoveIsValid(boardState, point.x, point.y, player) === validityReason.valid,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
Find all empty point groups where either:
|
||||
* all of its immediate surrounding player-controlled points are in the same continuous chain, or
|
||||
* it is completely surrounded by some single larger chain and the edge of the board
|
||||
|
||||
Eyes are important, because a chain of pieces cannot be captured if it fully surrounds two or more eyes.
|
||||
*/
|
||||
export function getAllEyesByChainId(boardState: BoardState, player: playerColors) {
|
||||
const allChains = getAllChains(boardState);
|
||||
const eyeCandidates = getAllPotentialEyes(boardState, allChains, player);
|
||||
const eyes: { [s: string]: PointState[][] } = {};
|
||||
|
||||
eyeCandidates.forEach((candidate) => {
|
||||
if (candidate.neighbors.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If only one chain surrounds the empty space, it is a true eye
|
||||
if (candidate.neighbors.length === 1) {
|
||||
const neighborChainID = candidate.neighbors[0][0].chain;
|
||||
eyes[neighborChainID] = eyes[neighborChainID] || [];
|
||||
eyes[neighborChainID].push(candidate.chain);
|
||||
return;
|
||||
}
|
||||
|
||||
// If any chain fully encircles the empty space (even if there are other chains encircled as well), the eye is true
|
||||
const neighborsEncirclingEye = findNeighboringChainsThatFullyEncircleEmptySpace(
|
||||
boardState,
|
||||
candidate.chain,
|
||||
candidate.neighbors,
|
||||
allChains,
|
||||
);
|
||||
neighborsEncirclingEye.forEach((neighborChain) => {
|
||||
const neighborChainID = neighborChain[0].chain;
|
||||
eyes[neighborChainID] = eyes[neighborChainID] || [];
|
||||
eyes[neighborChainID].push(candidate.chain);
|
||||
});
|
||||
});
|
||||
|
||||
return eyes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all eyes, grouped by the chain they are adjacent to
|
||||
*/
|
||||
export function getAllEyes(boardState: BoardState, player: playerColors, eyesObject?: { [s: string]: PointState[][] }) {
|
||||
const eyes = eyesObject ?? getAllEyesByChainId(boardState, player);
|
||||
return Object.keys(eyes).map((key) => eyes[key]);
|
||||
}
|
||||
|
||||
/**
|
||||
Find all empty spaces completely surrounded by a single player color.
|
||||
For each player chain number, add any empty space chains that are completely surrounded by a single player's color to
|
||||
an array at that chain number's index.
|
||||
*/
|
||||
export function getAllPotentialEyes(
|
||||
boardState: BoardState,
|
||||
allChains: PointState[][],
|
||||
player: playerColors,
|
||||
_maxSize?: number,
|
||||
) {
|
||||
const nodeCount = boardState.board.map((row) => row.filter((p) => p)).flat().length;
|
||||
const maxSize = _maxSize ?? Math.min(nodeCount * 0.4, 11);
|
||||
const emptyPointChains = allChains.filter((chain) => chain[0].player === playerColors.empty);
|
||||
const eyeCandidates: { neighbors: PointState[][]; chain: PointState[]; id: string }[] = [];
|
||||
|
||||
emptyPointChains
|
||||
.filter((chain) => chain.length <= maxSize)
|
||||
.forEach((chain) => {
|
||||
const neighboringChains = getAllNeighboringChains(boardState, chain, allChains);
|
||||
|
||||
const hasWhitePieceNeighbor = neighboringChains.find(
|
||||
(neighborChain) => neighborChain[0]?.player === playerColors.white,
|
||||
);
|
||||
const hasBlackPieceNeighbor = neighboringChains.find(
|
||||
(neighborChain) => neighborChain[0]?.player === playerColors.black,
|
||||
);
|
||||
|
||||
// Record the neighbor chains of the eye candidate empty chain, if all of its neighbors are the same color piece
|
||||
if (
|
||||
(hasWhitePieceNeighbor && !hasBlackPieceNeighbor && player === playerColors.white) ||
|
||||
(!hasWhitePieceNeighbor && hasBlackPieceNeighbor && player === playerColors.black)
|
||||
) {
|
||||
eyeCandidates.push({
|
||||
neighbors: neighboringChains,
|
||||
chain: chain,
|
||||
id: chain[0].chain,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return eyeCandidates;
|
||||
}
|
||||
|
||||
/**
|
||||
* For each chain bordering an eye candidate:
|
||||
* remove all other neighboring chains. (replace with empty points)
|
||||
* check if the eye candidate is a simple true eye now
|
||||
* If so, the original candidate is a true eye.
|
||||
*/
|
||||
function findNeighboringChainsThatFullyEncircleEmptySpace(
|
||||
boardState: BoardState,
|
||||
candidateChain: PointState[],
|
||||
neighborChainList: PointState[][],
|
||||
allChains: PointState[][],
|
||||
) {
|
||||
const boardMax = boardState.board[0].length - 1;
|
||||
const candidateSpread = findFurthestPointsOfChain(candidateChain);
|
||||
return neighborChainList.filter((neighborChain, index) => {
|
||||
// If the chain does not go far enough to surround the eye in question, don't bother building an eval board
|
||||
const neighborSpread = findFurthestPointsOfChain(neighborChain);
|
||||
|
||||
const couldWrapNorth =
|
||||
neighborSpread.north > candidateSpread.north ||
|
||||
(candidateSpread.north === boardMax && neighborSpread.north === boardMax);
|
||||
const couldWrapEast =
|
||||
neighborSpread.east > candidateSpread.east ||
|
||||
(candidateSpread.east === boardMax && neighborSpread.east === boardMax);
|
||||
const couldWrapSouth =
|
||||
neighborSpread.south < candidateSpread.south || (candidateSpread.south === 0 && neighborSpread.south === 0);
|
||||
const couldWrapWest =
|
||||
neighborSpread.west < candidateSpread.west || (candidateSpread.west === 0 && neighborSpread.west === 0);
|
||||
|
||||
if (!couldWrapNorth || !couldWrapEast || !couldWrapSouth || !couldWrapWest) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const evaluationBoard = getStateCopy(boardState);
|
||||
const examplePoint = candidateChain[0];
|
||||
const otherChainNeighborPoints = removePointAtIndex(neighborChainList, index)
|
||||
.flat()
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined);
|
||||
otherChainNeighborPoints.forEach((point) => {
|
||||
const pointToEdit = evaluationBoard.board[point.x]?.[point.y];
|
||||
if (pointToEdit) {
|
||||
pointToEdit.player = playerColors.empty;
|
||||
}
|
||||
});
|
||||
const updatedBoard = updateChains(evaluationBoard);
|
||||
const newChains = getAllChains(updatedBoard);
|
||||
const newChainID = updatedBoard.board[examplePoint.x]?.[examplePoint.y]?.chain;
|
||||
const chain = newChains.find((chain) => chain[0].chain === newChainID) || [];
|
||||
const newNeighborChains = getAllNeighboringChains(boardState, chain, allChains);
|
||||
|
||||
return newNeighborChains.length === 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the furthest that a chain extends in each of the cardinal directions
|
||||
*/
|
||||
function findFurthestPointsOfChain(chain: PointState[]) {
|
||||
return chain.reduce(
|
||||
(directions, point) => {
|
||||
if (point.y > directions.north) {
|
||||
directions.north = point.y;
|
||||
}
|
||||
if (point.y < directions.south) {
|
||||
directions.south = point.y;
|
||||
}
|
||||
if (point.x > directions.east) {
|
||||
directions.east = point.x;
|
||||
}
|
||||
if (point.x < directions.west) {
|
||||
directions.west = point.x;
|
||||
}
|
||||
|
||||
return directions;
|
||||
},
|
||||
{
|
||||
north: chain[0].y,
|
||||
east: chain[0].x,
|
||||
south: chain[0].y,
|
||||
west: chain[0].x,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an element from an array at the given index
|
||||
*/
|
||||
function removePointAtIndex(arr: PointState[][], index: number) {
|
||||
const newArr = [...arr];
|
||||
newArr.splice(index, 1);
|
||||
return newArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all player chains that are adjacent / touching the current chain
|
||||
*/
|
||||
export function getAllNeighboringChains(boardState: BoardState, chain: PointState[], allChains: PointState[][]) {
|
||||
const playerNeighbors = getPlayerNeighbors(boardState, chain);
|
||||
|
||||
const neighboringChains = playerNeighbors.reduce(
|
||||
(neighborChains, neighbor) =>
|
||||
neighborChains.add(allChains.find((chain) => chain[0].chain === neighbor.chain) || []),
|
||||
new Set<PointState[]>(),
|
||||
);
|
||||
|
||||
return [...neighboringChains];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all points that have player pieces adjacent to the given point
|
||||
*/
|
||||
export function getPlayerNeighbors(boardState: BoardState, chain: PointState[]) {
|
||||
return getAllNeighbors(boardState, chain).filter((neighbor) => neighbor && neighbor.player !== playerColors.empty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all points adjacent to the given point
|
||||
*/
|
||||
export function getAllNeighbors(boardState: BoardState, chain: PointState[]) {
|
||||
const allNeighbors = chain.reduce((chainNeighbors: Set<PointState>, point: PointState) => {
|
||||
getArrayFromNeighbor(findNeighbors(boardState, point.x, point.y))
|
||||
.filter((neighborPoint) => !isPointInChain(neighborPoint, chain))
|
||||
.forEach((neighborPoint) => chainNeighbors.add(neighborPoint));
|
||||
return chainNeighbors;
|
||||
}, new Set<PointState>());
|
||||
return [...allNeighbors];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if chain has a point that matches the given coordinates
|
||||
*/
|
||||
export function isPointInChain(point: PointState, chain: PointState[]) {
|
||||
return !!chain.find((chainPoint) => chainPoint.x === point.x && chainPoint.y === point.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks through the board history to see if the current state is identical to any previous state
|
||||
* Capped at 5 for calculation speed, because loops of size 6 are essentially impossible
|
||||
*/
|
||||
function checkIfBoardStateIsRepeated(boardState: BoardState) {
|
||||
const currentBoard = boardState.board;
|
||||
return boardState.history.slice(-5).find((state) => {
|
||||
for (let x = 0; x < state.length; x++) {
|
||||
for (let y = 0; y < state[x].length; y++) {
|
||||
if (currentBoard[x]?.[y]?.player && currentBoard[x]?.[y]?.player !== state[x]?.[y]?.player) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all groups of connected pieces, or empty space groups
|
||||
*/
|
||||
export function getAllChains(boardState: BoardState): PointState[][] {
|
||||
const chains: { [s: string]: PointState[] } = {};
|
||||
|
||||
for (let x = 0; x < boardState.board.length; x++) {
|
||||
for (let y = 0; y < boardState.board[x].length; y++) {
|
||||
const point = boardState.board[x]?.[y];
|
||||
// If the current chain is already analyzed, skip it
|
||||
if (!point || point.chain === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
chains[point.chain] = chains[point.chain] || [];
|
||||
chains[point.chain].push(point);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(chains).map((key) => chains[key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find any group of stones with no liberties (who therefore are to be removed from the board)
|
||||
*/
|
||||
export function findAllCapturedChains(chainList: PointState[][], playerWhoMoved: PlayerColor) {
|
||||
const opposingPlayer = playerWhoMoved === playerColors.white ? playerColors.black : playerColors.white;
|
||||
const enemyChainsToCapture = findCapturedChainOfColor(chainList, opposingPlayer);
|
||||
|
||||
if (enemyChainsToCapture) {
|
||||
return enemyChainsToCapture;
|
||||
}
|
||||
|
||||
const friendlyChainsToCapture = findCapturedChainOfColor(chainList, playerWhoMoved);
|
||||
if (friendlyChainsToCapture) {
|
||||
return friendlyChainsToCapture;
|
||||
}
|
||||
}
|
||||
|
||||
function findCapturedChainOfColor(chainList: PointState[][], playerColor: PlayerColor) {
|
||||
return chainList.filter((chain) => chain?.[0].player === playerColor && chain?.[0].liberties?.length === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all empty points adjacent to any piece in a given chain
|
||||
*/
|
||||
export function findLibertiesForChain(boardState: BoardState, chain: PointState[]): PointState[] {
|
||||
return getAllNeighbors(boardState, chain).filter((neighbor) => neighbor && neighbor.player === playerColors.empty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all empty points adjacent to any piece in the chain that a given point belongs to
|
||||
*/
|
||||
export function findChainLibertiesForPoint(boardState: BoardState, x: number, y: number): PointState[] {
|
||||
const chain = findAdjacentPointsInChain(boardState, x, y);
|
||||
return findLibertiesForChain(boardState, chain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object that includes which of the cardinal neighbors are empty
|
||||
* (adjacent 'liberties' of the current piece )
|
||||
*/
|
||||
export function findAdjacentLibertiesForPoint(boardState: BoardState, x: number, y: number): Neighbor {
|
||||
const neighbors = findNeighbors(boardState, x, y);
|
||||
|
||||
const hasNorthLiberty = neighbors.north && neighbors.north.player === playerColors.empty;
|
||||
const hasEastLiberty = neighbors.east && neighbors.east.player === playerColors.empty;
|
||||
const hasSouthLiberty = neighbors.south && neighbors.south.player === playerColors.empty;
|
||||
const hasWestLiberty = neighbors.west && neighbors.west.player === playerColors.empty;
|
||||
|
||||
return {
|
||||
north: hasNorthLiberty ? neighbors.north : null,
|
||||
east: hasEastLiberty ? neighbors.east : null,
|
||||
south: hasSouthLiberty ? neighbors.south : null,
|
||||
west: hasWestLiberty ? neighbors.west : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object that includes which of the cardinal neighbors are either empty or contain the
|
||||
* current player's pieces. Used for making the connection map on the board
|
||||
*/
|
||||
export function findAdjacentLibertiesAndAlliesForPoint(
|
||||
boardState: BoardState,
|
||||
x: number,
|
||||
y: number,
|
||||
_player?: PlayerColor,
|
||||
): Neighbor {
|
||||
const currentPoint = boardState.board[x]?.[y];
|
||||
const player =
|
||||
_player || (!currentPoint || currentPoint.player === playerColors.empty ? undefined : currentPoint.player);
|
||||
const adjacentLiberties = findAdjacentLibertiesForPoint(boardState, x, y);
|
||||
const neighbors = findNeighbors(boardState, x, y);
|
||||
|
||||
return {
|
||||
north: adjacentLiberties.north || neighbors.north?.player === player ? neighbors.north : null,
|
||||
east: adjacentLiberties.east || neighbors.east?.player === player ? neighbors.east : null,
|
||||
south: adjacentLiberties.south || neighbors.south?.player === player ? neighbors.south : null,
|
||||
west: adjacentLiberties.west || neighbors.west?.player === player ? neighbors.west : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a simplified version of the board state. "X" represents black pieces, "O" white, and "." empty points.
|
||||
*
|
||||
* For example, a 5x5 board might look like this:
|
||||
* ```
|
||||
* [
|
||||
* "XX.O.",
|
||||
* "X..OO",
|
||||
* ".XO..",
|
||||
* "XXO..",
|
||||
* ".XOO.",
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* Each string represents a vertical column on the board, and each character in the string represents a point.
|
||||
*
|
||||
* Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index [1][0].
|
||||
*
|
||||
* Note that the [0][0] point is shown on the bottom-left on the visual board (as is traditional), and each
|
||||
* string represents a vertical column on the board. In other words, the printed example above can be understood to
|
||||
* be rotated 90 degrees clockwise compared to the board UI as shown in the IPvGO game.
|
||||
*
|
||||
*/
|
||||
export function getSimplifiedBoardState(board: Board): string[] {
|
||||
return board.map((column) =>
|
||||
column.reduce((str, point) => {
|
||||
if (!point) {
|
||||
return str + "#";
|
||||
}
|
||||
if (point.player === playerColors.black) {
|
||||
return str + "X";
|
||||
}
|
||||
if (point.player === playerColors.white) {
|
||||
return str + "O";
|
||||
}
|
||||
return str + ".";
|
||||
}, ""),
|
||||
);
|
||||
}
|
||||
|
||||
export function getBoardFromSimplifiedBoardState(
|
||||
boardStrings: string[],
|
||||
ai = opponents.Daedalus,
|
||||
lastPlayer = playerColors.black,
|
||||
) {
|
||||
const newBoardState = getNewBoardState(boardStrings[0].length, ai);
|
||||
newBoardState.previousPlayer = lastPlayer;
|
||||
|
||||
for (let x = 0; x < boardStrings[0].length; x++) {
|
||||
for (let y = 0; y < boardStrings[0].length; y++) {
|
||||
const boardStringPoint = boardStrings[x]?.[y];
|
||||
const newBoardPoint = newBoardState.board[x]?.[y];
|
||||
if (boardStringPoint === "#") {
|
||||
newBoardState.board[x][y] = null;
|
||||
}
|
||||
if (boardStringPoint === "X" && newBoardPoint?.player) {
|
||||
newBoardPoint.player = playerColors.black;
|
||||
}
|
||||
if (boardStringPoint === "O" && newBoardPoint?.player) {
|
||||
newBoardPoint.player = playerColors.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updateCaptures(newBoardState, lastPlayer);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { BoardState, playerColors, type PointState } from "../boardState/goConstants";
|
||||
import {
|
||||
getAllChains,
|
||||
getAllEyes,
|
||||
getAllNeighboringChains,
|
||||
getAllPotentialEyes,
|
||||
getAllValidMoves,
|
||||
} from "./boardAnalysis";
|
||||
import { contains, isNotNull } from "../boardState/boardState";
|
||||
|
||||
/**
|
||||
* Any empty space fully encircled by the opponent is not worth playing in, unless one of its borders explicitly has a weakness
|
||||
*
|
||||
* Specifically, ignore any empty space encircled by the opponent, unless one of the chains that is on the exterior:
|
||||
* * does not have too many more liberties
|
||||
* * has been fully encircled on the outside by the current player
|
||||
* * Only has liberties remaining inside the abovementioned empty space
|
||||
*
|
||||
* In which case, only the liberties of that one weak chain are worth considering. Other parts of that fully-encircled
|
||||
* enemy space, and other similar spaces, should be ignored, otherwise the game drags on too long
|
||||
*/
|
||||
export function findDisputedTerritory(boardState: BoardState, player: playerColors, excludeFriendlyEyes?: boolean) {
|
||||
let validMoves = getAllValidMoves(boardState, player);
|
||||
if (excludeFriendlyEyes) {
|
||||
const friendlyEyes = getAllEyes(boardState, player)
|
||||
.filter((eye) => eye.length >= 2)
|
||||
.flat()
|
||||
.flat();
|
||||
validMoves = validMoves.filter((point) => !contains(friendlyEyes, point));
|
||||
}
|
||||
const opponent = player === playerColors.white ? playerColors.black : playerColors.white;
|
||||
const chains = getAllChains(boardState);
|
||||
const emptySpacesToAnalyze = getAllPotentialEyes(boardState, chains, opponent);
|
||||
const nodesInsideEyeSpacesToAnalyze = emptySpacesToAnalyze.map((space) => space.chain).flat();
|
||||
|
||||
const playableNodesInsideOfEnemySpace = emptySpacesToAnalyze.reduce((playableNodes: PointState[], space) => {
|
||||
// Look for any opponent chains on the border of the empty space, to see if it has a weakness
|
||||
const attackableLiberties = space.neighbors
|
||||
.map((neighborChain) => {
|
||||
const liberties = neighborChain[0].liberties ?? [];
|
||||
|
||||
// Ignore border chains with too many liberties, they can't effectively be attacked
|
||||
if (liberties.length > 4) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get all opponent chains that make up the border of the opponent-controlled space
|
||||
const neighborChains = getAllNeighboringChains(boardState, neighborChain, chains);
|
||||
|
||||
// Ignore border chains that do not touch the current player's pieces somewhere, as they are likely fully interior
|
||||
// to the empty space in question, or only share a border with the edge of the board and the space, or are not yet
|
||||
// surrounded on the exterior and ready to be attacked within
|
||||
if (!neighborChains.find((chain) => chain?.[0]?.player === player)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const libertiesInsideOfSpaceToAnalyze = liberties
|
||||
.filter(isNotNull)
|
||||
.filter((point) => contains(space.chain, point));
|
||||
|
||||
// If the chain has any liberties outside the empty space being analyzed, it is not yet fully surrounded,
|
||||
// and should not be attacked yet
|
||||
if (libertiesInsideOfSpaceToAnalyze.length !== liberties.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// If the enemy chain is fully surrounded on the outside of the space by the current player, then its liberties
|
||||
// inside the empty space is worth considering for an attack
|
||||
return libertiesInsideOfSpaceToAnalyze;
|
||||
})
|
||||
.flat();
|
||||
|
||||
return [...playableNodes, ...attackableLiberties];
|
||||
}, []);
|
||||
|
||||
// Return only valid moves that are not inside enemy surrounded empty spaces, or ones that are explicitly next to an enemy chain that can be attacked
|
||||
return validMoves.filter(
|
||||
(move) => !contains(nodesInsideEyeSpacesToAnalyze, move) || contains(playableNodesInsideOfEnemySpace, move),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
If a group of stones has more than one empty holes that it completely surrounds, it cannot be captured, because white can
|
||||
only play one stone at a time.
|
||||
Thus, the empty space of those holes is firmly claimed by the player surrounding them, and it can be ignored as a play area
|
||||
Once all points are either stones or claimed territory in this way, the game is over
|
||||
|
||||
Note that this does not detect mutual eyes formed by two chains making an eye together, or eyes via seki, or some other edge cases.
|
||||
*/
|
||||
export function findClaimedTerritory(boardState: BoardState) {
|
||||
const whiteClaimedTerritory = getAllEyes(boardState, playerColors.white).filter(
|
||||
(eyesForChainN) => eyesForChainN.length >= 2,
|
||||
);
|
||||
const blackClaimedTerritory = getAllEyes(boardState, playerColors.black).filter(
|
||||
(eyesForChainN) => eyesForChainN.length >= 2,
|
||||
);
|
||||
return [...blackClaimedTerritory, ...whiteClaimedTerritory].flat().flat();
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/** @param {NS} ns */
|
||||
export async function main(ns) {
|
||||
let result;
|
||||
|
||||
do {
|
||||
const board = ns.go.getBoardState();
|
||||
const validMoves = ns.go.analysis.getValidMoves();
|
||||
|
||||
const [growX, growY] = getGrowMove(board, validMoves);
|
||||
const [randX, randY] = getRandomMove(board, validMoves);
|
||||
// Try to pick a grow move, otherwise choose a random move
|
||||
const x = growX ?? randX;
|
||||
const y = growY ?? randY;
|
||||
|
||||
if (x === undefined) {
|
||||
// Pass turn if no moves are found
|
||||
result = await ns.go.passTurn();
|
||||
} else {
|
||||
// Play the selected move
|
||||
result = await ns.go.makeMove(x, y);
|
||||
}
|
||||
|
||||
await ns.sleep(100);
|
||||
} while (result?.type !== "gameOver" && result?.type !== "pass");
|
||||
|
||||
// After the opponent passes, end the game by passing as well
|
||||
await ns.go.passTurn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose one of the empty points on the board at random to play
|
||||
*/
|
||||
const getRandomMove = (board, validMoves) => {
|
||||
const moveOptions = [];
|
||||
const size = board[0].length;
|
||||
|
||||
// Look through all the points on the board
|
||||
for (let x = 0; x < size; x++) {
|
||||
for (let y = 0; y < size; y++) {
|
||||
// Make sure the point is a valid move
|
||||
const isValidMove = validMoves[x][y];
|
||||
// Leave some spaces to make it harder to capture our pieces
|
||||
const isNotReservedSpace = x % 2 || y % 2;
|
||||
|
||||
if (isValidMove && isNotReservedSpace) {
|
||||
moveOptions.push([x, y]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Choose one of the found moves at random
|
||||
const randomIndex = Math.floor(Math.random() * moveOptions.length);
|
||||
return moveOptions[randomIndex] ?? [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Choose a point connected to a friendly stone to play
|
||||
*/
|
||||
const getGrowMove = (board, validMoves) => {
|
||||
const moveOptions = [];
|
||||
const size = board[0].length;
|
||||
|
||||
// Look through all the points on the board
|
||||
for (let x = 0; x < size; x++) {
|
||||
for (let y = 0; y < size; y++) {
|
||||
// make sure the move is valid
|
||||
const isValidMove = validMoves[x][y];
|
||||
// Leave some open spaces to make it harder to capture our pieces
|
||||
const isNotReservedSpace = x % 2 || y % 2;
|
||||
|
||||
// Make sure we are connected to a friendly piece
|
||||
const neighbors = getNeighbors(board, x, y);
|
||||
const hasFriendlyNeighbor = neighbors.includes("X");
|
||||
|
||||
if (isValidMove && isNotReservedSpace && hasFriendlyNeighbor) {
|
||||
moveOptions.push([x, y]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Choose one of the found moves at random
|
||||
const randomIndex = Math.floor(Math.random() * moveOptions.length);
|
||||
return moveOptions[randomIndex] ?? [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Find all adjacent points in the four connected directions
|
||||
*/
|
||||
const getNeighbors = (board, x, y) => {
|
||||
const north = board[x + 1]?.[y];
|
||||
const east = board[x][y + 1];
|
||||
const south = board[x - 1]?.[y];
|
||||
const west = board[x]?.[y - 1];
|
||||
|
||||
return [north, east, south, west];
|
||||
};
|
||||
@@ -0,0 +1,811 @@
|
||||
import {
|
||||
BoardState,
|
||||
EyeMove,
|
||||
Move,
|
||||
MoveOptions,
|
||||
opponentDetails,
|
||||
opponents,
|
||||
PlayerColor,
|
||||
playerColors,
|
||||
playTypes,
|
||||
PointState,
|
||||
} from "../boardState/goConstants";
|
||||
import { findNeighbors, floor, isDefined, isNotNull, passTurn } from "../boardState/boardState";
|
||||
import {
|
||||
evaluateIfMoveIsValid,
|
||||
evaluateMoveResult,
|
||||
findEffectiveLibertiesOfNewMove,
|
||||
findEnemyNeighborChainWithFewestLiberties,
|
||||
findMinLibertyCountOfAdjacentChains,
|
||||
getAllChains,
|
||||
getAllEyes,
|
||||
getAllEyesByChainId,
|
||||
getAllNeighboringChains,
|
||||
getAllValidMoves,
|
||||
} from "./boardAnalysis";
|
||||
import { findDisputedTerritory } from "./controlledTerritory";
|
||||
import { findAnyMatchedPatterns } from "./patternMatching";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
import { Player } from "@player";
|
||||
import { AugmentationName } from "@enums";
|
||||
|
||||
/*
|
||||
Basic GO AIs, each with some personality and weaknesses
|
||||
|
||||
The AIs are aware of chains of connected pieces, their liberties, and their eyes.
|
||||
They know how to lok for moves that capture or threaten capture, moves that create eyes, and moves that take
|
||||
away liberties from their opponent, as well as some pattern matching on strong move ideas.
|
||||
|
||||
They do not know about larger jump moves, nor about frameworks on the board. Also, they each have a tendancy to
|
||||
over-focus on a different type of move, giving each AI a different playstyle and weakness to exploit.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Finds an array of potential moves based on the current board state, then chooses one
|
||||
* based on the given opponent's personality and preferences. If no preference is given by the AI,
|
||||
* will choose one from the reasonable moves at random.
|
||||
*
|
||||
* @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) {
|
||||
await sleep(200);
|
||||
const rng = new WHRNG(rngOverride || Player.totalPlaytime);
|
||||
const smart = isSmart(opponent, rng.random());
|
||||
const moves = await getMoveOptions(boardState, player, rng.random(), smart);
|
||||
|
||||
const priorityMove = getFactionMove(moves, opponent, rng.random());
|
||||
if (priorityMove) {
|
||||
return {
|
||||
type: playTypes.move,
|
||||
x: priorityMove.x,
|
||||
y: priorityMove.y,
|
||||
};
|
||||
}
|
||||
|
||||
// If no priority move is chosen, pick one of the reasonable moves
|
||||
const moveOptions = [
|
||||
moves.growth?.point,
|
||||
moves.surround?.point,
|
||||
moves.defend?.point,
|
||||
moves.expansion?.point,
|
||||
moves.pattern,
|
||||
moves.eyeMove?.point,
|
||||
moves.eyeBlock?.point,
|
||||
]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined)
|
||||
.filter((point) => evaluateIfMoveIsValid(boardState, point.x, point.y, player, false));
|
||||
|
||||
const chosenMove = moveOptions[floor(rng.random() * moveOptions.length)];
|
||||
|
||||
if (chosenMove) {
|
||||
await sleep(200);
|
||||
console.debug(`Non-priority move chosen: ${chosenMove.x} ${chosenMove.y}`);
|
||||
return {
|
||||
type: playTypes.move,
|
||||
x: chosenMove.x,
|
||||
y: chosenMove.y,
|
||||
};
|
||||
} else {
|
||||
console.debug("No valid moves found");
|
||||
return handleNoMoveFound(boardState, player);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if the AI is merely passing their turn, or if the game should end.
|
||||
*
|
||||
* Ends the game if the player passed on the previous turn before the AI passes,
|
||||
* or if the player will be forced to pass their next turn after the AI passes.
|
||||
*/
|
||||
function handleNoMoveFound(boardState: BoardState, player: playerColors) {
|
||||
passTurn(boardState, player);
|
||||
const opposingPlayer = player === playerColors.white ? playerColors.black : playerColors.white;
|
||||
const remainingTerritory = getAllValidMoves(boardState, opposingPlayer).length;
|
||||
if (remainingTerritory > 0 && boardState.passCount < 2) {
|
||||
return {
|
||||
type: playTypes.pass,
|
||||
x: -1,
|
||||
y: -1,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: playTypes.gameOver,
|
||||
x: -1,
|
||||
y: -1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a group of move options, chooses one based on the given opponent's personality (if any fit their priorities)
|
||||
*/
|
||||
function getFactionMove(moves: MoveOptions, faction: opponents, rng: number): PointState | null {
|
||||
if (faction === opponents.Netburners) {
|
||||
return getNetburnersPriorityMove(moves, rng);
|
||||
}
|
||||
if (faction === opponents.SlumSnakes) {
|
||||
return getSlumSnakesPriorityMove(moves, rng);
|
||||
}
|
||||
if (faction === opponents.TheBlackHand) {
|
||||
return getBlackHandPriorityMove(moves, rng);
|
||||
}
|
||||
if (faction === opponents.Tetrads) {
|
||||
return getTetradPriorityMove(moves, rng);
|
||||
}
|
||||
if (faction === opponents.Daedalus) {
|
||||
return getDaedalusPriorityMove(moves, rng);
|
||||
}
|
||||
|
||||
return getIlluminatiPriorityMove(moves, rng);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if certain failsafes and mistake avoidance are enabled for the given move
|
||||
*/
|
||||
function isSmart(faction: opponents, rng: number) {
|
||||
if (faction === opponents.Netburners) {
|
||||
return false;
|
||||
}
|
||||
if (faction === opponents.SlumSnakes) {
|
||||
return rng < 0.3;
|
||||
}
|
||||
if (faction === opponents.TheBlackHand) {
|
||||
return rng < 0.8;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Netburners mostly just put random points around the board, but occasionally have a smart move
|
||||
*/
|
||||
function getNetburnersPriorityMove(moves: MoveOptions, rng: number): PointState | null {
|
||||
if (rng < 0.2) {
|
||||
return getIlluminatiPriorityMove(moves, rng);
|
||||
} else if (rng < 0.4 && moves.expansion) {
|
||||
return moves.expansion.point;
|
||||
} else if (rng < 0.6 && moves.growth) {
|
||||
return moves.growth.point;
|
||||
} else if (rng < 0.75) {
|
||||
return moves.random;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slum snakes prioritize defending their pieces and building chains that snake around as much of the bord as possible.
|
||||
*/
|
||||
function getSlumSnakesPriorityMove(moves: MoveOptions, rng: number): PointState | null {
|
||||
if (moves.defendCapture) {
|
||||
console.debug("defend capture: defend move chosen");
|
||||
return moves.defendCapture.point;
|
||||
}
|
||||
|
||||
if (rng < 0.2) {
|
||||
return getIlluminatiPriorityMove(moves, rng);
|
||||
} else if (rng < 0.6 && moves.growth) {
|
||||
return moves.growth.point;
|
||||
} else if (rng < 0.65) {
|
||||
return moves.random;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Black hand just wants to smOrk. They always capture or smother the opponent if possible.
|
||||
*/
|
||||
function getBlackHandPriorityMove(moves: MoveOptions, rng: number): PointState | null {
|
||||
if (moves.capture) {
|
||||
console.debug("capture: capture move chosen");
|
||||
return moves.capture.point;
|
||||
}
|
||||
|
||||
if (moves.surround && moves.surround.point && (moves.surround?.newLibertyCount ?? 999) <= 1) {
|
||||
console.debug("surround move chosen");
|
||||
return moves.surround.point;
|
||||
}
|
||||
|
||||
if (moves.defendCapture) {
|
||||
console.debug("defend capture: defend move chosen");
|
||||
return moves.defendCapture.point;
|
||||
}
|
||||
|
||||
if (moves.surround && moves.surround.point && (moves.surround?.newLibertyCount ?? 999) <= 2) {
|
||||
console.debug("surround move chosen");
|
||||
return moves.surround.point;
|
||||
}
|
||||
|
||||
if (rng < 0.3) {
|
||||
return getIlluminatiPriorityMove(moves, rng);
|
||||
} else if (rng < 0.75 && moves.surround) {
|
||||
return moves.surround.point;
|
||||
} else if (rng < 0.8) {
|
||||
return moves.random;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tetrads really like to be up close and personal, cutting and circling their opponent
|
||||
*/
|
||||
function getTetradPriorityMove(moves: MoveOptions, rng: number) {
|
||||
if (moves.capture) {
|
||||
console.debug("capture: capture move chosen");
|
||||
return moves.capture.point;
|
||||
}
|
||||
|
||||
if (moves.defendCapture) {
|
||||
console.debug("defend capture: defend move chosen");
|
||||
return moves.defendCapture.point;
|
||||
}
|
||||
|
||||
if (moves.pattern) {
|
||||
console.debug("pattern match move chosen");
|
||||
return moves.pattern;
|
||||
}
|
||||
|
||||
if (moves.surround && moves.surround.point && (moves.surround?.newLibertyCount ?? 9) <= 1) {
|
||||
console.debug("surround move chosen");
|
||||
return moves.surround.point;
|
||||
}
|
||||
|
||||
if (rng < 0.4) {
|
||||
return getIlluminatiPriorityMove(moves, rng);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Daedalus almost always picks the Illuminati move, but very occasionally gets distracted.
|
||||
*/
|
||||
function getDaedalusPriorityMove(moves: MoveOptions, rng: number): PointState | null {
|
||||
if (rng < 0.9) {
|
||||
return getIlluminatiPriorityMove(moves, rng);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* First prioritizes capturing of opponent pieces.
|
||||
* Then, preventing capture of their own pieces.
|
||||
* Then, creating "eyes" to solidify their control over the board
|
||||
* Then, finding opportunities to capture on their next move
|
||||
* Then, blocking the opponent's attempts to create eyes
|
||||
* Finally, will match any of the predefined local patterns indicating a strong move.
|
||||
*/
|
||||
function getIlluminatiPriorityMove(moves: MoveOptions, rng: number): PointState | null {
|
||||
if (moves.capture) {
|
||||
console.debug("capture: capture move chosen");
|
||||
return moves.capture.point;
|
||||
}
|
||||
|
||||
if (moves.defendCapture) {
|
||||
console.debug("defend capture: defend move chosen");
|
||||
return moves.defendCapture.point;
|
||||
}
|
||||
|
||||
if (moves.eyeMove) {
|
||||
console.debug("Create eye move chosen");
|
||||
return moves.eyeMove.point;
|
||||
}
|
||||
|
||||
if (moves.surround && moves.surround.point && (moves.surround?.newLibertyCount ?? 9) <= 1) {
|
||||
console.debug("surround move chosen");
|
||||
return moves.surround.point;
|
||||
}
|
||||
|
||||
if (moves.eyeBlock) {
|
||||
console.debug("Block eye move chosen");
|
||||
return moves.eyeBlock.point;
|
||||
}
|
||||
|
||||
if (moves.corner) {
|
||||
console.debug("Corner move chosen");
|
||||
return moves.corner;
|
||||
}
|
||||
|
||||
const hasMoves = [moves.eyeMove, moves.eyeBlock, moves.growth, moves.defend, moves.surround].filter((m) => m).length;
|
||||
const usePattern = rng > 0.25 || !hasMoves;
|
||||
|
||||
if (moves.pattern && usePattern) {
|
||||
console.debug("pattern match move chosen");
|
||||
return moves.pattern;
|
||||
}
|
||||
|
||||
if (rng > 0.4 && moves.jump) {
|
||||
console.debug("Jump move chosen");
|
||||
return moves.jump.point;
|
||||
}
|
||||
|
||||
if (rng < 0.6 && moves.surround && moves.surround.point && (moves.surround?.newLibertyCount ?? 9) <= 2) {
|
||||
console.debug("surround move chosen");
|
||||
return moves.surround.point;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a move that places a piece to influence (and later control) a corner
|
||||
*/
|
||||
function getCornerMove(boardState: BoardState) {
|
||||
const boardEdge = boardState.board[0].length - 1;
|
||||
const cornerMax = boardEdge - 2;
|
||||
if (isCornerAvailableForMove(boardState, cornerMax, cornerMax, boardEdge, boardEdge)) {
|
||||
return boardState.board[cornerMax][cornerMax];
|
||||
}
|
||||
if (isCornerAvailableForMove(boardState, 0, cornerMax, cornerMax, boardEdge)) {
|
||||
return boardState.board[2][cornerMax];
|
||||
}
|
||||
if (isCornerAvailableForMove(boardState, 0, 0, 2, 2)) {
|
||||
return boardState.board[2][2];
|
||||
}
|
||||
if (isCornerAvailableForMove(boardState, cornerMax, 0, boardEdge, 2)) {
|
||||
return boardState.board[cornerMax][2];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all non-offline nodes in a given area
|
||||
*/
|
||||
function findLiveNodesInArea(boardState: BoardState, x1: number, y1: number, x2: number, y2: number) {
|
||||
const foundPoints: PointState[] = [];
|
||||
boardState.board.forEach((column) =>
|
||||
column.forEach(
|
||||
(point) => point && point.x >= x1 && point.x <= x2 && point.y >= y1 && point.y <= y2 && foundPoints.push(point),
|
||||
),
|
||||
);
|
||||
return foundPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a corner is largely intact and currently empty, and thus a good target for corner takeover moves
|
||||
*/
|
||||
function isCornerAvailableForMove(boardState: BoardState, x1: number, y1: number, x2: number, y2: number) {
|
||||
const foundPoints = findLiveNodesInArea(boardState, x1, y1, x2, y2);
|
||||
const foundPieces = foundPoints.filter((point) => point.player !== playerColors.empty);
|
||||
return foundPoints.length >= 7 ? foundPieces.length === 0 : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a move from the list of open-area moves
|
||||
*/
|
||||
function getExpansionMove(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
rng: number,
|
||||
moveArray?: Move[],
|
||||
) {
|
||||
const moveOptions = moveArray ?? getExpansionMoveArray(boardState, player, availableSpaces);
|
||||
const randomIndex = floor(rng * moveOptions.length);
|
||||
return moveOptions[randomIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a move in open space that is nearby a friendly piece
|
||||
*/
|
||||
function getJumpMove(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
rng: number,
|
||||
moveArray?: Move[],
|
||||
) {
|
||||
const board = boardState.board;
|
||||
const moveOptions = (moveArray ?? getExpansionMoveArray(boardState, player, availableSpaces)).filter(({ point }) =>
|
||||
[
|
||||
board[point.x]?.[point.y + 2],
|
||||
board[point.x + 2]?.[point.y],
|
||||
board[point.x]?.[point.y - 2],
|
||||
board[point.x - 2]?.[point.y],
|
||||
].some((point) => point?.player === player),
|
||||
);
|
||||
|
||||
const randomIndex = floor(rng * moveOptions.length);
|
||||
return moveOptions[randomIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a move in an open area to expand influence and later build on
|
||||
*/
|
||||
export function getExpansionMoveArray(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
): Move[] {
|
||||
// Look for any empty spaces fully surrounded by empty spaces to expand into
|
||||
const emptySpaces = availableSpaces.filter((space) => {
|
||||
const neighbors = findNeighbors(boardState, space.x, space.y);
|
||||
return (
|
||||
[neighbors.north, neighbors.east, neighbors.south, neighbors.west].filter(
|
||||
(point) => point && point.player === playerColors.empty,
|
||||
).length === 4
|
||||
);
|
||||
});
|
||||
|
||||
// Once no such empty areas exist anymore, instead expand into any disputed territory
|
||||
// to gain a few more points in endgame
|
||||
const disputedSpaces = emptySpaces.length ? [] : getDisputedTerritoryMoves(boardState, player, availableSpaces, 1);
|
||||
|
||||
const moveOptions = [...emptySpaces, ...disputedSpaces];
|
||||
|
||||
return moveOptions.map((point) => {
|
||||
return {
|
||||
point: point,
|
||||
newLibertyCount: -1,
|
||||
oldLibertyCount: -1,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getDisputedTerritoryMoves(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
maxChainSize = 99,
|
||||
) {
|
||||
const chains = getAllChains(boardState).filter((chain) => chain.length <= maxChainSize);
|
||||
|
||||
return availableSpaces.filter((space) => {
|
||||
const chain = chains.find((chain) => chain[0].chain === space.chain) ?? [];
|
||||
const playerNeighbors = getAllNeighboringChains(boardState, chain, chains);
|
||||
const hasWhitePieceNeighbor = playerNeighbors.find(
|
||||
(neighborChain) => neighborChain[0]?.player === playerColors.white,
|
||||
);
|
||||
const hasBlackPieceNeighbor = playerNeighbors.find(
|
||||
(neighborChain) => neighborChain[0]?.player === playerColors.black,
|
||||
);
|
||||
|
||||
return hasWhitePieceNeighbor && hasBlackPieceNeighbor;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all moves that increases the liberties of the player's pieces, making them harder to capture and occupy more space on the board.
|
||||
*/
|
||||
async function getLibertyGrowthMoves(boardState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
|
||||
const friendlyChains = getAllChains(boardState).filter((chain) => chain[0].player === player);
|
||||
|
||||
if (!friendlyChains.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get all liberties of friendly chains as potential growth move options
|
||||
const liberties = friendlyChains
|
||||
.map((chain) =>
|
||||
chain[0].liberties?.filter(isNotNull).map((liberty) => ({
|
||||
libertyPoint: liberty,
|
||||
oldLibertyCount: chain[0].liberties?.length,
|
||||
})),
|
||||
)
|
||||
.flat()
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined)
|
||||
.filter((liberty) =>
|
||||
availableSpaces.find((point) => liberty.libertyPoint.x === point.x && liberty.libertyPoint.y === point.y),
|
||||
);
|
||||
|
||||
// Find a liberty where playing a piece increases the liberty of the chain (aka expands or defends the chain)
|
||||
return liberties
|
||||
.map((liberty) => {
|
||||
const move = liberty.libertyPoint;
|
||||
|
||||
const newLibertyCount = findEffectiveLibertiesOfNewMove(boardState, move.x, move.y, player).length;
|
||||
|
||||
// Get the smallest liberty count of connected chains to represent the old state
|
||||
const oldLibertyCount = findMinLibertyCountOfAdjacentChains(boardState, move.x, move.y, player);
|
||||
|
||||
return {
|
||||
point: move,
|
||||
oldLibertyCount: oldLibertyCount,
|
||||
newLibertyCount: newLibertyCount,
|
||||
};
|
||||
})
|
||||
.filter((move) => move.newLibertyCount > 1 && move.newLibertyCount >= move.oldLibertyCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a move that increases the player's liberties by the maximum amount
|
||||
*/
|
||||
async function getGrowthMove(
|
||||
initialState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
rng: number,
|
||||
) {
|
||||
const growthMoves = await getLibertyGrowthMoves(initialState, player, availableSpaces);
|
||||
|
||||
const maxLibertyCount = Math.max(...growthMoves.map((l) => l.newLibertyCount - l.oldLibertyCount));
|
||||
|
||||
const moveCandidates = growthMoves.filter((l) => l.newLibertyCount - l.oldLibertyCount === maxLibertyCount);
|
||||
return moveCandidates[floor(rng * moveCandidates.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a move that specifically increases a chain's liberties from 1 to more than 1, preventing capture
|
||||
*/
|
||||
async function getDefendMove(initialState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
|
||||
const growthMoves = await getLibertyGrowthMoves(initialState, player, availableSpaces);
|
||||
const libertyIncreases =
|
||||
growthMoves?.filter((move) => move.oldLibertyCount <= 1 && move.newLibertyCount > move.oldLibertyCount) ?? [];
|
||||
|
||||
const maxLibertyCount = Math.max(...libertyIncreases.map((l) => l.newLibertyCount - l.oldLibertyCount));
|
||||
|
||||
if (maxLibertyCount < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const moveCandidates = libertyIncreases.filter((l) => l.newLibertyCount - l.oldLibertyCount === maxLibertyCount);
|
||||
return moveCandidates[floor(Math.random() * moveCandidates.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a move that reduces the opponent's liberties as much as possible,
|
||||
* capturing (or making it easier to capture) their pieces
|
||||
*/
|
||||
async function getSurroundMove(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
smart = true,
|
||||
) {
|
||||
const opposingPlayer = player === playerColors.black ? playerColors.white : playerColors.black;
|
||||
const enemyChains = getAllChains(boardState).filter((chain) => chain[0].player === opposingPlayer);
|
||||
|
||||
if (!enemyChains.length || !availableSpaces.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enemyLiberties = enemyChains
|
||||
.map((chain) => chain[0].liberties)
|
||||
.flat()
|
||||
.filter((liberty) => availableSpaces.find((point) => liberty?.x === point.x && liberty?.y === point.y))
|
||||
.filter(isNotNull);
|
||||
|
||||
const captureMoves: Move[] = [];
|
||||
const atariMoves: Move[] = [];
|
||||
const surroundMoves: Move[] = [];
|
||||
|
||||
enemyLiberties.forEach((move) => {
|
||||
const newLibertyCount = findEffectiveLibertiesOfNewMove(boardState, move.x, move.y, player).length;
|
||||
|
||||
const weakestEnemyChain = findEnemyNeighborChainWithFewestLiberties(
|
||||
boardState,
|
||||
move.x,
|
||||
move.y,
|
||||
player === playerColors.black ? playerColors.white : playerColors.black,
|
||||
);
|
||||
const weakestEnemyChainLength = weakestEnemyChain?.length ?? 99;
|
||||
|
||||
const enemyChainLibertyCount = weakestEnemyChain?.[0]?.liberties?.length ?? 99;
|
||||
|
||||
const enemyLibertyGroups = [
|
||||
...(weakestEnemyChain?.[0]?.liberties ?? []).reduce(
|
||||
(chainIDs, point) => chainIDs.add(point?.chain ?? ""),
|
||||
new Set<string>(),
|
||||
),
|
||||
];
|
||||
|
||||
// Do not suggest moves that do not capture anything and let your opponent immediately capture
|
||||
if (newLibertyCount <= 2 && enemyChainLibertyCount > 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If a neighboring enemy chain has only one liberty, the current move suggestion will capture
|
||||
if (enemyChainLibertyCount <= 1) {
|
||||
captureMoves.push({
|
||||
point: move,
|
||||
oldLibertyCount: enemyChainLibertyCount,
|
||||
newLibertyCount: enemyChainLibertyCount - 1,
|
||||
});
|
||||
}
|
||||
|
||||
// If the move puts the enemy chain in threat of capture, it forces the opponent to respond.
|
||||
// Only do this if your piece cannot be captured, or if the enemy group is surrounded and vulnerable to losing its only interior space
|
||||
else if (
|
||||
enemyChainLibertyCount === 2 &&
|
||||
(newLibertyCount >= 2 || (enemyLibertyGroups.length === 1 && weakestEnemyChainLength > 3) || !smart)
|
||||
) {
|
||||
atariMoves.push({
|
||||
point: move,
|
||||
oldLibertyCount: enemyChainLibertyCount,
|
||||
newLibertyCount: enemyChainLibertyCount - 1,
|
||||
});
|
||||
}
|
||||
|
||||
// If the move will not immediately get re-captured, and limit's the opponent's liberties
|
||||
else if (newLibertyCount >= 2) {
|
||||
surroundMoves.push({
|
||||
point: move,
|
||||
oldLibertyCount: enemyChainLibertyCount,
|
||||
newLibertyCount: enemyChainLibertyCount - 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return [...captureMoves, ...atariMoves, ...surroundMoves][0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all moves that would create an eye for the given player.
|
||||
*
|
||||
* An "eye" is empty point(s) completely surrounded by a single player's connected pieces.
|
||||
* If a chain has multiple eyes, it cannot be captured by the opponent (since they can only fill one eye at a time,
|
||||
* and suiciding your own pieces is not legal unless it captures the opponents' first)
|
||||
*/
|
||||
function getEyeCreationMoves(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
maxLiberties = 99,
|
||||
) {
|
||||
const allEyes = getAllEyesByChainId(boardState, player);
|
||||
const currentEyes = getAllEyes(boardState, player, allEyes);
|
||||
|
||||
const currentLivingGroupIDs = Object.keys(allEyes).filter((chainId) => allEyes[chainId].length >= 2);
|
||||
const currentLivingGroupsCount = currentLivingGroupIDs.length;
|
||||
const currentEyeCount = currentEyes.filter((eye) => eye.length).length;
|
||||
|
||||
const chains = getAllChains(boardState);
|
||||
const friendlyLiberties = chains
|
||||
.filter((chain) => chain[0].player === player)
|
||||
.filter((chain) => chain.length > 1)
|
||||
.filter((chain) => chain[0].liberties && chain[0].liberties?.length <= maxLiberties)
|
||||
.filter((chain) => !currentLivingGroupIDs.includes(chain[0].chain))
|
||||
.map((chain) => chain[0].liberties)
|
||||
.flat()
|
||||
.filter(isNotNull)
|
||||
.filter((point) =>
|
||||
availableSpaces.find((availablePoint) => availablePoint.x === point.x && availablePoint.y === point.y),
|
||||
)
|
||||
.filter((point: PointState) => {
|
||||
console.warn("eye check ", point.x, point.y);
|
||||
const neighbors = findNeighbors(boardState, point.x, point.y);
|
||||
const neighborhood = [neighbors.north, neighbors.east, neighbors.south, neighbors.west];
|
||||
return (
|
||||
neighborhood.filter((point) => !point || point?.player === player).length >= 2 &&
|
||||
neighborhood.some((point) => point?.player === playerColors.empty)
|
||||
);
|
||||
});
|
||||
|
||||
const eyeCreationMoves = friendlyLiberties.reduce((moveOptions: EyeMove[], point: PointState) => {
|
||||
const evaluationBoard = evaluateMoveResult(boardState, point.x, point.y, player);
|
||||
const newEyes = getAllEyes(evaluationBoard, player);
|
||||
const newLivingGroupsCount = newEyes.filter((eye) => eye.length >= 2).length;
|
||||
const newEyeCount = newEyes.filter((eye) => eye.length).length;
|
||||
if (
|
||||
newLivingGroupsCount > currentLivingGroupsCount ||
|
||||
(newEyeCount > currentEyeCount && newLivingGroupsCount === currentLivingGroupsCount)
|
||||
) {
|
||||
moveOptions.push({
|
||||
point: point,
|
||||
createsLife: newLivingGroupsCount > currentLivingGroupsCount,
|
||||
});
|
||||
}
|
||||
return moveOptions;
|
||||
}, []);
|
||||
|
||||
return eyeCreationMoves.sort((moveA, moveB) => +moveB.createsLife - +moveA.createsLife);
|
||||
}
|
||||
|
||||
function getEyeCreationMove(boardState: BoardState, player: PlayerColor, availableSpaces: PointState[]) {
|
||||
return getEyeCreationMoves(boardState, player, availableSpaces)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is only one move that would create two eyes for the opponent, it should be blocked if possible
|
||||
*/
|
||||
function getEyeBlockingMove(boardState: BoardState, player: PlayerColor, availablePoints: PointState[]) {
|
||||
const opposingPlayer = player === playerColors.white ? playerColors.black : playerColors.white;
|
||||
const opponentEyeMoves = getEyeCreationMoves(boardState, opposingPlayer, availablePoints, 5);
|
||||
const twoEyeMoves = opponentEyeMoves.filter((move) => move.createsLife);
|
||||
const oneEyeMoves = opponentEyeMoves.filter((move) => !move.createsLife);
|
||||
|
||||
if (twoEyeMoves.length === 1) {
|
||||
return twoEyeMoves[0];
|
||||
}
|
||||
if (!twoEyeMoves.length && oneEyeMoves.length === 1) {
|
||||
return oneEyeMoves[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a group of reasonable moves based on the current board state, to be passed to the factions' AI to decide on
|
||||
*/
|
||||
async function getMoveOptions(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
rng: number,
|
||||
smart = true,
|
||||
): Promise<MoveOptions> {
|
||||
const availableSpaces = findDisputedTerritory(boardState, player, smart);
|
||||
const contestedPoints = getDisputedTerritoryMoves(boardState, player, availableSpaces);
|
||||
const expansionMoves = getExpansionMoveArray(boardState, player, availableSpaces);
|
||||
|
||||
// If the player is passing, and all territory is surrounded by a single color: do not suggest moves that
|
||||
// needlessly extend the game, unless they actually can change the score
|
||||
const endGameAvailable = !contestedPoints.length && boardState.passCount;
|
||||
|
||||
const growthMove = endGameAvailable ? null : await getGrowthMove(boardState, player, availableSpaces, rng);
|
||||
await sleep(80);
|
||||
const expansionMove = await getExpansionMove(boardState, player, availableSpaces, rng, expansionMoves);
|
||||
await sleep(80);
|
||||
const jumpMove = await getJumpMove(boardState, player, availableSpaces, rng, expansionMoves);
|
||||
await sleep(80);
|
||||
const defendMove = await getDefendMove(boardState, player, availableSpaces);
|
||||
await sleep(80);
|
||||
const surroundMove = await getSurroundMove(boardState, player, availableSpaces, smart);
|
||||
await sleep(80);
|
||||
const eyeMove = endGameAvailable ? null : getEyeCreationMove(boardState, player, availableSpaces);
|
||||
await sleep(80);
|
||||
const eyeBlock = endGameAvailable ? null : getEyeBlockingMove(boardState, player, availableSpaces);
|
||||
await sleep(80);
|
||||
const cornerMove = getCornerMove(boardState);
|
||||
const pattern = endGameAvailable
|
||||
? null
|
||||
: await findAnyMatchedPatterns(boardState, player, availableSpaces, smart, rng);
|
||||
|
||||
// Only offer a random move if there are some contested spaces on the board.
|
||||
// (Random move should not be picked if the AI would otherwise pass turn.)
|
||||
const random = contestedPoints.length ? availableSpaces[floor(rng * availableSpaces.length)] : null;
|
||||
|
||||
const captureMove = surroundMove && surroundMove?.newLibertyCount === 0 ? surroundMove : null;
|
||||
const defendCaptureMove =
|
||||
defendMove && defendMove.oldLibertyCount == 1 && defendMove?.newLibertyCount > 1 ? defendMove : null;
|
||||
|
||||
console.debug("---------------------");
|
||||
console.debug("capture: ", captureMove?.point?.x, captureMove?.point?.y);
|
||||
console.debug("defendCapture: ", defendCaptureMove?.point?.x, defendCaptureMove?.point?.y);
|
||||
console.debug("eyeMove: ", eyeMove?.point?.x, eyeMove?.point?.y);
|
||||
console.debug("eyeBlock: ", eyeBlock?.point?.x, eyeBlock?.point?.y);
|
||||
console.debug("pattern: ", pattern?.x, pattern?.y);
|
||||
console.debug("surround: ", surroundMove?.point?.x, surroundMove?.point?.y);
|
||||
console.debug("defend: ", defendMove?.point?.x, defendMove?.point?.y);
|
||||
console.debug("Growth: ", growthMove?.point?.x, growthMove?.point?.y);
|
||||
console.debug("Expansion: ", expansionMove?.point?.x, expansionMove?.point?.y);
|
||||
console.debug("Jump: ", jumpMove?.point?.x, jumpMove?.point?.y);
|
||||
console.debug("Corner: ", cornerMove?.x, cornerMove?.y);
|
||||
console.debug("Random: ", random?.x, random?.y);
|
||||
|
||||
return {
|
||||
capture: captureMove,
|
||||
defendCapture: defendCaptureMove,
|
||||
eyeMove: eyeMove,
|
||||
eyeBlock: eyeBlock,
|
||||
pattern: pattern,
|
||||
growth: growthMove,
|
||||
expansion: expansionMove,
|
||||
jump: jumpMove,
|
||||
defend: defendMove,
|
||||
surround: surroundMove,
|
||||
corner: cornerMove,
|
||||
random: random,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the starting score for white.
|
||||
*/
|
||||
export function getKomi(opponent: opponents) {
|
||||
return opponentDetails[opponent].komi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows time to pass
|
||||
*/
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function showWorldDemon() {
|
||||
return Player.augmentations.some((a) => a.name === AugmentationName.TheRedPill) && Player.sourceFileLvl(1);
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
// Inspired by https://github.com/pasky/michi/blob/master/michi.py
|
||||
import { BoardState, PlayerColor, playerColors, PointState } from "../boardState/goConstants";
|
||||
import { sleep } from "./goAI";
|
||||
import { findEffectiveLibertiesOfNewMove } from "./boardAnalysis";
|
||||
import { floor } from "../boardState/boardState";
|
||||
|
||||
export const threeByThreePatterns = [
|
||||
// 3x3 piece patterns; X,O are color pieces; x,o are any state except the opposite color piece;
|
||||
// " " is off the edge of the board; "?" is any state (even off the board)
|
||||
[
|
||||
"XOX", // hane pattern - enclosing hane
|
||||
"...",
|
||||
"???",
|
||||
],
|
||||
[
|
||||
"XO.", // hane pattern - non-cutting hane
|
||||
"...",
|
||||
"?.?",
|
||||
],
|
||||
[
|
||||
"XO?", // hane pattern - magari
|
||||
"X..",
|
||||
"o.?",
|
||||
],
|
||||
[
|
||||
".O.", // generic pattern - katatsuke or diagonal attachment; similar to magari
|
||||
"X..",
|
||||
"...",
|
||||
],
|
||||
[
|
||||
"XO?", // cut1 pattern (kiri] - unprotected cut
|
||||
"O.x",
|
||||
"?x?",
|
||||
],
|
||||
[
|
||||
"XO?", // cut1 pattern (kiri] - peeped cut
|
||||
"O.X",
|
||||
"???",
|
||||
],
|
||||
[
|
||||
"?X?", // cut2 pattern (de]
|
||||
"O.O",
|
||||
"xxx",
|
||||
],
|
||||
[
|
||||
"OX?", // cut keima
|
||||
"x.O",
|
||||
"???",
|
||||
],
|
||||
[
|
||||
"X.?", // side pattern - chase
|
||||
"O.?",
|
||||
" ",
|
||||
],
|
||||
[
|
||||
"OX?", // side pattern - block side cut
|
||||
"X.O",
|
||||
" ",
|
||||
],
|
||||
[
|
||||
"?X?", // side pattern - block side connection
|
||||
"o.O",
|
||||
" ",
|
||||
],
|
||||
[
|
||||
"?XO", // side pattern - sagari
|
||||
"o.o",
|
||||
" ",
|
||||
],
|
||||
[
|
||||
"?OX", // side pattern - cut
|
||||
"X.O",
|
||||
" ",
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Searches the board for any point that matches the expanded pattern set
|
||||
*/
|
||||
export async function findAnyMatchedPatterns(
|
||||
boardState: BoardState,
|
||||
player: PlayerColor,
|
||||
availableSpaces: PointState[],
|
||||
smart = true,
|
||||
rng: number,
|
||||
) {
|
||||
const board = boardState.board;
|
||||
const boardSize = board[0].length;
|
||||
const patterns = expandAllThreeByThreePatterns();
|
||||
const moves = [];
|
||||
for (let x = 0; x < boardSize; x++) {
|
||||
for (let y = 0; y < boardSize; y++) {
|
||||
const neighborhood = getNeighborhood(boardState, x, y);
|
||||
const matchedPattern = patterns.find((pattern) => checkMatch(neighborhood, pattern, player));
|
||||
|
||||
if (
|
||||
matchedPattern &&
|
||||
availableSpaces.find((availablePoint) => availablePoint.x === x && availablePoint.y === y) &&
|
||||
(!smart || findEffectiveLibertiesOfNewMove(boardState, x, y, player).length > 1)
|
||||
) {
|
||||
moves.push(board[x][y]);
|
||||
}
|
||||
}
|
||||
await sleep(10);
|
||||
}
|
||||
return moves[floor(rng * moves.length)] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
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) {
|
||||
const patternArr = pattern.join("").split("");
|
||||
const neighborhoodArray = neighborhood.flat();
|
||||
return patternArr.every((str, index) => matches(str, neighborhoodArray[index], player));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the 8 points adjacent and diagonally adjacent to the given point
|
||||
*/
|
||||
function getNeighborhood(boardState: BoardState, x: number, y: number) {
|
||||
const board = boardState.board;
|
||||
return [
|
||||
[board[x - 1]?.[y - 1], board[x - 1]?.[y], board[x - 1]?.[y + 1]],
|
||||
[board[x]?.[y - 1], board[x]?.[y], board[x]?.[y + 1]],
|
||||
[board[x + 1]?.[y - 1], board[x + 1]?.[y], board[x + 1]?.[y + 1]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if the given point matches the given string representation, false otherwise
|
||||
*
|
||||
* Capital X and O only match stones of that color
|
||||
* lowercase x and o match stones of that color, or empty space, or the edge of the board
|
||||
* a period "." only matches empty nodes
|
||||
* A space " " only matches the edge of the board
|
||||
* question mark "?" matches anything
|
||||
*/
|
||||
function matches(stringPoint: string, point: PointState | null, player: PlayerColor) {
|
||||
const opponent = player === playerColors.white ? playerColors.black : playerColors.white;
|
||||
switch (stringPoint) {
|
||||
case "X": {
|
||||
return point?.player === player;
|
||||
}
|
||||
case "O": {
|
||||
return point?.player === opponent;
|
||||
}
|
||||
case "x": {
|
||||
return point?.player !== opponent;
|
||||
}
|
||||
case "o": {
|
||||
return point?.player !== player;
|
||||
}
|
||||
case ".": {
|
||||
return point?.player === playerColors.empty;
|
||||
}
|
||||
case " ": {
|
||||
return point === null;
|
||||
}
|
||||
case "?": {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all variations of the pattern list, by expanding it using rotation and mirroring
|
||||
*/
|
||||
function expandAllThreeByThreePatterns() {
|
||||
const rotatedPatterns = [
|
||||
...threeByThreePatterns,
|
||||
...threeByThreePatterns.map(rotate90Degrees),
|
||||
...threeByThreePatterns.map(rotate90Degrees).map(rotate90Degrees),
|
||||
...threeByThreePatterns.map(rotate90Degrees).map(rotate90Degrees).map(rotate90Degrees),
|
||||
];
|
||||
const mirroredPatterns = [...rotatedPatterns, ...rotatedPatterns.map(verticalMirror)];
|
||||
return [...mirroredPatterns, ...mirroredPatterns.map(horizontalMirror)];
|
||||
}
|
||||
|
||||
function rotate90Degrees(pattern: string[]) {
|
||||
return [
|
||||
`${pattern[2][0]}${pattern[1][0]}${pattern[0][0]}`,
|
||||
`${pattern[2][1]}${pattern[1][1]}${pattern[0][1]}`,
|
||||
`${pattern[2][2]}${pattern[1][2]}${pattern[0][2]}`,
|
||||
];
|
||||
}
|
||||
|
||||
function verticalMirror(pattern: string[]) {
|
||||
return [pattern[2], pattern[1], pattern[0]];
|
||||
}
|
||||
|
||||
function horizontalMirror(pattern: string[]) {
|
||||
return [
|
||||
pattern[0].split("").reverse().join(),
|
||||
pattern[1].split("").reverse().join(),
|
||||
pattern[2].split("").reverse().join(),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import {
|
||||
BoardState,
|
||||
getGoPlayerStartingState,
|
||||
opponents,
|
||||
PlayerColor,
|
||||
playerColors,
|
||||
PointState,
|
||||
} from "../boardState/goConstants";
|
||||
import { getAllChains, getPlayerNeighbors } from "./boardAnalysis";
|
||||
import { getKomi } from "./goAI";
|
||||
import { Player } from "@player";
|
||||
import { getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
|
||||
import { floor, isNotNull } from "../boardState/boardState";
|
||||
import { Factions } from "../../Faction/Factions";
|
||||
import { FactionName } from "@enums";
|
||||
|
||||
/**
|
||||
* Returns the score of the current board.
|
||||
* Each player gets one point for each piece on the board, and one point for any empty node
|
||||
* fully surrounded by their pieces
|
||||
*/
|
||||
export function getScore(boardState: BoardState) {
|
||||
const komi = getKomi(boardState.ai) ?? 6.5;
|
||||
const whitePieces = getColoredPieceCount(boardState, playerColors.white);
|
||||
const blackPieces = getColoredPieceCount(boardState, playerColors.black);
|
||||
const territoryScores = getTerritoryScores(boardState);
|
||||
|
||||
return {
|
||||
[playerColors.white]: {
|
||||
pieces: whitePieces,
|
||||
territory: territoryScores[playerColors.white],
|
||||
komi: komi,
|
||||
sum: whitePieces + territoryScores[playerColors.white] + komi,
|
||||
},
|
||||
[playerColors.black]: {
|
||||
pieces: blackPieces,
|
||||
territory: territoryScores[playerColors.black],
|
||||
komi: 0,
|
||||
sum: blackPieces + territoryScores[playerColors.black],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles ending the game. Sets the previous player to null to prevent further moves, calculates score, and updates
|
||||
* player node count and power, and game history
|
||||
*/
|
||||
export function endGoGame(boardState: BoardState) {
|
||||
if (boardState.previousPlayer === null) {
|
||||
return;
|
||||
}
|
||||
boardState.previousPlayer = null;
|
||||
const statusToUpdate = getPlayerStats(boardState.ai);
|
||||
statusToUpdate.favor = statusToUpdate.favor ?? 0;
|
||||
const score = getScore(boardState);
|
||||
|
||||
if (score[playerColors.black].sum < score[playerColors.white].sum) {
|
||||
resetWinstreak(boardState.ai, true);
|
||||
statusToUpdate.nodePower += floor(score[playerColors.black].sum * 0.25);
|
||||
} else {
|
||||
statusToUpdate.wins++;
|
||||
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
|
||||
statusToUpdate.winStreak = statusToUpdate.oldWinStreak < 0 ? 1 : statusToUpdate.winStreak + 1;
|
||||
|
||||
if (statusToUpdate.winStreak > statusToUpdate.highestWinStreak) {
|
||||
statusToUpdate.highestWinStreak = statusToUpdate.winStreak;
|
||||
}
|
||||
|
||||
const factionName = boardState.ai as unknown as FactionName;
|
||||
if (
|
||||
statusToUpdate.winStreak % 2 === 0 &&
|
||||
Player.factions.includes(factionName) &&
|
||||
statusToUpdate.favor < getMaxFavor() &&
|
||||
Factions?.[factionName]
|
||||
) {
|
||||
Factions[factionName].favor++;
|
||||
statusToUpdate.favor++;
|
||||
}
|
||||
}
|
||||
|
||||
statusToUpdate.nodePower +=
|
||||
score[playerColors.black].sum *
|
||||
getDifficultyMultiplier(score[playerColors.white].komi, boardState.board[0].length) *
|
||||
getWinstreakMultiplier(statusToUpdate.winStreak, statusToUpdate.oldWinStreak);
|
||||
|
||||
statusToUpdate.nodes += score[playerColors.black].sum;
|
||||
Player.go.boardState = boardState;
|
||||
Player.go.previousGameFinalBoardState = boardState;
|
||||
|
||||
// Update multipliers with new bonuses, once at the end of the game
|
||||
Player.applyEntropy(Player.entropy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the winstreak to zero for the given opponent, and adds a loss
|
||||
*/
|
||||
export function resetWinstreak(opponent: opponents, gameComplete: boolean) {
|
||||
const statusToUpdate = getPlayerStats(opponent);
|
||||
statusToUpdate.losses++;
|
||||
statusToUpdate.oldWinStreak = statusToUpdate.winStreak;
|
||||
if (statusToUpdate.winStreak >= 0) {
|
||||
statusToUpdate.winStreak = -1;
|
||||
} else if (gameComplete) {
|
||||
// Only increase the "dry streak" count if the game actually finished
|
||||
statusToUpdate.winStreak--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number pieces of a given color on the board
|
||||
*/
|
||||
function getColoredPieceCount(boardState: BoardState, color: PlayerColor) {
|
||||
return boardState.board.reduce(
|
||||
(sum, row) => sum + row.filter(isNotNull).filter((point) => point.player === color).length,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all empty spaces fully surrounded by a single player's stones
|
||||
*/
|
||||
function getTerritoryScores(boardState: BoardState) {
|
||||
const emptyTerritoryChains = getAllChains(boardState).filter((chain) => chain?.[0]?.player === playerColors.empty);
|
||||
|
||||
return emptyTerritoryChains.reduce(
|
||||
(scores, currentChain) => {
|
||||
const chainColor = checkTerritoryOwnership(boardState, currentChain);
|
||||
return {
|
||||
[playerColors.white]:
|
||||
scores[playerColors.white] + (chainColor === playerColors.white ? currentChain.length : 0),
|
||||
[playerColors.black]:
|
||||
scores[playerColors.black] + (chainColor === playerColors.black ? currentChain.length : 0),
|
||||
};
|
||||
},
|
||||
{
|
||||
[playerColors.white]: 0,
|
||||
[playerColors.black]: 0,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all neighbors of the empty points in question. If they are all one color, that player controls that space
|
||||
*/
|
||||
function checkTerritoryOwnership(boardState: BoardState, emptyPointChain: PointState[]) {
|
||||
if (emptyPointChain.length > boardState.board[0].length ** 2 - 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const playerNeighbors = getPlayerNeighbors(boardState, emptyPointChain);
|
||||
const hasWhitePieceNeighbors = playerNeighbors.find((p) => p.player === playerColors.white);
|
||||
const hasBlackPieceNeighbors = playerNeighbors.find((p) => p.player === playerColors.black);
|
||||
const isWhiteTerritory = hasWhitePieceNeighbors && !hasBlackPieceNeighbors;
|
||||
const isBlackTerritory = hasBlackPieceNeighbors && !hasWhitePieceNeighbors;
|
||||
return isWhiteTerritory ? playerColors.white : isBlackTerritory ? playerColors.black : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* prints the board state to the console
|
||||
*/
|
||||
export function logBoard(boardState: BoardState): void {
|
||||
const state = boardState.board;
|
||||
console.log("--------------");
|
||||
for (let x = 0; x < state.length; x++) {
|
||||
let output = `${x}: `;
|
||||
for (let y = 0; y < state[x].length; y++) {
|
||||
const point = state[x][y];
|
||||
output += ` ${point?.chain ?? ""}`;
|
||||
}
|
||||
console.log(output);
|
||||
}
|
||||
}
|
||||
|
||||
export function getPlayerStats(opponent: opponents) {
|
||||
if (!Player.go.status[opponent]) {
|
||||
Player.go = getGoPlayerStartingState();
|
||||
}
|
||||
return Player.go.status[opponent];
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
// The character 'wei' is part of the original Chinese name of the game Go, meaning to surround or enclose
|
||||
export const weiArt = `
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
... .:lc;,'',:c:. ..
|
||||
.:;,'.,dOOxddONWNNNNNWNOdxxl,,.
|
||||
.''.... .:dxdolc::kWWNXK0KWMMMMMMMMMMMMMMMMMNK0kl:'..
|
||||
...... 'colcc:;,'.,xNXXK0OkxxkXMMMWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKkxo:;'..
|
||||
'c:::;,,'.....;xK00Okxxdold0WMWWWNNNXXWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNX0xxo;.
|
||||
.'......'. .'''...... .dkkxddollc:::dKWWWNNNXXKK000XMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMW0xooc.
|
||||
:OKOOO0KOl::;. .. ....... .loolllc::;;,,'.'dNNNXKK00OOkxxONMMMMMMWWWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWWXko;.
|
||||
.kMMMMMMNXNXkdxd:.';;,,''....... ;0KK00OOkkxxddoollkWMMWWWWWWNNNNNXXNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNNNNWWWWWWWWWWWWWWWWWWWWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMW0d:,.
|
||||
:KMMMMMMMMMMMMMN0KXNNNNXXXKKKK000XWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKkkOOOO00000KKKKXXXXXNNNNNWWWWWx,',,,;;;:::cccccllllcccc:;cOXXMMMMMMMMMMMMMMMMMMMMMMMMMMWKxd;
|
||||
.cx0WMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKKKXXNNNWWMKocclloodxxkkO0Oo' .............''''',,,;;;:;. 'cdKWMMMMMMMMMMMMMMMMMMMMMMMMMMWKc
|
||||
.:ONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNNNWWWWWWMMXkodxxkO0KKX0o'..',,;:cl; ... 'cONMMMMMMMMMMMMMMMMMMMMMMMMWOl'
|
||||
;lxNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN0kO00KXXNWXd;,;::cloddkx;...';:'.... .dNMMMMMMMMMMMMMMMMMMMMMMMNOo'
|
||||
'kWMMMMMMMMMMMMMMMNXXXXKKKKKXXNNWWWOoclodxkO0x' ....',,;, .xK00000d;;:,.. 'odKMMMMMMMMMMMMMMMMMMMMMMXl,.
|
||||
.ckNMMMMMMMMMMMMMWx'.........',;:cc:. . .dXWMMMWNXNKxoc''. .,0MMMMMMMMMMMMMMMMMMMMMXOd'
|
||||
.cXMMMMMMMMMMMMMWx. .:l0MMMMMMMMMWK0koc:' cKMMMMMMMMMMMMMMMMMMMMMXo'
|
||||
'kWMMMMMMMMMMMMMWx. .:KMMMMMMMMMMMMWWXkl;.. .dNMMMMMMMMMMMMMMMMMMMMKl,
|
||||
,xkXMMMMMMMMMMMMWk. ,o0WMMMMMMMMMMMMMWXkd; .kWMMMMMMMMMMMMMMMMMMMMK:
|
||||
.;0MMMMMMMMMMMMWk' .dWMMMMMMMMMMMMMMWKOc ... .... .lOXMMMMMMMMMMMMMMMMMMK0o.
|
||||
:KMMMMMMMMMMMMWO' 'kWMMMMMMMMMMMMMNxc:. ..;c:o00kxxkOOd::c,.. .kMMMMMMMMMMMMMMMMMWo'.
|
||||
.oXMMMMMMMMMMMMWO, 'kWMMMMMMMMMMMMXxl' .,;oxOXNWMMMMMMMWNXWKkxo;;' 'OMMMMMMMMMMMMMMMMMWx.
|
||||
.xNMMMMMMMMMMMMW0, .kWMMMMMMMMMMMMKl' ''.. .:lllOKKWMMMMMMMMMMMMMMMMMWXK0kl'.. ;0MMMMMMMMMMMMMMMMMWk'
|
||||
.OWMMMMMMMMMMMMW0; .dWMMMMMMMMMMMKxxocc::dKKOkxONWWMMMMMMMMMMMMMMMMMMMMMMMMMMN0ko' .cXMMMMMMMMMMMMMMMMMW0;
|
||||
'ONWMMMMMMMMMMMMK: ',dWMMMMMMMMMMMKxxkKWWNNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWXd:. .dNMMMMMMMMMMMMMMMMMMXc
|
||||
.;OMMMMMMMMMMMMK: .''..... .xKXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKO: .ckKMMMMMMMMMMMMMMMWK0o
|
||||
.xMMMMMMMMMMMMXc .. .:oc;,',xNNXK0OOkxkXMMMMMMMMMMMMMMMMMMMMMMWXXXXNWXdcoxkOkxdOWMMMMMMMMMMMMMMMMMMWXOx: .oWMMMMMMMMMMMMMMNc'.
|
||||
.kMMMMMMMMMMMMNl ;xxdxXMWNNXXWMMMMMMMMMMMMMMMMMMMMMMMWWWMNkdkOKO;.',;c;. ;OWMMMMMMMMMMMMMMMMWXOdc' .xWMMMMMMMMMMMMMMNo.
|
||||
.OMMMMMMMMMMMMNo 'coONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWOolol. ... ;KWMMMMMMMMMMMMMMWXOxc' 'kWMMMMMMMMMMMMMMNx.
|
||||
'OMMMMMMMMMMMMWd. .';oKWNXXXXXXXXXNNW0dxd0MMMMMMMMMXxo; 'kWMMMMMMMMMMMMMWOlc' ,OWMMMMMMMMMMMMMMWO'
|
||||
,0MMMMMMMMMMWOo, .;:,''......',;c',xx0MMMMMMMMMXl. .oNMMMMMMMMMMMMN0x; ,0WMMMMMMMMMMMMMMW0;
|
||||
;0MMMMMMMMMMX: .kWMMMMMMMMMMWO' :oxNMMMMMMMMMMMNOc'. ;0WMMMMMMMMMMMMMMMK:
|
||||
;0MMMMMMMMMMNc .lXMMMMMMMMMXd;. :0WMMMMMMMMMMM0o:. ;0MMMMMMMMMMMMMMMMXc
|
||||
:KMMMMMMMMMMNl :KMMMMMMMMMXl. ,:xWMMMMMMMMMW0o:. :KMMMMMMMMMMMMMMMMNc
|
||||
:KMMMMMMMMMMNo. ..,OMMMMMMMMNkc. .o0XMMMMMMMMMMWk;. .'.... .... :KMMMMMMMMMMMMMMMMWl
|
||||
cKMMMMMMMMMMNo. ,kOXMMMMMMMM0; .;OMMMMMMMMMMWOc' .lolc::;',kNXK0OOkkO00x:;:,.. cXMMMMMMMMMMMMMMWKo.
|
||||
cKMMMMMMMMMMNd. 'OWMMMMMMMMWXl. .;lddKMMMMMMMMMMW0xxxdoolkWMWWWNNNNWMMMMMMMMMMMNXNKkdo;. cXMMMMMMMMMMMMMMK;
|
||||
cKMMMMMMMMMMNd. .dNMMMMMMMWO:...,'.... .cxkxddollc:lONNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWXKOl:' lNMMMMMMMMMMMMMMK,
|
||||
cKMMMMMMMMMMWx. ....lXMMMMMMMWO:',l0XXK0OkkxONMMMMMMWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNkoc. lNMMMMMMMMMMMMMMK,
|
||||
:KMMMMMMMMMMWk' .::;,'...'xK0kkNMMMMMMMMWNXXXWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKl. oWMMMMMMMMMMMMMMK,
|
||||
:KMMMMMMMMMMWk' .,,,,''''.... .:dxdolc::kWWNNXXK0KNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN0kkOOO0000000KKKKKKKKKKKKKKXXXXXXXXXXXXXXXXXXXXNXl. .dWMMMMMMMMMMMMMMK,
|
||||
:KMMMMMMMMMMWO, .:;,c0NWNNNNXXK0OkkxkNMMMWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNKKXXXNNNWWWMMXdcclloddxkkOOOl. ...........................'''''''''''''''''. .:xNMMMMMMMMMMMMK,
|
||||
;0MMMMMMMMMMWO, .xKXXNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWWWWWWKxdxkO00KXXo...'',;;:ccllc, ... :XMMMMMMMMMMMMK,
|
||||
;0MMMMMMMMMMW0; .oxONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXO0KXNk;;:lol, ....... cNMMMMMMMMMMMMK,
|
||||
,0MMMMMMMMMMWK; .,cxKXXWMMMMMMMMMMMMMMMNKXNN0olodxOl...',. lNMMMMMMMMMMMMK,
|
||||
,OMMMMMMMMMMWK: .,::lkXWWMMMMMMWWXkxkkc,;::. .lNMMMMMMMMMMMMK,
|
||||
'OMMMMMMMMMMMX: ..:x0KOkkOK0xl;.... .oNMMMMMMMMMMMMK,
|
||||
'OMMMMMMMMMMMXc .,..;:..,' .. 'clc;,,,;:l:... .dNMMMMMMMMMMMMK,
|
||||
.kMMMMMMMMMMMNc .. .;;:,'.,dOkdxKWWNNNNNWNOxo;''. .xNMMMMMMMMMMMMK,
|
||||
.kMMMMMMMMMN0x, .'...',. ','....;dkxdlc:ldONNXK0KWMMMMMMMMMMMMMMMX00xoc,. .xNMMMMMMMMMMMMK,
|
||||
.xMMMMMMMMMK, .,d0KKXOc;:c. .. ... .clc;,;dKXK0OkkXMMMMWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMW0occ;. .xWMMMMMMMMMMMMK,
|
||||
.xMMMMMMMMMK; .,lXMMMNXNWNOxd:'...:O0OkdoxNWWNNXNWMMMMMMWNWWWWWWMMMMMMMMMMWWWMMMMMMMMMMMMMMMMMNNXk;. .xWMMMMMMMMMMMMK,
|
||||
.d0NMMMMMMMMMK: .cdONMMMMMMMMN0KKK0XWMMMMMMMMMMMMMWKO0XNNOc;::clooodxxxxxxdloxKWMMMMMMMMMMMMMMMMMMNxc. .xNMMMMMMMMMMMMK,
|
||||
'0MMMMMMMMMMMXc .':OWMMMMMMMMMMMMMMWKKKXXNWNxloxOk;...,;;. .'lNMMMMMMMMMMMMMMMMMMWKo. .xNMMMMMMMMMMMMK,
|
||||
'OMMMMMMMMMMMXl. .cd0WMMMMMMMMWOOX0o'..';::. ',lNMMMMMMMMMMMMMMMWWXkl;. .dNMMMMMMMMMMMMK,
|
||||
.kWMMMMMMMMMMXo. .;OWMMMMMMMMNxc:'. l0XWMMMMMMMMMMMMMWKxd:.. .dWMMMMMMMMMMMMK,
|
||||
.kWMMMMMMMMMMNd. .cOWMMMMMMMMWNo ,OWMMMMMMMMMMMMWKxl:. .dNMMMMMMMMMMMMK,
|
||||
.xNMMMMMMMMMMNx. 'kNMMMMMMMMMXc ';xWMMMMMMMMMMMKo:,. .dNMMMMMMMMMMMMK,
|
||||
.dNMMMMMMMMMMWk. 'oXMMMMMMMMK: .l0XMMMMMMMMMMMXkc. .dNMMMMMMMMMMMMK,
|
||||
.oXMMMMMMMMMMWO' .oXMMMMMMMWO, .c0MMMMMMMMMMWOol, .dNMMMMMMMMMMMMK,
|
||||
.lXMMMMMMMMMMW0, 'ONWMMMMMMWk. ';',l0WMMMMMMMMMNOx: .dNMMMMMMMMMMMMK,
|
||||
.cKMMMMMMMMMMMK, 'coKMMMMMMWd. .'... ;xxdccOWXKKNMMMMMMMMMNOo;. .dNMMMMMMMMMMMMK,
|
||||
:KMMMMMMMMMMMX; .cKMMMMMMWx'. ... .clc;,:kKKOkKWWMWWMMMMMMMMMMMMMMMXko:' .dNMMMMMMMMMMMMK,
|
||||
;0MMMMMMMMMXxc. .xXWMMMMMMNKo. .::;'..,d0OxddONWNNXNMMMMMMMMMMMMMMMMMMMMMMMMMMWXkl:' .dNMMMMMMMMMMMMK,
|
||||
'0MMMMMMMMMO. .clOMMMMMMMXxc:o0WNXXK0KWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXk: .dNMMMMMMMMMMMMK,
|
||||
.OMMMMMMMMMO. ;0MMMMMMMMWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMNKKXXNNWWWWMMKocclloodxko. .oNMMMMMMMMMMMMK,
|
||||
.:dKMMMMMMMMMO' .oXWMMMMMMMMMMMMMMMMMMWNWWMMMMMMMMKxkO0KKOc'.',;::ccll; .oNMMMMMMMMMMMMK,
|
||||
;KMMMMMMMMMMM0, .:oOWMMMMMMMMWKOO0KXXNk;:KMMMMMMMM0oddl,... .oNMMMMMMMMMMMMK,
|
||||
,0WMMMMMMMMMM0, ;kXWMMMMXkOk, ....',. ;0MMMMMMMMMMN0dc:,. .oNMMMMMMMMMMMMK,
|
||||
'kWMMMMMMMMMM0; ,cxNMMWKd,. .lXMMMMMMMMMMMWNNNO, .oNMMMMMMMMMMMMK,
|
||||
.xNMMMMMMMMMMK; 'oOKX0o, .xNMMMMMMMMMMMMMMMO' .oNMMMMMMMMMMMM0,
|
||||
.dNMMMMMMMMMMK: .,,,,. 'OWMMMMMMMMMMMMMMMK: .oNMMMMMMMMMMMMK,
|
||||
.lXMMMMMMMMMMK: '0MMMMMMMMMMMMMW0dkl. .oNMMMMMMMMMMMMNd.
|
||||
:XMMMMMMMMMMKc. '0MMMMMMMMMMMMMWd''. .''.'',. .oNMMMMMMMMMMMMMW0;
|
||||
;KMMMMMMMMMMXc. .,l0MMMMMMMMMMMM0x; .. .cc,'dXXXXXOl:c:. .oNMMMMMMMMMMMMMMNl
|
||||
.cxNMMMMMMMMMMXl. .dMMMMMMMMMMWOlc'.,:;'.;k0xdkNWNNWMMMMMWNNN0kl,'. .lNMMMMMMMMMMMMMMNc
|
||||
cNMMMMMMMMMMMMXo. .''....xMMMMMMMMMMNxclccxXNXKXWMMMMMMMMMMMMMMMMMMMMN0xc. .lNMMMMMMMMMMMMMMXc
|
||||
;KWMMMMMMMMMMMXo. ......... .:oolcc:;,'.;ONXXKOkKMMMMMMMMMMMWWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMM0c:. .lNMMMMMMMMMMMMMMX:
|
||||
,OWMMMMMMMMMMMNd. ..:OXXXXK0OOkxxdooldXMMWWWWNNNXNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNWWWWWMKxddkOKx;;. lNMMMMMMMMMMMMMWK;
|
||||
'kWMMMMMMMMMMMNd. ;ONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXOkO0KXNWXo,;:clodo, ... lNMMMMMMMMMMMMMW0;
|
||||
.dNMMMMMMMMMMMWx. .lx0WMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKKNMMMMMMMMMMWOoloodxkOx;. ...';,. lNMMMMMMMMMMMMMW0,
|
||||
.lNMMMMMMMMMMMWk. .;dOO0WMMMMMMMMMWWWMMW0dddxkkOO0KKXNk,'kMMMMMMMMMMW0; . lNMMMMMMMMMMMMMWO'
|
||||
cNMMMMMMMMMMMMO' ..;kWMMMMMMMMWkcodxd, ........ .kMMMMMMMMMMMNc cNMMMMMMMMMMMMMWk'
|
||||
,d0WMMMMMMMMMMNXO' 'kWMMMMMMMMWd. .kMMMMMMMMMXd;. :XMMMMMMMMMMMMMWx.
|
||||
lNMMMMMMMMMMMMk;;. .oNMMMMMMMMNO; .xMMMMMMMMMK, 'ckNMMMMMMMMMMMMMNd.
|
||||
:KMMMMMMMMMMMMx. ;lxNMMMMMMMNxl, .xMMMMMMMMMK: dWMMMMMMMMMMMMMMMNo.
|
||||
;0WMMMMMMMMMMMk. ;0WMMMMMMMWXd' .xMMMMMMMMMXc oNMMMMMMMMMMMMMMMNl.
|
||||
'kWMMMMMMMMMMMk. ,oKWMMMMMMWXd' .xMMMMMMMMMXo. lNMMMMMMMMMMMMMMMNc
|
||||
.dWMMMMMMMMMMMO' 'lKMMMMMMMMKdc. .xMMMMMMMMMNd. .'.... ..'. cXMMMMMMMMMMMMMMMXc
|
||||
.'xWMMMMMMMMMMM0, ,okXMMMMMMMKoc. .xMMMMMMMMMNk,...... ;olc:;,'.:0NXK0OOkkkO00xc;... cXMMMMMMMMMMMMMMMNl..
|
||||
.oKWMMMMMMMMMMMM0; ,dkXMMMMMMMW0:. .xMMMMMMMMMWXKK000Okkxddlo0MWWWNNNXNWMMMMMMMMMMMWNXOdoc;;' :KMMMMMMMMMMMMMMMMNXl
|
||||
.oNMMMMMMMMMMMMMK: 'lkNMMMMMMMWO;. .''... 'dxdolc:ckNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNKK0dc. :KMMMMMMMMMMMMMMMMMWo
|
||||
cKMMMMMMMMMMMMMXc. ..lKWMMMMMMMMWk:;:loc:;,'cOXK0Okx0WMMMMWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKxc. ;0MMMMMMMMMMMMMMMMMNl
|
||||
,0MMMMMMMMMMMMMXl. .;o0WMMMMMMMMMMW0xkXMWWNNXXNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM0dl. ;0MMMMMMMMMMMMMMMMMXl
|
||||
.kMMMMMMMMMMMMMNo. .o0NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNOOO000KKXXNNNNNNNXXXXXKKKK000000000000000000000ko:. ,OWMMMMMMMMMMMMMMMMXc
|
||||
.cxXMMMMMMMMMMMMMWd. .oxKMMMMMMMMMMMMMMMMMMMMMMMMMMWKKXXNNWWWOlooo0MMMMMMMMO' .......'',,,,'''''............................. ,OWMMMMMMMMMMMMMMMMK:
|
||||
.kWMMMMMMMMMMMMMMWx. .cx0NMMMMMMMMMMMMWWWWWMXkxk0K0l'.',;:cl' .xMMMMMMMM0, 'OWMMMMMMMMMMMMMMMMK:
|
||||
.lXMMMMMMMMMMMMWKkc. .,coONMMMMMN0OKN0c;codc. .... .xMMMMMMMMK: 'kWMMMMMMMMMMMMMMMM0;
|
||||
;0MMMMMMMMMMMMWo .':xXWW0doc'.,. .xMMMMMMMMXl. 'kWMMMMMMMMMMMMMMMW0;
|
||||
.,;OMMMMMMMMMMMMWo. .;::c, .xMMMMMMMMNd. .kWMMMMMMMMMMMMMMMWO,
|
||||
,OKNMMMMMMMMMMMMWd. .kMMMMMMMMWk. .xWMMMMMMMMMMMMMMMWO,
|
||||
.xNMMMMMMMMMMMMMWx. .kMMMMMMM0do. .xWMMMMMMMMMMMMMMMWO'
|
||||
.cXMMMMMMMMMMMMMWk' 'OMMMMMMMk. .dWMMMMMMMMMMMMMMMWk'
|
||||
,odXMMMMMMMMMMMMMWO, ,0MMMMMMM0; .oWMMMMMMMMMMMMMMMWk'
|
||||
;0WMMMMMMMMMMMMMMM0; :KMMMMMMNKo. ..oWMMMMMMMMMMMMMMMWk.
|
||||
.xNMMMMMMMMMMMMMMMXc .lXMMMMMWkc;. .oxOWMMMMMMMMMMMMMMMWx.
|
||||
..lNMMMMMMMMMMMMMMMNl .d0XMMMWKk: .xWMMMMMMMMMMMMMMMMMWx.
|
||||
ck0WMMMMMMMMMMMMMNkdc ';xWMWKd:. .oNMMMMMMMMMMMMMMMMMWd.
|
||||
,OWMMMMMMMMMMMMMMNc. ;dOXKo:. .:c:;,,'''''',;;;. :KMMMMMMMMMMMMMMMMMWd.
|
||||
.'dWMMMMMMMMMMMMMMNd. ..;dxl. ','''........ .dkxxdolc:;dNWWNNNXXXXXXNNNOlcol..'. ,OMMMMMMMMMMMMMMMMMWd.
|
||||
.dOKMMMMMMMMMMMMMMMWk' ....... .:;. .cdoolllcc::;;,,''.,dKNNXXXKK00OOOkxxONMMMMMMMWWMMMMMMMMMMMMMMMMWWWMN0Okoc;. .;OMMMMMMMMMMMMMMMMMWo.
|
||||
.lXMMMMMMMMMMMMMMMMW0; .;cc::;;,,''...... ,kKK00OOkkxxddoollxNMMMWWWWWWWNNNNNXXXXWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNXkc,,..dKWMMMMMMMMMMMMMMMMMWo.
|
||||
;0MMMMMMMMMMMMMMMMMX: .','''....... .cxkkxxddoollcc::;ckNMWWWNNNXXXKKK000KWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWK0OolxXMMMMMMMMMMMMMMMMMMWo.
|
||||
.kMMMMMMMMMMMMMMMNOxo;,''..lXWNNXXKK000OOkkxxdONMMMMMMMMMMMWWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWWWMMMMMMMMMMMMMMMMMMMWd.
|
||||
.,,xMMMMMMMMMMMMMMMWNNNNNNNXXNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNNNWWWWWWWWWWWWWWWWWWWWWWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWd.
|
||||
'OXNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMW0kOOO000KKXXXNNNNWWWWO;',,;;:::cclllloooollllllldKMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWx.
|
||||
.xKNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNKKKXXXXNNNNWWWWWMM0l:cclllooddxxkkkOO00x' .......''',,,;;;:. .cx0WMMMMMMMMMMMMMMMMMMMMMMMMMMMWk'
|
||||
.,OMMMMMMMMMMMMMMMMWNWMMMMMMMMMMMMMMMMMMMMWWNNWWWWWWWWWMMMMMNkooddxxkkOO000KKXXNXo.....'',,;;;::ccll, ... .cx0WMMMMMMMMMMMMMMMMMMMMMMMMMMW0;
|
||||
cKMMMMMMMMMMMMMMMNOx0NXK00OO0000KKXXXNNWWKl',;;::cclloooddxl. ............ .:d0WMMMMMMMMMMMMMMMMMMMMMMMMWKOl
|
||||
'oKWMMMMMMMMMMMMKddl;,.............'',,;;,. .:d0WMMMMMMMMMMMMMMMMMMMMMMMNo,.
|
||||
.l0WMMMMMMMMMMM0,. .:o0WMMMMMMMMMMMMMMMMMMMMMMNO,
|
||||
.lKWMMMMMMMMMMXc. .:o0MMMMMMMMMMMMMMMMMMMMMXxo,
|
||||
.lxXMMMMMMMMNKx. .:d0WMMMMMMMMMMMMMMMMMMKdc.
|
||||
.,xXWMMMMMWkc;. .:oOWMMMMMMMMMMMMMMMMKd:.
|
||||
;dkKWMMWkc, .,xNMMMMMMMMMMMMMMMXx:.
|
||||
.:lxKkoo; .:kNMMMMMMMMMMMMMNx;'
|
||||
.;;. 'okXMMMMMMMMMMN0k:
|
||||
,oONMMMMMWK0klc,.
|
||||
.,cd0XXNXkc,.
|
||||
.,;,',;.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
`;
|
||||
|
||||
export const bitverseArt = `
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.*/////*
|
||||
.((((((/
|
||||
.((((((/
|
||||
...... .((((((/ .......
|
||||
*((((((* .((((((/ .(((((((.
|
||||
*((((((*. .((((((/ .(((((((.
|
||||
....... *((((((*. .((((((/ ,(((((((. ......
|
||||
,(((((((* *((((((*. .((((((/ ,(((((((. ,(((((((/.
|
||||
,(((((((/ *((((((*. .((((((/ ,(((((((. ,(((((((/.
|
||||
,(((((((/ *((((((*. .((((((/ ,(((((((. ,(((((((/.
|
||||
*((((((*. ,(((((((/ *((((((*. .((((((/ ,(((((((. ,(((((((/. ,(((((((.
|
||||
*((((((*. ,(((((((* *((((((*. .((((((/ ,(((((((. ,(((((((/. ,(((((((.
|
||||
*((((((*. ,(((((((/ *((((((*. .((((((/ ,(((((((. ,(((((((/. ,(((((((.
|
||||
/((((((, *((((((*. ,(((((((/ *((((((*. .((((((/ ,(((((((. ,(((((((/. ,(((((((. .*((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ *((((((*. .((((((/ ,(((((((. ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ *((((((*. ,((((((/. ,(((((((. ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ *((((((*. .*((((((((/, ,(((((((. ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ *((((((*. .*((((((((((/, ,(((((((. ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ *((((((*. ,/(((((((((((((*. ,(((((((. ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ *((((((*. ,((((((((((((((((*. ,(((((((. ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((/. ,(((((((/ *((((((*. .*(((((((((((((((((((, .(((((((. ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ *((((((*. ./(((((((((((((((((((((, ,/((((((. ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ *(((((((/*.*((((((((/(((((((((((((((/,,/((((((((. ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ *((((((((((((((((((*,((((((/,/((((((((((((((((((. ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ .*((((((((((((((/,..((((((/..*((((((((((((((/, ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ .*(((((((((((/. .((((((/. *(((((((((((/,. ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ ./((((((((*. .((((((/ ./((((((((,. ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,(((((((/ .*((((((((,. .((((((/ ./(((((((/, ,(((((((/. ,(((((((. ./((((((,
|
||||
.(((((((, .*(((((((/. ,(((((((/ .((((((((/. .((((((/ ,(((((##(/. ,(((((((/. ,((((((((,. ./((((((,
|
||||
.(((((((, ./(((((((((*. ,(((((((/. ,((((((((*. .((((((/ *((((((((*. ,(((((((/. ,(((((((((/,. ./((((((,
|
||||
.(((((((, ,/(#(((((((/. ,((((((((/(((((((((((((((((((((, .((((((/ ./(((((((((((((((((((((/(((((((/. .,((((((((((*. ./((((((,
|
||||
.(((((((, ,/(((((((((/, ,((((((((((((((((((#((((((((((, .((((((/ .*((((((((((((((((((((((((((((/. .*((((((((((*. ./((((((,
|
||||
.(((((((,..*((((((((((*. .,(((((((((((((((((((((((((((/. .((((((/ .,(((((((((((((((((((((((((((/. ,/(((((((((/.../((((((,
|
||||
.(((((((**((((((((((/. ,(((((((((((((((((((((((((((*. .((((((/ ,(((((((((((((((((((((((((((/. ,((((((((((/*/((((((,
|
||||
.(((((((((((((((#/,. ,/(((((((((/,. ./((((((*. .((((((/ ,(((((((,. .*((((((((((*. .*((((((((((((((((,
|
||||
.(((((((((((((((*. .*((((((((#(*. /((((((*. .((((((/ ,(((((((. ,/(((((((((/,. ,/((((((((((((((,
|
||||
.((((((((((((/. ,/(((((((((/. /((((((*. .((((((/ ,(((((((. .*((((((((((*. .*((((((((((((,
|
||||
.((((((((((/, ,/(((((((((/,. *((((((*. .((((((/ .(((((((. .*((((((((((*. .*((((((((((,
|
||||
.((((((((*. .*((((((((((*. *((((((*. .((((((/ ,(((((((. ,/(((((((((/, ,/(((((((,
|
||||
.(((((((,. .,/(((((((((/. *((((((*. .((((((/ ,(((((((. .,((((((((((*. ./((((((,
|
||||
.(((((#(, ./((((((((((,. *((((((*. .((((((/ ,(((((((. ./((((((((((,. ./((((((,
|
||||
.(((((((, .*((((((((#(*. *((((((*. .((((((/ ,(((((((. ./(((((((((/, .*((((((,
|
||||
.(((((((, ,/(((((((((/. *((((((*. .((((((/ ,(((((((. .*((((((((((*. ./((((((,
|
||||
.(((((((, ./(((((((((/, *((((((*. .((((((/ ,(((((((. .*((((((((((* ./((((((,
|
||||
.(((((((, *((((((((*. ./(((((((*. .(((#((/ ,((((((((*. ,/((((((((. ./((((((,
|
||||
.(((((((, *((((((/, ,/((((((((, .(((#((/ ./((((((((*. .*(((((((. ./((((((,
|
||||
.(((((((, *((((((/. ./((((((((*. .((((((/. ,(((((((((,. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ,/((((((((*. ,((((((((*. ,(((((((((*. ,((#((((. ./((((((,
|
||||
.(((((((, *((((((/. .*((((((((/. .*(#(((((((#/, ,((((((((/, ,(((((((. ./((((((,
|
||||
.(((((((, *((((((/. ,/((((((((*. ,/(((((((((((((*. ,/((((((((,. ,(((((((. ./((((((,
|
||||
.(((((((, *((((((/. .*((((((((/. *((((((((((((((((/. *((((((((/,......................*(((((((. ./((((((,
|
||||
.(((((((, *((((((*. ./((((((((*. .*(((((((/,,/((((((((, ,(((((((((((((((((((((((((((((((((((((((. ./((((((,
|
||||
.(((((((,. *((((((((((((((((((((((((((((((((((((/. .*((((((((/. .,(((((((#/, .*(((((((((((((((((((((((((((((((((((((. ./((((((,
|
||||
.(((((((/,. *(((((((((((((((((((((((((((((((((((/. .*((((((#(*. ,(((((((((, .*((((((((((/(((((((((((((((((((((((((. .*(((((((,
|
||||
.((((((((#(,. *((((((((((((((((((((((((((((((((((,. ,((((((((/, .*#(((((((*. ./((((((/. .,(((((((. .*(#(((((((,
|
||||
.*((((((((((, *((((((/*,,,,,,,,,,,,,,,*,,*((((((/. .*((((((((*. ,((((((((/, .*((((((/ ,(((((((. ./(((((((((/,.
|
||||
,/(((((((((/. *((((((*. .((((((/. ./(((((((/, .*((((((((, *((((((/ ,(((((((. .*((((((((((*.
|
||||
.,((((((((((/. *((((((*. .((((((/. .(((((((/. .,(((((((, *((((((/ ,(((((((. .*((((((((((/.
|
||||
,/((((((((#/,*((((((*. .((((((/. /((((((, ./((((((, .*((((((/ ,(((((((,*((((((((((*.
|
||||
,/((((((((((((((((*. .((((((/. /((((((, ./((((((, .*((((((/ ,(((((((((((((((((*.
|
||||
./#(((((((((((((*. .((((((/. /((((((, ./((((((, .*((((((/ ,(((((((((((((((,.
|
||||
.*((((((((((((*. .((((((/. /((((((, ./((((((, .*((((((/ ,((((((((((((/,
|
||||
.*((((((((((*. .((((((/. /((((((, ./((((((, .*((((((/ ,((((((((((/,.
|
||||
.,/(((((((*. .((((((/. ./((((((, ./((((((, .*((((((/ ,((((((((*,
|
||||
.*((((((*. .((((((/. ./((((((*. ./((((((, .*((((((/ ,(((((((.
|
||||
*((((((*. .((((((/. ./((((((*. .,/((((((, .*((((((/ ,(((((((.
|
||||
*((((((*. .((((((/. ./(((((((/. .*((((((((, .*((((((/ ,(((((((.
|
||||
*((((((/. .((((((/. ./(((((((((/,. .*((((((((((. *((((((/ ,(((((((.
|
||||
*((((((*. .((((((/. ,/((((((((#/. .*((((((((((*. *((((((/ .,(((((((.
|
||||
*(((((((/, .((((((/. .*((((((((((*..,/(((((((((/, .*((((((/ .*((((((((.
|
||||
,((((((((((*. .((((((/. ,/(((((((((((((((((((*. .*((((((/ ./(((((((((/.
|
||||
.*((((((((#(,. .((((((/. ,/(((((((((((((#(*. .*((((((/ ./(((((((((/,
|
||||
.*((((((((((*. .((((((/. ,/(((((((((((*. .*((((((/ ./(((((((((/,
|
||||
.,((((((((((*. .((((((/. .*(((((((,. .*((((((/ .,/(((((((((/.
|
||||
.*((((((((((*. .((((((/. .((((((/ .*((((((/ ,/(((((((((/,
|
||||
,/(((((((*. .((((((/. .((((((/ *((((((/ ./(((((((*.
|
||||
.,/(((*. ..... .... .... ,/(((*.
|
||||
.. ..
|
||||
|
||||
|
||||
|
||||
`;
|
||||
@@ -0,0 +1,344 @@
|
||||
import {
|
||||
bitverseBoardShape,
|
||||
Board,
|
||||
BoardState,
|
||||
Move,
|
||||
Neighbor,
|
||||
opponents,
|
||||
PlayerColor,
|
||||
playerColors,
|
||||
PointState,
|
||||
validityReason,
|
||||
} from "./goConstants";
|
||||
import { getExpansionMoveArray } from "../boardAnalysis/goAI";
|
||||
import {
|
||||
evaluateIfMoveIsValid,
|
||||
findAllCapturedChains,
|
||||
findLibertiesForChain,
|
||||
getAllChains,
|
||||
getBoardFromSimplifiedBoardState,
|
||||
} from "../boardAnalysis/boardAnalysis";
|
||||
import { endGoGame } from "../boardAnalysis/scoring";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes";
|
||||
|
||||
/**
|
||||
* Generates a new BoardState object with the given opponent and size
|
||||
*/
|
||||
export function getNewBoardState(
|
||||
boardSize: number,
|
||||
ai = opponents.Netburners,
|
||||
applyObstacles = false,
|
||||
boardToCopy?: Board,
|
||||
): BoardState {
|
||||
if (ai === opponents.w0r1d_d43m0n) {
|
||||
boardToCopy = resetCoordinates(rotate90Degrees(getBoardFromSimplifiedBoardState(bitverseBoardShape).board));
|
||||
}
|
||||
|
||||
const newBoardState = {
|
||||
history: [],
|
||||
previousPlayer: playerColors.white,
|
||||
ai: ai,
|
||||
passCount: 0,
|
||||
cheatCount: 0,
|
||||
board: Array.from({ length: boardSize }, (_, x) =>
|
||||
Array.from({ length: boardSize }, (_, y) =>
|
||||
!boardToCopy || boardToCopy?.[x]?.[y]
|
||||
? {
|
||||
player: boardToCopy?.[x]?.[y]?.player ?? playerColors.empty,
|
||||
chain: "",
|
||||
liberties: null,
|
||||
x,
|
||||
y,
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
if (applyObstacles) {
|
||||
addObstacles(newBoardState);
|
||||
}
|
||||
|
||||
const handicap = getHandicap(newBoardState.board[0].length, ai);
|
||||
if (handicap) {
|
||||
applyHandicap(newBoardState, handicap);
|
||||
}
|
||||
return newBoardState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines how many starting pieces the opponent has on the board
|
||||
*/
|
||||
export function getHandicap(boardSize: number, opponent: opponents) {
|
||||
// Illuminati and WD get a few starting routers
|
||||
if (opponent === opponents.Illuminati || opponent === opponents.w0r1d_d43m0n) {
|
||||
return ceil(boardSize * 0.35);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a new move on the given board, and update the board state accordingly
|
||||
*/
|
||||
export function makeMove(boardState: BoardState, x: number, y: number, player: PlayerColor) {
|
||||
// Do not update on invalid moves
|
||||
const validity = evaluateIfMoveIsValid(boardState, x, y, player, false);
|
||||
if (validity !== validityReason.valid || !boardState.board[x][y]?.player) {
|
||||
console.debug(`Invalid move attempted! ${x} ${y} ${player} : ${validity}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
boardState.history.push(getBoardCopy(boardState).board);
|
||||
boardState.history = boardState.history.slice(-4);
|
||||
const point = boardState.board[x][y];
|
||||
if (!point) {
|
||||
return false;
|
||||
}
|
||||
point.player = player;
|
||||
boardState.previousPlayer = player;
|
||||
boardState.passCount = 0;
|
||||
|
||||
return updateCaptures(boardState, player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass the current player's turn without making a move.
|
||||
* Ends the game if this is the second pass in a row.
|
||||
*/
|
||||
export function passTurn(boardState: BoardState, player: playerColors, allowEndGame = true) {
|
||||
if (boardState.previousPlayer === null || boardState.previousPlayer === player) {
|
||||
return;
|
||||
}
|
||||
boardState.previousPlayer =
|
||||
boardState.previousPlayer === playerColors.black ? playerColors.white : playerColors.black;
|
||||
boardState.passCount++;
|
||||
|
||||
if (boardState.passCount >= 2 && allowEndGame) {
|
||||
endGoGame(boardState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a number of random moves on the board before the game starts, to give one player an edge.
|
||||
*/
|
||||
export function applyHandicap(boardState: BoardState, handicap: number) {
|
||||
const availableMoves = getEmptySpaces(boardState);
|
||||
const handicapMoveOptions = getExpansionMoveArray(boardState, playerColors.black, availableMoves);
|
||||
const handicapMoves: Move[] = [];
|
||||
|
||||
// select random distinct moves from the move options list up to the specified handicap amount
|
||||
for (let i = 0; i < handicap && i < handicapMoveOptions.length; i++) {
|
||||
const index = floor(Math.random() * handicapMoveOptions.length);
|
||||
handicapMoves.push(handicapMoveOptions[index]);
|
||||
handicapMoveOptions.splice(index, 1);
|
||||
}
|
||||
|
||||
handicapMoves.forEach((move: Move) => {
|
||||
const point = boardState.board[move.point.x][move.point.y];
|
||||
return move.point && point && (point.player = playerColors.white);
|
||||
});
|
||||
return updateChains(boardState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all groups of connected stones on the board, and updates the points in them with their
|
||||
* chain information and liberties.
|
||||
*/
|
||||
export function updateChains(boardState: BoardState, resetChains = true) {
|
||||
resetChains && clearChains(boardState);
|
||||
|
||||
for (let x = 0; x < boardState.board.length; x++) {
|
||||
for (let y = 0; y < boardState.board[x].length; y++) {
|
||||
const point = boardState.board[x][y];
|
||||
// If the current point is already analyzed, skip it
|
||||
if (!point || point.chain !== "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const chainMembers = findAdjacentPointsInChain(boardState, x, y);
|
||||
const libertiesForChain = findLibertiesForChain(boardState, chainMembers);
|
||||
const id = `${point.x},${point.y}`;
|
||||
|
||||
chainMembers.forEach((member) => {
|
||||
member.chain = id;
|
||||
member.liberties = libertiesForChain;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return boardState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign each point on the board a chain ID, and link its list of 'liberties' (which are empty spaces
|
||||
* adjacent to some point on the chain including the current point).
|
||||
*
|
||||
* Then, remove any chains with no liberties.
|
||||
*/
|
||||
export function updateCaptures(initialState: BoardState, playerWhoMoved: PlayerColor, resetChains = true): BoardState {
|
||||
const boardState = updateChains(initialState, resetChains);
|
||||
const chains = getAllChains(boardState);
|
||||
|
||||
const chainsToCapture = findAllCapturedChains(chains, playerWhoMoved);
|
||||
if (!chainsToCapture?.length) {
|
||||
return boardState;
|
||||
}
|
||||
|
||||
chainsToCapture?.forEach((chain) => captureChain(chain));
|
||||
return updateChains(boardState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a chain from the board, after being captured
|
||||
*/
|
||||
function captureChain(chain: PointState[]) {
|
||||
chain.forEach((point) => {
|
||||
point.player = playerColors.empty;
|
||||
point.chain = "";
|
||||
point.liberties = [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the chain data from given points, in preparation for being recalculated later
|
||||
*/
|
||||
function clearChains(boardState: BoardState): BoardState {
|
||||
for (const x in boardState.board) {
|
||||
for (const y in boardState.board[x]) {
|
||||
const point = boardState.board[x][y];
|
||||
if (point && point.chain && point.liberties) {
|
||||
point.chain = "";
|
||||
point.liberties = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return boardState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all the pieces in the current continuous group, or '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
|
||||
*/
|
||||
export function findAdjacentPointsInChain(boardState: BoardState, x: number, y: number) {
|
||||
const point = boardState.board[x][y];
|
||||
if (!point) {
|
||||
return [];
|
||||
}
|
||||
const checkedPoints: PointState[] = [];
|
||||
const adjacentPoints: PointState[] = [point];
|
||||
const pointsToCheckNeighbors: PointState[] = [point];
|
||||
|
||||
while (pointsToCheckNeighbors.length) {
|
||||
const currentPoint = pointsToCheckNeighbors.pop();
|
||||
if (!currentPoint) {
|
||||
break;
|
||||
}
|
||||
|
||||
checkedPoints.push(currentPoint);
|
||||
const neighbors = findNeighbors(boardState, currentPoint.x, currentPoint.y);
|
||||
|
||||
[neighbors.north, neighbors.east, neighbors.south, neighbors.west]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined)
|
||||
.forEach((neighbor) => {
|
||||
if (neighbor && neighbor.player === currentPoint.player && !contains(checkedPoints, neighbor)) {
|
||||
adjacentPoints.push(neighbor);
|
||||
pointsToCheckNeighbors.push(neighbor);
|
||||
}
|
||||
checkedPoints.push(neighbor);
|
||||
});
|
||||
}
|
||||
|
||||
return adjacentPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all empty spaces on the board.
|
||||
*/
|
||||
export function getEmptySpaces(boardState: BoardState): PointState[] {
|
||||
const emptySpaces: PointState[] = [];
|
||||
|
||||
boardState.board.forEach((column) => {
|
||||
column.forEach((point) => {
|
||||
if (point && point.player === playerColors.empty) {
|
||||
emptySpaces.push(point);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return emptySpaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a deep copy of the given board state
|
||||
*/
|
||||
export function getStateCopy(initialState: BoardState) {
|
||||
const boardState = cloneDeep(initialState);
|
||||
|
||||
boardState.history = [...initialState.history];
|
||||
boardState.previousPlayer = initialState.previousPlayer;
|
||||
boardState.ai = initialState.ai;
|
||||
boardState.passCount = initialState.passCount;
|
||||
|
||||
return boardState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a deep copy of the given BoardState's board
|
||||
*/
|
||||
export function getBoardCopy(boardState: BoardState) {
|
||||
const boardCopy = getNewBoardState(boardState.board[0].length);
|
||||
const board = boardState.board;
|
||||
|
||||
for (let x = 0; x < board.length; x++) {
|
||||
for (let y = 0; y < board[x].length; y++) {
|
||||
const pointToEdit = boardCopy.board[x][y];
|
||||
const point = board[x][y];
|
||||
if (!point || !pointToEdit) {
|
||||
boardCopy.board[x][y] = null;
|
||||
} else {
|
||||
pointToEdit.player = point.player;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return boardCopy;
|
||||
}
|
||||
|
||||
export function contains(arr: PointState[], point: PointState) {
|
||||
return !!arr.find((p) => p && p.x === point.x && p.y === point.y);
|
||||
}
|
||||
|
||||
export function findNeighbors(boardState: BoardState, x: number, y: number): Neighbor {
|
||||
const board = boardState.board;
|
||||
return {
|
||||
north: board[x]?.[y + 1],
|
||||
east: board[x + 1]?.[y],
|
||||
south: board[x]?.[y - 1],
|
||||
west: board[x - 1]?.[y],
|
||||
};
|
||||
}
|
||||
|
||||
export function getArrayFromNeighbor(neighborObject: Neighbor): PointState[] {
|
||||
return [neighborObject.north, neighborObject.east, neighborObject.south, neighborObject.west]
|
||||
.filter(isNotNull)
|
||||
.filter(isDefined);
|
||||
}
|
||||
|
||||
export function isNotNull<T>(argument: T | null): argument is T {
|
||||
return argument !== null;
|
||||
}
|
||||
export function isDefined<T>(argument: T | undefined): argument is T {
|
||||
return argument !== undefined;
|
||||
}
|
||||
|
||||
export function floor(n: number) {
|
||||
return ~~n;
|
||||
}
|
||||
export function ceil(n: number) {
|
||||
const floored = floor(n);
|
||||
return floored === n ? n : floored + 1;
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
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: Move | null;
|
||||
defendCapture: Move | null;
|
||||
eyeMove: EyeMove | null;
|
||||
eyeBlock: EyeMove | null;
|
||||
pattern: PointState | null;
|
||||
growth: Move | null;
|
||||
expansion: Move | null;
|
||||
jump: Move | null;
|
||||
defend: Move | null;
|
||||
surround: Move | null;
|
||||
corner: PointState | null;
|
||||
random: PointState | null;
|
||||
};
|
||||
|
||||
export type Move = {
|
||||
point: PointState;
|
||||
oldLibertyCount: number | null;
|
||||
newLibertyCount: number | null;
|
||||
};
|
||||
|
||||
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 = [
|
||||
"########...########",
|
||||
"######.#...#.######",
|
||||
"###.#..#...#..#.###",
|
||||
".#..#..#...#..#..#.",
|
||||
".#.....#...#.....#.",
|
||||
"...................",
|
||||
"...................",
|
||||
"...................",
|
||||
"...................",
|
||||
".....##.....##.....",
|
||||
"....###.....###....",
|
||||
"....##.......##....",
|
||||
"....#.........#....",
|
||||
".........#.........",
|
||||
"#........#........#",
|
||||
"##.......#.......##",
|
||||
"##.......#.......##",
|
||||
"###.............###",
|
||||
"####...........####",
|
||||
];
|
||||
@@ -0,0 +1,605 @@
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
|
||||
export const pointStyle = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
hover: {},
|
||||
valid: {},
|
||||
priorPoint: {},
|
||||
point: {
|
||||
position: "relative",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
|
||||
"&$hover$valid:hover $innerPoint": {
|
||||
outlineColor: theme.colors.white,
|
||||
},
|
||||
"&$hover$priorPoint $innerPoint": {
|
||||
outlineColor: theme.colors.white,
|
||||
},
|
||||
"&$hover$priorPoint $priorStoneTrad$blackPoint": {
|
||||
outlineColor: theme.colors.white,
|
||||
display: "block",
|
||||
},
|
||||
"&$hover$priorPoint $priorStoneTrad$whitePoint": {
|
||||
outlineColor: theme.colors.black,
|
||||
display: "block",
|
||||
},
|
||||
"&$hover:hover $coordinates": {
|
||||
display: "block",
|
||||
},
|
||||
"&:hover $broken": {
|
||||
opacity: "0.4",
|
||||
},
|
||||
},
|
||||
broken: {
|
||||
backgroundImage: "repeating-radial-gradient(circle at 17% 32%, white, black 0.00085px)",
|
||||
backgroundPosition: "center",
|
||||
animation: `$static 5s linear infinite`,
|
||||
opacity: "0",
|
||||
margin: "8px",
|
||||
borderRadius: "4px",
|
||||
width: "83%",
|
||||
height: "83%",
|
||||
transition: "all 0.3s",
|
||||
"& $coordinates": {
|
||||
fontSize: "10px",
|
||||
},
|
||||
},
|
||||
"@keyframes static": {
|
||||
from: {
|
||||
backgroundSize: "100% 100%",
|
||||
},
|
||||
to: {
|
||||
backgroundSize: "200% 200%",
|
||||
},
|
||||
},
|
||||
hideOverflow: {
|
||||
overflow: "hidden",
|
||||
},
|
||||
traditional: {
|
||||
"& $innerPoint": {
|
||||
display: "none",
|
||||
},
|
||||
"& $broken": {
|
||||
backgroundImage: "none",
|
||||
backgroundColor: "black",
|
||||
},
|
||||
"& $tradStone": {
|
||||
display: "block",
|
||||
},
|
||||
"& $liberty": {
|
||||
backgroundColor: "black",
|
||||
transition: "none",
|
||||
"&:not($northLiberty):not($southLiberty):not($eastLiberty):not($westLiberty)": {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
},
|
||||
"& $northLiberty, & $southLiberty": {
|
||||
width: "0.9px",
|
||||
},
|
||||
"& $eastLiberty, & $westLiberty": {
|
||||
height: "0.9px",
|
||||
},
|
||||
"&$nineteenByNineteen": {
|
||||
"& $blackPoint": {
|
||||
"&:before": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(30px, 5vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
"& $whitePoint": {
|
||||
"&:before": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(30px, 5vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
"& $coordinates": {
|
||||
fontSize: "0.9vw",
|
||||
},
|
||||
},
|
||||
"&$thirteenByThirteen": {
|
||||
"& $blackPoint": {
|
||||
"&:before": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(40px, 6vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
"& $whitePoint": {
|
||||
"&:before": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(40px, 6vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
"& $coordinates": {
|
||||
fontSize: "0.9vw",
|
||||
},
|
||||
},
|
||||
"&$nineByNine": {
|
||||
"& $blackPoint": {
|
||||
"&:before": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(60px, 7vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
"& $whitePoint": {
|
||||
"&:before": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(60px, 7vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
"&$sevenBySeven": {
|
||||
"& $blackPoint": {
|
||||
"&:before": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(80px, 8vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
"& $whitePoint": {
|
||||
"&:before": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(80px, 8vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
"& $coordinates": {
|
||||
color: "black",
|
||||
left: "15%",
|
||||
},
|
||||
"& $blackPoint ~ $coordinates": {
|
||||
color: "white",
|
||||
},
|
||||
},
|
||||
fiveByFive: {
|
||||
"& $blackPoint": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(35px, 4vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
|
||||
},
|
||||
"& $whitePoint": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(35px, 4vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
sevenBySeven: {
|
||||
"& $blackPoint": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(23px, 3vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
|
||||
},
|
||||
"& $whitePoint": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(25px, 3vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
nineByNine: {
|
||||
"& $filledPoint": {
|
||||
boxShadow: "0px 0px 30px hsla(0, 100%, 100%, 0.48)",
|
||||
},
|
||||
"& $blackPoint": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(15px, 2vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
|
||||
},
|
||||
"& $whitePoint": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(15px, 2vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
thirteenByThirteen: {
|
||||
"& $filledPoint": {
|
||||
boxShadow: "0px 0px 18px hsla(0, 100%, 100%, 0.48)",
|
||||
},
|
||||
"& $blackPoint": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
|
||||
},
|
||||
"& $whitePoint": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
nineteenByNineteen: {
|
||||
"& $filledPoint": {
|
||||
boxShadow: "0px 0px 18px hsla(0, 100%, 100%, 0.48)",
|
||||
},
|
||||
"& $blackPoint": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
|
||||
},
|
||||
"& $whitePoint": {
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
|
||||
},
|
||||
|
||||
"& $innerPoint": {
|
||||
width: "70%",
|
||||
height: "70%",
|
||||
margin: "15%",
|
||||
},
|
||||
},
|
||||
tradStone: {
|
||||
display: "none",
|
||||
borderRadius: "50%",
|
||||
margin: 0,
|
||||
|
||||
"&:before": {
|
||||
zIndex: 2,
|
||||
borderRadius: "50%",
|
||||
bottom: 0,
|
||||
content: '" "',
|
||||
display: "block",
|
||||
left: 0,
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
},
|
||||
"&:after": {
|
||||
boxShadow: "2.5px 4px 0.5em hsla(0, 0%, 0%, 0.5)",
|
||||
zIndex: 1,
|
||||
borderRadius: "50%",
|
||||
bottom: 0,
|
||||
content: '" "',
|
||||
display: "block",
|
||||
left: 0,
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
},
|
||||
|
||||
"&$blackPoint": {
|
||||
position: "static",
|
||||
outlineWidth: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
margin: 0,
|
||||
|
||||
"&:before": {
|
||||
backgroundColor: "black",
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, black 65%), radial-gradient(calc(min(150px, 11vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
"&$whitePoint": {
|
||||
backgroundColor: "transparent",
|
||||
width: 0,
|
||||
height: 0,
|
||||
margin: 0,
|
||||
|
||||
"&:before": {
|
||||
backgroundColor: "hsla(0, 0%, 90%, 1)",
|
||||
backgroundImage:
|
||||
"linear-gradient(145deg, transparent, white 65%), radial-gradient(calc(min(150px, 11vw)) at 42% 38%, white 0%, white 35%, transparent 36%)",
|
||||
},
|
||||
},
|
||||
"&$emptyPoint": {
|
||||
width: 0,
|
||||
height: 0,
|
||||
margin: 0,
|
||||
backgroundColor: "transparent",
|
||||
|
||||
"&:before": {
|
||||
display: "none",
|
||||
},
|
||||
"&:after": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
innerPoint: {
|
||||
outlineStyle: "solid",
|
||||
outlineWidth: "1px",
|
||||
outlineColor: "transparent",
|
||||
borderRadius: "50%",
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
margin: "25%",
|
||||
position: "absolute",
|
||||
},
|
||||
emptyPoint: {
|
||||
width: "10%",
|
||||
height: "10%",
|
||||
margin: "45%",
|
||||
backgroundColor: "white",
|
||||
position: "relative",
|
||||
},
|
||||
filledPoint: {
|
||||
outlineStyle: "solid",
|
||||
outlineWidth: "1px",
|
||||
borderRadius: "50%",
|
||||
position: "relative",
|
||||
boxShadow: "0px 0px 40px hsla(0, 100%, 100%, 0.48)",
|
||||
},
|
||||
whitePoint: {
|
||||
width: "70%",
|
||||
height: "70%",
|
||||
margin: "15%",
|
||||
backgroundColor: "hsla(0, 0%, 85%, 1)",
|
||||
outlineStyle: "none",
|
||||
},
|
||||
blackPoint: {
|
||||
width: "70%",
|
||||
height: "70%",
|
||||
margin: "15%",
|
||||
backgroundColor: "black",
|
||||
outlineColor: "white",
|
||||
},
|
||||
fadeLoopAnimation: {
|
||||
animation: `$fadeLoop 800ms ${theme.transitions.easing.easeInOut} infinite alternate`,
|
||||
},
|
||||
"@keyframes fadeLoop": {
|
||||
"0%": {
|
||||
opacity: 0.4,
|
||||
},
|
||||
"100%": {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
liberty: {
|
||||
position: "absolute",
|
||||
transition: "all 0.5s ease-out",
|
||||
backgroundColor: "transparent",
|
||||
width: "2%",
|
||||
height: "2%",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
},
|
||||
libertyWhite: {
|
||||
backgroundColor: theme.colors.cha,
|
||||
},
|
||||
libertyBlack: {
|
||||
backgroundColor: theme.colors.success,
|
||||
},
|
||||
northLiberty: {
|
||||
width: "2%",
|
||||
height: "54%",
|
||||
top: "-3%",
|
||||
left: "50%",
|
||||
},
|
||||
southLiberty: {
|
||||
width: "2%",
|
||||
height: "50%",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
},
|
||||
eastLiberty: {
|
||||
width: "50%",
|
||||
height: "2%",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
},
|
||||
westLiberty: {
|
||||
width: "50%",
|
||||
height: "2%",
|
||||
top: "50%",
|
||||
left: "0",
|
||||
},
|
||||
coordinates: {
|
||||
color: "white",
|
||||
fontFamily: `"Lucida Console", "Lucida Sans Unicode", "Fira Mono", Consolas, "Courier New", Courier, monospace, "Times New Roman"`,
|
||||
fontSize: "calc(min(1.3vw, 12px))",
|
||||
display: "none",
|
||||
position: "relative",
|
||||
top: "15%",
|
||||
left: "8%",
|
||||
zIndex: "10",
|
||||
userSelect: "none",
|
||||
},
|
||||
priorStoneTrad: {
|
||||
display: "none",
|
||||
outlineStyle: "solid",
|
||||
outlineWidth: "4px",
|
||||
outlineColor: "transparent",
|
||||
borderRadius: "50%",
|
||||
width: "50%",
|
||||
height: "50%",
|
||||
margin: "25%",
|
||||
background: "none",
|
||||
position: "absolute",
|
||||
zIndex: "10",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const boardStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
tab: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
whiteSpace: "pre",
|
||||
height: "50px",
|
||||
minHeight: "unset",
|
||||
width: "210px",
|
||||
},
|
||||
gameboardWrapper: {
|
||||
position: "relative",
|
||||
width: "calc(min(100vw - 250px, 90vh - 150px, 800px))",
|
||||
height: "calc(min(100vw - 250px, 90vh - 150px, 800px))",
|
||||
padding: "5px",
|
||||
},
|
||||
boardFrame: {
|
||||
position: "relative",
|
||||
width: "752px",
|
||||
},
|
||||
statusPageGameboard: {
|
||||
position: "relative",
|
||||
width: "calc(min(400px, max(60vw - 250px, 300px)))",
|
||||
height: "calc(min(400px, max(60vw - 250px, 300px)))",
|
||||
},
|
||||
historyPageGameboard: {
|
||||
position: "relative",
|
||||
width: "calc(min(300px, max(60vw - 250px, 250px)))",
|
||||
height: "calc(min(300px, max(60vw - 250px, 250px)))",
|
||||
},
|
||||
statusPageScore: {
|
||||
width: "400px",
|
||||
paddingLeft: "20px",
|
||||
},
|
||||
factionStatus: {
|
||||
padding: "10px",
|
||||
margin: "10px",
|
||||
borderWidth: "1px",
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.colors.success,
|
||||
width: "320px",
|
||||
},
|
||||
board: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
},
|
||||
traditional: {
|
||||
backgroundColor: "#ca973e",
|
||||
},
|
||||
opponentName: {
|
||||
paddingTop: "3px",
|
||||
paddingBottom: "5px",
|
||||
paddingRight: "10px",
|
||||
},
|
||||
opponentLabel: {
|
||||
padding: "3px 10px 5px 10px",
|
||||
},
|
||||
opponentTitle: {
|
||||
padding: "10px 0 0 0",
|
||||
},
|
||||
flavorText: {
|
||||
minHeight: "120px",
|
||||
padding: "0px 12px",
|
||||
},
|
||||
link: {
|
||||
textDecoration: "underline",
|
||||
opacity: 0.7,
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
inlineFlexBox: {
|
||||
display: "inline-flex",
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
resetBoard: {
|
||||
width: "200px",
|
||||
},
|
||||
buttonHighlight: {
|
||||
borderStyle: "solid",
|
||||
borderWidth: "8px",
|
||||
borderColor: theme.colors.success,
|
||||
padding: "0 12px",
|
||||
width: "200px",
|
||||
animation: `$fadeLoop 600ms ${theme.transitions.easing.easeInOut} infinite alternate`,
|
||||
},
|
||||
"@keyframes fadeLoop": {
|
||||
"0%": {
|
||||
opacity: 0.6,
|
||||
},
|
||||
"100%": {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
scoreBox: {
|
||||
display: "inline-flex",
|
||||
flexDirection: "row",
|
||||
whiteSpace: "pre",
|
||||
padding: "10px 30px",
|
||||
},
|
||||
searchBox: {
|
||||
maxWidth: "550px",
|
||||
minHeight: "460px",
|
||||
},
|
||||
fiveByFive: {
|
||||
height: "20%",
|
||||
"& $fiveByFive": {
|
||||
width: "20%",
|
||||
height: "100%",
|
||||
},
|
||||
},
|
||||
sevenBySeven: {
|
||||
height: "14%",
|
||||
"& $sevenBySeven": {
|
||||
width: "14%",
|
||||
height: "100%",
|
||||
},
|
||||
},
|
||||
nineByNine: {
|
||||
height: "11%",
|
||||
"& $nineByNine": {
|
||||
width: "11%",
|
||||
height: "100%",
|
||||
},
|
||||
},
|
||||
thirteenByThirteen: {
|
||||
height: "7.5%",
|
||||
"& $thirteenByThirteen": {
|
||||
width: "7.5%",
|
||||
height: "100%",
|
||||
},
|
||||
},
|
||||
nineteenByNineteen: {
|
||||
height: "5.2%",
|
||||
"& $nineteenByNineteen": {
|
||||
width: "5.2%",
|
||||
height: "100%",
|
||||
},
|
||||
},
|
||||
background: {
|
||||
position: "absolute",
|
||||
opacity: 0.09,
|
||||
color: theme.colors.white,
|
||||
fontFamily: "monospace",
|
||||
fontSize: "calc(min(.65vh - 2px, 0.65vw - 2px))",
|
||||
whiteSpace: "pre",
|
||||
pointerEvents: "none",
|
||||
paddingTop: "15px",
|
||||
},
|
||||
bitverseBackground: {
|
||||
"&$background": {
|
||||
fontSize: "calc(min(.83vh - 1px, 0.72vw, 7.856px))",
|
||||
opacity: 0.11,
|
||||
},
|
||||
},
|
||||
instructionScroller: {
|
||||
height: "calc(100vh - 80px)",
|
||||
overflowY: "scroll",
|
||||
marginTop: "10px",
|
||||
},
|
||||
instructionBoard: {
|
||||
width: "350px",
|
||||
height: "350px",
|
||||
},
|
||||
instructionBoardWrapper: {
|
||||
maxWidth: "400px",
|
||||
minHeight: "485px",
|
||||
marginRight: "20px",
|
||||
},
|
||||
instructionsBlurb: {
|
||||
width: "60%",
|
||||
minWidth: "500px",
|
||||
marginRight: "20px",
|
||||
},
|
||||
translucent: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
cellNone: {
|
||||
borderBottom: "none",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
color: theme.colors.success,
|
||||
},
|
||||
cellBottomPadding: {
|
||||
paddingBottom: "20px",
|
||||
},
|
||||
keyText: {
|
||||
paddingTop: "10px",
|
||||
color: theme.colors.int,
|
||||
},
|
||||
scoreModal: {
|
||||
width: "400px",
|
||||
},
|
||||
centeredText: {
|
||||
textAlign: "center",
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Board, boardSizes, BoardState, PointState } from "./goConstants";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
import { Player } from "@player";
|
||||
import { floor } from "./boardState";
|
||||
|
||||
type rand = (n1: number, n2: number) => number;
|
||||
|
||||
export function addObstacles(boardState: BoardState) {
|
||||
const rng = new WHRNG(Player.totalPlaytime);
|
||||
const random = (n1: number, n2: number) => n1 + floor((n2 - n1 + 1) * rng.random());
|
||||
|
||||
const shouldRemoveCorner = !random(0, 4);
|
||||
const shouldRemoveRows = !shouldRemoveCorner && !random(0, 4);
|
||||
const shouldAddCenterBreak = !shouldRemoveCorner && !shouldRemoveRows && random(0, 3);
|
||||
const obstacleTypeCount = +shouldRemoveCorner + +shouldRemoveRows + +shouldAddCenterBreak;
|
||||
|
||||
const edgeDeadCount = random(0, (getScale(boardState.board) + 2 - obstacleTypeCount) * 1.5);
|
||||
|
||||
if (shouldRemoveCorner) {
|
||||
boardState.board = addDeadCorners(boardState.board, random);
|
||||
}
|
||||
|
||||
if (shouldAddCenterBreak) {
|
||||
boardState.board = addCenterBreak(boardState.board, random);
|
||||
}
|
||||
|
||||
boardState.board = randomizeRotation(boardState.board, random);
|
||||
|
||||
if (shouldRemoveRows) {
|
||||
boardState.board = removeRows(boardState.board, random);
|
||||
}
|
||||
|
||||
boardState.board = addDeadNodesToEdge(boardState.board, random, edgeDeadCount);
|
||||
|
||||
boardState.board = resetCoordinates(boardState.board);
|
||||
}
|
||||
|
||||
export function resetCoordinates(board: Board) {
|
||||
const size = board[0].length;
|
||||
for (let x = 0; x < size; x++) {
|
||||
for (let y = 0; y < size; y++) {
|
||||
const point = board[x]?.[y];
|
||||
if (point) {
|
||||
point.x = x;
|
||||
point.y = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
return board;
|
||||
}
|
||||
|
||||
function getScale(board: Board) {
|
||||
return boardSizes.indexOf(board[0].length);
|
||||
}
|
||||
|
||||
function removeRows(board: Board, random: rand) {
|
||||
const rowsToRemove = Math.max(random(-2, getScale(board)), 1);
|
||||
for (let i = 0; i < rowsToRemove; i++) {
|
||||
board[i] = board[i].map(() => null);
|
||||
}
|
||||
board = rotateNTimes(board, 3);
|
||||
return board;
|
||||
}
|
||||
|
||||
function addDeadNodesToEdge(board: Board, random: rand, maxPerEdge: number) {
|
||||
const size = board[0].length;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const count = random(0, maxPerEdge);
|
||||
for (let j = 0; j < count; j++) {
|
||||
const yIndex = Math.max(random(-2, size - 1), 0);
|
||||
board[0][yIndex] = null;
|
||||
}
|
||||
board = rotate90Degrees(board);
|
||||
}
|
||||
|
||||
return board;
|
||||
}
|
||||
|
||||
function addDeadCorners(board: Board, random: rand) {
|
||||
const scale = getScale(board) + 1;
|
||||
|
||||
addDeadCorner(board, random, scale);
|
||||
|
||||
if (!random(0, 3)) {
|
||||
board = rotate90Degrees(board);
|
||||
board = rotate90Degrees(board);
|
||||
|
||||
addDeadCorner(board, random, scale - 2);
|
||||
}
|
||||
|
||||
return randomizeRotation(board, random);
|
||||
}
|
||||
|
||||
function addDeadCorner(board: Board, random: rand, size: number) {
|
||||
let currentSize = size;
|
||||
for (let i = 0; i < size && i < currentSize; i++) {
|
||||
random(0, 1) && currentSize--;
|
||||
board[i].forEach((point, index) => index < currentSize && point && (board[point.x][point.y] = null));
|
||||
}
|
||||
return board;
|
||||
}
|
||||
|
||||
function addCenterBreak(board: Board, random: rand) {
|
||||
const size = board[0].length;
|
||||
const maxOffset = getScale(board);
|
||||
const xIndex = random(0, maxOffset * 2) - maxOffset + floor(size / 2);
|
||||
const length = random(1, floor(size / 2 - 1));
|
||||
|
||||
board[xIndex] = board[xIndex].map((point, index) => (index < length ? null : point));
|
||||
|
||||
return randomizeRotation(board, random);
|
||||
}
|
||||
|
||||
function randomizeRotation(board: Board, random: rand) {
|
||||
return rotateNTimes(board, random(0, 3));
|
||||
}
|
||||
|
||||
function rotateNTimes(board: Board, rotations: number) {
|
||||
for (let i = 0; i < rotations; i++) {
|
||||
board = rotate90Degrees(board);
|
||||
}
|
||||
return board;
|
||||
}
|
||||
|
||||
export function rotate90Degrees(board: Board) {
|
||||
return board[0].map((_, index: number) => board.map((row: (PointState | null)[]) => row[index]).reverse());
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
|
||||
import { getGoPlayerStartingState, opponentDetails, opponentList, opponents } from "../boardState/goConstants";
|
||||
import { Player } from "@player";
|
||||
import { defaultMultipliers, mergeMultipliers, Multipliers } from "../../PersonObjects/Multipliers";
|
||||
import { PlayerObject } from "../../PersonObjects/Player/PlayerObject";
|
||||
import { formatPercent } from "../../ui/formatNumber";
|
||||
import { getPlayerStats } from "../boardAnalysis/scoring";
|
||||
|
||||
/**
|
||||
* Calculates the effect size of the given player boost, based on the node power (points based on number of subnet
|
||||
* nodes captured and player wins) and effect power (scalar for individual boosts)
|
||||
*/
|
||||
export function CalculateEffect(nodes: number, faction: opponents): number {
|
||||
const power = getEffectPowerForFaction(faction);
|
||||
const sourceFileBonus = Player.sourceFileLvl(14) ? 1.25 : 1;
|
||||
return (
|
||||
1 + Math.log(nodes + 1) * Math.pow(nodes + 1, 0.33) * 0.005 * power * currentNodeMults.GoPower * sourceFileBonus
|
||||
);
|
||||
}
|
||||
|
||||
export function getMaxFavor() {
|
||||
const sourceFileLevel = Player.sourceFileLvl(14);
|
||||
|
||||
if (sourceFileLevel === 1) {
|
||||
return 90;
|
||||
}
|
||||
if (sourceFileLevel === 2) {
|
||||
return 100;
|
||||
}
|
||||
if (sourceFileLevel >= 3) {
|
||||
return 120;
|
||||
}
|
||||
|
||||
return 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a formatted description of the current bonus from this faction
|
||||
*/
|
||||
export function getBonusText(opponent: opponents) {
|
||||
const nodePower = getPlayerStats(opponent).nodePower;
|
||||
const effectPercent = formatPercent(CalculateEffect(nodePower, opponent) - 1);
|
||||
const effectDescription = getEffectTypeForFaction(opponent);
|
||||
return `${effectPercent} ${effectDescription}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player object, using the multipliers gained from node power for each faction
|
||||
*/
|
||||
export function updateGoMults(): void {
|
||||
const mults = calculateMults();
|
||||
Player.mults = mergeMultipliers(Player.mults, mults);
|
||||
Player.updateSkillLevels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a multiplier object based on the player's total node power for each faction
|
||||
*/
|
||||
function calculateMults(): Multipliers {
|
||||
const mults = defaultMultipliers();
|
||||
[...opponentList, opponents.w0r1d_d43m0n].forEach((opponent) => {
|
||||
if (!Player.go?.status?.[opponent]) {
|
||||
Player.go = getGoPlayerStartingState();
|
||||
}
|
||||
|
||||
const effect = CalculateEffect(getPlayerStats(opponent).nodePower, opponent);
|
||||
switch (opponent) {
|
||||
case opponents.Netburners:
|
||||
mults.hacknet_node_money *= effect;
|
||||
break;
|
||||
case opponents.SlumSnakes:
|
||||
mults.crime_success *= effect;
|
||||
break;
|
||||
case opponents.TheBlackHand:
|
||||
mults.hacking_money *= effect;
|
||||
break;
|
||||
case opponents.Tetrads:
|
||||
mults.strength *= effect;
|
||||
mults.dexterity *= effect;
|
||||
mults.agility *= effect;
|
||||
break;
|
||||
case opponents.Daedalus:
|
||||
mults.company_rep *= effect;
|
||||
mults.faction_rep *= effect;
|
||||
break;
|
||||
case opponents.Illuminati:
|
||||
mults.hacking_speed *= effect;
|
||||
break;
|
||||
case opponents.w0r1d_d43m0n:
|
||||
mults.hacking *= effect;
|
||||
break;
|
||||
}
|
||||
});
|
||||
return mults;
|
||||
}
|
||||
|
||||
export function resetGoNodePower(player: PlayerObject) {
|
||||
opponentList.forEach((opponent) => {
|
||||
player.go.status[opponent].nodePower = 0;
|
||||
player.go.status[opponent].nodes = 0;
|
||||
player.go.status[opponent].winStreak = 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function playerHasDiscoveredGo() {
|
||||
const playedGame = Player.go.boardState.history.length || Player.go.previousGameFinalBoardState?.history?.length;
|
||||
const hasRecords = opponentList.find((opponent) => getPlayerStats(opponent).wins + getPlayerStats(opponent).losses);
|
||||
const isInBn14 = Player.bitNodeN === 14;
|
||||
|
||||
// TODO: remove this once testing is completed
|
||||
const isInTesting = true;
|
||||
|
||||
return !!(playedGame || hasRecords || isInBn14 || isInTesting);
|
||||
}
|
||||
|
||||
function getEffectPowerForFaction(opponent: opponents) {
|
||||
return opponentDetails[opponent].bonusPower;
|
||||
}
|
||||
|
||||
export function getEffectTypeForFaction(opponent: opponents) {
|
||||
return opponentDetails[opponent].bonusDescription;
|
||||
}
|
||||
|
||||
export function getWinstreakMultiplier(winStreak: number, previousWinStreak: number) {
|
||||
if (winStreak < 0) {
|
||||
return 0.5;
|
||||
}
|
||||
// If you break a dry streak, gain extra bonus based on the length of the dry streak (up to 5x bonus)
|
||||
if (previousWinStreak < 0 && winStreak > 0) {
|
||||
const dryStreakBroken = -1 * previousWinStreak;
|
||||
return 1 + 0.5 * Math.min(dryStreakBroken, 8);
|
||||
}
|
||||
// Win streak bonus caps at x3
|
||||
return 1 + 0.25 * Math.min(winStreak, 8);
|
||||
}
|
||||
|
||||
export function getDifficultyMultiplier(komi: number, boardSize: number) {
|
||||
const isTinyBoardVsIlluminati = boardSize === 5 && komi === opponentDetails[opponents.Illuminati].komi;
|
||||
return isTinyBoardVsIlluminati ? 8 : (komi + 0.5) * 0.25;
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
import { BoardState, opponentList, Play, playerColors, playTypes, validityReason } from "../boardState/goConstants";
|
||||
import { getMove, sleep } from "../boardAnalysis/goAI";
|
||||
import { Player } from "@player";
|
||||
import {
|
||||
getNewBoardState,
|
||||
getStateCopy,
|
||||
makeMove,
|
||||
passTurn,
|
||||
updateCaptures,
|
||||
updateChains,
|
||||
} from "../boardState/boardState";
|
||||
import { evaluateIfMoveIsValid, getControlledSpace, getSimplifiedBoardState } from "../boardAnalysis/boardAnalysis";
|
||||
import { getScore, resetWinstreak } from "../boardAnalysis/scoring";
|
||||
import { WorkerScript } from "../../Netscript/WorkerScript";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
|
||||
/**
|
||||
* Pass player's turn and await the opponent's response (or logs the end of the game if both players pass)
|
||||
*/
|
||||
export async function handlePassTurn(logger: (s: string) => void) {
|
||||
passTurn(Player.go.boardState, playerColors.black);
|
||||
if (Player.go.boardState.previousPlayer === null) {
|
||||
logEndGame(logger);
|
||||
return Promise.resolve({
|
||||
type: playTypes.gameOver,
|
||||
x: -1,
|
||||
y: -1,
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
return getAIMove(logger, Player.go.boardState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and applies the player's router placement
|
||||
*/
|
||||
export async function makePlayerMove(logger: (s: string) => void, x: number, y: number) {
|
||||
const validity = evaluateIfMoveIsValid(Player.go.boardState, x, y, playerColors.black);
|
||||
const result = makeMove(Player.go.boardState, x, y, playerColors.black);
|
||||
|
||||
if (validity !== validityReason.valid || !result) {
|
||||
await sleep(500);
|
||||
logger(`ERROR: Invalid move: ${validity}`);
|
||||
return Promise.resolve(invalidMoveResponse);
|
||||
}
|
||||
|
||||
logger(`Go move played: ${x}, ${y}`);
|
||||
|
||||
const playerUpdatedBoard = getStateCopy(result);
|
||||
return getAIMove(logger, playerUpdatedBoard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a move from the current faction in response to the player's move
|
||||
*/
|
||||
async function getAIMove(logger: (s: string) => void, boardState: BoardState, success = true): Promise<Play> {
|
||||
let resolve: (value: Play) => void;
|
||||
const aiMoveResult = new Promise<Play>((res) => {
|
||||
resolve = res;
|
||||
});
|
||||
|
||||
getMove(boardState, playerColors.white, Player.go.boardState.ai).then(async (result) => {
|
||||
// If a new game has started while this async code ran, drop it
|
||||
if (boardState.history.length > Player.go.boardState.history.length) {
|
||||
return resolve({ ...result, success: false });
|
||||
}
|
||||
if (result.type === "gameOver") {
|
||||
logEndGame(logger);
|
||||
}
|
||||
if (result.type !== playTypes.move) {
|
||||
Player.go.boardState = boardState;
|
||||
return resolve({ ...result, success });
|
||||
}
|
||||
|
||||
const aiUpdatedBoard = makeMove(boardState, result.x, result.y, playerColors.white);
|
||||
if (!aiUpdatedBoard) {
|
||||
boardState.previousPlayer = playerColors.white;
|
||||
Player.go.boardState = boardState;
|
||||
logger(`Invalid AI move attempted: ${result.x}, ${result.y}. This should not happen.`);
|
||||
} else {
|
||||
Player.go.boardState = aiUpdatedBoard;
|
||||
logger(`Opponent played move: ${result.x}, ${result.y}`);
|
||||
}
|
||||
|
||||
await sleep(200);
|
||||
resolve({ ...result, success });
|
||||
});
|
||||
return aiMoveResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a grid of booleans indicating if the coordinates at that location are a valid move for the player (black pieces)
|
||||
*/
|
||||
export function getValidMoves() {
|
||||
const boardState = Player.go.boardState;
|
||||
// Map the board matrix into true/false values
|
||||
return boardState.board.map((column, x) =>
|
||||
column.reduce((validityArray: boolean[], point, y) => {
|
||||
const isValid = evaluateIfMoveIsValid(boardState, x, y, playerColors.black) === validityReason.valid;
|
||||
validityArray.push(isValid);
|
||||
return validityArray;
|
||||
}, []),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a grid with an ID for each contiguous chain of same-state nodes (excluding dead/offline nodes)
|
||||
*/
|
||||
export function getChains() {
|
||||
const chains: string[] = [];
|
||||
// Turn the internal chain IDs into nice consecutive numbers for display to the player
|
||||
return Player.go.boardState.board.map((column) =>
|
||||
column.reduce((chainIdArray: (number | null)[], point) => {
|
||||
if (!point) {
|
||||
chainIdArray.push(null);
|
||||
return chainIdArray;
|
||||
}
|
||||
if (!chains.includes(point.chain)) {
|
||||
chains.push(point.chain);
|
||||
}
|
||||
chainIdArray.push(chains.indexOf(point.chain));
|
||||
return chainIdArray;
|
||||
}, []),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a grid of numbers representing the number of open-node connections each player-owned chain has.
|
||||
*/
|
||||
export function getLiberties() {
|
||||
return Player.go.boardState.board.map((column) =>
|
||||
column.reduce((libertyArray: number[], point) => {
|
||||
libertyArray.push(point?.liberties?.length || -1);
|
||||
return libertyArray;
|
||||
}, []),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a grid indicating which player, if any, controls the empty nodes by fully encircling it with their routers
|
||||
*/
|
||||
export function getControlledEmptyNodes() {
|
||||
const boardState = Player.go.boardState;
|
||||
const controlled = getControlledSpace(boardState);
|
||||
return controlled.map((column, x: number) =>
|
||||
column.reduce((ownedPoints: string, owner: playerColors, y: number) => {
|
||||
if (owner === playerColors.white) {
|
||||
return ownedPoints + "O";
|
||||
}
|
||||
if (owner === playerColors.black) {
|
||||
return ownedPoints + "X";
|
||||
}
|
||||
if (!boardState.board[x][y]) {
|
||||
return ownedPoints + "#";
|
||||
}
|
||||
if (boardState.board[x][y]?.player === playerColors.empty) {
|
||||
return ownedPoints + "?";
|
||||
}
|
||||
return ownedPoints + ".";
|
||||
}, ""),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle post-game logging
|
||||
*/
|
||||
function logEndGame(logger: (s: string) => void) {
|
||||
const boardState = Player.go.boardState;
|
||||
const score = getScore(boardState);
|
||||
logger(
|
||||
`Subnet complete! Final score: ${boardState.ai}: ${score[playerColors.white].sum}, Player: ${
|
||||
score[playerColors.black].sum
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the board, resets winstreak if applicable
|
||||
*/
|
||||
export function resetBoardState(error: (s: string) => void, opponentString: string, boardSize: number) {
|
||||
const opponent = opponentList.find((faction) => faction === opponentString);
|
||||
|
||||
if (![5, 7, 9, 13].includes(boardSize)) {
|
||||
error(`Invalid subnet size requested (${boardSize}, size must be 5, 7, 9, or 13`);
|
||||
return;
|
||||
}
|
||||
if (!opponent) {
|
||||
error(`Invalid opponent requested (${opponentString}), valid options are ${opponentList.join(", ")}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const oldBoardState = Player.go.boardState;
|
||||
if (oldBoardState.previousPlayer !== null && oldBoardState.history.length) {
|
||||
resetWinstreak(oldBoardState.ai, false);
|
||||
}
|
||||
|
||||
Player.go.boardState = getNewBoardState(boardSize, opponent, true);
|
||||
return getSimplifiedBoardState(Player.go.boardState.board);
|
||||
}
|
||||
|
||||
/** Validate singularity access by throwing an error if the player does not have access. */
|
||||
export function checkCheatApiAccess(error: (s: string) => void): void {
|
||||
const hasSourceFile = Player.sourceFileLvl(14) > 1;
|
||||
const isBitnodeFourteenTwo = Player.sourceFileLvl(14) === 1 && Player.bitNodeN === 14;
|
||||
if (!hasSourceFile && !isBitnodeFourteenTwo) {
|
||||
error(
|
||||
`The go.cheat API requires Source-File 14.2 to run, a power up you obtain later in the game.
|
||||
It will be very obvious when and how you can obtain it.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const invalidMoveResponse: Play = {
|
||||
success: false,
|
||||
type: playTypes.invalid,
|
||||
x: -1,
|
||||
y: -1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if the attempted cheat move is successful. If so, applies the cheat via the callback, and gets the opponent's response.
|
||||
*
|
||||
* If it fails, determines if the player's turn is skipped, or if the player is ejected from the subnet.
|
||||
*/
|
||||
export async function determineCheatSuccess(
|
||||
logger: (s: string) => void,
|
||||
callback: () => void,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
): Promise<Play> {
|
||||
const state = Player.go.boardState;
|
||||
const rng = new WHRNG(Player.totalPlaytime);
|
||||
// If cheat is successful, run callback
|
||||
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount)) {
|
||||
callback();
|
||||
state.cheatCount++;
|
||||
return getAIMove(logger, state, true);
|
||||
}
|
||||
// If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly losing
|
||||
else if (state.cheatCount && (ejectRngOverride ?? rng.random()) < 0.1) {
|
||||
logger(`Cheat failed! You have been ejected from the subnet.`);
|
||||
resetBoardState(logger, state.ai, state.board[0].length);
|
||||
return {
|
||||
type: playTypes.gameOver,
|
||||
x: -1,
|
||||
y: -1,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
// If the cheat fails, your turn is skipped
|
||||
else {
|
||||
logger(`Cheat failed. Your turn has been skipped.`);
|
||||
passTurn(state, playerColors.black, false);
|
||||
state.cheatCount++;
|
||||
return getAIMove(logger, state, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cheating success rate scales with player's crime success rate, and decreases with prior cheat attempts.
|
||||
*/
|
||||
export function cheatSuccessChance(cheatCount: number) {
|
||||
return Math.min(0.6 * (0.8 ^ cheatCount) * Player.mults.crime_success, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw a runtime error that halts the player's script
|
||||
*/
|
||||
export function throwError(ws: WorkerScript, errorMessage: string) {
|
||||
throw `RUNTIME ERROR\n${ws.name}@${ws.hostname} (PID - ${ws.pid})\n\n ${errorMessage}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to remove an existing router from the board. Can fail. If failed, can immediately end the game
|
||||
*/
|
||||
export function cheatRemoveRouter(
|
||||
logger: (s: string) => void,
|
||||
x: number,
|
||||
y: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
) {
|
||||
const point = Player.go.boardState.board[x][y];
|
||||
if (!point) {
|
||||
logger(`The node ${x},${y} is offline, so you cannot clear this point with removeRouter().`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
if (point.player === playerColors.empty) {
|
||||
logger(`The point ${x},${y} does not have a router on it, so you cannot clear this point with removeRouter().`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
point.player = playerColors.empty;
|
||||
Player.go.boardState = updateChains(Player.go.boardState);
|
||||
Player.go.boardState.previousPlayer = playerColors.black;
|
||||
logger(`Cheat successful. The point ${x},${y} was cleared.`);
|
||||
},
|
||||
successRngOverride,
|
||||
ejectRngOverride,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts play two moves at once. Can fail. If failed, can immediately end the game
|
||||
*/
|
||||
export function cheatPlayTwoMoves(
|
||||
logger: (s: string) => void,
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
) {
|
||||
const point1 = Player.go.boardState.board[x1][y1];
|
||||
if (!point1) {
|
||||
logger(`The node ${x1},${y1} is offline, so you cannot place a router there.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
if (point1.player !== playerColors.empty) {
|
||||
logger(`The point ${x1},${y1} is not empty, so you cannot place a router there.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
const point2 = Player.go.boardState.board[x2][y2];
|
||||
if (!point2) {
|
||||
logger(`The node ${x2},${y2} is offline, so you cannot place a router there.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
if (point2.player !== playerColors.empty) {
|
||||
logger(`The point ${x2},${y2} is not empty, so you cannot place a router there.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
point1.player = playerColors.black;
|
||||
point2.player = playerColors.black;
|
||||
Player.go.boardState = updateCaptures(Player.go.boardState, playerColors.black);
|
||||
Player.go.boardState.previousPlayer = playerColors.black;
|
||||
|
||||
logger(`Cheat successful. Two go moves played: ${x1},${y1} and ${x2},${y2}`);
|
||||
},
|
||||
successRngOverride,
|
||||
ejectRngOverride,
|
||||
);
|
||||
}
|
||||
|
||||
export function cheatRepairOfflineNode(
|
||||
logger: (s: string) => void,
|
||||
x: number,
|
||||
y: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
) {
|
||||
const point = Player.go.boardState.board[x][y];
|
||||
if (point) {
|
||||
logger(`The node ${x},${y} is not offline, so you cannot repair the node.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
Player.go.boardState.board[x][y] = {
|
||||
chain: "",
|
||||
liberties: null,
|
||||
y,
|
||||
player: playerColors.empty,
|
||||
x,
|
||||
};
|
||||
Player.go.boardState = updateChains(Player.go.boardState);
|
||||
Player.go.boardState.previousPlayer = playerColors.black;
|
||||
logger(`Cheat successful. The point ${x},${y} was repaired.`);
|
||||
},
|
||||
successRngOverride,
|
||||
ejectRngOverride,
|
||||
);
|
||||
}
|
||||
|
||||
export function cheatDestroyNode(
|
||||
logger: (s: string) => void,
|
||||
x: number,
|
||||
y: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
) {
|
||||
const point = Player.go.boardState.board[x][y];
|
||||
if (!point) {
|
||||
logger(`The node ${x},${y} is already offline, so you cannot destroy the node.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
if (point.player !== playerColors.empty) {
|
||||
logger(`The point ${x},${y} is not empty, so you cannot destroy this node.`);
|
||||
return invalidMoveResponse;
|
||||
}
|
||||
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
Player.go.boardState.board[x][y] = null;
|
||||
Player.go.boardState = updateChains(Player.go.boardState);
|
||||
Player.go.boardState.previousPlayer = playerColors.black;
|
||||
logger(`Cheat successful. The point ${x},${y} was repaired.`);
|
||||
},
|
||||
successRngOverride,
|
||||
ejectRngOverride,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useMemo } from "react";
|
||||
import Grid from "@mui/material/Grid";
|
||||
|
||||
import { getSizeClass, GoPoint } from "./GoPoint";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { getAllValidMoves, getControlledSpace } from "../boardAnalysis/boardAnalysis";
|
||||
import { BoardState, opponents, playerColors } from "../boardState/goConstants";
|
||||
|
||||
interface IProps {
|
||||
boardState: BoardState;
|
||||
traditional: boolean;
|
||||
clickHandler: (x: number, y: number) => any;
|
||||
hover: boolean;
|
||||
}
|
||||
|
||||
export function GoGameboard({ boardState, traditional, clickHandler, hover }: IProps): React.ReactElement {
|
||||
useRerender(400);
|
||||
|
||||
const currentPlayer =
|
||||
boardState.ai !== opponents.none || boardState.previousPlayer === playerColors.white
|
||||
? playerColors.black
|
||||
: playerColors.white;
|
||||
|
||||
const availablePoints = useMemo(
|
||||
() => (hover ? getAllValidMoves(boardState, currentPlayer) : []),
|
||||
[boardState, hover, currentPlayer],
|
||||
);
|
||||
|
||||
const ownedEmptyNodes = useMemo(() => getControlledSpace(boardState), [boardState]);
|
||||
|
||||
function pointIsValid(x: number, y: number) {
|
||||
return !!availablePoints.find((point) => point.x === x && point.y === y);
|
||||
}
|
||||
|
||||
const boardSize = boardState.board[0].length;
|
||||
const classes = boardStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container id="goGameboard" className={`${classes.board} ${traditional ? classes.traditional : ""}`}>
|
||||
{boardState.board.map((row, y) => {
|
||||
const yIndex = boardState.board[0].length - y - 1;
|
||||
return (
|
||||
<Grid container key={`column_${yIndex}`} item className={getSizeClass(boardSize, classes)}>
|
||||
{row.map((point, x: number) => {
|
||||
const xIndex = x;
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
key={`point_${xIndex}_${yIndex}`}
|
||||
onClick={() => clickHandler(xIndex, yIndex)}
|
||||
className={getSizeClass(boardSize, classes)}
|
||||
>
|
||||
<GoPoint
|
||||
state={boardState}
|
||||
x={xIndex}
|
||||
y={yIndex}
|
||||
traditional={traditional}
|
||||
hover={hover}
|
||||
valid={pointIsValid(xIndex, yIndex)}
|
||||
emptyPointOwner={ownedEmptyNodes[xIndex]?.[yIndex]}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { SnackbarEvents } from "../../ui/React/Snackbar";
|
||||
import { ToastVariant } from "@enums";
|
||||
import { Box, Button, Typography } from "@mui/material";
|
||||
|
||||
import { BoardState, opponents, playerColors, playTypes, validityReason } from "../boardState/goConstants";
|
||||
import { getNewBoardState, getStateCopy, makeMove, passTurn } from "../boardState/boardState";
|
||||
import { getMove } from "../boardAnalysis/goAI";
|
||||
import { bitverseArt, weiArt } from "../boardState/asciiArt";
|
||||
import { getScore, resetWinstreak } from "../boardAnalysis/scoring";
|
||||
import { evaluateIfMoveIsValid, getAllValidMoves } from "../boardAnalysis/boardAnalysis";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { OptionSwitch } from "../../ui/React/OptionSwitch";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { Player } from "@player";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { GoScoreModal } from "./GoScoreModal";
|
||||
import { GoGameboard } from "./GoGameboard";
|
||||
import { GoSubnetSearch } from "./GoSubnetSearch";
|
||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||
|
||||
interface IProps {
|
||||
showInstructions: () => void;
|
||||
}
|
||||
|
||||
// FUTURE: bonus time?
|
||||
|
||||
/*
|
||||
// FUTURE: add AI cheating.
|
||||
* unlikely unless player cheats first
|
||||
* more common on some factions
|
||||
* play two moves that don't capture
|
||||
*/
|
||||
|
||||
export function GoGameboardWrapper({ showInstructions }: IProps): React.ReactElement {
|
||||
const rerender = useRerender(400);
|
||||
|
||||
const boardState = Player.go.boardState;
|
||||
const traditional = Settings.GoTraditionalStyle;
|
||||
const [showPriorMove, setShowPriorMove] = useState(false);
|
||||
const [opponent, setOpponent] = useState<opponents>(boardState.ai);
|
||||
const [scoreOpen, setScoreOpen] = useState(false);
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [waitingOnAI, setWaitingOnAI] = useState(false);
|
||||
|
||||
const classes = boardStyles();
|
||||
const boardSize = boardState.board[0].length;
|
||||
const currentPlayer = boardState.previousPlayer === playerColors.white ? playerColors.black : playerColors.white;
|
||||
const score = getScore(boardState);
|
||||
|
||||
// Only run this once on first component mount, to handle scenarios where the game was saved or closed while waiting on the AI to make a move
|
||||
useEffect(() => {
|
||||
if (boardState.previousPlayer === playerColors.black && !waitingOnAI) {
|
||||
takeAiTurn(Player.go.boardState);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
async function clickHandler(x: number, y: number) {
|
||||
if (showPriorMove) {
|
||||
SnackbarEvents.emit(
|
||||
`Currently showing a past board state. Please disable "Show previous move" to continue.`,
|
||||
ToastVariant.WARNING,
|
||||
2000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock the board when it isn't the player's turn
|
||||
const gameOver = boardState.previousPlayer === null;
|
||||
const notYourTurn = boardState.previousPlayer === playerColors.black && opponent !== opponents.none;
|
||||
if (notYourTurn) {
|
||||
SnackbarEvents.emit(`It is not your turn to play.`, ToastVariant.WARNING, 2000);
|
||||
return;
|
||||
}
|
||||
if (gameOver) {
|
||||
SnackbarEvents.emit(`The game is complete, please reset to continue.`, ToastVariant.WARNING, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
const validity = evaluateIfMoveIsValid(boardState, x, y, currentPlayer);
|
||||
if (validity != validityReason.valid) {
|
||||
SnackbarEvents.emit(`Invalid move: ${validity}`, ToastVariant.ERROR, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedBoard = makeMove(boardState, x, y, currentPlayer);
|
||||
if (updatedBoard) {
|
||||
updateBoard(updatedBoard);
|
||||
opponent !== opponents.none && takeAiTurn(updatedBoard);
|
||||
}
|
||||
}
|
||||
|
||||
function passPlayerTurn() {
|
||||
if (boardState.previousPlayer === playerColors.white) {
|
||||
passTurn(boardState, playerColors.black);
|
||||
updateBoard(boardState);
|
||||
}
|
||||
if (boardState.previousPlayer === null) {
|
||||
endGame();
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
opponent !== opponents.none && takeAiTurn(boardState);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async function takeAiTurn(board: BoardState) {
|
||||
if (board.previousPlayer === null) {
|
||||
return;
|
||||
}
|
||||
setWaitingOnAI(true);
|
||||
const initialState = getStateCopy(board);
|
||||
const move = await getMove(initialState, playerColors.white, opponent);
|
||||
|
||||
// If a new game has started while this async code ran, just drop it
|
||||
if (boardState.history.length > Player.go.boardState.history.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (move.type === playTypes.pass) {
|
||||
SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000);
|
||||
updateBoard(initialState);
|
||||
return;
|
||||
}
|
||||
|
||||
if (move.type === playTypes.gameOver || move.x === null || move.y === null) {
|
||||
endGame(initialState);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedBoard = await makeMove(initialState, move.x, move.y, playerColors.white);
|
||||
|
||||
if (updatedBoard) {
|
||||
setTimeout(() => {
|
||||
updateBoard(updatedBoard);
|
||||
setWaitingOnAI(false);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function newSubnet() {
|
||||
setScoreOpen(false);
|
||||
setSearchOpen(true);
|
||||
}
|
||||
|
||||
function resetState(newBoardSize = boardSize, newOpponent = opponent) {
|
||||
setScoreOpen(false);
|
||||
setSearchOpen(false);
|
||||
setOpponent(newOpponent);
|
||||
if (boardState.previousPlayer !== null && boardState.history.length) {
|
||||
resetWinstreak(boardState.ai, false);
|
||||
}
|
||||
|
||||
const newBoardState = getNewBoardState(newBoardSize, newOpponent, false);
|
||||
updateBoard(newBoardState);
|
||||
}
|
||||
|
||||
function updateBoard(initialBoardState: BoardState) {
|
||||
Player.go.boardState = getStateCopy(initialBoardState);
|
||||
rerender();
|
||||
}
|
||||
|
||||
function endGame(state = boardState) {
|
||||
setScoreOpen(true);
|
||||
updateBoard(state);
|
||||
}
|
||||
|
||||
function getPriorMove() {
|
||||
if (!boardState.history.length) {
|
||||
return boardState;
|
||||
}
|
||||
const priorBoard = boardState.history.slice(-1)[0];
|
||||
const updatedState = getStateCopy(boardState);
|
||||
updatedState.board = priorBoard;
|
||||
updatedState.previousPlayer =
|
||||
boardState.previousPlayer === playerColors.black ? playerColors.white : playerColors.black;
|
||||
|
||||
return updatedState;
|
||||
}
|
||||
|
||||
function showPreviousMove(newValue: boolean) {
|
||||
if (boardState.history.length) {
|
||||
setShowPriorMove(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
function setTraditional(newValue: boolean) {
|
||||
Settings.GoTraditionalStyle = newValue;
|
||||
}
|
||||
|
||||
const endGameAvailable = boardState.previousPlayer === playerColors.white && boardState.passCount;
|
||||
const noLegalMoves = useMemo(
|
||||
() => boardState.previousPlayer === playerColors.white && !getAllValidMoves(boardState, playerColors.black).length,
|
||||
[boardState],
|
||||
);
|
||||
const disablePassButton =
|
||||
opponent !== opponents.none && boardState.previousPlayer === playerColors.black && waitingOnAI;
|
||||
|
||||
const scoreBoxText = boardState.history.length
|
||||
? `Score: Black: ${score[playerColors.black].sum} White: ${score[playerColors.white].sum}`
|
||||
: "Place a router to begin!";
|
||||
|
||||
const getPassButtonLabel = () => {
|
||||
if (endGameAvailable) {
|
||||
return "End Game";
|
||||
}
|
||||
if (boardState.previousPlayer === null) {
|
||||
return "View Final Score";
|
||||
}
|
||||
if (boardState.previousPlayer === playerColors.black && waitingOnAI) {
|
||||
return "Waiting for opponent";
|
||||
}
|
||||
const currentPlayer = boardState.previousPlayer === playerColors.black ? playerColors.white : playerColors.black;
|
||||
return `Pass Turn${boardState.ai === opponents.none ? ` (${currentPlayer})` : ""}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<GoSubnetSearch
|
||||
open={searchOpen}
|
||||
search={resetState}
|
||||
cancel={() => setSearchOpen(false)}
|
||||
showInstructions={showInstructions}
|
||||
/>
|
||||
<GoScoreModal
|
||||
open={scoreOpen}
|
||||
onClose={() => setScoreOpen(false)}
|
||||
newSubnet={() => newSubnet()}
|
||||
finalScore={score}
|
||||
opponent={opponent}
|
||||
></GoScoreModal>
|
||||
<div className={classes.boardFrame}>
|
||||
{traditional ? (
|
||||
""
|
||||
) : (
|
||||
<div className={`${classes.background} ${boardSize === 19 ? classes.bitverseBackground : ""}`}>
|
||||
{boardSize === 19 ? bitverseArt : weiArt}
|
||||
</div>
|
||||
)}
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||
<br />
|
||||
<Typography variant={"h6"} className={classes.opponentLabel}>
|
||||
{opponent !== opponents.none ? "Subnet owner: " : ""}{" "}
|
||||
{opponent === opponents.w0r1d_d43m0n ? <CorruptableText content={opponent} /> : opponent}
|
||||
</Typography>
|
||||
<br />
|
||||
</Box>
|
||||
<div className={`${classes.gameboardWrapper} ${showPriorMove ? classes.translucent : ""}`}>
|
||||
<GoGameboard
|
||||
boardState={showPriorMove ? getPriorMove() : boardState}
|
||||
traditional={traditional}
|
||||
clickHandler={clickHandler}
|
||||
hover={!showPriorMove}
|
||||
/>
|
||||
</div>
|
||||
<Box className={classes.inlineFlexBox}>
|
||||
<Button onClick={() => setSearchOpen(true)} className={classes.resetBoard}>
|
||||
Find New Subnet
|
||||
</Button>
|
||||
<Typography className={classes.scoreBox}>{scoreBoxText}</Typography>
|
||||
<Button
|
||||
disabled={disablePassButton}
|
||||
onClick={passPlayerTurn}
|
||||
className={endGameAvailable || noLegalMoves ? classes.buttonHighlight : classes.resetBoard}
|
||||
>
|
||||
{getPassButtonLabel()}
|
||||
</Button>
|
||||
</Box>
|
||||
<div className={classes.opponentLabel}>
|
||||
<Box className={classes.inlineFlexBox}>
|
||||
<br />
|
||||
<OptionSwitch
|
||||
checked={traditional}
|
||||
onChange={(newValue) => setTraditional(newValue)}
|
||||
text="Traditional Go look"
|
||||
tooltip={<>Show stones and grid as if it was a standard Go board</>}
|
||||
/>
|
||||
<OptionSwitch
|
||||
checked={showPriorMove}
|
||||
disabled={!boardState.history.length}
|
||||
onChange={(newValue) => showPreviousMove(newValue)}
|
||||
text="Show previous move"
|
||||
tooltip={<>Show the board as it was before the last move</>}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import React from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { Grid, Table, TableBody, TableCell, TableRow, Tooltip } from "@mui/material";
|
||||
|
||||
import { opponentList, opponents } from "../boardState/goConstants";
|
||||
import { getPlayerStats, getScore } from "../boardAnalysis/scoring";
|
||||
import { Player } from "@player";
|
||||
import { GoGameboard } from "./GoGameboard";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { getBonusText, getMaxFavor } from "../effects/effect";
|
||||
import { formatNumber } from "../../ui/formatNumber";
|
||||
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
|
||||
import { getNewBoardState } from "../boardState/boardState";
|
||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||
import { showWorldDemon } from "../boardAnalysis/goAI";
|
||||
|
||||
export const GoHistoryPage = (): React.ReactElement => {
|
||||
useRerender(400);
|
||||
const classes = boardStyles();
|
||||
const priorBoard = Player.go.previousGameFinalBoardState ?? getNewBoardState(7);
|
||||
const score = getScore(priorBoard);
|
||||
const opponent = priorBoard.ai;
|
||||
const opponentsToShow = showWorldDemon() ? [...opponentList, opponents.w0r1d_d43m0n] : opponentList;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container>
|
||||
<Grid item>
|
||||
<div className={classes.statusPageScore}>
|
||||
<Typography variant="h5">Previous Subnet:</Typography>
|
||||
<GoScoreSummaryTable score={score} opponent={opponent} />
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<div className={`${classes.historyPageGameboard} ${classes.translucent}`}>
|
||||
<GoGameboard
|
||||
boardState={priorBoard}
|
||||
traditional={false}
|
||||
clickHandler={(x, y) => ({ x, y })}
|
||||
hover={false}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<br />
|
||||
<br />
|
||||
<Typography variant="h5">Faction Stats:</Typography>
|
||||
<Grid container style={{ maxWidth: "1020px" }}>
|
||||
{opponentsToShow.map((faction, index) => {
|
||||
const data = getPlayerStats(faction);
|
||||
return (
|
||||
<Grid item key={opponentsToShow[index]} className={classes.factionStatus}>
|
||||
<Typography>
|
||||
{" "}
|
||||
<strong className={classes.keyText}>
|
||||
{faction === opponents.w0r1d_d43m0n ? <CorruptableText content="????????????" /> : faction}
|
||||
</strong>
|
||||
</Typography>
|
||||
<Table sx={{ display: "table", mb: 1, width: "100%" }}>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Wins:</TableCell>
|
||||
<TableCell className={classes.cellNone}>
|
||||
{data.wins} / {data.losses + data.wins}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Current winstreak:</TableCell>
|
||||
<TableCell className={classes.cellNone}>{data.winStreak}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
|
||||
Highest winstreak:
|
||||
</TableCell>
|
||||
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
|
||||
{data.highestWinStreak}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
The total number of empty points and routers <br /> you took control of, across all subnets
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Captured nodes:</TableCell>
|
||||
<TableCell className={classes.cellNone}>{data.nodes}</TableCell>
|
||||
</TableRow>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
Node power is what stat bonuses scale from, and is gained on each completed subnet. <br />
|
||||
It is calculated from the number of nodes you control, multiplied by modifiers for the <br />
|
||||
opponent difficulty, if you won or lost, and your current winstreak.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TableRow>
|
||||
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>Node power:</TableCell>
|
||||
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
|
||||
{formatNumber(data.nodePower, 2)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
Win streaks against a faction will give you +1 favor to that faction <br />
|
||||
at certain numbers of wins (up to a max of 100 favor), <br />
|
||||
if you are currently a member of that faction
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Favor from winstreaks:</TableCell>
|
||||
<TableCell className={classes.cellNone}>
|
||||
{data.favor ?? 0} {data.favor === getMaxFavor() ? "(max)" : ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Tooltip>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<br />
|
||||
<Tooltip title={<>The total stat multiplier gained via your current node power.</>}>
|
||||
<Typography>
|
||||
<strong className={classes.keyText}>Bonus:</strong>
|
||||
<br />
|
||||
<strong className={classes.keyText}>{getBonusText(faction)}</strong>
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,243 @@
|
||||
import React from "react";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { Grid, Link, Typography } from "@mui/material";
|
||||
import { getBoardFromSimplifiedBoardState } from "../boardAnalysis/boardAnalysis";
|
||||
import { opponents, playerColors } from "../boardState/goConstants";
|
||||
import { GoTutorialChallenge } from "./GoTutorialChallenge";
|
||||
import { Router } from "../../ui/GameRoot";
|
||||
import { Page } from "../../ui/Router";
|
||||
import { getMaxFavor } from "../effects/effect";
|
||||
|
||||
const captureChallenge = (
|
||||
<GoTutorialChallenge
|
||||
state={getBoardFromSimplifiedBoardState(
|
||||
[".....", "OX...", "OXX..", "OOX.O", "OOX.."],
|
||||
opponents.none,
|
||||
playerColors.white,
|
||||
)}
|
||||
description={
|
||||
"CHALLENGE: This white network on the bottom is vulnerable! Click on the board to place a router. Capture some white pieces by cutting off their access to any empty nodes."
|
||||
}
|
||||
correctMoves={[{ x: 0, y: 0 }]}
|
||||
correctText={
|
||||
"Correct! With no open ports, the white routers are destroyed. Now you surround and control the empty nodes in the bottom-right."
|
||||
}
|
||||
incorrectText={"Unfortunately the white routers still touch at least one empty node. Hit 'Reset' to try again."}
|
||||
/>
|
||||
);
|
||||
|
||||
const saveTheNetworkChallenge = (
|
||||
<GoTutorialChallenge
|
||||
state={getBoardFromSimplifiedBoardState(
|
||||
["OO.##", "XO..#", "XX..#", "XO...", "XO..."],
|
||||
opponents.none,
|
||||
playerColors.white,
|
||||
)}
|
||||
description={
|
||||
"CHALLENGE: Your routers are in trouble! They only have one open port. Save the black network by connecting them to more empty nodes."
|
||||
}
|
||||
correctMoves={[{ x: 2, y: 2 }]}
|
||||
correctText={
|
||||
"Correct! Now the network touches three empty nodes instead of one, making it much harder to cut them off."
|
||||
}
|
||||
incorrectText={
|
||||
"Unfortunately your network can still be cut off from all empty ports in just one move by white. Hit 'Reset' to try again."
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const onlyGoodMoveChallenge = (
|
||||
<GoTutorialChallenge
|
||||
state={getBoardFromSimplifiedBoardState(
|
||||
["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."}
|
||||
correctMoves={[{ x: 2, y: 0 }]}
|
||||
correctText={
|
||||
"Correct! Now the network touches two empty nodes instead of one, making it much harder to cut them off."
|
||||
}
|
||||
incorrectText={
|
||||
"Incorrect. Your left network can still be cut off from empty ports in just one move. Also, you blocked one of your only open ports from your right network!"
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const makeTwoEyesChallenge = (
|
||||
<GoTutorialChallenge
|
||||
state={getBoardFromSimplifiedBoardState(
|
||||
["XXOO.", ".XXOO", ".XXO.", ".XXOO", "XXOO."],
|
||||
opponents.none,
|
||||
playerColors.white,
|
||||
)}
|
||||
description={
|
||||
"CHALLENGE: The black routers are only connected to one empty-node group. Place a router such that they are connected to TWO empty node groups instead."
|
||||
}
|
||||
correctMoves={[{ x: 2, y: 0 }]}
|
||||
correctText={
|
||||
"Correct! Now that your network surrounds empty nodes in multiple different areas, it is impossible for the network to be captured by white because of the suicide rule (unless you fill in your own empty nodes!)."
|
||||
}
|
||||
incorrectText={
|
||||
"Incorrect. The black network still only touches one group of open nodes. (Hint: Try dividing up the bottom open-node group.) Hit 'Reset' to try again."
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export const GoInstructionsPage = (): React.ReactElement => {
|
||||
const classes = boardStyles();
|
||||
return (
|
||||
<div className={classes.instructionScroller}>
|
||||
<>
|
||||
<Typography variant="h4">IPvGO</Typography>
|
||||
<br />
|
||||
<Typography>
|
||||
In late 2070, the .org bubble burst, and most of the newly-implemented IPvGO 'net collapsed overnight. Since
|
||||
then, various factions have been fighting over small subnets to control their computational power. These
|
||||
subnets are very valuable in the right hands, if you can wrest them from their current owners.
|
||||
</Typography>
|
||||
<br />
|
||||
<br />
|
||||
<Grid container columns={2}>
|
||||
<Grid item className={classes.instructionsBlurb}>
|
||||
<Typography variant="h5">How to take over IPvGO Subnets</Typography>
|
||||
<br />
|
||||
<Typography>
|
||||
Your goal is to control more <i>empty nodes</i> in the subnet than the faction currently holding it, by
|
||||
surrounding those open nodes with your routers.
|
||||
<br />
|
||||
<br />
|
||||
Each turn you place a router in an empty node (or pass). The router will connect to your adjacent routers,
|
||||
forming networks. A network's remaining open ports are indicated by lines heading out towards the empty
|
||||
nodes adjacent to the network.
|
||||
<br />
|
||||
<br />
|
||||
If a group of routers no longer is connected to any empty nodes, they will experience intense packet loss
|
||||
and be removed from the subnet. Make sure you ALWAYS have access to several empty nodes in each of your
|
||||
networks! A network with only one remaining open port will start to fade in and out, because it is at risk
|
||||
of being destroyed.
|
||||
<br />
|
||||
<br />
|
||||
You also can use your routers to limit your opponent's access to empty nodes as much as possible. Cut a
|
||||
network off from any empty nodes, and their entire group of routers will be removed!
|
||||
<br />
|
||||
<br />
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item className={classes.instructionBoardWrapper}>
|
||||
{captureChallenge}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<br />
|
||||
<br />
|
||||
<Grid container>
|
||||
<Grid item className={classes.instructionBoardWrapper}>
|
||||
{saveTheNetworkChallenge}
|
||||
</Grid>
|
||||
<Grid item className={classes.instructionsBlurb}>
|
||||
<Typography variant="h5">Winning the Subnet</Typography>
|
||||
<br />
|
||||
<Typography>
|
||||
The game ends when all of the open nodes on the subnet are completely surrounded by a single color, or
|
||||
when both players pass consecutively.
|
||||
<br />
|
||||
<br />
|
||||
Once the subnet is fully claimed, each player will get one point for each empty node they fully surround
|
||||
on the subnet, plus a point for each router they have. You can use the edge of the board along with your
|
||||
routers to fully surround and claim empty nodes. <br />
|
||||
<br />
|
||||
White will also get a few points (called "komi") as a home-field advantage in the subnet, and to balance
|
||||
black's advantage of having the first move.
|
||||
<br />
|
||||
<br />
|
||||
Any territory you control at the end of the game will award you stat multiplier bonuses. Winning the node
|
||||
will increase the amount gained, but is not required.
|
||||
<br />
|
||||
<br />
|
||||
Win streaks against a faction will give you +1 favor to that faction at certain numbers of wins (up to a
|
||||
max of {getMaxFavor()} favor), if you are currently a member of that faction.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<br />
|
||||
<br />
|
||||
<Grid container>
|
||||
<Grid item className={classes.instructionsBlurb}>
|
||||
<Typography variant="h5">Special Rule Details</Typography>
|
||||
<br />
|
||||
<Typography>
|
||||
* Because these subnets have fallen into disrepair, they are not always perfectly square. Dead areas, such
|
||||
as the top-left corner in the example above, are not part of the subnet. They do not count as territory,
|
||||
and do not provide open ports to adjacent routers.
|
||||
<br />
|
||||
<br />
|
||||
* You cannot suicide your own routers by cutting off access to their last remaining open node. You also
|
||||
cannot suicide a router by placing it in a node that is completely surrounded by your opponent's routers.
|
||||
<br />
|
||||
<br />
|
||||
* There is one exception to the suicide rule: You can place a router on ANY node if it would capture any
|
||||
of the opponent's routers.
|
||||
<br />
|
||||
<br />
|
||||
* You cannot repeat previous board states. This rule prevents infinite loops of capturing and
|
||||
re-capturing. This means that in some cases you cannot immediately capture an enemy network that is
|
||||
flashing and vulnerable.
|
||||
<br />
|
||||
<br />
|
||||
Note that you CAN re-capture eventually, but you must play somewhere else on the board first, to make the
|
||||
overall board state different.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item className={classes.instructionBoardWrapper}>
|
||||
{onlyGoodMoveChallenge}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<br />
|
||||
<br />
|
||||
<Grid container>
|
||||
<Grid item className={classes.instructionBoardWrapper}>
|
||||
{makeTwoEyesChallenge}
|
||||
</Grid>
|
||||
<Grid item className={classes.instructionsBlurb}>
|
||||
<Typography variant="h5">Strategy</Typography>
|
||||
<br />
|
||||
<br />
|
||||
<Typography>
|
||||
* You can place routers and look at the board state via the "ns.go" api. For more details, go to the IPvGO
|
||||
page in the{" "}
|
||||
<Link style={{ cursor: "pointer" }} onClick={() => Router.toPage(Page.Documentation)}>
|
||||
Bitburner Documentation
|
||||
</Link>
|
||||
<br />
|
||||
<br />
|
||||
* If a network surrounds a single empty node, the opponent can eventually capture it by filling in that
|
||||
node. However, if your network has two separate empty nodes inside of it, the suicide rule prevents the
|
||||
opponent from filling up either of them. This means your network cannot be captured! Try to place your
|
||||
networks surround several different empty nodes, and avoid filling in your network's empty nodes when
|
||||
possible.
|
||||
<br />
|
||||
<br />
|
||||
* Pay attention to when a network of routers has only one or two open ports to empty spaces! That is your
|
||||
opportunity to defend your network, or capture the opposing faction's.
|
||||
<br />
|
||||
<br />
|
||||
* Every faction has a different style, and different weaknesses. Try to identify what they are good and
|
||||
bad at doing.
|
||||
<br />
|
||||
<br />
|
||||
* The best way to learn strategies is to experiment and find out what works!
|
||||
<br />
|
||||
<br />* This game is Go with slightly simplified scoring. For more rule details and strategies try{" "}
|
||||
<Link href={"https://way-to-go.gitlab.io/#/en/capture-stones"} target={"_blank"} rel="noreferrer">
|
||||
The Way to Go interactive guide.
|
||||
</Link>{" "}
|
||||
<br />
|
||||
<br />
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<br />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import React from "react";
|
||||
import { ClassNameMap } from "@mui/styles";
|
||||
|
||||
import { BoardState, columnIndexes, playerColors } from "../boardState/goConstants";
|
||||
import { findNeighbors } from "../boardState/boardState";
|
||||
import { pointStyle } from "../boardState/goStyles";
|
||||
import { findAdjacentLibertiesAndAlliesForPoint } from "../boardAnalysis/boardAnalysis";
|
||||
|
||||
interface IProps {
|
||||
state: BoardState;
|
||||
x: number;
|
||||
y: number;
|
||||
traditional: boolean;
|
||||
hover: boolean;
|
||||
valid: boolean;
|
||||
emptyPointOwner: playerColors;
|
||||
}
|
||||
|
||||
export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwner }: IProps): React.ReactElement {
|
||||
const classes = pointStyle();
|
||||
|
||||
const currentPoint = state.board[x]?.[y];
|
||||
const player = currentPoint?.player;
|
||||
|
||||
const isInAtari =
|
||||
currentPoint && currentPoint.liberties?.length === 1 && player !== playerColors.empty && !traditional;
|
||||
const liberties = player !== playerColors.empty ? findAdjacentLibertiesAndAlliesForPoint(state, x, y) : null;
|
||||
const neighbors = findNeighbors(state, x, y);
|
||||
|
||||
const hasNorthLiberty = traditional ? neighbors.north : liberties?.north;
|
||||
const hasEastLiberty = traditional ? neighbors.east : liberties?.east;
|
||||
const hasSouthLiberty = traditional ? neighbors.south : liberties?.south;
|
||||
const hasWestLiberty = traditional ? neighbors.west : liberties?.west;
|
||||
|
||||
const pointClass =
|
||||
player === playerColors.white
|
||||
? classes.whitePoint
|
||||
: player === playerColors.black
|
||||
? classes.blackPoint
|
||||
: classes.emptyPoint;
|
||||
|
||||
const colorLiberty = `${player === playerColors.white ? classes.libertyWhite : classes.libertyBlack} ${
|
||||
classes.liberty
|
||||
}`;
|
||||
|
||||
const sizeClass = getSizeClass(state.board[0].length, classes);
|
||||
|
||||
const isNewStone = state.history?.[state.history?.length - 1]?.[x]?.[y]?.player === playerColors.empty;
|
||||
const isPriorMove = player === state.previousPlayer && isNewStone;
|
||||
|
||||
const emptyPointColorClass =
|
||||
emptyPointOwner === playerColors.white
|
||||
? classes.libertyWhite
|
||||
: emptyPointOwner === playerColors.black
|
||||
? classes.libertyBlack
|
||||
: "";
|
||||
|
||||
const mainClassName = `${classes.point} ${sizeClass} ${traditional ? classes.traditional : ""} ${
|
||||
hover ? classes.hover : ""
|
||||
} ${valid ? classes.valid : ""} ${isPriorMove ? classes.priorPoint : ""}
|
||||
${isInAtari ? classes.fadeLoopAnimation : ""}`;
|
||||
|
||||
return (
|
||||
<div className={`${mainClassName} ${currentPoint ? "" : classes.hideOverflow}`}>
|
||||
{currentPoint ? (
|
||||
<>
|
||||
<div className={hasNorthLiberty ? `${classes.northLiberty} ${colorLiberty}` : classes.liberty}></div>
|
||||
<div className={hasEastLiberty ? `${classes.eastLiberty} ${colorLiberty}` : classes.liberty}></div>
|
||||
<div className={hasSouthLiberty ? `${classes.southLiberty} ${colorLiberty}` : classes.liberty}></div>
|
||||
<div className={hasWestLiberty ? `${classes.westLiberty} ${colorLiberty}` : classes.liberty}></div>
|
||||
<div className={`${classes.innerPoint} `}>
|
||||
<div
|
||||
className={`${pointClass} ${player !== playerColors.empty ? classes.filledPoint : emptyPointColorClass}`}
|
||||
></div>
|
||||
</div>
|
||||
<div className={`${pointClass} ${classes.tradStone}`} />
|
||||
{traditional ? <div className={`${pointClass} ${classes.priorStoneTrad}`}></div> : ""}
|
||||
<div className={classes.coordinates}>
|
||||
{columnIndexes[x]}
|
||||
{traditional ? "" : "."}
|
||||
{y + 1}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={classes.broken}>
|
||||
<div className={classes.coordinates}>no signal</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function getSizeClass(
|
||||
size: number,
|
||||
classes: ClassNameMap<"fiveByFive" | "sevenBySeven" | "nineByNine" | "thirteenByThirteen" | "nineteenByNineteen">,
|
||||
) {
|
||||
switch (size) {
|
||||
case 5:
|
||||
return classes.fiveByFive;
|
||||
case 7:
|
||||
return classes.sevenBySeven;
|
||||
case 9:
|
||||
return classes.nineByNine;
|
||||
case 13:
|
||||
return classes.thirteenByThirteen;
|
||||
case 19:
|
||||
return classes.nineteenByNineteen;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
import { goScore, opponents, playerColors } from "../boardState/goConstants";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { GoScorePowerSummary } from "./GoScorePowerSummary";
|
||||
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
finalScore: goScore;
|
||||
newSubnet: () => void;
|
||||
opponent: opponents;
|
||||
}
|
||||
|
||||
export const GoScoreModal = ({ open, onClose, finalScore, newSubnet, opponent }: IProps): React.ReactElement => {
|
||||
const classes = boardStyles();
|
||||
|
||||
const blackScore = finalScore[playerColors.black];
|
||||
const whiteScore = finalScore[playerColors.white];
|
||||
|
||||
const playerWinsText = opponent === opponents.none ? "Black wins!" : "You win!";
|
||||
const opponentWinsText = opponent === opponents.none ? "White wins!" : `Winner: ${opponent}`;
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<>
|
||||
<div className={classes.scoreModal}>
|
||||
<Typography variant="h5" className={classes.centeredText}>
|
||||
Game complete!
|
||||
</Typography>
|
||||
<GoScoreSummaryTable score={finalScore} opponent={opponent} />
|
||||
<br />
|
||||
<Typography variant="h5" className={classes.centeredText}>
|
||||
{blackScore.sum > whiteScore.sum ? playerWinsText : opponentWinsText}
|
||||
</Typography>
|
||||
<br />
|
||||
{opponent !== opponents.none ? (
|
||||
<>
|
||||
<GoScorePowerSummary opponent={opponent} finalScore={finalScore} />
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Button onClick={newSubnet}>New Subnet</Button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
import React from "react";
|
||||
import { Table, TableBody, TableCell, TableRow, Typography, Tooltip } from "@mui/material";
|
||||
import { Player } from "@player";
|
||||
import { getBonusText, getDifficultyMultiplier, getMaxFavor, getWinstreakMultiplier } from "../effects/effect";
|
||||
import { goScore, opponents, playerColors } from "../boardState/goConstants";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { formatNumber } from "../../ui/formatNumber";
|
||||
import { FactionName } from "@enums";
|
||||
import { getPlayerStats } from "../boardAnalysis/scoring";
|
||||
|
||||
interface IProps {
|
||||
finalScore: goScore;
|
||||
opponent: opponents;
|
||||
}
|
||||
|
||||
export const GoScorePowerSummary = ({ finalScore, opponent }: IProps) => {
|
||||
const classes = boardStyles();
|
||||
const status = getPlayerStats(opponent);
|
||||
const winStreak = status.winStreak;
|
||||
const oldWinStreak = status.winStreak;
|
||||
const nodePower = formatNumber(status.nodePower, 2);
|
||||
const blackScore = finalScore[playerColors.black];
|
||||
const whiteScore = finalScore[playerColors.white];
|
||||
|
||||
const difficultyMultiplier = getDifficultyMultiplier(whiteScore.komi, Player.go.boardState.board[0].length);
|
||||
const winstreakMultiplier = getWinstreakMultiplier(winStreak, oldWinStreak);
|
||||
const nodePowerIncrease = formatNumber(blackScore.sum * difficultyMultiplier * winstreakMultiplier, 2);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography>
|
||||
<strong>Subnet power gained:</strong>
|
||||
</Typography>
|
||||
<br />
|
||||
<Table sx={{ display: "table", mb: 1, width: "100%" }}>
|
||||
<TableBody>
|
||||
<Tooltip title={<>The total number of empty points and routers you took control of on this subnet</>}>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Nodes Captured:</TableCell>
|
||||
<TableCell className={classes.cellNone}>{blackScore.sum}</TableCell>
|
||||
</TableRow>
|
||||
</Tooltip>
|
||||
<Tooltip title={<>The difficulty multiplier for this opponent faction</>}>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Difficulty Multiplier:</TableCell>
|
||||
<TableCell className={classes.cellNone}>{formatNumber(difficultyMultiplier, 2)}x</TableCell>
|
||||
</TableRow>
|
||||
</Tooltip>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>{winStreak >= 0 ? "Win" : "Loss"} Streak:</TableCell>
|
||||
<TableCell className={classes.cellNone}>{winStreak}</TableCell>
|
||||
</TableRow>
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
Consecutive wins award progressively higher multipliers for node power. Coming back from a loss streak
|
||||
also gives an extra bonus.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TableRow>
|
||||
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
|
||||
{winStreak >= 0 ? "Win Streak" : "Loss"} Multiplier:
|
||||
</TableCell>
|
||||
<TableCell className={`${classes.cellNone} ${classes.cellBottomPadding}`}>
|
||||
{formatNumber(winstreakMultiplier, 2)}x
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
Node power is what stat bonuses scale from, and is gained on each completed subnet. <br />
|
||||
It is calculated from the number of nodes you control, multiplied by modifiers for the <br />
|
||||
opponent difficulty, if you won or lost, and your current winstreak.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Node power gained:</TableCell>
|
||||
<TableCell className={classes.cellNone}>{nodePowerIncrease}</TableCell>
|
||||
</TableRow>
|
||||
</Tooltip>
|
||||
<Tooltip title={<>Your total node power from all subnets</>}>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Total node power:</TableCell>
|
||||
<TableCell className={classes.cellNone}>{nodePower}</TableCell>
|
||||
</TableRow>
|
||||
</Tooltip>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{winStreak && winStreak % 2 === 0 && Player.factions.includes(opponent as unknown as FactionName) ? (
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
Win streaks against a faction will give you +1 favor to that faction <br /> at certain numbers of wins (up
|
||||
to a max of {getMaxFavor()} favor), <br />
|
||||
if you are currently a member of that faction
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Typography className={`${classes.inlineFlexBox} ${classes.keyText}`}>
|
||||
<span>Winstreak Bonus: </span>
|
||||
<span>+1 favor to {opponent}</span>
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Tooltip title={<>The total stat multiplier gained via your current node power.</>}>
|
||||
<Typography className={`${classes.inlineFlexBox} ${classes.keyText}`}>
|
||||
<span>New Total Bonus: </span>
|
||||
<span>{getBonusText(opponent)}</span>
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
import { Table, TableBody, TableCell, TableRow, Tooltip } from "@mui/material";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { goScore, opponents, playerColors } from "../boardState/goConstants";
|
||||
|
||||
interface IProps {
|
||||
score: goScore;
|
||||
opponent: opponents;
|
||||
}
|
||||
|
||||
export const GoScoreSummaryTable = ({ score, opponent }: IProps) => {
|
||||
const classes = boardStyles();
|
||||
const blackScore = score[playerColors.black];
|
||||
const whiteScore = score[playerColors.white];
|
||||
const blackPlayerName = opponent === opponents.none ? "Black" : "You";
|
||||
const whitePlayerName = opponent === opponents.none ? "White" : opponent;
|
||||
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
<Table sx={{ display: "table", mb: 1, width: "100%" }}>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone} />
|
||||
<TableCell className={classes.cellNone}>
|
||||
<strong>{whitePlayerName}:</strong>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cellNone}>
|
||||
<strong>{blackPlayerName}:</strong>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Owned Empty Nodes:</TableCell>
|
||||
<TableCell className={classes.cellNone}>{whiteScore.territory}</TableCell>
|
||||
<TableCell className={classes.cellNone}>{blackScore.territory}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Routers placed:</TableCell>
|
||||
<TableCell className={classes.cellNone}>{whiteScore.pieces}</TableCell>
|
||||
<TableCell className={classes.cellNone}>{blackScore.pieces}</TableCell>
|
||||
</TableRow>
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
Komi represents the current faction's home-field advantage on this subnet, <br />
|
||||
to balance the first-move advantage that the player with the black routers has.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>Komi:</TableCell>
|
||||
<TableCell className={classes.cellNone}>{whiteScore.komi}</TableCell>
|
||||
<TableCell className={classes.cellNone} />
|
||||
</TableRow>
|
||||
</Tooltip>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>
|
||||
<br />
|
||||
<strong className={classes.keyText}>Total score:</strong>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cellNone}>
|
||||
<strong className={classes.keyText}>{whiteScore.sum}</strong>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cellNone}>
|
||||
<strong className={classes.keyText}>{blackScore.sum}</strong>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { opponentList } from "../boardState/goConstants";
|
||||
import { getScore } from "../boardAnalysis/scoring";
|
||||
import { Player } from "@player";
|
||||
import { Grid, Table, TableBody, TableCell, TableRow } from "@mui/material";
|
||||
import { GoGameboard } from "./GoGameboard";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { getBonusText } from "../effects/effect";
|
||||
import { GoScoreSummaryTable } from "./GoScoreSummaryTable";
|
||||
|
||||
export const GoStatusPage = (): React.ReactElement => {
|
||||
useRerender(400);
|
||||
const classes = boardStyles();
|
||||
const score = getScore(Player.go.boardState);
|
||||
const opponent = Player.go.boardState.ai;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Grid container>
|
||||
<Grid item>
|
||||
<div className={classes.statusPageScore}>
|
||||
<Typography variant="h5">Current Subnet:</Typography>
|
||||
<GoScoreSummaryTable score={score} opponent={opponent} />
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<div className={classes.statusPageGameboard}>
|
||||
<GoGameboard
|
||||
boardState={Player.go.boardState}
|
||||
traditional={false}
|
||||
clickHandler={(x, y) => ({ x, y })}
|
||||
hover={false}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<br />
|
||||
<Typography variant="h5">Summary of All Subnet Boosts:</Typography>
|
||||
<br />
|
||||
<Table sx={{ display: "table", mb: 1, width: "550px" }}>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className={classes.cellNone}>
|
||||
<strong>Faction:</strong>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cellNone}>
|
||||
<strong>Effect:</strong>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{opponentList.map((faction, index) => {
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell className={classes.cellNone}>
|
||||
<br />
|
||||
<span>{faction}:</span>
|
||||
</TableCell>
|
||||
<TableCell className={classes.cellNone}>
|
||||
<br />
|
||||
<strong className={classes.keyText}>{getBonusText(faction)}</strong>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Box, Button, MenuItem, Select, SelectChangeEvent, Tooltip, Typography } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import { boardSizes, opponentDetails, opponentList, opponents } from "../boardState/goConstants";
|
||||
import { Player } from "@player";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
import { Modal } from "../../ui/React/Modal";
|
||||
import { getHandicap } from "../boardState/boardState";
|
||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { getPlayerStats } from "../boardAnalysis/scoring";
|
||||
import { showWorldDemon } from "../boardAnalysis/goAI";
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
search: (size: number, opponent: opponents) => void;
|
||||
cancel: () => void;
|
||||
showInstructions: () => void;
|
||||
}
|
||||
|
||||
export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProps): React.ReactElement => {
|
||||
const classes = boardStyles();
|
||||
const [opponent, setOpponent] = useState<opponents>(Player.go.boardState?.ai ?? opponents.SlumSnakes);
|
||||
const preselectedBoardSize =
|
||||
opponent === opponents.w0r1d_d43m0n ? 19 : Math.min(Player.go.boardState?.board?.[0]?.length ?? 7, 13);
|
||||
const [boardSize, setBoardSize] = useState(preselectedBoardSize);
|
||||
|
||||
const opponentFactions = [opponents.none, ...opponentList];
|
||||
if (showWorldDemon()) {
|
||||
opponentFactions.push(opponents.w0r1d_d43m0n);
|
||||
}
|
||||
|
||||
const handicap = getHandicap(boardSize, opponent);
|
||||
|
||||
function changeOpponent(event: SelectChangeEvent): void {
|
||||
const newOpponent = event.target.value as opponents;
|
||||
setOpponent(newOpponent);
|
||||
if (newOpponent === opponents.w0r1d_d43m0n) {
|
||||
setBoardSize(19);
|
||||
|
||||
const stats = getPlayerStats(opponents.w0r1d_d43m0n);
|
||||
if (stats?.wins + stats?.losses === 0) {
|
||||
Settings.GoTraditionalStyle = false;
|
||||
}
|
||||
} else if (boardSize > 13) {
|
||||
setBoardSize(13);
|
||||
}
|
||||
}
|
||||
|
||||
function changeBoardSize(event: SelectChangeEvent) {
|
||||
const newSize = +event.target.value;
|
||||
setBoardSize(newSize);
|
||||
}
|
||||
|
||||
const onSearch = () => {
|
||||
search(boardSize, opponent);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={cancel}>
|
||||
<div className={classes.searchBox}>
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||
<br />
|
||||
<Typography variant="h4">IPvGO Subnet Search</Typography>
|
||||
<br />
|
||||
</Box>
|
||||
<br />
|
||||
<br />
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||
<Typography className={classes.opponentLabel}>
|
||||
{opponent !== opponents.none ? "Opponent Faction: " : ""}
|
||||
</Typography>
|
||||
<Select value={opponent} onChange={changeOpponent} sx={{ mr: 1 }}>
|
||||
{opponentFactions.map((faction) => (
|
||||
<MenuItem key={faction} value={faction}>
|
||||
{faction === opponents.w0r1d_d43m0n ? (
|
||||
<CorruptableText content="???????????????" />
|
||||
) : (
|
||||
`${faction} (${opponentDetails[faction].description})`
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Box>
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||
<Typography className={classes.opponentLabel}>Subnet size: </Typography>
|
||||
{opponent === opponents.w0r1d_d43m0n ? (
|
||||
<Typography>????</Typography>
|
||||
) : (
|
||||
<Select value={`${boardSize}`} onChange={changeBoardSize} sx={{ mr: 1 }}>
|
||||
{boardSizes.map((size) => (
|
||||
<MenuItem key={size} value={size}>
|
||||
{size}x{size}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</Box>
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
This faction will also get a few points as a home-field advantage in the subnet, and to balance the
|
||||
player's advantage of having the first move.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Typography className={classes.opponentLabel}>Komi: {opponentDetails[opponent].komi}</Typography>
|
||||
</Tooltip>
|
||||
{handicap ? (
|
||||
<Tooltip title={<>This faction has placed a few routers to defend their subnet already.</>}>
|
||||
<Typography className={classes.opponentLabel}>Handicap: {handicap}</Typography>
|
||||
</Tooltip>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Box>
|
||||
<br />
|
||||
<br />
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle} ${classes.flavorText}`}>
|
||||
<Typography>
|
||||
{opponent === opponents.w0r1d_d43m0n ? (
|
||||
<>
|
||||
<CorruptableText content={opponentDetails[opponent].flavorText.slice(0, 40)} />
|
||||
<CorruptableText content={opponentDetails[opponent].flavorText.slice(40)} />
|
||||
</>
|
||||
) : (
|
||||
opponentDetails[opponent].flavorText
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<br />
|
||||
<br />
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||
<Typography>
|
||||
{opponent !== opponents.none ? "Faction subnet bonus:" : ""} {opponentDetails[opponent].bonusDescription}
|
||||
</Typography>
|
||||
</Box>
|
||||
<br />
|
||||
<br />
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||
<Button onClick={onSearch}>Search for Subnet</Button>
|
||||
<Button onClick={cancel}>Cancel</Button>
|
||||
</Box>
|
||||
<Box className={`${classes.inlineFlexBox} ${classes.opponentTitle}`}>
|
||||
<Typography onClick={showInstructions} className={classes.link}>
|
||||
How to Play
|
||||
</Typography>
|
||||
</Box>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useState } from "react";
|
||||
import { Typography, Button } from "@mui/material";
|
||||
|
||||
import { BoardState, playerColors, validityReason } from "../boardState/goConstants";
|
||||
import { GoGameboard } from "./GoGameboard";
|
||||
import { evaluateIfMoveIsValid } from "../boardAnalysis/boardAnalysis";
|
||||
import { SnackbarEvents } from "../../ui/React/Snackbar";
|
||||
import { ToastVariant } from "@enums";
|
||||
import { getStateCopy, makeMove } from "../boardState/boardState";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
|
||||
interface IProps {
|
||||
state: BoardState;
|
||||
description: string;
|
||||
correctMoves: [{ x: number; y: number }];
|
||||
correctText: string;
|
||||
incorrectText: string;
|
||||
incorrectMoves1?: [{ x: number; y: number }];
|
||||
incorrectText1?: string;
|
||||
incorrectMoves2?: [{ x: number; y: number }];
|
||||
incorrectText2?: string;
|
||||
}
|
||||
|
||||
export function GoTutorialChallenge({
|
||||
state,
|
||||
description,
|
||||
correctMoves,
|
||||
correctText,
|
||||
incorrectText,
|
||||
incorrectMoves1,
|
||||
incorrectText1,
|
||||
incorrectMoves2,
|
||||
incorrectText2,
|
||||
}: IProps): React.ReactElement {
|
||||
const classes = boardStyles();
|
||||
const [currentState, setCurrentState] = useState(getStateCopy(state));
|
||||
const [displayText, setDisplayText] = useState(description);
|
||||
const [showReset, setShowReset] = useState(false);
|
||||
|
||||
const handleClick = (x: number, y: number) => {
|
||||
if (currentState.history.length) {
|
||||
SnackbarEvents.emit(`Hit 'Reset' to try again`, ToastVariant.WARNING, 2000);
|
||||
return;
|
||||
}
|
||||
setShowReset(true);
|
||||
|
||||
const validity = evaluateIfMoveIsValid(currentState, x, y, playerColors.black);
|
||||
if (validity != validityReason.valid) {
|
||||
setDisplayText(
|
||||
"Invalid move: You cannot suicide your routers by placing them with no access to any empty ports.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedBoard = makeMove(currentState, x, y, playerColors.black);
|
||||
|
||||
if (updatedBoard) {
|
||||
setCurrentState(getStateCopy(updatedBoard));
|
||||
|
||||
if (correctMoves.find((move) => move.x === x && move.y === y)) {
|
||||
setDisplayText(correctText);
|
||||
} else if (incorrectMoves1?.find((move) => move.x === x && move.y === y)) {
|
||||
setDisplayText(incorrectText1 ?? "");
|
||||
} else if (incorrectMoves2?.find((move) => move.x === x && move.y === y)) {
|
||||
setDisplayText(incorrectText2 ?? "");
|
||||
} else {
|
||||
setDisplayText(incorrectText);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
setCurrentState(getStateCopy(state));
|
||||
setDisplayText(description);
|
||||
setShowReset(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={classes.instructionBoard}>
|
||||
<GoGameboard boardState={currentState} traditional={false} clickHandler={handleClick} hover={true} />
|
||||
</div>
|
||||
<Typography>{displayText}</Typography>
|
||||
{showReset ? <Button onClick={reset}>Reset</Button> : ""}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user