From 931b13e9bae8843e11b0e9bb5621e044e68a17c9 Mon Sep 17 00:00:00 2001 From: Chris Midgley Date: Tue, 19 Aug 2025 00:21:26 +0100 Subject: [PATCH 1/6] feat: accept negative modifier weights For example, `busker-solver modifiers="-Mana Cost" allbusks` now works and shows that power 130, busk 3 gives Dreams and Lights. --- src/main.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main.ts b/src/main.ts index 4bac5e5..ea8d49c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -76,14 +76,12 @@ function parseWeightedModifiers(input: string): Partial Date: Tue, 19 Aug 2025 16:05:51 +0100 Subject: [PATCH 2/6] fix: display useful effects with negative weights --- src/main.ts | 4 ++-- src/utils.ts | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main.ts b/src/main.ts index ea8d49c..ce10071 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,5 @@ import { Args } from "grimoire-kolmafia"; -import { Effect, Modifier, myPath, print, toEffect, toModifier } from "kolmafia"; +import { Effect, Modifier, myPath, print, toEffect } from "kolmafia"; import { $effects, $path, get, have, NumericModifier, sinceKolmafiaRevision } from "libram"; import { findOptimalOutfitPower, @@ -163,7 +163,7 @@ export function main(command?: string): void { printBuskResult( result, - Object.keys(weightedModifiers).map((mod) => toModifier(mod)), + weightedModifiers, desiredEffects ); } diff --git a/src/utils.ts b/src/utils.ts index ff09c69..29f545b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -12,6 +12,7 @@ import { print, Slot, toEffect, + toModifier, toSlot, } from "kolmafia"; import { @@ -80,7 +81,7 @@ function multipliers(slot: Slot): number { export function printBuskResult( result: BuskResult | null, - modifiers: Modifier[], + modifiers: Partial>, desiredEffects: Effect[] = [] ): void { if (!result) { @@ -101,13 +102,15 @@ export function printBuskResult( $modifier`Familiar Experience`, ]; + const modKeys = Object.keys(modifiers).map((mod) => toModifier(mod)); + for (const { effects, daRaw, buskIndex } of bestBusks) { const desiredMatches = effects.filter((e) => desiredEffects.includes(e)); print(`Power ${daRaw} Busk ${buskIndex + 1}`); // Calculate total buff per weighted modifier const weightedTotals = new Map(); - for (const mod of modifiers) { + for (const mod of modKeys) { const total = sum(effects, (ef) => numericModifier(ef, mod)); weightedTotals.set(mod, total); } @@ -127,8 +130,8 @@ export function printBuskResult( print(`Total buffs: ${totalBuffsStr}`); // For each weighted modifier, print contributing effects - for (const mod of modifiers) { - const contributingEffects = effects.filter((e) => numericModifier(e, mod) > 0); + for (const mod of modKeys) { + const contributingEffects = effects.filter((e) => numericModifier(e, mod) * modifiers[mod.name as NumericModifier]! > 0); if (contributingEffects.length === 0) continue; print(`${mod.name}:`); @@ -137,7 +140,7 @@ export function printBuskResult( // Optionally print other useful modifiers if args.othermodifiers is true if (othermodifiers) { - const otherMods = otherModifiersList.filter((mod) => !modifiers.includes(mod)); + const otherMods = otherModifiersList.filter((mod) => !modKeys.includes(mod)); if (otherMods.length > 0) { print(`Other Useful Modifiers:`); for (const mod of otherMods) { @@ -149,7 +152,7 @@ export function printBuskResult( } } const usefulEffects = effects.filter((e) => - modifiers.some((mod) => numericModifier(e, mod) > 0) + modKeys.some((mod) => numericModifier(e, mod) * modifiers[mod.name as NumericModifier]! > 0) ); const otherEffects = effects.filter( (e) => !desiredEffects.includes(e) && !usefulEffects.includes(e) From 4f2379c72bc115b723aaaa07b18c1050318de680 Mon Sep 17 00:00:00 2001 From: Chris Midgley Date: Tue, 19 Aug 2025 16:06:08 +0100 Subject: [PATCH 3/6] chore: prettier --- src/main.ts | 6 +----- src/utils.ts | 4 +++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main.ts b/src/main.ts index ce10071..0611c0f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -161,10 +161,6 @@ export function main(command?: string): void { .join(", ")}]` ); - printBuskResult( - result, - weightedModifiers, - desiredEffects - ); + printBuskResult(result, weightedModifiers, desiredEffects); } } diff --git a/src/utils.ts b/src/utils.ts index 29f545b..f846ded 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -131,7 +131,9 @@ export function printBuskResult( // For each weighted modifier, print contributing effects for (const mod of modKeys) { - const contributingEffects = effects.filter((e) => numericModifier(e, mod) * modifiers[mod.name as NumericModifier]! > 0); + const contributingEffects = effects.filter( + (e) => numericModifier(e, mod) * modifiers[mod.name as NumericModifier]! > 0 + ); if (contributingEffects.length === 0) continue; print(`${mod.name}:`); From bee813293705a7cec66246450471dc3c8d55706c Mon Sep 17 00:00:00 2001 From: Chris Midgley Date: Tue, 19 Aug 2025 16:23:17 +0100 Subject: [PATCH 4/6] chore: convert parsed modifier object to map --- src/main.ts | 14 ++++++++------ src/utils.ts | 26 ++++++++++++++------------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/main.ts b/src/main.ts index 0611c0f..d2296f6 100644 --- a/src/main.ts +++ b/src/main.ts @@ -68,10 +68,10 @@ export let test = false; export let pathpower = 0; export let owned = false; -function parseWeightedModifiers(input: string): Partial> { - if (!input.trim()) return {}; +function parseWeightedModifiers(input: string): Map { + if (!input.trim()) return new Map(); - const result: Partial> = {}; + const result = new Map(); const parts = input.split(",").map((s) => s.trim()); for (const part of parts) { @@ -81,7 +81,7 @@ function parseWeightedModifiers(input: string): Partial(); const valuerFn = normalizeEffectValuer(hybridEffectValuer(desiredEffects, weightedModifiers)); @@ -156,7 +158,7 @@ export function main(command?: string): void { print( `Hybrid strategy: prioritizing effects [${desiredEffects .map((e) => e.name) - .join(", ")}], fallback modifiers [${Object.entries(weightedModifiers) + .join(", ")}], fallback modifiers [${[...weightedModifiers.entries()] .map(([m, w]) => `${w}×${m}`) .join(", ")}]` ); diff --git a/src/utils.ts b/src/utils.ts index f846ded..52816db 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -81,7 +81,7 @@ function multipliers(slot: Slot): number { export function printBuskResult( result: BuskResult | null, - modifiers: Partial>, + modifiers: Map, desiredEffects: Effect[] = [] ): void { if (!result) { @@ -102,7 +102,7 @@ export function printBuskResult( $modifier`Familiar Experience`, ]; - const modKeys = Object.keys(modifiers).map((mod) => toModifier(mod)); + const modKeys = [...modifiers.keys()].map((mod) => toModifier(mod)); for (const { effects, daRaw, buskIndex } of bestBusks) { const desiredMatches = effects.filter((e) => desiredEffects.includes(e)); @@ -132,7 +132,7 @@ export function printBuskResult( // For each weighted modifier, print contributing effects for (const mod of modKeys) { const contributingEffects = effects.filter( - (e) => numericModifier(e, mod) * modifiers[mod.name as NumericModifier]! > 0 + (e) => numericModifier(e, mod) * modifiers.get(mod.name as NumericModifier)! > 0 ); if (contributingEffects.length === 0) continue; @@ -154,7 +154,9 @@ export function printBuskResult( } } const usefulEffects = effects.filter((e) => - modKeys.some((mod) => numericModifier(e, mod) * modifiers[mod.name as NumericModifier]! > 0) + modKeys.some( + (mod) => numericModifier(e, mod) * modifiers.get(mod.name as NumericModifier)! > 0 + ) ); const otherEffects = effects.filter( (e) => !desiredEffects.includes(e) && !usefulEffects.includes(e) @@ -180,13 +182,13 @@ export function printBuskResult( export function makeBuskResultFromPowers( powers: number[], - weightedModifiers: Partial>, + weightedModifiers: Map, uselessEffects: Effect[], buskStartIndex = get("_beretBuskingUses", 0) ): BuskResult { // eslint-disable-next-line @typescript-eslint/no-unused-vars const effectValuer = (effect: Effect, _duration: number) => - Object.entries(weightedModifiers) + [...weightedModifiers.entries()] .map(([mod, weight]) => (weight ?? 0) * numericModifier(effect, mod)) .reduce((a, b) => a + b, 0); @@ -212,7 +214,7 @@ export function makeBuskResultFromPowers( export function hybridEffectValuer( desiredEffects: Effect[], - weightedModifiers: Partial> + weightedModifiers: Map ): (effect: Effect, duration: number, all?: [Effect, number][]) => number { const wantedSet = new Set(desiredEffects); return (effect, duration) => { @@ -221,7 +223,7 @@ export function hybridEffectValuer( return 1e6 + duration; // Big constant ensures it's preferred } return sum( - Object.entries(weightedModifiers), + [...weightedModifiers.entries()], ([mod, weight]) => weight * numericModifier(effect, mod) ); }; @@ -237,18 +239,18 @@ export function normalizeEffectValuer( const set = new Set(valuer); return (effect, duration) => (set.has(effect) ? duration : 0); } else { - // valuer is a weighted modifier object + // valuer is a map // eslint-disable-next-line @typescript-eslint/no-unused-vars return (effect, _duration) => sum( - Object.entries(valuer), + [...valuer.entries()], ([modifier, weight]) => weight * numericModifier(effect, modifier) ); } } export type EffectValuer = - | Partial> + | Map | ((effect: Effect, duration: number) => number) | Effect[]; @@ -329,7 +331,7 @@ export function findOptimalOutfitPower( * @returns The power-sum at which you'll find the optimal busk for this situation. */ export function findOptimalOutfitPower( - weightedModifiers: Partial>, + weightedModifiers: Map, buskUses?: number, uselessEffects?: Effect[], buyItem?: boolean From c84dd2d7a683333732a907f7d7cc1b464d81fedf Mon Sep 17 00:00:00 2001 From: Chris Midgley Date: Tue, 19 Aug 2025 16:28:52 +0100 Subject: [PATCH 5/6] feat: store modifier in map, not modifier name --- src/main.ts | 14 +++++++------- src/utils.ts | 19 ++++++++----------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/main.ts b/src/main.ts index d2296f6..5aa861e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,5 @@ import { Args } from "grimoire-kolmafia"; -import { Effect, Modifier, myPath, print, toEffect } from "kolmafia"; +import { Effect, Modifier, myPath, print, toEffect, toModifier } from "kolmafia"; import { $effects, $path, get, have, NumericModifier, sinceKolmafiaRevision } from "libram"; import { findOptimalOutfitPower, @@ -68,10 +68,10 @@ export let test = false; export let pathpower = 0; export let owned = false; -function parseWeightedModifiers(input: string): Map { - if (!input.trim()) return new Map(); +function parseWeightedModifiers(input: string): Map { + if (!input.trim()) return new Map(); - const result = new Map(); + const result = new Map(); const parts = input.split(",").map((s) => s.trim()); for (const part of parts) { @@ -80,8 +80,8 @@ function parseWeightedModifiers(input: string): Map { if (weightedMatch) { const sign = weightedMatch[1] === "-" ? -1 : 1; const weight = weightedMatch[2] === undefined ? 1 : Number(weightedMatch[2]); - const modifierName = weightedMatch[3].trim() as NumericModifier; - result.set(modifierName, sign * weight); + const modifier = toModifier(weightedMatch[3].trim()); + result.set(modifier, sign * weight); } } return result; @@ -140,7 +140,7 @@ export function main(command?: string): void { const weightedModifiers = args.modifiers !== Modifier.none.name ? parseWeightedModifiers(args.modifiers) - : new Map(); + : new Map(); const valuerFn = normalizeEffectValuer(hybridEffectValuer(desiredEffects, weightedModifiers)); diff --git a/src/utils.ts b/src/utils.ts index 52816db..79a9e38 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -26,7 +26,6 @@ import { have as have_, logger, maxBy, - NumericModifier, sum, } from "libram"; import { @@ -81,7 +80,7 @@ function multipliers(slot: Slot): number { export function printBuskResult( result: BuskResult | null, - modifiers: Map, + modifiers: Map, desiredEffects: Effect[] = [] ): void { if (!result) { @@ -102,7 +101,7 @@ export function printBuskResult( $modifier`Familiar Experience`, ]; - const modKeys = [...modifiers.keys()].map((mod) => toModifier(mod)); + const modKeys = [...modifiers.keys()]; for (const { effects, daRaw, buskIndex } of bestBusks) { const desiredMatches = effects.filter((e) => desiredEffects.includes(e)); @@ -132,7 +131,7 @@ export function printBuskResult( // For each weighted modifier, print contributing effects for (const mod of modKeys) { const contributingEffects = effects.filter( - (e) => numericModifier(e, mod) * modifiers.get(mod.name as NumericModifier)! > 0 + (e) => numericModifier(e, mod) * modifiers.get(mod)! > 0 ); if (contributingEffects.length === 0) continue; @@ -154,9 +153,7 @@ export function printBuskResult( } } const usefulEffects = effects.filter((e) => - modKeys.some( - (mod) => numericModifier(e, mod) * modifiers.get(mod.name as NumericModifier)! > 0 - ) + modKeys.some((mod) => numericModifier(e, mod) * modifiers.get(mod)! > 0) ); const otherEffects = effects.filter( (e) => !desiredEffects.includes(e) && !usefulEffects.includes(e) @@ -182,7 +179,7 @@ export function printBuskResult( export function makeBuskResultFromPowers( powers: number[], - weightedModifiers: Map, + weightedModifiers: Map, uselessEffects: Effect[], buskStartIndex = get("_beretBuskingUses", 0) ): BuskResult { @@ -214,7 +211,7 @@ export function makeBuskResultFromPowers( export function hybridEffectValuer( desiredEffects: Effect[], - weightedModifiers: Map + weightedModifiers: Map ): (effect: Effect, duration: number, all?: [Effect, number][]) => number { const wantedSet = new Set(desiredEffects); return (effect, duration) => { @@ -250,7 +247,7 @@ export function normalizeEffectValuer( } export type EffectValuer = - | Map + | Map | ((effect: Effect, duration: number) => number) | Effect[]; @@ -331,7 +328,7 @@ export function findOptimalOutfitPower( * @returns The power-sum at which you'll find the optimal busk for this situation. */ export function findOptimalOutfitPower( - weightedModifiers: Map, + weightedModifiers: Map, buskUses?: number, uselessEffects?: Effect[], buyItem?: boolean From 3f23dff2938919bdbda99b5a5dec53fb9ea9694b Mon Sep 17 00:00:00 2001 From: Chris Midgley Date: Tue, 19 Aug 2025 17:45:08 +0100 Subject: [PATCH 6/6] feat: allow requesting boolean modifiers --- src/main.ts | 8 ++++++-- src/utils.ts | 34 ++++++++++++++++++---------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/main.ts b/src/main.ts index 5aa861e..7d91043 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { Args } from "grimoire-kolmafia"; import { Effect, Modifier, myPath, print, toEffect, toModifier } from "kolmafia"; -import { $effects, $path, get, have, NumericModifier, sinceKolmafiaRevision } from "libram"; +import { $effects, $path, get, have, sinceKolmafiaRevision } from "libram"; import { findOptimalOutfitPower, hybridEffectValuer, @@ -81,7 +81,11 @@ function parseWeightedModifiers(input: string): Map { const sign = weightedMatch[1] === "-" ? -1 : 1; const weight = weightedMatch[2] === undefined ? 1 : Number(weightedMatch[2]); const modifier = toModifier(weightedMatch[3].trim()); - result.set(modifier, sign * weight); + if (modifier.type === "numeric" || modifier.type === "boolean") { + result.set(modifier, sign * weight); + } else { + print(`Error: modifier '${weightedMatch[3]}' not numeric or boolean`); + } } } return result; diff --git a/src/utils.ts b/src/utils.ts index 79a9e38..eadffb5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import { beretBuskingEffects, + booleanModifier, canEquip, Effect, getPower, @@ -12,7 +13,6 @@ import { print, Slot, toEffect, - toModifier, toSlot, } from "kolmafia"; import { @@ -110,7 +110,7 @@ export function printBuskResult( // Calculate total buff per weighted modifier const weightedTotals = new Map(); for (const mod of modKeys) { - const total = sum(effects, (ef) => numericModifier(ef, mod)); + const total = sum(effects, (ef) => modifier(ef, mod)); weightedTotals.set(mod, total); } @@ -130,9 +130,7 @@ export function printBuskResult( // For each weighted modifier, print contributing effects for (const mod of modKeys) { - const contributingEffects = effects.filter( - (e) => numericModifier(e, mod) * modifiers.get(mod)! > 0 - ); + const contributingEffects = effects.filter((e) => modifier(e, mod) * modifiers.get(mod)! > 0); if (contributingEffects.length === 0) continue; print(`${mod.name}:`); @@ -145,7 +143,7 @@ export function printBuskResult( if (otherMods.length > 0) { print(`Other Useful Modifiers:`); for (const mod of otherMods) { - const total = sum(effects, (ef) => numericModifier(ef, mod)); + const total = sum(effects, (ef) => modifier(ef, mod)); if (total > 0) { print(` ${mod.name}: ${total}`); } @@ -153,7 +151,7 @@ export function printBuskResult( } } const usefulEffects = effects.filter((e) => - modKeys.some((mod) => numericModifier(e, mod) * modifiers.get(mod)! > 0) + modKeys.some((mod) => modifier(e, mod) * modifiers.get(mod)! > 0) ); const otherEffects = effects.filter( (e) => !desiredEffects.includes(e) && !usefulEffects.includes(e) @@ -186,7 +184,7 @@ export function makeBuskResultFromPowers( // eslint-disable-next-line @typescript-eslint/no-unused-vars const effectValuer = (effect: Effect, _duration: number) => [...weightedModifiers.entries()] - .map(([mod, weight]) => (weight ?? 0) * numericModifier(effect, mod)) + .map(([mod, weight]) => (weight ?? 0) * modifier(effect, mod)) .reduce((a, b) => a + b, 0); const busks: Busk[] = powers.map((power, index) => { @@ -219,10 +217,7 @@ export function hybridEffectValuer( // Strongly prioritize desired effects return 1e6 + duration; // Big constant ensures it's preferred } - return sum( - [...weightedModifiers.entries()], - ([mod, weight]) => weight * numericModifier(effect, mod) - ); + return sum([...weightedModifiers.entries()], ([mod, weight]) => weight * modifier(effect, mod)); }; } @@ -239,10 +234,7 @@ export function normalizeEffectValuer( // valuer is a map // eslint-disable-next-line @typescript-eslint/no-unused-vars return (effect, _duration) => - sum( - [...valuer.entries()], - ([modifier, weight]) => weight * numericModifier(effect, modifier) - ); + sum([...valuer.entries()], ([mod, weight]) => weight * modifier(effect, mod)); } } @@ -433,3 +425,13 @@ export function findOutfit(power: number, buyItem: boolean) { } return outfit; } + +function modifier(effect: Effect, mod: Modifier): number { + if (mod.type === "numeric") { + return numericModifier(effect, mod); + } else if (mod.type === "boolean") { + return booleanModifier(effect, mod) ? 1 : 0; + } + // we limit to numeric or boolean modifiers when parsing + return 0; +}