Skip to content

Commit

Permalink
Coinmaster (#12)
Browse files Browse the repository at this point in the history
* Add Coinmasters

* further merge fixes

* lint fix attempt

* reduce queries

* add low back into default

---------

Co-authored-by: Tokoeka <[email protected]>
Co-authored-by: Tokoeka <[email protected]>
  • Loading branch information
3 people authored Jan 30, 2024
1 parent dc38d71 commit 98ae816
Show file tree
Hide file tree
Showing 9 changed files with 1,716 additions and 2,202 deletions.
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,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 @@ -73,6 +75,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 @@ -111,7 +115,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 @@ -124,11 +128,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 low`

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 @@ -4,6 +4,7 @@ import {
cliExecute,
closetAmount,
displayAmount,
isCoinmasterItem,
Item,
itemAmount,
mallPrice,
Expand All @@ -15,9 +16,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 @@ -132,6 +135,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 @@ -140,7 +150,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 @@ -155,6 +165,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 @@ -164,4 +175,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");
}
102 changes: 38 additions & 64 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
import {
autosell,
autosellPrice,
cliExecute,
isAccessible,
isDarkMode,
Item,
itemAmount,
mallPrice,
print,
putCloset,
putDisplay,
putShop,
sellPrice,
toInt,
toItem,
use,
visitUrl,
wellStocked,
} from "kolmafia";
import { ALL_TAB_TITLES, InventoryType, 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 low";
const DEFAULT_ACTIONS =
"closet use coinmaster mall autosell display sell kmail fuel collection low";

function items(tabId: TabId, type: InventoryType): Item[] {
const tab = visitUrl(`${type}.php?which=f${tabId}`);
Expand Down Expand Up @@ -67,54 +64,8 @@ function tabCollections(): Map<string, Item[]> {
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;
}

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 +90,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
23 changes: 19 additions & 4 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Item } from "kolmafia";
import { Coinmaster, Item } from "kolmafia";

type OptionsParams = {
collections: Map<string, Item[]>;
coinmasters: Map<Item, [Coinmaster, Item]>;
};

export class Options {
keep?: number;
Expand All @@ -10,11 +15,16 @@ export class Options {
priceUpperThreshold?: number;
priceLowerThreshold?: number;
default?: string;
collectionsMap: Map<string, Item[]> = new Map<string, Item[]>();
best?: boolean;
collections: Map<string, Item[]> = new Map();
coinmasters: Map<Item, [Coinmaster, Item]> = new Map();

static parse(optionsStr: string[], collectionsMap: Map<string, Item[]>): Options {
static parse(optionsStr: string[], params?: OptionsParams): Options {
const options: Options = new Options();
options.collectionsMap = collectionsMap;
if (params) {
options.collections = params.collections;
options.coinmasters = params.coinmasters;
}
for (const optionStr of optionsStr) {
const keep = optionStr.match(/keep(\d+)/);
if (keep && keep[1]) {
Expand Down Expand Up @@ -56,6 +66,11 @@ export class Options {
options.body = body[1];
continue;
}
const best = optionStr.match(/best/);
if (best) {
options.best = true;
continue;
}
if (optionStr.length > 0) {
throw `Unsupported Option: ${optionStr}`;
}
Expand Down
Loading

0 comments on commit 98ae816

Please sign in to comment.