ASH sweet_synthesis issue

Veracity

Developer
Staff member
There is an interesting setup in the Mall right now regarding candies that are useful for "Synthesis: Greed" - Meat Drop +300%.

fruitfilm:

460 limit 5 @ 1,000 - Hoop Dreams
1189 @ 19,990 - Malibu Stacey's Dream House

Gummi-DNA:

356 limit 5 @ 1,490 - Hoop Dreams
615 @ 9,190 - Malibu Stacey's Dream House

Note synthesizing with those two candies costs 2,490 for the first five casts - and then 29,180 for any more than five.

That first price happens to be the most cost effective pairing. The second one, not so much.

For some reason, even when I synthesize one cast at a time, the synthesize() function wants to keep using that candy pairing - and after five casts it gets Very Expensive. You are welcome, Malibu Stacey; your shop cleaned up from me today. :)

This is with a freshly logged in character who has visited the "cheap" store five times in a previous login today.

Code:
> ash sweet_synthesis_pair( $effect[ Synthesis: Greed ] )

Returned: aggregate item [2]
0 => fruitfilm
1 => Gummi-DNA

> ash mall_price( $item[ fruitfilm] )

Searching for "fruitfilm"...
Search complete.
Returned: 19990

> ash sweet_synthesis_pair( $effect[ Synthesis: Greed ] )

Returned: aggregate item [2]
0 => Crimbo fudge
1 => Gummi-DNA

> ash mall_price( $item[ Gummi-DNA ] )

Searching for "Gummi-DNA"...
Search complete.
Returned: 9190

> ash sweet_synthesis_pair( $effect[ Synthesis: Greed ] )

Returned: aggregate item [2]
0 => Crimbo fudge
1 => strawberry-flavored Hob-O

1) Notice that it used cached mall prices without checking.
2) Notice that had it checked, it would have found a different pairing.

CandyDatabase's constructor for a Candy object uses MallPriceDatabase.getPrice(itemId) to get the lowest price.

Code:
    update_candy_prices();
...
        // Do it one cast at a time, since candy costs can vary widely
        while ( synthesis_casts-- > 0 ) {
        sweet_synthesis( 1, synthesis_target, flags );
        spleen -= 1;
        }

Here is a character casting Sweet Synthesis the first time:

Code:
mall.php?category=allitems&consumable_byme=0&weaponattribute=3&wearable_byme=0&nolimits=0&max_price=0&sortresultsby=price&justitems=0&x_cheapest=0&pudnuggler=%22fruitfilm%22

buy 1 fruitfilm for 1000 each from shop #3483633 on 20211214

mall.php?category=allitems&consumable_byme=0&weaponattribute=3&wearable_byme=0&nolimits=0&max_price=0&sortresultsby=price&justitems=0&x_cheapest=0&pudnuggler=%22Gummi-DNA%22

buy 1 Gummi-DNA for 1490 each from shop #3483633 on 20211214

cast 1 Sweet Synthesis

synthesize 1 fruitfilm, Gummi-DNA
You acquire an effect: Synthesis: Greed (30)
Notice that it checked the mall price...

Here are 4 more times:

Code:
buy 1 fruitfilm for 1000 each from shop #3483633 on 20211214

buy 1 Gummi-DNA for 1490 each from shop #3483633 on 20211214

cast 1 Sweet Synthesis

synthesize 1 fruitfilm, Gummi-DNA
You acquire an effect: Synthesis: Greed (30)

buy 1 fruitfilm for 1000 each from shop #3483633 on 20211214

buy 1 Gummi-DNA for 1490 each from shop #3483633 on 20211214

cast 1 Sweet Synthesis

synthesize 1 fruitfilm, Gummi-DNA
You acquire an effect: Synthesis: Greed (30)

buy 1 fruitfilm for 1000 each from shop #3483633 on 20211214

buy 1 Gummi-DNA for 1490 each from shop #3483633 on 20211214

cast 1 Sweet Synthesis

synthesize 1 fruitfilm, Gummi-DNA
You acquire an effect: Synthesis: Greed (30)

buy 1 fruitfilm for 1000 each from shop #3483633 on 20211214

buy 1 Gummi-DNA for 1490 each from shop #3483633 on 20211214

cast 1 Sweet Synthesis

synthesize 1 fruitfilm, Gummi-DNA
You acquire an effect: Synthesis: Greed (30)
Notice that it knew which shops to buy from.

