diff --git a/packages/garbo-lib/src/resources/autumnaton.ts b/packages/garbo-lib/src/resources/autumnaton.ts index 5c2c595ae..ceeffaaa1 100644 --- a/packages/garbo-lib/src/resources/autumnaton.ts +++ b/packages/garbo-lib/src/resources/autumnaton.ts @@ -51,8 +51,9 @@ export class AutumnAtonManager { }: Partial) { if (averageItemValue) this.averageItemValue = averageItemValue; if (estimatedTurns) this.estimatedTurns = estimatedTurns; - if (estimatedTurnsTomorrow) + if (estimatedTurnsTomorrow) { this.estimatedTurnsTomorrow = estimatedTurnsTomorrow; + } } bestLocation(locations: Location[]): Location { @@ -87,8 +88,9 @@ export class AutumnAtonManager { acuityOverride?: number, slotOverride?: number, ): number { - if (location === $location`Shadow Rift`) - setLocation($location`Shadow Rift`); // FIXME This bypasses a mafia bug where ingress is not updated + if (location === $location`Shadow Rift`) { + setLocation($location`Shadow Rift`); + } // FIXME This bypasses a mafia bug where ingress is not updated const rates = appearanceRates(location); const monsters = getMonsters(location).filter( (m) => diff --git a/packages/garbo/src/resources/autumnaton.ts b/packages/garbo/src/resources/autumnaton.ts deleted file mode 100644 index bbdf2da93..000000000 --- a/packages/garbo/src/resources/autumnaton.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { garboAverageValue, garboValue } from "../garboValue"; -import { estimatedGarboTurns, estimatedTurnsTomorrow } from "../turns"; -import { - appearanceRates, - availableAmount, - getMonsters, - itemDropsArray, - Location, - setLocation, -} from "kolmafia"; -import { - $items, - $location, - $locations, - AutumnAton, - get, - maxBy, - sum, -} from "libram"; -import { globalOptions } from "../config"; - -const locationBanlist = $locations`The Daily Dungeon`; // The Daily Dungeon has no native monsters -const badAttributes = ["LUCKY", "ULTRARARE", "BOSS"]; - -export function bestAutumnatonLocation(locations: Location[]): Location { - return maxBy(bestLocationsByUpgrade(locations), averageAutumnatonValue); -} - -function averageAutumnatonValue( - location: Location, - acuityOverride?: number, - slotOverride?: number, -): number { - if (location === $location`Shadow Rift`) setLocation($location`Shadow Rift`); // FIXME This bypasses a mafia bug where ingress is not updated - const rates = appearanceRates(location); - const monsters = getMonsters(location).filter( - (m) => - !badAttributes.some((s) => m.attributes.includes(s)) && rates[m.name] > 0, - ); - - if (monsters.length === 0) { - return seasonalItemValue(location); // We still get seasonal items, even if there are no monsters - } else { - const maximumDrops = slotOverride ?? AutumnAton.zoneItems(); - const acuityCutoff = 20 - (acuityOverride ?? AutumnAton.visualAcuity()) * 5; - const validDrops = monsters - .flatMap((m) => itemDropsArray(m)) - .map(({ rate, type, drop }) => ({ - value: !["c", "0", "a"].includes(type) ? garboValue(drop) : 0, - preAcuityExpectation: ["c", "0", ""].includes(type) - ? (2 * rate) / 100 - : 0, - postAcuityExpectation: - rate >= acuityCutoff && ["c", "0", ""].includes(type) - ? (8 * rate) / 100 - : 0, - })); - const overallExpectedDropQuantity = sum( - validDrops, - ({ preAcuityExpectation, postAcuityExpectation }) => - preAcuityExpectation + postAcuityExpectation, - ); - const expectedCollectionValue = sum( - validDrops, - ({ value, preAcuityExpectation, postAcuityExpectation }) => { - // This gives us the adjusted amount to fit within our total amount of available drop slots - const adjustedDropAmount = - (preAcuityExpectation + postAcuityExpectation) * - Math.min(1, maximumDrops / overallExpectedDropQuantity); - return adjustedDropAmount * value; - }, - ); - return seasonalItemValue(location) + expectedCollectionValue; - } -} - -function seasonalItemValue( - location: Location, - seasonalOverride?: number, -): number { - // Find the value of the drops based on zone difficulty/type - const autumnItems = $items`autumn leaf, AutumnFest ale, autumn breeze, autumn dollar, autumn years wisdom`; - const avgValueOfRandomAutumnItem = garboAverageValue(...autumnItems); - const autumnMeltables = $items`autumn debris shield, autumn leaf pendant, autumn sweater-weather sweater`; - const autumnItem = AutumnAton.getUniques(location)?.item; - const seasonalItemDrops = seasonalOverride ?? AutumnAton.seasonalItems(); - if (autumnItem) { - return ( - (seasonalItemDrops > 1 ? avgValueOfRandomAutumnItem : 0) + - (autumnMeltables.includes(autumnItem) - ? // If we already have the meltable, then we get a random item, else value at 0 - availableAmount(autumnItem) > 0 - ? avgValueOfRandomAutumnItem - : 0 - : garboValue(autumnItem)) - ); - } else { - // If we're in a location without any uniques, we still get cowcatcher items - return seasonalItemDrops > 1 ? avgValueOfRandomAutumnItem : 0; - } -} - -function expectedRemainingExpeditions(legs = AutumnAton.legs()): number { - // Better estimate upgrade value if not ascending - const availableAutumnatonTurns = - estimatedGarboTurns() - - AutumnAton.turnsLeft() + - (globalOptions.ascend ? 0 : estimatedTurnsTomorrow); - const quests = get("_autumnatonQuests"); - const legOffsetFactor = 11 * Math.max(quests - legs - 1, 0); - return Math.floor( - Math.sqrt( - quests ** 2 + (2 * (availableAutumnatonTurns - legOffsetFactor)) / 11, - ), - ); -} - -const profitRelevantUpgrades = [ - "leftarm1", - "leftleg1", - "rightarm1", - "rightleg1", - "cowcatcher", - "periscope", - "radardish", -] as const; - -function profitFromExtraAcuity( - bestLocationContainingUpgrade: Location, - bestLocationWithInstalledUpgrade: Location, -): number { - return ( - averageAutumnatonValue(bestLocationContainingUpgrade) + - averageAutumnatonValue(bestLocationWithInstalledUpgrade) * - Math.max(0, expectedRemainingExpeditions() - 1) - ); -} -function profitFromExtraLeg( - bestLocationContainingUpgrade: Location, - bestLocationWithInstalledUpgrade: Location, -): number { - return ( - averageAutumnatonValue(bestLocationContainingUpgrade) + - averageAutumnatonValue(bestLocationWithInstalledUpgrade) * - Math.max(0, expectedRemainingExpeditions(AutumnAton.legs() + 1) - 1) - ); -} -function profitFromExtraArm( - bestLocationContainingUpgrade: Location, - bestLocationWithInstalledUpgrade: Location, -): number { - return ( - averageAutumnatonValue(bestLocationContainingUpgrade) + - averageAutumnatonValue(bestLocationWithInstalledUpgrade) * - Math.max(0, expectedRemainingExpeditions() - 1) - ); -} -function profitFromExtraAutumnItem( - bestLocationContainingUpgrade: Location, - bestLocationWithInstalledUpgrade: Location, -): number { - return ( - averageAutumnatonValue(bestLocationContainingUpgrade) + - (seasonalItemValue(bestLocationWithInstalledUpgrade) + - averageAutumnatonValue(bestLocationWithInstalledUpgrade)) * - Math.max(0, expectedRemainingExpeditions() - 1) - ); -} - -function makeUpgradeValuator( - fullLocations: Location[], - currentBestLocation: Location, -) { - return function (upgrade: AutumnAton.Upgrade) { - const upgradeLocations = fullLocations.filter( - (location) => AutumnAton.getUniques(location)?.upgrade === upgrade, - ); - - if (!upgradeLocations.length) { - return { upgrade, profit: 0 }; - } - - const bestLocationContainingUpgrade = maxBy( - upgradeLocations, - averageAutumnatonValue, - ); - - switch (upgrade) { - case "periscope": - case "radardish": { - const bestLocationWithInstalledUpgrade = maxBy( - fullLocations, - (loc: Location) => - averageAutumnatonValue(loc, AutumnAton.visualAcuity() + 1), - ); - return { - upgrade, - profit: profitFromExtraAcuity( - bestLocationContainingUpgrade, - bestLocationWithInstalledUpgrade, - ), - }; - } - case "rightleg1": - case "leftleg1": { - return { - upgrade, - profit: profitFromExtraLeg( - bestLocationContainingUpgrade, - currentBestLocation, - ), - }; - } - case "rightarm1": - case "leftarm1": { - const bestLocationWithInstalledUpgrade = maxBy( - fullLocations, - (loc: Location) => - averageAutumnatonValue(loc, undefined, AutumnAton.zoneItems() + 1), - ); - return { - upgrade, - profit: profitFromExtraArm( - bestLocationContainingUpgrade, - bestLocationWithInstalledUpgrade, - ), - }; - } - case "cowcatcher": { - return { - upgrade, - profit: profitFromExtraAutumnItem( - bestLocationContainingUpgrade, - currentBestLocation, - ), - }; - } - default: { - return { upgrade, profit: 0 }; - } - } - }; -} - -function bestLocationsByUpgrade(fullLocations: Location[]): Location[] { - const validLocations = fullLocations.filter( - (l) => l.parent !== "Clan Basement" && !locationBanlist.includes(l), - ); - // This function shouldn't be getting called if we don't have an expedition left - if (expectedRemainingExpeditions() < 1) { - return validLocations; - } - const currentUpgrades = AutumnAton.currentUpgrades(); - const acquirableUpgrades = profitRelevantUpgrades.filter( - (upgrade) => !currentUpgrades.includes(upgrade), - ); - - if (acquirableUpgrades.length === 0) { - return validLocations; - } - - const currentBestLocation = maxBy(validLocations, averageAutumnatonValue); - const currentExpectedProfit = - averageAutumnatonValue(currentBestLocation) * - expectedRemainingExpeditions(); - - const upgradeValuations = acquirableUpgrades.map( - makeUpgradeValuator(validLocations, currentBestLocation), - ); - - const { upgrade: highestValueUpgrade, profit: profitFromBestUpgrade } = maxBy( - upgradeValuations, - "profit", - ); - - if (profitFromBestUpgrade > currentExpectedProfit) { - const upgradeLocations = validLocations.filter( - (location) => - AutumnAton.getUniques(location)?.upgrade === highestValueUpgrade, - ); - return upgradeLocations; - } else { - return validLocations; - } -} diff --git a/packages/garbo/src/resources/index.ts b/packages/garbo/src/resources/index.ts index 45f5b1537..4cda2b783 100644 --- a/packages/garbo/src/resources/index.ts +++ b/packages/garbo/src/resources/index.ts @@ -2,7 +2,6 @@ export * from "./scepter"; export * from "./candyMap"; export * from "./gingerbread"; export * from "./worksheds"; -export * from "./autumnaton"; export * from "./extrovermectin"; export * from "./realm"; export * from "./synthesis"; diff --git a/packages/garbo/src/tasks/post/index.ts b/packages/garbo/src/tasks/post/index.ts index fdbdd5267..5a575aba5 100644 --- a/packages/garbo/src/tasks/post/index.ts +++ b/packages/garbo/src/tasks/post/index.ts @@ -47,8 +47,11 @@ import { } from "../../lib"; import { teleportEffects } from "../../mood"; import { Quest } from "grimoire-kolmafia"; -import { bestAutumnatonLocation } from "../../resources"; -import { estimatedGarboTurns, remainingUserTurns } from "../../turns"; +import { + estimatedGarboTurns, + estimatedTurnsTomorrow, + remainingUserTurns, +} from "../../turns"; import { acquire } from "../../acquire"; import { garboAverageValue } from "../../garboValue"; import workshedTasks from "./worksheds"; @@ -56,10 +59,20 @@ import { GarboPostTask } from "./lib"; import { GarboTask } from "../engine"; import { hotTubAvailable } from "../../resources/clanVIP"; import { lavaDogsAccessible, lavaDogsComplete } from "../../resources/doghouse"; +import { AutumnAtonManager } from "garbo-lib"; const STUFF_TO_CLOSET = $items`bowling ball, funky junk key`; const STUFF_TO_USE = $items`Armory keycard, bottle-opener keycard, SHAWARMA Initiative Keycard`; +let _autumnAtonManager: AutumnAtonManager; +const autumnAtonManager = () => + (_autumnAtonManager ??= new AutumnAtonManager({ + averageItemValue: garboAverageValue, + estimatedTurns: estimatedGarboTurns, + estimatedTurnsTomorrow: () => + globalOptions.ascend ? 0 : estimatedTurnsTomorrow, + })); + function closetStuff(): GarboPostTask { return { name: "Closet Stuff", @@ -214,7 +227,7 @@ function fallbot(): GarboPostTask { globalOptions.ascend || AutumnAton.turnsForQuest() < estimatedGarboTurns() + remainingUserTurns(), do: () => { - AutumnAton.sendTo(bestAutumnatonLocation); + AutumnAton.sendTo(autumnAtonManager().bestLocation); }, available: () => AutumnAton.have(), post: () => {