diff --git a/markdown/bitburner.go.md b/markdown/bitburner.go.md index 02e57f9e3..d477706a5 100644 --- a/markdown/bitburner.go.md +++ b/markdown/bitburner.go.md @@ -30,5 +30,5 @@ export interface Go | [makeMove(x, y)](./bitburner.go.makemove.md) | Make a move on the IPvGO subnet gameboard, and await the opponent's response. x:0 y:0 represents the bottom-left corner of the board in the UI. | | [opponentNextTurn(logOpponentMove)](./bitburner.go.opponentnextturn.md) | Returns a promise that resolves with the success or failure state of your last move, and the AI's response, if applicable. x:0 y:0 represents the bottom-left corner of the board in the UI. | | [passTurn()](./bitburner.go.passturn.md) |

Pass the player's turn rather than making a move, and await the opponent's response. This ends the game if the opponent passed on the previous turn, or if the opponent passes on their following turn.

This can also be used if you pick up the game in a state where the opponent needs to play next. For example: if BitBurner was closed while waiting for the opponent to make a move, you may need to call passTurn() to get them to play their move on game start.

| -| [resetBoardState(opponent, boardSize)](./bitburner.go.resetboardstate.md) |

Gets new IPvGO subnet with the specified size owned by the listed faction, ready for the player to make a move. This will reset your win streak if the current game is not complete and you have already made moves.

Note that some factions will have a few routers on the subnet at this state.

opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????",

| +| [resetBoardState(opponent, boardSize)](./bitburner.go.resetboardstate.md) |

Gets new IPvGO subnet with the specified size owned by the listed faction, ready for the player to make a move. This will reset your win streak if the current game is not complete and you have already made moves.

Note that some factions will have a few routers on the subnet at this state.

opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????" or "No AI",

