Bug csend with commas acts unexpectedly when sending meat

Not sure whether "0 seal tooth" should return an item with a count of 0 or null.
Let me adjust my thinking.

ItemFinder.getFirstMatchingItem should return an AdventureResult with a count of 0, since it did succeed in parsing a single item.

ItemFinder.getMatchingItemList currently returns an array with a single element - the AdventureResult from getFirstMatchingItem - if it gets something other than a null. Otherwise, it assumes there were commas and does its own parsing.

I expect all the callers of the latter iterate over the elements of the array - or perhaps pass the array as-is to (a subclass of) TransferItemRequest.
In the latter case, I expect TransferItemRequest will filter them out.

I'm now thinking that getMatchingItemList should probably return a 0-length array if the only AdventureResult it was given had a count of 0.
 
Yeah.

Code:
> closet take 0 sand dollar

Removing items from closet...
Transfer failed for sand dollar (0)

We submitted:

inventory.php?action=closetpull&ajax=1&whichitem=3575&qty=0&pwd

KoL returned:

Code:
<script type="text/javascript">if (window.updateInv) updateInv([])</script><script type="text/javascript">if (!window.updateInv && parent.mainpane.updateInv) parent.mainpane.updateInv([])</script><center><table  width=95%  cellspacing=0 cellpadding=0><tr><td style="color: white;" align=center bgcolor=blue><b>Results:</b></td></tr><tr><td style="padding: 5px; border: 1px solid blue;"><center><table><tr><td>There was a problem, please try again.</Td></tr></table></center></td></tr><tr><td height=4></td></tr></table></center>

"There was a problem, please try again."

Code:
> closet put 100 meat

Placing meat into closet...
Requests complete.

> closet take 0 meat

> closet take 100 meat

Removing meat from closet...
Requests complete.
That did not make a request, at least.
 
Code:
  public static AdventureResult[] getMatchingItemList(
      String itemList, boolean errorOnFailure, List<AdventureResult> sourceList, Match filterType) {
    AdventureResult firstMatch =
        ItemFinder.getFirstMatchingItem(itemList, false, sourceList, filterType);
    if (firstMatch != null) {
      if (firstMatch.getCount() == 0) return new AdventureResult[0];
      AdventureResult[] items = new AdventureResult[1];
      items[0] = firstMatch;
      return items;
    }
...

Adding that one line fixes that.

If you have multiple items separated by commas, the loop later in that method splits on commas and calls ItemFinder.getFirstMatchingItem on each. If that returns non-null, it calls AdventureResult.addResultToList(items, firstMatch) to save it - and that method will not add an AdventureResult with count of zero.
 
The other question is whether errors should abort entirely. Currently a request of "1 seal tooth, 10 foobar, 9 1 meat," will put Mafia into an error state, but return a length 2 array with 1 seal tooth and 9 "1 meat" items. This is half of the "sends all your meat" problem: if it aborted there, at least the user would have some idea that commas were wrecking everything.

It could instead simply return a blank array (or callers should check whether Mafia is in error state before continuing, but I wonder how many places don't).

I've moved towards thinking your original statement was right: the parser should return a valid item with a count of 0, and the callers should check the count before blindly sending a request.
 
Slowly looks around, and tries to put the worms back in the can.

Code:
csend 1,000,000 meat to buffy

You would expect this to send 1 million meat to buffy, however the commas has this act in an unexpected manner.
Perhaps you should not have expected that since the command accepts a comma delimited list so the commas in the number should be operator error.

Given the way this is moving I note from https://wiki.kolmafia.us/index.php/CLI_Reference

Quantity Parameter​

Any place that a number can be used to define a quantity, such as autosell 5 heavy D, you can use one of the following to indicate a variable quantity:

  • * is used to indicate all items in inventory.
autosell * heavy D
  • 0 is also used to indicate all items in inventory.
autosell 0 heavy D
  • a negative number means sell off all items except for the number listed. For example, -5 means keep 5 and sell the rest.
autosell -5 heavy D

so maybe you can atone by editing the wiki once we think we are done.


:-)
 
One other surprising (to me) piece of behavior related to this bug: the failure to uniquely parse "1" or "000" as an item didn't generate an abort.
 
One other surprising (to me) piece of behavior related to this bug: the failure to uniquely parse "1" or "000" as an item didn't generate an abort.
1 alone is like any other ambiguous string. 1-ball and 11-leaf clover both match it. Current behavior presses onwards. 000 is another interesting case. As a number, it reduces it to 0 and then is a partial match for a lot of strings (but not an initial partial map). It's not impossible that at some point we'd get an item that started with 000, so if we special case for it, we'd need to be able to unwind that if it stopped being the right decision.

Should we be aborting on a list item not parsing to a single item? That would be new behavior. Is not having some of something bad? Is not parsing something worth aborting other pulls? I dunno, but I am feeling conservative about changing it.
 
I'm personally less concerned about changing behavior of CLI commands because these are largely intended to be used in an interactive context. Yes, you can write scripts with them, but said scripts are more akin to macros because they have limited logic.

Suppose that you write something like:
Code:
stash take 10 leaf clover, schlitz
drink green beer
with autoSatisfyWithStash = false, autoSatisfyWithMall = true.

Currently, this will buy the green beer with the mall instead of yelling at the user "hey 10 leaf clover isn't an item, it's `ten-leaf clover`". (note that you can type "11 leaf clover" which will match 11-leaf clover)

When experimenting to find some other cases, I stumbled across other bits of surprising behavior in ItemFinder:
Code:
334 scroll -> 1x 334 scroll
scroll -> 1x scroll of drastic healing (note there are multiple items starting with "scroll of"; this was the first added to the game)
334 croll -> 334x scroll of drastic healing
I would be moderately upset if I tried to `acquire 668 croll` and instead purchased 668 scrolls of drastic healing.
 
Yeah. We have a test suite for ItemFinder which is pretty exhaustive for counts, items with commas, items by item number via square brackets or pilcrows or something, but we also have a "fuzzy matching" algorithm for items.

Executive summary, before a wall of text:

I would like to have a test suite specifically dealing with fuzzy matching for user input of item names.
Why does "croll" mean "scroll of drastic healing"? It is neither the first such match by itemId or alphabetically.
How, exactly, do we match "mmj" to "magical mystery juice"?

I think, somehow, that the bulk of all that is via ItemDatabase.getMatchingNames(String substring) calling StringUtilities.getMatchingNames(ItemDatabase.canonicalNames, substring).

ItemDatabase.canonicalNames is an (alphabetically) sorted array of "canonical" (i.e. lower-cased) item names.

Given that, ItemDatabase.getCanonicalName and ItemFinder.getFirstMatchingItemName also have "heuristics", some of which seem to have become dead code, many years after it was first written.

1) Let's demystify fuzzy name matching via a test suite.
2) ItemDatabase.getMatchingNames returns a list of names to ItemFinder.getMatchingItemList, which will give an error if there are multiple matches after filtering. Usually. Why not "croll"?
3) All the CLI commands which want to match items - "pull", "closet", etc. - call that. Which is to say, ambiguous input may or may not match what the user wants or expects.
4) Why do "ambiguity" errors not abort the CLI command?

