mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-12-19 04:35:46 +01:00
GO: Alternate fix for race conditions (#1260)
This commit is contained in:
parent
1b8205e9d5
commit
e23db93c8b
@ -1,6 +1,6 @@
|
||||
import type { GoOpponent } from "@enums";
|
||||
import type { BoardState, OpponentStats, Play } from "./Types";
|
||||
|
||||
import { GoPlayType, type GoOpponent } from "@enums";
|
||||
import { getRecordValues, PartialRecord } from "../Types/Record";
|
||||
import { getNewBoardState } from "./boardState/boardState";
|
||||
import { EventEmitter } from "../utils/EventEmitter";
|
||||
@ -10,7 +10,7 @@ export class GoObject {
|
||||
previousGame: BoardState | null = null;
|
||||
currentGame: BoardState = getNewBoardState(7);
|
||||
stats: PartialRecord<GoOpponent, OpponentStats> = {};
|
||||
nextTurn: Promise<Play> | null = null;
|
||||
nextTurn: Promise<Play> = Promise.resolve({ type: GoPlayType.gameOver, x: null, y: null });
|
||||
|
||||
prestigeAugmentation() {
|
||||
for (const stats of getRecordValues(this.stats)) {
|
||||
|
@ -2,13 +2,14 @@ import type { BoardState, OpponentStats, SimpleBoard } from "./Types";
|
||||
import type { PartialRecord } from "../Types/Record";
|
||||
|
||||
import { Truthy } from "lodash";
|
||||
import { GoColor, GoOpponent } from "@enums";
|
||||
import { GoColor, GoOpponent, GoPlayType } from "@enums";
|
||||
import { Go } from "./Go";
|
||||
import { boardStateFromSimpleBoard, simpleBoardFromBoard } from "./boardAnalysis/boardAnalysis";
|
||||
import { boardStateFromSimpleBoard, getPreviousMove, simpleBoardFromBoard } from "./boardAnalysis/boardAnalysis";
|
||||
import { assertLoadingType } from "../utils/TypeAssertion";
|
||||
import { getEnumHelper } from "../utils/EnumHelper";
|
||||
import { boardSizes } from "./Constants";
|
||||
import { isInteger, isNumber } from "../types";
|
||||
import { makeAIMove } from "./boardAnalysis/goAI";
|
||||
|
||||
type PreviousGameSaveData = { ai: GoOpponent; board: SimpleBoard; previousPlayer: GoColor | null } | null;
|
||||
type CurrentGameSaveData = PreviousGameSaveData & {
|
||||
@ -77,6 +78,18 @@ export function loadGo(data: unknown): boolean {
|
||||
Go.currentGame = currentGame;
|
||||
Go.previousGame = previousGame;
|
||||
Go.stats = stats;
|
||||
|
||||
// If it's the AI's turn, initiate their turn, which will populate nextTurn
|
||||
if (currentGame.previousPlayer === GoColor.black && currentGame.ai !== GoOpponent.none) makeAIMove(currentGame);
|
||||
// If it's not the AI's turn and we're not in gameover status, initialize nextTurn promise based on the previous move/pass
|
||||
else if (currentGame.previousPlayer) {
|
||||
const previousMove = getPreviousMove();
|
||||
Go.nextTurn = Promise.resolve(
|
||||
previousMove
|
||||
? { type: GoPlayType.move, x: previousMove[0], y: previousMove[1] }
|
||||
: { type: GoPlayType.pass, x: null, y: null },
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -51,11 +51,17 @@ export type PointState = {
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type Play = {
|
||||
type: GoPlayType;
|
||||
x: number | null;
|
||||
y: number | null;
|
||||
};
|
||||
export type Play =
|
||||
| {
|
||||
type: GoPlayType.move;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
| {
|
||||
type: GoPlayType.gameOver | GoPlayType.pass;
|
||||
x: null;
|
||||
y: null;
|
||||
};
|
||||
|
||||
export type Neighbor = {
|
||||
north: PointState | null;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { Board, BoardState, Neighbor, PointState, SimpleBoard } from "../Types";
|
||||
|
||||
import { GoValidity, GoOpponent, GoColor } from "@enums";
|
||||
import { Go } from "../Go";
|
||||
import {
|
||||
findAdjacentPointsInChain,
|
||||
findNeighbors,
|
||||
@ -638,3 +639,27 @@ export function getColorOnSimpleBoard(simpleBoard: SimpleBoard, x: number, y: nu
|
||||
if (char === ".") return GoColor.empty;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Find a move made by the previous player, if present. */
|
||||
export function getPreviousMove(): [number, number] | null {
|
||||
const priorBoard = Go.currentGame?.previousBoards[0];
|
||||
if (Go.currentGame.passCount || !priorBoard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const rowIndexString in Go.currentGame.board) {
|
||||
const row = Go.currentGame.board[+rowIndexString] ?? [];
|
||||
for (const pointIndexString in row) {
|
||||
const point = row[+pointIndexString];
|
||||
const priorColor = point && priorBoard && getColorOnSimpleBoard(priorBoard, point.x, point.y);
|
||||
const currentColor = point?.color;
|
||||
const isPreviousPlayer = currentColor === Go.currentGame.previousPlayer;
|
||||
const isChanged = priorColor !== currentColor;
|
||||
if (priorColor && currentColor && isPreviousPlayer && isChanged) {
|
||||
return [+rowIndexString, +pointIndexString];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import type { Board, BoardState, EyeMove, Move, MoveOptions, PointState } from "../Types";
|
||||
import type { Board, BoardState, EyeMove, Move, MoveOptions, Play, PointState } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { AugmentationName, GoOpponent, GoColor, GoPlayType } from "@enums";
|
||||
import { opponentDetails } from "../Constants";
|
||||
import { findNeighbors, floor, isDefined, isNotNull, passTurn } from "../boardState/boardState";
|
||||
import { findNeighbors, floor, isDefined, isNotNull, makeMove, passTurn } from "../boardState/boardState";
|
||||
import {
|
||||
evaluateIfMoveIsValid,
|
||||
evaluateMoveResult,
|
||||
@ -19,6 +19,49 @@ import {
|
||||
import { findDisputedTerritory } from "./controlledTerritory";
|
||||
import { findAnyMatchedPatterns } from "./patternMatching";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
import { Go, GoEvents } from "../Go";
|
||||
|
||||
let currentAITurn: Promise<Play> | null = null;
|
||||
|
||||
/**
|
||||
* Retrieves a move from the current faction in response to the player's move
|
||||
*/
|
||||
export function makeAIMove(boardState: BoardState): Promise<Play> {
|
||||
// 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<Play> => {
|
||||
if (boardState !== Go.currentGame) return play; //Stale game
|
||||
|
||||
// Handle AI passing
|
||||
if (play.type === GoPlayType.pass) {
|
||||
passTurn(boardState, GoColor.white);
|
||||
// if passTurn called endGoGame, or the player has no valid moves left, the move should be shown as a game over
|
||||
if (boardState.previousPlayer === null || !getAllValidMoves(boardState, GoColor.black).length) {
|
||||
return { type: GoPlayType.gameOver, x: null, y: null };
|
||||
}
|
||||
return play;
|
||||
}
|
||||
|
||||
// Handle AI making a move
|
||||
await sleep(500);
|
||||
const aiUpdatedBoard = makeMove(boardState, play.x, play.y, GoColor.white);
|
||||
|
||||
// Handle the AI breaking. This shouldn't ever happen.
|
||||
if (!aiUpdatedBoard) {
|
||||
boardState.previousPlayer = GoColor.white;
|
||||
console.error(`Invalid AI move attempted: ${play.x}, ${play.y}. This should not happen.`);
|
||||
}
|
||||
|
||||
return play;
|
||||
})
|
||||
.finally(() => {
|
||||
currentAITurn = null;
|
||||
GoEvents.emit();
|
||||
});
|
||||
|
||||
return Go.nextTurn;
|
||||
}
|
||||
|
||||
/*
|
||||
Basic GO AIs, each with some personality and weaknesses
|
||||
@ -38,7 +81,12 @@ import { WHRNG } from "../../Casino/RNG";
|
||||
*
|
||||
* @returns a promise that will resolve with a move (or pass) from the designated AI opponent.
|
||||
*/
|
||||
export async function getMove(boardState: BoardState, player: GoColor, opponent: GoOpponent, rngOverride?: number) {
|
||||
export async function getMove(
|
||||
boardState: BoardState,
|
||||
player: GoColor,
|
||||
opponent: GoOpponent,
|
||||
rngOverride?: number,
|
||||
): Promise<Play & { type: GoPlayType.move | GoPlayType.pass }> {
|
||||
await sleep(300);
|
||||
const rng = new WHRNG(rngOverride || Player.totalPlaytime);
|
||||
const smart = isSmart(opponent, rng.random());
|
||||
@ -72,40 +120,10 @@ export async function getMove(boardState: BoardState, player: GoColor, opponent:
|
||||
if (chosenMove) {
|
||||
await sleep(200);
|
||||
//console.debug(`Non-priority move chosen: ${chosenMove.x} ${chosenMove.y}`);
|
||||
return {
|
||||
type: GoPlayType.move,
|
||||
x: chosenMove.x,
|
||||
y: chosenMove.y,
|
||||
};
|
||||
} else {
|
||||
//console.debug("No valid moves found");
|
||||
return handleNoMoveFound(boardState, player);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if the AI is merely passing their turn, or if the game should end.
|
||||
*
|
||||
* Ends the game if the player passed on the previous turn before the AI passes,
|
||||
* or if the player will be forced to pass their next turn after the AI passes.
|
||||
*/
|
||||
function handleNoMoveFound(boardState: BoardState, player: GoColor) {
|
||||
passTurn(boardState, player);
|
||||
const opposingPlayer = player === GoColor.white ? GoColor.black : GoColor.white;
|
||||
const remainingTerritory = getAllValidMoves(boardState, opposingPlayer).length;
|
||||
if (remainingTerritory > 0 && boardState.passCount < 2) {
|
||||
return {
|
||||
type: GoPlayType.pass,
|
||||
x: null,
|
||||
y: null,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: GoPlayType.gameOver,
|
||||
x: null,
|
||||
y: null,
|
||||
};
|
||||
return { type: GoPlayType.move, x: chosenMove.x, y: chosenMove.y };
|
||||
}
|
||||
// Pass if no valid moves were found
|
||||
return { type: GoPlayType.pass, x: null, y: null };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -50,7 +50,6 @@ export function endGoGame(boardState: BoardState) {
|
||||
type: GoPlayType.gameOver,
|
||||
x: null,
|
||||
y: null,
|
||||
success: true,
|
||||
});
|
||||
|
||||
boardState.previousPlayer = null;
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { BoardState, Play, SimpleOpponentStats } from "../Types";
|
||||
import { Play, SimpleOpponentStats } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { AugmentationName, GoColor, GoOpponent, GoPlayType, GoValidity } from "@enums";
|
||||
import { Go, GoEvents } from "../Go";
|
||||
import { getMove, sleep } from "../boardAnalysis/goAI";
|
||||
import { getNewBoardState, makeMove, passTurn, updateCaptures, updateChains } from "../boardState/boardState";
|
||||
import { makeAIMove } from "../boardAnalysis/goAI";
|
||||
import {
|
||||
evaluateIfMoveIsValid,
|
||||
getColorOnSimpleBoard,
|
||||
getControlledSpace,
|
||||
getPreviousMove,
|
||||
simpleBoardFromBoard,
|
||||
} from "../boardAnalysis/boardAnalysis";
|
||||
import { getOpponentStats, getScore, resetWinstreak } from "../boardAnalysis/scoring";
|
||||
@ -104,7 +104,7 @@ export async function handlePassTurn(logger: (s: string) => void) {
|
||||
logEndGame(logger);
|
||||
return getOpponentNextMove(false, logger);
|
||||
} else {
|
||||
return getAIMove(Go.currentGame);
|
||||
return makeAIMove(Go.currentGame);
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,26 +122,13 @@ export async function makePlayerMove(logger: (s: string) => void, error: (s: str
|
||||
|
||||
GoEvents.emit();
|
||||
logger(`Go move played: ${x}, ${y}`);
|
||||
return getAIMove(boardState);
|
||||
return makeAIMove(boardState);
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the promise that provides the opponent's move, once it finishes thinking.
|
||||
*/
|
||||
export async function getOpponentNextMove(logOpponentMove = true, logger: (s: string) => void) {
|
||||
// Handle the case where Go.nextTurn isn't populated yet
|
||||
if (!Go.nextTurn) {
|
||||
const previousMove = getPreviousMove();
|
||||
const type =
|
||||
Go.currentGame.previousPlayer === null ? GoPlayType.gameOver : previousMove ? GoPlayType.move : GoPlayType.pass;
|
||||
|
||||
Go.nextTurn = Promise.resolve({
|
||||
type,
|
||||
x: previousMove?.[0] ?? null,
|
||||
y: previousMove?.[1] ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
// Only asynchronously log the opponent move if not disabled by the player
|
||||
if (logOpponentMove) {
|
||||
return Go.nextTurn.then((move) => {
|
||||
@ -159,43 +146,6 @@ export async function getOpponentNextMove(logOpponentMove = true, logger: (s: st
|
||||
return Go.nextTurn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a move from the current faction in response to the player's move
|
||||
*/
|
||||
export async function getAIMove(boardState: BoardState): Promise<Play> {
|
||||
let resolve: (value: Play) => void;
|
||||
Go.nextTurn = new Promise<Play>((res) => {
|
||||
resolve = res;
|
||||
});
|
||||
|
||||
getMove(boardState, GoColor.white, Go.currentGame.ai).then(async (result) => {
|
||||
if (result.type === GoPlayType.pass) {
|
||||
passTurn(Go.currentGame, GoColor.white);
|
||||
}
|
||||
|
||||
// If there is no move to apply, simply return the result
|
||||
if (boardState !== Go.currentGame || result.type !== GoPlayType.move || result.x === null || result.y === null) {
|
||||
return resolve(result);
|
||||
}
|
||||
|
||||
await sleep(400);
|
||||
const aiUpdatedBoard = makeMove(boardState, result.x, result.y, GoColor.white);
|
||||
|
||||
// Handle the AI breaking. This shouldn't ever happen.
|
||||
if (!aiUpdatedBoard) {
|
||||
boardState.previousPlayer = GoColor.white;
|
||||
console.error(`Invalid AI move attempted: ${result.x}, ${result.y}. This should not happen.`);
|
||||
GoEvents.emit();
|
||||
return resolve(result);
|
||||
}
|
||||
|
||||
await sleep(300);
|
||||
GoEvents.emit();
|
||||
resolve(result);
|
||||
});
|
||||
return Go.nextTurn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a grid of booleans indicating if the coordinates at that location are a valid move for the player (black pieces)
|
||||
*/
|
||||
@ -297,32 +247,6 @@ export function getCurrentPlayer(): "None" | "White" | "Black" {
|
||||
return Go.currentGame.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a move made by the previous player, if present.
|
||||
*/
|
||||
export function getPreviousMove(): [number, number] | null {
|
||||
const priorBoard = Go.currentGame?.previousBoards[0];
|
||||
if (Go.currentGame.passCount || !priorBoard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const rowIndexString in Go.currentGame.board) {
|
||||
const row = Go.currentGame.board[+rowIndexString] ?? [];
|
||||
for (const pointIndexString in row) {
|
||||
const point = row[+pointIndexString];
|
||||
const priorColor = point && priorBoard && getColorOnSimpleBoard(priorBoard, point.x, point.y);
|
||||
const currentColor = point?.color;
|
||||
const isPreviousPlayer = currentColor === Go.currentGame.previousPlayer;
|
||||
const isChanged = priorColor !== currentColor;
|
||||
if (priorColor && currentColor && isPreviousPlayer && isChanged) {
|
||||
return [+rowIndexString, +pointIndexString];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle post-game logging
|
||||
*/
|
||||
@ -418,7 +342,7 @@ export async function determineCheatSuccess(
|
||||
callback();
|
||||
state.cheatCount++;
|
||||
GoEvents.emit();
|
||||
return getAIMove(state);
|
||||
return makeAIMove(state);
|
||||
}
|
||||
// If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly losing
|
||||
else if (state.cheatCount && (ejectRngOverride ?? rng.random()) < 0.1) {
|
||||
@ -435,7 +359,7 @@ export async function determineCheatSuccess(
|
||||
logger(`Cheat failed. Your turn has been skipped.`);
|
||||
passTurn(state, GoColor.black, false);
|
||||
state.cheatCount++;
|
||||
return getAIMove(state);
|
||||
return makeAIMove(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ import { GoScoreModal } from "./GoScoreModal";
|
||||
import { GoGameboard } from "./GoGameboard";
|
||||
import { GoSubnetSearch } from "./GoSubnetSearch";
|
||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||
import { getAIMove } from "../effects/netscriptGoImplementation";
|
||||
import { makeAIMove } from "../boardAnalysis/goAI";
|
||||
|
||||
interface GoGameboardWrapperProps {
|
||||
showInstructions: () => void;
|
||||
@ -45,21 +45,13 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
const [showPriorMove, setShowPriorMove] = useState(false);
|
||||
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 === GoColor.white ? GoColor.black : GoColor.white;
|
||||
const waitingOnAI = boardState.previousPlayer === GoColor.black && boardState.ai !== GoOpponent.none;
|
||||
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 === GoColor.black && !waitingOnAI && boardState.ai !== GoOpponent.none) {
|
||||
takeAiTurn(Go.currentGame);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Do not implement useCallback for this function without ensuring GoGameboard still rerenders for every move
|
||||
// Currently this function changing is what triggers a GoGameboard rerender, which is needed
|
||||
async function clickHandler(x: number, y: number) {
|
||||
@ -117,8 +109,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
}
|
||||
|
||||
async function takeAiTurn(boardState: BoardState) {
|
||||
setWaitingOnAI(true);
|
||||
const move = await getAIMove(boardState);
|
||||
const move = await makeAIMove(boardState);
|
||||
|
||||
if (move.type === GoPlayType.pass) {
|
||||
SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000);
|
||||
@ -130,7 +121,6 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
setScoreOpen(true);
|
||||
return;
|
||||
}
|
||||
setWaitingOnAI(false);
|
||||
}
|
||||
|
||||
function newSubnet() {
|
||||
@ -172,8 +162,6 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
const endGameAvailable = boardState.previousPlayer === GoColor.white && boardState.passCount;
|
||||
const noLegalMoves =
|
||||
boardState.previousPlayer === GoColor.white && !getAllValidMoves(boardState, GoColor.black).length;
|
||||
const disablePassButton =
|
||||
Go.currentGame.ai !== GoOpponent.none && boardState.previousPlayer === GoColor.black && waitingOnAI;
|
||||
|
||||
const scoreBoxText = boardState.previousBoards.length
|
||||
? `Score: Black: ${score[GoColor.black].sum} White: ${score[GoColor.white].sum}`
|
||||
@ -186,7 +174,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
if (boardState.previousPlayer === null) {
|
||||
return "View Final Score";
|
||||
}
|
||||
if (boardState.previousPlayer === GoColor.black && waitingOnAI) {
|
||||
if (waitingOnAI) {
|
||||
return "Waiting for opponent";
|
||||
}
|
||||
const currentPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
|
||||
@ -242,7 +230,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
</Button>
|
||||
<Typography className={classes.scoreBox}>{scoreBoxText}</Typography>
|
||||
<Button
|
||||
disabled={disablePassButton}
|
||||
disabled={waitingOnAI}
|
||||
onClick={passPlayerTurn}
|
||||
className={endGameAvailable || noLegalMoves ? classes.buttonHighlight : classes.resetBoard}
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user