| diff --git a/markdown/bitburner.go.resetboardstate.md b/markdown/bitburner.go.resetboardstate.md index afec085ef..b00ec5ac7 100644 --- a/markdown/bitburner.go.resetboardstate.md +++ b/markdown/bitburner.go.resetboardstate.md @@ -8,7 +8,7 @@ Gets new IPvGO subnet with the specified size owned by the listed faction, ready Note that some factions will have a few routers on the subnet at this state. -opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????", +opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????" or "No AI", **Signature:** diff --git a/src/Go/Constants.ts b/src/Go/Constants.ts index b434f6793..ff6a4e0a7 100644 --- a/src/Go/Constants.ts +++ b/src/Go/Constants.ts @@ -6,7 +6,8 @@ export const opponentDetails = { [GoOpponent.none]: { komi: 5.5, description: "Practice Board", - flavorText: "Practice on a subnet where you place both colors of routers.", + flavorText: + "Practice on a subnet where you place both colors of routers, or play as white against your IPvGO script.", bonusDescription: "", bonusPower: 0, }, diff --git a/src/Go/boardAnalysis/boardAnalysis.ts b/src/Go/boardAnalysis/boardAnalysis.ts index 9a3fee8b1..5fd6c2c5d 100644 --- a/src/Go/boardAnalysis/boardAnalysis.ts +++ b/src/Go/boardAnalysis/boardAnalysis.ts @@ -1,6 +1,6 @@ -import type { Board, BoardState, Neighbor, PointState, SimpleBoard } from "../Types"; +import type { Board, BoardState, Neighbor, Play, PointState, SimpleBoard } from "../Types"; -import { GoValidity, GoOpponent, GoColor } from "@enums"; +import { GoValidity, GoOpponent, GoColor, GoPlayType } from "@enums"; import { Go } from "../Go"; import { findAdjacentPointsInChain, @@ -655,3 +655,23 @@ export function getPreviousMove(): [number, number] | null { return null; } + +/** + * Gets the last move, if it was made by the specified color and is present + */ +export function getPreviousMoveDetails(): Play { + const priorMove = getPreviousMove(); + if (priorMove) { + return { + type: GoPlayType.move, + x: priorMove[0], + y: priorMove[1], + }; + } + + return { + type: !priorMove && Go.currentGame?.passCount ? GoPlayType.pass : GoPlayType.gameOver, + x: null, + y: null, + }; +} diff --git a/src/Go/boardAnalysis/goAI.ts b/src/Go/boardAnalysis/goAI.ts index 885ab716d..e4d0198fb 100644 --- a/src/Go/boardAnalysis/goAI.ts +++ b/src/Go/boardAnalysis/goAI.ts @@ -1,7 +1,7 @@ import type { Board, BoardState, EyeMove, Move, MoveOptions, Play, PointState } from "../Types"; import { Player } from "@player"; -import { AugmentationName, GoOpponent, GoColor, GoPlayType } from "@enums"; +import { AugmentationName, GoColor, GoOpponent, GoPlayType } from "@enums"; import { opponentDetails } from "../Constants"; import { findNeighbors, isNotNullish, makeMove, passTurn } from "../boardState/boardState"; import { @@ -15,22 +15,35 @@ import { getAllEyesByChainId, getAllNeighboringChains, getAllValidMoves, + getPreviousMoveDetails, } from "./boardAnalysis"; import { findDisputedTerritory } from "./controlledTerritory"; import { findAnyMatchedPatterns } from "./patternMatching"; import { WHRNG } from "../../Casino/RNG"; import { Go, GoEvents } from "../Go"; -let currentAITurn: Promise | null = null; +let isAiThinking: boolean = false; +let currentTurnResolver: (() => void) | null = null; /** * Retrieves a move from the current faction in response to the player's move */ export function makeAIMove(boardState: BoardState): Promise { // If AI is already taking their turn, return the existing turn. - if (currentAITurn) return currentAITurn; - currentAITurn = Go.nextTurn = getMove(boardState, GoColor.white, Go.currentGame.ai) - .then(async (play): Promise => { + if (isAiThinking) { + return Go.nextTurn; + } + isAiThinking = true; + + // If the AI is disabled, simply make a promise to be resolved once the player makes a move as white + if (boardState.ai === GoOpponent.none) { + GoEvents.emit(); + // Update currentTurnResolver to call Go.nextTurn's resolve function with the last played move's details + Go.nextTurn = new Promise((resolve) => (currentTurnResolver = () => resolve(getPreviousMoveDetails()))); + } + // If an AI is in use, find the faction's move in response, and resolve the Go.nextTurn promise once it is found and played. + else { + Go.nextTurn = getMove(boardState, GoColor.white, Go.currentGame.ai).then(async (play): Promise => { if (boardState !== Go.currentGame) return play; //Stale game // Handle AI passing @@ -54,15 +67,29 @@ export function makeAIMove(boardState: BoardState): Promise { } return play; - }) - .finally(() => { - currentAITurn = null; - GoEvents.emit(); }); + } + + // Once the AI moves (or the player playing as white with No AI moves), + // clear the isAiThinking semaphore and update the board UI. + Go.nextTurn = Go.nextTurn.finally(() => { + isAiThinking = false; + GoEvents.emit(); + }); return Go.nextTurn; } +/** + * Resolves the current turn. + * This is used for players manually playing against their script on the no-ai board. + */ +export function resolveCurrentTurn() { + // Call the resolve function on Go.nextTurn, if it exists + currentTurnResolver?.(); + currentTurnResolver = null; +} + /* Basic GO AIs, each with some personality and weaknesses diff --git a/src/Go/ui/GoGameboardWrapper.tsx b/src/Go/ui/GoGameboardWrapper.tsx index 4b8bb6de2..466f1e5c5 100644 --- a/src/Go/ui/GoGameboardWrapper.tsx +++ b/src/Go/ui/GoGameboardWrapper.tsx @@ -18,7 +18,7 @@ import { GoScoreModal } from "./GoScoreModal"; import { GoGameboard } from "./GoGameboard"; import { GoSubnetSearch } from "./GoSubnetSearch"; import { CorruptableText } from "../../ui/React/CorruptableText"; -import { makeAIMove } from "../boardAnalysis/goAI"; +import { makeAIMove, resolveCurrentTurn } from "../boardAnalysis/goAI"; interface GoGameboardWrapperProps { showInstructions: () => void; @@ -85,7 +85,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps const didUpdateBoard = makeMove(boardState, x, y, currentPlayer); if (didUpdateBoard) { rerender(); - Go.currentGame.ai !== GoOpponent.none && takeAiTurn(boardState); + takeAiTurn(boardState); } } @@ -104,11 +104,17 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps } setTimeout(() => { - Go.currentGame.ai !== GoOpponent.none && takeAiTurn(boardState); + takeAiTurn(boardState); }, 100); } async function takeAiTurn(boardState: BoardState) { + // If white is being played manually, halt and notify any scripts playing as black if present, instead of making an AI move + if (Go.currentGame.ai === GoOpponent.none) { + Go.currentGame.previousPlayer && resolveCurrentTurn(); + return; + } + const move = await makeAIMove(boardState); if (move.type === GoPlayType.pass) { @@ -137,6 +143,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps Go.currentGame = getNewBoardState(newBoardSize, newOpponent, true); rerender(); + resolveCurrentTurn(); } function getPriorMove() { @@ -159,17 +166,19 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps rerender(); } - const endGameAvailable = boardState.previousPlayer === GoColor.white && boardState.passCount; - const noLegalMoves = - boardState.previousPlayer === GoColor.white && !getAllValidMoves(boardState, GoColor.black).length; + const ongoingNoAiGame = boardState.ai === GoOpponent.none && boardState.previousPlayer; + const manualTurnAvailable = ongoingNoAiGame || boardState.previousPlayer === GoColor.white; + const endGameAvailable = manualTurnAvailable && boardState.passCount; + const noLegalMoves = manualTurnAvailable && !getAllValidMoves(boardState, currentPlayer).length; const scoreBoxText = boardState.previousBoards.length ? `Score: Black: ${score[GoColor.black].sum} White: ${score[GoColor.white].sum}` : "Place a router to begin!"; const getPassButtonLabel = () => { + const playerString = boardState.ai === GoOpponent.none ? ` (${currentPlayer})` : ""; if (endGameAvailable) { - return "End Game"; + return `End Game${playerString}`; } if (boardState.previousPlayer === null) { return "View Final Score"; @@ -177,8 +186,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps if (waitingOnAI) { return "Waiting for opponent"; } - const currentPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black; - return `Pass Turn${boardState.ai === GoOpponent.none ? ` (${currentPlayer})` : ""}`; + return `Pass Turn${playerString}`; }; return ( diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts index bf9263925..c8145c8c1 100644 --- a/src/ScriptEditor/NetscriptDefinitions.d.ts +++ b/src/ScriptEditor/NetscriptDefinitions.d.ts @@ -4079,7 +4079,7 @@ export interface Go { * * Note that some factions will have a few routers on the subnet at this state. * - * opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????", + * opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????" or "No AI", * * @returns a simplified version of the board state as an array of strings representing the board columns. See ns.Go.getBoardState() for full details *