Skip to content

Commit

Permalink
Add Coinmasters
Browse files Browse the repository at this point in the history
  • Loading branch information
pstalcup committed Aug 28, 2023
1 parent a3e3368 commit fb71d6a
Show file tree
Hide file tree
Showing 9 changed files with 1,709 additions and 2,256 deletions.
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ Naming a tab involves specifying what to do with all items in that tab by naming
* This will convert items in this tab into Asdon Martin fuel
* `collection`
* This will send any items with a matching keeping-tabs-collection directive in your notes to the relevant party
* `coinmaster`
* This will trade any items in this tab with a coinmaster. First it will consider any directives specified in your notes (see Coinmaster Directives below). Then, if `best` is specified as an option, it will compute the most cost effective item

### Options

Expand All @@ -65,6 +67,8 @@ All options are supported in all tabs, unless specified. They are white space se
* (only supported by `kmail`) to whom to send the kmail. Can be player name or player ID number
* `body=text`
* (only supported by `kmail` and `collection`) the text of the kmail to send
* `best`
* (only supported by `coinmaster`) pick the best (most cost effective) item you can trade the given coin for (include the coin itself if it is tradeable)

### Examples

Expand Down Expand Up @@ -103,7 +107,7 @@ If you want to specify multiple items, you can instead use:

`keeping-tabs-collection: 'playername'=itemid1,itemid2`

To find the items ID, you can type `js toInt(Item.get("item name here"))` in the KoLMafia CLI
To find the items ID, you can type `js Item.get("item name here").id` in the KoLMafia CLI

You can create an arbitrary amount of collection directives by putting multiple rows in your notes:

