From 1eeaa5f1a217a64f85f879f5ab6c86fd3e730609 Mon Sep 17 00:00:00 2001 From: Patrick Stalcup Date: Tue, 19 Sep 2023 10:59:25 -0400 Subject: [PATCH 1/3] support snapper as a familiar --- src/familiar/freeFightFamiliar.ts | 32 ++++++++++++++++++++++++++++++- src/fights.ts | 5 ++++- src/tasks/dailyFamiliars.ts | 8 ++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/familiar/freeFightFamiliar.ts b/src/familiar/freeFightFamiliar.ts index 322b35ac5..0242c2f76 100644 --- a/src/familiar/freeFightFamiliar.ts +++ b/src/familiar/freeFightFamiliar.ts @@ -1,5 +1,16 @@ import { Familiar, familiarWeight, inebrietyLimit, Location, myInebriety } from "kolmafia"; -import { $familiar, $item, $location, clamp, findLeprechaunMultiplier, get, have } from "libram"; +import { + $familiar, + $item, + $location, + $phylum, + $skill, + clamp, + findLeprechaunMultiplier, + get, + have, + Snapper, +} from "libram"; import { canOpenRedPresent } from "."; import { garboValue } from "../value"; import getConstantValueFamiliars from "./constantValueFamiliars"; @@ -69,6 +80,25 @@ export function menu(options: MenuOptions = {}): GeneralFamiliar[] { limit: "special", }); } + if ( + location === $location`Barf Mountain` && + Snapper.have() && + Snapper.getTrackedPhylum() === $phylum`dude` + ) { + // when running snapper, there are 4 dudes in the zone, and a variable number of nondudes + const dudeRate = + 4 / + (4 + // 4 dudes + 1 + // 1 garbage tourist + (have($skill`Transcendent Olfaction`) ? 3 : 0) + + (have($skill`Gallapagosian Mating Call`) ? 1 : 0)); + familiarMenu.push({ + familiar: $familiar`Red-Nosed Snapper`, + expectedValue: (dudeRate * garboValue($item`human musk`)) / 11, + leprechaunMultiplier: 0, + limit: "none", + }); + } } const meatFam = meatFamiliar(); diff --git a/src/fights.ts b/src/fights.ts index 14645fc60..5a6a171b8 100644 --- a/src/fights.ts +++ b/src/fights.ts @@ -106,6 +106,7 @@ import { Requirement, Robortender, set, + Snapper, SourceTerminal, sum, tryFindFreeRun, @@ -994,7 +995,9 @@ const freeFightSources = [ new FreeFight( () => (wantPills() ? 5 - get("_saberForceUses") : 0), () => { - if (have($familiar`Red-Nosed Snapper`)) cliExecute(`snapper ${$phylum`dude`}`); + if (Snapper.have() && Snapper.getTrackedPhylum() !== $phylum`dude`) { + Snapper.trackPhylum($phylum`dude`); + } setChoice(1387, 3); if ( have($skill`Comprehensive Cartography`) && diff --git a/src/tasks/dailyFamiliars.ts b/src/tasks/dailyFamiliars.ts index 793cfdde4..1c5a49c3c 100644 --- a/src/tasks/dailyFamiliars.ts +++ b/src/tasks/dailyFamiliars.ts @@ -18,11 +18,13 @@ import { $familiars, $item, $items, + $phylum, CrimboShrub, get, have, Robortender, set, + Snapper, sum, withProperty, } from "libram"; @@ -227,6 +229,12 @@ const DailyFamiliarTasks: GarboTask[] = [ !!get("garbo_felizValue", 0) || today - get("garbo_felizValueDate", 0) < 24 * 60 * 60 * 1000, do: () => felizValue, }, + { + name: "Snapper Hunts Dudes", + ready: () => Snapper.have(), + completed: () => Snapper.getTrackedPhylum() === $phylum`dude`, + do: () => Snapper.trackPhylum($phylum`dude`), + }, ]; export const DailyFamiliarsQuest: Quest = { From adebb7f47b59c6e2bc1cc5e665262ff5ba4d532d Mon Sep 17 00:00:00 2001 From: Patrick Stalcup Date: Tue, 19 Sep 2023 12:05:33 -0400 Subject: [PATCH 2/3] use more complex calculations that factor in queue rejection --- src/familiar/freeFightFamiliar.ts | 36 +++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/familiar/freeFightFamiliar.ts b/src/familiar/freeFightFamiliar.ts index 0242c2f76..18c84e5b5 100644 --- a/src/familiar/freeFightFamiliar.ts +++ b/src/familiar/freeFightFamiliar.ts @@ -3,6 +3,7 @@ import { $familiar, $item, $location, + $monsters, $phylum, $skill, clamp, @@ -85,13 +86,34 @@ export function menu(options: MenuOptions = {}): GeneralFamiliar[] { Snapper.have() && Snapper.getTrackedPhylum() === $phylum`dude` ) { - // when running snapper, there are 4 dudes in the zone, and a variable number of nondudes - const dudeRate = - 4 / - (4 + // 4 dudes - 1 + // 1 garbage tourist - (have($skill`Transcendent Olfaction`) ? 3 : 0) + - (have($skill`Gallapagosian Mating Call`) ? 1 : 0)); + /* + # 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; + familiarMenu.push({ familiar: $familiar`Red-Nosed Snapper`, expectedValue: (dudeRate * garboValue($item`human musk`)) / 11, From 1b82ae81dc3c49a33c0536e478117a0f9fb7e64f Mon Sep 17 00:00:00 2001 From: Patrick Stalcup Date: Wed, 20 Sep 2023 20:55:32 -0400 Subject: [PATCH 3/3] Improve snapper encounter rate computation --- src/familiar/freeFightFamiliar.ts | 38 +++------------ src/lib.ts | 81 +++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 30 deletions(-) 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; +}