IPVGO: Remove current game history from savefile, re-implement superko (#1175)

This commit is contained in:
Michael Ficocelli 2024-03-20 20:37:20 -04:00 committed by GitHub
parent fc8958af83
commit 1e5f7184a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 45 additions and 29 deletions

@ -12,7 +12,6 @@ import { isInteger, isNumber } from "../types";
type PreviousGameSaveData = { ai: GoOpponent; board: SimpleBoard; previousPlayer: GoColor | null } | null; type PreviousGameSaveData = { ai: GoOpponent; board: SimpleBoard; previousPlayer: GoColor | null } | null;
type CurrentGameSaveData = PreviousGameSaveData & { type CurrentGameSaveData = PreviousGameSaveData & {
previousBoard: SimpleBoard | null;
cheatCount: number; cheatCount: number;
passCount: number; passCount: number;
}; };
@ -35,7 +34,6 @@ export function getGoSave(): SaveFormat {
currentGame: { currentGame: {
ai: Go.currentGame.ai, ai: Go.currentGame.ai,
board: simpleBoardFromBoard(Go.currentGame.board), board: simpleBoardFromBoard(Go.currentGame.board),
previousBoard: Go.currentGame.previousBoard,
previousPlayer: Go.currentGame.previousPlayer, previousPlayer: Go.currentGame.previousPlayer,
cheatCount: Go.currentGame.cheatCount, cheatCount: Go.currentGame.cheatCount,
passCount: Go.currentGame.passCount, passCount: Go.currentGame.passCount,
@ -94,8 +92,6 @@ function loadCurrentGame(currentGame: unknown): BoardState | string {
const requiredSize = currentGame.board.length; const requiredSize = currentGame.board.length;
const board = loadSimpleBoard(currentGame.board, requiredSize); const board = loadSimpleBoard(currentGame.board, requiredSize);
if (typeof board === "string") return board; if (typeof board === "string") return board;
const previousBoard = currentGame.previousBoard ? loadSimpleBoard(currentGame.previousBoard, requiredSize) : null;
if (typeof previousBoard === "string") return previousBoard;
const previousPlayer = getEnumHelper("GoColor").getMember(currentGame.previousPlayer) ?? null; const previousPlayer = getEnumHelper("GoColor").getMember(currentGame.previousPlayer) ?? null;
if (!isInteger(currentGame.cheatCount) || currentGame.cheatCount < 0) if (!isInteger(currentGame.cheatCount) || currentGame.cheatCount < 0)
return "invalid number for currentGame.cheatCount"; return "invalid number for currentGame.cheatCount";
@ -105,7 +101,7 @@ function loadCurrentGame(currentGame: unknown): BoardState | string {
boardState.previousPlayer = previousPlayer; boardState.previousPlayer = previousPlayer;
boardState.cheatCount = currentGame.cheatCount; boardState.cheatCount = currentGame.cheatCount;
boardState.passCount = currentGame.passCount; boardState.passCount = currentGame.passCount;
boardState.previousBoard = previousBoard; boardState.previousBoards = [];
return boardState; return boardState;
} }

@ -36,8 +36,8 @@ export type EyeMove = {
export type BoardState = { export type BoardState = {
board: Board; board: Board;
previousPlayer: GoColor | null; previousPlayer: GoColor | null;
/** The previous board position as a SimpleBoard */ /** The previous board positions as a SimpleBoard */
previousBoard: SimpleBoard | null; previousBoards: SimpleBoard[];
ai: GoOpponent; ai: GoOpponent;
passCount: number; passCount: number;
cheatCount: number; cheatCount: number;

@ -44,7 +44,7 @@ export function evaluateIfMoveIsValid(boardState: BoardState, x: number, y: numb
} }
// Detect if the move might be an immediate repeat (only one board of history is saved to check) // Detect if the move might be an immediate repeat (only one board of history is saved to check)
const possibleRepeat = boardState.previousBoard && getColorOnSimpleBoard(boardState.previousBoard, x, y) === player; const possibleRepeat = boardState.previousBoards.find((board) => getColorOnSimpleBoard(board, x, y) === player);
if (shortcut) { if (shortcut) {
// If the current point has some adjacent open spaces, it is not suicide. If the move is not repeated, it is legal // If the current point has some adjacent open spaces, it is not suicide. If the move is not repeated, it is legal
@ -85,9 +85,11 @@ export function evaluateIfMoveIsValid(boardState: BoardState, x: number, y: numb
if (evaluationBoard[x]?.[y]?.color !== player) { if (evaluationBoard[x]?.[y]?.color !== player) {
return GoValidity.noSuicide; return GoValidity.noSuicide;
} }
if (possibleRepeat && boardState.previousBoard) { if (possibleRepeat && boardState.previousBoards.length) {
const simpleEvalBoard = simpleBoardFromBoard(evaluationBoard); const simpleEvalBoard = simpleBoardFromBoard(evaluationBoard);
if (areSimpleBoardsIdentical(simpleEvalBoard, boardState.previousBoard)) return GoValidity.boardRepeated; if (boardState.previousBoards.find((board) => areSimpleBoardsIdentical(simpleEvalBoard, board))) {
return GoValidity.boardRepeated;
}
} }
return GoValidity.valid; return GoValidity.valid;

@ -27,7 +27,7 @@ export function getNewBoardState(
} }
const newBoardState: BoardState = { const newBoardState: BoardState = {
previousBoard: null, previousBoards: [],
previousPlayer: GoColor.white, previousPlayer: GoColor.white,
ai: ai, ai: ai,
passCount: 0, passCount: 0,
@ -82,7 +82,12 @@ export function makeMove(boardState: BoardState, x: number, y: number, player: G
return false; return false;
} }
boardState.previousBoard = simpleBoardFromBoard(boardState.board); // Only maintain last 7 moves
boardState.previousBoards.unshift(simpleBoardFromBoard(boardState.board));
if (boardState.previousBoards.length > 7) {
boardState.previousBoards.pop();
}
const point = boardState.board[x][y]; const point = boardState.board[x][y];
if (!point) return false; if (!point) return false;
@ -266,7 +271,7 @@ export function getEmptySpaces(board: Board): PointState[] {
export function getStateCopy(initialState: BoardState) { export function getStateCopy(initialState: BoardState) {
const boardState = structuredClone(initialState); const boardState = structuredClone(initialState);
boardState.previousBoard = initialState.previousBoard ? [...initialState.previousBoard] : null; boardState.previousBoards = initialState.previousBoards ?? [];
boardState.previousPlayer = initialState.previousPlayer; boardState.previousPlayer = initialState.previousPlayer;
boardState.ai = initialState.ai; boardState.ai = initialState.ai;
boardState.passCount = initialState.passCount; boardState.passCount = initialState.passCount;

@ -98,7 +98,7 @@ function calculateMults(): Multipliers {
} }
export function playerHasDiscoveredGo() { export function playerHasDiscoveredGo() {
const playedGame = Go.currentGame.previousBoard; const playedGame = Go.currentGame.previousBoards.length;
const hasRecords = getRecordValues(Go.stats).some((stats) => stats.wins + stats.losses); const hasRecords = getRecordValues(Go.stats).some((stats) => stats.wins + stats.losses);
const isInBn14 = Player.bitNodeN === 14; const isInBn14 = Player.bitNodeN === 14;

@ -300,11 +300,11 @@ export function getCurrentPlayer(): "None" | "White" | "Black" {
* Find a move made by the previous player, if present. * Find a move made by the previous player, if present.
*/ */
export function getPreviousMove(): [number, number] | null { export function getPreviousMove(): [number, number] | null {
if (Go.currentGame.passCount) { const priorBoard = Go.currentGame?.previousBoards[0];
if (Go.currentGame.passCount || !priorBoard) {
return null; return null;
} }
const priorBoard = Go.currentGame?.previousBoard;
for (const rowIndexString in Go.currentGame.board) { for (const rowIndexString in Go.currentGame.board) {
const row = Go.currentGame.board[+rowIndexString] ?? []; const row = Go.currentGame.board[+rowIndexString] ?? [];
for (const pointIndexString in row) { for (const pointIndexString in row) {
@ -348,7 +348,7 @@ export function resetBoardState(error: (s: string) => void, opponent: GoOpponent
} }
const oldBoardState = Go.currentGame; const oldBoardState = Go.currentGame;
if (oldBoardState.previousPlayer !== null && oldBoardState.previousBoard) { if (oldBoardState.previousPlayer !== null && oldBoardState.previousBoards.length) {
resetWinstreak(oldBoardState.ai, false); resetWinstreak(oldBoardState.ai, false);
} }

@ -143,7 +143,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
setScoreOpen(false); setScoreOpen(false);
setSearchOpen(false); setSearchOpen(false);
setOpponent(newOpponent); setOpponent(newOpponent);
if (boardState.previousPlayer !== null && boardState.previousBoard) { if (boardState.previousPlayer !== null && boardState.previousBoards.length) {
resetWinstreak(boardState.ai, false); resetWinstreak(boardState.ai, false);
} }
@ -152,16 +152,16 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
} }
function getPriorMove() { function getPriorMove() {
if (!boardState.previousBoard) return boardState; if (!boardState.previousBoards.length) return boardState;
const priorState = getStateCopy(boardState); const priorState = getStateCopy(boardState);
priorState.previousPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black; priorState.previousPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
priorState.board = boardFromSimpleBoard(boardState.previousBoard); priorState.board = boardFromSimpleBoard(boardState.previousBoards[0]);
updateCaptures(priorState.board, priorState.previousPlayer); updateCaptures(priorState.board, priorState.previousPlayer);
return priorState; return priorState;
} }
function showPreviousMove(newValue: boolean) { function showPreviousMove(newValue: boolean) {
if (boardState.previousBoard) { if (boardState.previousBoards.length) {
setShowPriorMove(newValue); setShowPriorMove(newValue);
} }
} }
@ -175,7 +175,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
boardState.previousPlayer === GoColor.white && !getAllValidMoves(boardState, GoColor.black).length; boardState.previousPlayer === GoColor.white && !getAllValidMoves(boardState, GoColor.black).length;
const disablePassButton = opponent !== GoOpponent.none && boardState.previousPlayer === GoColor.black && waitingOnAI; const disablePassButton = opponent !== GoOpponent.none && boardState.previousPlayer === GoColor.black && waitingOnAI;
const scoreBoxText = boardState.previousBoard 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!";
@ -256,7 +256,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
/> />
<OptionSwitch <OptionSwitch
checked={showPriorMove} checked={showPriorMove}
disabled={!boardState.previousBoard} disabled={!boardState.previousBoards.length}
onChange={(newValue) => showPreviousMove(newValue)} onChange={(newValue) => showPreviousMove(newValue)}
text="Show previous move" text="Show previous move"
tooltip={<>Show the board as it was before the last move</>} tooltip={<>Show the board as it was before the last move</>}

@ -41,7 +41,8 @@ export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwne
const sizeClass = getSizeClass(state.board[0].length, classes); const sizeClass = getSizeClass(state.board[0].length, classes);
const isNewStone = state.previousBoard && getColorOnSimpleBoard(state.previousBoard, x, y) === GoColor.empty; const isNewStone =
state.previousBoards.length && getColorOnSimpleBoard(state.previousBoards[0], x, y) === GoColor.empty;
const isPriorMove = player === state.previousPlayer && isNewStone; const isPriorMove = player === state.previousPlayer && isNewStone;
const emptyPointColorClass = const emptyPointColorClass =

@ -38,7 +38,7 @@ export function GoTutorialChallenge({
const [showReset, setShowReset] = useState(false); const [showReset, setShowReset] = useState(false);
const handleClick = (x: number, y: number) => { const handleClick = (x: number, y: number) => {
if (stateRef.current.previousBoard) { if (stateRef.current.previousBoards.length) {
SnackbarEvents.emit(`Hit 'Reset' to try again`, ToastVariant.WARNING, 2000); SnackbarEvents.emit(`Hit 'Reset' to try again`, ToastVariant.WARNING, 2000);
return; return;
} }
@ -67,6 +67,7 @@ export function GoTutorialChallenge({
const reset = () => { const reset = () => {
stateRef.current = getStateCopy(state); stateRef.current = getStateCopy(state);
stateRef.current.previousBoards = [];
setDisplayText(description); setDisplayText(description);
setShowReset(false); setShowReset(false);
}; };

@ -81,7 +81,7 @@ describe("Netscript Go API unit tests", () => {
it("should correctly retrieve the current game state", async () => { it("should correctly retrieve the current game state", async () => {
const board = ["OXX..", ".....", ".....", "...XX", "...X."]; const board = ["OXX..", ".....", ".....", "...XX", "...X."];
const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.black); const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.black);
boardState.previousBoard = ["OX..", ".....", ".....", "...XX", "...X."]; boardState.previousBoards = [["OX..", ".....", ".....", "...XX", "...X."]];
Go.currentGame = boardState; Go.currentGame = boardState;
const result = getGameState(); const result = getGameState();
@ -232,7 +232,7 @@ describe("Netscript Go API unit tests", () => {
await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 1, 0); await cheatPlayTwoMoves(mockLogger, 4, 3, 3, 4, 1, 0);
expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet."); expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
expect(Go.currentGame.previousBoard).toEqual(null); expect(Go.currentGame.previousBoards).toEqual([]);
}); });
}); });
describe("cheatRemoveRouter() tests", () => { describe("cheatRemoveRouter() tests", () => {
@ -270,7 +270,7 @@ describe("Netscript Go API unit tests", () => {
await cheatRemoveRouter(mockLogger, 0, 0, 1, 0); await cheatRemoveRouter(mockLogger, 0, 0, 1, 0);
expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet."); expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
expect(Go.currentGame.previousBoard).toEqual(null); expect(Go.currentGame.previousBoards).toEqual([]);
}); });
}); });
describe("cheatRepairOfflineNode() tests", () => { describe("cheatRepairOfflineNode() tests", () => {

@ -53,4 +53,16 @@ describe("Go board analysis tests", () => {
expect(validity).toEqual(GoValidity.noSuicide); expect(validity).toEqual(GoValidity.noSuicide);
}); });
it("identifies invalid moves from repeat", async () => {
const board = [".X...", ".....", ".....", ".....", "....."];
const boardState = boardStateFromSimpleBoard(board);
boardState.previousBoards.push([".X...", ".....", ".....", ".....", "....."]);
boardState.previousBoards.push([".X...", ".....", ".....", ".....", "....."]);
boardState.previousBoards.push([".X...", ".....", ".....", ".....", "....."]);
boardState.previousBoards.push(["OX...", ".....", ".....", ".....", "....."]);
const validity = evaluateIfMoveIsValid(boardState, 0, 0, GoColor.white, false);
expect(validity).toEqual(GoValidity.boardRepeated);
});
}); });

@ -41,7 +41,6 @@ exports[`Check Save File Continuity GoSave continuity 1`] = `
], ],
"cheatCount": 0, "cheatCount": 0,
"passCount": 0, "passCount": 0,
"previousBoard": null,
"previousPlayer": "White", "previousPlayer": "White",
}, },
"previousGame": null, "previousGame": null,