---------------------------------------------------------------

I generally think of "fuzzy matching" as substring matching - and yes, that is part of it - but if a substring can't narrow an item down to a single match, heuristics!

Some of it is in ItemDatabase.getCanonicalName(String itemName, int count, boolean subStringMatch).
The non-substring-match part has some oddities dealing with plurals, but is relatively straightforward.
But the substring matching when we look for all sorts of weird pluralization is chock full of heuristics.

I think the idea was to let users type in plurals and we reduce them to a particular item id. I think that was before we made an effort to have accurate plurals for all items. Which is to say, parsing user input, rather than item names from KoL or ASH. (CLI scripts use the same commands as users might type - but I would hope that actual scripts will not depend on fuzzy matching. :)

But, look:

Code:
> pull 2 scrolls of drastic healing

[scrolls of drastic healing] has no matches.

> pull 2 scroll of drastic healing

Pulling items from storage...
Requests complete

That is the legitimate plural of that item, but, even though ItemFinder knows we are asking for 2, it's not looking up the name in a way that will check for plurals.

Looking for callers of ItemDatabase.getCanonicalName where "count" might be something other than 1:

Code:
./persistence/ItemDatabase.java:1147:    String name = ItemDatabase.getCanonicalName(itemName, count, substringMatch);
./persistence/ItemDatabase.java:1285:      return ItemDatabase.getCanonicalName(canonicalName.split(" ")[1] + " snowcone", count);

The second is a recursive call in the method itself.
The first is in ItemDatabase.getItemIds(String itemName, int count, boolean subStringMatch).

Looking at the callers of THAT method:

Code:
./textui/Parser.java:4114:            int[] ids = ItemDatabase.getItemIds(name, 1, false);
./textui/DataTypes.java:726:    int[] itemIds = ItemDatabase.getItemIds(name, 1, false);
./textui/parsetree/Type.java:308:                  ? ItemDatabase.getItemIds(name, 1, false)
./textui/command/TestCommand.java:383:      int[] itemIds = ItemDatabase.getItemIds(string, 1, true);
./AdventureResult.java:450:            && ItemDatabase.getItemIds(this.name, 1, false).length > 1)
./request/MallSearchRequest.java:256:      int[] itemIds = ItemDatabase.getItemIds(itemName, 1, true);

