diff --git a/packages/garbo/src/familiar/barfFamiliar.ts b/packages/garbo/src/familiar/barfFamiliar.ts index 8450c499a..f20663476 100644 --- a/packages/garbo/src/familiar/barfFamiliar.ts +++ b/packages/garbo/src/familiar/barfFamiliar.ts @@ -35,7 +35,6 @@ import { getExperienceFamiliarLimit } from "./experienceFamiliars"; import { getAllJellyfishDrops, menu } from "./freeFightFamiliar"; import { GeneralFamiliar, timeToMeatify, turnsAvailable } from "./lib"; import { meatFamiliar } from "./meatFamiliar"; -import { setMarginalFamiliarsExcessValue } from "../session"; import { garboValue } from "../garboValue"; const ITEM_DROP_VALUE = 0.72; @@ -68,7 +67,7 @@ function getCachedOutfitValues(fam: Familiar) { avoid: $items`Kramco Sausage-o-Matic™, cursed magnifying glass, protonic accelerator pack, "I Voted!" sticker, li'l pirate costume, bag of many confections`, }, true, - ).dress(); + ).outfit.dress(); const outfit = outfitSlots.map((slot) => equippedItem(slot)); const bonuses = bonusGear(BonusEquipMode.EMBEZZLER, false); @@ -132,34 +131,35 @@ function totalFamiliarValue({ return expectedValue + outfitValue + familiarAbilityValue(familiar); } -function turnsNeededForFamiliar( - { familiar, limit, outfitValue }: MarginalFamiliar, +function turnsNeededFromBaseline( baselineToCompareAgainst: MarginalFamiliar, -): number { - switch (limit) { - case "drops": - return sum( - getAllDrops(familiar).filter( - ({ expectedValue }) => - outfitValue + familiarAbilityValue(familiar) + expectedValue > - totalFamiliarValue(baselineToCompareAgainst), - ), - ({ expectedTurns }) => expectedTurns, - ); - - case "experience": - return getExperienceFamiliarLimit(familiar); - - case "none": - return 0; - - case "special": - return getSpecialFamiliarLimit({ - familiar, - outfitValue, - baselineToCompareAgainst, - }); - } +): (option: MarginalFamiliar) => number { + return ({ familiar, limit, outfitValue }: MarginalFamiliar) => { + switch (limit) { + case "drops": + return sum( + getAllDrops(familiar).filter( + ({ expectedValue }) => + outfitValue + familiarAbilityValue(familiar) + expectedValue > + totalFamiliarValue(baselineToCompareAgainst), + ), + ({ expectedTurns }) => expectedTurns, + ); + + case "experience": + return getExperienceFamiliarLimit(familiar); + + case "none": + return 0; + + case "special": + return getSpecialFamiliarLimit({ + familiar, + outfitValue, + baselineToCompareAgainst, + }); + } + }; } function calculateOutfitValue(f: GeneralFamiliar): MarginalFamiliar { @@ -172,9 +172,33 @@ function calculateOutfitValue(f: GeneralFamiliar): MarginalFamiliar { return { ...f, outfitValue, outfitWeight }; } -export function barfFamiliar(): Familiar { - if (timeToMeatify()) return $familiar`Grey Goose`; - if (get("garbo_IgnoreMarginalFamiliars", false)) return meatFamiliar(); + +function extraValue( + target: MarginalFamiliar, + meat: MarginalFamiliar, + jellyfish: MarginalFamiliar | undefined, +) { + const targetValue = totalFamiliarValue(target); + const meatFamiliarValue = totalFamiliarValue(meat); + + const jellyfishValue = jellyfish + ? garboValue($item`stench jelly`) / 20 + + familiarAbilityValue(jellyfish.familiar) + + jellyfish.outfitValue + : 0; + return Math.max(targetValue - Math.max(meatFamiliarValue, jellyfishValue), 0); +} + +export function barfFamiliar(): { familiar: Familiar; extraValue: number } { + if (timeToMeatify()) { + return { familiar: $familiar`Grey Goose`, extraValue: 0 }; + } + + if (get("garbo_IgnoreMarginalFamiliars", false)) { + return { familiar: meatFamiliar(), extraValue: 0 }; + } + + const meat = meatFamiliar(); const fullMenu = menu({ canChooseMacro: true, @@ -182,9 +206,7 @@ export function barfFamiliar(): Familiar { includeExperienceFamiliars: false, }).map(calculateOutfitValue); - const meatFamiliarEntry = fullMenu.find( - ({ familiar }) => familiar === meatFamiliar(), - ); + const meatFamiliarEntry = fullMenu.find(({ familiar }) => familiar === meat); if (!meatFamiliarEntry) { throw new Error("Something went wrong when initializing familiars!"); @@ -196,50 +218,38 @@ export function barfFamiliar(): Familiar { ); if (viableMenu.every(({ limit }) => limit !== "none")) { - const turnsNeeded = sum(viableMenu, (option: MarginalFamiliar) => - turnsNeededForFamiliar(option, meatFamiliarEntry), + const turnsNeeded = sum( + viableMenu, + turnsNeededFromBaseline(meatFamiliarEntry), ); if (turnsNeeded < turnsAvailable()) { const shrubAvailable = viableMenu.some( ({ familiar }) => familiar === $familiar`Crimbo Shrub`, ); - return shrubAvailable ? $familiar`Crimbo Shrub` : meatFamiliar(); + return { + familiar: shrubAvailable ? $familiar`Crimbo Shrub` : meat, + extraValue: 0, + }; } } if (viableMenu.length === 0) { - setMarginalFamiliarsExcessValue(0); - return meatFamiliar(); + return { familiar: meat, extraValue: 0 }; } const best = maxBy(viableMenu, totalFamiliarValue); - // Because we run marginal familiars at the end, our marginal MPA is inflated by best.expectedValue - meatFamiliarValue every turn - // Technically it's the nominalFamiliarValue, which for now is the max of meatFamiliar and jellyfish (if we have the jellyfish) - const jellyfish = fullMenu.find( - ({ familiar }) => familiar === $familiar`Space Jellyfish`, - ); - const jellyfishValue = jellyfish - ? garboValue($item`stench jelly`) / 20 + - familiarAbilityValue(jellyfish.familiar) + - jellyfish.outfitValue - : 0; - const excessValue = Math.max( - best.expectedValue + - best.outfitValue + - familiarAbilityValue(best.familiar) - - Math.max(meatFamiliarValue, jellyfishValue), - 0, - ); - setMarginalFamiliarsExcessValue(excessValue); - - const familiarPrintout = (x: MarginalFamiliar) => - `(expected value of ${x.expectedValue.toFixed( + const familiarPrintout = ({ + expectedValue, + familiar, + outfitValue, + }: MarginalFamiliar) => + `(expected value of ${expectedValue.toFixed( 1, - )} from familiar drops, ${familiarAbilityValue(x.familiar).toFixed( + )} from familiar drops, ${familiarAbilityValue(familiar).toFixed( 1, - )} from familiar abilities and ${x.outfitValue.toFixed(1)} from outfit)`; + )} from familiar abilities and ${outfitValue.toFixed(1)} from outfit)`; print( `Choosing to use ${best.familiar} ${familiarPrintout(best)} over ${ @@ -248,7 +258,14 @@ export function barfFamiliar(): Familiar { HIGHLIGHT, ); - return best.familiar; + const jellyfish = fullMenu.find( + ({ familiar }) => familiar === $familiar`Space Jellyfish`, + ); + + return { + familiar: best.familiar, + extraValue: extraValue(best, meatFamiliarEntry, jellyfish), + }; } function getSpecialFamiliarLimit({ diff --git a/packages/garbo/src/outfit/barf.ts b/packages/garbo/src/outfit/barf.ts index b22fc8afe..3c66540b3 100644 --- a/packages/garbo/src/outfit/barf.ts +++ b/packages/garbo/src/outfit/barf.ts @@ -93,7 +93,10 @@ const POINTER_RING_SPECS: ( const trueInebrietyLimit = () => inebrietyLimit() - (myFamiliar() === $familiar`Stooper` ? 1 : 0); -export function barfOutfit(spec: OutfitSpec = {}, sim = false): Outfit { +export function barfOutfit( + spec: OutfitSpec = {}, + sim = false, +): { outfit: Outfit; extraValue: number } { cleaverCheck(); validateGarbageFoldable(spec); const outfit = Outfit.from( @@ -101,7 +104,8 @@ export function barfOutfit(spec: OutfitSpec = {}, sim = false): Outfit { new Error(`Failed to construct outfit from spec ${toJson(spec)}!`), ); - outfit.familiar ??= barfFamiliar(); + const { familiar, extraValue } = barfFamiliar(); + outfit.familiar ??= familiar; if (outfit.familiar === $familiar`Jill-of-All-Trades`) { outfit.equip($item`LED candle`); @@ -165,5 +169,5 @@ export function barfOutfit(spec: OutfitSpec = {}, sim = false): Outfit { parka: "kachungasaur", }); - return outfit; + return { outfit, extraValue }; } diff --git a/packages/garbo/src/session.ts b/packages/garbo/src/session.ts index 5582d6cbc..a897a2c28 100644 --- a/packages/garbo/src/session.ts +++ b/packages/garbo/src/session.ts @@ -1,50 +1,18 @@ -import { - inebrietyLimit, - Item, - myAdventures, - myInebriety, - print, - setProperty, - totalTurnsPlayed, -} from "kolmafia"; +import { Item, print } from "kolmafia"; import { $items, get, Session, set } from "libram"; import { globalOptions } from "./config"; import { formatNumber, HIGHLIGHT, resetDailyPreference } from "./lib"; import { failedWishes } from "./potions"; import { garboValue } from "./garboValue"; +import { estimatedGarboTurns } from "./turns"; -function printSession(session: Session): void { - const value = session.value(garboValue); - const printProfit = ( - details: { item: Item; value: number; quantity: number }[], - ) => { - for (const { item, quantity, value } of details) { - print(` ${item} (${quantity}) @ ${Math.floor(value)}`); - } - }; - const lowValue = value.itemDetails - .filter((detail) => detail.value < 0) - .sort((a, b) => a.value - b.value); - const highValue = value.itemDetails - .filter((detail) => detail.value > 0) - .sort((a, b) => b.value - a.value); - - print(`Total Session Value: ${value.total}`); - print( - `Of that, ${value.meat} came from meat and ${value.items} came from items`, - ); - print(` You gained meat on ${highValue.length} items including:`); - printProfit(highValue); - print(` You lost meat on ${lowValue.length} items including:`); - printProfit(lowValue); -} - -let session: Session | null = null; +type SessionKey = "full" | "barf" | "meat-start" | "meat-end" | "item"; +const sessions: Map = new Map(); /** * Start a new session, deleting any old session */ export function startSession(): void { - session = Session.current(); + sessions.set("full", Session.current()); } /** @@ -52,189 +20,121 @@ export function startSession(): void { * @returns The difference */ export function sessionSinceStart(): Session { + const session = sessions.get("full"); if (session) { return Session.current().diff(session); } return Session.current(); } -export function valueSession(): void { - printSession(Session.current()); - Session.current().toFile("test.json"); +let extraValue = 0; +export function trackMarginalTurnExtraValue(additionalValue: number) { + extraValue += additionalValue; } -let marginalSession: Session | null = null; -let marginalSessionDiff: Session | null = null; -let barfSession: Session | null = null; -let barfSessionStartTurns = totalTurnsPlayed(); -let continueMeatTracking = true; -let numTrackedItemTurns: number | null = null; -let marginalFamiliarsExcessValue = 0; -export function setMarginalFamiliarsExcessValue(val: number): void { - marginalFamiliarsExcessValue = Math.max(0, val); -} -let marginalFamiliarsExcessTotal = 0; - -// Hardcode a few outliers that we know aren't marginal -// (e.g. those that have a drop limit which we would likely already cap) -// Note that familiar drops (that have limits) are already handled above -const outlierItemList = $items`Extrovermectin™, Volcoino, Poké-Gro fertilizer`; - -export function trackBarfSessionStatistics(): void { - // If we are overdrunk, don't track statistics - if (myInebriety() > inebrietyLimit()) return; - - // Start barfSession if we have not done so - if (!barfSession) { - barfSession = Session.current(); - barfSessionStartTurns = totalTurnsPlayed(); - } - - // Start tracking items if at least one of these is true - // 1) We have run at least 100 barf turns - // 2) We have less than 200 adv to run - if ( - !marginalSession && - (totalTurnsPlayed() - barfSessionStartTurns >= 100 || - (myAdventures() <= 200 + 25 + globalOptions.saveTurns && - myAdventures() > globalOptions.saveTurns + 25)) - ) { - marginalSession = Session.current(); - numTrackedItemTurns = myAdventures() - globalOptions.saveTurns - 25; - } - - // Start tracking meat if we have less than 75 turns left - // Also create a backup tracker for items - - if (marginalSession) { - marginalFamiliarsExcessTotal += marginalFamiliarsExcessValue; - } - marginalFamiliarsExcessValue = 0; - - if ( - (!get("_garboMarginalMeatCheckpoint") || !get("_garboMarginalMeatTurns")) && - myAdventures() - 25 - globalOptions.saveTurns <= 50 && - myAdventures() > 25 + globalOptions.saveTurns - ) { - const { meat, items } = sessionSinceStart().value(garboValue); - const numTrackedMeatTurns = myAdventures() - 25 - globalOptions.saveTurns; - setProperty("_garboMarginalMeatCheckpoint", meat.toFixed(0)); - setProperty( - "_garboMarginalItemCheckpoint", - (items - marginalFamiliarsExcessTotal).toFixed(0), - ); - setProperty("_garboMarginalMeatTurns", numTrackedMeatTurns.toFixed(0)); - } +export function trackMarginalMpa() { + const barf = sessions.get("barf"); + const current = Session.current(); + if (!barf) { + sessions.set("barf", Session.current()); + } else { + const turns = barf.diff(current).totalTurns; + // track items if we have run at least 100 turns in barf mountain or we have less than 200 turns left in barf mountain + const item = sessions.get("item"); + if (!item && (turns > 100 || estimatedGarboTurns() <= 200)) { + sessions.set("item", current); + } + // start tracking meat if there are less than 75 turns left in barf mountain + const meatStart = sessions.get("meat-start"); + if (!meatStart && estimatedGarboTurns() <= 75) { + sessions.set("meat-start", current); + } - // Stop tracking meat if we have less than 25 turns left - if ( - get("_garboMarginalMeatCheckpoint") && - get("_garboMarginalMeatTurns") && - continueMeatTracking && - myAdventures() - 25 - globalOptions.saveTurns <= 0 - ) { - continueMeatTracking = false; - const { meat, items } = sessionSinceStart().value(garboValue); - const meatDiff = meat - get("_garboMarginalMeatCheckpoint", 0); - const itemDiff = - items - - marginalFamiliarsExcessTotal - - get("_garboMarginalItemCheckpoint", 0); - setProperty("_garboMarginalMeatValue", meatDiff.toFixed(0)); - setProperty("_garboMarginalItemValue", itemDiff.toFixed(0)); - if (marginalSession) { - marginalSessionDiff = Session.current().diff(marginalSession); + // stop tracking meat if there are less than 25 turns left in barf moutain + const meatEnd = sessions.get("meat-end"); + if (!meatEnd && estimatedGarboTurns() <= 25) { + sessions.set("meat-end", current); } } } -function printMarginalSession(): void { - if ( - myInebriety() > inebrietyLimit() || - myAdventures() > globalOptions.saveTurns - ) { - return; - } - - if (get("_garboMarginalMeatValue") && get("_garboMarginalMeatTurns")) { - const meat = get("_garboMarginalMeatValue", 0); - const meatTurns = get("_garboMarginalMeatTurns", 0); - - if (meatTurns <= 0) { - print("Error in estimating marginal MPA - meat turns tracked = 0", "red"); - return; - } +const outlierItemList = $items`Extrovermectin™, Volcoino, Poké-Gro fertilizer`; - const MPA = meat / meatTurns; +function printMarginalSession() { + const barf = sessions.get("barf"); + const meatStart = sessions.get("meat-start"); + const meatEnd = sessions.get("meat-end"); + const item = sessions.get("item"); + + // we can only print out marginal items if we've started tracking for marginal value + if (barf && meatStart && meatEnd) { + const { itemDetails: barfItemDetails } = barf.value(garboValue); + + const isOutlier = (detail: { + item: Item; + value: number; + quantity: number; + }) => + outlierItemList.includes(detail.item) || + (detail.quantity === 1 && + detail.value >= 5000 && + barfItemDetails.some((d) => d.item === detail.item && d.quantity <= 2)); + + const meatMpa = Session.computeMPA(meatStart, meatEnd, { + value: garboValue, + isOutlier, + }); + + if (item) { + // MPA printout including maringal items + const itemMpa = Session.computeMPA(item, Session.current(), { + value: garboValue, + isOutlier, + excludeValue: { item: extraValue }, + }); - // Only evaluate item outliers if we have run a good number of turns (to reduce variance) - if ( - marginalSessionDiff && - barfSession && - numTrackedItemTurns && - numTrackedItemTurns >= Math.max(50, meatTurns) - ) { - const { items, itemDetails } = marginalSessionDiff.value(garboValue); - const barfItemDetails = Session.current() - .diff(barfSession) - .value(garboValue).itemDetails; - const outlierItemDetails = itemDetails - .filter( - (detail) => - outlierItemList.includes(detail.item) || - (detail.quantity === 1 && - detail.value >= 5000 && - barfItemDetails.some( - (d) => d.item === detail.item && d.quantity <= 2, - )), - ) - .sort((a, b) => b.value - a.value); print(`Outliers:`, HIGHLIGHT); - let outlierItems = 0; - for (const detail of outlierItemDetails) { + for (const detail of itemMpa.outlierItems) { print( `${detail.quantity} ${detail.item} worth ${detail.value.toFixed( 0, )} total`, HIGHLIGHT, ); - outlierItems += detail.value; } - const outlierIPA = - (items - marginalFamiliarsExcessTotal) / numTrackedItemTurns; - const IPA = - (items - marginalFamiliarsExcessTotal - outlierItems) / - numTrackedItemTurns; - const totalOutlierMPA = MPA + outlierIPA; - const totalMPA = MPA + IPA; + + const effectiveMpa = + itemMpa.mpa.effective - itemMpa.mpa.meat + meatMpa.mpa.meat; + const totalMpa = itemMpa.mpa.total - itemMpa.mpa.meat + meatMpa.mpa.meat; + print( `Marginal MPA: ${formatNumber( - Math.round(MPA * 100) / 100, + Math.round(meatMpa.mpa.meat * 100) / 100, )} [raw] + ${formatNumber( - Math.round(IPA * 100) / 100, + Math.round(itemMpa.mpa.items * 100) / 100, )} [items] (${formatNumber( - Math.round(outlierIPA * 100) / 100, + Math.round((itemMpa.mpa.total - itemMpa.mpa.effective) * 100) / 100, )} [outliers]) = ${formatNumber( - Math.round(totalMPA * 100) / 100, + Math.round(effectiveMpa * 100) / 100, )} [total] (${formatNumber( - Math.round(totalOutlierMPA * 100) / 100, + Math.round(totalMpa * 100) / 100, )} [w/ outliers])`, HIGHLIGHT, ); - } else if (get("_garboMarginalItemValue")) { - const items = get("_garboMarginalItemValue", 0); - const IPA = items / meatTurns; - const totalMPA = MPA + IPA; + } else { + // MPA printout excluding marginal items print( "Warning: Insufficient turns were run, so this estimate is subject to large variance. Be careful when using these values as is.", "red", ); print( `Marginal MPA: ${formatNumber( - Math.round(MPA * 100) / 100, + Math.round(meatMpa.mpa.meat * 100) / 100, )} [raw] + ${formatNumber( - Math.round(IPA * 100) / 100, - )} [items] = ${formatNumber(Math.round(totalMPA * 100) / 100)} [total]`, + Math.round(meatMpa.mpa.items * 100) / 100, + )} [items] = ${formatNumber( + Math.round(meatMpa.mpa.total * 100) / 100, + )} [total]`, HIGHLIGHT, ); } diff --git a/packages/garbo/src/tasks/barfTurn.ts b/packages/garbo/src/tasks/barfTurn.ts index 84a2020c3..1df01bbaa 100644 --- a/packages/garbo/src/tasks/barfTurn.ts +++ b/packages/garbo/src/tasks/barfTurn.ts @@ -64,6 +64,7 @@ import { deliverThesisIfAble } from "../fights"; import { computeDiet, consumeDiet } from "../diet"; import { GarboTask } from "./engine"; +import { trackMarginalMpa, trackMarginalTurnExtraValue } from "../session"; import { garboValue } from "../garboValue"; import { bestMidnightAvailable, @@ -657,7 +658,11 @@ export const BarfTurnQuest: Quest = { outfit: () => { const lubing = get("dinseyRollercoasterNext") && have($item`lube-shoes`); - return barfOutfit(lubing ? { equip: $items`lube-shoes` } : {}); + const { outfit, extraValue } = barfOutfit( + lubing ? { equip: $items`lube-shoes` } : {}, + ); + trackMarginalTurnExtraValue(extraValue); + return outfit; }, do: $location`Barf Mountain`, combat: new GarboStrategy( @@ -668,7 +673,10 @@ export const BarfTurnQuest: Quest = { Macro.meatKill(), ).abort(), ), - post: () => completeBarfQuest(), + post: () => { + completeBarfQuest(); + trackMarginalMpa(); + }, spendsTurn: true, }, ],