Skip to content

Commit

Permalink
export autumnaton manager for garbo
Browse files Browse the repository at this point in the history
  • Loading branch information
horrible-little-slime committed Dec 23, 2024
1 parent 86373ba commit 1934d6a
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 8 deletions.
2 changes: 1 addition & 1 deletion packages/garbo-lib/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/garbo-lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ import type { DraggableFight, WanderDetails, WanderOptions } from "./wanderer";

export { makeValue, WandererManager };
export type { ValueFunctions, WanderOptions, DraggableFight, WanderDetails };
export * from "./resources";
318 changes: 318 additions & 0 deletions packages/garbo-lib/src/resources/autumnaton.ts
Original file line number Diff line number Diff line change
@@ -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<AutumnAtonOptions>) {
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;
}
}
}
1 change: 1 addition & 0 deletions packages/garbo-lib/src/resources/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AutumnAtonManager } from "./autumnaton";
3 changes: 3 additions & 0 deletions packages/garbo-lib/src/resources/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { makeValue } from "../value";

export const DEFAULT_VALUE_FUNCTIONS = makeValue();
11 changes: 4 additions & 7 deletions packages/garbo/src/garboValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 1934d6a

Please sign in to comment.