BITNODE: IPvGO territory control strategy game (#934)

This commit is contained in:
Michael Ficocelli
2023-12-26 11:45:27 -05:00
committed by GitHub
parent c6141f2adf
commit 7ef12a0323
68 changed files with 7833 additions and 17 deletions
+73
View File
@@ -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>
</>
);
}
+292
View File
@@ -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>
</>
);
}
+140
View File
@@ -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>
);
};
+243
View File
@@ -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>
);
};
+111
View File
@@ -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;
}
}
+54
View File
@@ -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>
);
};
+118
View File
@@ -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>
</>
);
};
+72
View File
@@ -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>
</>
);
};
+71
View File
@@ -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>
);
};
+152
View File
@@ -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>
);
};
+87
View File
@@ -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>
);
}