Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coinmaster #12

Merged
merged 7 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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