Which is to say, all the code in ItemDatabase.getCanonicalName() that deals with "count > 1" is dead code at this point.

ItemFInder also has "fuzzy matching" heuristics in ItemFinder.getFirstMatchingItemName like this:

Code:
    // If there are multiple matches, such that one is a substring of the
    // others, choose the shorter one, on the grounds that the user would have
    // included part of the unique section of the longer name if that was the
    // item they actually intended.  This makes it easier to refer to
    // non-clockwork in-a-boxes, and DoD potions by flavor.

and

Code:
    // Candy hearts, snowcones and cupcakes take precedence over
    // all the other items in the game, IF exactly one such item
    // matches.
This is old code. The first bookshelf IOTM gave you snowcones. I have thousands of snowcones in storage.

We have no tests which specifically test ItemFinder.getFirstMatchingItemName.

Having looked at all that, why do I get this:

Code:
> inv drastic

scroll of drastic healing (10)

> pull 1 croll

Pulling items from storage...
Requests complete.

> inv drastic

scroll of drastic healing (11)

As you pointed out, "croll" is ambiguous.

ItemFinder.getMatchingNames gets a list of matches. It calls ItemDatabase.getMatchingNames, which calls StringUtilities.getMatchingNames with a (sorted) list of item names. There are 23 items that match "croll" - and drastic healing is not alphabetically the first.

Hah. I installed some debug logging and have identified the "croll" issue:

Code:
Looking for first matching item name in a list of 23 names.
Filtering by ANY
Reduced namelist to 1 names.
Returning scroll of drastic healing

Why?

ItemFinder.filterNameList:

Code:
    if (filterType != Match.FOOD && filterType != Match.BOOZE && filterType != Match.SPLEEN && filterType != Match.CANDY) {
      // First, check to see if there are an HP/MP restores
      // in the list of matches.  If there are, only return
      // the restorative items (the others are irrelevant).
      ...

Yet Another Heuristic. For a "pull" (or even "use"), why in the world are only "restores" relevant?

Well, enough musing,, for now.
 
Digging deeper, I have found the following places where we call ItemDatabase.getItemId() with a count which might not be 1.
Those call ItemDatabase.getItemIds() with a count, which calls ItemDatabase.getCanonicalName() with a count.
I.e., which will (for unrecognizable names) go through the weird plural detection code.

Code:
./textui/RuntimeLibrary.java:3284:        ItemDatabase.getItemId(name.toString(), (int) count.intValue()), true);
./AdventureResult.java:262:    this.id = ItemDatabase.getItemId(this.name, this.getCount());
./persistence/QuestDatabase.java:733:        itemToGet = ItemDatabase.getItemId(matcher.group(2), numberToGet, true);
./persistence/QuestDatabase.java:747:        itemToGet = ItemDatabase.getItemId(matcher.group(2), numberToGet, true);
./persistence/QuestDatabase.java:782:        itemToGet = ItemDatabase.getItemId(matcher.group(2), numberToGet, true);
./persistence/QuestDatabase.java:796:        itemToGet = ItemDatabase.getItemId(matcher.group(2), numberToGet, true);
./request/ClanLogRequest.java:232:        lastItemId = ItemDatabase.getItemId(entryMatcher.group(4), entryCount);
./request/TransferItemRequest.java:518:      int itemId = ItemDatabase.getItemId(name, count, true);
./request/TransferItemRequest.java:534:      int itemId = ItemDatabase.getItemId(name, count, true);
./webui/UseLinkDecorator.java:243:          itemId = ItemDatabase.getItemId(itemName, itemCount, itemCount > 1);
./session/ResultProcessor.java:657:    int itemId = ItemDatabase.getItemId(itemName, itemCount, itemCount > 1);
One of those is a user-supplied String. The others are extracted from KoL responsetexts.

Testing the ASH (user-supplied) call:

Code:
> ashq print(to_item("scroll of drastic healing", 1))
scroll of drastic healing

> ashq print(to_item("scroll of drastic healing", 2))
scroll of drastic healing

> ashq print(to_item("scrolls of drastic healing", 1))
none

> ashq print(to_item("scrolls of drastic healing", 2))
scroll of drastic healing

> ashq print(to_item("drastic", 2))
scroll of drastic healing

> ashq print(to_item("scroll of drastic", 2))
scroll of drastic healing

> ashq print(to_item("scrolls of drastic", 2))
none
 
Last edited:
Not that I want to do this myself, but my life would be easier if “acquire 8 lines” worked in addition to “acquire 8 line”. My fingers like to have number-noun agreement.
 
Back
Top