Here are 2 more casts:

Code:
buy 1 fruitfilm for 1000 each from shop #3483633 on 20211214

buy 1 fruitfilm for 19990 each from shop #2705901 on 20211214

buy 1 Gummi-DNA for 1490 each from shop #3483633 on 20211214

buy 1 Gummi-DNA for 9190 each from shop #2705901 on 20211214

cast 1 Sweet Synthesis

synthesize 1 fruitfilm, Gummi-DNA
You acquire an effect: Synthesis: Greed (30)

buy 1 fruitfilm for 1000 each from shop #3483633 on 20211214

buy 1 fruitfilm for 19990 each from shop #2705901 on 20211214

buy 1 Gummi-DNA for 1490 each from shop #3483633 on 20211214

buy 1 Gummi-DNA for 9190 each from shop #2705901 on 20211214

cast 1 Sweet Synthesis

synthesize 1 fruitfilm, Gummi-DNA
You acquire an effect: Synthesis: Greed (30)
Notice that it continued to try to buy from the cheap stores and when they failed, moved to the expensive stores.

1) When you've bought out a store, that store's price is not necessarily the cheapest any more.
2) In fact, that candy may no longer be part of the cheapest pair of candies for the desired effect.

If I don't want to keep giving Malibu Stacey millions of Meat, I'm going to have to figure out where and how to deal with this.
Perhaps MallPriceDatabase should update its cache when you buy your limit at a store? Somehow.
Pondering.
 

Veracity

Developer
Staff member
Code:
> ash update_candy_prices()

Searching for "chocolate-covered caviar"...
Search complete.
...

Searching for "stick of "gum""...
Search complete.
Updating mallprices.txt with 45 prices.
Returned: void

> ash sweet_synthesis_pair( $effect[ Synthesis: Greed ] )

Returned: aggregate item [2]
0 => Crimbo fudge
1 => strawberry-flavored Hob-O
Logging in fresh and updating candy prices recognizes the prices of sold out candies.
 

heeheehee

Developer
Staff member
Perhaps MallPriceDatabase should update its cache when you buy your limit at a store? Somehow.
Maybe a simpler variant:

StoreManager has a method called updateMallPrice, based on the results of a purchase. This is currently integrated into BuyCommand, but not InventoryManager.retrieveItem(), which is used more broadly (including in SweetSynthesisRequest).

Suppose instead we called that function in, say, StoreManager.searchMall which is used by retrieveItem?
 

Veracity

Developer
Staff member
Yeah. After I said I was pondering, I spent the day away from my computer. I’ll work on this tomorrow morning before running turns.

Today I reviewed and approved my first pull request. Perhaps tomorrow I will submit my first. :)
 

Veracity

Developer
Staff member
StoreManager has a method called updateMallPrice, based on the results of a purchase. This is currently integrated into BuyCommand, but not InventoryManager.retrieveItem(), which is used more broadly (including in SweetSynthesisRequest).

Suppose instead we called that function in, say, StoreManager.searchMall which is used by retrieveItem?
Here is BuyCommand.buy():

Code:
      ArrayList<PurchaseRequest> results =
          // Cheapest from Mall or NPC stores
          (interact && !mall)
              ? StoreManager.searchMall(match)
              :
              // Mall stores only
              (storage || mall)
                  ? StoreManager.searchOnlyMall(match)
                  :
                  // NPC stores only
                  StoreManager.searchNPCs(match);

      KoLmafia.makePurchases(
          results, results.toArray(new PurchaseRequest[0]), match.getCount(), false, priceLimit);

      if (interact && !storage) {
        StoreManager.updateMallPrice(match, results);
      }
Here is InventoryManager.doRetrieveItem():

Code:
    if (shouldUseMall) {
      if (sim) {
        return "buy";
      }

      ArrayList<PurchaseRequest> results = StoreManager.searchMall(item);
      KoLmafia.makePurchases(
          results,
          results.toArray(new PurchaseRequest[0]),
          InventoryManager.getPurchaseCount(itemId, missingCount),
          isAutomated,
          0);
      StoreManager.updateMallPrice(item, results);
      missingCount = item.getCount() - item.getCount(KoLConstants.inventory);

      if (missingCount <= 0) {
        return "";
      }
    }
There are some different conditionals, but essentially, both do:

