GO: Alternate fix for race conditions (#1260)

This commit is contained in:
Snarling 2024-05-11 19:58:59 -04:00 committed by GitHub
parent 1b8205e9d5
commit e23db93c8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 119 additions and 146 deletions

@ -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}
>