Expand All @@ -116,11 +120,30 @@ Keep in mind that it will only send out items that are in a corresponding `colle

It is recommended that you run `keeping-tabs debug collections` after adding a collection to verify it is registered and is the item you expect.

## Coinmaster Directives

For the `coinmaster` action, you can specify exact trades you want to occur

```
keeping-tabs-coinmaster: coinid=itemid
```

To find the items ID, you can type `js Item.get("item name here").id` in the KoLMafia CLI

You can create an arbitrary amount of coinmaster directives by putting multiple rows in your notes:

```
keeping-tabs-coinmaster: coinid1=itemid1
keeping-tabs-coinmaster: coinid2=itemid2
```

It is recommended that you run `keeping-tabs debug coinmaster` after adding a collection to verify it is registered and is the item you expect.

## Running

To get a full help documentation, you can run `keeping-tabs help`.

After adding your items to the favorite tabs in the game, just run hte command `keeping-tabs` on the command line. By default, it will run the command groups in the order `use mall autosell display kmail`
After adding your items to the favorite tabs in the game, just run hte command `keeping-tabs` on the command line. By default, it will run the command groups in the order `closet use coinmaster mall autosell display sell kmail fuel collection`

Once you have gotten the hang of it and are generally comfortable with the actions Keeping Tabs is taking, one commonly used workflow is to add it to a breakfast or logout script, so that you are constantly cycling out the items in your tabs.

Expand Down
31 changes: 29 additions & 2 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
autosell,
autosellPrice,
cliExecute,
isCoinmasterItem,
Item,
itemAmount,
mallPrice,
Expand All @@ -12,9 +13,11 @@ import {
use,
wellStocked,
} from "kolmafia";
import { Kmail } from "libram";
import { AsdonMartin, Kmail } from "libram";
import { Options } from "./options";
import { TabTitle } from "./types";
import { coinmasterBest, coinmasterBuyAll } from "./coinmaster";
import { warn } from "./lib";

function amount(item: Item, options: Options) {
if (options.keep) {
Expand Down Expand Up @@ -84,6 +87,13 @@ export const actions: {
};
},
fuel: (options: Options) => {
if (!AsdonMartin.installed()) {
warn("Asdon martin not installed, skipping fuel action");
return {
// eslint-disable-next-line @typescript-eslint/no-empty-function
action: (item: Item) => {},
};
}
return {
action: (item: Item) => cliExecute(`asdonmartin fuel ${amount(item, options)} ${item}`),
};
Expand All @@ -92,7 +102,7 @@ export const actions: {
const kmails = new Map<string, Item[]>();
return {
action: (item: Item) => {
options.collectionsMap.forEach((colItems, target) => {
options.collections.forEach((colItems, target) => {
if (colItems.includes(item)) {
const items = kmails.get(target);
if (items) {
Expand All @@ -107,6 +117,7 @@ export const actions: {
[...kmails.entries()].map((v) => {
const [target, items] = v;
const itemQuantities = new Map<Item, number>(items.map((i) => [i, amount(i, options)]));
print(`Sending Kmail to ${target}`);
Kmail.send(
target,
options.body ?? "For your collection, courtesy of keeping-tabs",
Expand All @@ -116,4 +127,20 @@ export const actions: {
},
};
},
coinmaster: (options: Options) => ({
action: (item: Item) => {
const targetPair = options.coinmasters.get(item);
const availableCoins = amount(item, options);
if (targetPair) {
const [coinmaster, targetItem] = targetPair;
coinmasterBuyAll(coinmaster, targetItem, availableCoins);
} else if (options.best) {
const best = coinmasterBest(item);
if (best && best[1] !== item) {
print(`Computed best for ${item} is ${best[1]} from ${best[0]}`);
coinmasterBuyAll(...best, availableCoins);
}
}
},
}),
};
43 changes: 43 additions & 0 deletions src/coinmaster.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { buy, Coinmaster, Item, mallPrice, sellPrice, sellsItem } from "kolmafia";
import { $item } from "libram";

export function coinmasterBuyAll(
coinmaster: Coinmaster,
target: Item,
availableCoins: number
): void {
const toBuy = Math.floor(availableCoins / sellPrice(coinmaster, target));
if (toBuy > 0) {
buy(coinmaster, toBuy, target);
}
}

function getSellableItem(item: Item) {
if (item === $item`Merc Core deployment orders`) {
return $item`one-day ticket to Conspiracy Island`;
}
return item;
}

export function coinmasterBest(coin: Item): [Coinmaster, Item] | undefined {
const coinmasters = Coinmaster.all().filter((c) => c.item === coin);
const price = (c: Coinmaster, i: Item) => mallPrice(i) / sellPrice(c, i);

const availablePurchases = coinmasters
.map((c): [Coinmaster, Item][] =>
Item.all()
.filter((i) => sellsItem(c, i) && getSellableItem(i).tradeable)
.map((i) => [c, getSellableItem(i)])
)
.reduce((arr, results) => [...arr, ...results], []);

if (availablePurchases.length > 0) {
const best = availablePurchases.reduce((best, current) =>
price(...best) < price(...current) ? current : best
);
if (!coin.tradeable || price(...best) > mallPrice(coin)) {
return best;
}
}
return;
}
5 changes: 5 additions & 0 deletions src/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { print } from "kolmafia";

export function warn(message: string): void {
print(message, "red");
}
155 changes: 37 additions & 118 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,120 +1,16 @@
import {
autosell,
autosellPrice,
cliExecute,
isDarkMode,
Item,
itemAmount,
mallPrice,
print,
putCloset,
putDisplay,
putShop,
toInt,
toItem,
use,
visitUrl,
wellStocked,
} from "kolmafia";
import { ALL_TAB_TITLES, InventoryType, isTabTitle, Tab, TabId, TabTitle } from "./types";
import { cliExecute, isAccessible, isDarkMode, print, sellPrice } from "kolmafia";
import { ALL_TAB_TITLES, isTabTitle, Tab, TabId, TabTitle } from "./types";
import { Options } from "./options";
import { actions, filters } from "./actions";
import { favoriteTabs, parseItems, parseNotes } from "./parse";
import { coinmasterBest } from "./coinmaster";
import { set } from "libram";

const HIGHLIGHT = isDarkMode() ? "yellow" : "blue";
const DEFAULT_ACTIONS = "closet use mall autosell display sell kmail fuel collection";

function items(tabId: TabId, type: InventoryType): Item[] {
const tab = visitUrl(`${type}.php?which=f${tabId}`);
const regexp = /ic(\d+)/g;
const items: Item[] = [];

let match;
while ((match = regexp.exec(tab)) !== null) {
const item = toItem(toInt(match[1]));
items.push(item);
}
return items;
}

function notesText(): string {
const questLogNotesHtml = visitUrl("questlog.php?which=4");
return questLogNotesHtml.substring(
questLogNotesHtml.indexOf(">", questLogNotesHtml.indexOf("<textarea")) + 1,
questLogNotesHtml.indexOf("</textarea")
);
}

function tabAliases(): Map<string, string> {
const questLogAliases: RegExpExecArray[] = notesText()
.split("\n")
.map((s) => /keeping-tabs: ?([A-Za-z0-9\- ]+)=(.*)/g.exec(s))
.filter((r) => r !== null) as RegExpExecArray[];

const values: [string, string][] = questLogAliases.map((r) => [r[1], r[2]]);
return new Map(values);
}

function tabCollections(): Map<string, Item[]> {
const questLogEntries: RegExpExecArray[] = notesText()
.split("\n")
.map((s) => /keeping-tabs-collection: ?'(.*)'=([0-9,]+)/g.exec(s))
.filter((r) => r !== null) as RegExpExecArray[];

const values: [string, Item[]][] = questLogEntries.map((r) => [
r[1],
r[2].split(",").map((i) => toItem(toInt(i))),
]);
return new Map(values);
}

function favoriteTabs(): Tab[] {
// visit the consumables tab to ensure that you get clickable links for
// all favorite tabs
const inventory = visitUrl(`inventory.php?which=1`);
const tabRegex =
/<a href="inventory.php\?which=f(\d+)">([A-Za-z0-9;&]+)(:[A-Za-z0-9;&\-#,<>=]+)?<\/a>/g;
const aliasRegex = /([A-Za-z0-9;&]+)(:[A-Za-z0-9;&\-#,<>=]+)?/g;

const tabs: Tab[] = [];
const aliases = tabAliases();

let match;
let aliasMatch;

while ((match = tabRegex.exec(inventory)) !== null) {
const title = match[2];
const options = match[3];
const alias = aliases.get(title);
const id = parseInt(match[1]);

if (isTabTitle(title)) {
tabs.push({
title,
id,
options: (options ?? ":").substring(1).split(","),
type: "inventory",
});
} else if (alias && (aliasMatch = aliasRegex.exec(alias))) {
const aliasTitle = aliasMatch[1];
const options = aliasMatch[2];
if (isTabTitle(aliasTitle)) {
tabs.push({
title: aliasTitle,
id: parseInt(match[1]),
options: (options ?? ":").substring(1).split(","),
type: "inventory",
alias: title,
});
}
}
}

return tabs;
}
const DEFAULT_ACTIONS = "closet use coinmaster mall autosell display sell kmail fuel collection";

function tabString(tab: Tab): string {
const options = Options.parse(tab.options, new Map());

const options = Options.parse(tab.options);
const title = tab.alias ? `${tab.title} (alias ${tab.alias})` : tab.title;
return options.empty() ? title : `${title} with ${options}`;
}
Expand All @@ -139,39 +35,62 @@ function help(mode: "execute" | "debug") {
}

function execute(splitArgs: string[]) {
const parsedNotes = parseNotes();

cliExecute("refresh inventory");
const tabs = favoriteTabs();
const tabs = favoriteTabs(parsedNotes.aliases);
const commands: TabTitle[] = splitArgs.filter(isTabTitle);
for (const command of commands) {
for (const tab of tabs) {
if (tab.title === command) {
const options = Options.parse(tab.options, tabCollections());
const options = Options.parse(tab.options, parsedNotes);
const tabForOptions = actions[tab.title](options);

print(`Running ${tabString(tab)}`, HIGHLIGHT);

items(tab.id, tab.type).filter(filters(options)).map(tabForOptions.action);
parseItems(tab.id, tab.type).filter(filters(options)).map(tabForOptions.action);
tabForOptions.finalize?.();
}
}
}
set("_keepingTabs", ["keeping-tabs", splitArgs].join(" "));
}

function debug(option: string) {
const parsedNotes = parseNotes();
if (option === "alias") {
const aliases = tabAliases();
print(`Parsed aliases:`, HIGHLIGHT);
[...aliases.entries()].forEach((v) => {
[...parsedNotes.aliases.entries()].forEach((v) => {
const [alias, title] = v;
print(`Alias ${alias} for action ${title}`, HIGHLIGHT);
});
} else if (option === "collections") {
const collections = tabCollections();
print(`Parsed collections:`, HIGHLIGHT);
[...collections.entries()].forEach((v) => {
[...parsedNotes.collections.entries()].forEach((v) => {
const [item, target] = v;
print(`Send ${item} to ${target}`);
});
} else if (option === "coinmasters") {
print(`Parsed coinmasters:`, HIGHLIGHT);
[...parsedNotes.coinmasters.entries()].forEach((v) => {
const [coin, [coinmaster, target]] = v;
print(
`Buy ${target} from ${coinmaster} using ${sellPrice(coinmaster, target)} ${coin} ${
isAccessible(coinmaster) ? "" : "(currently unaccessible)"
}`
);
const best = coinmasterBest(coin);
if (best) {
const [coinmaster, target] = best;
print(
`Best: Buy ${target} from ${coinmaster} using ${sellPrice(coinmaster, target)} ${coin} ${
isAccessible(coinmaster) ? "" : "(currently unaccessible)"
}`
);
}
});
} else {
print(`Invalid debug option '${option}'`);
}
}

Expand Down
Loading

0 comments on commit fb71d6a

Please sign in to comment.