diff --git a/assets/Steam/achievements/icons/BN14+.svg b/assets/Steam/achievements/icons/BN14+.svg new file mode 100644 index 000000000..2036eaed9 --- /dev/null +++ b/assets/Steam/achievements/icons/BN14+.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + BN14+ + + diff --git a/assets/Steam/achievements/icons/SF14.1.svg b/assets/Steam/achievements/icons/SF14.1.svg new file mode 100644 index 000000000..31ac91b6b --- /dev/null +++ b/assets/Steam/achievements/icons/SF14.1.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + SF14.1 + + diff --git a/assets/Steam/achievements/icons/ipvgo-anticheat.svg b/assets/Steam/achievements/icons/ipvgo-anticheat.svg new file mode 100644 index 000000000..77490021b --- /dev/null +++ b/assets/Steam/achievements/icons/ipvgo-anticheat.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + IPvGOAnticheat + + diff --git a/assets/Steam/achievements/icons/ipvgo-winning-streak.svg b/assets/Steam/achievements/icons/ipvgo-winning-streak.svg new file mode 100644 index 000000000..24993670f --- /dev/null +++ b/assets/Steam/achievements/icons/ipvgo-winning-streak.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + 10 Dan + + diff --git a/assets/Steam/achievements/template.svg b/assets/Steam/achievements/template.svg index 7a99878c8..dc0d8f69c 100644 --- a/assets/Steam/achievements/template.svg +++ b/assets/Steam/achievements/template.svg @@ -56,13 +56,13 @@ id="layer1"> TEXT diff --git a/dist/icons/achievements/BN14+.svg b/dist/icons/achievements/BN14+.svg new file mode 100644 index 000000000..967d5febe --- /dev/null +++ b/dist/icons/achievements/BN14+.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + BN14+ + + diff --git a/dist/icons/achievements/SF14.1.svg b/dist/icons/achievements/SF14.1.svg new file mode 100644 index 000000000..91df118eb --- /dev/null +++ b/dist/icons/achievements/SF14.1.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + SF14.1 + + diff --git a/dist/icons/achievements/ipvgo-anticheat.svg b/dist/icons/achievements/ipvgo-anticheat.svg new file mode 100644 index 000000000..28363990d --- /dev/null +++ b/dist/icons/achievements/ipvgo-anticheat.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + IPvGOAnticheat + + diff --git a/dist/icons/achievements/ipvgo-winning-streak.svg b/dist/icons/achievements/ipvgo-winning-streak.svg new file mode 100644 index 000000000..922b88559 --- /dev/null +++ b/dist/icons/achievements/ipvgo-winning-streak.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + 10 Dan + + diff --git a/src/Achievements/AchievementData.json b/src/Achievements/AchievementData.json index 252262a54..a30a3e7ad 100644 --- a/src/Achievements/AchievementData.json +++ b/src/Achievements/AchievementData.json @@ -131,6 +131,11 @@ "Name": "They're lunatics", "Description": "Acquire SF13.1" }, + "SF14.1": { + "ID": "SF14.1", + "Name": "IPvGO Subnet Takeover", + "Description": "Acquire SF14.1" + }, "MONEY_1Q": { "ID": "MONEY_1Q", "Name": "Here comes the money!", @@ -391,6 +396,16 @@ "Name": "More BitNodes, please!", "Description": "Reach SF x.3 in each BitNode." }, + "IPVGO_ANTICHEAT": { + "ID": "IPVGO_ANTICHEAT", + "Name": "IPvGO anticheat", + "Description": "Fail to cheat an IPvGO game and be ejected from the subnet." + }, + "IPVGO_WINNING_STREAK": { + "ID": "IPVGO_WINNING_STREAK", + "Name": "Ten Steps Ahead", + "Description": "Get a winning streak of 10 against Illuminati." + }, "CHALLENGE_BN1": { "ID": "CHALLENGE_BN1", "Name": "BN1: Challenge", @@ -436,6 +451,16 @@ "Name": "BN12: Challenge", "Description": "Destroy BN12 50 times." }, + "CHALLENGE_BN13": { + "ID": "CHALLENGE_BN13", + "Name": "BN13: Challenge", + "Description": "Complete BN13 without Stanek's Gift." + }, + "CHALLENGE_BN14": { + "ID": "CHALLENGE_BN14", + "Name": "BN14: Challenge", + "Description": "Destroy BN14 without making a move or cheating via IPvGO APIs." + }, "BYPASS": { "ID": "BYPASS", "Name": "Exploit: bypass", @@ -481,11 +506,6 @@ "Name": "UNACHIEVABLE", "Description": "This achievement cannot be unlocked." }, - "CHALLENGE_BN13": { - "ID": "CHALLENGE_BN13", - "Name": "BN13: Challenge", - "Description": "Complete BN13 without Stanek's Gift." - }, "DEVMENU": { "ID": "DEVMENU", "Name": "Exploit: you're not meant to access this", diff --git a/src/Achievements/Achievements.ts b/src/Achievements/Achievements.ts index 0c169afa4..77dce643c 100644 --- a/src/Achievements/Achievements.ts +++ b/src/Achievements/Achievements.ts @@ -29,16 +29,51 @@ import { workerScripts } from "../Netscript/WorkerScripts"; import { getRecordValues } from "../Types/Record"; import { ServerConstants } from "../Server/data/Constants"; -import { canAccessBitNodeFeature, isBitNodeFinished, knowAboutBitverse, validBitNodes } from "../BitNode/BitNodeUtils"; +import { canAccessBitNodeFeature, isBitNodeFinished, knowAboutBitverse } from "../BitNode/BitNodeUtils"; +import { validBitNodes } from "../BitNode/Constants"; import { isLegacyScript } from "../Paths/ScriptFilePath"; import { Settings } from "../Settings/Settings"; import { activateSteamAchievements } from "../Electron"; +import { Go } from "../Go/Go"; +import { type AchievementId, type SFAchievementId, SFAchievementIds } from "./Types"; -// Unable to correctly cast the JSON data into AchievementDataJson type otherwise... -const achievementData = ((data)).achievements; +function assertAchievements( + achievements: typeof data.achievements, +): asserts achievements is AchievementDataJson["achievements"] { + for (const [key, value] of Object.entries(achievements)) { + if (key !== value.ID) { + throw new Error(`Invalid achievement ID. Key: ${key}. Value: ${value.ID}`); + } + } +} + +/** + * The type of data.achievements is: + { + CYBERSEC: { + ID: string; + Name: string; + Description: string; + }; + NITESEC: { + ID: string; + Name: string; + Description: string; + }; + ... + } + * However, we want: + * - Typechecking at compile time: ID must be AchievementId, not string. + * - Runtime check: The value of ID must be the same as the key of the achievement. For example, with "CYBERSEC" + * achievement, the key is "CYBERSEC", so its ID must also be "CYBERSEC". + * + * We use assertAchievements to do the runtime check and assert the type. + */ +const achievementData = data.achievements; +assertAchievements(achievementData); export interface Achievement { - ID: string; + ID: AchievementId; Icon?: string; Name?: string; Description?: string; @@ -46,48 +81,59 @@ export interface Achievement { NotInSteam?: boolean; Condition: () => boolean; Visible?: () => boolean; - AdditionalUnlock?: string[]; // IDs of achievements that should be awarded when awarding this one + AdditionalUnlock?: AchievementId[]; // IDs of achievements that should be awarded when awarding this one } export interface PlayerAchievement { - ID: string; + ID: AchievementId; unlockedOn?: number; } export interface AchievementDataJson { - achievements: Record; + achievements: Record; } export interface AchievementData { - ID: string; + ID: AchievementId; Name: string; Description: string; } -function sfAchievements(): Record { - const achievements: Record = {}; - for (let i = 1; i <= 13; i++) { - const ID = `SF${i}.1`; - achievements[ID] = { - ...achievementData[ID], - Icon: ID, +function sfAchievements(): Record { + const achievements = {} as Record; + for (const id of SFAchievementIds) { + const matchResult = id.match(/SF(\d{1,2})\.1/); + if (!matchResult) { + throw new Error(`Unexpected SFAchievementId: ${id}`); + } + const bn = Number.parseInt(matchResult[1]); + if (!validBitNodes.includes(bn)) { + throw new Error(`Unexpected BN value in SFAchievementId: ${id}`); + } + achievements[id] = { + /** + * The type of achievementData is still the original type (CYBERSEC: { ID: string; Name: string; Description: string; }). + * We have to typecast it here. + */ + ...(achievementData as AchievementDataJson["achievements"])[id], + Icon: id, Visible: knowAboutBitverse, - Condition: () => Player.sourceFileLvl(i) >= 1, - NotInSteam: i >= 13, + Condition: () => Player.sourceFileLvl(bn) >= 1, + NotInSteam: bn >= 13, }; } return achievements; } -export const achievements: Record = { - [FactionName.CyberSec.toUpperCase()]: { - ...achievementData[FactionName.CyberSec.toUpperCase()], +export const achievements: Record = { + CYBERSEC: { + ...achievementData.CYBERSEC, Icon: "CSEC", Condition: () => Player.factions.includes(FactionName.CyberSec), }, - [FactionName.NiteSec.toUpperCase()]: { - ...achievementData[FactionName.NiteSec.toUpperCase()], - Icon: FactionName.NiteSec, + NITESEC: { + ...achievementData.NITESEC, + Icon: "NiteSec", Condition: () => Player.factions.includes(FactionName.NiteSec), }, THE_BLACK_HAND: { @@ -95,24 +141,24 @@ export const achievements: Record = { Icon: "TBH", Condition: () => Player.factions.includes(FactionName.TheBlackHand), }, - [FactionName.BitRunners.toUpperCase()]: { - ...achievementData[FactionName.BitRunners.toUpperCase()], - Icon: FactionName.BitRunners.toLowerCase(), + BITRUNNERS: { + ...achievementData.BITRUNNERS, + Icon: "bitrunners", Condition: () => Player.factions.includes(FactionName.BitRunners), }, - [FactionName.Daedalus.toUpperCase()]: { - ...achievementData[FactionName.Daedalus.toUpperCase()], - Icon: FactionName.Daedalus.toLowerCase(), + DAEDALUS: { + ...achievementData.DAEDALUS, + Icon: "daedalus", Condition: () => Player.factions.includes(FactionName.Daedalus), }, THE_COVENANT: { ...achievementData.THE_COVENANT, - Icon: FactionName.TheCovenant.toLowerCase().replace(/ /g, ""), + Icon: "thecovenant", Condition: () => Player.factions.includes(FactionName.TheCovenant), }, - [FactionName.Illuminati.toUpperCase()]: { - ...achievementData[FactionName.Illuminati.toUpperCase()], - Icon: FactionName.Illuminati.toLowerCase(), + ILLUMINATI: { + ...achievementData.ILLUMINATI, + Icon: "illuminati", Condition: () => Player.factions.includes(FactionName.Illuminati), }, "BRUTESSH.EXE": { @@ -522,6 +568,20 @@ export const achievements: Record = { Condition: () => validBitNodes.every((bn) => Player.sourceFileLvl(bn) >= 3), NotInSteam: true, }, + IPVGO_ANTICHEAT: { + ...achievementData.IPVGO_ANTICHEAT, + Icon: "ipvgo-anticheat", + Visible: knowAboutBitverse, + Condition: () => false, + NotInSteam: true, + }, + IPVGO_WINNING_STREAK: { + ...achievementData.IPVGO_WINNING_STREAK, + Icon: "ipvgo-winning-streak", + Visible: knowAboutBitverse, + Condition: () => false, + NotInSteam: true, + }, CHALLENGE_BN1: { ...achievementData.CHALLENGE_BN1, Icon: "BN1+", @@ -596,6 +656,22 @@ export const achievements: Record = { Visible: () => canAccessBitNodeFeature(12), Condition: () => Player.sourceFileLvl(12) >= 50, }, + CHALLENGE_BN13: { + ...achievementData.CHALLENGE_BN13, + Icon: "BN13+", + Visible: () => canAccessBitNodeFeature(13), + Condition: () => + Player.bitNodeN === 13 && + isBitNodeFinished() && + !Player.augmentations.some((a) => a.name === AugmentationName.StaneksGift1), + }, + CHALLENGE_BN14: { + ...achievementData.CHALLENGE_BN14, + Icon: "BN14+", + Visible: knowAboutBitverse, + Condition: () => Player.bitNodeN === 14 && isBitNodeFinished() && !Go.moveOrCheatViaApi, + NotInSteam: true, + }, BYPASS: { ...achievementData.BYPASS, Icon: "SF-1", @@ -651,15 +727,6 @@ export const achievements: Record = { // Hey Players! Yes, you're supposed to modify this to get the achievement! Condition: () => false, }, - CHALLENGE_BN13: { - ...achievementData.CHALLENGE_BN13, - Icon: "BN13+", - Visible: () => canAccessBitNodeFeature(13), - Condition: () => - Player.bitNodeN === 13 && - isBitNodeFinished() && - !Player.augmentations.some((a) => a.name === AugmentationName.StaneksGift1), - }, DEVMENU: { ...achievementData.DEVMENU, Icon: "SF-1", diff --git a/src/Achievements/README.md b/src/Achievements/README.md index 738b6b9b8..28052e947 100644 --- a/src/Achievements/README.md +++ b/src/Achievements/README.md @@ -23,5 +23,7 @@ Steps: - Match the order of achievements in `AchievementData.json`. - `Icon` must be the name of the .svg file. - `NotInSteam` must be true. -- Run `pack-for-web.sh`. +- Run `pack-for-web.sh` in `$PROJECT_DIR/assets/Steam/achievements`. - When committing, remember to commit the changes in `$PROJECT_DIR/dist/icons/achievements`. + +Note: If you add a new SFx.1 achievement, you must add its ID to `SFAchievementIds` in `$PROJECT_DIR/src/Achievements/Types.ts`. diff --git a/src/Achievements/Types.ts b/src/Achievements/Types.ts new file mode 100644 index 000000000..6105f7368 --- /dev/null +++ b/src/Achievements/Types.ts @@ -0,0 +1,20 @@ +import data from "./AchievementData.json"; + +export type AchievementId = keyof typeof data.achievements; +export const SFAchievementIds = [ + "SF1.1", + "SF2.1", + "SF3.1", + "SF4.1", + "SF5.1", + "SF6.1", + "SF7.1", + "SF8.1", + "SF9.1", + "SF10.1", + "SF11.1", + "SF12.1", + "SF13.1", + "SF14.1", +] as const; +export type SFAchievementId = (typeof SFAchievementIds)[number]; diff --git a/src/BitNode/BitNodeUtils.ts b/src/BitNode/BitNodeUtils.ts index e36957145..c21d25115 100644 --- a/src/BitNode/BitNodeUtils.ts +++ b/src/BitNode/BitNodeUtils.ts @@ -4,8 +4,7 @@ import { GetServer } from "../Server/AllServers"; import { Server } from "../Server/Server"; import { SpecialServers } from "../Server/data/SpecialServers"; import { JSONMap } from "../Types/Jsonable"; - -export const validBitNodes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; +import { validBitNodes } from "./Constants"; export function isBitNodeFinished(): boolean { const wd = GetServer(SpecialServers.WorldDaemon); diff --git a/src/BitNode/Constants.ts b/src/BitNode/Constants.ts new file mode 100644 index 000000000..1fbe265cd --- /dev/null +++ b/src/BitNode/Constants.ts @@ -0,0 +1 @@ +export const validBitNodes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; diff --git a/src/DevMenu/ui/AchievementsDev.tsx b/src/DevMenu/ui/AchievementsDev.tsx index 057c50cc4..98e5c6901 100644 --- a/src/DevMenu/ui/AchievementsDev.tsx +++ b/src/DevMenu/ui/AchievementsDev.tsx @@ -14,11 +14,12 @@ import LockOpenIcon from "@mui/icons-material/LockOpen"; import { Player } from "@player"; import { achievements } from "../../Achievements/Achievements"; import { Engine } from "../../engine"; +import type { AchievementId } from "../../Achievements/Types"; export function AchievementsDev(): React.ReactElement { const [playerAchievement, setPlayerAchievements] = useState(Player.achievements.map((m) => m.ID)); - function grantAchievement(id: string): void { + function grantAchievement(id: AchievementId): void { Player.giveAchievement(id); setPlayerAchievements(Player.achievements.map((m) => m.ID)); } @@ -28,7 +29,7 @@ export function AchievementsDev(): React.ReactElement { setPlayerAchievements(Player.achievements.map((m) => m.ID)); } - function removeAchievement(id: string): void { + function removeAchievement(id: AchievementId): void { Player.achievements = Player.achievements.filter((a) => a.ID !== id); setPlayerAchievements(Player.achievements.map((m) => m.ID)); } diff --git a/src/Go/Go.ts b/src/Go/Go.ts index 3c887a6ee..eebd25b43 100644 --- a/src/Go/Go.ts +++ b/src/Go/Go.ts @@ -16,6 +16,8 @@ export class GoObject { currentGame: BoardState = getNewBoardState(7); stats: PartialRecord = {}; storedCycles: number = 0; + // This flag is used when checking the achievement CHALLENGE_BN14. + moveOrCheatViaApi = false; prestigeAugmentation() { for (const opponent of getRecordKeys(Go.stats)) { @@ -36,6 +38,7 @@ export class GoObject { this.previousGame = null; this.currentGame = getNewBoardState(7); this.stats = {}; + this.moveOrCheatViaApi = false; resetGoPromises(); } diff --git a/src/Go/SaveLoad.ts b/src/Go/SaveLoad.ts index 1950b6ac2..39c6eef28 100644 --- a/src/Go/SaveLoad.ts +++ b/src/Go/SaveLoad.ts @@ -24,6 +24,7 @@ type SaveFormat = { currentGame: CurrentGameSaveData; stats: PartialRecord; storedCycles: number; + moveOrCheatViaApi: boolean; }; export function getGoSave(): SaveFormat { @@ -46,6 +47,7 @@ export function getGoSave(): SaveFormat { }, stats: Go.stats, storedCycles: Go.storedCycles, + moveOrCheatViaApi: Go.moveOrCheatViaApi, }; } @@ -85,6 +87,9 @@ export function loadGo(data: unknown): boolean { Go.previousGame = previousGame; Go.stats = stats; Go.storeCycles(loadStoredCycles(parsedData.storedCycles)); + if (typeof parsedData.moveOrCheatViaApi === "boolean") { + Go.moveOrCheatViaApi = parsedData.moveOrCheatViaApi; + } resetAI(); handleNextTurn(currentGame).catch((error) => { diff --git a/src/Go/boardAnalysis/scoring.ts b/src/Go/boardAnalysis/scoring.ts index 6f1f73b0e..967f48a85 100644 --- a/src/Go/boardAnalysis/scoring.ts +++ b/src/Go/boardAnalysis/scoring.ts @@ -1,7 +1,7 @@ import type { Board, BoardState, PointState } from "../Types"; import { Player } from "@player"; -import { GoOpponent, GoColor } from "@enums"; +import { GoOpponent, GoColor, FactionName } from "@enums"; import { newOpponentStats } from "../Constants"; import { getAllChains, getPlayerNeighbors } from "./boardAnalysis"; import { getKomi, resetAI } from "./goAI"; @@ -77,6 +77,10 @@ export function endGoGame(boardState: BoardState) { Factions[factionName].setFavor(newFavor); statusToUpdate.rep += repToAdd; } + + if (factionName === FactionName.Illuminati && statusToUpdate.winStreak >= 10) { + Player.giveAchievement("IPVGO_WINNING_STREAK"); + } } statusToUpdate.nodePower += diff --git a/src/Go/effects/netscriptGoImplementation.ts b/src/Go/effects/netscriptGoImplementation.ts index 74712938d..facca2b23 100644 --- a/src/Go/effects/netscriptGoImplementation.ts +++ b/src/Go/effects/netscriptGoImplementation.ts @@ -29,6 +29,7 @@ import { newOpponentStats } from "../Constants"; * Check the move based on the current settings */ export function validateMove(error: (s: string) => never, x: number, y: number, methodName = "", settings = {}): void { + Go.moveOrCheatViaApi = true; const check = { emptyNode: true, requireNonEmptyNode: false, @@ -502,10 +503,11 @@ export function determineCheatSuccess( if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount, playAsWhite)) { callback(); } - // If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly losing + // If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly ending the game else if (priorCheatCount && (ejectRngOverride ?? rng.random()) < 0.1 && state.ai !== GoOpponent.none) { logger(`Cheat failed! You have been ejected from the subnet.`); forceEndGoGame(state); + Player.giveAchievement("IPVGO_ANTICHEAT"); return handleNextTurn(state, true); } else { // If the cheat fails, your turn is skipped diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts index 300165ffb..dded0a2fa 100644 --- a/src/NetscriptFunctions.ts +++ b/src/NetscriptFunctions.ts @@ -108,7 +108,8 @@ import { ServerConstants } from "./Server/data/Constants"; import { assertFunctionWithNSContext } from "./Netscript/TypeAssertion"; import { Router } from "./ui/GameRoot"; import { Page } from "./ui/Router"; -import { canAccessBitNodeFeature, validBitNodes } from "./BitNode/BitNodeUtils"; +import { canAccessBitNodeFeature } from "./BitNode/BitNodeUtils"; +import { validBitNodes } from "./BitNode/Constants"; import { isIPAddress } from "./Types/strings"; import { compile } from "./NetscriptJSEvaluator"; import { Script } from "./Script/Script"; diff --git a/src/NetscriptFunctions/Singularity.ts b/src/NetscriptFunctions/Singularity.ts index 59a3235e9..6800840b5 100644 --- a/src/NetscriptFunctions/Singularity.ts +++ b/src/NetscriptFunctions/Singularity.ts @@ -48,7 +48,7 @@ import { ServerConstants } from "../Server/data/Constants"; import { blackOpsArray } from "../Bladeburner/data/BlackOperations"; import { calculateEffectiveRequiredReputation } from "../Company/utils"; import { addRepToFavor } from "../Faction/formulas/favor"; -import { validBitNodes } from "../BitNode/BitNodeUtils"; +import { validBitNodes } from "../BitNode/Constants"; import { exceptionAlert } from "../utils/helpers/exceptionAlert"; import { cat } from "../Terminal/commands/cat"; import { Crimes } from "../Crime/Crimes"; diff --git a/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts b/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts index c5d89f9c3..a6300a080 100644 --- a/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts +++ b/src/PersonObjects/Player/PlayerObjectGeneralMethods.ts @@ -54,6 +54,7 @@ import { AlertEvents } from "../../ui/React/AlertManager"; import { Augmentations } from "../../Augmentation/Augmentations"; import { PlayerEventType, PlayerEvents } from "./PlayerEvents"; import { Result } from "../../types"; +import type { AchievementId } from "../../Achievements/Types"; export function init(this: PlayerObject): void { /* Initialize Player's home computer */ @@ -561,9 +562,11 @@ export function giveExploit(this: PlayerObject, exploit: Exploit): void { } } -export function giveAchievement(this: PlayerObject, achievementId: string): void { +export function giveAchievement(this: PlayerObject, achievementId: AchievementId): void { const achievement = achievements[achievementId]; - if (!achievement) return; + if (!achievement) { + return; + } if (!this.achievements.map((a) => a.ID).includes(achievementId)) { this.achievements.push({ ID: achievementId, unlockedOn: new Date().getTime() }); SnackbarEvents.emit(`Unlocked Achievement: "${achievement.Name}"`, ToastVariant.SUCCESS, 2000); diff --git a/test/jest/__snapshots__/FullSave.test.ts.snap b/test/jest/__snapshots__/FullSave.test.ts.snap index 2215537ef..4c7c056a3 100644 --- a/test/jest/__snapshots__/FullSave.test.ts.snap +++ b/test/jest/__snapshots__/FullSave.test.ts.snap @@ -45,6 +45,7 @@ exports[`Check Save File Continuity GoSave continuity 1`] = ` "previousBoard": "", "previousPlayer": "White", }, + "moveOrCheatViaApi": false, "previousGame": null, "stats": {}, "storedCycles": 0,