mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-05-05 15:17:48 +02:00
BITNODE: IPvGO territory control strategy game (#934)
This commit is contained in:
committed by
GitHub
parent
c6141f2adf
commit
7ef12a0323
@@ -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