From 1934d6a72b9258172704a6323b698557391b1786 Mon Sep 17 00:00:00 2001 From: horrible little slime <69secret69email69@gmail.com> Date: Sun, 22 Dec 2024 22:00:39 -0500 Subject: [PATCH] export autumnaton manager for garbo --- packages/garbo-lib/package.json | 2 +- packages/garbo-lib/src/index.ts | 1 + .../garbo-lib/src/resources/autumnaton.ts | 318 ++++++++++++++++++ packages/garbo-lib/src/resources/index.ts | 1 + packages/garbo-lib/src/resources/lib.ts | 3 + packages/garbo/src/garboValue.ts | 11 +- 6 files changed, 328 insertions(+), 8 deletions(-) create mode 100644 packages/garbo-lib/src/resources/autumnaton.ts create mode 100644 packages/garbo-lib/src/resources/index.ts create mode 100644 packages/garbo-lib/src/resources/lib.ts diff --git a/packages/garbo-lib/package.json b/packages/garbo-lib/package.json index 15a938023..ce8c6bfbf 100644 --- a/packages/garbo-lib/package.json +++ b/packages/garbo-lib/package.json @@ -1,6 +1,6 @@ { "name": "garbo-lib", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "repository": "https://github.com/loathers/garbage-collector.git", "description": "A library for sequencing turns resource-optimally", diff --git a/packages/garbo-lib/src/index.ts b/packages/garbo-lib/src/index.ts index b2382ec10..6ec3bd62f 100644 --- a/packages/garbo-lib/src/index.ts +++ b/packages/garbo-lib/src/index.ts @@ -5,3 +5,4 @@ import type { DraggableFight, WanderDetails, WanderOptions } from "./wanderer"; export { makeValue, WandererManager }; export type { ValueFunctions, WanderOptions, DraggableFight, WanderDetails }; +export * from "./resources"; diff --git a/packages/garbo-lib/src/resources/autumnaton.ts b/packages/garbo-lib/src/resources/autumnaton.ts new file mode 100644 index 000000000..5c2c595ae --- /dev/null +++ b/packages/garbo-lib/src/resources/autumnaton.ts @@ -0,0 +1,318 @@ +import { + appearanceRates, + availableAmount, + getMonsters, + Item, + itemDropsArray, + Location, + myAdventures, + setLocation, +} from "kolmafia"; +import { DEFAULT_VALUE_FUNCTIONS } from "./lib"; +import { + $items, + $location, + $locations, + AutumnAton, + get, + maxBy, + sum, +} from "libram"; + +export type AutumnAtonOptions = { + averageItemValue: (...items: Item[]) => number; + estimatedTurns: () => number; + estimatedTurnsTomorrow: () => number; +}; + +export class AutumnAtonManager { + averageItemValue: (...items: Item[]) => number = + DEFAULT_VALUE_FUNCTIONS.averageValue; + estimatedTurns: () => number = myAdventures; + estimatedTurnsTomorrow: () => number = () => 0; + + static locationBanlist = $locations`The Daily Dungeon`; // The Daily Dungeon has no native monsters + static badAttributes = ["LUCKY", "ULTRARARE", "BOSS"]; + + static profitRelevantUpgrades = [ + "leftarm1", + "leftleg1", + "rightarm1", + "rightleg1", + "cowcatcher", + "periscope", + "radardish", + ] as const; + + constructor({ + averageItemValue, + estimatedTurns, + estimatedTurnsTomorrow, + }: Partial) { + if (averageItemValue) this.averageItemValue = averageItemValue; + if (estimatedTurns) this.estimatedTurns = estimatedTurns; + if (estimatedTurnsTomorrow) + this.estimatedTurnsTomorrow = estimatedTurnsTomorrow; + } + + bestLocation(locations: Location[]): Location { + return maxBy(this.bestLocationsByUpgrade(locations), this.averageValue); + } + + 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 = this.averageItemValue(...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 + : this.averageItemValue(autumnItem)) + ); + } else { + // If we're in a location without any uniques, we still get cowcatcher items + return seasonalItemDrops > 1 ? avgValueOfRandomAutumnItem : 0; + } + } + + averageValue( + 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) => + !AutumnAtonManager.badAttributes.some((s) => + m.attributes.includes(s), + ) && rates[m.name] > 0, + ); + + if (monsters.length === 0) { + return this.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) + ? this.averageItemValue(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 this.seasonalItemValue(location) + expectedCollectionValue; + } + } + + expectedRemainingExpeditions(legs = AutumnAton.legs()): number { + // Better estimate upgrade value if not ascending + const availableAutumnatonTurns = + this.estimatedTurns() - + AutumnAton.turnsLeft() + + this.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, + ), + ); + } + + profitFromExtraAcuity( + bestLocationContainingUpgrade: Location, + bestLocationWithInstalledUpgrade: Location, + ): number { + return ( + this.averageValue(bestLocationContainingUpgrade) + + this.averageValue(bestLocationWithInstalledUpgrade) * + Math.max(0, this.expectedRemainingExpeditions() - 1) + ); + } + + profitFromExtraLeg( + bestLocationContainingUpgrade: Location, + bestLocationWithInstalledUpgrade: Location, + ): number { + return ( + this.averageValue(bestLocationContainingUpgrade) + + this.averageValue(bestLocationWithInstalledUpgrade) * + Math.max( + 0, + this.expectedRemainingExpeditions(AutumnAton.legs() + 1) - 1, + ) + ); + } + + profitFromExtraArm( + bestLocationContainingUpgrade: Location, + bestLocationWithInstalledUpgrade: Location, + ): number { + return ( + this.averageValue(bestLocationContainingUpgrade) + + this.averageValue(bestLocationWithInstalledUpgrade) * + Math.max(0, this.expectedRemainingExpeditions() - 1) + ); + } + + profitFromExtraAutumnItem( + bestLocationContainingUpgrade: Location, + bestLocationWithInstalledUpgrade: Location, + ): number { + return ( + this.averageValue(bestLocationContainingUpgrade) + + (this.seasonalItemValue(bestLocationWithInstalledUpgrade) + + this.averageValue(bestLocationWithInstalledUpgrade)) * + Math.max(0, this.expectedRemainingExpeditions() - 1) + ); + } + + makeUpgradeValuator( + fullLocations: Location[], + currentBestLocation: Location, + ) { + return (upgrade: AutumnAton.Upgrade) => { + const upgradeLocations = fullLocations.filter( + (location) => AutumnAton.getUniques(location)?.upgrade === upgrade, + ); + + if (!upgradeLocations.length) { + return { upgrade, profit: 0 }; + } + + const bestLocationContainingUpgrade = maxBy(upgradeLocations, (l) => + this.averageValue(l), + ); + + switch (upgrade) { + case "periscope": + case "radardish": { + const bestLocationWithInstalledUpgrade = maxBy( + fullLocations, + (loc: Location) => + this.averageValue(loc, AutumnAton.visualAcuity() + 1), + ); + return { + upgrade, + profit: this.profitFromExtraAcuity( + bestLocationContainingUpgrade, + bestLocationWithInstalledUpgrade, + ), + }; + } + case "rightleg1": + case "leftleg1": { + return { + upgrade, + profit: this.profitFromExtraLeg( + bestLocationContainingUpgrade, + currentBestLocation, + ), + }; + } + case "rightarm1": + case "leftarm1": { + const bestLocationWithInstalledUpgrade = maxBy( + fullLocations, + (loc: Location) => + this.averageValue(loc, undefined, AutumnAton.zoneItems() + 1), + ); + return { + upgrade, + profit: this.profitFromExtraArm( + bestLocationContainingUpgrade, + bestLocationWithInstalledUpgrade, + ), + }; + } + case "cowcatcher": { + return { + upgrade, + profit: this.profitFromExtraAutumnItem( + bestLocationContainingUpgrade, + currentBestLocation, + ), + }; + } + default: { + return { upgrade, profit: 0 }; + } + } + }; + } + bestLocationsByUpgrade(fullLocations: Location[]): Location[] { + const validLocations = fullLocations.filter( + (l) => + l.parent !== "Clan Basement" && + !AutumnAtonManager.locationBanlist.includes(l), + ); + // This function shouldn't be getting called if we don't have an expedition left + if (this.expectedRemainingExpeditions() < 1) { + return validLocations; + } + const currentUpgrades = AutumnAton.currentUpgrades(); + const acquirableUpgrades = AutumnAtonManager.profitRelevantUpgrades.filter( + (upgrade) => !currentUpgrades.includes(upgrade), + ); + + if (acquirableUpgrades.length === 0) { + return validLocations; + } + + const currentBestLocation = maxBy(validLocations, (l) => + this.averageValue(l), + ); + const currentExpectedProfit = + this.averageValue(currentBestLocation) * + this.expectedRemainingExpeditions(); + + const upgradeValuations = acquirableUpgrades.map( + this.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-lib/src/resources/index.ts b/packages/garbo-lib/src/resources/index.ts new file mode 100644 index 000000000..15ba50429 --- /dev/null +++ b/packages/garbo-lib/src/resources/index.ts @@ -0,0 +1 @@ +export { AutumnAtonManager } from "./autumnaton"; diff --git a/packages/garbo-lib/src/resources/lib.ts b/packages/garbo-lib/src/resources/lib.ts new file mode 100644 index 000000000..87520f461 --- /dev/null +++ b/packages/garbo-lib/src/resources/lib.ts @@ -0,0 +1,3 @@ +import { makeValue } from "../value"; + +export const DEFAULT_VALUE_FUNCTIONS = makeValue(); diff --git a/packages/garbo/src/garboValue.ts b/packages/garbo/src/garboValue.ts index d163c5280..7ebc8121c 100644 --- a/packages/garbo/src/garboValue.ts +++ b/packages/garbo/src/garboValue.ts @@ -3,14 +3,11 @@ import { makeValue, ValueFunctions } from "garbo-lib"; import { $item } from "libram"; -let _valueFunctions: ValueFunctions | undefined = undefined; +let _valueFunctions: ValueFunctions | undefined; function garboValueFunctions(): ValueFunctions { - if (!_valueFunctions) { - _valueFunctions = makeValue({ - itemValues: new Map([[$item`fake hand`, 50000]]), - }); - } - return _valueFunctions; + return (_valueFunctions ??= makeValue({ + itemValues: new Map([[$item`fake hand`, 50_000]]), + })); } export function garboValue(item: Item): number {