diff --git a/src/familiar/freeFightFamiliar.ts b/src/familiar/freeFightFamiliar.ts index 18c84e5b5..9a96da43a 100644 --- a/src/familiar/freeFightFamiliar.ts +++ b/src/familiar/freeFightFamiliar.ts @@ -1,11 +1,9 @@ -import { Familiar, familiarWeight, inebrietyLimit, Location, myInebriety } from "kolmafia"; +import { Familiar, familiarWeight, inebrietyLimit, Location, Monster, myInebriety } from "kolmafia"; import { $familiar, $item, $location, - $monsters, $phylum, - $skill, clamp, findLeprechaunMultiplier, get, @@ -19,6 +17,7 @@ import getDropFamiliars from "./dropFamiliars"; import getExperienceFamiliars from "./experienceFamiliars"; import { GeneralFamiliar, timeToMeatify } from "./lib"; import { meatFamiliar } from "./meatFamiliar"; +import { barfEncounterRate } from "../lib"; type MenuOptions = { canChooseMacro?: boolean; @@ -86,33 +85,12 @@ export function menu(options: MenuOptions = {}): GeneralFamiliar[] { Snapper.have() && Snapper.getTrackedPhylum() === $phylum`dude` ) { - /* - # E stands for olfacted Garbage Tourist, A is angry toursit, F is horrible tourist family - import itertools - def rate(q): - m = ["E"] * 5 + ["A"] * 2 + ["F"] * 2 - options = list(itertools.product(m, m)) - dude = [m for m in options if (m[0] in ["A", "F"] and m[0] not in q) or (m[1] in ["A", "F"] and m[0] in q and m[0] != "E")] - return len(dude) / 81 - */ - - const dudes = $monsters`angry tourist, horrible tourist family`.filter((m) => - $location`Barf Mountain`.combatQueue.includes(`${m}`), - ).length; - - // if you don't have olfaction, just assume a simple rate calculation - const noOlfactRate = 4 / (1 + 4 + (have($skill`Gallapagosian Mating Call`) ? 1 : 0)); - - // when you have olfaction, you - // using the above python script, dude rate for number of dudes in queue is: - const olfactRate = - [ - 0.44, // 0 dudes = 44% chance - 0.32, // 1 dude = 32% chance - 0.19, // 2 dudes = 19% chance - ][dudes] ?? 0; - - const dudeRate = have($skill`Transcendent Olfaction`) ? olfactRate : noOlfactRate; + const encounterRate = barfEncounterRate({ snapper: true, snapperPhylum: $phylum`dude` }); + const dudeRate = [...encounterRate.entries()].reduce( + (acc: number, entry: [Monster, number]) => + entry[0].phylum === $phylum`dude` ? entry[1] + acc : acc, + 0, + ); familiarMenu.push({ familiar: $familiar`Red-Nosed Snapper`, diff --git a/src/lib.ts b/src/lib.ts index ae2a275f6..e588bc458 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -34,6 +34,7 @@ import { mySoulsauce, myTurncount, numericModifier, + Phylum, print, printHtml, restoreHp, @@ -60,6 +61,8 @@ import { $item, $location, $monster, + $monsters, + $phylum, $skill, $slot, ActionSource, @@ -80,6 +83,7 @@ import { PropertiesManager, property, set, + Snapper, SongBoom, sum, uneffect, @@ -683,3 +687,80 @@ export function printEventLog(): void { ); } } + +const rateCache = new Map>(); + +export function barfEncounterRate(options: { + snapper?: boolean; + snapperPhylum?: Phylum; + olfact?: Monster; + longCon?: Monster; + motif?: Monster; + turtle?: Monster; + monkeyPoint?: Monster; + humanity?: boolean; +}): Map { + const olfact = options.olfact ?? get("olfactedMonster"); + const longCon = options.longCon ?? get("longConMonster"); + const motif = options.motif ?? get("motifMonster"); + const turtle = options.turtle ?? get("_gallapagosMonster"); + const monkeyPoint = options.monkeyPoint ?? get("monkeyPointMonster"); + const snapper = options.snapper ?? myFamiliar() === $familiar`Red-Nosed Snapper`; + const snapperPhylum = options.snapperPhylum ?? Snapper.getTrackedPhylum(); + const humanity = options.humanity ?? have($effect`Ew, The Humanity`); + + const zoneMonsters = $monsters`garbage tourist, angry tourist, horrible tourist family`; + const encounterQueue = zoneMonsters.filter((m) => + $location`Barf Mountain`.combatQueue.includes(`${m}`), + ); + + const cacheKey = [ + olfact, + longCon, + motif, + turtle, + monkeyPoint, + snapper, + snapperPhylum, + humanity, + ...encounterQueue, + ] + .map((v) => `${v}`) + .join(":"); + + const cachedValue = rateCache.get(cacheKey); + if (cachedValue) { + return cachedValue; + } + + const copies = (target: Monster | null, n: number): Monster[] => + n === 0 ? [] : [...zoneMonsters.filter((m) => m === target), ...copies(target, n - 1)]; + + const monsterQueue = [ + ...zoneMonsters, + ...copies(olfact, 3), + ...copies(longCon, 3), + ...copies(motif, 2), + ...copies(turtle, 1), + ...copies(monkeyPoint, 2), + ...zoneMonsters + .filter((m) => snapper && m.phylum === snapperPhylum) + .flatMap((m) => copies(m, 2)), + ...zoneMonsters + .filter((m) => humanity && m.phylum === $phylum`dude`) + .flatMap((m) => copies(m, 2)), + ]; + + const encounters = monsterQueue.flatMap((m) => + monsterQueue.map((n) => + // olfaction, longcon, and motif caus that monster to ignore queue rejection + olfact === m || longCon === m || motif === m || !encounterQueue.includes(m) ? m : n, + ), + ); + + const encounterRate = new Map( + zoneMonsters.map((m) => [m, encounters.filter((n) => n === m).length / encounters.length]), + ); + rateCache.set(cacheKey, encounterRate); + return encounterRate; +}