IPVGO: Support playing manually as white against your scripts using the No AI type board (#1296)

This commit is contained in:
Michael Ficocelli 2024-06-02 20:41:31 -04:00 committed by GitHub
parent f40d4f8e92
commit a28bb4bd99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 80 additions and 24 deletions

@ -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. | | [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. | | [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) | <p>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.</p><p>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.</p> | | [passTurn()](./bitburner.go.passturn.md) | <p>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.</p><p>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.</p> |
| [resetBoardState(opponent, boardSize)](./bitburner.go.resetboardstate.md) | <p>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.</p><p>Note that some factions will have a few routers on the subnet at this state.</p><p>opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????",</p> | | [resetBoardState(opponent, boardSize)](./bitburner.go.resetboardstate.md) | <p>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.</p><p>Note that some factions will have a few routers on the subnet at this state.</p><p>opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????" or "No AI",</p> |

@ -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. 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:** **Signature:**

@ -6,7 +6,8 @@ export const opponentDetails = {
[GoOpponent.none]: { [GoOpponent.none]: {
komi: 5.5, komi: 5.5,
description: "Practice Board", 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: "", bonusDescription: "",
bonusPower: 0, bonusPower: 0,
}, },

@ -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 { Go } from "../Go";
import { import {
findAdjacentPointsInChain, findAdjacentPointsInChain,
@ -655,3 +655,23 @@ export function getPreviousMove(): [number, number] | null {
return 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,
};
}

@ -1,7 +1,7 @@
import type { Board, BoardState, EyeMove, Move, MoveOptions, Play, PointState } from "../Types"; import type { Board, BoardState, EyeMove, Move, MoveOptions, Play, PointState } from "../Types";
import { Player } from "@player"; import { Player } from "@player";
import { AugmentationName, GoOpponent, GoColor, GoPlayType } from "@enums"; import { AugmentationName, GoColor, GoOpponent, GoPlayType } from "@enums";
import { opponentDetails } from "../Constants"; import { opponentDetails } from "../Constants";
import { findNeighbors, isNotNullish, makeMove, passTurn } from "../boardState/boardState"; import { findNeighbors, isNotNullish, makeMove, passTurn } from "../boardState/boardState";
import { import {
@ -15,22 +15,35 @@ import {
getAllEyesByChainId, getAllEyesByChainId,
getAllNeighboringChains, getAllNeighboringChains,
getAllValidMoves, getAllValidMoves,
getPreviousMoveDetails,
} from "./boardAnalysis"; } from "./boardAnalysis";
import { findDisputedTerritory } from "./controlledTerritory"; import { findDisputedTerritory } from "./controlledTerritory";
import { findAnyMatchedPatterns } from "./patternMatching"; import { findAnyMatchedPatterns } from "./patternMatching";
import { WHRNG } from "../../Casino/RNG"; import { WHRNG } from "../../Casino/RNG";
import { Go, GoEvents } from "../Go"; import { Go, GoEvents } from "../Go";
let currentAITurn: Promise<Play> | 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 * Retrieves a move from the current faction in response to the player's move
*/ */
export function makeAIMove(boardState: BoardState): Promise<Play> { export function makeAIMove(boardState: BoardState): Promise<Play> {
// If AI is already taking their turn, return the existing turn. // If AI is already taking their turn, return the existing turn.
if (currentAITurn) return currentAITurn; if (isAiThinking) {
currentAITurn = Go.nextTurn = getMove(boardState, GoColor.white, Go.currentGame.ai) return Go.nextTurn;
.then(async (play): Promise<Play> => { }
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<Play> => {
if (boardState !== Go.currentGame) return play; //Stale game if (boardState !== Go.currentGame) return play; //Stale game
// Handle AI passing // Handle AI passing
@ -54,15 +67,29 @@ export function makeAIMove(boardState: BoardState): Promise<Play> {
} }
return play; 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; 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 Basic GO AIs, each with some personality and weaknesses

@ -18,7 +18,7 @@ import { GoScoreModal } from "./GoScoreModal";
import { GoGameboard } from "./GoGameboard"; import { GoGameboard } from "./GoGameboard";
import { GoSubnetSearch } from "./GoSubnetSearch"; import { GoSubnetSearch } from "./GoSubnetSearch";
import { CorruptableText } from "../../ui/React/CorruptableText"; import { CorruptableText } from "../../ui/React/CorruptableText";
import { makeAIMove } from "../boardAnalysis/goAI"; import { makeAIMove, resolveCurrentTurn } from "../boardAnalysis/goAI";
interface GoGameboardWrapperProps { interface GoGameboardWrapperProps {
showInstructions: () => void; showInstructions: () => void;
@ -85,7 +85,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
const didUpdateBoard = makeMove(boardState, x, y, currentPlayer); const didUpdateBoard = makeMove(boardState, x, y, currentPlayer);
if (didUpdateBoard) { if (didUpdateBoard) {
rerender(); rerender();
Go.currentGame.ai !== GoOpponent.none && takeAiTurn(boardState); takeAiTurn(boardState);
} }
} }
@ -104,11 +104,17 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
} }
setTimeout(() => { setTimeout(() => {
Go.currentGame.ai !== GoOpponent.none && takeAiTurn(boardState); takeAiTurn(boardState);
}, 100); }, 100);
} }
async function takeAiTurn(boardState: BoardState) { 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); const move = await makeAIMove(boardState);
if (move.type === GoPlayType.pass) { if (move.type === GoPlayType.pass) {
@ -137,6 +143,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
Go.currentGame = getNewBoardState(newBoardSize, newOpponent, true); Go.currentGame = getNewBoardState(newBoardSize, newOpponent, true);
rerender(); rerender();
resolveCurrentTurn();
} }
function getPriorMove() { function getPriorMove() {
@ -159,17 +166,19 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
rerender(); rerender();
} }
const endGameAvailable = boardState.previousPlayer === GoColor.white && boardState.passCount; const ongoingNoAiGame = boardState.ai === GoOpponent.none && boardState.previousPlayer;
const noLegalMoves = const manualTurnAvailable = ongoingNoAiGame || boardState.previousPlayer === GoColor.white;
boardState.previousPlayer === GoColor.white && !getAllValidMoves(boardState, GoColor.black).length; const endGameAvailable = manualTurnAvailable && boardState.passCount;
const noLegalMoves = manualTurnAvailable && !getAllValidMoves(boardState, currentPlayer).length;
const scoreBoxText = boardState.previousBoards.length const scoreBoxText = boardState.previousBoards.length
? `Score: Black: ${score[GoColor.black].sum} White: ${score[GoColor.white].sum}` ? `Score: Black: ${score[GoColor.black].sum} White: ${score[GoColor.white].sum}`
: "Place a router to begin!"; : "Place a router to begin!";
const getPassButtonLabel = () => { const getPassButtonLabel = () => {
const playerString = boardState.ai === GoOpponent.none ? ` (${currentPlayer})` : "";
if (endGameAvailable) { if (endGameAvailable) {
return "End Game"; return `End Game${playerString}`;
} }
if (boardState.previousPlayer === null) { if (boardState.previousPlayer === null) {
return "View Final Score"; return "View Final Score";
@ -177,8 +186,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
if (waitingOnAI) { if (waitingOnAI) {
return "Waiting for opponent"; return "Waiting for opponent";
} }
const currentPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black; return `Pass Turn${playerString}`;
return `Pass Turn${boardState.ai === GoOpponent.none ? ` (${currentPlayer})` : ""}`;
}; };
return ( return (

@ -4079,7 +4079,7 @@ export interface Go {
* *
* Note that some factions will have a few routers on the subnet at this state. * 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 * @returns a simplified version of the board state as an array of strings representing the board columns. See ns.Go.getBoardState() for full details
* *