StoreManager.searchMall -> KoLmafia.makePurchases -> StoreManager.updateMallPrice
 
Last edited:

heeheehee

Developer
Staff member
Hm. So, Sweet Synthesis by nature only buys items one at a time. I think it's notable that there are two MallPurchaseRequests, one of which fails (as expected, when buying from a store where you've hit the limit). And, that your prices don't seem to be updated after the second one (and that failure doesn't seem to be noted for later invocations).

Any chance this is tied to https://kolmafia.us/threads/buy-fun...ing-from-store-that-raised-limit-today.26957/ ?

(I also noted that CandyDatabase apparently has its own notion of price that it puts in Candy objects, but that's not directly cached anywhere, so I think that's a red herring for now.)
 

Veracity

Developer
Staff member
It calls retrieveItem which calls makePurchases which iterates through available requests trying to fulfill the purchase. When the first one fails (because you hit the daily limit), to tries the second, which succeeds.

Yes, Candy gets the mall price - but every time you call sweet_synthesis_pair, it regenerates the array of candy items, which means it refetches the mall price. The mall price does not change when you purchase from a store with limits - even when you buy out your limit.

I made the following experimental change to KoLmafia.makePurchases:

Code:
      int previousLimit = currentRequest.getLimit();
      int toPurchase =
          Math.min(
              (int) Math.min(Integer.MAX_VALUE, currentRequest.getAvailableMeat() / currentPrice),
              Math.min(previousLimit, desiredCount - currentCount));
      currentRequest.setLimit(toPurchase);

      RequestThread.postRequest(currentRequest);

      // Update how many of the item we have post-purchase
      int purchased = item.getCount(destination) - currentCount;
      remaining -= purchased;

      // We've purchased as many as we will from this store

      // Restore original limit
      currentRequest.setLimit(previousLimit);

      // If purchase succeeded.
      if (KoLmafia.permitsContinue()) {
        // If original limit was less than original quantity, we have purchased some of our daily limit
        if (previousLimit < currentRequest.getQuantity()) {
          currentRequest.setLimit(previousLimit - purchased);

          // If we have purchased the store's daily limit, done with store today.
          if (previousLimit == purchased) {
            currentRequest.setCanPurchase(false);
          }
        }

        // If this is not an NPC store, remove purchased items
        if (currentRequest.getQuantity() != PurchaseRequest.MAX_QUANTITY) {
          currentRequest.setQuantity(currentRequest.getQuantity() - purchased);
        }

        // If store is now empty. remove from result list
        if (currentRequest.getQuantity() == 0) {
          results.remove(currentRequest);
        }
      }
I mean, I changed the section of code to look like that. When you purchase an item from a store with a daily limit, it updates the limit in the PurchaseRequest to account for you having used some of it - and if you consume your whole limit, it marks the PurchaseRequest as unusable.

With this:

Code:
> ash sweet_synthesis_pair( $effect[ Synthesis: Greed ] )

Returned: aggregate item [2]
0 => fruitfilm
1 => Gummi-DNA

> ash sweet_synthesis( $effect[ Synthesis: Greed ] )

Searching for "fruitfilm"...
Search complete.
Purchasing fruitfilm (1 @ 950)...
Purchases complete.
Searching for "Gummi-DNA"...
Search complete.
Purchasing Gummi-DNA (1 @ 1,450)...
Purchases complete.
You acquire an effect: Synthesis: Greed (30)
Returned: true

> ash sweet_synthesis_pair( $effect[ Synthesis: Greed ] )

Returned: aggregate item [2]
0 => box of Dweebs
1 => Atomic Pop
Which is unexpected, but correct, given that we recalculate mall prices; fruitfilm and Gummi-DNA are now limited to 4 by the cheapest store, so the 5th cheapest store is now the expensive store.

It might be nice to keep buying using the REAL cheapest mall prices until the cheapest store has sold its limit.

But this is on the right track, I think. I'm going to keep experimenting.
 

Veracity

Developer
Staff member
I am happy with this as an immediate fix for what is actually a fairly serious issue; as currently exists, users of VMF using Sweet Synthesis will overspend by a lot. With this patch, they'll use the absolute cheapest candy pair once - and then will spend a little more on subsequent calls (since caching is stricter), rather than about 28,000 extra Meat on all casts past the first 5.

I submitted my first Pull Request. :)

 
Top